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
3070 .clone()
3071 .map(|reader| {
3072 Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
3073 as Box<dyn GuestModuleReader>
3074 });
3075 let module_reader = built_reader
3076 .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
3077 let execution = self
3078 .javascript_engine
3079 .start_execution_with_module_reader(
3080 StartJavascriptExecutionRequest {
3081 guest_runtime: guest_runtime_identity(vm, None, None),
3082 vm_id: vm_id.clone(),
3083 context_id: context.context_id,
3084 argv: std::iter::once(resolved.entrypoint.clone())
3085 .chain(resolved.execution_args.iter().cloned())
3086 .collect(),
3087 env: env.clone(),
3088 cwd: resolved.host_cwd.clone(),
3089 limits: javascript_execution_limits(vm),
3090 inline_code,
3091 },
3092 module_reader,
3093 guest_reader,
3094 )
3095 .map_err(javascript_error)?;
3096 (ActiveExecution::Javascript(execution), env.clone())
3097 }
3098 GuestRuntimeKind::Python => {
3099 let python_file_path = python_file_entrypoint(&resolved.entrypoint);
3100 let pyodide_dist_path = self
3101 .python_engine
3102 .bundled_pyodide_dist_path_for_vm(&vm_id)
3103 .map_err(python_error)?;
3104 let pyodide_cache_path = pyodide_dist_path
3105 .parent()
3106 .and_then(Path::parent)
3107 .unwrap_or(pyodide_dist_path.as_path())
3108 .join("pyodide-package-cache");
3109 add_runtime_guest_path_mapping(
3110 &mut env,
3111 PYTHON_PYODIDE_GUEST_ROOT,
3112 &pyodide_dist_path,
3113 );
3114 add_runtime_guest_path_mapping(
3115 &mut env,
3116 PYTHON_PYODIDE_CACHE_GUEST_ROOT,
3117 &pyodide_cache_path,
3118 );
3119 add_runtime_host_access_path(
3120 &mut env,
3121 "AGENTOS_EXTRA_FS_READ_PATHS",
3122 &pyodide_dist_path,
3123 true,
3124 );
3125 add_runtime_host_access_path(
3126 &mut env,
3127 "AGENTOS_EXTRA_FS_READ_PATHS",
3128 &pyodide_cache_path,
3129 true,
3130 );
3131 add_runtime_host_access_path(
3132 &mut env,
3133 "AGENTOS_EXTRA_FS_WRITE_PATHS",
3134 &pyodide_cache_path,
3135 false,
3136 );
3137 let context = self
3138 .python_engine
3139 .create_context(CreatePythonContextRequest {
3140 vm_id: vm_id.clone(),
3141 pyodide_dist_path,
3142 });
3143 let execution = self
3144 .python_engine
3145 .start_execution(StartPythonExecutionRequest {
3146 vm_id: vm_id.clone(),
3147 context_id: context.context_id,
3148 code: resolved.entrypoint.clone(),
3149 file_path: python_file_path,
3150 env: env.clone(),
3151 cwd: resolved.host_cwd.clone(),
3152 limits: python_execution_limits(vm),
3153 guest_runtime: guest_runtime_identity(vm, None, None),
3154 })
3155 .map_err(python_error)?;
3156 (ActiveExecution::Python(execution), env.clone())
3157 }
3158 GuestRuntimeKind::WebAssembly => {
3159 let wasm_limits = wasm_execution_limits(vm);
3160 let wasm_guest_runtime =
3161 guest_runtime_identity(vm, Some(u64::from(kernel_pid)), Some(0));
3162 let wasm_permission_tier = resolved.wasm_permission_tier.unwrap_or_else(|| {
3163 resolve_wasm_permission_tier(
3164 vm,
3165 Some(&resolved.command),
3166 None,
3167 &resolved.entrypoint,
3168 )
3169 });
3170 let context = self.wasm_engine.create_context(CreateWasmContextRequest {
3171 vm_id: vm_id.clone(),
3172 module_path: Some(resolved.entrypoint.clone()),
3173 });
3174 let execution = self
3175 .wasm_engine
3176 .start_execution(StartWasmExecutionRequest {
3177 vm_id: vm_id.clone(),
3178 context_id: context.context_id,
3179 argv: resolved.process_args.clone(),
3180 env: env.clone(),
3181 cwd: resolved.host_cwd.clone(),
3182 permission_tier: execution_wasm_permission_tier(wasm_permission_tier),
3183 limits: wasm_limits,
3184 guest_runtime: wasm_guest_runtime,
3185 })
3186 .map_err(wasm_error)?;
3187 (ActiveExecution::Wasm(Box::new(execution)), env)
3188 }
3189 };
3190 let child_pid = execution.child_pid();
3191 let kernel_stdin_writer_fd = install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?;
3192 vm.active_processes.insert(
3193 payload.process_id.clone(),
3194 ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
3195 .with_kernel_stdin_writer_fd(kernel_stdin_writer_fd)
3196 .with_guest_cwd(resolved.guest_cwd.clone())
3197 .with_env(process_env)
3198 .with_host_cwd(resolved.host_cwd.clone()),
3199 );
3200 self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
3201
3202 Ok(DispatchResult {
3203 response: self.respond(
3204 request,
3205 ResponsePayload::ProcessStarted(ProcessStartedResponse {
3206 process_id: payload.process_id,
3207 pid: Some(if child_pid == 0 {
3208 kernel_pid
3209 } else {
3210 child_pid
3211 }),
3212 }),
3213 ),
3214 events: Vec::new(),
3215 })
3216 }
3217
3218 pub(crate) async fn write_stdin(
3219 &mut self,
3220 request: &RequestFrame,
3221 payload: WriteStdinRequest,
3222 ) -> Result<DispatchResult, SidecarError> {
3223 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3224 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3225
3226 let vm = self
3227 .vms
3228 .get_mut(&vm_id)
3229 .ok_or_else(|| missing_vm_error(&vm_id))?;
3230 let process = vm
3231 .active_processes
3232 .get_mut(&payload.process_id)
3233 .ok_or_else(|| {
3234 SidecarError::InvalidState(format!(
3235 "VM {vm_id} has no active process {}",
3236 payload.process_id
3237 ))
3238 })?;
3239 process.execution.write_stdin(&payload.chunk)?;
3240 write_kernel_process_stdin(&mut vm.kernel, process, &payload.chunk)?;
3241
3242 Ok(DispatchResult {
3243 response: self.respond(
3244 request,
3245 ResponsePayload::StdinWritten(StdinWrittenResponse {
3246 process_id: payload.process_id,
3247 accepted_bytes: payload.chunk.len() as u64,
3248 }),
3249 ),
3250 events: Vec::new(),
3251 })
3252 }
3253
3254 pub(crate) async fn close_stdin(
3255 &mut self,
3256 request: &RequestFrame,
3257 payload: CloseStdinRequest,
3258 ) -> Result<DispatchResult, SidecarError> {
3259 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3260 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3261
3262 let vm = self
3263 .vms
3264 .get_mut(&vm_id)
3265 .ok_or_else(|| missing_vm_error(&vm_id))?;
3266 let process = vm
3267 .active_processes
3268 .get_mut(&payload.process_id)
3269 .ok_or_else(|| {
3270 SidecarError::InvalidState(format!(
3271 "VM {vm_id} has no active process {}",
3272 payload.process_id
3273 ))
3274 })?;
3275 process.execution.close_stdin()?;
3276 close_kernel_process_stdin(&mut vm.kernel, process)?;
3277
3278 Ok(DispatchResult {
3279 response: self.respond(
3280 request,
3281 ResponsePayload::StdinClosed(StdinClosedResponse {
3282 process_id: payload.process_id,
3283 }),
3284 ),
3285 events: Vec::new(),
3286 })
3287 }
3288
3289 pub(crate) async fn kill_process(
3290 &mut self,
3291 request: &RequestFrame,
3292 payload: KillProcessRequest,
3293 ) -> Result<DispatchResult, SidecarError> {
3294 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3295 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3296 self.kill_process_internal(&vm_id, &payload.process_id, &payload.signal)?;
3297
3298 Ok(DispatchResult {
3299 response: self.respond(
3300 request,
3301 ResponsePayload::ProcessKilled(ProcessKilledResponse {
3302 process_id: payload.process_id,
3303 }),
3304 ),
3305 events: Vec::new(),
3306 })
3307 }
3308
3309 pub(crate) async fn find_listener(
3310 &mut self,
3311 request: &RequestFrame,
3312 payload: FindListenerRequest,
3313 ) -> Result<DispatchResult, SidecarError> {
3314 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3315 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3316 require_vm_inspection_permission(
3317 &self.bridge,
3318 &vm_id,
3319 "network.inspect",
3320 "network",
3321 &socket_query_resource(SocketQueryKind::TcpListener, &payload),
3322 )?;
3323
3324 let listener =
3325 find_socket_state_entry(self.vms.get(&vm_id), SocketQueryKind::TcpListener, &payload)?;
3326
3327 Ok(DispatchResult {
3328 response: self.respond(
3329 request,
3330 ResponsePayload::ListenerSnapshot(ListenerSnapshotResponse { listener }),
3331 ),
3332 events: Vec::new(),
3333 })
3334 }
3335
3336 pub(crate) async fn get_process_snapshot(
3337 &mut self,
3338 request: &RequestFrame,
3339 _payload: GetProcessSnapshotRequest,
3340 ) -> Result<DispatchResult, SidecarError> {
3341 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3342 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3343 require_vm_inspection_permission(
3344 &self.bridge,
3345 &vm_id,
3346 "process.inspect",
3347 "process",
3348 "process://snapshot",
3349 )?;
3350
3351 let processes = self
3352 .vms
3353 .get_mut(&vm_id)
3354 .map(|vm| {
3355 prune_exited_process_snapshots(vm);
3356 snapshot_vm_processes(vm)
3357 })
3358 .unwrap_or_default();
3359
3360 Ok(DispatchResult {
3361 response: self.respond(
3362 request,
3363 ResponsePayload::ProcessSnapshot(ProcessSnapshotResponse { processes }),
3364 ),
3365 events: Vec::new(),
3366 })
3367 }
3368
3369 pub(crate) async fn find_bound_udp(
3370 &mut self,
3371 request: &RequestFrame,
3372 payload: FindBoundUdpRequest,
3373 ) -> Result<DispatchResult, SidecarError> {
3374 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3375 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3376
3377 let lookup_request = FindListenerRequest {
3378 host: payload.host,
3379 port: payload.port,
3380 path: None,
3381 };
3382 require_vm_inspection_permission(
3383 &self.bridge,
3384 &vm_id,
3385 "network.inspect",
3386 "network",
3387 &socket_query_resource(SocketQueryKind::UdpBound, &lookup_request),
3388 )?;
3389 let socket = find_socket_state_entry(
3390 self.vms.get(&vm_id),
3391 SocketQueryKind::UdpBound,
3392 &lookup_request,
3393 )?;
3394
3395 Ok(DispatchResult {
3396 response: self.respond(
3397 request,
3398 ResponsePayload::BoundUdpSnapshot(BoundUdpSnapshotResponse { socket }),
3399 ),
3400 events: Vec::new(),
3401 })
3402 }
3403
3404 pub(crate) async fn vm_fetch(
3405 &mut self,
3406 request: &RequestFrame,
3407 payload: VmFetchRequest,
3408 ) -> Result<DispatchResult, SidecarError> {
3409 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3410 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3411
3412 let vm = self
3413 .vms
3414 .get_mut(&vm_id)
3415 .ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
3416 let target_path = if payload.path.starts_with('/') {
3417 payload.path.clone()
3418 } else {
3419 format!("/{}", payload.path)
3420 };
3421 let request_url = Url::parse(&format!("http://127.0.0.1:{}{target_path}", payload.port))
3422 .map_err(|error| {
3423 SidecarError::InvalidState(format!(
3424 "invalid vm.fetch target {target_path:?}: {error}"
3425 ))
3426 })?;
3427 let header_values: BTreeMap<String, Value> = serde_json::from_str(&payload.headers_json)
3428 .map_err(|error| {
3429 SidecarError::InvalidState(format!(
3430 "vm.fetch headers_json must be valid JSON: {error}"
3431 ))
3432 })?;
3433 let options = JavascriptHttpRequestOptions {
3434 method: Some(payload.method),
3435 headers: header_values,
3436 body: payload.body,
3437 reject_unauthorized: None,
3438 };
3439 let headers = parse_http_header_collection(&options.headers, "vm.fetch headers")?;
3440 let target_process_id = find_kernel_http_listener_process(vm, payload.port);
3441 if let Some(target_process_id) = target_process_id {
3442 let max_fetch_response_bytes = vm.limits.http.max_fetch_response_bytes;
3443 let response_json = match dispatch_kernel_http_fetch(
3444 &self.bridge,
3445 &vm_id,
3446 vm,
3447 &target_process_id,
3448 payload.port,
3449 &target_path,
3450 &options,
3451 &headers,
3452 max_fetch_response_bytes,
3453 ) {
3454 Ok(response_json) => response_json,
3455 Err(error) => {
3456 if let Some(exit_code) = kernel_http_fetch_target_exit_code(&error) {
3457 let _ = vm;
3458 self.finish_active_process_exit(&vm_id, &target_process_id, exit_code)?;
3459 }
3460 return Err(error);
3461 }
3462 };
3463 let response = self.respond(
3464 request,
3465 ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
3466 );
3467 ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
3468
3469 return Ok(DispatchResult {
3470 response,
3471 events: Vec::new(),
3472 });
3473 }
3474
3475 let Some((target_process_id, server_id)) =
3476 vm.active_processes
3477 .iter()
3478 .find_map(|(process_id, process)| {
3479 process
3480 .http_servers
3481 .iter()
3482 .find(|(_, server)| server.guest_local_addr.port() == payload.port)
3483 .map(|(server_id, _)| (process_id.clone(), *server_id))
3484 })
3485 else {
3486 return Err(SidecarError::Execution(format!(
3487 "vm.fetch could not find a guest HTTP listener on port {}",
3488 payload.port
3489 )));
3490 };
3491 let socket_paths = build_javascript_socket_path_context(vm)?;
3492 let resource_limits = vm.kernel.resource_limits().clone();
3493 let process = vm
3494 .active_processes
3495 .get_mut(&target_process_id)
3496 .ok_or_else(|| {
3497 SidecarError::InvalidState(format!(
3498 "vm.fetch target process disappeared: {target_process_id}"
3499 ))
3500 })?;
3501 let request_json = serialize_http_loopback_request(&request_url, &options, &headers)?;
3502 let response_json = dispatch_loopback_http_request(LoopbackHttpDispatchRequest {
3503 bridge: &self.bridge,
3504 vm_id: &vm_id,
3505 dns: &vm.dns,
3506 socket_paths: &socket_paths,
3507 kernel: &mut vm.kernel,
3508 process,
3509 resource_limits: &resource_limits,
3510 server_id,
3511 request_json: &request_json,
3512 })?;
3513
3514 let response = self.respond(
3515 request,
3516 ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
3517 );
3518 ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
3519
3520 Ok(DispatchResult {
3521 response,
3522 events: Vec::new(),
3523 })
3524 }
3525
3526 pub(crate) async fn get_signal_state(
3527 &mut self,
3528 request: &RequestFrame,
3529 payload: GetSignalStateRequest,
3530 ) -> Result<DispatchResult, SidecarError> {
3531 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3532 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3533
3534 let handlers = self
3535 .vms
3536 .get(&vm_id)
3537 .and_then(|vm| vm.signal_states.get(&payload.process_id))
3538 .cloned()
3539 .unwrap_or_default();
3540
3541 Ok(DispatchResult {
3542 response: self.respond(
3543 request,
3544 ResponsePayload::SignalState(SignalStateResponse {
3545 process_id: payload.process_id,
3546 handlers: handlers.into_iter().collect(),
3547 }),
3548 ),
3549 events: Vec::new(),
3550 })
3551 }
3552
3553 pub(crate) async fn get_zombie_timer_count(
3554 &mut self,
3555 request: &RequestFrame,
3556 _payload: GetZombieTimerCountRequest,
3557 ) -> Result<DispatchResult, SidecarError> {
3558 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3559 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3560
3561 let count = self
3562 .vms
3563 .get(&vm_id)
3564 .map(|vm| vm.kernel.zombie_timer_count() as u64)
3565 .unwrap_or_default();
3566
3567 Ok(DispatchResult {
3568 response: self.respond(
3569 request,
3570 ResponsePayload::ZombieTimerCount(ZombieTimerCountResponse { count }),
3571 ),
3572 events: Vec::new(),
3573 })
3574 }
3575
3576 pub(crate) fn kill_process_internal(
3577 &mut self,
3578 vm_id: &str,
3579 process_id: &str,
3580 signal: &str,
3581 ) -> Result<(), SidecarError> {
3582 let signal_name = signal.to_owned();
3583 let signal = parse_signal(signal)?;
3584 let vm = self
3585 .vms
3586 .get_mut(vm_id)
3587 .ok_or_else(|| SidecarError::InvalidState(format!("unknown sidecar VM {vm_id}")))?;
3588 let process = vm.active_processes.get_mut(process_id).ok_or_else(|| {
3589 SidecarError::InvalidState(format!("VM {vm_id} has no active process {process_id}"))
3590 })?;
3591 let kernel_pid = process.kernel_pid;
3592
3593 enum KillBehavior {
3594 Tool,
3595 SharedV8StateOnly,
3596 SharedV8Continue,
3597 SharedV8Terminate,
3598 SharedV8DispatchOrTerminate,
3599 Noop,
3600 HostPid(u32),
3601 }
3602
3603 let behavior = match &process.execution {
3604 ActiveExecution::Tool(_) => KillBehavior::Tool,
3605 ActiveExecution::Javascript(execution)
3606 if execution.uses_shared_v8_runtime() && matches!(signal, 0 | libc::SIGSTOP) =>
3607 {
3608 KillBehavior::SharedV8StateOnly
3609 }
3610 ActiveExecution::Javascript(execution)
3611 if execution.uses_shared_v8_runtime() && signal == libc::SIGCONT =>
3612 {
3613 KillBehavior::SharedV8Continue
3614 }
3615 ActiveExecution::Wasm(execution)
3616 if execution.uses_shared_v8_runtime()
3617 && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
3618 {
3619 KillBehavior::SharedV8StateOnly
3620 }
3621 ActiveExecution::Python(execution)
3622 if execution.uses_shared_v8_runtime()
3623 && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
3624 {
3625 KillBehavior::SharedV8StateOnly
3626 }
3627 ActiveExecution::Javascript(execution)
3628 if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
3629 {
3630 KillBehavior::SharedV8Terminate
3631 }
3632 ActiveExecution::Wasm(execution)
3633 if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
3634 {
3635 KillBehavior::SharedV8Terminate
3636 }
3637 ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime() => {
3638 KillBehavior::SharedV8DispatchOrTerminate
3639 }
3640 ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime() => {
3641 KillBehavior::SharedV8Terminate
3642 }
3643 ActiveExecution::Python(execution) if execution.uses_shared_v8_runtime() => {
3644 KillBehavior::SharedV8Terminate
3645 }
3646 ActiveExecution::Javascript(execution) if execution.child_pid() == 0 => {
3647 KillBehavior::Noop
3648 }
3649 _ => KillBehavior::HostPid(process.execution.child_pid()),
3650 };
3651
3652 match behavior {
3653 KillBehavior::Tool => {
3654 let ActiveExecution::Tool(execution) = &process.execution else {
3655 unreachable!("kill behavior must match tool execution");
3656 };
3657 if signal != 0 {
3658 execution.cancelled.store(true, Ordering::Relaxed);
3659 process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
3660 128 + signal,
3661 ))?;
3662 }
3663 }
3664 KillBehavior::SharedV8StateOnly => {
3665 if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
3666 vm.kernel
3667 .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
3668 .map_err(kernel_error)?;
3669 }
3670 }
3671 KillBehavior::SharedV8Continue => {
3672 vm.kernel
3673 .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
3674 .map_err(kernel_error)?;
3675 if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
3676 process.execution.terminate()?;
3677 }
3678 }
3679 KillBehavior::SharedV8Terminate => {
3680 if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
3681 close_kernel_process_stdin(&mut vm.kernel, process)?;
3682 }
3683 process.execution.terminate()?;
3684 let needs_synthetic_exit = matches!(process.execution, ActiveExecution::Wasm(_))
3685 || (signal == SIGKILL
3686 && matches!(process.execution, ActiveExecution::Javascript(_)));
3687 if signal != 0 && needs_synthetic_exit {
3688 process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
3689 128 + signal,
3690 ))?;
3691 }
3692 }
3693 KillBehavior::SharedV8DispatchOrTerminate => {
3694 if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
3695 process.execution.terminate()?;
3696 }
3697 }
3698 KillBehavior::Noop => {}
3699 KillBehavior::HostPid(pid) => {
3700 if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
3701 close_kernel_process_stdin(&mut vm.kernel, process)?;
3702 }
3703 signal_runtime_process(pid, signal)?;
3704 }
3705 }
3706 emit_security_audit_event(
3707 &self.bridge,
3708 vm_id,
3709 "security.process.kill",
3710 audit_fields([
3711 (String::from("source"), String::from("control_plane")),
3712 (String::from("source_pid"), String::from("0")),
3713 (String::from("target_pid"), process.kernel_pid.to_string()),
3714 (String::from("process_id"), process_id.to_owned()),
3715 (String::from("signal"), signal_name),
3716 (
3717 String::from("host_pid"),
3718 process.execution.child_pid().to_string(),
3719 ),
3720 ]),
3721 );
3722 Ok(())
3723 }
3724
3725 pub async fn pump_process_events(
3726 &mut self,
3727 ownership: &OwnershipScope,
3728 ) -> Result<bool, SidecarError> {
3729 let mut emitted_any = false;
3730
3731 let mut queued_envelopes = Vec::new();
3732 {
3733 let pending_capacity = self.pending_process_event_capacity();
3734 let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
3735 SidecarError::InvalidState(String::from("process event receiver unavailable"))
3736 })?;
3737 loop {
3738 if queued_envelopes.len() >= pending_capacity {
3739 if receiver.is_empty() {
3740 break;
3741 }
3742 return Err(process_event_queue_overflow_error());
3743 }
3744 match receiver.try_recv() {
3745 Ok(envelope) => {
3746 queued_envelopes.push(envelope);
3747 emitted_any = true;
3748 }
3749 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
3750 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
3751 }
3752 }
3753 }
3754 for envelope in queued_envelopes {
3755 self.queue_pending_process_event(envelope)?;
3756 }
3757
3758 let vm_ids = self.vm_ids_for_scope(ownership)?;
3759 for vm_id in vm_ids {
3760 while let Some(vm) = self.vms.get(&vm_id) {
3761 let connection_id = vm.connection_id.clone();
3762 let session_id = vm.session_id.clone();
3763 let process_ids = self
3764 .vms
3765 .get(&vm_id)
3766 .map(|vm| vm.active_processes.keys().cloned().collect::<Vec<_>>())
3767 .unwrap_or_default();
3768 let mut emitted_this_pass = false;
3769
3770 for process_id in process_ids {
3771 if self
3772 .vms
3773 .get(&vm_id)
3774 .is_some_and(|vm| vm.detached_child_processes.contains(&process_id))
3775 {
3776 continue;
3777 }
3778 enum ProcessPollResult {
3779 Event(Box<Option<ActiveExecutionEvent>>),
3780 RecoverClosedChannel,
3781 }
3782 let poll_result = {
3783 let Some(vm) = self.vms.get_mut(&vm_id) else {
3784 continue;
3785 };
3786 let Some(process) = vm.active_processes.get_mut(&process_id) else {
3787 continue;
3788 };
3789 if let Some(event) = process.pending_execution_events.pop_front() {
3790 ProcessPollResult::Event(Box::new(Some(event)))
3791 } else {
3792 match process.execution.poll_event(Duration::ZERO).await {
3793 Ok(event) => ProcessPollResult::Event(Box::new(event)),
3794 Err(SidecarError::Execution(message))
3795 if (process.runtime == GuestRuntimeKind::JavaScript
3796 && closed_javascript_event_channel(&message))
3797 || (process.runtime == GuestRuntimeKind::Python
3798 && closed_python_event_channel(&message))
3799 || (process.runtime == GuestRuntimeKind::WebAssembly
3800 && closed_wasm_event_channel(&message)) =>
3801 {
3802 ProcessPollResult::RecoverClosedChannel
3803 }
3804 Err(other) => return Err(other),
3805 }
3806 }
3807 };
3808 let event = match poll_result {
3809 ProcessPollResult::Event(event) => *event,
3810 ProcessPollResult::RecoverClosedChannel => {
3811 self.recover_closed_root_runtime_process_event(&vm_id, &process_id)?
3812 }
3813 };
3814
3815 let Some(event) = event else {
3816 continue;
3817 };
3818
3819 if Self::internal_execution_event(&event) {
3820 self.handle_execution_event(&vm_id, &process_id, event)?;
3825 } else {
3826 self.queue_pending_process_event(ProcessEventEnvelope {
3827 connection_id: connection_id.clone(),
3828 session_id: session_id.clone(),
3829 vm_id: vm_id.clone(),
3830 process_id: process_id.clone(),
3831 event,
3832 })?;
3833 }
3834 emitted_any = true;
3835 emitted_this_pass = true;
3836 }
3837
3838 if !emitted_this_pass {
3839 break;
3840 }
3841 }
3842
3843 if self.pump_detached_child_process_events(&vm_id)? {
3844 emitted_any = true;
3845 }
3846 }
3847
3848 Ok(emitted_any)
3849 }
3850
3851 fn internal_execution_event(event: &ActiveExecutionEvent) -> bool {
3852 matches!(
3853 event,
3854 ActiveExecutionEvent::JavascriptSyncRpcRequest(_)
3855 | ActiveExecutionEvent::PythonVfsRpcRequest(_)
3856 | ActiveExecutionEvent::SignalState { .. }
3857 )
3858 }
3859
3860 fn recover_closed_root_runtime_process_event(
3861 &mut self,
3862 vm_id: &str,
3863 process_id: &str,
3864 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
3865 let Some(vm) = self.vms.get_mut(vm_id) else {
3866 return Ok(None);
3867 };
3868 let Some(process) = vm.active_processes.get(process_id) else {
3869 return Ok(None);
3870 };
3871 if process.execution.uses_shared_v8_runtime() {
3872 return Ok(None);
3873 }
3874 if process.runtime != GuestRuntimeKind::JavaScript
3875 && process.runtime != GuestRuntimeKind::Python
3876 && process.runtime != GuestRuntimeKind::WebAssembly
3877 {
3878 return Ok(None);
3879 }
3880 let runtime_child_pid = process.execution.child_pid();
3881 if runtime_child_pid == 0 {
3882 return Ok(None);
3883 }
3884 if let Some(status) = runtime_child_exit_status(runtime_child_pid)? {
3885 return Ok(Some(ActiveExecutionEvent::Exited(status)));
3886 }
3887 if runtime_child_is_alive(runtime_child_pid)? {
3888 return Ok(None);
3889 }
3890 Ok(Some(ActiveExecutionEvent::Exited(0)))
3891 }
3892
3893 fn active_process_by_path<'a>(
3894 process: &'a ActiveProcess,
3895 child_path: &[&str],
3896 ) -> Option<&'a ActiveProcess> {
3897 let mut current = process;
3898 for child_id in child_path {
3899 current = current.child_processes.get(*child_id)?;
3900 }
3901 Some(current)
3902 }
3903
3904 fn active_process_by_path_mut<'a>(
3905 process: &'a mut ActiveProcess,
3906 child_path: &[&str],
3907 ) -> Option<&'a mut ActiveProcess> {
3908 let mut current = process;
3909 for child_id in child_path {
3910 current = current.child_processes.get_mut(*child_id)?;
3911 }
3912 Some(current)
3913 }
3914
3915 fn active_process_by_owned_path_mut<'a>(
3916 process: &'a mut ActiveProcess,
3917 child_path: &[String],
3918 ) -> Option<&'a mut ActiveProcess> {
3919 let mut current = process;
3920 for child_id in child_path {
3921 current = current.child_processes.get_mut(child_id)?;
3922 }
3923 Some(current)
3924 }
3925
3926 fn active_process_path_by_kernel_pid(
3927 process: &ActiveProcess,
3928 kernel_pid: u32,
3929 ) -> Option<Vec<String>> {
3930 if process.kernel_pid == kernel_pid {
3931 return Some(Vec::new());
3932 }
3933
3934 for (child_id, child) in &process.child_processes {
3935 let Some(mut path) = Self::active_process_path_by_kernel_pid(child, kernel_pid) else {
3936 continue;
3937 };
3938 path.insert(0, child_id.clone());
3939 return Some(path);
3940 }
3941
3942 None
3943 }
3944
3945 fn descendant_parent_process<'a>(
3946 vm: &'a VmState,
3947 process_id: &str,
3948 child_path: &[&str],
3949 ) -> Option<&'a ActiveProcess> {
3950 let root = vm.active_processes.get(process_id)?;
3951 Self::active_process_by_path(root, child_path)
3952 }
3953
3954 fn descendant_parent_process_mut<'a>(
3955 vm: &'a mut VmState,
3956 process_id: &str,
3957 child_path: &[&str],
3958 ) -> Option<&'a mut ActiveProcess> {
3959 let root = vm.active_processes.get_mut(process_id)?;
3960 Self::active_process_by_path_mut(root, child_path)
3961 }
3962
3963 fn child_process_path_label(process_id: &str, child_path: &[&str]) -> String {
3964 if child_path.is_empty() {
3965 process_id.to_owned()
3966 } else {
3967 format!("{process_id}/{}", child_path.join("/"))
3968 }
3969 }
3970
3971 fn adopt_detached_child_processes(
3972 current_process_id: &str,
3973 process: &mut ActiveProcess,
3974 ) -> Vec<(String, ActiveProcess)> {
3975 let mut adopted = Vec::new();
3976 let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
3977 for child_id in child_ids {
3978 let child_process_id = format!("{current_process_id}/{child_id}");
3979 let Some(mut child) = process.child_processes.remove(&child_id) else {
3980 continue;
3981 };
3982 if child.detached {
3983 adopted.push((child_process_id, child));
3984 continue;
3985 }
3986
3987 adopted.extend(Self::adopt_detached_child_processes(
3988 &child_process_id,
3989 &mut child,
3990 ));
3991 process.child_processes.insert(child_id, child);
3992 }
3993 adopted
3994 }
3995
3996 fn child_process_signal_key<'a>(process_id: &'a str, child_path: &[&'a str]) -> &'a str {
3997 child_path.last().copied().unwrap_or(process_id)
3998 }
3999
4000 fn resolve_detached_child_process_path(
4001 vm: &VmState,
4002 detached_process_id: &str,
4003 ) -> Option<(String, Vec<String>)> {
4004 let root_process_id = vm
4005 .active_processes
4006 .keys()
4007 .filter(|candidate| {
4008 detached_process_id == candidate.as_str()
4009 || detached_process_id
4010 .strip_prefix(candidate.as_str())
4011 .is_some_and(|remainder| remainder.starts_with('/'))
4012 })
4013 .max_by_key(|candidate| candidate.len())?
4014 .clone();
4015
4016 let remainder = detached_process_id
4017 .strip_prefix(root_process_id.as_str())
4018 .unwrap_or_default();
4019 if remainder.is_empty() {
4020 return Some((root_process_id, Vec::new()));
4021 }
4022
4023 Some((
4024 root_process_id,
4025 remainder
4026 .trim_start_matches('/')
4027 .split('/')
4028 .map(str::to_owned)
4029 .collect(),
4030 ))
4031 }
4032
4033 fn pump_detached_child_process_events(&mut self, vm_id: &str) -> Result<bool, SidecarError> {
4034 let detached_process_ids = self
4035 .vms
4036 .get(vm_id)
4037 .map(|vm| {
4038 vm.detached_child_processes
4039 .iter()
4040 .cloned()
4041 .collect::<Vec<_>>()
4042 })
4043 .unwrap_or_default();
4044 let mut emitted_any = false;
4045 for detached_process_id in detached_process_ids {
4046 let Some((root_process_id, child_path)) = self
4047 .vms
4048 .get(vm_id)
4049 .and_then(|vm| Self::resolve_detached_child_process_path(vm, &detached_process_id))
4050 else {
4051 if let Some(vm) = self.vms.get_mut(vm_id) {
4052 vm.detached_child_processes.remove(&detached_process_id);
4053 }
4054 continue;
4055 };
4056 if child_path.is_empty() {
4057 loop {
4058 enum ProcessPollResult {
4059 Event(Box<Option<ActiveExecutionEvent>>),
4060 RecoverClosedChannel,
4061 }
4062 let poll_result = {
4063 let Some(vm) = self.vms.get_mut(vm_id) else {
4064 break;
4065 };
4066 let Some(process) = vm.active_processes.get_mut(&root_process_id) else {
4067 break;
4068 };
4069 if let Some(event) = process.pending_execution_events.pop_front() {
4070 ProcessPollResult::Event(Box::new(Some(event)))
4071 } else {
4072 match process.execution.poll_event_blocking(Duration::ZERO) {
4073 Ok(event) => ProcessPollResult::Event(Box::new(event)),
4074 Err(SidecarError::Execution(message))
4075 if (process.runtime == GuestRuntimeKind::JavaScript
4076 && closed_javascript_event_channel(&message))
4077 || (process.runtime == GuestRuntimeKind::Python
4078 && closed_python_event_channel(&message))
4079 || (process.runtime == GuestRuntimeKind::WebAssembly
4080 && closed_wasm_event_channel(&message)) =>
4081 {
4082 ProcessPollResult::RecoverClosedChannel
4083 }
4084 Err(error) => return Err(error),
4085 }
4086 }
4087 };
4088 let event = match poll_result {
4089 ProcessPollResult::Event(event) => *event,
4090 ProcessPollResult::RecoverClosedChannel => {
4091 self.recover_closed_root_runtime_process_event(vm_id, &root_process_id)?
4092 }
4093 };
4094 let Some(event) = event else {
4095 break;
4096 };
4097 let Some((connection_id, session_id)) = self
4098 .vms
4099 .get(vm_id)
4100 .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
4101 else {
4102 break;
4103 };
4104 match event {
4105 ActiveExecutionEvent::Stdout(chunk) => {
4106 self.queue_pending_process_event(ProcessEventEnvelope {
4107 connection_id,
4108 session_id,
4109 vm_id: vm_id.to_owned(),
4110 process_id: detached_process_id.clone(),
4111 event: ActiveExecutionEvent::Stdout(chunk),
4112 })?;
4113 emitted_any = true;
4114 }
4115 ActiveExecutionEvent::Stderr(chunk) => {
4116 self.queue_pending_process_event(ProcessEventEnvelope {
4117 connection_id,
4118 session_id,
4119 vm_id: vm_id.to_owned(),
4120 process_id: detached_process_id.clone(),
4121 event: ActiveExecutionEvent::Stderr(chunk),
4122 })?;
4123 emitted_any = true;
4124 }
4125 ActiveExecutionEvent::Exited(exit_code) => {
4126 if let Some(vm) = self.vms.get_mut(vm_id) {
4127 vm.detached_child_processes.remove(&detached_process_id);
4128 }
4129 self.queue_pending_process_event(ProcessEventEnvelope {
4130 connection_id,
4131 session_id,
4132 vm_id: vm_id.to_owned(),
4133 process_id: detached_process_id.clone(),
4134 event: ActiveExecutionEvent::Exited(exit_code),
4135 })?;
4136 emitted_any = true;
4137 break;
4138 }
4139 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
4140 self.handle_javascript_sync_rpc_request(
4141 vm_id,
4142 &root_process_id,
4143 request,
4144 )?;
4145 }
4146 ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
4147 self.handle_python_vfs_rpc_request(vm_id, &root_process_id, *request)?;
4148 }
4149 ActiveExecutionEvent::SignalState {
4150 signal,
4151 registration,
4152 } => {
4153 if let Some(vm) = self.vms.get_mut(vm_id) {
4154 vm.signal_states
4155 .entry(root_process_id.clone())
4156 .or_default()
4157 .insert(signal, registration);
4158 }
4159 }
4160 }
4161 }
4162 continue;
4163 }
4164
4165 let parent_path = child_path[..child_path.len() - 1]
4166 .iter()
4167 .map(String::as_str)
4168 .collect::<Vec<_>>();
4169 let child_process_id = child_path.last().expect("child path cannot be empty");
4170
4171 loop {
4172 let event = match self.poll_descendant_javascript_child_process(
4173 vm_id,
4174 &root_process_id,
4175 &parent_path,
4176 child_process_id,
4177 0,
4178 ) {
4179 Ok(event) => event,
4180 Err(SidecarError::InvalidState(message))
4181 if message.contains("unknown child process")
4182 || message.contains("unknown child process path") =>
4183 {
4184 if let Some(vm) = self.vms.get_mut(vm_id) {
4185 vm.detached_child_processes.remove(&detached_process_id);
4186 }
4187 break;
4188 }
4189 Err(error) if is_javascript_child_process_gone_error(&error) => {
4190 if let Some(vm) = self.vms.get_mut(vm_id) {
4191 vm.detached_child_processes.remove(&detached_process_id);
4192 }
4193 break;
4194 }
4195 Err(error) => return Err(error),
4196 };
4197
4198 let Some(event_type) = event.get("type").and_then(Value::as_str) else {
4199 break;
4200 };
4201 let Some((connection_id, session_id)) = self
4202 .vms
4203 .get(vm_id)
4204 .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
4205 else {
4206 break;
4207 };
4208
4209 let envelope = match event_type {
4210 "stdout" => Some(ProcessEventEnvelope {
4211 connection_id: connection_id.clone(),
4212 session_id: session_id.clone(),
4213 vm_id: vm_id.to_owned(),
4214 process_id: detached_process_id.clone(),
4215 event: ActiveExecutionEvent::Stdout(javascript_sync_rpc_bytes_arg(
4216 &[event.get("data").cloned().unwrap_or(Value::Null)],
4217 0,
4218 "detached child_process stdout",
4219 )?),
4220 }),
4221 "stderr" => Some(ProcessEventEnvelope {
4222 connection_id: connection_id.clone(),
4223 session_id: session_id.clone(),
4224 vm_id: vm_id.to_owned(),
4225 process_id: detached_process_id.clone(),
4226 event: ActiveExecutionEvent::Stderr(javascript_sync_rpc_bytes_arg(
4227 &[event.get("data").cloned().unwrap_or(Value::Null)],
4228 0,
4229 "detached child_process stderr",
4230 )?),
4231 }),
4232 "exit" => {
4233 if let Some(vm) = self.vms.get_mut(vm_id) {
4234 vm.detached_child_processes.remove(&detached_process_id);
4235 }
4236 Some(ProcessEventEnvelope {
4237 connection_id,
4238 session_id,
4239 vm_id: vm_id.to_owned(),
4240 process_id: detached_process_id.clone(),
4241 event: ActiveExecutionEvent::Exited(
4242 event
4243 .get("exitCode")
4244 .and_then(Value::as_i64)
4245 .map(|value| value as i32)
4246 .unwrap_or(1),
4247 ),
4248 })
4249 }
4250 _ => None,
4251 };
4252
4253 let Some(envelope) = envelope else {
4254 break;
4255 };
4256 self.queue_pending_process_event(envelope)?;
4257 emitted_any = true;
4258
4259 if event_type == "exit" {
4260 break;
4261 }
4262 }
4263 }
4264
4265 Ok(emitted_any)
4266 }
4267 pub(crate) fn drain_queued_descendant_javascript_child_process_events(
4268 &mut self,
4269 vm_id: &str,
4270 process_id: &str,
4271 child_path: &[&str],
4272 ) -> Result<(), SidecarError> {
4273 if child_path.is_empty() {
4274 return Ok(());
4275 }
4276 let target_process_id = Self::child_process_path_label(process_id, child_path);
4277 let mut child_capacity = self
4278 .vms
4279 .get(vm_id)
4280 .and_then(|vm| vm.active_processes.get(process_id))
4281 .and_then(|root| descendant_pending_execution_event_capacity(root, child_path));
4282
4283 let mut deferred = VecDeque::new();
4284 while let Some(envelope) = self.pending_process_events.pop_front() {
4285 if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
4286 if matches!(child_capacity, Some(0)) {
4287 self.pending_process_events.push_front(envelope);
4288 while let Some(deferred_envelope) = deferred.pop_back() {
4289 self.pending_process_events.push_front(deferred_envelope);
4290 }
4291 return Err(process_event_queue_overflow_error());
4292 }
4293 if let Some(vm) = self.vms.get_mut(vm_id) {
4294 if let Some(root) = vm.active_processes.get_mut(process_id) {
4295 if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
4296 child.queue_pending_execution_event(envelope.event)?;
4297 child_capacity = child_capacity.map(|capacity| capacity - 1);
4298 continue;
4299 }
4300 }
4301 }
4302 }
4303 deferred.push_back(envelope);
4304 }
4305 self.pending_process_events = deferred;
4306
4307 let mut queued = Vec::new();
4308 {
4309 let transfer_capacity = self
4310 .pending_process_event_capacity()
4311 .min(child_capacity.unwrap_or(usize::MAX));
4312 let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
4313 SidecarError::InvalidState(String::from("process event receiver unavailable"))
4314 })?;
4315 loop {
4316 if queued.len() >= transfer_capacity {
4317 if receiver.is_empty() {
4318 break;
4319 }
4320 return Err(process_event_queue_overflow_error());
4321 }
4322 match receiver.try_recv() {
4323 Ok(envelope) => queued.push(envelope),
4324 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
4325 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
4326 }
4327 }
4328 }
4329 for envelope in queued {
4330 if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
4331 if let Some(vm) = self.vms.get_mut(vm_id) {
4332 if let Some(root) = vm.active_processes.get_mut(process_id) {
4333 if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
4334 child.queue_pending_execution_event(envelope.event)?;
4335 continue;
4336 }
4337 }
4338 }
4339 }
4340 self.queue_pending_process_event(envelope)?;
4341 }
4342
4343 Ok(())
4344 }
4345
4346 pub(crate) fn handle_execution_event(
4347 &mut self,
4348 vm_id: &str,
4349 process_id: &str,
4350 event: ActiveExecutionEvent,
4351 ) -> Result<Option<EventFrame>, SidecarError> {
4352 let Some(vm) = self.vms.get(vm_id) else {
4353 log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
4354 return Ok(None);
4355 };
4356 if !vm.active_processes.contains_key(process_id) {
4357 log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
4358 return Ok(None);
4359 }
4360 let (connection_id, session_id) = { (vm.connection_id.clone(), vm.session_id.clone()) };
4361 let ownership = OwnershipScope::vm(&connection_id, &session_id, vm_id);
4362
4363 if self.capture_extension_process_output_event(vm_id, process_id, &event) {
4364 return Ok(None);
4365 }
4366
4367 match event {
4368 ActiveExecutionEvent::Stdout(chunk) => Ok(Some(EventFrame::new(
4369 ownership,
4370 EventPayload::ProcessOutput(ProcessOutputEvent {
4371 process_id: process_id.to_owned(),
4372 channel: StreamChannel::Stdout,
4373 chunk,
4374 }),
4375 ))),
4376 ActiveExecutionEvent::Stderr(chunk) => Ok(Some(EventFrame::new(
4377 ownership,
4378 EventPayload::ProcessOutput(ProcessOutputEvent {
4379 process_id: process_id.to_owned(),
4380 channel: StreamChannel::Stderr,
4381 chunk,
4382 }),
4383 ))),
4384 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
4385 self.handle_javascript_sync_rpc_request(vm_id, process_id, request)?;
4386 Ok(None)
4387 }
4388 ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
4389 self.handle_python_vfs_rpc_request(vm_id, process_id, *request)?;
4390 Ok(None)
4391 }
4392 ActiveExecutionEvent::SignalState {
4393 signal,
4394 registration,
4395 } => {
4396 let Some(vm) = self.vms.get_mut(vm_id) else {
4397 return Ok(None);
4398 };
4399 if !vm.active_processes.contains_key(process_id) {
4400 return Ok(None);
4401 }
4402 vm.signal_states
4403 .entry(process_id.to_owned())
4404 .or_default()
4405 .insert(signal, registration);
4406 Ok(None)
4407 }
4408 ActiveExecutionEvent::Exited(exit_code) => {
4409 let became_idle = self
4410 .finish_active_process_exit(vm_id, process_id, exit_code)?
4411 .unwrap_or(false);
4412
4413 if became_idle {
4414 self.bridge.emit_lifecycle(vm_id, LifecycleState::Ready)?;
4415 }
4416
4417 Ok(Some(EventFrame::new(
4418 ownership,
4419 EventPayload::ProcessExited(ProcessExitedEvent {
4420 process_id: process_id.to_owned(),
4421 exit_code,
4422 }),
4423 )))
4424 }
4425 }
4426 }
4427
4428 pub(crate) fn finish_active_process_exit(
4429 &mut self,
4430 vm_id: &str,
4431 process_id: &str,
4432 exit_code: i32,
4433 ) -> Result<Option<bool>, SidecarError> {
4434 let Some(vm) = self.vms.get_mut(vm_id) else {
4435 log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
4436 return Ok(None);
4437 };
4438 if !vm.active_processes.contains_key(process_id) {
4439 log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
4440 return Ok(None);
4441 }
4442
4443 prune_exited_process_snapshots(vm);
4444 let process_table = vm.kernel.list_processes();
4445 let Some(mut process) = vm.active_processes.remove(process_id) else {
4446 return Ok(None);
4447 };
4448 if let Some(info) = process_table.get(&process.kernel_pid) {
4449 vm.exited_process_snapshots
4450 .push_back(ExitedProcessSnapshot {
4451 captured_at: Instant::now(),
4452 process: build_process_snapshot_entry(
4453 process_id,
4454 &process,
4455 info,
4456 Some(exit_code),
4457 ),
4458 });
4459 }
4460 let detached_children = Self::adopt_detached_child_processes(process_id, &mut process);
4461 sync_process_host_writes_to_kernel(vm, &process)?;
4462 terminate_child_process_tree(&mut vm.kernel, &mut process);
4463 process.kernel_handle.finish(exit_code);
4464 let _ = vm.kernel.wait_and_reap(process.kernel_pid);
4465 vm.signal_states.remove(process_id);
4466 for (detached_process_id, detached_child) in detached_children {
4467 vm.detached_child_processes
4468 .insert(detached_process_id.clone());
4469 vm.active_processes
4470 .insert(detached_process_id, detached_child);
4471 }
4472 let became_idle = vm.active_processes.is_empty();
4473 self.prune_extension_process_resource(process_id);
4474
4475 Ok(Some(became_idle))
4476 }
4477
4478 pub(crate) fn drain_process_events_blocking_with_limit(
4479 &mut self,
4480 vm_id: &str,
4481 process_id: &str,
4482 max_events: usize,
4483 ) -> Result<Vec<ActiveExecutionEvent>, SidecarError> {
4484 let mut events = Vec::new();
4485 if max_events == 0 {
4486 return Ok(events);
4487 }
4488 let mut deadline = Instant::now() + Duration::from_millis(150);
4489
4490 loop {
4491 if events.len() >= max_events {
4492 break;
4493 }
4494 let event = {
4495 let Some(vm) = self.vms.get_mut(vm_id) else {
4496 break;
4497 };
4498 let Some(process) = vm.active_processes.get_mut(process_id) else {
4499 break;
4500 };
4501 if let Some(event) = process.pending_execution_events.pop_front() {
4502 Some(event)
4503 } else {
4504 match process.execution.poll_event_blocking(Duration::ZERO) {
4505 Ok(event) => event,
4506 Err(SidecarError::Execution(_)) => None,
4507 Err(other) => return Err(other),
4508 }
4509 }
4510 };
4511
4512 let Some(event) = event else {
4513 if Instant::now() >= deadline {
4514 break;
4515 }
4516 let blocking_wait = deadline.saturating_duration_since(Instant::now());
4517 if blocking_wait.is_zero() {
4518 break;
4519 }
4520 if events.len() >= max_events {
4521 break;
4522 }
4523 let delayed_event = {
4524 let Some(vm) = self.vms.get_mut(vm_id) else {
4525 break;
4526 };
4527 let Some(process) = vm.active_processes.get_mut(process_id) else {
4528 break;
4529 };
4530 if let Some(event) = process.pending_execution_events.pop_front() {
4531 Some(event)
4532 } else {
4533 match process.execution.poll_event_blocking(blocking_wait) {
4534 Ok(event) => event,
4535 Err(SidecarError::Execution(_)) => None,
4536 Err(other) => return Err(other),
4537 }
4538 }
4539 };
4540 let Some(event) = delayed_event else {
4541 break;
4542 };
4543 events.push(event);
4544 deadline = Instant::now() + Duration::from_millis(150);
4545 continue;
4546 };
4547 events.push(event);
4548 deadline = Instant::now() + Duration::from_millis(150);
4549 }
4550
4551 Ok(events)
4552 }
4553
4554 pub(crate) fn handle_python_vfs_rpc_request(
4555 &mut self,
4556 vm_id: &str,
4557 process_id: &str,
4558 request: PythonVfsRpcRequest,
4559 ) -> Result<(), SidecarError> {
4560 match request.method {
4561 PythonVfsRpcMethod::Read
4562 | PythonVfsRpcMethod::Write
4563 | PythonVfsRpcMethod::Stat
4564 | PythonVfsRpcMethod::ReadDir
4565 | PythonVfsRpcMethod::Mkdir => {
4566 filesystem_handle_python_vfs_rpc_request(self, vm_id, process_id, request)
4567 }
4568 PythonVfsRpcMethod::HttpRequest => {
4569 self.handle_python_http_rpc_request(vm_id, process_id, request)
4570 }
4571 PythonVfsRpcMethod::DnsLookup => {
4572 self.handle_python_dns_rpc_request(vm_id, process_id, request)
4573 }
4574 PythonVfsRpcMethod::SubprocessRun => {
4575 self.handle_python_subprocess_rpc_request(vm_id, process_id, request)
4576 }
4577 }
4578 }
4579
4580 fn handle_python_http_rpc_request(
4581 &mut self,
4582 vm_id: &str,
4583 process_id: &str,
4584 request: PythonVfsRpcRequest,
4585 ) -> Result<(), SidecarError> {
4586 let Some(vm) = self.vms.get(vm_id) else {
4587 return Ok(());
4588 };
4589 if !vm.active_processes.contains_key(process_id) {
4590 return Ok(());
4591 }
4592 let response = (|| {
4593 let url_text = request.url.as_deref().ok_or_else(|| {
4594 SidecarError::InvalidState(String::from("python httpRequest requires a url"))
4595 })?;
4596 let url = Url::parse(url_text)
4597 .map_err(|error| SidecarError::Execution(format!("ERR_INVALID_URL: {error}")))?;
4598 let host = url.host_str().ok_or_else(|| {
4599 SidecarError::Execution(String::from("ERR_INVALID_URL: missing host"))
4600 })?;
4601 let port = url.port_or_known_default().ok_or_else(|| {
4602 SidecarError::Execution(String::from("ERR_INVALID_URL: missing port"))
4603 })?;
4604 self.bridge.require_network_access(
4605 vm_id,
4606 NetworkOperation::Http,
4607 format_tcp_resource(host, port),
4608 )?;
4609 let pinned_addresses = if let Ok(literal_ip) = host.parse::<IpAddr>() {
4616 filter_dns_safe_ip_addrs(vec![literal_ip], host)?
4617 } else {
4618 filter_dns_safe_ip_addrs(
4619 resolve_dns_ip_addrs(
4620 &self.bridge,
4621 &vm.kernel,
4622 vm_id,
4623 &vm.dns,
4624 host,
4625 DnsLookupPolicy::SkipPermissions,
4626 )?,
4627 host,
4628 )?
4629 };
4630 let mut headers = BTreeMap::new();
4631 for (name, value) in &request.headers {
4632 headers.insert(name.clone(), Value::String(value.clone()));
4633 }
4634 let options = JavascriptHttpRequestOptions {
4635 method: Some(
4636 request
4637 .http_method
4638 .clone()
4639 .unwrap_or_else(|| String::from("GET")),
4640 ),
4641 headers,
4642 body: request.body_base64.as_deref().map(|body| {
4643 String::from_utf8(
4644 base64::engine::general_purpose::STANDARD
4645 .decode(body)
4646 .unwrap_or_default(),
4647 )
4648 .unwrap_or_default()
4649 }),
4650 reject_unauthorized: None,
4651 };
4652 let headers =
4653 parse_http_header_collection(&options.headers, "python httpRequest headers")?;
4654 let response =
4655 issue_outbound_http_request(&url, &options, &headers, &pinned_addresses)?;
4656 let payload_json = response.as_str().ok_or_else(|| {
4657 SidecarError::Execution(String::from(
4658 "python httpRequest returned a non-string response payload",
4659 ))
4660 })?;
4661 let payload: Value = serde_json::from_str(payload_json).map_err(|error| {
4662 SidecarError::Execution(format!(
4663 "python httpRequest response must be valid JSON: {error}"
4664 ))
4665 })?;
4666 let header_map = payload
4667 .get("headers")
4668 .and_then(Value::as_array)
4669 .map(|entries| {
4670 let mut normalized = BTreeMap::<String, Vec<String>>::new();
4671 for entry in entries {
4672 let Some(pair) = entry.as_array() else {
4673 continue;
4674 };
4675 let Some(name) = pair.first().and_then(Value::as_str) else {
4676 continue;
4677 };
4678 let Some(value) = pair.get(1).and_then(Value::as_str) else {
4679 continue;
4680 };
4681 normalized
4682 .entry(name.to_owned())
4683 .or_default()
4684 .push(value.to_owned());
4685 }
4686 normalized
4687 })
4688 .unwrap_or_default();
4689 Ok(PythonVfsRpcResponsePayload::Http {
4690 status: payload
4691 .get("status")
4692 .and_then(Value::as_u64)
4693 .map(|value| value as u16)
4694 .unwrap_or_default(),
4695 reason: payload
4696 .get("statusText")
4697 .and_then(Value::as_str)
4698 .unwrap_or_default()
4699 .to_owned(),
4700 url: payload
4701 .get("url")
4702 .and_then(Value::as_str)
4703 .unwrap_or(url_text)
4704 .to_owned(),
4705 headers: header_map,
4706 body_base64: payload
4707 .get("body")
4708 .and_then(Value::as_str)
4709 .unwrap_or_default()
4710 .to_owned(),
4711 })
4712 })();
4713
4714 self.respond_python_rpc(vm_id, process_id, request.id, response)
4715 }
4716
4717 fn handle_python_dns_rpc_request(
4718 &mut self,
4719 vm_id: &str,
4720 process_id: &str,
4721 request: PythonVfsRpcRequest,
4722 ) -> Result<(), SidecarError> {
4723 let Some(vm) = self.vms.get(vm_id) else {
4724 return Ok(());
4725 };
4726 if !vm.active_processes.contains_key(process_id) {
4727 return Ok(());
4728 }
4729 let response = (|| {
4730 let hostname = request.hostname.as_deref().ok_or_else(|| {
4731 SidecarError::InvalidState(String::from("python dnsLookup requires a hostname"))
4732 })?;
4733 let mut addresses = filter_dns_safe_ip_addrs(
4734 resolve_dns_ip_addrs(
4735 &self.bridge,
4736 &vm.kernel,
4737 vm_id,
4738 &vm.dns,
4739 hostname,
4740 DnsLookupPolicy::CheckPermissions,
4741 )?,
4742 hostname,
4743 )?;
4744 if let Some(family) = request.family {
4745 addresses.retain(|address| {
4746 matches!((family, address), (4, IpAddr::V4(_)) | (6, IpAddr::V6(_)))
4747 });
4748 }
4749 Ok(PythonVfsRpcResponsePayload::DnsLookup {
4750 addresses: addresses
4751 .into_iter()
4752 .map(|address| address.to_string())
4753 .collect(),
4754 })
4755 })();
4756
4757 self.respond_python_rpc(vm_id, process_id, request.id, response)
4758 }
4759
4760 fn handle_python_subprocess_rpc_request(
4761 &mut self,
4762 vm_id: &str,
4763 process_id: &str,
4764 request: PythonVfsRpcRequest,
4765 ) -> Result<(), SidecarError> {
4766 let command = request.command.clone().ok_or_else(|| {
4767 SidecarError::InvalidState(String::from("python subprocessRun requires a command"))
4768 })?;
4769 let (internal_bootstrap_env, cwd) = {
4770 let Some(vm) = self.vms.get(vm_id) else {
4771 return Ok(());
4772 };
4773 let Some(process) = vm.active_processes.get(process_id) else {
4774 return Ok(());
4775 };
4776 let virtual_home = guest_virtual_home(vm);
4777 let cwd = request.cwd.clone().or_else(|| {
4778 guest_runtime_path_for_host_path(
4779 &vm.guest_env,
4780 &virtual_home,
4781 &vm.host_cwd,
4782 &process.host_cwd.to_string_lossy(),
4783 )
4784 });
4785 (
4786 sanitize_javascript_child_process_internal_bootstrap_env(&vm.guest_env),
4787 cwd,
4788 )
4789 };
4790 let response = self
4791 .spawn_javascript_child_process_sync(
4792 vm_id,
4793 process_id,
4794 JavascriptChildProcessSpawnRequest {
4795 command,
4796 args: request.args.clone(),
4797 options: JavascriptChildProcessSpawnOptions {
4798 cwd,
4799 env: request.env.clone(),
4800 input: None,
4801 internal_bootstrap_env,
4802 shell: request.shell,
4803 detached: false,
4804 stdio: vec![
4805 String::from("pipe"),
4806 String::from("pipe"),
4807 String::from("pipe"),
4808 ],
4809 timeout: None,
4810 kill_signal: None,
4811 },
4812 },
4813 request.max_buffer,
4814 )
4815 .map(|payload| PythonVfsRpcResponsePayload::SubprocessRun {
4816 exit_code: payload
4817 .get("code")
4818 .and_then(Value::as_i64)
4819 .map(|value| value as i32)
4820 .unwrap_or(1),
4821 stdout: payload
4822 .get("stdout")
4823 .and_then(Value::as_str)
4824 .unwrap_or_default()
4825 .to_owned(),
4826 stderr: payload
4827 .get("stderr")
4828 .and_then(Value::as_str)
4829 .unwrap_or_default()
4830 .to_owned(),
4831 max_buffer_exceeded: payload
4832 .get("maxBufferExceeded")
4833 .and_then(Value::as_bool)
4834 .unwrap_or(false),
4835 });
4836
4837 self.respond_python_rpc(vm_id, process_id, request.id, response)
4838 }
4839
4840 fn respond_python_rpc(
4841 &mut self,
4842 vm_id: &str,
4843 process_id: &str,
4844 request_id: u64,
4845 response: Result<PythonVfsRpcResponsePayload, SidecarError>,
4846 ) -> Result<(), SidecarError> {
4847 let Some(vm) = self.vms.get_mut(vm_id) else {
4848 return Ok(());
4849 };
4850 let Some(process) = vm.active_processes.get_mut(process_id) else {
4851 return Ok(());
4852 };
4853 let result = match response {
4854 Ok(payload) => process
4855 .execution
4856 .respond_python_vfs_rpc_success(request_id, payload),
4857 Err(error) => process.execution.respond_python_vfs_rpc_error(
4858 request_id,
4859 "ERR_AGENTOS_PYTHON_VFS_RPC",
4860 error.to_string(),
4861 ),
4862 };
4863 match result {
4864 Ok(()) => Ok(()),
4865 Err(error) if is_broken_pipe_error(&error) => Ok(()),
4866 Err(error) => Err(error),
4867 }
4868 }
4869
4870 pub(crate) fn resolve_javascript_child_process_execution(
4871 &self,
4872 vm: &VmState,
4873 parent_env: &BTreeMap<String, String>,
4874 parent_guest_cwd: &str,
4875 parent_host_cwd: &Path,
4876 request: &JavascriptChildProcessSpawnRequest,
4877 ) -> Result<ResolvedChildProcessExecution, SidecarError> {
4878 let mut runtime_env = parent_env.clone();
4879 runtime_env.extend(request.options.internal_bootstrap_env.clone());
4880 let (guest_cwd, host_cwd_override) = request
4881 .options
4882 .cwd
4883 .as_deref()
4884 .map(|cwd| {
4885 let normalized_parent_host_cwd = normalize_host_path(parent_host_cwd);
4886 let requested_host_cwd = normalize_host_path(Path::new(cwd));
4887 if path_is_within_root(&requested_host_cwd, &normalized_parent_host_cwd) {
4888 let relative = requested_host_cwd
4889 .strip_prefix(&normalized_parent_host_cwd)
4890 .unwrap_or_else(|_| Path::new(""));
4891 let relative = relative.to_string_lossy().replace('\\', "/");
4892 let guest_cwd = if relative.is_empty() {
4893 parent_guest_cwd.to_owned()
4894 } else {
4895 normalize_path(&format!("{parent_guest_cwd}/{relative}"))
4896 };
4897 (guest_cwd, Some(requested_host_cwd))
4898 } else if Path::new(cwd).is_relative() {
4899 (
4900 normalize_path(&format!("{parent_guest_cwd}/{cwd}")),
4901 Some(normalize_host_path(&parent_host_cwd.join(cwd))),
4902 )
4903 } else {
4904 (normalize_path(cwd), None)
4905 }
4906 })
4907 .unwrap_or_else(|| (parent_guest_cwd.to_owned(), None));
4908 let inherited_host_cwd = (host_cwd_override.is_none() && guest_cwd == parent_guest_cwd)
4909 .then(|| normalize_host_path(parent_host_cwd));
4910 let host_cwd = host_cwd_override
4911 .or(inherited_host_cwd)
4912 .or_else(|| {
4913 host_runtime_path_for_guest_path_with_env(
4914 vm,
4915 &runtime_env,
4916 &guest_cwd,
4917 parent_host_cwd,
4918 )
4919 })
4920 .unwrap_or_else(|| {
4921 let candidate = PathBuf::from(&guest_cwd);
4922 if guest_cwd == parent_guest_cwd {
4923 normalize_host_path(parent_host_cwd)
4924 } else if candidate.is_absolute() {
4925 shadow_path_for_guest(vm, &guest_cwd)
4926 } else {
4927 vm.host_cwd.clone()
4928 }
4929 });
4930 let mut env = parent_env.clone();
4931 env.extend(request.options.env.clone());
4932 env.remove("AGENTOS_GUEST_ENTRYPOINT");
4935 env.remove("AGENTOS_NODE_EVAL");
4936
4937 let (command, process_args) = if request.options.shell {
4938 let tokens = tokenize_shell_free_command(&request.command);
4939 let requires_shell = command_requires_shell(&request.command)
4940 || tokens.first().is_some_and(|command| {
4941 is_posix_shell_builtin(command) || shell_first_token_requires_shell(command)
4942 });
4943 if requires_shell {
4944 if !vm.command_guest_paths.contains_key("sh") {
4945 return Err(SidecarError::InvalidState(format!(
4946 "shell-mode child_process command requires /bin/sh, which is not \
4947 installed in this VM (install a software package that provides sh, \
4948 for example @secure-exec/coreutils): {}",
4949 request.command
4950 )));
4951 }
4952 (
4953 String::from("sh"),
4954 vec![String::from("-c"), request.command.clone()],
4955 )
4956 } else {
4957 let Some((command, args)) = tokens.split_first() else {
4958 return Err(SidecarError::InvalidState(String::from(
4959 "child_process shell command must not be empty",
4960 )));
4961 };
4962 (command.clone(), args.to_vec())
4963 }
4964 } else {
4965 (request.command.clone(), request.args.clone())
4966 };
4967 let process_args = apply_shell_cwd_prefix(&command, process_args, &guest_cwd);
4968 if is_tool_command(vm, &command) {
4969 let command = normalized_tool_command_name(&command).unwrap_or(command);
4970 return Ok(ResolvedChildProcessExecution {
4971 command: command.clone(),
4972 process_args: std::iter::once(command.clone())
4973 .chain(process_args.iter().cloned())
4974 .collect(),
4975 runtime: GuestRuntimeKind::JavaScript,
4976 entrypoint: command,
4977 execution_args: process_args,
4978 env,
4979 guest_cwd,
4980 host_cwd,
4981 wasm_permission_tier: None,
4982 tool_command: true,
4983 });
4984 }
4985
4986 if is_path_like_specifier(&command)
4987 && matches!(
4988 Path::new(&command).extension().and_then(|ext| ext.to_str()),
4989 Some("js" | "mjs" | "cjs" | "ts" | "mts" | "cts")
4990 )
4991 {
4992 let guest_entrypoint = if command.starts_with('/') {
4993 normalize_path(&command)
4994 } else if command.starts_with("file:") {
4995 normalize_path(command.trim_start_matches("file:"))
4996 } else {
4997 normalize_path(&format!("{guest_cwd}/{command}"))
4998 };
4999 let host_entrypoint = if command.starts_with("./") || command.starts_with("../") {
5000 normalize_host_path(&host_cwd.join(&command))
5001 } else {
5002 host_runtime_path_for_guest_path_with_env(
5003 vm,
5004 &runtime_env,
5005 &guest_entrypoint,
5006 parent_host_cwd,
5007 )
5008 .unwrap_or_else(|| {
5009 let candidate = PathBuf::from(&guest_entrypoint);
5010 if candidate.is_absolute() {
5011 candidate
5012 } else {
5013 host_cwd.join(&guest_entrypoint)
5014 }
5015 })
5016 };
5017 env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
5018 let guest_entrypoint = env.get("AGENTOS_GUEST_ENTRYPOINT").cloned();
5019 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
5020
5021 return Ok(ResolvedChildProcessExecution {
5022 command: command.clone(),
5023 process_args: std::iter::once(command)
5024 .chain(process_args.iter().cloned())
5025 .collect(),
5026 runtime: GuestRuntimeKind::JavaScript,
5027 entrypoint: host_entrypoint.to_string_lossy().into_owned(),
5028 execution_args: process_args,
5029 env,
5030 guest_cwd,
5031 host_cwd,
5032 wasm_permission_tier: None,
5033 tool_command: false,
5034 });
5035 }
5036
5037 if is_node_runtime_command(&command) {
5038 if let Some(cli) = resolve_host_node_cli_entrypoint(&command) {
5039 env.insert(
5040 String::from("AGENTOS_NODE_EVAL"),
5041 build_host_node_cli_eval(&cli),
5042 );
5043 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5044 add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
5045 add_runtime_host_access_path(
5046 &mut env,
5047 "AGENTOS_EXTRA_FS_READ_PATHS",
5048 &cli.package_root,
5049 true,
5050 );
5051
5052 return Ok(ResolvedChildProcessExecution {
5053 command: command.clone(),
5054 process_args: std::iter::once(command.clone())
5055 .chain(process_args.iter().cloned())
5056 .collect(),
5057 runtime: GuestRuntimeKind::JavaScript,
5058 entrypoint: String::from("-e"),
5059 execution_args: std::iter::once(cli.guest_entrypoint.clone())
5060 .chain(process_args.iter().cloned())
5061 .collect(),
5062 env,
5063 guest_cwd,
5064 host_cwd,
5065 wasm_permission_tier: None,
5066 tool_command: false,
5067 });
5068 }
5069
5070 if process_args.is_empty() {
5071 env.insert(String::from("AGENTOS_NODE_EVAL"), String::new());
5072 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5073
5074 return Ok(ResolvedChildProcessExecution {
5075 command: command.clone(),
5076 process_args: vec![command.clone()],
5077 runtime: GuestRuntimeKind::JavaScript,
5078 entrypoint: String::from("-e"),
5079 execution_args: Vec::new(),
5080 env,
5081 guest_cwd,
5082 host_cwd,
5083 wasm_permission_tier: None,
5084 tool_command: false,
5085 });
5086 }
5087
5088 if let Some((entrypoint, execution_args)) =
5089 resolve_special_node_cli_invocation(&process_args, &mut env)
5090 {
5091 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5092
5093 return Ok(ResolvedChildProcessExecution {
5094 command: command.clone(),
5095 process_args: std::iter::once(command.clone())
5096 .chain(process_args.iter().cloned())
5097 .collect(),
5098 runtime: GuestRuntimeKind::JavaScript,
5099 entrypoint,
5100 execution_args,
5101 env,
5102 guest_cwd,
5103 host_cwd,
5104 wasm_permission_tier: None,
5105 tool_command: false,
5106 });
5107 }
5108
5109 let Some(entrypoint_specifier) = process_args.first() else {
5110 return Err(SidecarError::InvalidState(format!(
5111 "{command} child_process spawn requires an entrypoint"
5112 )));
5113 };
5114
5115 let (entrypoint, execution_args) = if is_path_like_specifier(entrypoint_specifier) {
5116 let guest_entrypoint = if entrypoint_specifier.starts_with('/') {
5117 normalize_path(entrypoint_specifier)
5118 } else if entrypoint_specifier.starts_with("file:") {
5119 normalize_path(entrypoint_specifier.trim_start_matches("file:"))
5120 } else {
5121 normalize_path(&format!("{guest_cwd}/{entrypoint_specifier}"))
5122 };
5123 let host_entrypoint = if entrypoint_specifier.starts_with("./")
5124 || entrypoint_specifier.starts_with("../")
5125 {
5126 normalize_host_path(&host_cwd.join(entrypoint_specifier))
5127 } else {
5128 host_runtime_path_for_guest_path_with_env(
5129 vm,
5130 &runtime_env,
5131 &guest_entrypoint,
5132 parent_host_cwd,
5133 )
5134 .unwrap_or_else(|| {
5135 let candidate = PathBuf::from(&guest_entrypoint);
5136 if candidate.is_absolute() {
5137 candidate
5138 } else {
5139 host_cwd.join(&guest_entrypoint)
5140 }
5141 })
5142 };
5143 env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
5144 (
5145 host_entrypoint.to_string_lossy().into_owned(),
5146 process_args.iter().skip(1).cloned().collect(),
5147 )
5148 } else {
5149 (
5150 entrypoint_specifier.clone(),
5151 process_args.iter().skip(1).cloned().collect(),
5152 )
5153 };
5154 let guest_entrypoint = env.get("AGENTOS_GUEST_ENTRYPOINT").cloned();
5155 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
5156
5157 return Ok(ResolvedChildProcessExecution {
5158 command: command.clone(),
5159 process_args: std::iter::once(command)
5160 .chain(process_args.iter().cloned())
5161 .collect(),
5162 runtime: GuestRuntimeKind::JavaScript,
5163 entrypoint,
5164 execution_args,
5165 env,
5166 guest_cwd,
5167 host_cwd,
5168 wasm_permission_tier: None,
5169 tool_command: false,
5170 });
5171 }
5172
5173 if command == PYTHON_COMMAND {
5174 return Err(SidecarError::InvalidState(String::from(
5175 "nested python child_process execution is not supported yet",
5176 )));
5177 }
5178
5179 let guest_entrypoint = resolve_guest_command_entrypoint(
5180 vm,
5181 &guest_cwd,
5182 &command,
5183 env.get("PATH").map(String::as_str),
5184 )
5185 .ok_or_else(|| SidecarError::InvalidState(format!("command not found: {command}")))?;
5186 let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
5187 let wasm_permission_tier = vm.command_permissions.get(&command).copied().or_else(|| {
5188 Path::new(&guest_entrypoint)
5189 .file_name()
5190 .and_then(|name| name.to_str())
5191 .and_then(|name| vm.command_permissions.get(name).copied())
5192 });
5193 if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
5194 resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
5195 {
5196 prepare_guest_runtime_env(
5197 vm,
5198 &mut env,
5199 &guest_cwd,
5200 &host_cwd,
5201 Some(javascript_guest_entrypoint),
5202 )?;
5203
5204 return Ok(ResolvedChildProcessExecution {
5205 command: command.clone(),
5206 process_args: std::iter::once(command)
5207 .chain(process_args.iter().cloned())
5208 .collect(),
5209 runtime: GuestRuntimeKind::JavaScript,
5210 entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
5211 execution_args: process_args,
5212 env,
5213 guest_cwd,
5214 host_cwd,
5215 wasm_permission_tier: None,
5216 tool_command: false,
5217 });
5218 }
5219 prepare_guest_runtime_env(
5220 vm,
5221 &mut env,
5222 &guest_cwd,
5223 &host_cwd,
5224 Some(guest_entrypoint.clone()),
5225 )?;
5226
5227 Ok(ResolvedChildProcessExecution {
5228 command: command.clone(),
5229 process_args: std::iter::once(command)
5230 .chain(process_args.iter().cloned())
5231 .collect(),
5232 runtime: GuestRuntimeKind::WebAssembly,
5233 entrypoint: host_entrypoint.to_string_lossy().into_owned(),
5234 execution_args: process_args,
5235 env,
5236 guest_cwd,
5237 host_cwd,
5238 wasm_permission_tier,
5239 tool_command: false,
5240 })
5241 }
5242
5243 pub(crate) fn spawn_javascript_child_process(
5244 &mut self,
5245 vm_id: &str,
5246 process_id: &str,
5247 request: JavascriptChildProcessSpawnRequest,
5248 ) -> Result<Value, SidecarError> {
5249 let resolved = {
5250 let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
5251 let parent = vm
5252 .active_processes
5253 .get(process_id)
5254 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5255 self.resolve_javascript_child_process_execution(
5256 vm,
5257 &parent.env,
5258 &parent.guest_cwd,
5259 &parent.host_cwd,
5260 &request,
5261 )?
5262 };
5263 let (parent_kernel_pid, child_process_id) = {
5264 let vm = self
5265 .vms
5266 .get_mut(vm_id)
5267 .ok_or_else(|| missing_vm_error(vm_id))?;
5268 let process = vm
5269 .active_processes
5270 .get_mut(process_id)
5271 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5272 (process.kernel_pid, process.allocate_child_process_id())
5273 };
5274 let sidecar_requests = self.sidecar_requests.clone();
5275 let vm = self
5276 .vms
5277 .get_mut(vm_id)
5278 .ok_or_else(|| missing_vm_error(vm_id))?;
5279 let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
5280 .tool_command
5281 {
5282 let tool_resolution = resolve_tool_command(
5283 vm,
5284 &resolved.command,
5285 &resolved.execution_args,
5286 Some(&resolved.guest_cwd),
5287 )?
5288 .ok_or_else(|| {
5289 SidecarError::InvalidState(format!(
5290 "tool command no longer resolves: {}",
5291 resolved.command
5292 ))
5293 })?;
5294 let kernel_handle = vm
5295 .kernel
5296 .create_virtual_process(
5297 EXECUTION_DRIVER_NAME,
5298 TOOL_DRIVER_NAME,
5299 &resolved.command,
5300 resolved.process_args.clone(),
5301 VirtualProcessOptions {
5302 parent_pid: Some(parent_kernel_pid),
5303 env: resolved.env.clone(),
5304 cwd: Some(resolved.guest_cwd.clone()),
5305 },
5306 )
5307 .map_err(kernel_error)?;
5308 let kernel_pid = kernel_handle.pid();
5309 let tool_execution = ToolExecution::default();
5310 let cancelled = tool_execution.cancelled.clone();
5311 let pending_events = tool_execution.pending_events.clone();
5312 let events_overflowed = tool_execution.events_overflowed.clone();
5313 spawn_tool_process_events(ToolProcessEventRequest {
5314 sidecar_requests: sidecar_requests.clone(),
5315 connection_id: vm.connection_id.clone(),
5316 session_id: vm.session_id.clone(),
5317 vm_id: vm_id.to_owned(),
5318 tool_resolution,
5319 cancelled,
5320 pending_events,
5321 events_overflowed,
5322 });
5323 (
5324 kernel_pid,
5325 kernel_handle,
5326 ActiveExecution::Tool(tool_execution),
5327 None,
5328 )
5329 } else {
5330 let kernel_command = match resolved.runtime {
5331 GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
5332 GuestRuntimeKind::WebAssembly => WASM_COMMAND,
5333 GuestRuntimeKind::Python => {
5334 unreachable!("python child_process execution is rejected")
5335 }
5336 };
5337 let kernel_handle = vm
5338 .kernel
5339 .spawn_process(
5340 kernel_command,
5341 resolved.process_args.clone(),
5342 SpawnOptions {
5343 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
5344 parent_pid: Some(parent_kernel_pid),
5345 env: resolved.env.clone(),
5346 cwd: Some(resolved.guest_cwd.clone()),
5347 },
5348 )
5349 .map_err(kernel_error)?;
5350 let kernel_pid = kernel_handle.pid();
5351 if request.options.detached {
5352 vm.kernel
5353 .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
5354 .map_err(kernel_error)?;
5355 }
5356 let mut execution_env = resolved.env.clone();
5357 execution_env.insert(
5358 String::from(EXECUTION_SANDBOX_ROOT_ENV),
5359 normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
5360 );
5361
5362 let execution = match resolved.runtime {
5363 GuestRuntimeKind::JavaScript => {
5364 execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
5365 &request.options.internal_bootstrap_env,
5366 ));
5367 execution_env.insert(
5368 String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
5369 String::from("1"),
5370 );
5371 let context =
5372 self.javascript_engine
5373 .create_context(CreateJavascriptContextRequest {
5374 vm_id: vm_id.to_owned(),
5375 bootstrap_module: None,
5376 compile_cache_root: Some(
5377 self.cache_root.join("node-compile-cache"),
5378 ),
5379 });
5380 let inline_code = load_javascript_entrypoint_source(
5381 vm,
5382 &resolved.host_cwd,
5383 &resolved.entrypoint,
5384 &execution_env,
5385 );
5386 prepare_javascript_shadow(vm, &resolved)?;
5387
5388 let built_reader = build_module_reader(vm, &resolved);
5389 let guest_reader = built_reader
5390 .clone()
5391 .map(|reader| {
5392 Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
5393 as Box<dyn GuestModuleReader>
5394 });
5395 let module_reader = built_reader
5396 .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
5397 let execution = self
5398 .javascript_engine
5399 .start_execution_with_module_reader(
5400 StartJavascriptExecutionRequest {
5401 guest_runtime: guest_runtime_identity(
5402 vm,
5403 Some(u64::from(kernel_pid)),
5404 Some(u64::from(parent_kernel_pid)),
5405 ),
5406 vm_id: vm_id.to_owned(),
5407 context_id: context.context_id,
5408 argv: std::iter::once(resolved.entrypoint.clone())
5409 .chain(resolved.execution_args.clone())
5410 .collect(),
5411 env: execution_env,
5412 cwd: resolved.host_cwd.clone(),
5413 limits: javascript_execution_limits(vm),
5414 inline_code,
5415 },
5416 module_reader,
5417 guest_reader,
5418 )
5419 .map_err(javascript_error)?;
5420 ActiveExecution::Javascript(execution)
5421 }
5422 GuestRuntimeKind::WebAssembly => {
5423 execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
5424 let wasm_limits = wasm_execution_limits(vm);
5425 let wasm_guest_runtime = guest_runtime_identity(
5426 vm,
5427 Some(u64::from(kernel_pid)),
5428 Some(u64::from(parent_kernel_pid)),
5429 );
5430 let context = self.wasm_engine.create_context(CreateWasmContextRequest {
5431 vm_id: vm_id.to_owned(),
5432 module_path: Some(resolved.entrypoint.clone()),
5433 });
5434 let execution = self
5435 .wasm_engine
5436 .start_execution(StartWasmExecutionRequest {
5437 vm_id: vm_id.to_owned(),
5438 context_id: context.context_id,
5439 argv: resolved.process_args.clone(),
5440 env: execution_env,
5441 cwd: resolved.host_cwd.clone(),
5442 permission_tier: execution_wasm_permission_tier(
5443 resolved
5444 .wasm_permission_tier
5445 .unwrap_or(WasmPermissionTier::Full),
5446 ),
5447 limits: wasm_limits,
5448 guest_runtime: wasm_guest_runtime,
5449 })
5450 .map_err(wasm_error)?;
5451 ActiveExecution::Wasm(Box::new(execution))
5452 }
5453 GuestRuntimeKind::Python => {
5454 unreachable!("python child_process execution is rejected")
5455 }
5456 };
5457 let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
5458 "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5459 "ignore" => {
5460 vm.kernel
5461 .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
5462 .map_err(kernel_error)?;
5463 None
5464 }
5465 "inherit" => None,
5466 _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5467 };
5468 (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
5469 };
5470
5471 let process = vm
5472 .active_processes
5473 .get_mut(process_id)
5474 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5475 process.child_processes.insert(
5476 child_process_id.clone(),
5477 ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
5478 .with_detached(request.options.detached)
5479 .with_guest_cwd(resolved.guest_cwd.clone())
5480 .with_env(resolved.env.clone())
5481 .with_host_cwd(resolved.host_cwd.clone()),
5482 );
5483 if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
5484 process
5485 .child_processes
5486 .get_mut(&child_process_id)
5487 .ok_or_else(|| {
5488 SidecarError::InvalidState(format!(
5489 "child process {child_process_id} disappeared during spawn"
5490 ))
5491 })?
5492 .kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
5493 }
5494 Ok(json!({
5495 "childId": child_process_id,
5496 "pid": kernel_pid,
5497 "command": resolved.command,
5498 "args": resolved.process_args,
5499 }))
5500 }
5501
5502 pub(crate) fn spawn_javascript_child_process_sync(
5503 &mut self,
5504 vm_id: &str,
5505 process_id: &str,
5506 request: JavascriptChildProcessSpawnRequest,
5507 max_buffer: Option<usize>,
5508 ) -> Result<Value, SidecarError> {
5509 let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
5510 let timeout_deadline = request
5511 .options
5512 .timeout
5513 .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
5514 let timeout_signal = request
5515 .options
5516 .kill_signal
5517 .clone()
5518 .unwrap_or_else(|| String::from("SIGTERM"));
5519 let spawned = self.spawn_javascript_child_process(vm_id, process_id, request)?;
5520 let child_process_id = spawned
5521 .get("childId")
5522 .and_then(Value::as_str)
5523 .ok_or_else(|| {
5524 SidecarError::InvalidState(String::from(
5525 "child_process.spawn_sync response is missing childId",
5526 ))
5527 })?
5528 .to_owned();
5529
5530 if let Some(input) = sync_input.as_deref() {
5531 self.write_javascript_child_process_stdin(vm_id, process_id, &child_process_id, input)?;
5532 }
5533 self.close_javascript_child_process_stdin(vm_id, process_id, &child_process_id)?;
5534
5535 let max_buffer = max_buffer.unwrap_or(1024 * 1024);
5536 let mut stdout = Vec::new();
5537 let mut stderr = Vec::new();
5538 let mut max_buffer_exceeded = false;
5539 let mut kill_sent = false;
5540 let mut timed_out = false;
5541
5542 let exit_code = loop {
5543 let wait_ms = if let Some(deadline) = timeout_deadline {
5544 let now = Instant::now();
5545 if now >= deadline {
5546 if !kill_sent {
5547 timed_out = true;
5548 self.kill_javascript_child_process(
5549 vm_id,
5550 process_id,
5551 &child_process_id,
5552 &timeout_signal,
5553 )?;
5554 kill_sent = true;
5555 }
5556 0
5557 } else {
5558 u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
5559 .unwrap_or(50)
5560 }
5561 } else {
5562 50
5563 };
5564 let event =
5565 self.poll_javascript_child_process(vm_id, process_id, &child_process_id, wait_ms)?;
5566 if event.is_null() {
5567 continue;
5568 }
5569
5570 match event.get("type").and_then(Value::as_str) {
5571 Some("stdout") => {
5572 let chunk = javascript_sync_rpc_bytes_arg(
5573 &[event.get("data").cloned().unwrap_or(Value::Null)],
5574 0,
5575 "child_process.spawn_sync stdout",
5576 )?;
5577 stdout.extend_from_slice(&chunk);
5578 if stdout.len() > max_buffer && !kill_sent {
5579 max_buffer_exceeded = true;
5580 self.kill_javascript_child_process(
5581 vm_id,
5582 process_id,
5583 &child_process_id,
5584 "SIGTERM",
5585 )?;
5586 kill_sent = true;
5587 }
5588 }
5589 Some("stderr") => {
5590 let chunk = javascript_sync_rpc_bytes_arg(
5591 &[event.get("data").cloned().unwrap_or(Value::Null)],
5592 0,
5593 "child_process.spawn_sync stderr",
5594 )?;
5595 stderr.extend_from_slice(&chunk);
5596 if stderr.len() > max_buffer && !kill_sent {
5597 max_buffer_exceeded = true;
5598 self.kill_javascript_child_process(
5599 vm_id,
5600 process_id,
5601 &child_process_id,
5602 "SIGTERM",
5603 )?;
5604 kill_sent = true;
5605 }
5606 }
5607 Some("exit") => {
5608 break event
5609 .get("exitCode")
5610 .and_then(Value::as_i64)
5611 .map(|value| value as i32)
5612 .unwrap_or(1);
5613 }
5614 _ => {}
5615 }
5616 };
5617
5618 Ok(json!({
5619 "stdout": String::from_utf8_lossy(&stdout),
5620 "stderr": String::from_utf8_lossy(&stderr),
5621 "code": exit_code,
5622 "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
5623 "timedOut": timed_out,
5624 "maxBufferExceeded": max_buffer_exceeded,
5625 }))
5626 }
5627
5628 fn spawn_descendant_javascript_child_process(
5629 &mut self,
5630 vm_id: &str,
5631 process_id: &str,
5632 current_process_path: &[&str],
5633 request: JavascriptChildProcessSpawnRequest,
5634 ) -> Result<Value, SidecarError> {
5635 let current_process_label =
5636 Self::child_process_path_label(process_id, current_process_path);
5637 let (resolved, parent_kernel_pid) = {
5638 let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
5639 let root = vm
5640 .active_processes
5641 .get(process_id)
5642 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5643 let parent =
5644 Self::active_process_by_path(root, current_process_path).ok_or_else(|| {
5645 SidecarError::InvalidState(format!(
5646 "unknown child process path {current_process_label} during nested spawn"
5647 ))
5648 })?;
5649 (
5650 self.resolve_javascript_child_process_execution(
5651 vm,
5652 &parent.env,
5653 &parent.guest_cwd,
5654 &parent.host_cwd,
5655 &request,
5656 )?,
5657 parent.kernel_pid,
5658 )
5659 };
5660
5661 let sidecar_requests = self.sidecar_requests.clone();
5662 let vm = self
5663 .vms
5664 .get_mut(vm_id)
5665 .ok_or_else(|| missing_vm_error(vm_id))?;
5666 let child_process_id = {
5667 let root = vm
5668 .active_processes
5669 .get_mut(process_id)
5670 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5671 let parent =
5672 Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
5673 SidecarError::InvalidState(format!(
5674 "unknown child process path {current_process_label} during nested spawn"
5675 ))
5676 })?;
5677 parent.allocate_child_process_id()
5678 };
5679 let mut child_path = current_process_path.to_vec();
5680 child_path.push(child_process_id.as_str());
5681 let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
5682 .tool_command
5683 {
5684 let tool_resolution = resolve_tool_command(
5685 vm,
5686 &resolved.command,
5687 &resolved.execution_args,
5688 Some(&resolved.guest_cwd),
5689 )?
5690 .ok_or_else(|| {
5691 SidecarError::InvalidState(format!(
5692 "tool command no longer resolves: {}",
5693 resolved.command
5694 ))
5695 })?;
5696 let kernel_handle = vm
5697 .kernel
5698 .create_virtual_process(
5699 EXECUTION_DRIVER_NAME,
5700 TOOL_DRIVER_NAME,
5701 &resolved.command,
5702 resolved.process_args.clone(),
5703 VirtualProcessOptions {
5704 parent_pid: Some(parent_kernel_pid),
5705 env: resolved.env.clone(),
5706 cwd: Some(resolved.guest_cwd.clone()),
5707 },
5708 )
5709 .map_err(kernel_error)?;
5710 let kernel_pid = kernel_handle.pid();
5711 let tool_execution = ToolExecution::default();
5712 let cancelled = tool_execution.cancelled.clone();
5713 let pending_events = tool_execution.pending_events.clone();
5714 let events_overflowed = tool_execution.events_overflowed.clone();
5715 spawn_tool_process_events(ToolProcessEventRequest {
5716 sidecar_requests: sidecar_requests.clone(),
5717 connection_id: vm.connection_id.clone(),
5718 session_id: vm.session_id.clone(),
5719 vm_id: vm_id.to_owned(),
5720 tool_resolution,
5721 cancelled,
5722 pending_events,
5723 events_overflowed,
5724 });
5725 (
5726 kernel_pid,
5727 kernel_handle,
5728 ActiveExecution::Tool(tool_execution),
5729 None,
5730 )
5731 } else {
5732 let kernel_command = match resolved.runtime {
5733 GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
5734 GuestRuntimeKind::WebAssembly => WASM_COMMAND,
5735 GuestRuntimeKind::Python => {
5736 unreachable!("python child_process execution is rejected")
5737 }
5738 };
5739 let kernel_handle = vm
5740 .kernel
5741 .spawn_process(
5742 kernel_command,
5743 resolved.process_args.clone(),
5744 SpawnOptions {
5745 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
5746 parent_pid: Some(parent_kernel_pid),
5747 env: resolved.env.clone(),
5748 cwd: Some(resolved.guest_cwd.clone()),
5749 },
5750 )
5751 .map_err(kernel_error)?;
5752 let kernel_pid = kernel_handle.pid();
5753 if request.options.detached {
5754 vm.kernel
5755 .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
5756 .map_err(kernel_error)?;
5757 }
5758 let mut execution_env = resolved.env.clone();
5759 execution_env.insert(
5760 String::from(EXECUTION_SANDBOX_ROOT_ENV),
5761 normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
5762 );
5763 let execution = match resolved.runtime {
5764 GuestRuntimeKind::JavaScript => {
5765 execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
5766 &request.options.internal_bootstrap_env,
5767 ));
5768 execution_env.insert(
5769 String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
5770 String::from("1"),
5771 );
5772 let context =
5773 self.javascript_engine
5774 .create_context(CreateJavascriptContextRequest {
5775 vm_id: vm_id.to_owned(),
5776 bootstrap_module: None,
5777 compile_cache_root: Some(
5778 self.cache_root.join("node-compile-cache"),
5779 ),
5780 });
5781 let inline_code = load_javascript_entrypoint_source(
5782 vm,
5783 &resolved.host_cwd,
5784 &resolved.entrypoint,
5785 &execution_env,
5786 );
5787 prepare_javascript_shadow(vm, &resolved)?;
5788
5789 let built_reader = build_module_reader(vm, &resolved);
5790 let guest_reader = built_reader
5791 .clone()
5792 .map(|reader| {
5793 Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
5794 as Box<dyn GuestModuleReader>
5795 });
5796 let module_reader = built_reader
5797 .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
5798 let execution = self
5799 .javascript_engine
5800 .start_execution_with_module_reader(
5801 StartJavascriptExecutionRequest {
5802 guest_runtime: guest_runtime_identity(
5803 vm,
5804 Some(u64::from(kernel_pid)),
5805 Some(u64::from(parent_kernel_pid)),
5806 ),
5807 vm_id: vm_id.to_owned(),
5808 context_id: context.context_id,
5809 argv: std::iter::once(resolved.entrypoint.clone())
5810 .chain(resolved.execution_args.clone())
5811 .collect(),
5812 env: execution_env,
5813 cwd: resolved.host_cwd.clone(),
5814 limits: javascript_execution_limits(vm),
5815 inline_code,
5816 },
5817 module_reader,
5818 guest_reader,
5819 )
5820 .map_err(javascript_error)?;
5821 ActiveExecution::Javascript(execution)
5822 }
5823 GuestRuntimeKind::WebAssembly => {
5824 execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
5825 let wasm_limits = wasm_execution_limits(vm);
5826 let wasm_guest_runtime = guest_runtime_identity(
5827 vm,
5828 Some(u64::from(kernel_pid)),
5829 Some(u64::from(parent_kernel_pid)),
5830 );
5831 let context = self.wasm_engine.create_context(CreateWasmContextRequest {
5832 vm_id: vm_id.to_owned(),
5833 module_path: Some(resolved.entrypoint.clone()),
5834 });
5835 let execution = self
5836 .wasm_engine
5837 .start_execution(StartWasmExecutionRequest {
5838 vm_id: vm_id.to_owned(),
5839 context_id: context.context_id,
5840 argv: resolved.process_args.clone(),
5841 env: execution_env,
5842 cwd: resolved.host_cwd.clone(),
5843 permission_tier: execution_wasm_permission_tier(
5844 resolved
5845 .wasm_permission_tier
5846 .unwrap_or(WasmPermissionTier::Full),
5847 ),
5848 limits: wasm_limits,
5849 guest_runtime: wasm_guest_runtime,
5850 })
5851 .map_err(wasm_error)?;
5852 ActiveExecution::Wasm(Box::new(execution))
5853 }
5854 GuestRuntimeKind::Python => {
5855 unreachable!("python child_process execution is rejected")
5856 }
5857 };
5858 let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
5859 "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5860 "ignore" => {
5861 vm.kernel
5862 .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
5863 .map_err(kernel_error)?;
5864 None
5865 }
5866 "inherit" => None,
5867 _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5868 };
5869 (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
5870 };
5871
5872 let root = vm
5873 .active_processes
5874 .get_mut(process_id)
5875 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5876 let parent =
5877 Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
5878 SidecarError::InvalidState(format!(
5879 "unknown child process path {current_process_label} during nested spawn"
5880 ))
5881 })?;
5882 parent.child_processes.insert(
5883 child_process_id.clone(),
5884 ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
5885 .with_detached(request.options.detached)
5886 .with_guest_cwd(resolved.guest_cwd.clone())
5887 .with_env(resolved.env.clone())
5888 .with_host_cwd(resolved.host_cwd.clone()),
5889 );
5890 if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
5891 parent
5892 .child_processes
5893 .get_mut(&child_process_id)
5894 .ok_or_else(|| {
5895 SidecarError::InvalidState(format!(
5896 "child process {child_process_id} disappeared during nested spawn"
5897 ))
5898 })?
5899 .kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
5900 }
5901 Ok(json!({
5902 "childId": child_process_id,
5903 "pid": kernel_pid,
5904 "command": resolved.command,
5905 "args": resolved.process_args,
5906 }))
5907 }
5908
5909 fn spawn_descendant_javascript_child_process_sync(
5910 &mut self,
5911 vm_id: &str,
5912 process_id: &str,
5913 current_process_path: &[&str],
5914 request: JavascriptChildProcessSpawnRequest,
5915 max_buffer: Option<usize>,
5916 ) -> Result<Value, SidecarError> {
5917 let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
5918 let timeout_deadline = request
5919 .options
5920 .timeout
5921 .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
5922 let timeout_signal = request
5923 .options
5924 .kill_signal
5925 .clone()
5926 .unwrap_or_else(|| String::from("SIGTERM"));
5927 let spawned = self.spawn_descendant_javascript_child_process(
5928 vm_id,
5929 process_id,
5930 current_process_path,
5931 request,
5932 )?;
5933 let child_process_id = spawned
5934 .get("childId")
5935 .and_then(Value::as_str)
5936 .ok_or_else(|| {
5937 SidecarError::InvalidState(String::from(
5938 "child_process.spawn_sync response is missing childId",
5939 ))
5940 })?
5941 .to_owned();
5942
5943 if let Some(input) = sync_input.as_deref() {
5944 self.write_descendant_javascript_child_process_stdin(
5945 vm_id,
5946 process_id,
5947 current_process_path,
5948 &child_process_id,
5949 input,
5950 )?;
5951 }
5952 self.close_descendant_javascript_child_process_stdin(
5953 vm_id,
5954 process_id,
5955 current_process_path,
5956 &child_process_id,
5957 )?;
5958
5959 let max_buffer = max_buffer.unwrap_or(1024 * 1024);
5960 let mut stdout = Vec::new();
5961 let mut stderr = Vec::new();
5962 let mut max_buffer_exceeded = false;
5963 let mut kill_sent = false;
5964 let mut timed_out = false;
5965
5966 let exit_code = loop {
5967 let wait_ms = if let Some(deadline) = timeout_deadline {
5968 let now = Instant::now();
5969 if now >= deadline {
5970 if !kill_sent {
5971 timed_out = true;
5972 self.kill_descendant_javascript_child_process(
5973 vm_id,
5974 process_id,
5975 current_process_path,
5976 &child_process_id,
5977 &timeout_signal,
5978 )?;
5979 kill_sent = true;
5980 }
5981 0
5982 } else {
5983 u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
5984 .unwrap_or(50)
5985 }
5986 } else {
5987 50
5988 };
5989 let event = self.poll_descendant_javascript_child_process(
5990 vm_id,
5991 process_id,
5992 current_process_path,
5993 &child_process_id,
5994 wait_ms,
5995 )?;
5996 if event.is_null() {
5997 continue;
5998 }
5999
6000 match event.get("type").and_then(Value::as_str) {
6001 Some("stdout") => {
6002 let chunk = javascript_sync_rpc_bytes_arg(
6003 &[event.get("data").cloned().unwrap_or(Value::Null)],
6004 0,
6005 "child_process.spawn_sync stdout",
6006 )?;
6007 stdout.extend_from_slice(&chunk);
6008 if stdout.len() > max_buffer && !kill_sent {
6009 max_buffer_exceeded = true;
6010 self.kill_descendant_javascript_child_process(
6011 vm_id,
6012 process_id,
6013 current_process_path,
6014 &child_process_id,
6015 "SIGTERM",
6016 )?;
6017 kill_sent = true;
6018 }
6019 }
6020 Some("stderr") => {
6021 let chunk = javascript_sync_rpc_bytes_arg(
6022 &[event.get("data").cloned().unwrap_or(Value::Null)],
6023 0,
6024 "child_process.spawn_sync stderr",
6025 )?;
6026 stderr.extend_from_slice(&chunk);
6027 if stderr.len() > max_buffer && !kill_sent {
6028 max_buffer_exceeded = true;
6029 self.kill_descendant_javascript_child_process(
6030 vm_id,
6031 process_id,
6032 current_process_path,
6033 &child_process_id,
6034 "SIGTERM",
6035 )?;
6036 kill_sent = true;
6037 }
6038 }
6039 Some("exit") => {
6040 break event
6041 .get("exitCode")
6042 .and_then(Value::as_i64)
6043 .map(|value| value as i32)
6044 .unwrap_or(1);
6045 }
6046 _ => {}
6047 }
6048 };
6049
6050 Ok(json!({
6051 "stdout": String::from_utf8_lossy(&stdout),
6052 "stderr": String::from_utf8_lossy(&stderr),
6053 "code": exit_code,
6054 "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
6055 "timedOut": timed_out,
6056 "maxBufferExceeded": max_buffer_exceeded,
6057 }))
6058 }
6059
6060 fn handle_descendant_javascript_child_process_rpc(
6061 &mut self,
6062 vm_id: &str,
6063 process_id: &str,
6064 current_process_path: &[&str],
6065 request: &JavascriptSyncRpcRequest,
6066 ) -> Result<Value, SidecarError> {
6067 match request.method.as_str() {
6068 "child_process.spawn" => {
6069 let Some(vm) = self.vms.get(vm_id) else {
6070 return Ok(Value::Null);
6071 };
6072 let (payload, _) = parse_javascript_child_process_spawn_request(vm, &request.args)?;
6073 self.spawn_descendant_javascript_child_process(
6074 vm_id,
6075 process_id,
6076 current_process_path,
6077 payload,
6078 )
6079 }
6080 "child_process.spawn_sync" => {
6081 let Some(vm) = self.vms.get(vm_id) else {
6082 return Ok(Value::Null);
6083 };
6084 let (payload, max_buffer) =
6085 parse_javascript_child_process_spawn_request(vm, &request.args)?;
6086 self.spawn_descendant_javascript_child_process_sync(
6087 vm_id,
6088 process_id,
6089 current_process_path,
6090 payload,
6091 max_buffer,
6092 )
6093 }
6094 "child_process.poll" => {
6095 let child_process_id =
6096 javascript_sync_rpc_arg_str(&request.args, 0, "child_process.poll child id")?;
6097 let wait_ms = javascript_sync_rpc_arg_u64_optional(
6098 &request.args,
6099 1,
6100 "child_process.poll wait ms",
6101 )?
6102 .unwrap_or_default();
6103 self.poll_descendant_javascript_child_process(
6104 vm_id,
6105 process_id,
6106 current_process_path,
6107 child_process_id,
6108 wait_ms,
6109 )
6110 }
6111 "child_process.write_stdin" => {
6112 let child_process_id = javascript_sync_rpc_arg_str(
6113 &request.args,
6114 0,
6115 "child_process.write_stdin child id",
6116 )?;
6117 let chunk = javascript_sync_rpc_bytes_arg(
6118 &request.args,
6119 1,
6120 "child_process.write_stdin chunk",
6121 )?;
6122 self.write_descendant_javascript_child_process_stdin(
6123 vm_id,
6124 process_id,
6125 current_process_path,
6126 child_process_id,
6127 &chunk,
6128 )?;
6129 Ok(Value::Null)
6130 }
6131 "child_process.close_stdin" => {
6132 let child_process_id = javascript_sync_rpc_arg_str(
6133 &request.args,
6134 0,
6135 "child_process.close_stdin child id",
6136 )?;
6137 self.close_descendant_javascript_child_process_stdin(
6138 vm_id,
6139 process_id,
6140 current_process_path,
6141 child_process_id,
6142 )?;
6143 Ok(Value::Null)
6144 }
6145 "child_process.kill" => {
6146 let child_process_id =
6147 javascript_sync_rpc_arg_str(&request.args, 0, "child_process.kill child id")?;
6148 let signal =
6149 javascript_sync_rpc_arg_str(&request.args, 1, "child_process.kill signal")?;
6150 self.kill_descendant_javascript_child_process(
6151 vm_id,
6152 process_id,
6153 current_process_path,
6154 child_process_id,
6155 signal,
6156 )?;
6157 Ok(Value::Null)
6158 }
6159 _ => Err(SidecarError::InvalidState(format!(
6160 "unsupported nested child process RPC method {}",
6161 request.method
6162 ))),
6163 }
6164 }
6165
6166 fn poll_descendant_javascript_child_process(
6167 &mut self,
6168 vm_id: &str,
6169 process_id: &str,
6170 current_process_path: &[&str],
6171 child_process_id: &str,
6172 wait_ms: u64,
6173 ) -> Result<Value, SidecarError> {
6174 let mut child_path = current_process_path.to_vec();
6175 child_path.push(child_process_id);
6176 let child_gone_error = || javascript_child_process_gone_error(process_id, &child_path);
6177 let deadline = Instant::now() + Duration::from_millis(wait_ms);
6178 let mut polled_once = false;
6179
6180 loop {
6181 self.drain_queued_descendant_javascript_child_process_events(
6182 vm_id,
6183 process_id,
6184 &child_path,
6185 )?;
6186 enum ChildPollResult {
6187 Event(Box<Option<ActiveExecutionEvent>>),
6188 RecoverRuntimeExit,
6189 Timeout,
6190 }
6191 let wait = if wait_ms == 0 {
6192 Duration::ZERO
6193 } else {
6194 deadline.saturating_duration_since(Instant::now())
6195 };
6196 let poll_result = {
6197 let Some(vm) = self.vms.get_mut(vm_id) else {
6198 return Ok(Value::Null);
6199 };
6200 let Some(parent) =
6201 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6202 else {
6203 return Err(child_gone_error());
6204 };
6205 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6206 return Err(child_gone_error());
6207 };
6208 if let Some(event) = child.pending_execution_events.pop_front() {
6209 ChildPollResult::Event(Box::new(Some(event)))
6210 } else if polled_once && wait.is_zero() {
6211 ChildPollResult::Timeout
6212 } else {
6213 polled_once = true;
6214 match child.execution.poll_event_blocking(wait) {
6215 Ok(Some(event)) => ChildPollResult::Event(Box::new(Some(event))),
6216 Ok(None) => ChildPollResult::RecoverRuntimeExit,
6217 Err(SidecarError::Execution(message))
6218 if (child.runtime == GuestRuntimeKind::JavaScript
6219 && closed_javascript_event_channel(&message))
6220 || (child.runtime == GuestRuntimeKind::Python
6221 && closed_python_event_channel(&message))
6222 || (child.runtime == GuestRuntimeKind::WebAssembly
6223 && closed_wasm_event_channel(&message)) =>
6224 {
6225 ChildPollResult::RecoverRuntimeExit
6226 }
6227 Err(error) => return Err(error),
6228 }
6229 }
6230 };
6231 let event = match poll_result {
6232 ChildPollResult::Event(event) => *event,
6233 ChildPollResult::Timeout => return Ok(Value::Null),
6234 ChildPollResult::RecoverRuntimeExit => self
6235 .recover_descendant_runtime_child_process_event(
6236 vm_id,
6237 process_id,
6238 current_process_path,
6239 child_process_id,
6240 wait.as_millis().try_into().unwrap_or(u64::MAX),
6241 )?,
6242 };
6243
6244 let Some(event) = event else {
6245 return Ok(Value::Null);
6246 };
6247
6248 match event {
6249 ActiveExecutionEvent::Stdout(chunk) => {
6250 return Ok(json!({
6251 "type": "stdout",
6252 "data": javascript_sync_rpc_bytes_value(&chunk),
6253 }));
6254 }
6255 ActiveExecutionEvent::Stderr(chunk) => {
6256 return Ok(json!({
6257 "type": "stderr",
6258 "data": javascript_sync_rpc_bytes_value(&chunk),
6259 }));
6260 }
6261 ActiveExecutionEvent::Exited(exit_code) => {
6262 let had_trailing_events = {
6263 let Some(vm) = self.vms.get_mut(vm_id) else {
6264 return Ok(Value::Null);
6265 };
6266 let Some(parent) = Self::descendant_parent_process_mut(
6267 vm,
6268 process_id,
6269 current_process_path,
6270 ) else {
6271 return Ok(Value::Null);
6272 };
6273 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6274 return Ok(Value::Null);
6275 };
6276 let deadline = Instant::now() + Duration::from_millis(150);
6277 loop {
6278 let wait = deadline.saturating_duration_since(Instant::now());
6279 let next = poll_child_execution_after_exit(child, wait)?;
6280 let Some(next) = next else {
6281 break;
6282 };
6283 if matches!(next, ActiveExecutionEvent::Exited(_)) {
6284 continue;
6285 }
6286 child.queue_pending_execution_event(next)?;
6287 if Instant::now() >= deadline {
6288 break;
6289 }
6290 }
6291 if !child.pending_execution_events.is_empty() {
6292 child.queue_pending_execution_event(ActiveExecutionEvent::Exited(
6293 exit_code,
6294 ))?;
6295 true
6296 } else {
6297 false
6298 }
6299 };
6300 if had_trailing_events {
6301 continue;
6302 }
6303
6304 let parent_signal_key =
6305 Self::child_process_signal_key(process_id, current_process_path);
6306 let Some(vm) = self.vms.get_mut(vm_id) else {
6307 return Ok(Value::Null);
6308 };
6309 let signal_name = {
6310 let Some(parent) = Self::descendant_parent_process_mut(
6311 vm,
6312 process_id,
6313 current_process_path,
6314 ) else {
6315 return Ok(Value::Null);
6316 };
6317 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6318 return Ok(Value::Null);
6319 };
6320 child.pending_self_signal_exit.take().and_then(|signal| {
6321 if exit_code == 128 + signal {
6322 canonical_signal_name(signal).map(str::to_owned)
6323 } else {
6324 None
6325 }
6326 })
6327 };
6328 let (parent_runtime_pid, parent_v8_signal_session, should_signal_parent) = {
6329 let Some(parent) =
6330 Self::descendant_parent_process(vm, process_id, current_process_path)
6331 else {
6332 return Ok(Value::Null);
6333 };
6334 (
6335 parent.execution.child_pid(),
6336 parent.execution.javascript_v8_session_handle().filter(|_| {
6337 matches!(
6338 &parent.execution,
6339 ActiveExecution::Javascript(execution)
6340 if execution.uses_shared_v8_runtime()
6341 )
6342 }),
6343 vm.signal_states
6344 .get(parent_signal_key)
6345 .and_then(|handlers| handlers.get(&(libc::SIGCHLD as u32)))
6346 .is_some_and(|registration| {
6347 registration.action != SignalDispositionAction::Default
6348 }),
6349 )
6350 };
6351 let Some(parent) =
6352 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6353 else {
6354 return Ok(Value::Null);
6355 };
6356 let Some(mut child) = parent.child_processes.remove(child_process_id) else {
6357 return Ok(Value::Null);
6358 };
6359 let child_process_label =
6360 Self::child_process_path_label(process_id, &child_path);
6361 let detached_children =
6362 Self::adopt_detached_child_processes(&child_process_label, &mut child);
6363 sync_process_host_writes_to_kernel(vm, &child)?;
6364 terminate_child_process_tree(&mut vm.kernel, &mut child);
6365 child.kernel_handle.finish(exit_code);
6366 let _ = vm.kernel.wait_and_reap(child.kernel_pid);
6367 vm.signal_states.remove(child_process_id);
6368 for (detached_process_id, detached_child) in detached_children {
6369 vm.detached_child_processes
6370 .insert(detached_process_id.clone());
6371 vm.active_processes
6372 .insert(detached_process_id, detached_child);
6373 }
6374 if should_signal_parent {
6375 if let Some(session) = parent_v8_signal_session {
6376 dispatch_v8_session_signal_async(session, libc::SIGCHLD);
6377 } else {
6378 signal_runtime_process(parent_runtime_pid, libc::SIGCHLD)?;
6379 }
6380 }
6381 let mut payload = Map::new();
6382 payload.insert(String::from("type"), Value::String(String::from("exit")));
6383 payload.insert(String::from("exitCode"), Value::from(exit_code));
6384 if let Some(signal_name) = signal_name {
6385 payload.insert(String::from("signal"), Value::String(signal_name));
6386 }
6387 return Ok(Value::Object(payload));
6388 }
6389 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
6390 let mut current_child_path = current_process_path.to_vec();
6391 current_child_path.push(child_process_id);
6392 let response = if request.method == "process.signal_state" {
6393 let (signal, registration) =
6394 parse_process_signal_state_request(&request.args)?;
6395 let Some(vm) = self.vms.get_mut(vm_id) else {
6396 return Ok(Value::Null);
6397 };
6398 let signal_key =
6399 Self::child_process_signal_key(process_id, ¤t_child_path)
6400 .to_owned();
6401 apply_process_signal_state_update(
6402 &mut vm.signal_states,
6403 &signal_key,
6404 signal,
6405 registration,
6406 );
6407 Ok(Value::Null)
6408 } else if request.method == "process.kill" {
6409 self.handle_descendant_process_kill_rpc(
6410 vm_id,
6411 process_id,
6412 current_process_path,
6413 child_process_id,
6414 &request,
6415 )
6416 } else if request.method.starts_with("child_process.") {
6417 self.handle_descendant_javascript_child_process_rpc(
6418 vm_id,
6419 process_id,
6420 ¤t_child_path,
6421 &request,
6422 )
6423 } else {
6424 let Some(vm) = self.vms.get_mut(vm_id) else {
6425 return Ok(Value::Null);
6426 };
6427 let resource_limits = vm.kernel.resource_limits().clone();
6428 let network_counts = vm_network_resource_counts(vm);
6429 let socket_paths = build_javascript_socket_path_context(vm)?;
6430 let Some(root) = vm.active_processes.get_mut(process_id) else {
6431 return Ok(Value::Null);
6432 };
6433 let Some(parent) =
6434 Self::active_process_by_path_mut(root, current_process_path)
6435 else {
6436 return Ok(Value::Null);
6437 };
6438 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6439 return Ok(Value::Null);
6440 };
6441 service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
6442 bridge: &self.bridge,
6443 vm_id,
6444 dns: &vm.dns,
6445 socket_paths: &socket_paths,
6446 kernel: &mut vm.kernel,
6447 process: child,
6448 sync_request: &request,
6449 resource_limits: &resource_limits,
6450 network_counts,
6451 })
6452 };
6453
6454 let Some(vm) = self.vms.get_mut(vm_id) else {
6455 return Ok(Value::Null);
6456 };
6457 let Some(parent) =
6458 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6459 else {
6460 return Ok(Value::Null);
6461 };
6462 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6463 return Ok(Value::Null);
6464 };
6465 let parent_signal_event = response.as_ref().ok().and_then(|result| {
6466 let target_path_label =
6467 Self::child_process_path_label(process_id, current_process_path);
6468 if request.method != "process.kill"
6469 || result.get("action").and_then(Value::as_str) != Some("user")
6470 || result.get("targetProcessPath").and_then(Value::as_str)
6471 != Some(target_path_label.as_str())
6472 {
6473 return None;
6474 }
6475 Some(json!({
6476 "type": "signal",
6477 "signal": result.get("signal").and_then(Value::as_str).unwrap_or_default(),
6478 "number": result.get("number").and_then(Value::as_i64).unwrap_or_default(),
6479 }))
6480 });
6481 match response {
6482 Ok(result) => child
6483 .execution
6484 .respond_javascript_sync_rpc_success(request.id, result)
6485 .or_else(ignore_stale_javascript_sync_rpc_response)?,
6486 Err(error) => child
6487 .execution
6488 .respond_javascript_sync_rpc_error(
6489 request.id,
6490 javascript_sync_rpc_error_code(&error),
6491 error.to_string(),
6492 )
6493 .or_else(ignore_stale_javascript_sync_rpc_response)?,
6494 }
6495 if let Some(event) = parent_signal_event {
6496 return Ok(event);
6497 }
6498 }
6499 ActiveExecutionEvent::PythonVfsRpcRequest(_) => {
6500 return Err(SidecarError::InvalidState(String::from(
6501 "nested Python child_process execution is not supported yet",
6502 )));
6503 }
6504 ActiveExecutionEvent::SignalState {
6505 signal,
6506 registration,
6507 } => {
6508 let Some(vm) = self.vms.get_mut(vm_id) else {
6509 return Ok(Value::Null);
6510 };
6511 let signal_key =
6512 Self::child_process_signal_key(process_id, &child_path).to_owned();
6513 apply_process_signal_state_update(
6514 &mut vm.signal_states,
6515 &signal_key,
6516 signal,
6517 registration.clone(),
6518 );
6519 return Ok(json!({
6520 "type": "signal_state",
6521 "signal": signal,
6522 "registration": registration,
6523 }));
6524 }
6525 }
6526 }
6527 }
6528
6529 fn recover_descendant_runtime_child_process_event(
6530 &mut self,
6531 vm_id: &str,
6532 process_id: &str,
6533 current_process_path: &[&str],
6534 child_process_id: &str,
6535 wait_ms: u64,
6536 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
6537 let (
6538 parent_kernel_pid,
6539 child_kernel_pid,
6540 child_runtime_pid,
6541 child_runtime,
6542 child_shared_runtime,
6543 ) = {
6544 let mut child_path = current_process_path.to_vec();
6545 child_path.push(child_process_id);
6546 let Some(vm) = self.vms.get_mut(vm_id) else {
6547 return Ok(None);
6548 };
6549 let Some(parent) =
6550 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6551 else {
6552 return Err(javascript_child_process_gone_error(process_id, &child_path));
6553 };
6554 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6555 return Err(javascript_child_process_gone_error(process_id, &child_path));
6556 };
6557 (
6558 parent.kernel_pid,
6559 child.kernel_pid,
6560 child.execution.child_pid(),
6561 child.runtime.clone(),
6562 child.execution.uses_shared_v8_runtime(),
6563 )
6564 };
6565 if child_runtime != GuestRuntimeKind::JavaScript
6566 && child_runtime != GuestRuntimeKind::Python
6567 && child_runtime != GuestRuntimeKind::WebAssembly
6568 {
6569 return Ok(None);
6570 }
6571 let wait_deadline = Instant::now() + Duration::from_millis(wait_ms.min(25));
6572 loop {
6573 let Some(vm) = self.vms.get_mut(vm_id) else {
6574 return Ok(None);
6575 };
6576 if let Some(process_info) = vm.kernel.list_processes().get(&child_kernel_pid) {
6577 if process_info.status == ProcessStatus::Exited {
6578 return Ok(Some(ActiveExecutionEvent::Exited(
6579 process_info.exit_code.unwrap_or(0),
6580 )));
6581 }
6582 }
6583 if let Some(wait_result) = vm
6584 .kernel
6585 .waitpid_with_options(
6586 EXECUTION_DRIVER_NAME,
6587 parent_kernel_pid,
6588 child_kernel_pid as i32,
6589 WaitPidFlags::WNOHANG,
6590 )
6591 .map_err(kernel_error)?
6592 {
6593 return Ok(Some(ActiveExecutionEvent::Exited(wait_result.status)));
6594 }
6595
6596 if !child_shared_runtime && child_runtime_pid != 0 {
6597 if let Some(status) = runtime_child_exit_status(child_runtime_pid)? {
6598 return Ok(Some(ActiveExecutionEvent::Exited(status)));
6599 }
6600 if !runtime_child_is_alive(child_runtime_pid)? {
6601 return Ok(Some(ActiveExecutionEvent::Exited(0)));
6602 }
6603 }
6604 if Instant::now() >= wait_deadline {
6605 return Ok(None);
6606 }
6607 std::thread::sleep(Duration::from_millis(5));
6608 }
6609 }
6610
6611 fn write_descendant_javascript_child_process_stdin(
6612 &mut self,
6613 vm_id: &str,
6614 process_id: &str,
6615 current_process_path: &[&str],
6616 child_process_id: &str,
6617 chunk: &[u8],
6618 ) -> Result<(), SidecarError> {
6619 let mut child_path = current_process_path.to_vec();
6620 child_path.push(child_process_id);
6621 let Some(vm) = self.vms.get_mut(vm_id) else {
6622 return Err(javascript_child_process_gone_error(process_id, &child_path));
6623 };
6624 let Some(root) = vm.active_processes.get_mut(process_id) else {
6625 return Err(javascript_child_process_gone_error(process_id, &child_path));
6626 };
6627 let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6628 return Err(javascript_child_process_gone_error(process_id, &child_path));
6629 };
6630 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6631 return Err(javascript_child_process_gone_error(process_id, &child_path));
6632 };
6633 if let Err(error) = child.execution.write_stdin(chunk) {
6634 if is_broken_pipe_error(&error) {
6635 return Ok(());
6636 }
6637 return Err(error);
6638 }
6639 write_kernel_process_stdin(&mut vm.kernel, child, chunk)
6640 }
6641
6642 fn close_descendant_javascript_child_process_stdin(
6643 &mut self,
6644 vm_id: &str,
6645 process_id: &str,
6646 current_process_path: &[&str],
6647 child_process_id: &str,
6648 ) -> Result<(), SidecarError> {
6649 let mut child_path = current_process_path.to_vec();
6650 child_path.push(child_process_id);
6651 let Some(vm) = self.vms.get_mut(vm_id) else {
6652 return Err(javascript_child_process_gone_error(process_id, &child_path));
6653 };
6654 let Some(root) = vm.active_processes.get_mut(process_id) else {
6655 return Err(javascript_child_process_gone_error(process_id, &child_path));
6656 };
6657 let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6658 return Err(javascript_child_process_gone_error(process_id, &child_path));
6659 };
6660 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6661 return Err(javascript_child_process_gone_error(process_id, &child_path));
6662 };
6663 child.execution.close_stdin()?;
6664 close_kernel_process_stdin(&mut vm.kernel, child)
6665 }
6666
6667 fn kill_descendant_javascript_child_process(
6668 &mut self,
6669 vm_id: &str,
6670 process_id: &str,
6671 current_process_path: &[&str],
6672 child_process_id: &str,
6673 signal: &str,
6674 ) -> Result<(), SidecarError> {
6675 let signal_name = signal.to_owned();
6676 let signal = parse_signal(signal)?;
6677 let Some(vm) = self.vms.get_mut(vm_id) else {
6678 return Ok(());
6679 };
6680 let Some(root) = vm.active_processes.get_mut(process_id) else {
6681 return Ok(());
6682 };
6683 let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6684 return Ok(());
6685 };
6686 let source_pid = parent.kernel_pid;
6687 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6688 return Ok(());
6689 };
6690 terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
6691 let child_process_label = if current_process_path.is_empty() {
6692 child_process_id.to_owned()
6693 } else {
6694 format!("{}/{}", current_process_path.join("/"), child_process_id)
6695 };
6696 emit_security_audit_event(
6697 &self.bridge,
6698 vm_id,
6699 "security.process.kill",
6700 audit_fields([
6701 (String::from("source"), String::from("guest_child_process")),
6702 (String::from("source_pid"), source_pid.to_string()),
6703 (String::from("target_pid"), child.kernel_pid.to_string()),
6704 (String::from("process_id"), process_id.to_owned()),
6705 (String::from("child_process_id"), child_process_label),
6706 (String::from("signal"), signal_name),
6707 ]),
6708 );
6709 Ok(())
6710 }
6711
6712 fn handle_descendant_process_kill_rpc(
6713 &mut self,
6714 vm_id: &str,
6715 process_id: &str,
6716 current_process_path: &[&str],
6717 child_process_id: &str,
6718 request: &JavascriptSyncRpcRequest,
6719 ) -> Result<Value, SidecarError> {
6720 let target_pid = javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
6721 let signal_name = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
6722 let signal = parse_signal(signal_name)?;
6723
6724 let mut source_path = current_process_path.to_vec();
6725 source_path.push(child_process_id);
6726
6727 if signal != 0 && target_pid < 0 {
6728 let pgid = target_pid.unsigned_abs();
6729 let caller_kernel_pid = {
6730 let Some(vm) = self.vms.get(vm_id) else {
6731 return Err(SidecarError::InvalidState(String::from(
6732 "ESRCH: unknown VM during process.kill",
6733 )));
6734 };
6735 let Some(root) = vm.active_processes.get(process_id) else {
6736 return Err(SidecarError::InvalidState(format!(
6737 "ESRCH: unknown process {process_id} during process.kill",
6738 )));
6739 };
6740 let Some(source) = Self::active_process_by_path(root, &source_path) else {
6741 return Err(SidecarError::InvalidState(format!(
6742 "ESRCH: unknown child process {child_process_id} during process.kill",
6743 )));
6744 };
6745 source.kernel_pid
6746 };
6747 let caller_is_member =
6748 self.signal_vm_process_group(vm_id, caller_kernel_pid, pgid, signal_name)?;
6749 if !caller_is_member {
6750 return Ok(Value::Null);
6751 }
6752 let Some(vm) = self.vms.get_mut(vm_id) else {
6753 return Ok(Value::Null);
6754 };
6755 let Some(root) = vm.active_processes.get_mut(process_id) else {
6756 return Ok(Value::Null);
6757 };
6758 let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
6759 return Ok(Value::Null);
6760 };
6761 source.pending_self_signal_exit = None;
6762 if !matches!(
6763 canonical_signal_name(signal),
6764 Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
6765 ) {
6766 source.pending_self_signal_exit = Some(signal);
6767 }
6768 return Ok(json!({
6769 "self": true,
6770 "action": "default",
6771 }));
6772 }
6773
6774 let Some(vm) = self.vms.get_mut(vm_id) else {
6775 return Err(SidecarError::InvalidState(String::from(
6776 "ESRCH: unknown VM during process.kill",
6777 )));
6778 };
6779
6780 if signal == 0 {
6781 vm.kernel
6782 .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
6783 .map_err(kernel_error)?;
6784 return Ok(Value::Null);
6785 }
6786
6787 let target_kernel_pid = u32::try_from(target_pid).map_err(|_| {
6788 SidecarError::InvalidState(format!("EINVAL: invalid process pid {target_pid}"))
6789 })?;
6790 let (source_pid, located_target_path) = {
6791 let Some(root) = vm.active_processes.get(process_id) else {
6792 return Err(SidecarError::InvalidState(format!(
6793 "ESRCH: unknown process {process_id} during process.kill",
6794 )));
6795 };
6796 let Some(source) = Self::active_process_by_path(root, &source_path) else {
6797 return Err(SidecarError::InvalidState(format!(
6798 "ESRCH: unknown child process {child_process_id} during process.kill",
6799 )));
6800 };
6801 vm.kernel
6802 .signal_process(EXECUTION_DRIVER_NAME, target_pid, 0)
6803 .map_err(kernel_error)?;
6804 (
6805 source.kernel_pid,
6806 Self::active_process_path_by_kernel_pid(root, target_kernel_pid),
6807 )
6808 };
6809 let Some(target_path) = located_target_path else {
6810 self.signal_vm_kernel_pid(vm_id, target_kernel_pid, signal_name)?;
6814 return Ok(Value::Null);
6815 };
6816 let Some(vm) = self.vms.get_mut(vm_id) else {
6817 return Err(SidecarError::InvalidState(String::from(
6818 "ESRCH: unknown VM during process.kill",
6819 )));
6820 };
6821
6822 if source_pid == target_kernel_pid {
6823 let Some(root) = vm.active_processes.get_mut(process_id) else {
6824 return Ok(Value::Null);
6825 };
6826 let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
6827 return Ok(Value::Null);
6828 };
6829 source.pending_self_signal_exit = None;
6830 if !matches!(
6831 canonical_signal_name(signal),
6832 Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
6833 ) {
6834 source.pending_self_signal_exit = Some(signal);
6835 }
6836 return Ok(json!({
6837 "self": true,
6838 "action": "default",
6839 }));
6840 }
6841
6842 let signal_key = target_path.last().map(String::as_str).unwrap_or(process_id);
6843 let registration = vm
6844 .signal_states
6845 .get(signal_key)
6846 .and_then(|handlers| handlers.get(&(signal as u32)))
6847 .cloned();
6848
6849 let action = match registration
6850 .as_ref()
6851 .map(|registration| ®istration.action)
6852 {
6853 Some(SignalDispositionAction::Ignore) => "ignore",
6854 Some(SignalDispositionAction::User) => {
6855 let Some(root) = vm.active_processes.get_mut(process_id) else {
6856 return Ok(Value::Null);
6857 };
6858 let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
6859 else {
6860 return Err(SidecarError::InvalidState(format!(
6861 "ESRCH: unknown process pid {target_pid}"
6862 )));
6863 };
6864 if let Some(session) = target.execution.javascript_v8_session_handle().filter(
6865 |_| matches!(&target.execution, ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime())
6866 || matches!(&target.execution, ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime()),
6867 ) {
6868 dispatch_v8_session_signal_async(session, signal);
6869 } else if !dispatch_v8_process_signal(target, signal)? {
6870 return Err(SidecarError::InvalidState(format!(
6871 "unsupported guest signal delivery for pid {target_pid}"
6872 )));
6873 }
6874 "user"
6875 }
6876 Some(SignalDispositionAction::Default) | None
6877 if matches!(
6878 canonical_signal_name(signal),
6879 Some("SIGWINCH" | "SIGCHLD" | "SIGURG")
6880 ) =>
6881 {
6882 "ignore"
6883 }
6884 Some(SignalDispositionAction::Default) | None => {
6885 let Some(root) = vm.active_processes.get_mut(process_id) else {
6886 return Ok(Value::Null);
6887 };
6888 let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
6889 else {
6890 return Err(SidecarError::InvalidState(format!(
6891 "ESRCH: unknown process pid {target_pid}"
6892 )));
6893 };
6894 apply_active_process_default_signal(&mut vm.kernel, target, signal)?;
6895 "default"
6896 }
6897 };
6898
6899 let target_path_label = Self::child_process_path_label(
6900 process_id,
6901 &target_path.iter().map(String::as_str).collect::<Vec<_>>(),
6902 );
6903 emit_security_audit_event(
6904 &self.bridge,
6905 vm_id,
6906 "security.process.kill",
6907 audit_fields([
6908 (String::from("source"), String::from("guest_process")),
6909 (String::from("source_pid"), source_pid.to_string()),
6910 (String::from("target_pid"), target_pid.to_string()),
6911 (String::from("process_id"), process_id.to_owned()),
6912 (
6913 String::from("target_process_path"),
6914 target_path_label.clone(),
6915 ),
6916 (String::from("signal"), signal_name.to_owned()),
6917 ]),
6918 );
6919
6920 Ok(json!({
6921 "self": false,
6922 "action": action,
6923 "signal": signal_name,
6924 "number": signal,
6925 "targetProcessPath": target_path_label,
6926 }))
6927 }
6928
6929 pub(crate) fn poll_javascript_child_process(
6930 &mut self,
6931 vm_id: &str,
6932 process_id: &str,
6933 child_process_id: &str,
6934 wait_ms: u64,
6935 ) -> Result<Value, SidecarError> {
6936 self.poll_descendant_javascript_child_process(
6937 vm_id,
6938 process_id,
6939 &[],
6940 child_process_id,
6941 wait_ms,
6942 )
6943 }
6944
6945 pub(crate) fn write_javascript_child_process_stdin(
6946 &mut self,
6947 vm_id: &str,
6948 process_id: &str,
6949 child_process_id: &str,
6950 chunk: &[u8],
6951 ) -> Result<(), SidecarError> {
6952 let Some(vm) = self.vms.get_mut(vm_id) else {
6953 return Err(javascript_child_process_gone_error(
6954 process_id,
6955 &[child_process_id],
6956 ));
6957 };
6958 let Some(child) = vm
6959 .active_processes
6960 .get_mut(process_id)
6961 .ok_or_else(|| missing_process_error(vm_id, process_id))?
6962 .child_processes
6963 .get_mut(child_process_id)
6964 else {
6965 return Err(javascript_child_process_gone_error(
6966 process_id,
6967 &[child_process_id],
6968 ));
6969 };
6970 if let Err(error) = child.execution.write_stdin(chunk) {
6971 if is_broken_pipe_error(&error) {
6972 return Ok(());
6973 }
6974 return Err(error);
6975 }
6976 write_kernel_process_stdin(&mut vm.kernel, child, chunk)
6977 }
6978
6979 pub(crate) fn close_javascript_child_process_stdin(
6980 &mut self,
6981 vm_id: &str,
6982 process_id: &str,
6983 child_process_id: &str,
6984 ) -> Result<(), SidecarError> {
6985 let Some(vm) = self.vms.get_mut(vm_id) else {
6986 return Err(javascript_child_process_gone_error(
6987 process_id,
6988 &[child_process_id],
6989 ));
6990 };
6991 let Some(child) = vm
6992 .active_processes
6993 .get_mut(process_id)
6994 .ok_or_else(|| missing_process_error(vm_id, process_id))?
6995 .child_processes
6996 .get_mut(child_process_id)
6997 else {
6998 return Err(javascript_child_process_gone_error(
6999 process_id,
7000 &[child_process_id],
7001 ));
7002 };
7003 child.execution.close_stdin()?;
7004 close_kernel_process_stdin(&mut vm.kernel, child)
7005 }
7006
7007 pub(crate) fn kill_javascript_child_process(
7008 &mut self,
7009 vm_id: &str,
7010 process_id: &str,
7011 child_process_id: &str,
7012 signal: &str,
7013 ) -> Result<(), SidecarError> {
7014 let signal_name = signal.to_owned();
7015 let signal = parse_signal(signal)?;
7016 let Some(vm) = self.vms.get_mut(vm_id) else {
7017 return Ok(());
7018 };
7019 let process = vm
7020 .active_processes
7021 .get_mut(process_id)
7022 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
7023 let source_pid = process.kernel_pid;
7024 let child = process
7025 .child_processes
7026 .get_mut(child_process_id)
7027 .ok_or_else(|| {
7028 SidecarError::InvalidState(format!(
7029 "unknown child process {child_process_id} during kill"
7030 ))
7031 })?;
7032 terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
7033 emit_security_audit_event(
7034 &self.bridge,
7035 vm_id,
7036 "security.process.kill",
7037 audit_fields([
7038 (String::from("source"), String::from("guest_child_process")),
7039 (String::from("source_pid"), source_pid.to_string()),
7040 (String::from("target_pid"), child.kernel_pid.to_string()),
7041 (String::from("process_id"), process_id.to_owned()),
7042 (
7043 String::from("child_process_id"),
7044 child_process_id.to_owned(),
7045 ),
7046 (String::from("signal"), signal_name),
7047 ]),
7048 );
7049 Ok(())
7050 }
7051
7052 pub(crate) fn signal_vm_kernel_pid(
7058 &mut self,
7059 vm_id: &str,
7060 target_kernel_pid: u32,
7061 signal_name: &str,
7062 ) -> Result<(), SidecarError> {
7063 let signal = parse_signal(signal_name)?;
7064 let located = {
7065 let Some(vm) = self.vms.get(vm_id) else {
7066 return Err(SidecarError::InvalidState(String::from(
7067 "ESRCH: unknown VM during process.kill",
7068 )));
7069 };
7070 let alive = vm
7071 .kernel
7072 .list_processes()
7073 .get(&target_kernel_pid)
7074 .is_some_and(|info| info.status != ProcessStatus::Exited);
7075 if !alive {
7076 return Err(SidecarError::InvalidState(format!(
7077 "ESRCH: no such process {target_kernel_pid}"
7078 )));
7079 }
7080 vm.active_processes.iter().find_map(|(process_id, root)| {
7081 Self::active_process_path_by_kernel_pid(root, target_kernel_pid)
7082 .map(|path| (process_id.clone(), path))
7083 })
7084 };
7085
7086 match located {
7087 Some((process_id, path)) if path.is_empty() => {
7088 self.kill_process_internal(vm_id, &process_id, signal_name)
7089 }
7090 Some((process_id, path)) => {
7091 let Some(vm) = self.vms.get_mut(vm_id) else {
7092 return Ok(());
7093 };
7094 let Some(root) = vm.active_processes.get_mut(&process_id) else {
7095 return Ok(());
7096 };
7097 let Some(target) = Self::active_process_by_owned_path_mut(root, &path) else {
7098 return Err(SidecarError::InvalidState(format!(
7099 "ESRCH: no such process {target_kernel_pid}"
7100 )));
7101 };
7102 terminate_tracked_child_process_for_signal(&mut vm.kernel, target, signal)?;
7103 emit_security_audit_event(
7104 &self.bridge,
7105 vm_id,
7106 "security.process.kill",
7107 audit_fields([
7108 (String::from("source"), String::from("guest_process")),
7109 (String::from("target_pid"), target_kernel_pid.to_string()),
7110 (String::from("process_id"), process_id),
7111 (String::from("signal"), signal_name.to_owned()),
7112 ]),
7113 );
7114 Ok(())
7115 }
7116 None => {
7117 let Some(vm) = self.vms.get_mut(vm_id) else {
7118 return Ok(());
7119 };
7120 let target_pid = i32::try_from(target_kernel_pid).map_err(|_| {
7121 SidecarError::InvalidState(format!(
7122 "EINVAL: invalid process pid {target_kernel_pid}"
7123 ))
7124 })?;
7125 vm.kernel
7126 .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
7127 .map_err(kernel_error)?;
7128 emit_security_audit_event(
7129 &self.bridge,
7130 vm_id,
7131 "security.process.kill",
7132 audit_fields([
7133 (String::from("source"), String::from("guest_process")),
7134 (String::from("target_pid"), target_kernel_pid.to_string()),
7135 (String::from("signal"), signal_name.to_owned()),
7136 ]),
7137 );
7138 Ok(())
7139 }
7140 }
7141 }
7142
7143 pub(crate) fn signal_vm_process_group(
7148 &mut self,
7149 vm_id: &str,
7150 caller_kernel_pid: u32,
7151 pgid: u32,
7152 signal_name: &str,
7153 ) -> Result<bool, SidecarError> {
7154 parse_signal(signal_name)?;
7155 let members = {
7156 let Some(vm) = self.vms.get(vm_id) else {
7157 return Err(SidecarError::InvalidState(String::from(
7158 "ESRCH: unknown VM during process.kill",
7159 )));
7160 };
7161 vm.kernel
7162 .list_processes()
7163 .into_iter()
7164 .filter(|(_, info)| info.pgid == pgid && info.status != ProcessStatus::Exited)
7165 .map(|(pid, _)| pid)
7166 .collect::<Vec<_>>()
7167 };
7168 if members.is_empty() {
7169 return Err(SidecarError::InvalidState(format!(
7170 "ESRCH: no such process group {pgid}"
7171 )));
7172 }
7173
7174 let mut caller_is_member = false;
7175 for member_pid in members {
7176 if member_pid == caller_kernel_pid {
7177 caller_is_member = true;
7178 continue;
7179 }
7180 match self.signal_vm_kernel_pid(vm_id, member_pid, signal_name) {
7181 Ok(()) => {}
7182 Err(error) if sidecar_error_is_esrch(&error) => {}
7185 Err(error) => return Err(error),
7186 }
7187 }
7188 Ok(caller_is_member)
7189 }
7190}
7191
7192fn terminate_tracked_child_process_for_signal(
7197 kernel: &mut SidecarKernel,
7198 child: &mut ActiveProcess,
7199 signal: i32,
7200) -> Result<(), SidecarError> {
7201 let should_terminate_shared_runtime = child.execution.uses_shared_v8_runtime()
7202 && signal != 0
7203 && !matches!(
7204 signal,
7205 libc::SIGHUP
7206 | libc::SIGINT
7207 | libc::SIGTERM
7208 | libc::SIGCHLD
7209 | libc::SIGWINCH
7210 | libc::SIGSTOP
7211 | libc::SIGCONT
7212 );
7213 if should_terminate_shared_runtime {
7214 child.execution.terminate()?;
7215 child.pending_self_signal_exit = Some(signal);
7216 child.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
7217 } else {
7218 kernel
7219 .kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, signal)
7220 .map_err(kernel_error)?;
7221 }
7222 Ok(())
7223}
7224
7225fn sidecar_error_is_esrch(error: &SidecarError) -> bool {
7226 error.to_string().contains("ESRCH")
7227}
7228
7229fn apply_active_process_default_signal(
7230 kernel: &mut SidecarKernel,
7231 process: &mut ActiveProcess,
7232 signal: i32,
7233) -> Result<(), SidecarError> {
7234 if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
7235 return kernel
7236 .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
7237 .map_err(kernel_error);
7238 }
7239
7240 if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
7241 close_kernel_process_stdin(kernel, process)?;
7242 }
7243
7244 if process.execution.uses_shared_v8_runtime() {
7245 process.execution.terminate()?;
7246 if signal != 0 && matches!(process.execution, ActiveExecution::Wasm(_)) {
7247 process.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
7248 }
7249 return Ok(());
7250 }
7251
7252 kernel
7253 .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
7254 .map_err(kernel_error)
7255}
7256
7257fn map_wasm_signal_registration(
7258 registration: secure_exec_execution::wasm::WasmSignalHandlerRegistration,
7259) -> SignalHandlerRegistration {
7260 SignalHandlerRegistration {
7261 action: match registration.action {
7262 secure_exec_execution::wasm::WasmSignalDispositionAction::Default => {
7263 crate::protocol::SignalDispositionAction::Default
7264 }
7265 secure_exec_execution::wasm::WasmSignalDispositionAction::Ignore => {
7266 crate::protocol::SignalDispositionAction::Ignore
7267 }
7268 secure_exec_execution::wasm::WasmSignalDispositionAction::User => {
7269 crate::protocol::SignalDispositionAction::User
7270 }
7271 },
7272 mask: registration.mask,
7273 flags: registration.flags,
7274 }
7275}
7276
7277fn parse_process_signal_state_request(
7278 args: &[Value],
7279) -> Result<(u32, SignalHandlerRegistration), SidecarError> {
7280 let signal = javascript_sync_rpc_arg_u32(args, 0, "process.signal_state signal")?;
7281 let action = javascript_sync_rpc_arg_str(args, 1, "process.signal_state action")?;
7282 let mask_json = javascript_sync_rpc_arg_str(args, 2, "process.signal_state mask")?;
7283 let flags = javascript_sync_rpc_arg_u32(args, 3, "process.signal_state flags")?;
7284 let mask: Vec<u32> = serde_json::from_str(mask_json).map_err(|error| {
7285 SidecarError::InvalidState(format!(
7286 "process.signal_state mask must be valid JSON: {error}"
7287 ))
7288 })?;
7289 let action = match action.trim().to_ascii_lowercase().as_str() {
7290 "default" => SignalDispositionAction::Default,
7291 "ignore" => SignalDispositionAction::Ignore,
7292 "user" => SignalDispositionAction::User,
7293 other => {
7294 return Err(SidecarError::InvalidState(format!(
7295 "unsupported process.signal_state action {other}"
7296 )));
7297 }
7298 };
7299
7300 Ok((
7301 signal,
7302 SignalHandlerRegistration {
7303 action,
7304 mask,
7305 flags,
7306 },
7307 ))
7308}
7309
7310fn apply_process_signal_state_update(
7311 signal_states: &mut BTreeMap<String, BTreeMap<u32, SignalHandlerRegistration>>,
7312 process_id: &str,
7313 signal: u32,
7314 registration: SignalHandlerRegistration,
7315) {
7316 if registration.action == SignalDispositionAction::Default
7317 && registration.mask.is_empty()
7318 && registration.flags == 0
7319 {
7320 let remove_process_entry = signal_states
7321 .get_mut(process_id)
7322 .map(|handlers| {
7323 handlers.remove(&signal);
7324 handlers.is_empty()
7325 })
7326 .unwrap_or(false);
7327 if remove_process_entry {
7328 signal_states.remove(process_id);
7329 }
7330 return;
7331 }
7332
7333 signal_states
7334 .entry(process_id.to_owned())
7335 .or_default()
7336 .insert(signal, registration);
7337}
7338
7339fn map_node_signal_registration(
7340 registration: NodeSignalHandlerRegistration,
7341) -> SignalHandlerRegistration {
7342 SignalHandlerRegistration {
7343 action: match registration.action {
7344 NodeSignalDispositionAction::Default => SignalDispositionAction::Default,
7345 NodeSignalDispositionAction::Ignore => SignalDispositionAction::Ignore,
7346 NodeSignalDispositionAction::User => SignalDispositionAction::User,
7347 },
7348 mask: registration.mask,
7349 flags: registration.flags,
7350 }
7351}
7352
7353fn javascript_child_process_sync_input_bytes(
7354 value: Option<&Value>,
7355) -> Result<Option<Vec<u8>>, SidecarError> {
7356 let Some(value) = value else {
7357 return Ok(None);
7358 };
7359
7360 match value {
7361 Value::Null => Ok(None),
7362 Value::String(text) => Ok(Some(text.as_bytes().to_vec())),
7363 other => javascript_sync_rpc_bytes_arg(
7364 std::slice::from_ref(other),
7365 0,
7366 "child_process.spawn_sync input",
7367 )
7368 .map(Some),
7369 }
7370}
7371
7372fn resolve_execute_request(
7377 vm: &VmState,
7378 payload: &ExecuteRequest,
7379) -> Result<ResolvedChildProcessExecution, SidecarError> {
7380 let payload_env: BTreeMap<String, String> = payload
7381 .env
7382 .iter()
7383 .map(|(k, v)| (k.clone(), v.clone()))
7384 .collect();
7385 if let Some(command) = payload.command.as_deref() {
7386 return resolve_command_execution(
7387 vm,
7388 command,
7389 &payload.args,
7390 &payload_env,
7391 payload.cwd.as_deref(),
7392 payload.wasm_permission_tier,
7393 );
7394 }
7395
7396 let runtime = payload.runtime.clone().ok_or_else(|| {
7397 SidecarError::InvalidState(String::from("execute requires either command or runtime"))
7398 })?;
7399 let entrypoint = payload.entrypoint.clone().ok_or_else(|| {
7400 SidecarError::InvalidState(String::from(
7401 "execute requires either command or entrypoint",
7402 ))
7403 })?;
7404 let (guest_cwd, host_cwd, allow_host_path_overrides) =
7405 resolve_execution_cwds(vm, payload.cwd.as_deref());
7406 let mut env = vm.guest_env.clone();
7407 env.extend(payload_env.clone());
7408
7409 let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint);
7410 if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7411 let requested_cwd = payload.cwd.as_deref().unwrap_or(guest_cwd.as_str());
7412 return Err(SidecarError::InvalidState(format!(
7413 "execution cwd {requested_cwd} is outside sandbox root {}",
7414 vm.host_cwd.to_string_lossy()
7415 )));
7416 }
7417 let host_entrypoint_override = allow_host_path_overrides
7418 .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint))
7419 .flatten();
7420
7421 let guest_entrypoint = host_entrypoint_override
7422 .as_ref()
7423 .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7424 .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, &entrypoint));
7425 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7426
7427 Ok(ResolvedChildProcessExecution {
7428 command: match runtime {
7429 GuestRuntimeKind::JavaScript => String::from(JAVASCRIPT_COMMAND),
7430 GuestRuntimeKind::Python => String::from(PYTHON_COMMAND),
7431 GuestRuntimeKind::WebAssembly => String::from(WASM_COMMAND),
7432 },
7433 process_args: std::iter::once(entrypoint.clone())
7434 .chain(payload.args.iter().cloned())
7435 .collect(),
7436 runtime,
7437 entrypoint: host_entrypoint_override
7438 .map(|(_, host_entrypoint)| host_entrypoint)
7439 .unwrap_or(entrypoint),
7440 execution_args: payload.args.clone(),
7441 env,
7442 guest_cwd,
7443 host_cwd,
7444 wasm_permission_tier: payload.wasm_permission_tier,
7445 tool_command: false,
7446 })
7447}
7448
7449fn resolve_command_execution(
7450 vm: &VmState,
7451 command: &str,
7452 args: &[String],
7453 extra_env: &BTreeMap<String, String>,
7454 cwd: Option<&str>,
7455 explicit_wasm_permission_tier: Option<WasmPermissionTier>,
7456) -> Result<ResolvedChildProcessExecution, SidecarError> {
7457 let (guest_cwd, host_cwd, allow_host_path_overrides) = resolve_execution_cwds(vm, cwd);
7458 let mut env = vm.guest_env.clone();
7459 env.extend(extra_env.clone());
7460 let args = apply_shell_cwd_prefix(command, args.to_vec(), &guest_cwd);
7461
7462 if is_tool_command(vm, command) {
7463 let command = normalized_tool_command_name(command).unwrap_or_else(|| command.to_owned());
7464 return Ok(ResolvedChildProcessExecution {
7465 command: command.clone(),
7466 process_args: std::iter::once(command.clone())
7467 .chain(args.iter().cloned())
7468 .collect(),
7469 runtime: GuestRuntimeKind::JavaScript,
7470 entrypoint: command,
7471 execution_args: args,
7472 env,
7473 guest_cwd,
7474 host_cwd,
7475 wasm_permission_tier: None,
7476 tool_command: true,
7477 });
7478 }
7479
7480 if is_node_runtime_command(command) {
7481 if let Some(cli) = resolve_host_node_cli_entrypoint(command) {
7482 env.insert(
7483 String::from("AGENTOS_NODE_EVAL"),
7484 build_host_node_cli_eval(&cli),
7485 );
7486 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7487 add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
7488 add_runtime_host_access_path(
7489 &mut env,
7490 "AGENTOS_EXTRA_FS_READ_PATHS",
7491 &cli.package_root,
7492 true,
7493 );
7494
7495 return Ok(ResolvedChildProcessExecution {
7496 command: String::from(JAVASCRIPT_COMMAND),
7497 process_args: std::iter::once(command.to_owned())
7498 .chain(args.iter().cloned())
7499 .collect(),
7500 runtime: GuestRuntimeKind::JavaScript,
7501 entrypoint: String::from("-e"),
7502 execution_args: std::iter::once(cli.guest_entrypoint.clone())
7503 .chain(args.iter().cloned())
7504 .collect(),
7505 env,
7506 guest_cwd,
7507 host_cwd,
7508 wasm_permission_tier: None,
7509 tool_command: false,
7510 });
7511 }
7512
7513 if args.is_empty() {
7514 env.insert(String::from("AGENTOS_NODE_EVAL"), String::new());
7515 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7516
7517 return Ok(ResolvedChildProcessExecution {
7518 command: String::from(JAVASCRIPT_COMMAND),
7519 process_args: vec![command.to_owned()],
7520 runtime: GuestRuntimeKind::JavaScript,
7521 entrypoint: String::from("-e"),
7522 execution_args: Vec::new(),
7523 env,
7524 guest_cwd,
7525 host_cwd,
7526 wasm_permission_tier: None,
7527 tool_command: false,
7528 });
7529 }
7530
7531 if let Some((entrypoint, execution_args)) =
7532 resolve_special_node_cli_invocation(&args, &mut env)
7533 {
7534 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7535
7536 return Ok(ResolvedChildProcessExecution {
7537 command: String::from(JAVASCRIPT_COMMAND),
7538 process_args: std::iter::once(command.to_owned())
7539 .chain(args.iter().cloned())
7540 .collect(),
7541 runtime: GuestRuntimeKind::JavaScript,
7542 entrypoint,
7543 execution_args,
7544 env,
7545 guest_cwd,
7546 host_cwd,
7547 wasm_permission_tier: None,
7548 tool_command: false,
7549 });
7550 }
7551
7552 let Some(entrypoint_specifier) = args.first() else {
7553 return Err(SidecarError::InvalidState(format!(
7554 "{command} execution requires an entrypoint"
7555 )));
7556 };
7557
7558 let (entrypoint, execution_args, guest_entrypoint) = {
7559 let requested_host_entrypoint =
7560 resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier);
7561 if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7562 let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
7563 return Err(SidecarError::InvalidState(format!(
7564 "execution cwd {requested_cwd} is outside sandbox root {}",
7565 vm.host_cwd.to_string_lossy()
7566 )));
7567 }
7568 let host_entrypoint_override = allow_host_path_overrides
7569 .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier))
7570 .flatten();
7571 let guest_entrypoint = host_entrypoint_override
7572 .as_ref()
7573 .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7574 .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, entrypoint_specifier));
7575 let entrypoint = host_entrypoint_override.map_or_else(
7576 || {
7577 guest_entrypoint.as_ref().map_or_else(
7578 || entrypoint_specifier.clone(),
7579 |guest_entrypoint| {
7580 resolve_vm_guest_path_to_host(vm, guest_entrypoint)
7581 .to_string_lossy()
7582 .into_owned()
7583 },
7584 )
7585 },
7586 |(_, host_entrypoint)| host_entrypoint,
7587 );
7588 (
7589 entrypoint,
7590 args.iter().skip(1).cloned().collect(),
7591 guest_entrypoint,
7592 )
7593 };
7594
7595 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7596
7597 return Ok(ResolvedChildProcessExecution {
7598 command: String::from(JAVASCRIPT_COMMAND),
7599 process_args: std::iter::once(command.to_owned())
7600 .chain(args.iter().cloned())
7601 .collect(),
7602 runtime: GuestRuntimeKind::JavaScript,
7603 entrypoint,
7604 execution_args,
7605 env,
7606 guest_cwd,
7607 host_cwd,
7608 wasm_permission_tier: None,
7609 tool_command: false,
7610 });
7611 }
7612
7613 if command.ends_with(".js") || command.ends_with(".mjs") || command.ends_with(".cjs") {
7614 let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, command);
7615 if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7616 let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
7617 return Err(SidecarError::InvalidState(format!(
7618 "execution cwd {requested_cwd} is outside sandbox root {}",
7619 vm.host_cwd.to_string_lossy()
7620 )));
7621 }
7622 let host_entrypoint_override = allow_host_path_overrides
7623 .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, command))
7624 .flatten();
7625 let guest_entrypoint = host_entrypoint_override
7626 .as_ref()
7627 .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7628 .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, command));
7629 let entrypoint = host_entrypoint_override.map_or_else(
7630 || {
7631 guest_entrypoint.as_ref().map_or_else(
7632 || command.to_owned(),
7633 |guest_entrypoint| {
7634 resolve_vm_guest_path_to_host(vm, guest_entrypoint)
7635 .to_string_lossy()
7636 .into_owned()
7637 },
7638 )
7639 },
7640 |(_, host_entrypoint)| host_entrypoint,
7641 );
7642 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7643
7644 return Ok(ResolvedChildProcessExecution {
7645 command: String::from(JAVASCRIPT_COMMAND),
7646 process_args: std::iter::once(command.to_owned())
7647 .chain(args.iter().cloned())
7648 .collect(),
7649 runtime: GuestRuntimeKind::JavaScript,
7650 entrypoint,
7651 execution_args: args.to_vec(),
7652 env,
7653 guest_cwd,
7654 host_cwd,
7655 wasm_permission_tier: None,
7656 tool_command: false,
7657 });
7658 }
7659
7660 let guest_entrypoint = resolve_guest_command_entrypoint(
7661 vm,
7662 &guest_cwd,
7663 command,
7664 env.get("PATH").map(String::as_str),
7665 )
7666 .ok_or_else(|| {
7667 SidecarError::InvalidState(format!(
7668 "command not found on native sidecar path: {command}"
7669 ))
7670 })?;
7671 let wasm_permission_tier = explicit_wasm_permission_tier
7672 .or_else(|| vm.command_permissions.get(command).copied())
7673 .or_else(|| {
7674 Path::new(&guest_entrypoint)
7675 .file_name()
7676 .and_then(|name| name.to_str())
7677 .and_then(|name| vm.command_permissions.get(name).copied())
7678 });
7679
7680 let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
7681 if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
7682 resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
7683 {
7684 prepare_guest_runtime_env(
7685 vm,
7686 &mut env,
7687 &guest_cwd,
7688 &host_cwd,
7689 Some(javascript_guest_entrypoint),
7690 )?;
7691
7692 return Ok(ResolvedChildProcessExecution {
7693 command: command.to_owned(),
7694 process_args: std::iter::once(command.to_owned())
7695 .chain(args.iter().cloned())
7696 .collect(),
7697 runtime: GuestRuntimeKind::JavaScript,
7698 entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
7699 execution_args: args.to_vec(),
7700 env,
7701 guest_cwd,
7702 host_cwd,
7703 wasm_permission_tier: None,
7704 tool_command: false,
7705 });
7706 }
7707 prepare_guest_runtime_env(
7708 vm,
7709 &mut env,
7710 &guest_cwd,
7711 &host_cwd,
7712 Some(guest_entrypoint.clone()),
7713 )?;
7714
7715 Ok(ResolvedChildProcessExecution {
7716 command: command.to_owned(),
7717 process_args: std::iter::once(command.to_owned())
7718 .chain(args.iter().cloned())
7719 .collect(),
7720 runtime: GuestRuntimeKind::WebAssembly,
7721 entrypoint: host_entrypoint.to_string_lossy().into_owned(),
7722 execution_args: args.to_vec(),
7723 env,
7724 guest_cwd,
7725 host_cwd,
7726 wasm_permission_tier,
7727 tool_command: false,
7728 })
7729}
7730
7731const MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH: usize = 4;
7732
7733fn resolve_javascript_command_entrypoint(
7734 vm: &VmState,
7735 guest_entrypoint: &str,
7736 host_entrypoint: &Path,
7737) -> Option<(String, PathBuf)> {
7738 resolve_javascript_command_entrypoint_inner(
7739 vm,
7740 guest_entrypoint,
7741 host_entrypoint,
7742 MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH,
7743 )
7744}
7745
7746fn resolve_javascript_command_entrypoint_inner(
7747 vm: &VmState,
7748 guest_entrypoint: &str,
7749 host_entrypoint: &Path,
7750 redirects_remaining: usize,
7751) -> Option<(String, PathBuf)> {
7752 if redirects_remaining > 0 {
7753 let symlink_target = fs::symlink_metadata(host_entrypoint)
7754 .ok()
7755 .filter(|metadata| metadata.file_type().is_symlink())
7756 .and_then(|_| fs::read_link(host_entrypoint).ok());
7757 if let Some(symlink_target) = symlink_target {
7758 let guest_parent = Path::new(guest_entrypoint)
7759 .parent()
7760 .and_then(|path| path.to_str())
7761 .unwrap_or("/");
7762 let symlink_guest_entrypoint = if symlink_target.is_absolute() {
7763 normalize_path(&symlink_target.to_string_lossy())
7764 } else {
7765 normalize_path(&format!(
7766 "{guest_parent}/{}",
7767 symlink_target.to_string_lossy().replace('\\', "/")
7768 ))
7769 };
7770 let symlink_host_entrypoint =
7771 resolve_vm_guest_path_to_host(vm, &symlink_guest_entrypoint);
7772 return resolve_javascript_command_entrypoint_inner(
7773 vm,
7774 &symlink_guest_entrypoint,
7775 &symlink_host_entrypoint,
7776 redirects_remaining - 1,
7777 );
7778 }
7779 }
7780
7781 let script = load_executable_script_preview(host_entrypoint)?;
7782 let interpreter = parse_script_interpreter_name(&script);
7783
7784 if interpreter.is_none() && is_probable_javascript_entrypoint(host_entrypoint, &script) {
7785 return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
7786 }
7787
7788 let interpreter = interpreter?;
7789 if interpreter == "node" {
7790 return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
7791 }
7792
7793 if redirects_remaining == 0 || !matches!(interpreter.as_str(), "sh" | "bash" | "dash") {
7794 return None;
7795 }
7796
7797 let shim_target = parse_node_shell_shim_target(&script)?;
7798 let guest_parent = Path::new(guest_entrypoint)
7799 .parent()
7800 .and_then(|path| path.to_str())
7801 .unwrap_or("/");
7802 let shim_guest_entrypoint = normalize_path(&format!("{guest_parent}/{shim_target}"));
7803 let shim_host_entrypoint = resolve_vm_guest_path_to_host(vm, &shim_guest_entrypoint);
7804 resolve_javascript_command_entrypoint_inner(
7805 vm,
7806 &shim_guest_entrypoint,
7807 &shim_host_entrypoint,
7808 redirects_remaining - 1,
7809 )
7810}
7811
7812fn load_executable_script_preview(path: &Path) -> Option<String> {
7813 let bytes = fs::read(path).ok()?;
7814 let preview_len = bytes.len().min(16 * 1024);
7815 Some(String::from_utf8_lossy(&bytes[..preview_len]).into_owned())
7816}
7817
7818fn parse_script_interpreter_name(script: &str) -> Option<String> {
7819 let shebang = script.lines().next()?.strip_prefix("#!")?.trim();
7820 let mut tokens = shebang.split_whitespace();
7821 let command = tokens.next()?;
7822 let command_name = Path::new(command).file_name()?.to_str()?;
7823 if command_name == "env" {
7824 for token in tokens {
7825 if token.starts_with('-') {
7826 continue;
7827 }
7828 return Path::new(token)
7829 .file_name()
7830 .and_then(|name| name.to_str())
7831 .map(ToOwned::to_owned);
7832 }
7833 return None;
7834 }
7835
7836 Some(command_name.to_owned())
7837}
7838
7839fn parse_node_shell_shim_target(script: &str) -> Option<String> {
7840 for line in script.lines() {
7841 let trimmed = line.trim();
7842 if !trimmed.starts_with("exec ") {
7843 continue;
7844 }
7845
7846 let mut remaining = trimmed;
7847 while let Some(start) = remaining.find("\"$basedir/") {
7848 let after_prefix = &remaining[start + "\"$basedir/".len()..];
7849 let end = after_prefix.find('"')?;
7850 let candidate = &after_prefix[..end];
7851 remaining = &after_prefix[end + 1..];
7852
7853 if candidate.is_empty() || candidate == "node" || candidate.ends_with("/node") {
7854 continue;
7855 }
7856
7857 return Some(candidate.to_owned());
7858 }
7859 }
7860
7861 None
7862}
7863
7864fn is_probable_javascript_entrypoint(path: &Path, script: &str) -> bool {
7865 let extension = path
7866 .extension()
7867 .and_then(|value| value.to_str())
7868 .unwrap_or_default();
7869 if matches!(extension, "js" | "cjs" | "mjs") {
7870 return true;
7871 }
7872
7873 if !path
7874 .components()
7875 .any(|component| component.as_os_str() == "node_modules")
7876 {
7877 return false;
7878 }
7879
7880 let preview = script.trim_start_matches('\u{feff}').trim_start();
7881 !preview.is_empty()
7882 && !preview.starts_with("#!")
7883 && (preview.starts_with("\"use strict\"")
7884 || preview.starts_with("'use strict'")
7885 || preview.starts_with("import ")
7886 || preview.starts_with("export ")
7887 || preview.starts_with("const ")
7888 || preview.starts_with("let ")
7889 || preview.starts_with("var ")
7890 || preview.starts_with("Object.defineProperty(exports")
7891 || preview.starts_with("module.exports")
7892 || preview.starts_with("require("))
7893}
7894
7895fn resolve_guest_execution_cwd(vm: &VmState, value: Option<&str>) -> String {
7896 value
7897 .map(normalize_path)
7898 .unwrap_or_else(|| vm.guest_cwd.clone())
7899}
7900
7901fn resolve_execution_cwds(vm: &VmState, value: Option<&str>) -> (String, PathBuf, bool) {
7902 if let Some(raw_cwd) = value {
7903 let normalized_vm_host_cwd = normalize_host_path(&vm.host_cwd);
7904 let requested_host_cwd = normalize_host_path(Path::new(raw_cwd));
7905 if path_is_within_root(&requested_host_cwd, &normalized_vm_host_cwd) {
7906 let relative = requested_host_cwd
7907 .strip_prefix(&normalized_vm_host_cwd)
7908 .unwrap_or_else(|_| Path::new(""));
7909 let relative = relative.to_string_lossy().replace('\\', "/");
7910 let guest_cwd = if relative.is_empty() {
7911 String::from("/")
7912 } else {
7913 normalize_path(&format!("/{relative}"))
7914 };
7915 return (guest_cwd, requested_host_cwd, true);
7916 }
7917 }
7918
7919 let guest_cwd = resolve_guest_execution_cwd(vm, value);
7920 let host_cwd = if value.is_none() {
7921 vm.host_cwd.clone()
7922 } else {
7923 resolve_vm_guest_path_to_host(vm, &guest_cwd)
7924 };
7925 (guest_cwd, host_cwd, value.is_none())
7926}
7927
7928fn resolve_vm_guest_path_to_host(vm: &VmState, guest_path: &str) -> PathBuf {
7929 host_mount_path_for_guest_path(vm, guest_path)
7930 .unwrap_or_else(|| shadow_path_for_guest(vm, guest_path))
7931}
7932
7933fn shadow_path_for_guest(vm: &VmState, guest_path: &str) -> PathBuf {
7934 let normalized = normalize_path(guest_path);
7935 let relative = normalized.trim_start_matches('/');
7936 if relative.is_empty() {
7937 return vm.cwd.clone();
7938 }
7939 vm.cwd.join(relative)
7940}
7941
7942fn apply_shell_cwd_prefix(command: &str, mut args: Vec<String>, guest_cwd: &str) -> Vec<String> {
7943 if guest_cwd == "/" || !is_shell_command(command) {
7944 return args;
7945 }
7946
7947 let Some(flag) = args.first() else {
7948 return args;
7949 };
7950 if !matches!(flag.as_str(), "-c" | "-lc") || args.len() < 2 {
7951 return args;
7952 }
7953
7954 let command_text = args[1].clone();
7955 let quoted_cwd = shell_single_quote(guest_cwd);
7956 args[1] = format!("cd {quoted_cwd} && {command_text}");
7957 args
7958}
7959
7960fn is_shell_command(command: &str) -> bool {
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("sh")
7967 || Path::new(command)
7968 .file_name()
7969 .and_then(|name| name.to_str())
7970 .unwrap_or(command)
7971 .trim_end_matches(".exe")
7972 .eq("bash")
7973}
7974
7975fn shell_single_quote(value: &str) -> String {
7976 if value.is_empty() {
7977 return String::from("''");
7978 }
7979 format!("'{}'", value.replace('\'', "'\"'\"'"))
7980}
7981
7982pub(crate) fn sync_active_process_host_writes_to_kernel(
7983 vm: &mut VmState,
7984) -> Result<(), SidecarError> {
7985 if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
7986 let shadow_root = vm.cwd.clone();
7987 sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
7988 }
7989
7990 let normalized_vm_root = normalize_host_path(&vm.cwd);
7991 let extra_roots = collect_active_process_host_sync_roots(vm, &normalized_vm_root);
7992 for (host_cwd, guest_cwd) in extra_roots {
7993 sync_host_directory_tree_to_kernel(vm, &host_cwd, &guest_cwd)?;
7994 }
7995
7996 Ok(())
7997}
7998
7999fn collect_active_process_host_sync_roots(
8000 vm: &VmState,
8001 normalized_vm_root: &Path,
8002) -> Vec<(PathBuf, String)> {
8003 let mut roots = Vec::new();
8004 let mut seen = BTreeSet::new();
8005
8006 for process in vm.active_processes.values() {
8007 collect_process_host_sync_roots(process, normalized_vm_root, &mut seen, &mut roots);
8008 }
8009
8010 roots
8011}
8012
8013fn collect_process_host_sync_roots(
8014 process: &ActiveProcess,
8015 normalized_vm_root: &Path,
8016 seen: &mut BTreeSet<(PathBuf, String)>,
8017 roots: &mut Vec<(PathBuf, String)>,
8018) {
8019 let normalized_host_cwd = normalize_host_path(&process.host_cwd);
8020 if !path_is_within_root(&normalized_host_cwd, normalized_vm_root) {
8021 let guest_cwd = normalize_path(&process.guest_cwd);
8022 if seen.insert((normalized_host_cwd.clone(), guest_cwd.clone())) {
8023 roots.push((normalized_host_cwd, guest_cwd));
8024 }
8025 }
8026
8027 for child in process.child_processes.values() {
8028 collect_process_host_sync_roots(child, normalized_vm_root, seen, roots);
8029 }
8030}
8031
8032fn sync_process_host_writes_to_kernel(
8033 vm: &mut VmState,
8034 process: &ActiveProcess,
8035) -> Result<(), SidecarError> {
8036 if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
8037 let shadow_root = vm.cwd.clone();
8038 sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
8039 }
8040
8041 if !path_is_within_root(
8042 &normalize_host_path(&process.host_cwd),
8043 &normalize_host_path(&vm.cwd),
8044 ) {
8045 sync_host_directory_tree_to_kernel(vm, &process.host_cwd, &process.guest_cwd)?;
8046 }
8047
8048 Ok(())
8049}
8050
8051fn sync_host_directory_tree_to_kernel(
8052 vm: &mut VmState,
8053 host_root: &Path,
8054 guest_root: &str,
8055) -> Result<(), SidecarError> {
8056 let normalized_host_root = normalize_host_path(host_root);
8057 let normalized_guest_root = normalize_path(guest_root);
8058 let mut synced_file_times = BTreeMap::new();
8059 sync_host_directory_tree_to_kernel_inner(
8060 vm,
8061 &normalized_host_root,
8062 &normalized_host_root,
8063 &normalized_guest_root,
8064 &mut synced_file_times,
8065 )
8066}
8067
8068fn sync_host_directory_tree_to_kernel_inner(
8069 vm: &mut VmState,
8070 host_root: &Path,
8071 current_host_dir: &Path,
8072 guest_root: &str,
8073 synced_file_times: &mut BTreeMap<(u64, u64), (u64, u64)>,
8074) -> Result<(), SidecarError> {
8075 let entries = match fs::read_dir(current_host_dir) {
8076 Ok(entries) => entries,
8077 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
8078 Err(error) => {
8079 return Err(SidecarError::Io(format!(
8080 "failed to read host shadow directory {}: {error}",
8081 current_host_dir.display()
8082 )));
8083 }
8084 };
8085
8086 for entry in entries {
8087 let entry = entry.map_err(|error| {
8088 SidecarError::Io(format!(
8089 "failed to read host shadow entry in {}: {error}",
8090 current_host_dir.display()
8091 ))
8092 })?;
8093 let host_path = entry.path();
8094 let file_type = entry.file_type().map_err(|error| {
8095 SidecarError::Io(format!(
8096 "failed to stat host shadow entry {}: {error}",
8097 host_path.display()
8098 ))
8099 })?;
8100 let relative_path = host_path
8101 .strip_prefix(host_root)
8102 .map_err(|error| {
8103 SidecarError::InvalidState(format!(
8104 "failed to relativize host shadow path {} against {}: {error}",
8105 host_path.display(),
8106 host_root.display()
8107 ))
8108 })?
8109 .to_string_lossy()
8110 .replace('\\', "/");
8111 let guest_path = if guest_root == "/" {
8112 normalize_path(&format!("/{relative_path}"))
8113 } else {
8114 normalize_path(&format!(
8115 "{}/{}",
8116 guest_root.trim_end_matches('/'),
8117 relative_path
8118 ))
8119 };
8120
8121 if should_skip_shadow_sync_path(vm, &guest_path) {
8122 continue;
8123 }
8124
8125 if file_type.is_dir() {
8126 let metadata = entry.metadata().map_err(|error| {
8127 SidecarError::Io(format!(
8128 "failed to read host shadow metadata {}: {error}",
8129 host_path.display()
8130 ))
8131 })?;
8132 if !is_shadow_bootstrap_dir(&guest_path)
8133 && !vm.kernel.exists(&guest_path).unwrap_or(false)
8134 {
8135 vm.kernel.mkdir(&guest_path, true).map_err(|error| {
8136 SidecarError::InvalidState(format!(
8137 "failed to sync host shadow directory {} to guest {}: {}",
8138 host_path.display(),
8139 guest_path,
8140 kernel_error(error)
8141 ))
8142 })?;
8143 vm.kernel
8144 .chmod(&guest_path, host_shadow_mode(&metadata))
8145 .map_err(|error| {
8146 SidecarError::InvalidState(format!(
8147 "failed to sync host shadow directory mode {} to guest {}: {}",
8148 host_path.display(),
8149 guest_path,
8150 kernel_error(error)
8151 ))
8152 })?;
8153 }
8154 sync_host_directory_tree_to_kernel_inner(
8155 vm,
8156 host_root,
8157 &host_path,
8158 guest_root,
8159 synced_file_times,
8160 )?;
8161 continue;
8162 }
8163
8164 if file_type.is_file() {
8165 let metadata = entry.metadata().map_err(|error| {
8166 SidecarError::Io(format!(
8167 "failed to read host shadow metadata {}: {error}",
8168 host_path.display()
8169 ))
8170 })?;
8171 let timestamp_key = (metadata.dev(), metadata.ino());
8172 let (atime_ms, mtime_ms) =
8173 *synced_file_times.entry(timestamp_key).or_insert_with(|| {
8174 (
8175 metadata_time_ms(metadata.atime(), metadata.atime_nsec()),
8176 metadata_time_ms(metadata.mtime(), metadata.mtime_nsec()),
8177 )
8178 });
8179 let desired_mode = host_shadow_mode(&metadata);
8180 let bytes = read_host_shadow_file(&host_path, desired_mode).map_err(|error| {
8181 SidecarError::Io(format!(
8182 "failed to read host shadow file {}: {error}",
8183 host_path.display()
8184 ))
8185 })?;
8186 vm.kernel.write_file(&guest_path, bytes).map_err(|error| {
8187 SidecarError::InvalidState(format!(
8188 "failed to sync host shadow file {} to guest {}: {}",
8189 host_path.display(),
8190 guest_path,
8191 kernel_error(error)
8192 ))
8193 })?;
8194 vm.kernel
8195 .chmod(&guest_path, desired_mode)
8196 .map_err(|error| {
8197 SidecarError::InvalidState(format!(
8198 "failed to sync host shadow file mode {} to guest {}: {}",
8199 host_path.display(),
8200 guest_path,
8201 kernel_error(error)
8202 ))
8203 })?;
8204 vm.kernel
8205 .utimes(&guest_path, atime_ms, mtime_ms)
8206 .map_err(|error| {
8207 SidecarError::InvalidState(format!(
8208 "failed to sync host shadow file times {} to guest {}: {}",
8209 host_path.display(),
8210 guest_path,
8211 kernel_error(error)
8212 ))
8213 })?;
8214 continue;
8215 }
8216
8217 if file_type.is_symlink() {
8218 let target = match fs::read_link(&host_path) {
8219 Ok(target) => target,
8220 Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
8221 Err(error) => {
8222 return Err(SidecarError::Io(format!(
8223 "failed to read host shadow symlink {}: {error}",
8224 host_path.display()
8225 )));
8226 }
8227 };
8228 replace_kernel_symlink(vm, &guest_path, &target.to_string_lossy())?;
8229 }
8230 }
8231
8232 Ok(())
8233}
8234
8235fn replace_kernel_symlink(
8236 vm: &mut VmState,
8237 guest_path: &str,
8238 target: &str,
8239) -> Result<(), SidecarError> {
8240 if vm.kernel.symlink(target, guest_path).is_ok() {
8241 return Ok(());
8242 }
8243
8244 if let Ok(existing_target) = vm.kernel.read_link(guest_path) {
8245 if existing_target == target {
8246 return Ok(());
8247 }
8248 }
8249
8250 let _ = vm.kernel.remove_file(guest_path);
8251 let _ = vm.kernel.remove_dir(guest_path);
8252 vm.kernel
8253 .symlink(target, guest_path)
8254 .map_err(kernel_error)?;
8255 Ok(())
8256}
8257
8258fn host_shadow_mode(metadata: &fs::Metadata) -> u32 {
8259 metadata.permissions().mode() & 0o7777
8260}
8261
8262fn read_host_shadow_file(host_path: &Path, mode: u32) -> std::io::Result<Vec<u8>> {
8268 match fs::read(host_path) {
8269 Ok(bytes) => Ok(bytes),
8270 Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
8271 fs::set_permissions(host_path, fs::Permissions::from_mode(mode | 0o400))?;
8272 let result = fs::read(host_path);
8273 fs::set_permissions(host_path, fs::Permissions::from_mode(mode))?;
8274 result
8275 }
8276 Err(error) => Err(error),
8277 }
8278}
8279
8280fn metadata_time_ms(seconds: i64, nanos: i64) -> u64 {
8281 let seconds = seconds.max(0) as u64;
8282 let nanos = nanos.max(0) as u64;
8283 seconds
8284 .saturating_mul(1_000)
8285 .saturating_add(nanos / 1_000_000)
8286}
8287
8288fn is_shadow_bootstrap_dir(path: &str) -> bool {
8289 matches!(
8290 path,
8291 "/dev"
8292 | "/proc"
8293 | "/tmp"
8294 | "/bin"
8295 | "/lib"
8296 | "/sbin"
8297 | "/boot"
8298 | "/etc"
8299 | "/root"
8300 | "/run"
8301 | "/srv"
8302 | "/sys"
8303 | "/opt"
8304 | "/mnt"
8305 | "/media"
8306 | "/home"
8307 | "/home/agentos"
8308 | "/usr"
8309 | "/usr/bin"
8310 | "/usr/games"
8311 | "/usr/include"
8312 | "/usr/lib"
8313 | "/usr/libexec"
8314 | "/usr/man"
8315 | "/usr/local"
8316 | "/usr/local/bin"
8317 | "/usr/sbin"
8318 | "/usr/share"
8319 | "/usr/share/man"
8320 | "/var"
8321 | "/var/cache"
8322 | "/var/empty"
8323 | "/var/lib"
8324 | "/var/lock"
8325 | "/var/log"
8326 | "/var/run"
8327 | "/var/spool"
8328 | "/var/tmp"
8329 | "/etc/agentos"
8330 | "/workspace"
8331 )
8332}
8333
8334#[cfg(test)]
8335mod shadow_sync_tests {
8336 use super::{is_protected_agentos_shadow_sync_path, is_shadow_bootstrap_dir};
8337
8338 #[test]
8339 fn shadow_bootstrap_sync_skips_virtual_home_tree() {
8340 assert!(is_shadow_bootstrap_dir("/home"));
8341 assert!(is_shadow_bootstrap_dir("/home/agentos"));
8342 }
8343
8344 #[test]
8345 fn protected_agentos_paths_are_not_shadow_synced() {
8346 assert!(is_protected_agentos_shadow_sync_path("/etc/agentos"));
8347 assert!(is_protected_agentos_shadow_sync_path(
8348 "/etc/agentos/instructions.md"
8349 ));
8350 assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos-copy"));
8351 assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos.md"));
8352 }
8353}
8354
8355fn is_kernel_owned_shadow_sync_path(path: &str) -> bool {
8356 matches!(path, "/dev" | "/proc" | "/sys")
8357 || path.starts_with("/dev/")
8358 || path.starts_with("/proc/")
8359 || path.starts_with("/sys/")
8360}
8361
8362pub(crate) fn is_protected_agentos_shadow_sync_path(path: &str) -> bool {
8363 path == "/etc/agentos" || path.starts_with("/etc/agentos/")
8364}
8365
8366fn should_skip_shadow_sync_path(vm: &VmState, guest_path: &str) -> bool {
8367 is_kernel_owned_shadow_sync_path(guest_path)
8368 || is_protected_agentos_shadow_sync_path(guest_path)
8369 || host_mount_path_for_guest_path_from_mounts(&vm.configuration.mounts, guest_path)
8370 .is_some()
8371}
8372
8373fn resolve_path_like_guest_specifier(cwd: &str, specifier: &str) -> String {
8374 if specifier.starts_with("file://") {
8375 normalize_path(specifier.trim_start_matches("file://"))
8376 } else if specifier.starts_with("file:") {
8377 normalize_path(specifier.trim_start_matches("file:"))
8378 } else if specifier.starts_with('/') {
8379 normalize_path(specifier)
8380 } else {
8381 normalize_path(&format!("{cwd}/{specifier}"))
8382 }
8383}
8384
8385fn guest_entrypoint_for_specifier(cwd: &str, specifier: &str) -> Option<String> {
8386 is_path_like_specifier(specifier).then(|| resolve_path_like_guest_specifier(cwd, specifier))
8387}
8388
8389fn is_node_runtime_command(command: &str) -> bool {
8390 matches!(command, "node" | "npm" | "npx")
8391 || Path::new(command)
8392 .file_name()
8393 .and_then(|name| name.to_str())
8394 .is_some_and(|name| matches!(name, "node" | "npm" | "npx"))
8395}
8396
8397fn resolve_special_node_cli_invocation(
8398 args: &[String],
8399 env: &mut BTreeMap<String, String>,
8400) -> Option<(String, Vec<String>)> {
8401 let first = args.first()?;
8402 match first.as_str() {
8403 "-e" | "--eval" => {
8404 env.insert(
8405 String::from("AGENTOS_NODE_EVAL"),
8406 args.get(1).cloned().unwrap_or_default(),
8407 );
8408 Some((first.clone(), args.iter().skip(2).cloned().collect()))
8409 }
8410 "-v" | "--version" => {
8411 env.insert(
8412 String::from("AGENTOS_NODE_EVAL"),
8413 String::from("console.log(process.version);"),
8414 );
8415 Some((String::from("-e"), args.to_vec()))
8416 }
8417 _ => None,
8418 }
8419}
8420
8421fn node_runtime_command_name(command: &str) -> Option<&str> {
8422 let name = Path::new(command)
8423 .file_name()
8424 .and_then(|name| name.to_str())?;
8425 matches!(name, "node" | "npm" | "npx").then_some(name)
8426}
8427
8428struct ResolvedHostNodeCliEntrypoint {
8429 command_name: String,
8430 guest_root: String,
8431 guest_entrypoint: String,
8432 package_root: PathBuf,
8433}
8434
8435fn resolve_host_node_cli_entrypoint(command: &str) -> Option<ResolvedHostNodeCliEntrypoint> {
8436 let command_name = node_runtime_command_name(command)?;
8437 if !matches!(command_name, "npm" | "npx") {
8438 return None;
8439 }
8440
8441 let path = std::env::var_os("PATH")?;
8442 for root in std::env::split_paths(&path) {
8443 let candidate = root.join(command_name);
8444 if !candidate.is_file() {
8445 continue;
8446 }
8447 let entrypoint = candidate.canonicalize().ok().unwrap_or(candidate);
8448 let package_root = entrypoint.parent()?.parent()?.to_path_buf();
8449 let guest_root = format!("/__secure_exec/node-runtime/{command_name}");
8450 let relative_entrypoint = entrypoint.strip_prefix(&package_root).ok()?;
8451 let guest_entrypoint = normalize_path(&format!(
8452 "{guest_root}/{}",
8453 relative_entrypoint.to_string_lossy().replace('\\', "/")
8454 ));
8455 return Some(ResolvedHostNodeCliEntrypoint {
8456 command_name: command_name.to_owned(),
8457 guest_root,
8458 guest_entrypoint,
8459 package_root,
8460 });
8461 }
8462
8463 None
8464}
8465
8466fn build_host_node_cli_eval(cli: &ResolvedHostNodeCliEntrypoint) -> String {
8467 let guest_npm_main = normalize_path(&format!("{}/lib/npm.js", cli.guest_root));
8468 let guest_npm_cli = normalize_path(&format!("{}/bin/npm-cli.js", cli.guest_root));
8469 let guest_package_json = normalize_path(&format!("{}/package.json", cli.guest_root));
8470 let guest_display_module = normalize_path(&format!("{}/lib/utils/display.js", cli.guest_root));
8471 let guest_log_file_module =
8472 normalize_path(&format!("{}/lib/utils/log-file.js", cli.guest_root));
8473 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); } } }";
8474 let display_stub = format!(
8475 "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 }};",
8476 display_module = serde_json::to_string(&guest_display_module)
8477 .unwrap_or_else(|_| format!("\"{guest_display_module}\"")),
8478 log_file_module = serde_json::to_string(&guest_log_file_module)
8479 .unwrap_or_else(|_| format!("\"{guest_log_file_module}\"")),
8480 );
8481 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)); }";
8482 match cli.command_name.as_str() {
8483 "npx" => format!(
8484 "{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); }});",
8485 debug_preamble = debug_preamble,
8486 display_stub = display_stub,
8487 registry_fetch_stub = registry_fetch_stub.replace(
8488 "__AGENTOS_NPM_MAIN__",
8489 &serde_json::to_string(&guest_npm_main)
8490 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8491 ),
8492 npm_main = serde_json::to_string(&guest_npm_main)
8493 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8494 npm_cli = serde_json::to_string(&guest_npm_cli)
8495 .unwrap_or_else(|_| format!("\"{guest_npm_cli}\"")),
8496 package_json = serde_json::to_string(&guest_package_json)
8497 .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
8498 ),
8499 _ => format!(
8500 "{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); }});",
8501 debug_preamble = debug_preamble,
8502 display_stub = display_stub,
8503 registry_fetch_stub = registry_fetch_stub.replace(
8504 "__AGENTOS_NPM_MAIN__",
8505 &serde_json::to_string(&guest_npm_main)
8506 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8507 ),
8508 npm_main = serde_json::to_string(&guest_npm_main)
8509 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8510 package_json = serde_json::to_string(&guest_package_json)
8511 .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
8512 ),
8513 }
8514}
8515
8516fn resolve_guest_command_entrypoint(
8517 vm: &VmState,
8518 guest_cwd: &str,
8519 command: &str,
8520 path_env: Option<&str>,
8521) -> Option<String> {
8522 if !is_path_like_specifier(command) {
8523 if let Some(entrypoint) = vm.command_guest_paths.get(command) {
8524 return Some(entrypoint.clone());
8525 }
8526
8527 for search_dir in guest_command_search_dirs(vm, guest_cwd, path_env) {
8528 let candidate = normalize_path(&format!("{search_dir}/{command}"));
8529 if let Some(entrypoint) = resolve_guest_command_path_candidate(vm, &candidate) {
8530 return Some(entrypoint);
8531 }
8532 }
8533
8534 return None;
8535 }
8536
8537 let normalized = resolve_path_like_guest_specifier(guest_cwd, command);
8538 resolve_guest_command_path_candidate(vm, &normalized).or_else(|| {
8539 let parent_dir = Path::new(&normalized).parent()?.to_str()?;
8543 if !guest_command_search_dirs(vm, guest_cwd, path_env)
8544 .iter()
8545 .any(|search_dir| normalize_path(search_dir) == normalize_path(parent_dir))
8546 {
8547 return None;
8548 }
8549
8550 let file_name = Path::new(&normalized).file_name()?.to_str()?;
8551 vm.command_guest_paths.get(file_name).cloned()
8552 })
8553}
8554
8555fn guest_command_search_dirs(vm: &VmState, guest_cwd: &str, path_env: Option<&str>) -> Vec<String> {
8556 let mut search_dirs = Vec::new();
8557 let mut seen = BTreeSet::new();
8558
8559 if let Some(path) = path_env.or_else(|| vm.guest_env.get("PATH").map(String::as_str)) {
8560 for segment in path.split(':') {
8561 let trimmed = segment.trim();
8562 if trimmed.is_empty() {
8563 continue;
8564 }
8565 let normalized = if trimmed.starts_with('/') {
8566 normalize_path(trimmed)
8567 } else {
8568 normalize_path(&format!("{guest_cwd}/{trimmed}"))
8569 };
8570 if seen.insert(normalized.clone()) {
8571 search_dirs.push(normalized);
8572 }
8573 }
8574 }
8575
8576 for fallback in ["/bin", "/usr/bin", "/usr/local/bin"] {
8577 let normalized = String::from(fallback);
8578 if seen.insert(normalized.clone()) {
8579 search_dirs.push(normalized);
8580 }
8581 }
8582
8583 search_dirs
8584}
8585
8586fn resolve_guest_command_path_candidate(vm: &VmState, candidate: &str) -> Option<String> {
8587 if candidate.starts_with("/bin/")
8588 || candidate.starts_with("/usr/bin/")
8589 || candidate.starts_with("/usr/local/bin/")
8590 || candidate.starts_with("/__secure_exec/commands/")
8591 {
8592 if let Some(file_name) = Path::new(candidate)
8593 .file_name()
8594 .and_then(|name| name.to_str())
8595 {
8596 if let Some(guest_entrypoint) = vm.command_guest_paths.get(file_name) {
8597 return Some(guest_entrypoint.clone());
8598 }
8599 }
8600 }
8601
8602 if vm
8603 .kernel
8604 .exists(candidate)
8605 .ok()
8606 .is_some_and(|exists| exists)
8607 {
8608 return Some(normalize_path(candidate));
8609 }
8610
8611 resolve_vm_guest_path_to_host(vm, candidate)
8612 .is_file()
8613 .then(|| normalize_path(candidate))
8614}
8615
8616fn resolve_host_entrypoint_within_vm_host_cwd(
8617 vm: &VmState,
8618 specifier: &str,
8619) -> Option<(String, String)> {
8620 let candidate = Path::new(specifier);
8621 if !candidate.is_absolute() {
8622 return None;
8623 }
8624
8625 let normalized_entrypoint = normalize_host_path(candidate);
8626 let normalized_host_cwd = normalize_host_path(&vm.host_cwd);
8627 if !path_is_within_root(&normalized_entrypoint, &normalized_host_cwd) {
8628 return None;
8629 }
8630
8631 let relative = normalized_entrypoint
8632 .strip_prefix(&normalized_host_cwd)
8633 .ok()?
8634 .to_string_lossy()
8635 .replace('\\', "/");
8636 let guest_entrypoint = if relative.is_empty() {
8637 String::from("/")
8638 } else {
8639 normalize_path(&format!("/{relative}"))
8640 };
8641 Some((
8642 guest_entrypoint,
8643 normalized_entrypoint.to_string_lossy().into_owned(),
8644 ))
8645}
8646
8647fn prepare_guest_runtime_env(
8648 vm: &VmState,
8649 env: &mut BTreeMap<String, String>,
8650 guest_cwd: &str,
8651 host_cwd: &Path,
8652 guest_entrypoint: Option<String>,
8653) -> Result<(), SidecarError> {
8654 let user = vm.kernel.user_profile();
8655 let path_mappings = runtime_guest_path_mappings(vm);
8656 let read_paths = expand_host_access_paths(
8657 std::iter::once(vm.cwd.clone())
8658 .chain(
8659 path_mappings
8660 .iter()
8661 .map(|mapping| PathBuf::from(&mapping.host_path)),
8662 )
8663 .chain(std::iter::once(host_cwd.to_path_buf()))
8664 .collect::<Vec<_>>()
8665 .as_slice(),
8666 );
8667 let write_paths = dedupe_host_paths(
8668 std::iter::once(vm.cwd.clone())
8669 .chain(std::iter::once(host_cwd.to_path_buf()))
8670 .chain(runtime_guest_writable_host_paths(vm))
8671 .collect::<Vec<_>>()
8672 .as_slice(),
8673 );
8674 let allowed_node_builtins = configured_allowed_node_builtins(vm);
8675 let loopback_exempt_ports = configured_loopback_exempt_ports(vm);
8676
8677 env.insert(
8678 String::from("AGENTOS_GUEST_PATH_MAPPINGS"),
8679 serde_json::to_string(&path_mappings).map_err(|error| {
8680 SidecarError::InvalidState(format!("failed to encode guest path mappings: {error}"))
8681 })?,
8682 );
8683 env.entry(String::from(EXECUTION_SANDBOX_ROOT_ENV))
8684 .or_insert_with(|| normalize_host_path(&vm.cwd).to_string_lossy().into_owned());
8685 env.insert(
8686 String::from("AGENTOS_EXTRA_FS_READ_PATHS"),
8687 serde_json::to_string(
8688 &read_paths
8689 .iter()
8690 .map(|path| path.to_string_lossy().into_owned())
8691 .collect::<Vec<_>>(),
8692 )
8693 .map_err(|error| {
8694 SidecarError::InvalidState(format!("failed to encode read paths: {error}"))
8695 })?,
8696 );
8697 env.insert(
8698 String::from("AGENTOS_EXTRA_FS_WRITE_PATHS"),
8699 serde_json::to_string(
8700 &write_paths
8701 .iter()
8702 .map(|path| path.to_string_lossy().into_owned())
8703 .collect::<Vec<_>>(),
8704 )
8705 .map_err(|error| {
8706 SidecarError::InvalidState(format!("failed to encode write paths: {error}"))
8707 })?,
8708 );
8709 env.insert(
8710 String::from("AGENTOS_ALLOWED_NODE_BUILTINS"),
8711 serde_json::to_string(&allowed_node_builtins).map_err(|error| {
8712 SidecarError::InvalidState(format!("failed to encode allowed builtins: {error}"))
8713 })?,
8714 );
8715 env.insert(
8718 String::from("AGENTOS_JS_PLATFORM"),
8719 js_runtime_platform_env(vm).to_owned(),
8720 );
8721 if let Some(resolution) = js_runtime_module_resolution_env(vm) {
8723 env.insert(
8724 String::from("AGENTOS_JS_MODULE_RESOLUTION"),
8725 resolution.to_owned(),
8726 );
8727 }
8728 if let Some(allowlist) = js_runtime_enforced_builtins(vm) {
8732 env.insert(
8733 String::from("AGENTOS_JS_BUILTIN_ALLOWLIST"),
8734 serde_json::to_string(&allowlist).map_err(|error| {
8735 SidecarError::InvalidState(format!(
8736 "failed to encode jsRuntime builtin allow-list: {error}"
8737 ))
8738 })?,
8739 );
8740 }
8741 env.entry(String::from("HOME"))
8748 .or_insert_with(|| user.homedir.clone());
8749 env.entry(String::from("USER"))
8750 .or_insert_with(|| user.username.clone());
8751 env.entry(String::from("LOGNAME"))
8752 .or_insert_with(|| user.username.clone());
8753 env.entry(String::from("SHELL"))
8754 .or_insert_with(|| user.shell.clone());
8755 env.entry(String::from("PATH")).or_insert_with(|| {
8756 vm.guest_env
8757 .get("PATH")
8758 .cloned()
8759 .unwrap_or_else(|| crate::vm::DEFAULT_GUEST_PATH_ENV.to_owned())
8760 });
8761 env.entry(String::from("TMPDIR"))
8762 .or_insert_with(|| String::from("/tmp"));
8763 env.insert(String::from("PWD"), guest_cwd.to_owned());
8764 if !loopback_exempt_ports.is_empty() {
8765 env.insert(
8766 String::from(LOOPBACK_EXEMPT_PORTS_ENV),
8767 serde_json::to_string(&loopback_exempt_ports).map_err(|error| {
8768 SidecarError::InvalidState(format!("failed to encode loopback exemptions: {error}"))
8769 })?,
8770 );
8771 }
8772 if let Some(guest_entrypoint) = guest_entrypoint {
8773 env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
8774 }
8775 Ok(())
8776}
8777
8778fn virtual_os_cpu_count(resource_limits: &ResourceLimits) -> usize {
8779 resource_limits.virtual_cpu_count.unwrap_or(1).max(1)
8780}
8781
8782fn virtual_os_totalmem_bytes(resource_limits: &ResourceLimits) -> u64 {
8783 resource_limits
8784 .max_wasm_memory_bytes
8785 .unwrap_or(1024 * 1024 * 1024)
8786}
8787
8788fn virtual_os_freemem_bytes(resource_limits: &ResourceLimits) -> u64 {
8789 resource_limits
8790 .max_wasm_memory_bytes
8791 .unwrap_or(512 * 1024 * 1024)
8792}
8793
8794fn javascript_execution_limits(vm: &VmState) -> JavascriptExecutionLimits {
8799 JavascriptExecutionLimits {
8800 v8_heap_limit_mb: vm.limits.js_runtime.v8_heap_limit_mb,
8801 sync_rpc_wait_timeout_ms: vm.limits.js_runtime.sync_rpc_wait_timeout_ms,
8802 }
8803}
8804
8805fn guest_runtime_identity(
8811 vm: &VmState,
8812 virtual_pid: Option<u64>,
8813 virtual_ppid: Option<u64>,
8814) -> GuestRuntimeConfig {
8815 let user = vm.kernel.user_profile();
8816 let resource_limits = vm.kernel.resource_limits();
8817 GuestRuntimeConfig {
8818 virtual_uid: Some(u64::from(user.uid)),
8819 virtual_gid: Some(u64::from(user.gid)),
8820 virtual_pid,
8821 virtual_ppid,
8822 virtual_exec_path: None,
8823 os_cpu_count: Some(virtual_os_cpu_count(resource_limits) as u64),
8824 os_totalmem: Some(virtual_os_totalmem_bytes(resource_limits)),
8825 os_freemem: Some(virtual_os_freemem_bytes(resource_limits)),
8826 os_homedir: Some(user.homedir.clone()),
8827 os_hostname: None,
8828 os_shell: Some(user.shell.clone()),
8829 os_user: Some(user.username.clone()),
8830 snapshot_userland_code: vm
8835 .configuration
8836 .js_runtime
8837 .as_ref()
8838 .and_then(|cfg| cfg.snapshot_userland_code.clone()),
8839 }
8840}
8841
8842fn guest_virtual_home(vm: &VmState) -> String {
8847 let homedir = vm.kernel.user_profile().homedir;
8848 if homedir.starts_with('/') {
8849 homedir
8850 } else {
8851 String::from("/root")
8852 }
8853}
8854
8855fn python_execution_limits(vm: &VmState) -> PythonExecutionLimits {
8857 PythonExecutionLimits {
8858 output_buffer_max_bytes: Some(vm.limits.python.output_buffer_max_bytes),
8859 execution_timeout_ms: Some(vm.limits.python.execution_timeout_ms),
8860 max_old_space_mb: Some(vm.limits.python.max_old_space_mb),
8861 vfs_rpc_timeout_ms: Some(vm.limits.python.vfs_rpc_timeout_ms),
8862 }
8863}
8864
8865fn wasm_execution_limits(vm: &VmState) -> WasmExecutionLimits {
8870 let resource_limits = vm.kernel.resource_limits();
8871 WasmExecutionLimits {
8872 max_fuel: resource_limits.max_wasm_fuel,
8873 max_memory_bytes: resource_limits.max_wasm_memory_bytes,
8874 max_stack_bytes: resource_limits
8875 .max_wasm_stack_bytes
8876 .map(|value| value as u64),
8877 }
8878}
8879
8880fn js_runtime_platform(vm: &VmState) -> vm_config::JsRuntimePlatform {
8883 vm.configuration
8884 .js_runtime
8885 .as_ref()
8886 .map(|cfg| cfg.platform)
8887 .unwrap_or(vm_config::JsRuntimePlatform::Node)
8888}
8889
8890fn js_runtime_platform_env(vm: &VmState) -> &'static str {
8893 match js_runtime_platform(vm) {
8894 vm_config::JsRuntimePlatform::Node => "node",
8895 vm_config::JsRuntimePlatform::Browser => "browser",
8896 vm_config::JsRuntimePlatform::Neutral => "neutral",
8897 vm_config::JsRuntimePlatform::Bare => "bare",
8898 }
8899}
8900
8901fn js_runtime_module_resolution_env(vm: &VmState) -> Option<&'static str> {
8904 let resolution = vm
8905 .configuration
8906 .js_runtime
8907 .as_ref()
8908 .map(|cfg| cfg.module_resolution)
8909 .unwrap_or(vm_config::JsModuleResolution::Node);
8910 match resolution {
8911 vm_config::JsModuleResolution::Node => None,
8912 vm_config::JsModuleResolution::Relative => Some("relative"),
8913 vm_config::JsModuleResolution::None => Some("none"),
8914 }
8915}
8916
8917fn js_runtime_enforced_builtins(vm: &VmState) -> Option<Vec<String>> {
8921 if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
8922 return Some(Vec::new());
8923 }
8924 vm.configuration
8925 .js_runtime
8926 .as_ref()
8927 .and_then(|cfg| cfg.allowed_builtins.clone())
8928}
8929
8930fn configured_allowed_node_builtins(vm: &VmState) -> Vec<String> {
8931 if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
8933 return Vec::new();
8934 }
8935 let configured = match vm
8938 .configuration
8939 .js_runtime
8940 .as_ref()
8941 .and_then(|cfg| cfg.allowed_builtins.as_ref())
8942 {
8943 Some(list) => list.clone(),
8944 None => DEFAULT_ALLOWED_NODE_BUILTINS
8945 .iter()
8946 .map(|value| (*value).to_owned())
8947 .collect::<Vec<_>>(),
8948 };
8949 dedupe_strings(&configured)
8950}
8951
8952fn configured_loopback_exempt_ports(vm: &VmState) -> Vec<String> {
8953 if !vm.configuration.loopback_exempt_ports.is_empty() {
8954 return vm
8955 .configuration
8956 .loopback_exempt_ports
8957 .iter()
8958 .map(ToString::to_string)
8959 .collect();
8960 }
8961
8962 vm.create_loopback_exempt_ports
8963 .iter()
8964 .map(ToString::to_string)
8965 .collect()
8966}
8967
8968fn mount_config_host_path(config: &str) -> Option<String> {
8970 serde_json::from_str::<Value>(config)
8971 .ok()?
8972 .get("hostPath")
8973 .and_then(Value::as_str)
8974 .map(str::to_owned)
8975}
8976
8977fn runtime_guest_writable_host_paths(vm: &VmState) -> Vec<PathBuf> {
8978 vm.configuration
8979 .mounts
8980 .iter()
8981 .filter(|mount| !mount.read_only)
8982 .filter_map(|mount| {
8983 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
8984 .then(|| mount_config_host_path(&mount.plugin.config))
8985 .flatten()
8986 .map(PathBuf::from)
8987 })
8988 .collect()
8989}
8990
8991fn runtime_guest_path_mappings(vm: &VmState) -> Vec<RuntimeGuestPathMapping> {
8992 let mut mappings = vm
8993 .configuration
8994 .mounts
8995 .iter()
8996 .filter_map(|mount| {
8997 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
8998 .then(|| {
8999 mount_config_host_path(&mount.plugin.config).map(|host_path| {
9000 RuntimeGuestPathMapping {
9001 guest_path: normalize_path(&mount.guest_path),
9002 host_path,
9003 read_only: mount.read_only,
9004 }
9005 })
9006 })
9007 .flatten()
9008 })
9009 .collect::<Vec<_>>();
9010 let mut command_root_mappings = vm
9011 .command_guest_paths
9012 .values()
9013 .filter_map(|guest_path| {
9014 Path::new(guest_path)
9015 .parent()
9016 .and_then(|parent| parent.to_str())
9017 .map(normalize_path)
9018 })
9019 .collect::<BTreeSet<_>>()
9020 .into_iter()
9021 .map(|guest_path| RuntimeGuestPathMapping {
9022 host_path: resolve_vm_guest_path_to_host(vm, &guest_path)
9023 .to_string_lossy()
9024 .into_owned(),
9025 guest_path,
9026 read_only: false,
9027 })
9028 .collect::<Vec<_>>();
9029 mappings.append(&mut command_root_mappings);
9030 let mut extra_node_modules_roots = mappings
9031 .iter()
9032 .filter(|mapping| mapping.guest_path.starts_with("/root/node_modules/"))
9033 .filter_map(|mapping| {
9034 host_node_modules_root(Path::new(&mapping.host_path)).map(|host_root| {
9035 RuntimeGuestPathMapping {
9036 guest_path: String::from("/root/node_modules"),
9037 host_path: host_root.to_string_lossy().into_owned(),
9038 read_only: mapping.read_only,
9039 }
9040 })
9041 })
9042 .collect::<Vec<_>>();
9043 mappings.append(&mut extra_node_modules_roots);
9044 mappings.push(RuntimeGuestPathMapping {
9045 guest_path: String::from("/"),
9046 host_path: vm.cwd.to_string_lossy().into_owned(),
9047 read_only: false,
9048 });
9049 mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.guest_path.len()));
9050 mappings.dedup_by(|left, right| {
9051 left.guest_path == right.guest_path && left.host_path == right.host_path
9052 });
9053 mappings
9054}
9055
9056fn build_module_reader(
9067 vm: &VmState,
9068 resolved: &ResolvedChildProcessExecution,
9069) -> Option<crate::plugins::host_dir::HostDirModuleReader> {
9070 let mut pairs: Vec<(String, PathBuf)> = vm
9071 .configuration
9072 .mounts
9073 .iter()
9074 .filter(|mount| mount.read_only)
9075 .filter(|mount| (mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
9076 .filter_map(|mount| {
9077 mount_config_host_path(&mount.plugin.config)
9078 .map(|host_path| (normalize_path(&mount.guest_path), PathBuf::from(host_path)))
9079 })
9080 .collect();
9081
9082 let guest_entrypoint = resolved
9083 .env
9084 .get("AGENTOS_GUEST_ENTRYPOINT")
9085 .map(|path| normalize_path(path));
9086 if let Some(guest_entrypoint) = guest_entrypoint.as_deref() {
9087 let entrypoint_in_read_only_mount = pairs.iter().any(|(guest_path, _)| {
9088 guest_entrypoint == guest_path
9089 || guest_entrypoint.starts_with(&format!("{guest_path}/"))
9090 });
9091 if !entrypoint_in_read_only_mount {
9092 return None;
9093 }
9094 }
9095
9096 let extra_roots: Vec<(String, PathBuf)> = pairs
9100 .iter()
9101 .filter(|(guest_path, _)| guest_path.starts_with("/root/node_modules/"))
9102 .filter_map(|(_, host_path)| {
9103 host_node_modules_root(host_path).map(|root| (String::from("/root/node_modules"), root))
9104 })
9105 .collect();
9106 pairs.extend(extra_roots);
9107
9108 crate::plugins::host_dir::HostDirModuleReader::from_mounts(pairs)
9109}
9110
9111fn host_node_modules_root(path: &Path) -> Option<PathBuf> {
9112 if let Some(root) = path
9113 .ancestors()
9114 .filter(|candidate| {
9115 candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
9116 })
9117 .last()
9118 .map(Path::to_path_buf)
9119 {
9120 return Some(root);
9121 }
9122
9123 fs::canonicalize(path)
9124 .ok()?
9125 .ancestors()
9126 .filter(|candidate| {
9127 candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
9128 })
9129 .last()
9130 .map(Path::to_path_buf)
9131}
9132
9133#[cfg(test)]
9134mod runtime_guest_path_mapping_tests {
9135 use super::{host_node_modules_root, javascript_sync_rpc_option_bool};
9136 use serde_json::json;
9137 use std::fs;
9138 use std::time::{SystemTime, UNIX_EPOCH};
9139
9140 #[test]
9141 fn host_node_modules_root_prefers_workspace_root_over_pnpm_package_node_modules() {
9142 let unique = SystemTime::now()
9143 .duration_since(UNIX_EPOCH)
9144 .expect("clock should be monotonic")
9145 .as_nanos();
9146 let temp = std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-{unique}"));
9147 let workspace_node_modules = temp.join("node_modules");
9148 let package_root = workspace_node_modules
9149 .join(".pnpm")
9150 .join("example@1.0.0")
9151 .join("node_modules")
9152 .join("@scope")
9153 .join("pkg");
9154 fs::create_dir_all(&package_root).expect("package root should be created");
9155
9156 let resolved =
9157 host_node_modules_root(&package_root).expect("node_modules root should resolve");
9158
9159 assert_eq!(resolved, workspace_node_modules);
9160
9161 fs::remove_dir_all(&temp).expect("temp tree should be removed");
9162 }
9163
9164 #[test]
9165 fn host_node_modules_root_preserves_symlinked_workspace_node_modules_path() {
9166 let unique = SystemTime::now()
9167 .duration_since(UNIX_EPOCH)
9168 .expect("clock should be monotonic")
9169 .as_nanos();
9170 let temp =
9171 std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-symlink-{unique}"));
9172 let workspace_node_modules = temp.join("node_modules");
9173 let package_link = workspace_node_modules.join("@scope").join("pkg");
9174 let real_package = temp.join("registry").join("agent").join("pkg");
9175 fs::create_dir_all(package_link.parent().expect("package parent should exist"))
9176 .expect("scoped parent should be created");
9177 fs::create_dir_all(&real_package).expect("real package root should be created");
9178 std::os::unix::fs::symlink(&real_package, &package_link)
9179 .expect("package symlink should be created");
9180
9181 let resolved =
9182 host_node_modules_root(&package_link).expect("node_modules root should resolve");
9183
9184 assert_eq!(resolved, workspace_node_modules);
9185
9186 fs::remove_dir_all(&temp).expect("temp tree should be removed");
9187 }
9188
9189 #[test]
9190 fn javascript_sync_rpc_option_bool_accepts_boolean_recursive_argument() {
9191 assert_eq!(
9192 javascript_sync_rpc_option_bool(&[json!("/workspace"), json!(true)], 1, "recursive"),
9193 Some(true)
9194 );
9195 assert_eq!(
9196 javascript_sync_rpc_option_bool(
9197 &[json!("/workspace"), json!({ "recursive": false })],
9198 1,
9199 "recursive"
9200 ),
9201 Some(false)
9202 );
9203 }
9204}
9205
9206#[cfg(test)]
9207mod kernel_poll_sync_rpc_tests {
9208 use super::{
9209 service_javascript_kernel_poll_sync_rpc, ActiveExecution, ActiveProcess,
9210 JavascriptSyncRpcRequest, KernelPollFdResponse, SidecarKernel, ToolExecution,
9211 EXECUTION_DRIVER_NAME, JAVASCRIPT_COMMAND,
9212 };
9213 use secure_exec_kernel::command_registry::CommandDriver;
9214 use secure_exec_kernel::kernel::{KernelVmConfig, SpawnOptions};
9215 use secure_exec_kernel::mount_table::MountTable;
9216 use secure_exec_kernel::permissions::Permissions;
9217 use secure_exec_kernel::poll::{POLLHUP, POLLIN};
9218 use secure_exec_kernel::vfs::MemoryFileSystem;
9219 use serde_json::{json, Value};
9220 #[test]
9221 fn javascript_kernel_poll_sync_rpc_reports_multiple_kernel_fds() {
9222 let mut config = KernelVmConfig::new("vm-js-kernel-poll");
9223 config.permissions = Permissions::allow_all();
9224 let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config);
9225 kernel
9226 .register_driver(CommandDriver::new(
9227 EXECUTION_DRIVER_NAME,
9228 [JAVASCRIPT_COMMAND],
9229 ))
9230 .expect("register execution driver");
9231
9232 let kernel_handle = kernel
9233 .spawn_process(
9234 JAVASCRIPT_COMMAND,
9235 Vec::new(),
9236 SpawnOptions {
9237 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
9238 ..SpawnOptions::default()
9239 },
9240 )
9241 .expect("spawn javascript kernel process");
9242 let pid = kernel_handle.pid();
9243
9244 let (stdin_read_fd, stdin_write_fd) = kernel
9245 .open_pipe(EXECUTION_DRIVER_NAME, pid)
9246 .expect("open kernel stdin pipe");
9247 kernel
9248 .fd_dup2(EXECUTION_DRIVER_NAME, pid, stdin_read_fd, 0)
9249 .expect("dup stdin pipe onto fd 0");
9250 kernel
9251 .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_read_fd)
9252 .expect("close original stdin read fd");
9253
9254 let process = ActiveProcess::new(
9255 pid,
9256 kernel_handle,
9257 super::GuestRuntimeKind::JavaScript,
9258 ActiveExecution::Tool(ToolExecution::default()),
9259 );
9260
9261 kernel
9262 .fd_write(EXECUTION_DRIVER_NAME, pid, stdin_write_fd, b"poll-ready")
9263 .expect("write kernel stdin payload");
9264 kernel
9265 .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_write_fd)
9266 .expect("close kernel stdin writer");
9267
9268 let response = service_javascript_kernel_poll_sync_rpc(
9269 &mut kernel,
9270 &process,
9271 &JavascriptSyncRpcRequest {
9272 id: 1,
9273 method: String::from("__kernel_poll"),
9274 args: vec![
9275 json!([
9276 { "fd": 0, "events": POLLIN.bits() },
9277 { "fd": 1, "events": POLLIN.bits() }
9278 ]),
9279 json!(250),
9280 ],
9281 },
9282 )
9283 .expect("poll kernel fds");
9284
9285 assert_eq!(response["readyCount"], Value::from(1));
9286 let fds: Vec<KernelPollFdResponse> =
9287 serde_json::from_value(response["fds"].clone()).expect("kernel poll fd response");
9288 assert_eq!(
9289 fds,
9290 vec![
9291 KernelPollFdResponse {
9292 fd: 0,
9293 events: POLLIN.bits(),
9294 revents: (POLLIN | POLLHUP).bits(),
9295 },
9296 KernelPollFdResponse {
9297 fd: 1,
9298 events: POLLIN.bits(),
9299 revents: 0,
9300 },
9301 ]
9302 );
9303
9304 process.kernel_handle.finish(0);
9305 kernel.waitpid(pid).expect("wait javascript kernel process");
9306 }
9307}
9308
9309fn dedupe_strings(values: &[String]) -> Vec<String> {
9310 let mut seen = BTreeSet::new();
9311 let mut deduped = Vec::new();
9312 for value in values {
9313 if seen.insert(value.clone()) {
9314 deduped.push(value.clone());
9315 }
9316 }
9317 deduped
9318}
9319
9320fn dedupe_host_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
9321 let mut seen = BTreeSet::new();
9322 let mut deduped = Vec::new();
9323 for path in paths {
9324 let normalized = normalize_host_path(path);
9325 let key = normalized.to_string_lossy().into_owned();
9326 if seen.insert(key) {
9327 deduped.push(normalized);
9328 }
9329 }
9330 deduped
9331}
9332
9333fn expand_host_access_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
9334 let mut expanded = Vec::new();
9335 let mut seen = BTreeSet::new();
9336
9337 let mut add_path = |candidate: PathBuf| {
9338 let normalized = normalize_host_path(&candidate);
9339 let key = normalized.to_string_lossy().into_owned();
9340 if seen.insert(key) {
9341 expanded.push(normalized);
9342 }
9343 };
9344
9345 for host_path in paths {
9346 add_path(host_path.clone());
9347 if let Ok(realpath) = fs::canonicalize(host_path) {
9348 add_path(realpath);
9349 }
9350
9351 if host_path.file_name().and_then(|name| name.to_str()) != Some("node_modules") {
9352 continue;
9353 }
9354
9355 let mut current = host_path.parent();
9356 while let Some(parent) = current {
9357 let candidate = parent.join("node_modules");
9358 if candidate.exists() {
9359 add_path(candidate.clone());
9360 if let Ok(realpath) = fs::canonicalize(&candidate) {
9361 add_path(realpath);
9362 }
9363 }
9364 current = parent.parent();
9365 }
9366 }
9367
9368 expanded
9369}
9370
9371fn prepare_javascript_shadow(
9372 vm: &mut VmState,
9373 resolved: &ResolvedChildProcessExecution,
9374) -> Result<(), SidecarError> {
9375 let guest_entrypoint = resolved
9376 .env
9377 .get("AGENTOS_GUEST_ENTRYPOINT")
9378 .cloned()
9379 .or_else(|| {
9387 resolve_host_entrypoint_within_vm_host_cwd(vm, &resolved.entrypoint)
9388 .map(|(guest_entrypoint, _)| guest_entrypoint)
9389 })
9390 .or_else(|| {
9391 resolved
9392 .entrypoint
9393 .starts_with('/')
9394 .then(|| normalize_path(&resolved.entrypoint))
9395 });
9396 let Some(guest_entrypoint) = guest_entrypoint else {
9397 return Ok(());
9398 };
9399 if host_mount_path_for_guest_path(vm, &guest_entrypoint).is_some() {
9400 return Ok(());
9401 }
9402 if vm.kernel.lstat(&guest_entrypoint).is_err() {
9403 let host_entrypoint = {
9404 let candidate = Path::new(&resolved.entrypoint);
9405 if candidate.is_absolute() {
9406 candidate.to_path_buf()
9407 } else {
9408 resolved.host_cwd.join(candidate)
9409 }
9410 };
9411 if host_entrypoint.exists() {
9412 materialize_host_path_to_shadow(vm, &guest_entrypoint, &host_entrypoint)?;
9413 return sync_shadow_entrypoint_into_kernel(vm, &guest_entrypoint);
9418 }
9419 }
9420 materialize_guest_path_to_shadow(vm, &guest_entrypoint)
9421}
9422
9423fn sync_shadow_entrypoint_into_kernel(
9428 vm: &mut VmState,
9429 guest_entrypoint: &str,
9430) -> Result<(), SidecarError> {
9431 if vm.kernel.exists(guest_entrypoint).unwrap_or(false) {
9432 return Ok(());
9433 }
9434 let shadow_path = shadow_path_for_guest(vm, guest_entrypoint);
9435 let bytes = match fs::read(&shadow_path) {
9436 Ok(bytes) => bytes,
9437 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
9438 Err(error) => {
9439 return Err(SidecarError::Io(format!(
9440 "failed to read staged shadow entrypoint {}: {error}",
9441 shadow_path.display()
9442 )));
9443 }
9444 };
9445 if let Some(parent) = guest_parent_path(guest_entrypoint) {
9446 if !vm.kernel.exists(&parent).unwrap_or(false) {
9447 vm.kernel.mkdir(&parent, true).map_err(kernel_error)?;
9448 }
9449 }
9450 vm.kernel
9451 .write_file(guest_entrypoint, bytes)
9452 .map_err(kernel_error)?;
9453 Ok(())
9454}
9455
9456fn guest_parent_path(guest_path: &str) -> Option<String> {
9457 let parent = Path::new(guest_path).parent()?;
9458 let parent = parent.to_string_lossy();
9459 if parent.is_empty() || parent == "/" {
9460 None
9461 } else {
9462 Some(parent.into_owned())
9463 }
9464}
9465
9466fn materialize_host_path_to_shadow(
9467 vm: &VmState,
9468 guest_path: &str,
9469 host_path: &Path,
9470) -> Result<(), SidecarError> {
9471 let shadow_path = shadow_path_for_guest(vm, guest_path);
9472 let metadata = fs::symlink_metadata(host_path)
9473 .map_err(|error| SidecarError::Io(format!("failed to stat host entrypoint: {error}")))?;
9474
9475 if metadata.file_type().is_symlink() {
9476 if let Some(parent) = shadow_path.parent() {
9477 fs::create_dir_all(parent).map_err(|error| {
9478 SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
9479 })?;
9480 }
9481 let _ = fs::remove_file(&shadow_path);
9482 let _ = fs::remove_dir_all(&shadow_path);
9483 let target = fs::read_link(host_path)
9484 .map_err(|error| SidecarError::Io(format!("failed to read host symlink: {error}")))?;
9485 std::os::unix::fs::symlink(&target, &shadow_path)
9486 .map_err(|error| SidecarError::Io(format!("failed to mirror host symlink: {error}")))?;
9487 return Ok(());
9488 }
9489
9490 if metadata.is_dir() {
9491 fs::create_dir_all(&shadow_path).map_err(|error| {
9492 SidecarError::Io(format!("failed to create shadow directory: {error}"))
9493 })?;
9494 fs::set_permissions(
9495 &shadow_path,
9496 fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
9497 )
9498 .map_err(|error| {
9499 SidecarError::Io(format!(
9500 "failed to set shadow directory mode on {}: {error}",
9501 shadow_path.display()
9502 ))
9503 })?;
9504 return Ok(());
9505 }
9506
9507 if let Some(parent) = shadow_path.parent() {
9508 fs::create_dir_all(parent).map_err(|error| {
9509 SidecarError::Io(format!("failed to create shadow parent: {error}"))
9510 })?;
9511 }
9512 let bytes = fs::read(host_path)
9513 .map_err(|error| SidecarError::Io(format!("failed to read host entrypoint: {error}")))?;
9514 fs::write(&shadow_path, bytes).map_err(|error| {
9515 SidecarError::Io(format!(
9516 "failed to mirror host file into shadow root: {error}"
9517 ))
9518 })?;
9519 fs::set_permissions(
9520 &shadow_path,
9521 fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
9522 )
9523 .map_err(|error| {
9524 SidecarError::Io(format!(
9525 "failed to set shadow file mode on {}: {error}",
9526 shadow_path.display()
9527 ))
9528 })?;
9529 Ok(())
9530}
9531
9532fn materialize_guest_path_to_shadow(
9533 vm: &mut VmState,
9534 guest_path: &str,
9535) -> Result<(), SidecarError> {
9536 let stat = vm.kernel.lstat(guest_path).map_err(kernel_error)?;
9537 let shadow_path = shadow_path_for_guest(vm, guest_path);
9538
9539 if stat.is_symbolic_link {
9540 if let Some(parent) = shadow_path.parent() {
9541 fs::create_dir_all(parent).map_err(|error| {
9542 SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
9543 })?;
9544 }
9545 let _ = fs::remove_file(&shadow_path);
9546 let _ = fs::remove_dir_all(&shadow_path);
9547 let target = vm.kernel.read_link(guest_path).map_err(kernel_error)?;
9548 std::os::unix::fs::symlink(&target, &shadow_path)
9549 .map_err(|error| SidecarError::Io(format!("failed to mirror symlink: {error}")))?;
9550 return Ok(());
9551 }
9552
9553 if stat.is_directory {
9554 fs::create_dir_all(&shadow_path).map_err(|error| {
9555 SidecarError::Io(format!("failed to create shadow directory: {error}"))
9556 })?;
9557 fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
9558 |error| {
9559 SidecarError::Io(format!(
9560 "failed to set shadow directory mode on {}: {error}",
9561 shadow_path.display()
9562 ))
9563 },
9564 )?;
9565 return Ok(());
9566 }
9567
9568 if let Some(parent) = shadow_path.parent() {
9569 fs::create_dir_all(parent).map_err(|error| {
9570 SidecarError::Io(format!("failed to create shadow parent: {error}"))
9571 })?;
9572 }
9573 let bytes = vm.kernel.read_file(guest_path).map_err(kernel_error)?;
9574 fs::write(&shadow_path, bytes).map_err(|error| {
9575 SidecarError::Io(format!(
9576 "failed to mirror guest file into shadow root: {error}"
9577 ))
9578 })?;
9579 fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
9580 |error| {
9581 SidecarError::Io(format!(
9582 "failed to set shadow file mode on {}: {error}",
9583 shadow_path.display()
9584 ))
9585 },
9586 )?;
9587 Ok(())
9588}
9589
9590fn load_javascript_entrypoint_source(
9591 vm: &mut VmState,
9592 host_cwd: &Path,
9593 entrypoint: &str,
9594 env: &BTreeMap<String, String>,
9595) -> Option<String> {
9596 let mut read_guest_file = |path: &str| {
9597 vm.kernel
9598 .read_file(path)
9599 .ok()
9600 .and_then(|bytes| String::from_utf8(bytes).ok())
9601 };
9602
9603 if let Some(source) = env
9604 .get("AGENTOS_GUEST_ENTRYPOINT")
9605 .filter(|path| path.starts_with('/'))
9606 .and_then(|path| read_guest_file(path))
9607 {
9608 return Some(source);
9609 }
9610
9611 if entrypoint.starts_with('/') {
9612 if let Some(source) = read_guest_file(entrypoint) {
9613 return Some(source);
9614 }
9615 }
9616
9617 let host_entrypoint = if Path::new(entrypoint).is_absolute() {
9618 PathBuf::from(entrypoint)
9619 } else {
9620 host_cwd.join(entrypoint)
9621 };
9622 let normalized_entrypoint = normalize_host_path(&host_entrypoint);
9623 let sandbox_root = normalize_host_path(&vm.cwd);
9624 let host_cwd = normalize_host_path(&vm.host_cwd);
9625 if !path_is_within_root(&normalized_entrypoint, &sandbox_root)
9626 && !path_is_within_root(&normalized_entrypoint, &host_cwd)
9627 {
9628 return None;
9629 }
9630
9631 fs::read_to_string(&normalized_entrypoint).ok()
9632}
9633
9634fn emit_dns_resolution_event<B>(
9635 bridge: &SharedBridge<B>,
9636 vm_id: &str,
9637 hostname: &str,
9638 source: KernelDnsResolutionSource,
9639 addresses: &[IpAddr],
9640 dns: &VmDnsConfig,
9641) where
9642 B: NativeSidecarBridge + Send + 'static,
9643 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9644{
9645 let _ = emit_structured_event(
9646 bridge,
9647 vm_id,
9648 "network.dns.resolved",
9649 audit_fields([
9650 ("hostname", hostname.to_owned()),
9651 ("source", source.as_str().to_owned()),
9652 (
9653 "addresses",
9654 addresses
9655 .iter()
9656 .map(ToString::to_string)
9657 .collect::<Vec<_>>()
9658 .join(","),
9659 ),
9660 ("address_count", addresses.len().to_string()),
9661 ("resolver_count", dns.name_servers.len().to_string()),
9662 (
9663 "resolvers",
9664 dns.name_servers
9665 .iter()
9666 .map(ToString::to_string)
9667 .collect::<Vec<_>>()
9668 .join(","),
9669 ),
9670 ]),
9671 );
9672}
9673
9674fn emit_dns_record_resolution_event<B>(
9675 bridge: &SharedBridge<B>,
9676 vm_id: &str,
9677 hostname: &str,
9678 resolution: &DnsRecordResolution,
9679 dns: &VmDnsConfig,
9680) where
9681 B: NativeSidecarBridge + Send + 'static,
9682 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9683{
9684 if let Some(addresses) = dns_resolution_ip_addrs(resolution.records()) {
9685 emit_dns_resolution_event(
9686 bridge,
9687 vm_id,
9688 hostname,
9689 resolution.source(),
9690 &addresses,
9691 dns,
9692 );
9693 return;
9694 }
9695
9696 let _ = emit_structured_event(
9697 bridge,
9698 vm_id,
9699 "network.dns.resolved",
9700 audit_fields([
9701 ("hostname", hostname.to_owned()),
9702 ("source", resolution.source().as_str().to_owned()),
9703 (
9704 "addresses",
9705 resolution
9706 .records()
9707 .iter()
9708 .map(summarize_dns_record)
9709 .collect::<Vec<_>>()
9710 .join(","),
9711 ),
9712 ("address_count", resolution.records().len().to_string()),
9713 ("resolver_count", dns.name_servers.len().to_string()),
9714 (
9715 "resolvers",
9716 dns.name_servers
9717 .iter()
9718 .map(ToString::to_string)
9719 .collect::<Vec<_>>()
9720 .join(","),
9721 ),
9722 ]),
9723 );
9724}
9725
9726fn emit_dns_resolution_failure_event<B>(
9727 bridge: &SharedBridge<B>,
9728 vm_id: &str,
9729 hostname: &str,
9730 dns: &VmDnsConfig,
9731 error: &SidecarError,
9732) where
9733 B: NativeSidecarBridge + Send + 'static,
9734 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9735{
9736 let _ = emit_structured_event(
9737 bridge,
9738 vm_id,
9739 "network.dns.resolve_failed",
9740 audit_fields([
9741 ("hostname", hostname.to_owned()),
9742 ("reason", error.to_string()),
9743 ("resolver_count", dns.name_servers.len().to_string()),
9744 (
9745 "resolvers",
9746 dns.name_servers
9747 .iter()
9748 .map(ToString::to_string)
9749 .collect::<Vec<_>>()
9750 .join(","),
9751 ),
9752 ]),
9753 );
9754}
9755
9756fn parse_dns_record_type(rrtype: &str) -> Result<RecordType, SidecarError> {
9757 match rrtype {
9758 "A" => Ok(RecordType::A),
9759 "AAAA" => Ok(RecordType::AAAA),
9760 "MX" => Ok(RecordType::MX),
9761 "TXT" => Ok(RecordType::TXT),
9762 "SRV" => Ok(RecordType::SRV),
9763 "CNAME" => Ok(RecordType::CNAME),
9764 "PTR" => Ok(RecordType::PTR),
9765 "NS" => Ok(RecordType::NS),
9766 "SOA" => Ok(RecordType::SOA),
9767 "NAPTR" => Ok(RecordType::NAPTR),
9768 "CAA" => Ok(RecordType::CAA),
9769 "ANY" => Ok(RecordType::ANY),
9770 other => Err(SidecarError::Execution(format!(
9771 "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
9772 ))),
9773 }
9774}
9775
9776fn dns_resolution_to_node_value(
9777 resolution: &DnsRecordResolution,
9778 requested_type: &str,
9779) -> Result<Value, SidecarError> {
9780 let safe_ips = dns_resolution_safe_ip_set(resolution.records(), resolution.hostname())?;
9781 match requested_type {
9782 "A" | "AAAA" => Ok(Value::Array(
9783 resolution
9784 .records()
9785 .iter()
9786 .filter_map(|record| dns_record_ip_string(record, &safe_ips))
9787 .map(Value::String)
9788 .collect(),
9789 )),
9790 "MX" => Ok(Value::Array(
9791 resolution
9792 .records()
9793 .iter()
9794 .filter_map(|record| match record.data() {
9795 RData::MX(mx) => Some(json!({
9796 "priority": mx.preference,
9797 "exchange": normalize_dns_name_for_node(&mx.exchange),
9798 "type": "MX",
9799 })),
9800 _ => None,
9801 })
9802 .collect(),
9803 )),
9804 "TXT" => Ok(Value::Array(
9805 resolution
9806 .records()
9807 .iter()
9808 .filter_map(|record| match record.data() {
9809 RData::TXT(txt) => Some(Value::Array(
9810 txt.txt_data
9811 .iter()
9812 .map(|entry| Value::String(String::from_utf8_lossy(entry).into_owned()))
9813 .collect(),
9814 )),
9815 _ => None,
9816 })
9817 .collect(),
9818 )),
9819 "SRV" => Ok(Value::Array(
9820 resolution
9821 .records()
9822 .iter()
9823 .filter_map(|record| match record.data() {
9824 RData::SRV(srv) => Some(json!({
9825 "priority": srv.priority,
9826 "weight": srv.weight,
9827 "port": srv.port,
9828 "name": normalize_dns_name_for_node(&srv.target),
9829 "type": "SRV",
9830 })),
9831 _ => None,
9832 })
9833 .collect(),
9834 )),
9835 "CNAME" => Ok(Value::Array(
9836 resolution
9837 .records()
9838 .iter()
9839 .filter_map(|record| match record.data() {
9840 RData::CNAME(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9841 _ => None,
9842 })
9843 .collect(),
9844 )),
9845 "PTR" => Ok(Value::Array(
9846 resolution
9847 .records()
9848 .iter()
9849 .filter_map(|record| match record.data() {
9850 RData::PTR(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9851 _ => None,
9852 })
9853 .collect(),
9854 )),
9855 "NS" => Ok(Value::Array(
9856 resolution
9857 .records()
9858 .iter()
9859 .filter_map(|record| match record.data() {
9860 RData::NS(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9861 _ => None,
9862 })
9863 .collect(),
9864 )),
9865 "SOA" => resolution
9866 .records()
9867 .iter()
9868 .find_map(|record| match record.data() {
9869 RData::SOA(soa) => Some(json!({
9870 "nsname": normalize_dns_name_for_node(&soa.mname),
9871 "hostmaster": normalize_dns_name_for_node(&soa.rname),
9872 "serial": soa.serial,
9873 "refresh": soa.refresh,
9874 "retry": soa.retry,
9875 "expire": soa.expire,
9876 "minttl": soa.minimum,
9877 })),
9878 _ => None,
9879 })
9880 .ok_or_else(|| {
9881 SidecarError::Execution(String::from("failed to resolve DNS SOA record"))
9882 }),
9883 "NAPTR" => Ok(Value::Array(
9884 resolution
9885 .records()
9886 .iter()
9887 .filter_map(|record| match record.data() {
9888 RData::NAPTR(naptr) => Some(json!({
9889 "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
9890 "service": String::from_utf8_lossy(&naptr.services).into_owned(),
9891 "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
9892 "replacement": normalize_dns_name_for_node(&naptr.replacement),
9893 "order": naptr.order,
9894 "preference": naptr.preference,
9895 })),
9896 _ => None,
9897 })
9898 .collect(),
9899 )),
9900 "CAA" => Ok(Value::Array(
9901 resolution
9902 .records()
9903 .iter()
9904 .filter_map(|record| match record.data() {
9905 RData::CAA(caa) => {
9906 let mut value = serde_json::Map::new();
9907 value.insert(
9908 "critical".to_owned(),
9909 Value::from(u8::from(caa.issuer_critical)),
9910 );
9911 value.insert("type".to_owned(), Value::String(String::from("CAA")));
9912 if caa.tag.eq_ignore_ascii_case("iodef") {
9913 value.insert(
9914 "iodef".to_owned(),
9915 Value::String(
9916 caa.value_as_iodef()
9917 .map(|url| url.to_string())
9918 .unwrap_or_else(|_| {
9919 String::from_utf8_lossy(&caa.value).into_owned()
9920 }),
9921 ),
9922 );
9923 } else if let Ok((issuer, _params)) = caa.value_as_issue() {
9924 let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
9925 "issuewild"
9926 } else {
9927 "issue"
9928 };
9929 value.insert(
9930 field.to_owned(),
9931 Value::String(
9932 issuer.as_ref().map(ToString::to_string).unwrap_or_else(|| {
9933 String::from_utf8_lossy(&caa.value).into_owned()
9934 }),
9935 ),
9936 );
9937 } else {
9938 value.insert(
9939 caa.tag.to_ascii_lowercase(),
9940 Value::String(String::from_utf8_lossy(&caa.value).into_owned()),
9941 );
9942 }
9943 Some(Value::Object(value))
9944 }
9945 _ => None,
9946 })
9947 .collect(),
9948 )),
9949 "ANY" => Ok(Value::Array(
9950 resolution
9951 .records()
9952 .iter()
9953 .filter_map(|record| dns_any_record_to_value(record, &safe_ips))
9954 .collect(),
9955 )),
9956 other => Err(SidecarError::Execution(format!(
9957 "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
9958 ))),
9959 }
9960}
9961
9962fn dns_resolution_safe_ip_set(
9963 records: &[Record],
9964 hostname: &str,
9965) -> Result<BTreeSet<IpAddr>, SidecarError> {
9966 let ips = records
9967 .iter()
9968 .filter_map(dns_record_ip_addr)
9969 .collect::<Vec<_>>();
9970 if ips.is_empty() {
9971 return Ok(BTreeSet::new());
9972 }
9973 Ok(filter_dns_safe_ip_addrs(ips, hostname)?
9974 .into_iter()
9975 .collect())
9976}
9977
9978fn dns_resolution_ip_addrs(records: &[Record]) -> Option<Vec<IpAddr>> {
9979 let ips = records
9980 .iter()
9981 .filter_map(dns_record_ip_addr)
9982 .collect::<Vec<_>>();
9983 if ips.is_empty() {
9984 return None;
9985 }
9986 Some(ips)
9987}
9988
9989fn dns_record_ip_addr(record: &Record) -> Option<IpAddr> {
9990 match record.data() {
9991 RData::A(address) => Some(IpAddr::V4(**address)),
9992 RData::AAAA(address) => Some(IpAddr::V6(**address)),
9993 _ => None,
9994 }
9995}
9996
9997fn dns_record_ip_string(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<String> {
9998 let ip = dns_record_ip_addr(record)?;
9999 safe_ips.contains(&ip).then(|| ip.to_string())
10000}
10001
10002fn dns_any_record_to_value(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<Value> {
10003 let value = match record.data() {
10004 RData::A(_) | RData::AAAA(_) => json!({
10005 "address": dns_record_ip_string(record, safe_ips)?,
10006 "ttl": record.ttl(),
10007 "type": record.record_type().to_string(),
10008 }),
10009 RData::MX(mx) => json!({
10010 "exchange": normalize_dns_name_for_node(&mx.exchange),
10011 "priority": mx.preference,
10012 "type": "MX",
10013 }),
10014 RData::TXT(txt) => json!({
10015 "entries": txt
10016 .txt_data
10017 .iter()
10018 .map(|entry| String::from_utf8_lossy(entry).into_owned())
10019 .collect::<Vec<_>>(),
10020 "type": "TXT",
10021 }),
10022 RData::SRV(srv) => json!({
10023 "name": normalize_dns_name_for_node(&srv.target),
10024 "port": srv.port,
10025 "priority": srv.priority,
10026 "weight": srv.weight,
10027 "type": "SRV",
10028 }),
10029 RData::CNAME(name) => json!({
10030 "value": normalize_dns_name_for_node(&name.0),
10031 "type": "CNAME",
10032 }),
10033 RData::PTR(name) => json!({
10034 "value": normalize_dns_name_for_node(&name.0),
10035 "type": "PTR",
10036 }),
10037 RData::NS(name) => json!({
10038 "value": normalize_dns_name_for_node(&name.0),
10039 "type": "NS",
10040 }),
10041 RData::SOA(soa) => json!({
10042 "nsname": normalize_dns_name_for_node(&soa.mname),
10043 "hostmaster": normalize_dns_name_for_node(&soa.rname),
10044 "serial": soa.serial,
10045 "refresh": soa.refresh,
10046 "retry": soa.retry,
10047 "expire": soa.expire,
10048 "minttl": soa.minimum,
10049 "type": "SOA",
10050 }),
10051 RData::NAPTR(naptr) => json!({
10052 "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
10053 "service": String::from_utf8_lossy(&naptr.services).into_owned(),
10054 "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
10055 "replacement": normalize_dns_name_for_node(&naptr.replacement),
10056 "order": naptr.order,
10057 "preference": naptr.preference,
10058 "type": "NAPTR",
10059 }),
10060 RData::CAA(caa) => {
10061 let mut value = serde_json::Map::new();
10062 value.insert(
10063 "critical".to_owned(),
10064 Value::from(u8::from(caa.issuer_critical)),
10065 );
10066 value.insert("type".to_owned(), Value::String(String::from("CAA")));
10067 if caa.tag.eq_ignore_ascii_case("iodef") {
10068 value.insert(
10069 "iodef".to_owned(),
10070 Value::String(
10071 caa.value_as_iodef()
10072 .map(|url| url.to_string())
10073 .unwrap_or_else(|_| String::from_utf8_lossy(&caa.value).into_owned()),
10074 ),
10075 );
10076 } else if let Ok((issuer, _params)) = caa.value_as_issue() {
10077 let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
10078 "issuewild"
10079 } else {
10080 "issue"
10081 };
10082 value.insert(
10083 field.to_owned(),
10084 Value::String(
10085 issuer
10086 .as_ref()
10087 .map(ToString::to_string)
10088 .unwrap_or_else(|| String::from_utf8_lossy(&caa.value).into_owned()),
10089 ),
10090 );
10091 }
10092 Value::Object(value)
10093 }
10094 _ => return None,
10095 };
10096 Some(value)
10097}
10098
10099fn normalize_dns_name_for_node(name: &impl ToString) -> String {
10100 name.to_string().trim_end_matches('.').to_owned()
10101}
10102
10103fn summarize_dns_record(record: &Record) -> String {
10104 match record.data() {
10105 RData::A(_) | RData::AAAA(_) => record.data().to_string(),
10106 _ => format!("{} {}", record.record_type(), record.data()),
10107 }
10108}
10109
10110fn find_socket_state_entry(
10118 vm: Option<&VmState>,
10119 kind: SocketQueryKind,
10120 request: &FindListenerRequest,
10121) -> Result<Option<SocketStateEntry>, SidecarError> {
10122 let vm = vm.ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
10123
10124 for (process_id, process) in &vm.active_processes {
10125 if let Some(path) = request.path.as_deref() {
10126 if matches!(kind, SocketQueryKind::TcpListener) {
10127 for listener in process.unix_listeners.values() {
10128 if listener.path() != path {
10129 continue;
10130 }
10131 return Ok(Some(SocketStateEntry {
10132 process_id: process_id.to_owned(),
10133 host: None,
10134 port: None,
10135 path: Some(path.to_owned()),
10136 }));
10137 }
10138 }
10139 }
10140
10141 if request.path.is_none() {
10142 if let Some(entry) =
10143 find_kernel_socket_state_entry(&vm.kernel, process_id, process, kind, request)?
10144 {
10145 return Ok(Some(entry));
10146 }
10147
10148 match kind {
10149 SocketQueryKind::TcpListener => {
10150 for server in process.http_servers.values() {
10151 let local_addr = server.guest_local_addr;
10152 let local_host = local_addr.ip().to_string();
10153 if !socket_host_matches(request.host.as_deref(), &local_host) {
10154 continue;
10155 }
10156 if let Some(port) = request.port {
10157 if local_addr.port() != port {
10158 continue;
10159 }
10160 }
10161 return Ok(Some(SocketStateEntry {
10162 process_id: process_id.to_owned(),
10163 host: Some(local_host),
10164 port: Some(local_addr.port()),
10165 path: None,
10166 }));
10167 }
10168
10169 for listener in process.tcp_listeners.values() {
10170 if listener.kernel_socket_id.is_some() {
10171 continue;
10172 }
10173 let local_addr = listener.guest_local_addr();
10174 let local_host = local_addr.ip().to_string();
10175 if !socket_host_matches(request.host.as_deref(), &local_host) {
10176 continue;
10177 }
10178 if let Some(port) = request.port {
10179 if local_addr.port() != port {
10180 continue;
10181 }
10182 }
10183 return Ok(Some(SocketStateEntry {
10184 process_id: process_id.to_owned(),
10185 host: Some(local_host),
10186 port: Some(local_addr.port()),
10187 path: None,
10188 }));
10189 }
10190 }
10191 SocketQueryKind::UdpBound => {
10192 for socket in process.udp_sockets.values() {
10193 if socket.kernel_socket_id.is_some() {
10194 continue;
10195 }
10196 let Some(local_addr) = socket.local_addr() else {
10197 continue;
10198 };
10199 let local_host = local_addr.ip().to_string();
10200 if !socket_host_matches(request.host.as_deref(), &local_host) {
10201 continue;
10202 }
10203 if let Some(port) = request.port {
10204 if local_addr.port() != port {
10205 continue;
10206 }
10207 }
10208 return Ok(Some(SocketStateEntry {
10209 process_id: process_id.to_owned(),
10210 host: Some(local_host),
10211 port: Some(local_addr.port()),
10212 path: None,
10213 }));
10214 }
10215 }
10216 }
10217 }
10218
10219 let child_pid = process.execution.child_pid();
10220 let inodes = socket_inodes_for_pid(child_pid)?;
10221 if inodes.is_empty() {
10222 continue;
10223 }
10224
10225 if let Some(path) = request.path.as_deref() {
10226 if let Some(listener) = find_unix_socket_for_pid(child_pid, &inodes, path, process_id)?
10227 {
10228 return Ok(Some(listener));
10229 }
10230 continue;
10231 }
10232
10233 let table_paths = match kind {
10234 SocketQueryKind::TcpListener => [
10235 format!("/proc/{child_pid}/net/tcp"),
10236 format!("/proc/{child_pid}/net/tcp6"),
10237 ],
10238 SocketQueryKind::UdpBound => [
10239 format!("/proc/{child_pid}/net/udp"),
10240 format!("/proc/{child_pid}/net/udp6"),
10241 ],
10242 };
10243 for table_path in table_paths {
10244 if let Some(entry) = find_inet_socket_for_pid(
10245 &table_path,
10246 &inodes,
10247 kind,
10248 request.host.as_deref(),
10249 request.port,
10250 process_id,
10251 )? {
10252 return Ok(Some(entry));
10253 }
10254 }
10255 }
10256
10257 Ok(None)
10258}
10259
10260fn require_vm_inspection_permission<B>(
10261 bridge: &SharedBridge<B>,
10262 vm_id: &str,
10263 capability: &str,
10264 domain: &str,
10265 resource: &str,
10266) -> Result<(), SidecarError>
10267where
10268 B: NativeSidecarBridge + Send + 'static,
10269 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
10270{
10271 let decision = bridge.static_permission_decision(vm_id, capability, domain, Some(resource));
10272 if decision.as_ref().is_some_and(|decision| decision.allow) {
10273 return Ok(());
10274 }
10275
10276 let reason = decision
10277 .and_then(|decision| decision.reason)
10278 .unwrap_or_else(|| format!("{capability} permission required"));
10279 Err(SidecarError::Execution(format!(
10280 "EACCES: permission denied, {resource}: {reason}"
10281 )))
10282}
10283
10284fn socket_query_resource(kind: SocketQueryKind, request: &FindListenerRequest) -> String {
10285 if let Some(path) = request.path.as_deref() {
10286 return format!("unix://{path}");
10287 }
10288
10289 let host = request.host.as_deref().unwrap_or("*");
10290 let port = request
10291 .port
10292 .map_or_else(|| String::from("*"), |port| port.to_string());
10293 match kind {
10294 SocketQueryKind::TcpListener => format!("tcp://{host}:{port}"),
10295 SocketQueryKind::UdpBound => format!("udp://{host}:{port}"),
10296 }
10297}
10298
10299fn snapshot_vm_processes(vm: &VmState) -> Vec<ProcessSnapshotEntry> {
10300 let process_table = vm.kernel.list_processes();
10301 snapshot_vm_processes_inner(vm, &process_table)
10302}
10303
10304fn snapshot_vm_processes_inner(
10305 vm: &VmState,
10306 process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
10307) -> Vec<ProcessSnapshotEntry> {
10308 let mut entries = Vec::new();
10309
10310 for (process_id, process) in &vm.active_processes {
10311 collect_process_snapshot_entries(process_id, process, process_table, &mut entries);
10312 }
10313
10314 for exited in &vm.exited_process_snapshots {
10315 entries.push(exited.process.clone());
10316 }
10317
10318 entries
10319}
10320
10321fn prune_exited_process_snapshots(vm: &mut VmState) {
10322 let cutoff = Instant::now() - EXITED_PROCESS_SNAPSHOT_RETENTION;
10323 while vm
10324 .exited_process_snapshots
10325 .front()
10326 .is_some_and(|snapshot| snapshot.captured_at < cutoff)
10327 {
10328 vm.exited_process_snapshots.pop_front();
10329 }
10330}
10331
10332fn build_process_snapshot_entry(
10333 process_id: &str,
10334 process: &ActiveProcess,
10335 info: &secure_exec_kernel::process_table::ProcessInfo,
10336 exit_code: Option<i32>,
10337) -> ProcessSnapshotEntry {
10338 ProcessSnapshotEntry {
10339 process_id: process_id.to_owned(),
10340 pid: info.pid,
10341 ppid: info.ppid,
10342 pgid: info.pgid,
10343 sid: info.sid,
10344 driver: info.driver.clone(),
10345 command: info.command.clone(),
10346 args: Vec::new(),
10347 cwd: process.guest_cwd.clone(),
10348 status: if exit_code.is_some() {
10349 ProcessSnapshotStatus::Exited
10350 } else {
10351 match info.status {
10352 ProcessStatus::Running => ProcessSnapshotStatus::Running,
10353 ProcessStatus::Stopped => ProcessSnapshotStatus::Stopped,
10354 ProcessStatus::Exited => ProcessSnapshotStatus::Exited,
10355 }
10356 },
10357 exit_code: exit_code.or(info.exit_code),
10358 }
10359}
10360
10361fn collect_process_snapshot_entries(
10362 process_id: &str,
10363 process: &ActiveProcess,
10364 process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
10365 entries: &mut Vec<ProcessSnapshotEntry>,
10366) {
10367 if let Some(info) = process_table.get(&process.kernel_pid) {
10368 entries.push(build_process_snapshot_entry(
10369 process_id, process, info, None,
10370 ));
10371 }
10372
10373 for (child_id, child) in &process.child_processes {
10374 let child_process_id = format!("{process_id}/{child_id}");
10375 collect_process_snapshot_entries(&child_process_id, child, process_table, entries);
10376 }
10377}
10378
10379fn find_kernel_socket_state_entry(
10380 kernel: &SidecarKernel,
10381 process_id: &str,
10382 process: &ActiveProcess,
10383 kind: SocketQueryKind,
10384 request: &FindListenerRequest,
10385) -> Result<Option<SocketStateEntry>, SidecarError> {
10386 let entry = match kind {
10387 SocketQueryKind::TcpListener => process
10388 .tcp_listeners
10389 .values()
10390 .filter_map(|listener| listener.kernel_socket_id)
10391 .find_map(|socket_id| {
10392 kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
10393 }),
10394 SocketQueryKind::UdpBound => process
10395 .udp_sockets
10396 .values()
10397 .filter_map(|socket| socket.kernel_socket_id)
10398 .find_map(|socket_id| {
10399 kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
10400 }),
10401 };
10402
10403 if entry.is_some() {
10404 return Ok(entry);
10405 }
10406
10407 for child in process.child_processes.values() {
10408 if let Some(entry) =
10409 find_kernel_socket_state_entry(kernel, process_id, child, kind, request)?
10410 {
10411 return Ok(Some(entry));
10412 }
10413 }
10414
10415 Ok(None)
10416}
10417
10418fn kernel_socket_state_entry(
10419 kernel: &SidecarKernel,
10420 process_id: &str,
10421 socket_id: SocketId,
10422 kind: SocketQueryKind,
10423 request: &FindListenerRequest,
10424) -> Option<SocketStateEntry> {
10425 let record = kernel.socket_get(socket_id)?;
10426 let local_address = record.local_address()?;
10427 match kind {
10428 SocketQueryKind::TcpListener if record.state() == SocketState::Listening => {}
10429 SocketQueryKind::TcpListener => return None,
10430 SocketQueryKind::UdpBound => {}
10431 }
10432
10433 if !socket_host_matches(request.host.as_deref(), local_address.host()) {
10434 return None;
10435 }
10436 if request
10437 .port
10438 .is_some_and(|port| local_address.port() != port)
10439 {
10440 return None;
10441 }
10442
10443 Some(SocketStateEntry {
10444 process_id: process_id.to_owned(),
10445 host: Some(local_address.host().to_owned()),
10446 port: Some(local_address.port()),
10447 path: None,
10448 })
10449}
10450
10451fn socket_inodes_for_pid(pid: u32) -> Result<BTreeSet<u64>, SidecarError> {
10452 let fd_dir = PathBuf::from(format!("/proc/{pid}/fd"));
10453 let entries = match fs::read_dir(&fd_dir) {
10454 Ok(entries) => entries,
10455 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(BTreeSet::new()),
10456 Err(error) => {
10457 return Err(SidecarError::Io(format!(
10458 "failed to read socket descriptors for process {pid}: {error}"
10459 )));
10460 }
10461 };
10462
10463 let mut inodes = BTreeSet::new();
10464 for entry in entries {
10465 let entry = entry.map_err(|error| {
10466 SidecarError::Io(format!(
10467 "failed to inspect fd entry for process {pid}: {error}"
10468 ))
10469 })?;
10470 let target = match fs::read_link(entry.path()) {
10471 Ok(target) => target,
10472 Err(_) => continue,
10473 };
10474 if let Some(inode) = parse_socket_inode(&target) {
10475 inodes.insert(inode);
10476 }
10477 }
10478
10479 Ok(inodes)
10480}
10481
10482fn parse_socket_inode(target: &Path) -> Option<u64> {
10483 let value = target.to_string_lossy();
10484 let trimmed = value.strip_prefix("socket:[")?.strip_suffix(']')?;
10485 trimmed.parse().ok()
10486}
10487
10488fn unix_socket_path(addr: &UnixSocketAddr) -> Option<String> {
10489 addr.as_pathname()
10490 .map(|path| path.to_string_lossy().into_owned())
10491}
10492
10493fn find_unix_socket_for_pid(
10494 pid: u32,
10495 inodes: &BTreeSet<u64>,
10496 path: &str,
10497 process_id: &str,
10498) -> Result<Option<SocketStateEntry>, SidecarError> {
10499 let table_path = format!("/proc/{pid}/net/unix");
10500 let contents = match fs::read_to_string(&table_path) {
10501 Ok(contents) => contents,
10502 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
10503 Err(error) => {
10504 return Err(SidecarError::Io(format!(
10505 "failed to inspect unix sockets for process {pid}: {error}"
10506 )));
10507 }
10508 };
10509
10510 for line in contents.lines().skip(1) {
10511 let columns = line.split_whitespace().collect::<Vec<_>>();
10512 if columns.len() < 8 {
10513 continue;
10514 }
10515 let Ok(inode) = columns[6].parse::<u64>() else {
10516 continue;
10517 };
10518 if !inodes.contains(&inode) || columns[7] != path {
10519 continue;
10520 }
10521 return Ok(Some(SocketStateEntry {
10522 process_id: process_id.to_owned(),
10523 host: None,
10524 port: None,
10525 path: Some(path.to_owned()),
10526 }));
10527 }
10528
10529 Ok(None)
10530}
10531
10532fn find_inet_socket_for_pid(
10533 table_path: &str,
10534 inodes: &BTreeSet<u64>,
10535 kind: SocketQueryKind,
10536 requested_host: Option<&str>,
10537 requested_port: Option<u16>,
10538 process_id: &str,
10539) -> Result<Option<SocketStateEntry>, SidecarError> {
10540 for entry in parse_proc_net_entries(table_path)? {
10541 if !inodes.contains(&entry.inode) {
10542 continue;
10543 }
10544 if matches!(kind, SocketQueryKind::TcpListener) && entry.state != "0A" {
10545 continue;
10546 }
10547 if !socket_host_matches(requested_host, &entry.local_host) {
10548 continue;
10549 }
10550 if let Some(port) = requested_port {
10551 if entry.local_port != port {
10552 continue;
10553 }
10554 }
10555 return Ok(Some(SocketStateEntry {
10556 process_id: process_id.to_owned(),
10557 host: Some(entry.local_host),
10558 port: Some(entry.local_port),
10559 path: None,
10560 }));
10561 }
10562
10563 Ok(None)
10564}
10565
10566fn is_unspecified_socket_host(host: &str) -> bool {
10567 host == "0.0.0.0" || host == "::"
10568}
10569
10570fn is_loopback_socket_host(host: &str) -> bool {
10571 host == "127.0.0.1" || host == "::1" || host.eq_ignore_ascii_case("localhost")
10572}
10573
10574pub(crate) fn vm_network_resource_counts(vm: &VmState) -> NetworkResourceCounts {
10575 let snapshot = vm.kernel.resource_snapshot();
10576 let mut counts = NetworkResourceCounts {
10577 sockets: snapshot.sockets,
10578 connections: snapshot.socket_connections,
10579 };
10580 for process in vm.active_processes.values() {
10581 let process_counts = process.sidecar_only_network_resource_counts();
10582 counts.sockets += process_counts.sockets;
10583 counts.connections += process_counts.connections;
10584 }
10585 counts
10586}
10587
10588#[allow(clippy::too_many_arguments)]
10589fn collect_javascript_socket_port_state(
10590 kernel: &SidecarKernel,
10591 process_id: &str,
10592 process: &ActiveProcess,
10593 tcp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10594 http_loopback_targets: &mut BTreeMap<
10595 (JavascriptSocketFamily, u16),
10596 JavascriptHttpLoopbackTarget,
10597 >,
10598 udp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10599 udp_host_to_guest: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10600 used_tcp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10601 used_udp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10602) {
10603 for (family, port) in process.tcp_port_reservations.values() {
10604 used_tcp_ports.entry(*family).or_default().insert(*port);
10605 }
10606
10607 let mut record_tcp_listener = |guest_addr: SocketAddr, host_port: u16| {
10608 let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10609 used_tcp_ports
10610 .entry(family)
10611 .or_default()
10612 .insert(guest_addr.port());
10613 tcp_guest_to_host.insert((family, guest_addr.port()), host_port);
10616 };
10617
10618 for listener in process.tcp_listeners.values() {
10619 let local_addr = listener
10620 .kernel_socket_id
10621 .and_then(|socket_id| kernel.socket_get(socket_id))
10622 .and_then(|record| record.local_address().cloned())
10623 .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
10624 .unwrap_or_else(|| listener.guest_local_addr());
10625 record_tcp_listener(local_addr, local_addr.port());
10626 }
10627
10628 for (server_id, server) in &process.http_servers {
10629 let host_port = match server.listener.local_addr() {
10630 Ok(addr) => addr.port(),
10631 Err(_) => continue,
10632 };
10633 record_tcp_listener(server.guest_local_addr, host_port);
10634 let family = JavascriptSocketFamily::from_ip(server.guest_local_addr.ip());
10635 http_loopback_targets.insert(
10636 (family, server.guest_local_addr.port()),
10637 JavascriptHttpLoopbackTarget {
10638 process_id: process_id.to_owned(),
10639 server_id: *server_id,
10640 },
10641 );
10642 }
10643
10644 if let Ok(http2) = process.http2.shared.lock() {
10645 for server in http2.servers.values() {
10646 record_tcp_listener(server.guest_local_addr, server.actual_local_addr.port());
10647 }
10648 }
10649
10650 for socket in process.tcp_sockets.values() {
10651 let guest_addr = socket
10652 .kernel_socket_id
10653 .and_then(|socket_id| kernel.socket_get(socket_id))
10654 .and_then(|record| record.local_address().cloned())
10655 .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
10656 .unwrap_or(socket.guest_local_addr);
10657 let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10658 used_tcp_ports
10659 .entry(family)
10660 .or_default()
10661 .insert(guest_addr.port());
10662 }
10663
10664 for socket in process.udp_sockets.values() {
10665 let guest_addr = socket
10666 .kernel_socket_id
10667 .and_then(|socket_id| kernel.socket_get(socket_id))
10668 .and_then(|record| record.local_address().cloned())
10669 .and_then(|address| {
10670 resolve_udp_bind_addr(address.host(), address.port(), socket.family).ok()
10671 })
10672 .or_else(|| socket.local_addr());
10673 let Some(guest_addr) = guest_addr else {
10674 continue;
10675 };
10676 let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10677 used_udp_ports
10678 .entry(family)
10679 .or_default()
10680 .insert(guest_addr.port());
10681 if let Some(host_addr) = socket
10682 .socket
10683 .as_ref()
10684 .and_then(|socket| socket.local_addr().ok())
10685 {
10686 if is_loopback_ip(guest_addr.ip()) {
10687 udp_guest_to_host.insert((family, guest_addr.port()), host_addr.port());
10688 udp_host_to_guest.insert((family, host_addr.port()), guest_addr.port());
10689 }
10690 } else if socket.kernel_socket_id.is_some() && is_loopback_ip(guest_addr.ip()) {
10691 udp_guest_to_host.insert((family, guest_addr.port()), guest_addr.port());
10692 udp_host_to_guest.insert((family, guest_addr.port()), guest_addr.port());
10693 }
10694 }
10695
10696 for (child_process_id, child) in &process.child_processes {
10697 let child_id = format!("{process_id}/{child_process_id}");
10698 collect_javascript_socket_port_state(
10699 kernel,
10700 &child_id,
10701 child,
10702 tcp_guest_to_host,
10703 http_loopback_targets,
10704 udp_guest_to_host,
10705 udp_host_to_guest,
10706 used_tcp_ports,
10707 used_udp_ports,
10708 );
10709 }
10710}
10711
10712pub(crate) fn build_javascript_socket_path_context(
10713 vm: &VmState,
10714) -> Result<JavascriptSocketPathContext, SidecarError> {
10715 let mut loopback_exempt_ports = vm.create_loopback_exempt_ports.clone();
10716 loopback_exempt_ports.extend(vm.configuration.loopback_exempt_ports.iter().copied());
10717 let mut tcp_loopback_guest_to_host_ports = BTreeMap::new();
10718 let mut http_loopback_targets = BTreeMap::new();
10719 let mut udp_loopback_guest_to_host_ports = BTreeMap::new();
10720 let mut udp_loopback_host_to_guest_ports = BTreeMap::new();
10721 let mut used_tcp_guest_ports = BTreeMap::new();
10722 let mut used_udp_guest_ports = BTreeMap::new();
10723 for (process_id, process) in &vm.active_processes {
10724 collect_javascript_socket_port_state(
10725 &vm.kernel,
10726 process_id,
10727 process,
10728 &mut tcp_loopback_guest_to_host_ports,
10729 &mut http_loopback_targets,
10730 &mut udp_loopback_guest_to_host_ports,
10731 &mut udp_loopback_host_to_guest_ports,
10732 &mut used_tcp_guest_ports,
10733 &mut used_udp_guest_ports,
10734 );
10735 }
10736 Ok(JavascriptSocketPathContext {
10737 sandbox_root: vm.cwd.clone(),
10738 mounts: vm.configuration.mounts.clone(),
10739 listen_policy: vm.listen_policy,
10740 loopback_exempt_ports,
10741 tcp_loopback_guest_to_host_ports,
10742 http_loopback_targets,
10743 udp_loopback_guest_to_host_ports,
10744 udp_loopback_host_to_guest_ports,
10745 used_tcp_guest_ports,
10746 used_udp_guest_ports,
10747 })
10748}
10749
10750fn check_network_resource_limit(
10751 limit: Option<usize>,
10752 current: usize,
10753 additional: usize,
10754 label: &str,
10755) -> Result<(), SidecarError> {
10756 if let Some(limit) = limit {
10757 if current.saturating_add(additional) > limit {
10758 return Err(SidecarError::Execution(format!(
10759 "EAGAIN: maximum {label} count reached"
10760 )));
10761 }
10762 }
10763 Ok(())
10764}
10765
10766fn normalize_tcp_listen_host(
10767 host: Option<&str>,
10768) -> Result<(JavascriptSocketFamily, &'static str, &'static str), SidecarError> {
10769 match host.unwrap_or("127.0.0.1") {
10770 "127.0.0.1" | "localhost" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "127.0.0.1")),
10771 "::1" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::1")),
10772 "0.0.0.0" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "0.0.0.0")),
10773 "::" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::")),
10774 other => Err(SidecarError::Execution(format!(
10775 "EACCES: TCP listeners must bind to loopback or unspecified addresses, got {other}"
10776 ))),
10777 }
10778}
10779
10780fn normalize_udp_bind_host(
10781 host: Option<&str>,
10782 family: JavascriptUdpFamily,
10783) -> Result<(&'static str, &'static str, JavascriptSocketFamily), SidecarError> {
10784 match (family, host) {
10785 (JavascriptUdpFamily::Ipv4, None) | (JavascriptUdpFamily::Ipv4, Some("0.0.0.0")) => {
10786 Ok(("127.0.0.1", "0.0.0.0", JavascriptSocketFamily::Ipv4))
10787 }
10788 (JavascriptUdpFamily::Ipv4, Some("127.0.0.1"))
10789 | (JavascriptUdpFamily::Ipv4, Some("localhost")) => {
10790 Ok(("127.0.0.1", "127.0.0.1", JavascriptSocketFamily::Ipv4))
10791 }
10792 (JavascriptUdpFamily::Ipv6, None) | (JavascriptUdpFamily::Ipv6, Some("::")) => {
10793 Ok(("::1", "::", JavascriptSocketFamily::Ipv6))
10794 }
10795 (JavascriptUdpFamily::Ipv6, Some("::1"))
10796 | (JavascriptUdpFamily::Ipv6, Some("localhost")) => {
10797 Ok(("::1", "::1", JavascriptSocketFamily::Ipv6))
10798 }
10799 (JavascriptUdpFamily::Ipv4, Some(other)) => Err(SidecarError::Execution(format!(
10800 "EACCES: udp4 sockets must bind to 127.0.0.1 or 0.0.0.0, got {other}"
10801 ))),
10802 (JavascriptUdpFamily::Ipv6, Some(other)) => Err(SidecarError::Execution(format!(
10803 "EACCES: udp6 sockets must bind to ::1 or ::, got {other}"
10804 ))),
10805 }
10806}
10807
10808fn allocate_guest_listen_port(
10809 requested_port: u16,
10810 family: JavascriptSocketFamily,
10811 used_ports: &BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10812 policy: VmListenPolicy,
10813) -> Result<u16, SidecarError> {
10814 let is_allowed = |port: u16| {
10815 port >= policy.port_min
10816 && port <= policy.port_max
10817 && (policy.allow_privileged || port >= 1024)
10818 };
10819 let used = used_ports.get(&family);
10820
10821 if requested_port != 0 {
10822 if !is_allowed(requested_port) {
10823 let reason = if requested_port < 1024 && !policy.allow_privileged {
10824 format!(
10825 "EACCES: privileged listen port {requested_port} requires {}=true",
10826 VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY
10827 )
10828 } else {
10829 format!(
10830 "EACCES: listen port {requested_port} is outside the allowed range {}-{}",
10831 policy.port_min, policy.port_max
10832 )
10833 };
10834 return Err(SidecarError::Execution(reason));
10835 }
10836 if used.is_some_and(|ports| ports.contains(&requested_port)) {
10837 return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
10838 libc::EADDRINUSE,
10839 )));
10840 }
10841 return Ok(requested_port);
10842 }
10843
10844 let allocation_start = policy
10845 .port_min
10846 .max(if policy.allow_privileged { 1 } else { 1024 });
10847 for candidate in allocation_start..=policy.port_max {
10848 if used.is_some_and(|ports| ports.contains(&candidate)) {
10849 continue;
10850 }
10851 return Ok(candidate);
10852 }
10853
10854 Err(sidecar_net_error(std::io::Error::from_raw_os_error(
10855 libc::EADDRINUSE,
10856 )))
10857}
10858
10859fn socket_host_matches(requested: Option<&str>, actual: &str) -> bool {
10860 match requested {
10861 None => true,
10862 Some(requested) if requested == actual => true,
10863 Some(requested)
10864 if is_unspecified_socket_host(requested) && is_unspecified_socket_host(actual) =>
10865 {
10866 true
10867 }
10868 Some(requested) if is_unspecified_socket_host(requested) => is_loopback_socket_host(actual),
10869 Some(requested) if requested.eq_ignore_ascii_case("localhost") => {
10870 is_loopback_socket_host(actual)
10871 }
10872 _ => false,
10873 }
10874}
10875
10876fn parse_proc_net_entries(table_path: &str) -> Result<Vec<ProcNetEntry>, SidecarError> {
10877 let contents = match fs::read_to_string(table_path) {
10878 Ok(contents) => contents,
10879 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
10880 Err(error) => {
10881 return Err(SidecarError::Io(format!(
10882 "failed to inspect socket table {table_path}: {error}"
10883 )));
10884 }
10885 };
10886
10887 let mut entries = Vec::new();
10888 for line in contents.lines().skip(1) {
10889 let columns = line.split_whitespace().collect::<Vec<_>>();
10890 if columns.len() < 10 {
10891 continue;
10892 }
10893 let Some((host, port)) = parse_proc_ip_port(columns[1]) else {
10894 continue;
10895 };
10896 let Ok(inode) = columns[9].parse::<u64>() else {
10897 continue;
10898 };
10899 entries.push(ProcNetEntry {
10900 local_host: host,
10901 local_port: port,
10902 state: columns[3].to_owned(),
10903 inode,
10904 });
10905 }
10906
10907 Ok(entries)
10908}
10909
10910fn parse_proc_ip_port(value: &str) -> Option<(String, u16)> {
10911 let (raw_ip, raw_port) = value.split_once(':')?;
10912 let port = u16::from_str_radix(raw_port, 16).ok()?;
10913 let host = match raw_ip.len() {
10914 8 => {
10915 let raw = u32::from_str_radix(raw_ip, 16).ok()?;
10916 Ipv4Addr::from(raw.to_le_bytes()).to_string()
10917 }
10918 32 => {
10919 let mut bytes = [0_u8; 16];
10920 for (index, chunk) in raw_ip.as_bytes().chunks(8).enumerate() {
10921 let word = u32::from_str_radix(std::str::from_utf8(chunk).ok()?, 16).ok()?;
10922 bytes[index * 4..(index + 1) * 4].copy_from_slice(&word.to_le_bytes());
10923 }
10924 Ipv6Addr::from(bytes).to_string()
10925 }
10926 _ => return None,
10927 };
10928 Some((host, port))
10929}
10930
10931fn python_file_entrypoint(entrypoint: &str) -> Option<PathBuf> {
10932 let path = Path::new(entrypoint);
10933 (path.extension().and_then(|extension| extension.to_str()) == Some("py"))
10934 .then(|| path.to_path_buf())
10935}
10936
10937fn add_runtime_guest_path_mapping(
10938 env: &mut BTreeMap<String, String>,
10939 guest_path: &str,
10940 host_path: &Path,
10941) {
10942 let mut mappings = env
10943 .get("AGENTOS_GUEST_PATH_MAPPINGS")
10944 .and_then(|value| serde_json::from_str::<Vec<Value>>(value).ok())
10945 .unwrap_or_default();
10946 mappings.retain(|mapping| {
10947 mapping
10948 .get("guestPath")
10949 .and_then(Value::as_str)
10950 .map(|existing| normalize_path(existing) != normalize_path(guest_path))
10951 .unwrap_or(true)
10952 });
10953 mappings.push(json!({
10954 "guestPath": normalize_path(guest_path),
10955 "hostPath": host_path.display().to_string(),
10956 }));
10957 if let Ok(serialized) = serde_json::to_string(&mappings) {
10958 env.insert(String::from("AGENTOS_GUEST_PATH_MAPPINGS"), serialized);
10959 }
10960}
10961
10962fn add_runtime_host_access_path(
10963 env: &mut BTreeMap<String, String>,
10964 key: &str,
10965 host_path: &Path,
10966 expand: bool,
10967) {
10968 let existing = env
10969 .get(key)
10970 .and_then(|value| serde_json::from_str::<Vec<String>>(value).ok())
10971 .unwrap_or_default()
10972 .into_iter()
10973 .map(PathBuf::from)
10974 .collect::<Vec<_>>();
10975 let mut paths = existing;
10976 paths.push(host_path.to_path_buf());
10977 let normalized = if expand {
10978 expand_host_access_paths(&paths)
10979 } else {
10980 dedupe_host_paths(&paths)
10981 };
10982 let serialized = normalized
10983 .iter()
10984 .map(|path| path.to_string_lossy().into_owned())
10985 .collect::<Vec<_>>();
10986 if let Ok(serialized) = serde_json::to_string(&serialized) {
10987 env.insert(key.to_owned(), serialized);
10988 }
10989}
10990
10991fn is_path_like_specifier(specifier: &str) -> bool {
10994 specifier.starts_with('/')
10995 || specifier.starts_with("./")
10996 || specifier.starts_with("../")
10997 || specifier.starts_with("file:")
10998}
10999
11000fn execution_wasm_permission_tier(tier: WasmPermissionTier) -> ExecutionWasmPermissionTier {
11001 match tier {
11002 WasmPermissionTier::Full => ExecutionWasmPermissionTier::Full,
11003 WasmPermissionTier::ReadWrite => ExecutionWasmPermissionTier::ReadWrite,
11004 WasmPermissionTier::ReadOnly => ExecutionWasmPermissionTier::ReadOnly,
11005 WasmPermissionTier::Isolated => ExecutionWasmPermissionTier::Isolated,
11006 }
11007}
11008
11009fn resolve_wasm_permission_tier(
11010 vm: &VmState,
11011 command_name: Option<&str>,
11012 explicit_tier: Option<WasmPermissionTier>,
11013 entrypoint: &str,
11014) -> WasmPermissionTier {
11015 explicit_tier
11016 .or_else(|| command_name.and_then(|command| vm.command_permissions.get(command).copied()))
11017 .or_else(|| {
11018 Path::new(entrypoint)
11019 .file_name()
11020 .and_then(|name| name.to_str())
11021 .and_then(|command| vm.command_permissions.get(command).copied())
11022 })
11023 .unwrap_or(WasmPermissionTier::Full)
11024}
11025
11026fn tokenize_shell_free_command(command: &str) -> Vec<String> {
11027 command
11028 .split_whitespace()
11029 .filter(|segment| !segment.is_empty())
11030 .map(str::to_owned)
11031 .collect()
11032}
11033
11034fn is_posix_shell_builtin(command: &str) -> bool {
11035 matches!(
11036 command,
11037 "." | ":"
11038 | "break"
11039 | "cd"
11040 | "continue"
11041 | "eval"
11042 | "exec"
11043 | "exit"
11044 | "export"
11045 | "readonly"
11046 | "return"
11047 | "set"
11048 | "shift"
11049 | "times"
11050 | "trap"
11051 | "umask"
11052 | "unset"
11053 )
11054}
11055
11056fn shell_first_token_requires_shell(token: &str) -> bool {
11062 token.contains('=') || is_shell_reserved_word(token)
11063}
11064
11065fn is_shell_reserved_word(token: &str) -> bool {
11066 matches!(
11067 token,
11068 "if" | "then"
11069 | "elif"
11070 | "else"
11071 | "fi"
11072 | "for"
11073 | "in"
11074 | "do"
11075 | "done"
11076 | "while"
11077 | "until"
11078 | "case"
11079 | "esac"
11080 | "{"
11081 | "}"
11082 | "!"
11083 )
11084}
11085
11086fn command_requires_shell(command: &str) -> bool {
11087 command.chars().any(|ch| {
11088 matches!(
11089 ch,
11090 '|' | '&'
11091 | ';'
11092 | '<'
11093 | '>'
11094 | '('
11095 | ')'
11096 | '$'
11097 | '`'
11098 | '*'
11099 | '?'
11100 | '['
11101 | ']'
11102 | '{'
11103 | '}'
11104 | '~'
11105 | '\''
11106 | '"'
11107 | '\\'
11108 | '\n'
11109 )
11110 })
11111}
11112
11113fn host_mount_path_for_guest_path(vm: &VmState, guest_path: &str) -> Option<PathBuf> {
11114 let normalized = normalize_path(guest_path);
11115
11116 let mut mounts = vm
11117 .configuration
11118 .mounts
11119 .iter()
11120 .filter_map(|mount| {
11121 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11122 .then(|| {
11123 mount_config_host_path(&mount.plugin.config)
11124 .map(|host_path| (mount.guest_path.as_str(), host_path))
11125 })
11126 .flatten()
11127 })
11128 .collect::<Vec<_>>();
11129 mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
11130
11131 for (guest_root, host_root) in mounts {
11132 if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
11133 continue;
11134 }
11135
11136 let suffix = normalized
11137 .strip_prefix(guest_root)
11138 .unwrap_or_default()
11139 .trim_start_matches('/');
11140 let mut path = PathBuf::from(host_root);
11141 if !suffix.is_empty() {
11142 path.push(suffix);
11143 }
11144 return Some(path);
11145 }
11146
11147 None
11148}
11149
11150fn host_runtime_path_for_guest_path_with_env(
11151 vm: &VmState,
11152 runtime_env: &BTreeMap<String, String>,
11153 guest_path: &str,
11154 default_host_cwd: &Path,
11155) -> Option<PathBuf> {
11156 if let Some(path) = host_mount_path_for_guest_path(vm, guest_path) {
11157 return Some(path);
11158 }
11159 if let Some(path) = host_path_from_runtime_guest_mappings(runtime_env, guest_path) {
11160 return Some(path);
11161 }
11162
11163 let normalized = normalize_path(guest_path);
11164 let virtual_home = guest_virtual_home(vm);
11165
11166 if normalized == virtual_home || normalized.starts_with(&format!("{virtual_home}/")) {
11167 let suffix = normalized
11168 .strip_prefix(&virtual_home)
11169 .unwrap_or_default()
11170 .trim_start_matches('/');
11171 let mut host_path = default_host_cwd.to_path_buf();
11172 if !suffix.is_empty() {
11173 host_path.push(suffix);
11174 }
11175 return Some(host_path);
11176 }
11177
11178 None
11179}
11180
11181#[derive(Deserialize, Serialize)]
11182struct RuntimeGuestPathMapping {
11183 #[serde(rename = "guestPath")]
11184 guest_path: String,
11185 #[serde(rename = "hostPath")]
11186 host_path: String,
11187 #[serde(rename = "readOnly", default)]
11188 read_only: bool,
11189}
11190
11191pub(crate) fn host_path_from_runtime_guest_mappings(
11192 runtime_env: &BTreeMap<String, String>,
11193 guest_path: &str,
11194) -> Option<PathBuf> {
11195 let mappings = runtime_env
11196 .get("AGENTOS_GUEST_PATH_MAPPINGS")
11197 .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
11198 let normalized = normalize_path(guest_path);
11199
11200 let mut sorted_mappings = mappings
11201 .into_iter()
11202 .filter_map(|mapping| {
11203 (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
11204 normalize_path(&mapping.guest_path),
11205 PathBuf::from(mapping.host_path),
11206 ))
11207 })
11208 .collect::<Vec<_>>();
11209 sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.0.len()));
11210
11211 for (guest_root, mut host_root) in sorted_mappings {
11212 if guest_root != "/"
11213 && normalized != guest_root
11214 && !normalized.starts_with(&format!("{guest_root}/"))
11215 {
11216 continue;
11217 }
11218 if guest_root == "/" && !normalized.starts_with('/') {
11219 continue;
11220 }
11221
11222 if host_root.is_relative() {
11223 host_root = std::env::current_dir().ok()?.join(host_root);
11224 }
11225
11226 let suffix = if guest_root == "/" {
11227 normalized.trim_start_matches('/')
11228 } else {
11229 normalized
11230 .strip_prefix(&guest_root)
11231 .unwrap_or_default()
11232 .trim_start_matches('/')
11233 };
11234 if !suffix.is_empty() {
11235 host_root.push(suffix);
11236 }
11237 return Some(host_root);
11238 }
11239
11240 None
11241}
11242
11243fn guest_runtime_path_for_host_path(
11244 runtime_env: &BTreeMap<String, String>,
11245 virtual_home: &str,
11246 cwd: &Path,
11247 host_path: &str,
11248) -> Option<String> {
11249 let resolved = if host_path.starts_with("file://") {
11250 PathBuf::from(host_path.trim_start_matches("file://"))
11251 } else if host_path.starts_with("file:") {
11252 PathBuf::from(host_path.trim_start_matches("file:"))
11253 } else {
11254 let candidate = PathBuf::from(host_path);
11255 if candidate.is_absolute() {
11256 candidate
11257 } else if host_path.starts_with("./") || host_path.starts_with("../") {
11258 cwd.join(candidate)
11259 } else {
11260 return None;
11261 }
11262 };
11263 let normalized = normalize_host_path(&resolved);
11264
11265 if let Some(path) = guest_path_from_runtime_host_mappings(runtime_env, &normalized) {
11266 return Some(path);
11267 }
11268
11269 let normalized_cwd = normalize_host_path(cwd);
11270 if !path_is_within_root(&normalized, &normalized_cwd) {
11271 return None;
11272 }
11273
11274 let virtual_home = if virtual_home.starts_with('/') {
11275 virtual_home.to_string()
11276 } else {
11277 String::from("/root")
11278 };
11279 let suffix = normalized
11280 .strip_prefix(&normalized_cwd)
11281 .ok()?
11282 .to_string_lossy()
11283 .replace('\\', "/")
11284 .trim_start_matches('/')
11285 .to_owned();
11286
11287 Some(if suffix.is_empty() {
11288 virtual_home
11289 } else {
11290 normalize_path(&format!("{virtual_home}/{suffix}"))
11291 })
11292}
11293
11294fn guest_path_from_runtime_host_mappings(
11295 runtime_env: &BTreeMap<String, String>,
11296 host_path: &Path,
11297) -> Option<String> {
11298 let mappings = runtime_env
11299 .get("AGENTOS_GUEST_PATH_MAPPINGS")
11300 .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
11301 let normalized = normalize_host_path(host_path);
11302
11303 let mut sorted_mappings = mappings
11304 .into_iter()
11305 .filter_map(|mapping| {
11306 (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
11307 normalize_path(&mapping.guest_path),
11308 normalize_host_path(Path::new(&mapping.host_path)),
11309 ))
11310 })
11311 .collect::<Vec<_>>();
11312 sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.1.as_os_str().len()));
11313
11314 for (guest_root, host_root) in sorted_mappings {
11315 if !path_is_within_root(&normalized, &host_root) {
11316 continue;
11317 }
11318 let suffix = normalized
11319 .strip_prefix(&host_root)
11320 .ok()?
11321 .to_string_lossy()
11322 .replace('\\', "/")
11323 .trim_start_matches('/')
11324 .to_owned();
11325
11326 return Some(if suffix.is_empty() {
11327 guest_root
11328 } else if guest_root == "/" {
11329 normalize_path(&format!("/{suffix}"))
11330 } else {
11331 normalize_path(&format!("{guest_root}/{suffix}"))
11332 });
11333 }
11334
11335 None
11336}
11337
11338fn host_mount_path_for_guest_path_from_mounts(
11339 mounts: &[crate::protocol::MountDescriptor],
11340 guest_path: &str,
11341) -> Option<PathBuf> {
11342 let normalized = normalize_path(guest_path);
11343
11344 let mut host_mounts = mounts
11345 .iter()
11346 .filter_map(|mount| {
11347 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11348 .then(|| {
11349 mount_config_host_path(&mount.plugin.config)
11350 .map(|host_path| (mount.guest_path.as_str(), host_path))
11351 })
11352 .flatten()
11353 })
11354 .collect::<Vec<_>>();
11355 host_mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
11356
11357 for (guest_root, host_root) in host_mounts {
11358 if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
11359 continue;
11360 }
11361
11362 let suffix = normalized
11363 .strip_prefix(guest_root)
11364 .unwrap_or_default()
11365 .trim_start_matches('/');
11366 let mut path = PathBuf::from(host_root);
11367 if !suffix.is_empty() {
11368 path.push(suffix);
11369 }
11370 return Some(path);
11371 }
11372
11373 None
11374}
11375
11376#[cfg(test)]
11377mod host_mount_path_for_guest_path_from_mounts_tests {
11378 use super::host_mount_path_for_guest_path_from_mounts;
11379 use crate::protocol::{MountDescriptor, MountPluginDescriptor};
11380 use serde_json::json;
11381 use std::path::PathBuf;
11382
11383 #[test]
11384 fn resolves_module_access_mount_paths() {
11385 let mounts = vec![MountDescriptor {
11386 guest_path: String::from("/root/node_modules"),
11387 read_only: true,
11388 plugin: MountPluginDescriptor {
11389 id: String::from("module_access"),
11390 config: json!({
11391 "hostPath": "/tmp/workspace/node_modules",
11392 })
11393 .to_string(),
11394 },
11395 }];
11396
11397 let resolved =
11398 host_mount_path_for_guest_path_from_mounts(&mounts, "/root/node_modules/pkg/index.js")
11399 .expect("module_access mount should resolve");
11400
11401 assert_eq!(
11402 resolved,
11403 PathBuf::from("/tmp/workspace/node_modules/pkg/index.js")
11404 );
11405 }
11406}
11407
11408fn resolve_guest_socket_host_path(
11409 context: &JavascriptSocketPathContext,
11410 guest_path: &str,
11411) -> PathBuf {
11412 if let Some(path) = host_mount_path_for_guest_path_from_mounts(&context.mounts, guest_path) {
11413 return path;
11414 }
11415
11416 let normalized = normalize_path(guest_path);
11417 let mut host_path = context.sandbox_root.clone();
11418 let suffix = normalized.trim_start_matches('/');
11419 if !suffix.is_empty() {
11420 host_path.push(suffix);
11421 }
11422 host_path
11423}
11424
11425fn ensure_kernel_parent_directories(
11426 kernel: &mut SidecarKernel,
11427 path: &str,
11428) -> Result<(), SidecarError> {
11429 let parent = dirname(path);
11430 if parent != "/" && !kernel.exists(&parent).map_err(kernel_error)? {
11431 kernel.mkdir(&parent, true).map_err(kernel_error)?;
11432 }
11433 Ok(())
11434}
11435
11436pub(crate) fn sanitize_javascript_child_process_internal_bootstrap_env(
11440 env: &BTreeMap<String, String>,
11441) -> BTreeMap<String, String> {
11442 const ALLOWED_KEYS: &[&str] = &[
11443 "AGENTOS_ALLOWED_NODE_BUILTINS",
11444 "AGENTOS_GUEST_PATH_MAPPINGS",
11445 "AGENTOS_LOOPBACK_EXEMPT_PORTS",
11446 "AGENTOS_VIRTUAL_PROCESS_EXEC_PATH",
11447 "AGENTOS_VIRTUAL_PROCESS_UID",
11448 "AGENTOS_VIRTUAL_PROCESS_GID",
11449 "AGENTOS_VIRTUAL_PROCESS_VERSION",
11450 ];
11451
11452 env.iter()
11453 .filter(|(key, _)| {
11454 ALLOWED_KEYS.contains(&key.as_str()) || key.starts_with("AGENTOS_VIRTUAL_OS_")
11455 })
11456 .map(|(key, value)| (key.clone(), value.clone()))
11457 .collect()
11458}
11459
11460fn resolve_tcp_bind_addr(host: &str, port: u16) -> Result<SocketAddr, SidecarError> {
11465 (host, port)
11466 .to_socket_addrs()
11467 .map_err(sidecar_net_error)?
11468 .next()
11469 .ok_or_else(|| {
11470 SidecarError::Execution(format!("failed to resolve TCP bind address {host}:{port}"))
11471 })
11472}
11473
11474pub(crate) fn format_dns_resource(hostname: &str) -> String {
11475 format!("dns://{hostname}")
11476}
11477
11478pub(crate) fn format_tcp_resource(host: &str, port: u16) -> String {
11479 format!("tcp://{host}:{port}")
11480}
11481
11482fn is_loopback_ip(ip: IpAddr) -> bool {
11483 match ip {
11484 IpAddr::V4(ip) => ip.is_loopback(),
11485 IpAddr::V6(ip) => {
11486 ip.is_loopback()
11487 || ip
11488 .to_ipv4_mapped()
11489 .is_some_and(|mapped| mapped.is_loopback())
11490 }
11491 }
11492}
11493
11494fn loopback_cidr(ip: IpAddr) -> &'static str {
11495 match ip {
11496 IpAddr::V4(ip) if ip.is_loopback() => "127.0.0.0/8",
11497 IpAddr::V6(ip)
11498 if ip
11499 .to_ipv4_mapped()
11500 .is_some_and(|mapped| mapped.is_loopback()) =>
11501 {
11502 "127.0.0.0/8"
11503 }
11504 IpAddr::V6(_) => "::1/128",
11505 IpAddr::V4(_) => "127.0.0.0/8",
11506 }
11507}
11508
11509fn ipv4_compatible_embedded(ip: Ipv6Addr) -> Option<Ipv4Addr> {
11515 let segments = ip.segments();
11516 if segments[0..6].iter().any(|&s| s != 0) {
11517 return None;
11518 }
11519 let embedded = (u32::from(segments[6]) << 16) | u32::from(segments[7]);
11520 if embedded == 0 || embedded == 1 {
11523 return None;
11524 }
11525 Some(Ipv4Addr::from(embedded))
11526}
11527
11528fn restricted_non_loopback_ip_range(ip: IpAddr) -> Option<(&'static str, &'static str)> {
11529 match ip {
11530 IpAddr::V4(ip) => {
11531 if ip.is_unspecified() {
11532 return Some(("0.0.0.0/32", "unspecified"));
11535 }
11536 let [first, second, ..] = ip.octets();
11537 match (first, second) {
11538 (10, _) => Some(("10.0.0.0/8", "private")),
11539 (100, 64..=127) => Some(("100.64.0.0/10", "carrier-grade-nat")),
11540 (172, 16..=31) => Some(("172.16.0.0/12", "private")),
11541 (192, 168) => Some(("192.168.0.0/16", "private")),
11542 (169, 254) => Some(("169.254.0.0/16", "link-local")),
11543 (224..=239, _) => Some(("224.0.0.0/4", "multicast")),
11548 (240..=255, _) => Some(("240.0.0.0/4", "reserved")),
11549 _ => None,
11550 }
11551 }
11552 IpAddr::V6(ip) => {
11553 if let Some(mapped) = ip.to_ipv4_mapped() {
11554 return restricted_non_loopback_ip_range(IpAddr::V4(mapped));
11555 }
11556 if let Some(compat) = ipv4_compatible_embedded(ip) {
11563 return restricted_non_loopback_ip_range(IpAddr::V4(compat));
11564 }
11565
11566 if ip.is_unspecified() {
11567 return Some(("::/128", "unspecified"));
11570 }
11571
11572 let segments = ip.segments();
11573 if (segments[0] & 0xfe00) == 0xfc00 {
11574 return Some(("fc00::/7", "unique-local"));
11575 }
11576 if (segments[0] & 0xffc0) == 0xfe80 {
11577 return Some(("fe80::/10", "link-local"));
11578 }
11579 None
11580 }
11581 }
11582}
11583
11584fn blocked_dns_resolution_error(
11585 resource: &str,
11586 ip: IpAddr,
11587 cidr: &str,
11588 label: &str,
11589) -> SidecarError {
11590 SidecarError::Execution(format!(
11591 "EACCES: blocked outbound network access to {resource}: {ip} is within restricted {label} range {cidr}"
11592 ))
11593}
11594
11595fn blocked_loopback_connect_error(resource: &str, ip: IpAddr, port: u16) -> SidecarError {
11596 SidecarError::Execution(format!(
11597 "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}",
11598 loopback_cidr(ip)
11599 ))
11600}
11601
11602fn filter_dns_safe_ip_addrs(
11603 addresses: Vec<IpAddr>,
11604 hostname: &str,
11605) -> Result<Vec<IpAddr>, SidecarError> {
11606 let resource = format_dns_resource(hostname);
11607 let mut allowed = Vec::new();
11608 let mut blocked = None;
11609
11610 for ip in addresses {
11611 if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
11612 blocked.get_or_insert((ip, cidr, label));
11613 continue;
11614 }
11615 allowed.push(ip);
11616 }
11617
11618 if allowed.is_empty() {
11619 let (ip, cidr, label) = blocked.expect("blocked DNS results should capture a reason");
11620 return Err(blocked_dns_resolution_error(&resource, ip, cidr, label));
11621 }
11622
11623 Ok(allowed)
11624}
11625
11626fn loopback_connect_allowed(context: &JavascriptSocketPathContext, port: u16) -> bool {
11627 context.loopback_port_allowed(port)
11628}
11629
11630fn filter_tcp_connect_ip_addrs(
11631 addresses: Vec<IpAddr>,
11632 host: &str,
11633 port: u16,
11634 context: &JavascriptSocketPathContext,
11635) -> Result<Vec<IpAddr>, SidecarError> {
11636 let resource = format_tcp_resource(host, port);
11637 let mut allowed = Vec::new();
11638 let mut blocked = None;
11639
11640 for ip in addresses {
11641 if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
11642 blocked.get_or_insert_with(|| blocked_dns_resolution_error(&resource, ip, cidr, label));
11643 continue;
11644 }
11645 if is_loopback_ip(ip) && !loopback_connect_allowed(context, port) {
11646 blocked.get_or_insert_with(|| blocked_loopback_connect_error(&resource, ip, port));
11647 continue;
11648 }
11649 allowed.push(ip);
11650 }
11651
11652 if allowed.is_empty() {
11653 return Err(blocked.expect("blocked TCP connect results should capture a reason"));
11654 }
11655
11656 Ok(allowed)
11657}
11658
11659fn resolve_tcp_connect_addr<B>(
11660 bridge: &SharedBridge<B>,
11661 kernel: &SidecarKernel,
11662 vm_id: &str,
11663 dns: &VmDnsConfig,
11664 host: &str,
11665 port: u16,
11666 context: &JavascriptSocketPathContext,
11667) -> Result<ResolvedTcpConnectAddr, SidecarError>
11668where
11669 B: NativeSidecarBridge + Send + 'static,
11670 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11671{
11672 let allowed = filter_tcp_connect_ip_addrs(
11673 resolve_dns_ip_addrs(
11674 bridge,
11675 kernel,
11676 vm_id,
11677 dns,
11678 host,
11679 DnsLookupPolicy::SkipPermissions,
11680 )?,
11681 host,
11682 port,
11683 context,
11684 )?;
11685 let ip = allowed
11686 .iter()
11687 .copied()
11688 .find(|candidate| {
11689 let family = JavascriptSocketFamily::from_ip(*candidate);
11690 context.translate_tcp_loopback_port(family, port).is_some()
11691 })
11692 .or_else(|| allowed.iter().copied().find(IpAddr::is_ipv4))
11695 .or_else(|| allowed.first().copied())
11696 .ok_or_else(|| {
11697 SidecarError::Execution(format!("failed to resolve TCP address {host}:{port}"))
11698 })?;
11699 let family = JavascriptSocketFamily::from_ip(ip);
11700 let translated_loopback_port = context.translate_tcp_loopback_port(family, port);
11701 let use_kernel_loopback = is_loopback_ip(ip) && translated_loopback_port == Some(port);
11702 let actual_port = if is_loopback_ip(ip) {
11703 translated_loopback_port.unwrap_or(port)
11704 } else {
11705 port
11706 };
11707 Ok(ResolvedTcpConnectAddr {
11708 actual_addr: SocketAddr::new(ip, actual_port),
11709 guest_remote_addr: SocketAddr::new(ip, port),
11710 use_kernel_loopback,
11711 })
11712}
11713
11714fn resolve_dns_ip_addrs<B>(
11715 bridge: &SharedBridge<B>,
11716 kernel: &SidecarKernel,
11717 vm_id: &str,
11718 dns: &VmDnsConfig,
11719 hostname: &str,
11720 policy: DnsLookupPolicy,
11721) -> Result<Vec<IpAddr>, SidecarError>
11722where
11723 B: NativeSidecarBridge + Send + 'static,
11724 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11725{
11726 let resolution = match kernel.resolve_dns(hostname, policy) {
11727 Ok(resolution) => resolution,
11728 Err(error) => {
11729 let sidecar_error = kernel_error(error.clone());
11730 if error.code() != "EACCES" {
11731 emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
11732 }
11733 return Err(sidecar_error);
11734 }
11735 };
11736 emit_dns_resolution_event(
11737 bridge,
11738 vm_id,
11739 hostname,
11740 resolution.source(),
11741 resolution.addresses(),
11742 dns,
11743 );
11744 Ok(resolution.addresses().to_vec())
11745}
11746
11747fn resolve_dns_records<B>(
11748 bridge: &SharedBridge<B>,
11749 kernel: &SidecarKernel,
11750 vm_id: &str,
11751 dns: &VmDnsConfig,
11752 hostname: &str,
11753 record_type: RecordType,
11754 policy: DnsLookupPolicy,
11755) -> Result<DnsRecordResolution, SidecarError>
11756where
11757 B: NativeSidecarBridge + Send + 'static,
11758 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11759{
11760 let resolution = match kernel.resolve_dns_records(hostname, record_type, policy) {
11761 Ok(resolution) => resolution,
11762 Err(error) => {
11763 let sidecar_error = kernel_error(error.clone());
11764 if error.code() != "EACCES" {
11765 emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
11766 }
11767 return Err(sidecar_error);
11768 }
11769 };
11770 emit_dns_record_resolution_event(bridge, vm_id, hostname, &resolution, dns);
11771 Ok(resolution)
11772}
11773
11774fn filter_dns_ip_addrs(
11775 addresses: Vec<IpAddr>,
11776 family: Option<u8>,
11777) -> Result<Vec<IpAddr>, SidecarError> {
11778 let filtered: Vec<_> = match family.unwrap_or(0) {
11779 0 => addresses,
11780 4 => addresses
11781 .into_iter()
11782 .filter(|ip| matches!(ip, IpAddr::V4(_)))
11783 .collect(),
11784 6 => addresses
11785 .into_iter()
11786 .filter(|ip| matches!(ip, IpAddr::V6(_)))
11787 .collect(),
11788 other => {
11789 return Err(SidecarError::InvalidState(format!(
11790 "unsupported dns family {other}"
11791 )));
11792 }
11793 };
11794
11795 if filtered.is_empty() {
11796 return Err(SidecarError::Execution(String::from(
11797 "failed to resolve DNS address for requested family",
11798 )));
11799 }
11800
11801 Ok(filtered)
11802}
11803
11804fn resolve_udp_bind_addr(
11805 host: &str,
11806 port: u16,
11807 family: JavascriptUdpFamily,
11808) -> Result<SocketAddr, SidecarError> {
11809 (host, port)
11810 .to_socket_addrs()
11811 .map_err(sidecar_net_error)?
11812 .find(|addr| family.matches_addr(addr))
11813 .ok_or_else(|| {
11814 SidecarError::Execution(format!(
11815 "failed to resolve {} UDP bind address {host}:{port}",
11816 family.socket_type()
11817 ))
11818 })
11819}
11820
11821fn resolve_udp_addr<B>(request: UdpRemoteAddrRequest<'_, B>) -> Result<SocketAddr, SidecarError>
11822where
11823 B: NativeSidecarBridge + Send + 'static,
11824 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11825{
11826 let UdpRemoteAddrRequest {
11827 bridge,
11828 kernel,
11829 vm_id,
11830 dns,
11831 host,
11832 port,
11833 family,
11834 context,
11835 } = request;
11836 resolve_dns_ip_addrs(
11837 bridge,
11838 kernel,
11839 vm_id,
11840 dns,
11841 host,
11842 DnsLookupPolicy::SkipPermissions,
11843 )?
11844 .into_iter()
11845 .map(|ip| {
11846 let family_key = JavascriptSocketFamily::from_ip(ip);
11847 let actual_port = if is_loopback_ip(ip) {
11848 context
11849 .translate_udp_loopback_port(family_key, port)
11850 .unwrap_or(port)
11851 } else {
11852 port
11853 };
11854 SocketAddr::new(ip, actual_port)
11855 })
11856 .find(|addr| family.matches_addr(addr))
11857 .ok_or_else(|| {
11858 SidecarError::Execution(format!(
11859 "failed to resolve {} UDP address {host}:{port}",
11860 family.socket_type()
11861 ))
11862 })
11863}
11864
11865fn socket_addr_family(addr: &SocketAddr) -> &'static str {
11866 match addr {
11867 SocketAddr::V4(_) => "IPv4",
11868 SocketAddr::V6(_) => "IPv6",
11869 }
11870}
11871
11872fn javascript_net_timeout_value() -> Value {
11873 Value::String(String::from(JAVASCRIPT_NET_TIMEOUT_SENTINEL))
11874}
11875
11876fn javascript_net_json_string(value: Value, label: &str) -> Result<Value, SidecarError> {
11877 serde_json::to_string(&value)
11878 .map(Value::String)
11879 .map_err(|error| {
11880 SidecarError::InvalidState(format!("failed to serialize {label} payload: {error}"))
11881 })
11882}
11883
11884fn javascript_net_read_value(
11885 event: Option<JavascriptTcpSocketEvent>,
11886) -> Result<Value, SidecarError> {
11887 match event {
11888 Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(Value::String(
11889 base64::engine::general_purpose::STANDARD.encode(chunk),
11890 )),
11891 Some(JavascriptTcpSocketEvent::End | JavascriptTcpSocketEvent::Close { .. }) => {
11892 Ok(Value::Null)
11893 }
11894 Some(JavascriptTcpSocketEvent::Error { code, message }) => {
11895 let detail = code.unwrap_or_else(|| String::from("socket read"));
11896 Err(SidecarError::Execution(format!("{detail}: {message}")))
11897 }
11898 None => Ok(javascript_net_timeout_value()),
11899 }
11900}
11901
11902fn io_error_code(error: &std::io::Error) -> Option<String> {
11903 match error.raw_os_error() {
11904 Some(libc::EADDRINUSE) => Some(String::from("EADDRINUSE")),
11905 Some(libc::EADDRNOTAVAIL) => Some(String::from("EADDRNOTAVAIL")),
11906 Some(libc::ECONNREFUSED) => Some(String::from("ECONNREFUSED")),
11907 Some(libc::ECONNRESET) => Some(String::from("ECONNRESET")),
11908 Some(libc::EINVAL) => Some(String::from("EINVAL")),
11909 Some(libc::EPIPE) => Some(String::from("EPIPE")),
11910 Some(libc::ETIMEDOUT) => Some(String::from("ETIMEDOUT")),
11911 Some(libc::EHOSTUNREACH) => Some(String::from("EHOSTUNREACH")),
11912 Some(libc::ENETUNREACH) => Some(String::from("ENETUNREACH")),
11913 _ => None,
11914 }
11915}
11916
11917fn sidecar_net_error(error: std::io::Error) -> SidecarError {
11918 let message = match io_error_code(&error) {
11919 Some(code) => format!("{code}: {error}"),
11920 None => error.to_string(),
11921 };
11922 SidecarError::Execution(message)
11923}
11924
11925fn tls_provider() -> Arc<rustls::crypto::CryptoProvider> {
11926 Arc::new(aws_lc_rs::default_provider())
11927}
11928
11929fn tls_local_certificates(
11930 options: &JavascriptTlsBridgeOptions,
11931) -> Result<Vec<Vec<u8>>, SidecarError> {
11932 let Some(certificates) = options.cert.as_ref() else {
11933 return Ok(Vec::new());
11934 };
11935 tls_material_entries(certificates)
11936}
11937
11938fn tls_material_entries(material: &JavascriptTlsMaterial) -> Result<Vec<Vec<u8>>, SidecarError> {
11939 match material {
11940 JavascriptTlsMaterial::Single(entry) => tls_data_value(entry).map(|value| vec![value]),
11941 JavascriptTlsMaterial::Many(entries) => entries.iter().map(tls_data_value).collect(),
11942 }
11943}
11944
11945fn tls_data_value(value: &JavascriptTlsDataValue) -> Result<Vec<u8>, SidecarError> {
11946 match value {
11947 JavascriptTlsDataValue::Buffer { data } => base64::engine::general_purpose::STANDARD
11948 .decode(data)
11949 .map_err(|error| {
11950 SidecarError::InvalidState(format!("TLS material contains invalid base64: {error}"))
11951 }),
11952 JavascriptTlsDataValue::String { data } => Ok(data.as_bytes().to_vec()),
11953 }
11954}
11955
11956fn tls_certificates_from_material(
11957 material: &JavascriptTlsMaterial,
11958) -> Result<Vec<CertificateDer<'static>>, SidecarError> {
11959 let mut certificates = Vec::new();
11960 for entry in tls_material_entries(material)? {
11961 let mut reader = std::io::BufReader::new(Cursor::new(entry.clone()));
11962 let parsed = rustls_pemfile::certs(&mut reader)
11963 .collect::<Result<Vec<_>, _>>()
11964 .map_err(sidecar_net_error)?;
11965 if parsed.is_empty() {
11966 certificates.push(CertificateDer::from(entry));
11967 } else {
11968 certificates.extend(parsed);
11969 }
11970 }
11971 if certificates.is_empty() {
11972 return Err(SidecarError::InvalidState(String::from(
11973 "TLS certificate material did not contain any certificates",
11974 )));
11975 }
11976 Ok(certificates)
11977}
11978
11979fn tls_private_key_from_material(
11980 material: &JavascriptTlsMaterial,
11981) -> Result<PrivateKeyDer<'static>, SidecarError> {
11982 for entry in tls_material_entries(material)? {
11983 let mut reader = std::io::BufReader::new(Cursor::new(entry));
11984 if let Some(key) = rustls_pemfile::private_key(&mut reader).map_err(sidecar_net_error)? {
11985 return Ok(key);
11986 }
11987 }
11988 Err(SidecarError::InvalidState(String::from(
11989 "TLS private key material did not contain a supported key",
11990 )))
11991}
11992
11993fn tls_root_store(options: &JavascriptTlsBridgeOptions) -> Result<RootCertStore, SidecarError> {
11994 let mut roots = RootCertStore::empty();
11995 if let Some(ca) = options.ca.as_ref() {
11996 for certificate in tls_certificates_from_material(ca)? {
11997 roots.add(certificate).map_err(|error| {
11998 SidecarError::InvalidState(format!("failed to add TLS CA certificate: {error}"))
11999 })?;
12000 }
12001 return Ok(roots);
12002 }
12003
12004 for certificate in rustls_native_certs::load_native_certs().certs {
12005 roots.add(certificate).map_err(|error| {
12006 SidecarError::InvalidState(format!(
12007 "failed to add native TLS certificate to root store: {error}"
12008 ))
12009 })?;
12010 }
12011 Ok(roots)
12012}
12013
12014fn build_client_tls_stream(
12015 stream: TcpStream,
12016 options: &JavascriptTlsBridgeOptions,
12017) -> Result<rustls::StreamOwned<ClientConnection, TcpStream>, SidecarError> {
12018 let config = build_client_tls_config(options)?;
12019 let server_name = options
12020 .servername
12021 .clone()
12022 .unwrap_or_else(|| String::from("localhost"));
12023 let server_name = ServerName::try_from(server_name)
12024 .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
12025 stream
12026 .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12027 .map_err(sidecar_net_error)?;
12028 stream
12029 .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12030 .map_err(sidecar_net_error)?;
12031 let mut tls_stream = rustls::StreamOwned::new(
12032 ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
12033 SidecarError::Execution(format!("failed to start TLS client: {error}"))
12034 })?,
12035 stream,
12036 );
12037 while tls_stream.conn.is_handshaking() {
12038 tls_stream
12039 .conn
12040 .complete_io(&mut tls_stream.sock)
12041 .map_err(sidecar_net_error)?;
12042 }
12043 tls_stream
12044 .sock
12045 .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
12046 .map_err(sidecar_net_error)?;
12047 tls_stream
12048 .sock
12049 .set_write_timeout(None)
12050 .map_err(sidecar_net_error)?;
12051 Ok(tls_stream)
12052}
12053
12054fn build_client_loopback_tls_stream(
12055 transport: crate::state::LoopbackTlsEndpoint,
12056 options: &JavascriptTlsBridgeOptions,
12057) -> Result<rustls::StreamOwned<ClientConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
12058{
12059 let config = build_client_tls_config(options)?;
12060 let server_name = options
12061 .servername
12062 .clone()
12063 .unwrap_or_else(|| String::from("localhost"));
12064 let server_name = ServerName::try_from(server_name)
12065 .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
12066 let mut tls_stream = rustls::StreamOwned::new(
12067 ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
12068 SidecarError::Execution(format!("failed to start TLS client: {error}"))
12069 })?,
12070 transport,
12071 );
12072 match tls_stream.conn.complete_io(&mut tls_stream.sock) {
12073 Ok(_) => {}
12074 Err(error)
12075 if matches!(
12076 error.kind(),
12077 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12078 ) => {}
12079 Err(error) => return Err(sidecar_net_error(error)),
12080 }
12081 Ok(tls_stream)
12082}
12083
12084fn build_client_tls_config(
12085 options: &JavascriptTlsBridgeOptions,
12086) -> Result<ClientConfig, SidecarError> {
12087 let provider = tls_provider();
12088 let builder = ClientConfig::builder_with_provider(provider.clone())
12089 .with_safe_default_protocol_versions()
12090 .map_err(|error| {
12091 SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
12092 })?;
12093
12094 let mut config = if options.reject_unauthorized == Some(false) {
12095 let verifier = Arc::new(InsecureTlsVerifier {
12096 supported_schemes: provider
12097 .signature_verification_algorithms
12098 .supported_schemes(),
12099 });
12100 builder
12101 .dangerous()
12102 .with_custom_certificate_verifier(verifier)
12103 .with_no_client_auth()
12104 } else {
12105 builder
12106 .with_root_certificates(tls_root_store(options)?)
12107 .with_no_client_auth()
12108 };
12109
12110 if let Some(protocols) = options.alpn_protocols.as_ref() {
12111 config.alpn_protocols = protocols
12112 .iter()
12113 .map(|protocol| protocol.as_bytes().to_vec())
12114 .collect();
12115 }
12116 Ok(config)
12117}
12118
12119fn build_server_tls_stream(
12120 stream: TcpStream,
12121 options: &JavascriptTlsBridgeOptions,
12122) -> Result<rustls::StreamOwned<ServerConnection, TcpStream>, SidecarError> {
12123 let config = build_server_tls_config(options)?;
12124 stream
12125 .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12126 .map_err(sidecar_net_error)?;
12127 stream
12128 .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12129 .map_err(sidecar_net_error)?;
12130 let mut tls_stream = rustls::StreamOwned::new(
12131 ServerConnection::new(Arc::new(config)).map_err(|error| {
12132 SidecarError::Execution(format!("failed to start TLS server: {error}"))
12133 })?,
12134 stream,
12135 );
12136 while tls_stream.conn.is_handshaking() {
12137 tls_stream
12138 .conn
12139 .complete_io(&mut tls_stream.sock)
12140 .map_err(sidecar_net_error)?;
12141 }
12142 tls_stream
12143 .sock
12144 .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
12145 .map_err(sidecar_net_error)?;
12146 tls_stream
12147 .sock
12148 .set_write_timeout(None)
12149 .map_err(sidecar_net_error)?;
12150 Ok(tls_stream)
12151}
12152
12153fn build_server_loopback_tls_stream(
12154 transport: crate::state::LoopbackTlsEndpoint,
12155 options: &JavascriptTlsBridgeOptions,
12156) -> Result<rustls::StreamOwned<ServerConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
12157{
12158 let config = build_server_tls_config(options)?;
12159 Ok(rustls::StreamOwned::new(
12160 ServerConnection::new(Arc::new(config)).map_err(|error| {
12161 SidecarError::Execution(format!("failed to start TLS server: {error}"))
12162 })?,
12163 transport,
12164 ))
12165}
12166
12167fn build_server_tls_config(
12168 options: &JavascriptTlsBridgeOptions,
12169) -> Result<ServerConfig, SidecarError> {
12170 let certificates = tls_certificates_from_material(options.cert.as_ref().ok_or_else(|| {
12171 SidecarError::InvalidState(String::from("TLS server upgrade requires a certificate"))
12172 })?)?;
12173 let key = tls_private_key_from_material(options.key.as_ref().ok_or_else(|| {
12174 SidecarError::InvalidState(String::from("TLS server upgrade requires a private key"))
12175 })?)?;
12176
12177 let mut config = ServerConfig::builder_with_provider(tls_provider())
12178 .with_safe_default_protocol_versions()
12179 .map_err(|error| {
12180 SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
12181 })?
12182 .with_no_client_auth()
12183 .with_single_cert(certificates, key)
12184 .map_err(|error| {
12185 SidecarError::InvalidState(format!("invalid TLS server config: {error}"))
12186 })?;
12187
12188 if let Some(protocols) = options.alpn_protocols.as_ref() {
12189 config.alpn_protocols = protocols
12190 .iter()
12191 .map(|protocol| protocol.as_bytes().to_vec())
12192 .collect();
12193 }
12194 Ok(config)
12195}
12196
12197fn tls_protocol_name(version: rustls::ProtocolVersion) -> String {
12198 match version {
12199 rustls::ProtocolVersion::TLSv1_2 => String::from("TLSv1.2"),
12200 rustls::ProtocolVersion::TLSv1_3 => String::from("TLSv1.3"),
12201 other => other
12202 .as_str()
12203 .map(str::to_owned)
12204 .unwrap_or_else(|| format!("{other:?}")),
12205 }
12206}
12207
12208fn tls_cipher_bridge_value(suite: rustls::SupportedCipherSuite) -> Value {
12209 tls_bridge_object(vec![
12210 (
12211 "name",
12212 suite
12213 .suite()
12214 .as_str()
12215 .map(|value| Value::String(value.to_owned()))
12216 .unwrap_or(Value::Null),
12217 ),
12218 (
12219 "standardName",
12220 suite
12221 .suite()
12222 .as_str()
12223 .map(|value| Value::String(value.to_owned()))
12224 .unwrap_or(Value::Null),
12225 ),
12226 (
12227 "version",
12228 Value::String(if suite.tls13().is_some() {
12229 String::from("TLSv1.3")
12230 } else {
12231 String::from("TLSv1.2")
12232 }),
12233 ),
12234 ])
12235}
12236
12237fn tls_certificate_bridge_value(certificate: &[u8], detailed: bool) -> Value {
12238 let mut fields = vec![("raw", tls_bridge_buffer_value(certificate))];
12239 if detailed {
12240 fields.push(("issuerCertificate", tls_bridge_undefined_value()));
12241 }
12242 tls_bridge_object(fields)
12243}
12244
12245fn tls_bridge_buffer_value(bytes: &[u8]) -> Value {
12246 json!({
12247 "type": "buffer",
12248 "data": base64::engine::general_purpose::STANDARD.encode(bytes),
12249 })
12250}
12251
12252fn tls_bridge_object(entries: Vec<(&str, Value)>) -> Value {
12253 let value = entries
12254 .into_iter()
12255 .map(|(key, value)| (key.to_owned(), value))
12256 .collect::<serde_json::Map<String, Value>>();
12257 json!({
12258 "type": "object",
12259 "id": 1,
12260 "value": value,
12261 })
12262}
12263
12264fn tls_bridge_undefined_value() -> Value {
12265 json!({
12266 "type": "undefined",
12267 })
12268}
12269
12270fn spawn_tcp_socket_reader(
12271 stream: TcpStream,
12272 sender: Sender<JavascriptTcpSocketEvent>,
12273 tls_mode: Arc<AtomicBool>,
12274 saw_local_shutdown: Arc<AtomicBool>,
12275 saw_remote_end: Arc<AtomicBool>,
12276 close_notified: Arc<AtomicBool>,
12277) {
12278 thread::spawn(move || {
12279 let mut stream = stream;
12280 let mut buffer = vec![0_u8; 64 * 1024];
12281 loop {
12282 if tls_mode.load(Ordering::SeqCst) {
12283 break;
12284 }
12285 match stream.read(&mut buffer) {
12286 Ok(0) => {
12287 saw_remote_end.store(true, Ordering::SeqCst);
12288 let _ = sender.send(JavascriptTcpSocketEvent::End);
12289 if saw_local_shutdown.load(Ordering::SeqCst)
12290 && !close_notified.swap(true, Ordering::SeqCst)
12291 {
12292 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12293 }
12294 break;
12295 }
12296 Ok(bytes_read) => {
12297 if sender
12298 .send(JavascriptTcpSocketEvent::Data(
12299 buffer[..bytes_read].to_vec(),
12300 ))
12301 .is_err()
12302 {
12303 break;
12304 }
12305 }
12306 Err(error)
12307 if matches!(
12308 error.kind(),
12309 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12310 ) =>
12311 {
12312 continue;
12313 }
12314 Err(error) => {
12315 let code = io_error_code(&error);
12316 let _ = sender.send(JavascriptTcpSocketEvent::Error {
12317 code,
12318 message: error.to_string(),
12319 });
12320 if !close_notified.swap(true, Ordering::SeqCst) {
12321 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12322 }
12323 break;
12324 }
12325 }
12326 }
12327 });
12328}
12329
12330fn spawn_tls_socket_reader(
12331 tls_stream: Arc<Mutex<Option<ActiveTlsStream>>>,
12332 sender: Sender<JavascriptTcpSocketEvent>,
12333 saw_local_shutdown: Arc<AtomicBool>,
12334 saw_remote_end: Arc<AtomicBool>,
12335 close_notified: Arc<AtomicBool>,
12336) {
12337 thread::spawn(move || {
12338 let mut buffer = vec![0_u8; 64 * 1024];
12339 loop {
12340 let read_result = {
12341 let mut guard = match tls_stream.lock() {
12342 Ok(guard) => guard,
12343 Err(_) => return,
12344 };
12345 let Some(stream) = guard.as_mut() else {
12346 return;
12347 };
12348 stream.read(&mut buffer)
12349 };
12350
12351 match read_result {
12352 Ok(0) => {
12353 saw_remote_end.store(true, Ordering::SeqCst);
12354 let _ = sender.send(JavascriptTcpSocketEvent::End);
12355 if saw_local_shutdown.load(Ordering::SeqCst)
12356 && !close_notified.swap(true, Ordering::SeqCst)
12357 {
12358 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12359 }
12360 break;
12361 }
12362 Ok(bytes_read) => {
12363 if sender
12364 .send(JavascriptTcpSocketEvent::Data(
12365 buffer[..bytes_read].to_vec(),
12366 ))
12367 .is_err()
12368 {
12369 break;
12370 }
12371 }
12372 Err(error)
12373 if matches!(
12374 error.kind(),
12375 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12376 ) =>
12377 {
12378 std::thread::sleep(Duration::from_millis(1));
12381 continue;
12382 }
12383 Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
12384 saw_remote_end.store(true, Ordering::SeqCst);
12385 let _ = sender.send(JavascriptTcpSocketEvent::End);
12386 if saw_local_shutdown.load(Ordering::SeqCst)
12387 && !close_notified.swap(true, Ordering::SeqCst)
12388 {
12389 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12390 }
12391 break;
12392 }
12393 Err(error) => {
12394 let code = io_error_code(&error);
12395 let _ = sender.send(JavascriptTcpSocketEvent::Error {
12396 code,
12397 message: error.to_string(),
12398 });
12399 if !close_notified.swap(true, Ordering::SeqCst) {
12400 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12401 }
12402 break;
12403 }
12404 }
12405 }
12406 });
12407}
12408
12409fn spawn_unix_socket_reader(
12410 stream: UnixStream,
12411 sender: Sender<JavascriptTcpSocketEvent>,
12412 saw_local_shutdown: Arc<AtomicBool>,
12413 saw_remote_end: Arc<AtomicBool>,
12414 close_notified: Arc<AtomicBool>,
12415) {
12416 thread::spawn(move || {
12417 let mut stream = stream;
12418 let mut buffer = vec![0_u8; 64 * 1024];
12419 loop {
12420 match stream.read(&mut buffer) {
12421 Ok(0) => {
12422 saw_remote_end.store(true, Ordering::SeqCst);
12423 let _ = sender.send(JavascriptTcpSocketEvent::End);
12424 if saw_local_shutdown.load(Ordering::SeqCst)
12425 && !close_notified.swap(true, Ordering::SeqCst)
12426 {
12427 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12428 }
12429 break;
12430 }
12431 Ok(bytes_read) => {
12432 if sender
12433 .send(JavascriptTcpSocketEvent::Data(
12434 buffer[..bytes_read].to_vec(),
12435 ))
12436 .is_err()
12437 {
12438 break;
12439 }
12440 }
12441 Err(error) => {
12442 let code = io_error_code(&error);
12443 let _ = sender.send(JavascriptTcpSocketEvent::Error {
12444 code,
12445 message: error.to_string(),
12446 });
12447 if !close_notified.swap(true, Ordering::SeqCst) {
12448 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12449 }
12450 break;
12451 }
12452 }
12453 }
12454 });
12455}
12456
12457fn terminate_child_process_tree(kernel: &mut SidecarKernel, process: &mut ActiveProcess) {
12458 let sqlite_database_ids = process.sqlite_databases.keys().copied().collect::<Vec<_>>();
12459 for database_id in sqlite_database_ids {
12460 let _ = close_sqlite_database(kernel, process, database_id);
12461 }
12462 process.sqlite_statements.clear();
12463 process.http_servers.clear();
12464 process.pending_http_requests.clear();
12465 if let Ok(mut http2) = process.http2.shared.lock() {
12466 let sessions = http2.sessions.values().cloned().collect::<Vec<_>>();
12467 http2.server_events.clear();
12468 http2.session_events.clear();
12469 http2.streams.clear();
12470 http2.servers.clear();
12471 http2.sessions.clear();
12472 drop(http2);
12473 for session in sessions {
12474 let (respond_to, _rx) = mpsc::channel();
12475 let _ = session.command_tx.send(Http2SessionCommand::Close {
12476 abrupt: true,
12477 respond_to,
12478 });
12479 }
12480 }
12481
12482 let listener_ids = process.tcp_listeners.keys().cloned().collect::<Vec<_>>();
12483 for listener_id in listener_ids {
12484 if let Some(listener) = process.tcp_listeners.remove(&listener_id) {
12485 let _ = listener.close(kernel, process.kernel_pid);
12486 }
12487 }
12488
12489 let sockets = process.tcp_sockets.keys().cloned().collect::<Vec<_>>();
12490 for socket_id in sockets {
12491 if let Some(socket) = process.tcp_sockets.remove(&socket_id) {
12492 let _ = socket.close(kernel, process.kernel_pid);
12493 }
12494 }
12495
12496 let unix_listener_ids = process.unix_listeners.keys().cloned().collect::<Vec<_>>();
12497 for listener_id in unix_listener_ids {
12498 if let Some(listener) = process.unix_listeners.remove(&listener_id) {
12499 let _ = listener.close();
12500 }
12501 }
12502
12503 let unix_sockets = process.unix_sockets.keys().cloned().collect::<Vec<_>>();
12504 for socket_id in unix_sockets {
12505 if let Some(socket) = process.unix_sockets.remove(&socket_id) {
12506 let _ = socket.close();
12507 }
12508 }
12509
12510 let udp_socket_ids = process.udp_sockets.keys().cloned().collect::<Vec<_>>();
12511 for socket_id in udp_socket_ids {
12512 if let Some(mut socket) = process.udp_sockets.remove(&socket_id) {
12513 socket.close(kernel, process.kernel_pid);
12514 }
12515 }
12516
12517 let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
12518 for child_id in child_ids {
12519 let Some(mut child) = process.child_processes.remove(&child_id) else {
12520 continue;
12521 };
12522 terminate_child_process_tree(kernel, &mut child);
12523 let _ = kernel.kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, SIGTERM);
12524 let _ = signal_runtime_process(child.execution.child_pid(), SIGTERM);
12525 child.kernel_handle.finish(0);
12526 let _ = kernel.wait_and_reap(child.kernel_pid);
12527 }
12528}
12529
12530fn service_javascript_sqlite_sync_rpc(
12531 kernel: &mut SidecarKernel,
12532 process: &mut ActiveProcess,
12533 request: &JavascriptSyncRpcRequest,
12534) -> Result<Value, SidecarError> {
12535 match request.method.as_str() {
12536 "sqlite.constants" => Ok(json!({})),
12537 "sqlite.open" => sqlite_open_database(kernel, process, request),
12538 "sqlite.close" => {
12539 let database_id =
12540 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.close database id")?;
12541 close_sqlite_database(kernel, process, database_id)?;
12542 Ok(Value::Null)
12543 }
12544 "sqlite.exec" => sqlite_exec_database(kernel, process, request),
12545 "sqlite.query" => sqlite_query_database(process, request),
12546 "sqlite.prepare" => sqlite_prepare_statement(process, request),
12547 "sqlite.location" => {
12548 let database_id =
12549 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.location database id")?;
12550 let database = sqlite_database(process, database_id)?;
12551 Ok(database
12552 .vm_path
12553 .as_ref()
12554 .map(|path| Value::String(path.clone()))
12555 .unwrap_or(Value::Null))
12556 }
12557 "sqlite.checkpoint" => {
12558 let database_id =
12559 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.checkpoint database id")?;
12560 let kernel_pid = process.kernel_pid;
12561 let database = sqlite_database_mut(process, database_id)?;
12562 sqlite_sync_database(kernel, kernel_pid, database)?;
12563 Ok(Value::Null)
12564 }
12565 "sqlite.statement.run" => sqlite_run_statement(kernel, process, request),
12566 "sqlite.statement.get" => sqlite_get_statement(process, request),
12567 "sqlite.statement.all" | "sqlite.statement.iterate" => {
12568 sqlite_all_statement(process, request)
12569 }
12570 "sqlite.statement.columns" => sqlite_statement_columns(process, request),
12571 "sqlite.statement.setReturnArrays" => {
12572 let statement_id = javascript_sync_rpc_arg_u64(
12573 &request.args,
12574 0,
12575 "sqlite.statement.setReturnArrays statement id",
12576 )?;
12577 let enabled = javascript_sync_rpc_arg_bool(
12578 &request.args,
12579 1,
12580 "sqlite.statement.setReturnArrays enabled",
12581 )?;
12582 sqlite_statement_mut(process, statement_id)?.return_arrays = enabled;
12583 Ok(Value::Null)
12584 }
12585 "sqlite.statement.setReadBigInts" => {
12586 let statement_id = javascript_sync_rpc_arg_u64(
12587 &request.args,
12588 0,
12589 "sqlite.statement.setReadBigInts statement id",
12590 )?;
12591 let enabled = javascript_sync_rpc_arg_bool(
12592 &request.args,
12593 1,
12594 "sqlite.statement.setReadBigInts enabled",
12595 )?;
12596 sqlite_statement_mut(process, statement_id)?.read_bigints = enabled;
12597 Ok(Value::Null)
12598 }
12599 "sqlite.statement.setAllowBareNamedParameters" => {
12600 let statement_id = javascript_sync_rpc_arg_u64(
12601 &request.args,
12602 0,
12603 "sqlite.statement.setAllowBareNamedParameters statement id",
12604 )?;
12605 let enabled = javascript_sync_rpc_arg_bool(
12606 &request.args,
12607 1,
12608 "sqlite.statement.setAllowBareNamedParameters enabled",
12609 )?;
12610 sqlite_statement_mut(process, statement_id)?.allow_bare_named_parameters = enabled;
12611 Ok(Value::Null)
12612 }
12613 "sqlite.statement.setAllowUnknownNamedParameters" => {
12614 let statement_id = javascript_sync_rpc_arg_u64(
12615 &request.args,
12616 0,
12617 "sqlite.statement.setAllowUnknownNamedParameters statement id",
12618 )?;
12619 let enabled = javascript_sync_rpc_arg_bool(
12620 &request.args,
12621 1,
12622 "sqlite.statement.setAllowUnknownNamedParameters enabled",
12623 )?;
12624 sqlite_statement_mut(process, statement_id)?.allow_unknown_named_parameters = enabled;
12625 Ok(Value::Null)
12626 }
12627 "sqlite.statement.finalize" => {
12628 let statement_id = javascript_sync_rpc_arg_u64(
12629 &request.args,
12630 0,
12631 "sqlite.statement.finalize statement id",
12632 )?;
12633 process
12634 .sqlite_statements
12635 .remove(&statement_id)
12636 .ok_or_else(|| {
12637 SidecarError::InvalidState(format!(
12638 "sqlite statement handle not found: {statement_id}"
12639 ))
12640 })?;
12641 Ok(Value::Null)
12642 }
12643 other => Err(SidecarError::InvalidState(format!(
12644 "unsupported JavaScript sqlite sync RPC method {other}"
12645 ))),
12646 }
12647}
12648
12649fn sqlite_open_database(
12650 kernel: &mut SidecarKernel,
12651 process: &mut ActiveProcess,
12652 request: &JavascriptSyncRpcRequest,
12653) -> Result<Value, SidecarError> {
12654 ensure_per_process_state_handle_capacity(process.sqlite_databases.len(), "sqlite database")?;
12655 let path = request.args.first().and_then(Value::as_str);
12656 let vm_path = path.filter(|value| !value.is_empty() && *value != ":memory:");
12657 let options = request.args.get(1);
12658 let read_only = sqlite_option_bool(options, "readOnly").unwrap_or(false);
12659 let create = sqlite_option_bool(options, "create").unwrap_or(!read_only);
12660 let timeout_ms = sqlite_option_u64(options, "timeout");
12661
12662 process.next_sqlite_database_id += 1;
12663 let database_id = process.next_sqlite_database_id;
12664
12665 let host_path = if vm_path.is_some() {
12666 Some(
12667 std::env::temp_dir()
12668 .join(format!(
12669 "secure-exec-sidecar-sqlite-{}-{database_id}",
12670 process.kernel_pid
12671 ))
12672 .join("database.sqlite"),
12673 )
12674 } else {
12675 None
12676 };
12677
12678 if let Some(host_path) = host_path.as_ref() {
12679 if let Some(parent) = host_path.parent() {
12680 fs::create_dir_all(parent).map_err(|error| {
12681 SidecarError::Io(format!(
12682 "failed to prepare sqlite temp directory {}: {error}",
12683 parent.display()
12684 ))
12685 })?;
12686 }
12687 }
12688
12689 if let (Some(vm_path), Some(host_path)) = (vm_path, host_path.as_ref()) {
12690 if kernel
12691 .exists_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
12692 .map_err(kernel_error)?
12693 {
12694 let contents = kernel
12695 .read_file_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
12696 .map_err(kernel_error)?;
12697 fs::write(host_path, contents).map_err(|error| {
12698 SidecarError::Io(format!(
12699 "failed to materialize sqlite database {}: {error}",
12700 host_path.display()
12701 ))
12702 })?;
12703 } else if read_only && !create {
12704 return Err(SidecarError::InvalidState(format!(
12705 "sqlite database does not exist: {vm_path}"
12706 )));
12707 }
12708 }
12709
12710 let target = host_path
12711 .as_ref()
12712 .map(|path| path.to_string_lossy().into_owned())
12713 .unwrap_or_else(|| String::from(":memory:"));
12714 let mut flags = if read_only {
12715 SqliteOpenFlags::SQLITE_OPEN_READ_ONLY
12716 } else {
12717 SqliteOpenFlags::SQLITE_OPEN_READ_WRITE
12718 };
12719 if create && !read_only {
12720 flags |= SqliteOpenFlags::SQLITE_OPEN_CREATE;
12721 }
12722
12723 let connection = SqliteConnection::open_with_flags(&target, flags).map_err(|error| {
12724 SidecarError::InvalidState(format!(
12725 "sqlite database open failed for {}: {error}",
12726 vm_path.unwrap_or(":memory:")
12727 ))
12728 })?;
12729 if let Some(timeout_ms) = timeout_ms {
12730 connection
12731 .busy_timeout(Duration::from_millis(timeout_ms))
12732 .map_err(sqlite_error)?;
12733 }
12734 if host_path.is_some() && !read_only {
12735 let _ = connection.pragma_update(None, "journal_mode", "WAL");
12736 }
12737
12738 process.sqlite_databases.insert(
12739 database_id,
12740 ActiveSqliteDatabase {
12741 connection,
12742 host_path,
12743 vm_path: vm_path.map(String::from),
12744 dirty: false,
12745 transaction_depth: 0,
12746 read_only,
12747 },
12748 );
12749
12750 Ok(json!(database_id))
12751}
12752
12753fn sqlite_exec_database(
12754 kernel: &mut SidecarKernel,
12755 process: &mut ActiveProcess,
12756 request: &JavascriptSyncRpcRequest,
12757) -> Result<Value, SidecarError> {
12758 let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.exec database id")?;
12759 let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.exec sql")?;
12760 let kernel_pid = process.kernel_pid;
12761 let database = sqlite_database_mut(process, database_id)?;
12762 let before = database.connection.total_changes();
12763 database
12764 .connection
12765 .execute_batch(sql)
12766 .map_err(sqlite_error)?;
12767 mark_sqlite_mutation(database, sql);
12768 sqlite_sync_database(kernel, kernel_pid, database)?;
12769 Ok(json!(database
12770 .connection
12771 .total_changes()
12772 .saturating_sub(before)))
12773}
12774
12775fn sqlite_query_database(
12776 process: &mut ActiveProcess,
12777 request: &JavascriptSyncRpcRequest,
12778) -> Result<Value, SidecarError> {
12779 let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.query database id")?;
12780 let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.query sql")?;
12781 let params = request.args.get(2);
12782 let options = request.args.get(3);
12783 let return_arrays = sqlite_option_bool(options, "returnArrays").unwrap_or(false);
12784 let read_bigints = sqlite_option_bool(options, "readBigInts").unwrap_or(false);
12785 let database = sqlite_database_mut(process, database_id)?;
12786 sqlite_query_rows(
12787 &mut database.connection,
12788 sql,
12789 params,
12790 return_arrays,
12791 read_bigints,
12792 true,
12793 false,
12794 )
12795}
12796
12797fn sqlite_prepare_statement(
12798 process: &mut ActiveProcess,
12799 request: &JavascriptSyncRpcRequest,
12800) -> Result<Value, SidecarError> {
12801 ensure_per_process_state_handle_capacity(process.sqlite_statements.len(), "sqlite statement")?;
12802 let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.prepare database id")?;
12803 let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.prepare sql")?;
12804 let _ = sqlite_database(process, database_id)?;
12805 process.next_sqlite_statement_id += 1;
12806 let statement_id = process.next_sqlite_statement_id;
12807 process.sqlite_statements.insert(
12808 statement_id,
12809 ActiveSqliteStatement {
12810 database_id,
12811 sql: sql.to_owned(),
12812 return_arrays: false,
12813 read_bigints: false,
12814 allow_bare_named_parameters: false,
12815 allow_unknown_named_parameters: false,
12816 },
12817 );
12818 Ok(json!(statement_id))
12819}
12820
12821fn sqlite_run_statement(
12822 kernel: &mut SidecarKernel,
12823 process: &mut ActiveProcess,
12824 request: &JavascriptSyncRpcRequest,
12825) -> Result<Value, SidecarError> {
12826 let statement_id =
12827 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.run statement id")?;
12828 let params = request.args.get(1);
12829 let statement_state = sqlite_statement(process, statement_id)?.clone();
12830 let kernel_pid = process.kernel_pid;
12831 let database = sqlite_database_mut(process, statement_state.database_id)?;
12832 let before = database.connection.total_changes();
12833 {
12834 let mut statement = database
12835 .connection
12836 .prepare(&statement_state.sql)
12837 .map_err(sqlite_error)?;
12838 bind_sqlite_parameters(
12839 &mut statement,
12840 params,
12841 statement_state.allow_bare_named_parameters,
12842 statement_state.allow_unknown_named_parameters,
12843 )?;
12844 statement.raw_execute().map_err(sqlite_error)?;
12845 }
12846 let changes = database.connection.total_changes().saturating_sub(before);
12847 let last_insert_rowid = database.connection.last_insert_rowid();
12848 mark_sqlite_mutation(database, &statement_state.sql);
12849 sqlite_sync_database(kernel, kernel_pid, database)?;
12850 let result = json!({
12851 "changes": changes,
12852 "lastInsertRowid": encode_sqlite_integer(last_insert_rowid, true),
12853 });
12854 Ok(result)
12855}
12856
12857fn sqlite_get_statement(
12858 process: &mut ActiveProcess,
12859 request: &JavascriptSyncRpcRequest,
12860) -> Result<Value, SidecarError> {
12861 let statement_id =
12862 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.get statement id")?;
12863 let params = request.args.get(1);
12864 let statement_state = sqlite_statement(process, statement_id)?.clone();
12865 let database = sqlite_database_mut(process, statement_state.database_id)?;
12866 let rows = sqlite_query_rows(
12867 &mut database.connection,
12868 &statement_state.sql,
12869 params,
12870 statement_state.return_arrays,
12871 statement_state.read_bigints,
12872 statement_state.allow_bare_named_parameters,
12873 statement_state.allow_unknown_named_parameters,
12874 )?;
12875 Ok(rows
12876 .as_array()
12877 .and_then(|rows| rows.first().cloned())
12878 .unwrap_or(Value::Null))
12879}
12880
12881fn sqlite_all_statement(
12882 process: &mut ActiveProcess,
12883 request: &JavascriptSyncRpcRequest,
12884) -> Result<Value, SidecarError> {
12885 let statement_id =
12886 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.all statement id")?;
12887 let params = request.args.get(1);
12888 let statement_state = sqlite_statement(process, statement_id)?.clone();
12889 let database = sqlite_database_mut(process, statement_state.database_id)?;
12890 sqlite_query_rows(
12891 &mut database.connection,
12892 &statement_state.sql,
12893 params,
12894 statement_state.return_arrays,
12895 statement_state.read_bigints,
12896 statement_state.allow_bare_named_parameters,
12897 statement_state.allow_unknown_named_parameters,
12898 )
12899}
12900
12901fn sqlite_statement_columns(
12902 process: &mut ActiveProcess,
12903 request: &JavascriptSyncRpcRequest,
12904) -> Result<Value, SidecarError> {
12905 let statement_id =
12906 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.columns statement id")?;
12907 let statement_state = sqlite_statement(process, statement_id)?.clone();
12908 let database = sqlite_database_mut(process, statement_state.database_id)?;
12909 let statement = database
12910 .connection
12911 .prepare(&statement_state.sql)
12912 .map_err(sqlite_error)?;
12913 Ok(Value::Array(
12914 statement
12915 .column_names()
12916 .iter()
12917 .map(|name| json!({ "name": name }))
12918 .collect(),
12919 ))
12920}
12921
12922fn sqlite_query_rows(
12923 connection: &mut SqliteConnection,
12924 sql: &str,
12925 params: Option<&Value>,
12926 return_arrays: bool,
12927 read_bigints: bool,
12928 allow_bare_named_parameters: bool,
12929 allow_unknown_named_parameters: bool,
12930) -> Result<Value, SidecarError> {
12931 let mut statement = connection.prepare(sql).map_err(sqlite_error)?;
12932 let column_names = statement
12933 .column_names()
12934 .iter()
12935 .map(|name| (*name).to_owned())
12936 .collect::<Vec<_>>();
12937 let column_count = statement.column_count();
12938 bind_sqlite_parameters(
12939 &mut statement,
12940 params,
12941 allow_bare_named_parameters,
12942 allow_unknown_named_parameters,
12943 )?;
12944 let mut rows = statement.raw_query();
12945 let mut encoded_rows = Vec::new();
12946 while let Some(row) = rows.next().map_err(sqlite_error)? {
12947 encoded_rows.push(encode_sqlite_row(
12948 row,
12949 &column_names,
12950 column_count,
12951 return_arrays,
12952 read_bigints,
12953 )?);
12954 }
12955 Ok(Value::Array(encoded_rows))
12956}
12957
12958fn encode_sqlite_row(
12959 row: &rusqlite::Row<'_>,
12960 column_names: &[String],
12961 column_count: usize,
12962 return_arrays: bool,
12963 read_bigints: bool,
12964) -> Result<Value, SidecarError> {
12965 if return_arrays {
12966 let mut values = Vec::with_capacity(column_count);
12967 for index in 0..column_count {
12968 values.push(encode_sqlite_value_ref(
12969 row.get_ref(index).map_err(sqlite_error)?,
12970 read_bigints,
12971 )?);
12972 }
12973 return Ok(Value::Array(values));
12974 }
12975
12976 let mut object = Map::with_capacity(column_count);
12977 for (index, name) in column_names.iter().enumerate() {
12978 object.insert(
12979 name.clone(),
12980 encode_sqlite_value_ref(row.get_ref(index).map_err(sqlite_error)?, read_bigints)?,
12981 );
12982 }
12983 Ok(Value::Object(object))
12984}
12985
12986fn encode_sqlite_value_ref(
12987 value: SqliteValueRef<'_>,
12988 read_bigints: bool,
12989) -> Result<Value, SidecarError> {
12990 Ok(match value {
12991 SqliteValueRef::Null => Value::Null,
12992 SqliteValueRef::Integer(number) => encode_sqlite_integer(number, read_bigints),
12993 SqliteValueRef::Real(number) => json!(number),
12994 SqliteValueRef::Text(text) => Value::String(String::from_utf8_lossy(text).into_owned()),
12995 SqliteValueRef::Blob(bytes) => json!({
12996 "__agentosSqliteType": "uint8array",
12997 "value": base64::engine::general_purpose::STANDARD.encode(bytes),
12998 }),
12999 })
13000}
13001
13002fn encode_sqlite_integer(number: i64, read_bigints: bool) -> Value {
13003 if read_bigints || number.abs() > SQLITE_JS_SAFE_INTEGER_MAX {
13004 json!({
13005 "__agentosSqliteType": "bigint",
13006 "value": number.to_string(),
13007 })
13008 } else {
13009 json!(number)
13010 }
13011}
13012
13013fn bind_sqlite_parameters(
13014 statement: &mut SqliteStatement<'_>,
13015 params: Option<&Value>,
13016 allow_bare_named_parameters: bool,
13017 allow_unknown_named_parameters: bool,
13018) -> Result<(), SidecarError> {
13019 let Some(params) = params else {
13020 return Ok(());
13021 };
13022 match params {
13023 Value::Null => Ok(()),
13024 Value::Array(values) => {
13025 for (index, value) in values.iter().enumerate() {
13026 statement
13027 .raw_bind_parameter(index + 1, decode_sqlite_parameter(value)?)
13028 .map_err(sqlite_error)?;
13029 }
13030 Ok(())
13031 }
13032 Value::Object(map)
13033 if map
13034 .get("__agentosSqliteType")
13035 .and_then(Value::as_str)
13036 .is_none() =>
13037 {
13038 for (key, value) in map {
13039 let index =
13040 resolve_sqlite_parameter_index(statement, key, allow_bare_named_parameters)?;
13041 let Some(index) = index else {
13042 if allow_unknown_named_parameters {
13043 continue;
13044 }
13045 return Err(SidecarError::InvalidState(format!(
13046 "sqlite named parameter not found: {key}"
13047 )));
13048 };
13049 statement
13050 .raw_bind_parameter(index, decode_sqlite_parameter(value)?)
13051 .map_err(sqlite_error)?;
13052 }
13053 Ok(())
13054 }
13055 other => statement
13056 .raw_bind_parameter(1, decode_sqlite_parameter(other)?)
13057 .map_err(sqlite_error),
13058 }
13059}
13060
13061fn resolve_sqlite_parameter_index(
13062 statement: &mut SqliteStatement<'_>,
13063 key: &str,
13064 allow_bare_named_parameters: bool,
13065) -> Result<Option<usize>, SidecarError> {
13066 let mut candidates = vec![key.to_owned()];
13067 if allow_bare_named_parameters
13068 && !key.starts_with(':')
13069 && !key.starts_with('@')
13070 && !key.starts_with('$')
13071 {
13072 candidates.push(format!(":{key}"));
13073 candidates.push(format!("@{key}"));
13074 candidates.push(format!("${key}"));
13075 }
13076 for candidate in candidates {
13077 if let Some(index) = statement
13078 .parameter_index(&candidate)
13079 .map_err(sqlite_error)?
13080 {
13081 return Ok(Some(index));
13082 }
13083 }
13084 Ok(None)
13085}
13086
13087fn decode_sqlite_parameter(value: &Value) -> Result<rusqlite::types::Value, SidecarError> {
13088 Ok(match value {
13089 Value::Null => rusqlite::types::Value::Null,
13090 Value::Bool(value) => rusqlite::types::Value::Integer(i64::from(*value)),
13091 Value::Number(value) => match (value.as_i64(), value.as_f64()) {
13092 (Some(integer), _) => rusqlite::types::Value::Integer(integer),
13093 (_, Some(real)) => rusqlite::types::Value::Real(real),
13094 _ => {
13095 return Err(SidecarError::InvalidState(String::from(
13096 "sqlite parameter number is not representable",
13097 )));
13098 }
13099 },
13100 Value::String(value) => rusqlite::types::Value::Text(value.clone()),
13101 Value::Array(_) => {
13102 return Err(SidecarError::InvalidState(String::from(
13103 "sqlite parameters do not support nested arrays",
13104 )));
13105 }
13106 Value::Object(map) => match map.get("__agentosSqliteType").and_then(Value::as_str) {
13107 Some("bigint") => rusqlite::types::Value::Integer(
13108 map.get("value")
13109 .and_then(Value::as_str)
13110 .ok_or_else(|| {
13111 SidecarError::InvalidState(String::from(
13112 "sqlite bigint parameter missing string value",
13113 ))
13114 })?
13115 .parse::<i64>()
13116 .map_err(|error| {
13117 SidecarError::InvalidState(format!(
13118 "sqlite bigint parameter is not a signed 64-bit integer: {error}"
13119 ))
13120 })?,
13121 ),
13122 Some("uint8array") => rusqlite::types::Value::Blob(
13123 base64::engine::general_purpose::STANDARD
13124 .decode(map.get("value").and_then(Value::as_str).ok_or_else(|| {
13125 SidecarError::InvalidState(String::from(
13126 "sqlite blob parameter missing base64 value",
13127 ))
13128 })?)
13129 .map_err(|error| {
13130 SidecarError::InvalidState(format!(
13131 "sqlite blob parameter contains invalid base64: {error}"
13132 ))
13133 })?,
13134 ),
13135 Some(other) => {
13136 return Err(SidecarError::InvalidState(format!(
13137 "unsupported sqlite tagged parameter type {other}"
13138 )));
13139 }
13140 None => {
13141 return Err(SidecarError::InvalidState(String::from(
13142 "sqlite named parameter objects must be passed as the top-level params object",
13143 )));
13144 }
13145 },
13146 })
13147}
13148
13149fn close_sqlite_database(
13150 kernel: &mut SidecarKernel,
13151 process: &mut ActiveProcess,
13152 database_id: u64,
13153) -> Result<(), SidecarError> {
13154 let mut database = process
13155 .sqlite_databases
13156 .remove(&database_id)
13157 .ok_or_else(|| {
13158 SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13159 })?;
13160 process
13161 .sqlite_statements
13162 .retain(|_, statement| statement.database_id != database_id);
13163 sqlite_sync_database(kernel, process.kernel_pid, &mut database)?;
13164 let host_path = database.host_path.clone();
13165 drop(database);
13166 cleanup_sqlite_host_artifacts(host_path.as_deref())?;
13167 Ok(())
13168}
13169
13170fn ensure_per_process_state_handle_capacity(len: usize, label: &str) -> Result<(), SidecarError> {
13171 if len >= MAX_PER_PROCESS_STATE_HANDLES {
13172 return Err(SidecarError::InvalidState(format!(
13173 "{label} handle limit exceeded: limit is {MAX_PER_PROCESS_STATE_HANDLES}"
13174 )));
13175 }
13176 Ok(())
13177}
13178
13179fn sqlite_sync_database(
13180 kernel: &mut SidecarKernel,
13181 kernel_pid: u32,
13182 database: &mut ActiveSqliteDatabase,
13183) -> Result<(), SidecarError> {
13184 if !database.dirty
13185 || database.transaction_depth > 0
13186 || database.read_only
13187 || database.host_path.is_none()
13188 || database.vm_path.is_none()
13189 {
13190 return Ok(());
13191 }
13192
13193 let _ = database
13194 .connection
13195 .execute_batch("PRAGMA wal_checkpoint(TRUNCATE)");
13196 let host_path = database.host_path.as_ref().expect("sqlite host path");
13197 if !host_path.exists() {
13198 return Ok(());
13199 }
13200 ensure_vm_parent_dir(
13201 kernel,
13202 kernel_pid,
13203 database.vm_path.as_deref().expect("sqlite vm path"),
13204 )?;
13205 let contents = fs::read(host_path).map_err(|error| {
13206 SidecarError::Io(format!(
13207 "failed to read sqlite temp database {}: {error}",
13208 host_path.display()
13209 ))
13210 })?;
13211 kernel
13212 .write_file_for_process(
13213 EXECUTION_DRIVER_NAME,
13214 kernel_pid,
13215 database.vm_path.as_deref().expect("sqlite vm path"),
13216 contents,
13217 None,
13218 )
13219 .map_err(kernel_error)?;
13220 database.dirty = false;
13221 Ok(())
13222}
13223
13224fn cleanup_sqlite_host_artifacts(host_path: Option<&Path>) -> Result<(), SidecarError> {
13225 let Some(host_path) = host_path else {
13226 return Ok(());
13227 };
13228 let parent = host_path.parent().map(PathBuf::from);
13229 for suffix in ["", "-wal", "-shm"] {
13230 let path = PathBuf::from(format!("{}{}", host_path.display(), suffix));
13231 if path.exists() {
13232 fs::remove_file(&path).map_err(|error| {
13233 SidecarError::Io(format!(
13234 "failed to remove sqlite temp artifact {}: {error}",
13235 path.display()
13236 ))
13237 })?;
13238 }
13239 }
13240 if let Some(parent) = parent {
13241 let _ = fs::remove_dir_all(parent);
13242 }
13243 Ok(())
13244}
13245
13246fn ensure_vm_parent_dir(
13247 kernel: &mut SidecarKernel,
13248 kernel_pid: u32,
13249 path: &str,
13250) -> Result<(), SidecarError> {
13251 let parent = dirname(path);
13252 if parent == "/" || parent == "." {
13253 return Ok(());
13254 }
13255 let mut current = String::new();
13256 for segment in parent.split('/').filter(|segment| !segment.is_empty()) {
13257 current.push('/');
13258 current.push_str(segment);
13259 if !kernel
13260 .exists_for_process(EXECUTION_DRIVER_NAME, kernel_pid, ¤t)
13261 .map_err(kernel_error)?
13262 {
13263 kernel
13264 .mkdir_for_process(EXECUTION_DRIVER_NAME, kernel_pid, ¤t, false, None)
13265 .map_err(kernel_error)?;
13266 }
13267 }
13268 Ok(())
13269}
13270
13271fn sqlite_database(
13272 process: &ActiveProcess,
13273 database_id: u64,
13274) -> Result<&ActiveSqliteDatabase, SidecarError> {
13275 process.sqlite_databases.get(&database_id).ok_or_else(|| {
13276 SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13277 })
13278}
13279
13280fn sqlite_database_mut(
13281 process: &mut ActiveProcess,
13282 database_id: u64,
13283) -> Result<&mut ActiveSqliteDatabase, SidecarError> {
13284 process
13285 .sqlite_databases
13286 .get_mut(&database_id)
13287 .ok_or_else(|| {
13288 SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13289 })
13290}
13291
13292fn sqlite_statement(
13293 process: &ActiveProcess,
13294 statement_id: u64,
13295) -> Result<&ActiveSqliteStatement, SidecarError> {
13296 process.sqlite_statements.get(&statement_id).ok_or_else(|| {
13297 SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
13298 })
13299}
13300
13301fn sqlite_statement_mut(
13302 process: &mut ActiveProcess,
13303 statement_id: u64,
13304) -> Result<&mut ActiveSqliteStatement, SidecarError> {
13305 process
13306 .sqlite_statements
13307 .get_mut(&statement_id)
13308 .ok_or_else(|| {
13309 SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
13310 })
13311}
13312
13313fn mark_sqlite_mutation(database: &mut ActiveSqliteDatabase, sql: &str) {
13314 let normalized = sql.trim_start().to_ascii_lowercase();
13315 if normalized.starts_with("begin") || normalized.starts_with("savepoint") {
13316 database.dirty = true;
13317 database.transaction_depth += 1;
13318 return;
13319 }
13320 if normalized.starts_with("commit") || normalized.starts_with("release savepoint") {
13321 database.dirty = true;
13322 database.transaction_depth = database.transaction_depth.saturating_sub(1);
13323 return;
13324 }
13325 if normalized.starts_with("rollback") && !normalized.starts_with("rollback to") {
13326 database.dirty = true;
13327 database.transaction_depth = database.transaction_depth.saturating_sub(1);
13328 return;
13329 }
13330 if normalized.starts_with("insert")
13331 || normalized.starts_with("update")
13332 || normalized.starts_with("delete")
13333 || normalized.starts_with("replace")
13334 || normalized.starts_with("create")
13335 || normalized.starts_with("alter")
13336 || normalized.starts_with("drop")
13337 || normalized.starts_with("vacuum")
13338 || normalized.starts_with("reindex")
13339 || normalized.starts_with("analyze")
13340 || normalized.starts_with("attach")
13341 || normalized.starts_with("detach")
13342 || normalized.starts_with("pragma")
13343 {
13344 database.dirty = true;
13345 }
13346}
13347
13348fn sqlite_option_bool(options: Option<&Value>, key: &str) -> Option<bool> {
13349 options
13350 .and_then(|value| value.get(key))
13351 .and_then(Value::as_bool)
13352}
13353
13354fn sqlite_option_u64(options: Option<&Value>, key: &str) -> Option<u64> {
13355 options
13356 .and_then(|value| value.get(key))
13357 .and_then(Value::as_u64)
13358}
13359
13360fn sqlite_error(error: rusqlite::Error) -> SidecarError {
13361 SidecarError::InvalidState(format!("sqlite error: {error}"))
13362}
13363
13364pub(crate) fn javascript_sync_rpc_arg_str<'a>(
13365 args: &'a [Value],
13366 index: usize,
13367 label: &str,
13368) -> Result<&'a str, SidecarError> {
13369 args.get(index)
13370 .and_then(Value::as_str)
13371 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a string argument")))
13372}
13373
13374pub(crate) fn javascript_sync_rpc_arg_bool(
13375 args: &[Value],
13376 index: usize,
13377 label: &str,
13378) -> Result<bool, SidecarError> {
13379 args.get(index)
13380 .and_then(Value::as_bool)
13381 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a boolean argument")))
13382}
13383
13384pub(crate) fn javascript_sync_rpc_encoding(args: &[Value]) -> Option<String> {
13385 args.get(1).and_then(|value| {
13386 value.as_str().map(str::to_owned).or_else(|| {
13387 value
13388 .get("encoding")
13389 .and_then(Value::as_str)
13390 .map(str::to_owned)
13391 })
13392 })
13393}
13394
13395pub(crate) fn javascript_sync_rpc_option_bool(
13396 args: &[Value],
13397 index: usize,
13398 key: &str,
13399) -> Option<bool> {
13400 let value = args.get(index)?;
13401 if key == "recursive" {
13402 if let Some(boolean) = value.as_bool() {
13403 return Some(boolean);
13404 }
13405 }
13406 value.get(key).and_then(Value::as_bool)
13407}
13408
13409pub(crate) fn javascript_sync_rpc_option_u32(
13410 args: &[Value],
13411 index: usize,
13412 key: &str,
13413) -> Result<Option<u32>, SidecarError> {
13414 let Some(value) = args.get(index).and_then(|value| {
13415 if value.is_object() {
13416 value.get(key)
13417 } else if key == "mode" && value.is_number() {
13418 Some(value)
13419 } else {
13420 None
13421 }
13422 }) else {
13423 return Ok(None);
13424 };
13425 if value.is_null() {
13426 return Ok(None);
13427 }
13428
13429 let numeric = value
13430 .as_u64()
13431 .or_else(|| {
13432 value
13433 .as_f64()
13434 .filter(|number| number.is_finite() && *number >= 0.0)
13435 .map(|number| number as u64)
13436 })
13437 .ok_or_else(|| SidecarError::InvalidState(format!("{key} must be numeric")))?;
13438
13439 u32::try_from(numeric)
13440 .map(Some)
13441 .map_err(|_| SidecarError::InvalidState(format!("{key} must fit within u32")))
13442}
13443
13444pub(crate) fn javascript_sync_rpc_arg_u32(
13445 args: &[Value],
13446 index: usize,
13447 label: &str,
13448) -> Result<u32, SidecarError> {
13449 let value = javascript_sync_rpc_arg_u64(args, index, label)?;
13450 u32::try_from(value)
13451 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
13452}
13453
13454pub(crate) fn javascript_sync_rpc_arg_i32(
13455 args: &[Value],
13456 index: usize,
13457 label: &str,
13458) -> Result<i32, SidecarError> {
13459 let Some(value) = args.get(index) else {
13460 return Err(SidecarError::InvalidState(format!("{label} is required")));
13461 };
13462
13463 let numeric = value
13464 .as_i64()
13465 .or_else(|| {
13466 value
13467 .as_f64()
13468 .filter(|number| number.is_finite())
13469 .map(|number| number as i64)
13470 })
13471 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))?;
13472
13473 i32::try_from(numeric)
13474 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within i32")))
13475}
13476
13477pub(crate) fn javascript_sync_rpc_arg_u32_optional(
13478 args: &[Value],
13479 index: usize,
13480 label: &str,
13481) -> Result<Option<u32>, SidecarError> {
13482 javascript_sync_rpc_arg_u64_optional(args, index, label)?
13483 .map(|value| {
13484 u32::try_from(value)
13485 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
13486 })
13487 .transpose()
13488}
13489
13490pub(crate) fn javascript_sync_rpc_arg_u64(
13491 args: &[Value],
13492 index: usize,
13493 label: &str,
13494) -> Result<u64, SidecarError> {
13495 let Some(value) = args.get(index) else {
13496 return Err(SidecarError::InvalidState(format!("{label} is required")));
13497 };
13498
13499 value
13500 .as_u64()
13501 .or_else(|| {
13502 value
13503 .as_f64()
13504 .filter(|number| number.is_finite() && *number >= 0.0)
13505 .map(|number| number as u64)
13506 })
13507 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))
13508}
13509
13510pub(crate) fn javascript_sync_rpc_arg_u64_optional(
13511 args: &[Value],
13512 index: usize,
13513 label: &str,
13514) -> Result<Option<u64>, SidecarError> {
13515 let Some(value) = args.get(index) else {
13516 return Ok(None);
13517 };
13518 if value.is_null() {
13519 return Ok(None);
13520 }
13521 javascript_sync_rpc_arg_u64(args, index, label).map(Some)
13522}
13523
13524pub(crate) fn javascript_sync_rpc_bytes_arg(
13525 args: &[Value],
13526 index: usize,
13527 label: &str,
13528) -> Result<Vec<u8>, SidecarError> {
13529 let Some(value) = args.get(index) else {
13530 return Err(SidecarError::InvalidState(format!("{label} is required")));
13531 };
13532
13533 if let Some(text) = value.as_str() {
13534 return Ok(text.as_bytes().to_vec());
13535 }
13536
13537 let Some(base64_value) = value
13538 .get("__agentOSType")
13539 .and_then(Value::as_str)
13540 .filter(|kind| *kind == "bytes")
13541 .and_then(|_| value.get("base64"))
13542 .and_then(Value::as_str)
13543 else {
13544 return Err(SidecarError::InvalidState(format!(
13545 "{label} must be a string or encoded bytes payload"
13546 )));
13547 };
13548
13549 base64::engine::general_purpose::STANDARD
13550 .decode(base64_value)
13551 .map_err(|error| {
13552 SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
13553 })
13554}
13555
13556pub(crate) fn javascript_sync_rpc_bytes_value(bytes: &[u8]) -> Value {
13557 json!({
13558 "__agentOSType": "bytes",
13559 "base64": base64::engine::general_purpose::STANDARD.encode(bytes),
13560 })
13561}
13562
13563#[derive(Debug, Deserialize)]
13564struct KernelPollFdRequest {
13565 fd: u32,
13566 events: u16,
13567}
13568
13569#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
13570struct KernelPollFdResponse {
13571 fd: u32,
13572 events: u16,
13573 revents: u16,
13574}
13575
13576fn javascript_sync_rpc_base64_arg(
13577 args: &[Value],
13578 index: usize,
13579 label: &str,
13580) -> Result<Vec<u8>, SidecarError> {
13581 let value = javascript_sync_rpc_arg_str(args, index, label)?;
13582 base64::engine::general_purpose::STANDARD
13583 .decode(value)
13584 .map_err(|error| {
13585 SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
13586 })
13587}
13588
13589static SYNC_RPC_STATS: std::sync::OnceLock<
13596 std::sync::Mutex<std::collections::BTreeMap<String, u64>>,
13597> = std::sync::OnceLock::new();
13598
13599fn sync_rpc_trace_enabled() -> bool {
13600 std::env::var("AGENTOS_SYNC_RPC_TRACE").as_deref() == Ok("1")
13601}
13602
13603fn record_sync_rpc(method: &str) {
13604 let stats =
13605 SYNC_RPC_STATS.get_or_init(|| std::sync::Mutex::new(std::collections::BTreeMap::new()));
13606 let Ok(mut map) = stats.lock() else {
13607 return;
13608 };
13609 *map.entry(method.to_string()).or_insert(0) += 1;
13610 let total: u64 = map.values().sum();
13611 if total == 1 || total % 50 == 0 {
13612 let mut top: Vec<(&String, &u64)> = map.iter().collect();
13613 top.sort_by(|a, b| b.1.cmp(a.1));
13614 let breakdown = top
13615 .iter()
13616 .take(8)
13617 .map(|(m, c)| format!("{m}={c}"))
13618 .collect::<Vec<_>>()
13619 .join(" ");
13620 tracing::info!(target: "secure_exec_sidecar::perf", total, %breakdown, "sync_rpc count");
13621 }
13622}
13623
13624pub(crate) fn service_javascript_sync_rpc<B>(
13625 request: JavascriptSyncRpcServiceRequest<'_, B>,
13626) -> Result<Value, SidecarError>
13627where
13628 B: NativeSidecarBridge + Send + 'static,
13629 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
13630{
13631 if sync_rpc_trace_enabled() {
13632 record_sync_rpc(request.sync_request.method.as_str());
13633 }
13634 let JavascriptSyncRpcServiceRequest {
13635 bridge,
13636 vm_id,
13637 dns,
13638 socket_paths,
13639 kernel,
13640 process,
13641 sync_request: request,
13642 resource_limits,
13643 network_counts,
13644 } = request;
13645 match request.method.as_str() {
13646 "_resolveModule"
13649 | "_resolveModuleSync"
13650 | "__resolve_module"
13651 | "_batchResolveModules"
13652 | "__batch_resolve_modules"
13653 | "_loadFile"
13654 | "_loadFileSync"
13655 | "__load_file"
13656 | "_moduleFormat"
13657 | "__module_format" => service_javascript_module_sync_rpc(kernel, process, request),
13658 "_loadPolyfill" | "__load_polyfill" => {
13660 service_javascript_internal_bridge_sync_rpc(process, request)
13661 }
13662 "__kernel_stdin_read" => match &process.execution {
13663 ActiveExecution::Javascript(execution) => execution
13664 .read_kernel_stdin_sync_rpc(request)
13665 .map_err(|error| SidecarError::Execution(error.to_string())),
13666 ActiveExecution::Python(_) | ActiveExecution::Wasm(_) | ActiveExecution::Tool(_) => {
13667 service_javascript_kernel_stdin_sync_rpc(kernel, process, request)
13668 }
13669 },
13670 "__kernel_stdio_write" => {
13671 service_javascript_kernel_stdio_write_sync_rpc(kernel, process, request)
13672 }
13673 "__kernel_poll" => service_javascript_kernel_poll_sync_rpc(kernel, process, request),
13674 "__pty_set_raw_mode" => {
13675 service_javascript_pty_set_raw_mode_sync_rpc(kernel, process, request)
13676 }
13677 "crypto.hashDigest"
13678 | "crypto.hmacDigest"
13679 | "crypto.pbkdf2"
13680 | "crypto.scrypt"
13681 | "crypto.cipheriv"
13682 | "crypto.decipheriv"
13683 | "crypto.cipherivCreate"
13684 | "crypto.cipherivUpdate"
13685 | "crypto.cipherivFinal"
13686 | "crypto.sign"
13687 | "crypto.verify"
13688 | "crypto.asymmetricOp"
13689 | "crypto.createKeyObject"
13690 | "crypto.generateKeyPairSync"
13691 | "crypto.generateKeySync"
13692 | "crypto.generatePrimeSync"
13693 | "crypto.diffieHellman"
13694 | "crypto.diffieHellmanGroup"
13695 | "crypto.diffieHellmanSessionCreate"
13696 | "crypto.diffieHellmanSessionCall"
13697 | "crypto.diffieHellmanSessionDestroy"
13698 | "crypto.subtle" => service_javascript_crypto_sync_rpc(process, request),
13699 "dns.lookup" | "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
13700 service_javascript_dns_sync_rpc(bridge, kernel, vm_id, dns, request)
13701 }
13702 "net.http_listen" | "net.http_close" | "net.http_wait" | "net.http_respond" => {
13703 service_javascript_net_sync_rpc(JavascriptNetSyncRpcServiceRequest {
13704 bridge,
13705 vm_id,
13706 dns,
13707 socket_paths,
13708 kernel,
13709 process,
13710 sync_request: request,
13711 resource_limits,
13712 network_counts,
13713 })
13714 }
13715 "net.http2_server_listen"
13716 | "net.http2_server_poll"
13717 | "net.http2_server_close"
13718 | "net.http2_server_respond"
13719 | "net.http2_server_wait"
13720 | "net.http2_session_connect"
13721 | "net.http2_session_request"
13722 | "net.http2_session_settings"
13723 | "net.http2_session_set_local_window_size"
13724 | "net.http2_session_goaway"
13725 | "net.http2_session_close"
13726 | "net.http2_session_destroy"
13727 | "net.http2_session_poll"
13728 | "net.http2_session_wait"
13729 | "net.http2_stream_respond"
13730 | "net.http2_stream_push_stream"
13731 | "net.http2_stream_write"
13732 | "net.http2_stream_end"
13733 | "net.http2_stream_close"
13734 | "net.http2_stream_pause"
13735 | "net.http2_stream_resume"
13736 | "net.http2_stream_respond_with_file" => {
13737 service_javascript_http2_sync_rpc(JavascriptHttp2SyncRpcServiceRequest {
13738 bridge,
13739 kernel,
13740 vm_id,
13741 dns,
13742 socket_paths,
13743 process,
13744 sync_request: request,
13745 resource_limits,
13746 network_counts,
13747 })
13748 }
13749 "net.connect"
13750 | "net.reserve_tcp_port"
13751 | "net.release_tcp_port"
13752 | "net.listen"
13753 | "net.poll"
13754 | "net.socket_wait_connect"
13755 | "net.socket_read"
13756 | "net.socket_set_no_delay"
13757 | "net.socket_set_keep_alive"
13758 | "net.socket_upgrade_tls"
13759 | "net.socket_get_tls_client_hello"
13760 | "net.socket_tls_query"
13761 | "net.server_poll"
13762 | "net.server_accept"
13763 | "net.server_connections"
13764 | "net.upgrade_socket_write"
13765 | "net.upgrade_socket_end"
13766 | "net.upgrade_socket_destroy"
13767 | "net.write"
13768 | "net.shutdown"
13769 | "net.destroy"
13770 | "net.server_close"
13771 | "tls.get_ciphers" => {
13772 service_javascript_net_sync_rpc(JavascriptNetSyncRpcServiceRequest {
13773 bridge,
13774 vm_id,
13775 dns,
13776 socket_paths,
13777 kernel,
13778 process,
13779 sync_request: request,
13780 resource_limits,
13781 network_counts,
13782 })
13783 }
13784 "dgram.createSocket"
13785 | "dgram.bind"
13786 | "dgram.send"
13787 | "dgram.poll"
13788 | "dgram.close"
13789 | "dgram.address"
13790 | "dgram.setBufferSize"
13791 | "dgram.getBufferSize" => {
13792 service_javascript_dgram_sync_rpc(JavascriptDgramSyncRpcServiceRequest {
13793 bridge,
13794 kernel,
13795 vm_id,
13796 dns,
13797 socket_paths,
13798 process,
13799 sync_request: request,
13800 resource_limits,
13801 network_counts,
13802 })
13803 }
13804 "sqlite.constants"
13805 | "sqlite.open"
13806 | "sqlite.close"
13807 | "sqlite.exec"
13808 | "sqlite.query"
13809 | "sqlite.prepare"
13810 | "sqlite.location"
13811 | "sqlite.checkpoint"
13812 | "sqlite.statement.run"
13813 | "sqlite.statement.get"
13814 | "sqlite.statement.all"
13815 | "sqlite.statement.iterate"
13816 | "sqlite.statement.columns"
13817 | "sqlite.statement.setReturnArrays"
13818 | "sqlite.statement.setReadBigInts"
13819 | "sqlite.statement.setAllowBareNamedParameters"
13820 | "sqlite.statement.setAllowUnknownNamedParameters"
13821 | "sqlite.statement.finalize" => {
13822 service_javascript_sqlite_sync_rpc(kernel, process, request)
13823 }
13824 "process.kill" => {
13825 let target_pid =
13826 javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
13827 let signal = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
13828 let parsed_signal = parse_signal(signal)?;
13829 if parsed_signal == 0 {
13830 kernel
13831 .signal_process(EXECUTION_DRIVER_NAME, target_pid, parsed_signal)
13832 .map_err(kernel_error)?;
13833 return Ok(Value::Null);
13834 }
13835 let process_pid = i32::try_from(process.kernel_pid)
13836 .map_err(|_| SidecarError::InvalidState("process pid exceeds i32".into()))?;
13837 if target_pid != process_pid {
13838 return Err(SidecarError::InvalidState(format!(
13839 "unknown process pid {target_pid}"
13840 )));
13841 }
13842 process.pending_self_signal_exit = None;
13843 if parsed_signal != 0
13844 && !matches!(
13845 canonical_signal_name(parsed_signal),
13846 Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
13847 )
13848 {
13849 process.pending_self_signal_exit = Some(parsed_signal);
13850 }
13851 Ok(json!({
13852 "self": true,
13853 "action": "default",
13854 }))
13855 }
13856 "process.umask" => {
13857 let new_mask = javascript_sync_rpc_arg_u32_optional(&request.args, 0, "process umask")?;
13858 kernel
13859 .umask(EXECUTION_DRIVER_NAME, process.kernel_pid, new_mask)
13860 .map(|mask| json!(mask))
13861 .map_err(kernel_error)
13862 }
13863 "fs.chmodSync" | "fs.promises.chmod" => {
13864 let response =
13865 service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request)?;
13866 mirror_process_chmod_to_host(process, request)?;
13867 Ok(response)
13868 }
13869 _ => service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request),
13870 }
13871}
13872
13873fn service_javascript_internal_bridge_sync_rpc(
13874 process: &ActiveProcess,
13875 request: &JavascriptSyncRpcRequest,
13876) -> Result<Value, SidecarError> {
13877 let method = match request.method.as_str() {
13881 "_loadPolyfill" | "__load_polyfill" => "_loadPolyfill",
13882 other => {
13883 return Err(SidecarError::InvalidState(format!(
13884 "unsupported JavaScript internal bridge method {other}"
13885 )));
13886 }
13887 };
13888
13889 handle_internal_bridge_call_from_host_context(
13890 &process.host_cwd,
13891 &process.guest_cwd,
13892 &process.env,
13893 method,
13894 &request.args,
13895 )
13896 .ok_or_else(|| {
13897 SidecarError::InvalidState(format!(
13898 "JavaScript internal bridge method {method} returned no value"
13899 ))
13900 })
13901}
13902
13903fn mirror_process_chmod_to_host(
13904 process: &ActiveProcess,
13905 request: &JavascriptSyncRpcRequest,
13906) -> Result<(), SidecarError> {
13907 let guest_path = javascript_sync_rpc_arg_str(&request.args, 0, "filesystem chmod path")?;
13908 let mode = javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem chmod mode")? & 0o7777;
13909 let Some(host_path) = resolve_process_guest_path_to_host(process, guest_path) else {
13910 return Ok(());
13911 };
13912 if !host_path.exists() {
13913 return Ok(());
13914 }
13915 fs::set_permissions(&host_path, fs::Permissions::from_mode(mode)).map_err(|error| {
13916 SidecarError::Io(format!(
13917 "failed to mirror chmod to host path {}: {error}",
13918 host_path.display()
13919 ))
13920 })
13921}
13922
13923fn resolve_process_guest_path_to_host(
13924 process: &ActiveProcess,
13925 guest_path: &str,
13926) -> Option<PathBuf> {
13927 let normalized_guest_path = if guest_path.starts_with('/') {
13928 normalize_path(guest_path)
13929 } else {
13930 normalize_path(&format!(
13931 "{}/{}",
13932 process.guest_cwd.trim_end_matches('/'),
13933 guest_path
13934 ))
13935 };
13936 if let Some(host_path) =
13937 host_path_from_runtime_guest_mappings(&process.env, &normalized_guest_path)
13938 {
13939 return Some(host_path);
13940 }
13941 let normalized_guest_cwd = normalize_path(&process.guest_cwd);
13942 let mut host_root = normalize_host_path(&process.host_cwd);
13943 for _ in normalized_guest_cwd
13944 .trim_start_matches('/')
13945 .split('/')
13946 .filter(|segment| !segment.is_empty())
13947 {
13948 host_root = host_root.parent()?.to_path_buf();
13949 }
13950 if normalized_guest_path == "/" {
13951 Some(host_root)
13952 } else {
13953 Some(host_root.join(normalized_guest_path.trim_start_matches('/')))
13954 }
13955}
13956
13957pub(crate) fn service_javascript_crypto_sync_rpc(
13958 process: &mut ActiveProcess,
13959 request: &JavascriptSyncRpcRequest,
13960) -> Result<Value, SidecarError> {
13961 match request.method.as_str() {
13962 "crypto.hashDigest" => {
13963 let algorithm = javascript_crypto_digest_algorithm(
13964 &request.args,
13965 0,
13966 "crypto.hashDigest algorithm",
13967 )?;
13968 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hashDigest data")?;
13969 Ok(Value::String(
13970 base64::engine::general_purpose::STANDARD.encode(algorithm.digest(&data)),
13971 ))
13972 }
13973 "crypto.hmacDigest" => {
13974 let algorithm = javascript_crypto_digest_algorithm(
13975 &request.args,
13976 0,
13977 "crypto.hmacDigest algorithm",
13978 )?;
13979 let key = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hmacDigest key")?;
13980 let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.hmacDigest data")?;
13981 Ok(Value::String(
13982 base64::engine::general_purpose::STANDARD.encode(algorithm.hmac(&key, &data)?),
13983 ))
13984 }
13985 "crypto.pbkdf2" => {
13986 let password =
13987 javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.pbkdf2 password")?;
13988 let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.pbkdf2 salt")?;
13989 let iterations =
13990 javascript_sync_rpc_arg_u32(&request.args, 2, "crypto.pbkdf2 iterations")?;
13991 if iterations == 0 {
13992 return Err(SidecarError::InvalidState(String::from(
13993 "crypto.pbkdf2 iterations must be greater than zero",
13994 )));
13995 }
13996 let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
13997 &request.args,
13998 3,
13999 "crypto.pbkdf2 key length",
14000 )?)
14001 .map_err(|_| {
14002 SidecarError::InvalidState(String::from(
14003 "crypto.pbkdf2 key length must fit within usize",
14004 ))
14005 })?;
14006 let algorithm =
14007 javascript_crypto_digest_algorithm(&request.args, 4, "crypto.pbkdf2 digest")?;
14008 let mut output = vec![0u8; key_len];
14009 algorithm.pbkdf2(&password, &salt, iterations, &mut output);
14010 Ok(Value::String(
14011 base64::engine::general_purpose::STANDARD.encode(output),
14012 ))
14013 }
14014 "crypto.scrypt" => {
14015 let password =
14016 javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.scrypt password")?;
14017 let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.scrypt salt")?;
14018 let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
14019 &request.args,
14020 2,
14021 "crypto.scrypt key length",
14022 )?)
14023 .map_err(|_| {
14024 SidecarError::InvalidState(String::from(
14025 "crypto.scrypt key length must fit within usize",
14026 ))
14027 })?;
14028 let options_json =
14029 javascript_sync_rpc_arg_str(&request.args, 3, "crypto.scrypt options")?;
14030 let options: JavascriptScryptOptions =
14031 serde_json::from_str(options_json).map_err(|error| {
14032 SidecarError::InvalidState(format!(
14033 "crypto.scrypt options must be valid JSON: {error}"
14034 ))
14035 })?;
14036 let cost = options.cost.unwrap_or(DEFAULT_SCRYPT_COST);
14037 if cost == 0 || !cost.is_power_of_two() {
14038 return Err(SidecarError::InvalidState(String::from(
14039 "crypto.scrypt cost must be a positive power of two",
14040 )));
14041 }
14042 let log_n = u8::try_from(cost.ilog2()).map_err(|_| {
14043 SidecarError::InvalidState(String::from(
14044 "crypto.scrypt cost exceeds supported parameter range",
14045 ))
14046 })?;
14047 let params = ScryptParams::new(
14048 log_n,
14049 options.block_size.unwrap_or(DEFAULT_SCRYPT_BLOCK_SIZE),
14050 options
14051 .parallelization
14052 .unwrap_or(DEFAULT_SCRYPT_PARALLELIZATION),
14053 key_len,
14054 )
14055 .map_err(|error| {
14056 SidecarError::InvalidState(format!("crypto.scrypt options are invalid: {error}"))
14057 })?;
14058 let mut output = vec![0u8; key_len];
14059 scrypt(&password, &salt, ¶ms, &mut output).map_err(|error| {
14060 SidecarError::Execution(format!("crypto.scrypt failed: {error}"))
14061 })?;
14062 Ok(Value::String(
14063 base64::engine::general_purpose::STANDARD.encode(output),
14064 ))
14065 }
14066 "crypto.cipheriv" => service_javascript_crypto_cipheriv_sync_rpc(request),
14067 "crypto.decipheriv" => service_javascript_crypto_decipheriv_sync_rpc(request),
14068 "crypto.cipherivCreate" => {
14069 service_javascript_crypto_cipheriv_create_sync_rpc(process, request)
14070 }
14071 "crypto.cipherivUpdate" => {
14072 service_javascript_crypto_cipheriv_update_sync_rpc(process, request)
14073 }
14074 "crypto.cipherivFinal" => {
14075 service_javascript_crypto_cipheriv_final_sync_rpc(process, request)
14076 }
14077 "crypto.sign" => service_javascript_crypto_sign_sync_rpc(request),
14078 "crypto.verify" => service_javascript_crypto_verify_sync_rpc(request),
14079 "crypto.asymmetricOp" => service_javascript_crypto_asymmetric_op_sync_rpc(request),
14080 "crypto.createKeyObject" => service_javascript_crypto_create_key_object_sync_rpc(request),
14081 "crypto.generateKeyPairSync" => {
14082 service_javascript_crypto_generate_key_pair_sync_rpc(request)
14083 }
14084 "crypto.generateKeySync" => service_javascript_crypto_generate_key_sync_rpc(request),
14085 "crypto.generatePrimeSync" => service_javascript_crypto_generate_prime_sync_rpc(request),
14086 "crypto.diffieHellman" => service_javascript_crypto_diffie_hellman_sync_rpc(request),
14087 "crypto.diffieHellmanGroup" => {
14088 service_javascript_crypto_diffie_hellman_group_sync_rpc(request)
14089 }
14090 "crypto.diffieHellmanSessionCreate" => {
14091 service_javascript_crypto_diffie_hellman_session_create_sync_rpc(process, request)
14092 }
14093 "crypto.diffieHellmanSessionCall" => {
14094 service_javascript_crypto_diffie_hellman_session_call_sync_rpc(process, request)
14095 }
14096 "crypto.diffieHellmanSessionDestroy" => {
14097 service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(process, request)
14098 }
14099 "crypto.subtle" => service_javascript_crypto_subtle_sync_rpc(request),
14100 _ => Err(SidecarError::InvalidState(format!(
14101 "unsupported JavaScript crypto sync RPC method {}",
14102 request.method
14103 ))),
14104 }
14105}
14106
14107fn javascript_crypto_digest_algorithm(
14108 args: &[Value],
14109 index: usize,
14110 label: &str,
14111) -> Result<JavascriptCryptoDigestAlgorithm, SidecarError> {
14112 JavascriptCryptoDigestAlgorithm::parse(javascript_sync_rpc_arg_str(args, index, label)?)
14113}
14114
14115impl JavascriptCryptoDigestAlgorithm {
14116 fn parse(value: &str) -> Result<Self, SidecarError> {
14117 match value.trim().to_ascii_lowercase().replace('-', "").as_str() {
14118 "md5" => Ok(Self::Md5),
14119 "sha1" => Ok(Self::Sha1),
14120 "sha256" => Ok(Self::Sha256),
14121 "sha512" => Ok(Self::Sha512),
14122 _ => Err(SidecarError::InvalidState(format!(
14123 "unsupported crypto digest algorithm {value}"
14124 ))),
14125 }
14126 }
14127
14128 fn digest(self, data: &[u8]) -> Vec<u8> {
14129 match self {
14130 Self::Md5 => Md5::digest(data).to_vec(),
14131 Self::Sha1 => Sha1::digest(data).to_vec(),
14132 Self::Sha256 => Sha256::digest(data).to_vec(),
14133 Self::Sha512 => Sha512::digest(data).to_vec(),
14134 }
14135 }
14136
14137 fn hmac(self, key: &[u8], data: &[u8]) -> Result<Vec<u8>, SidecarError> {
14138 match self {
14139 Self::Md5 => {
14140 let mut mac = Hmac::<Md5>::new_from_slice(key).map_err(|error| {
14141 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14142 })?;
14143 mac.update(data);
14144 Ok(mac.finalize().into_bytes().to_vec())
14145 }
14146 Self::Sha1 => {
14147 let mut mac = Hmac::<Sha1>::new_from_slice(key).map_err(|error| {
14148 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14149 })?;
14150 mac.update(data);
14151 Ok(mac.finalize().into_bytes().to_vec())
14152 }
14153 Self::Sha256 => {
14154 let mut mac = Hmac::<Sha256>::new_from_slice(key).map_err(|error| {
14155 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14156 })?;
14157 mac.update(data);
14158 Ok(mac.finalize().into_bytes().to_vec())
14159 }
14160 Self::Sha512 => {
14161 let mut mac = Hmac::<Sha512>::new_from_slice(key).map_err(|error| {
14162 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14163 })?;
14164 mac.update(data);
14165 Ok(mac.finalize().into_bytes().to_vec())
14166 }
14167 }
14168 }
14169
14170 fn pbkdf2(self, password: &[u8], salt: &[u8], iterations: u32, output: &mut [u8]) {
14171 match self {
14172 Self::Md5 => pbkdf2_hmac::<Md5>(password, salt, iterations, output),
14173 Self::Sha1 => pbkdf2_hmac::<Sha1>(password, salt, iterations, output),
14174 Self::Sha256 => pbkdf2_hmac::<Sha256>(password, salt, iterations, output),
14175 Self::Sha512 => pbkdf2_hmac::<Sha512>(password, salt, iterations, output),
14176 }
14177 }
14178}
14179
14180#[derive(Debug, Clone)]
14181enum JavascriptCryptoKeyMaterial {
14182 Private(PKey<Private>),
14183 Public(PKey<Public>),
14184 Secret(Vec<u8>),
14185}
14186
14187#[derive(Debug, Clone, Deserialize, Serialize)]
14188struct JavascriptSerializedSandboxKeyObject {
14189 #[serde(rename = "type")]
14190 kind: String,
14191 #[serde(skip_serializing_if = "Option::is_none")]
14192 pem: Option<String>,
14193 #[serde(skip_serializing_if = "Option::is_none")]
14194 raw: Option<String>,
14195 #[serde(skip_serializing_if = "Option::is_none", rename = "asymmetricKeyType")]
14196 asymmetric_key_type: Option<String>,
14197 #[serde(
14198 skip_serializing_if = "Option::is_none",
14199 rename = "asymmetricKeyDetails"
14200 )]
14201 asymmetric_key_details: Option<Map<String, Value>>,
14202 #[serde(skip_serializing_if = "Option::is_none")]
14203 jwk: Option<Value>,
14204}
14205
14206#[derive(Debug, Clone)]
14207struct JavascriptDirectKeyInput {
14208 key: JavascriptCryptoKeyMaterial,
14209 padding: Option<Padding>,
14210}
14211
14212fn service_javascript_crypto_cipheriv_sync_rpc(
14213 request: &JavascriptSyncRpcRequest,
14214) -> Result<Value, SidecarError> {
14215 service_javascript_crypto_cipheriv_inner(request, false)
14216}
14217
14218fn service_javascript_crypto_decipheriv_sync_rpc(
14219 request: &JavascriptSyncRpcRequest,
14220) -> Result<Value, SidecarError> {
14221 service_javascript_crypto_cipheriv_inner(request, true)
14222}
14223
14224fn service_javascript_crypto_cipheriv_create_sync_rpc(
14225 process: &mut ActiveProcess,
14226 request: &JavascriptSyncRpcRequest,
14227) -> Result<Value, SidecarError> {
14228 ensure_per_process_state_handle_capacity(process.cipher_sessions.len(), "cipher session")?;
14229 let mode = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.cipherivCreate mode")?;
14230 let decrypt = mode == "decipher";
14231 let algorithm =
14232 javascript_sync_rpc_arg_str(&request.args, 1, "crypto.cipherivCreate algorithm")?;
14233 let key = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.cipherivCreate key")?;
14234 let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 3, "crypto.cipherivCreate iv")?;
14235 let options =
14236 javascript_sync_rpc_json_arg_optional(&request.args, 4, "crypto.cipherivCreate options")?;
14237 let auth_tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options.as_ref())?;
14238 let context = javascript_crypto_build_cipher_context(
14239 algorithm,
14240 &key,
14241 iv.as_deref(),
14242 decrypt,
14243 options.as_ref(),
14244 )?;
14245 process.next_cipher_session_id += 1;
14246 let session_id = process.next_cipher_session_id;
14247 process.cipher_sessions.insert(
14248 session_id,
14249 ActiveCipherSession {
14250 algorithm: algorithm.to_string(),
14251 auth_tag_len,
14252 context,
14253 },
14254 );
14255 Ok(json!(session_id))
14256}
14257
14258fn service_javascript_crypto_cipheriv_update_sync_rpc(
14259 process: &mut ActiveProcess,
14260 request: &JavascriptSyncRpcRequest,
14261) -> Result<Value, SidecarError> {
14262 let session_id =
14263 javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivUpdate session id")?;
14264 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.cipherivUpdate data")?;
14265 let session = process
14266 .cipher_sessions
14267 .get_mut(&session_id)
14268 .ok_or_else(|| {
14269 SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
14270 })?;
14271 let result = javascript_crypto_cipher_update(&mut session.context, &data)?;
14272 Ok(Value::String(
14273 base64::engine::general_purpose::STANDARD.encode(result),
14274 ))
14275}
14276
14277fn service_javascript_crypto_cipheriv_final_sync_rpc(
14278 process: &mut ActiveProcess,
14279 request: &JavascriptSyncRpcRequest,
14280) -> Result<Value, SidecarError> {
14281 let session_id =
14282 javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivFinal session id")?;
14283 let mut session = process.cipher_sessions.remove(&session_id).ok_or_else(|| {
14284 SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
14285 })?;
14286 let data = javascript_crypto_cipher_finalize(&mut session.context)?;
14287 let mut response = Map::new();
14288 response.insert(
14289 String::from("data"),
14290 Value::String(base64::engine::general_purpose::STANDARD.encode(data)),
14291 );
14292 if javascript_crypto_is_aead(&session.algorithm) {
14293 let mut auth_tag = vec![0_u8; session.auth_tag_len];
14294 session
14295 .context
14296 .get_tag(&mut auth_tag)
14297 .map_err(javascript_crypto_openssl_error)?;
14298 response.insert(
14299 String::from("authTag"),
14300 Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
14301 );
14302 }
14303 Ok(Value::String(serde_json::to_string(&response).map_err(
14304 |error| SidecarError::InvalidState(format!("serialize cipher final response: {error}")),
14305 )?))
14306}
14307
14308fn service_javascript_crypto_sign_sync_rpc(
14309 request: &JavascriptSyncRpcRequest,
14310) -> Result<Value, SidecarError> {
14311 let algorithm = request.args.first().and_then(Value::as_str);
14312 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.sign data")?;
14313 let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.sign key")?;
14314 let key_input =
14315 javascript_crypto_parse_direct_key_input(key_json, Some("private"), "crypto.sign key")?;
14316 let private_key = javascript_crypto_expect_private_key(key_input.key, "crypto.sign key")?;
14317 let mut signer = javascript_crypto_new_signer(algorithm, &private_key)?;
14318 if let Some(padding) = key_input.padding {
14319 signer
14320 .set_rsa_padding(padding)
14321 .map_err(javascript_crypto_openssl_error)?;
14322 }
14323 signer
14324 .update(&data)
14325 .map_err(javascript_crypto_openssl_error)?;
14326 Ok(Value::String(
14327 base64::engine::general_purpose::STANDARD.encode(
14328 signer
14329 .sign_to_vec()
14330 .map_err(javascript_crypto_openssl_error)?,
14331 ),
14332 ))
14333}
14334
14335fn service_javascript_crypto_verify_sync_rpc(
14336 request: &JavascriptSyncRpcRequest,
14337) -> Result<Value, SidecarError> {
14338 let algorithm = request.args.first().and_then(Value::as_str);
14339 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.verify data")?;
14340 let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.verify key")?;
14341 let signature = javascript_sync_rpc_base64_arg(&request.args, 3, "crypto.verify signature")?;
14342 let key_input =
14343 javascript_crypto_parse_direct_key_input(key_json, Some("public"), "crypto.verify key")?;
14344 let public_key = javascript_crypto_expect_public_key(key_input.key, "crypto.verify key")?;
14345 let mut verifier = javascript_crypto_new_verifier(algorithm, &public_key)?;
14346 if let Some(padding) = key_input.padding {
14347 verifier
14348 .set_rsa_padding(padding)
14349 .map_err(javascript_crypto_openssl_error)?;
14350 }
14351 verifier
14352 .update(&data)
14353 .map_err(javascript_crypto_openssl_error)?;
14354 Ok(json!(verifier
14355 .verify(&signature)
14356 .map_err(javascript_crypto_openssl_error)?))
14357}
14358
14359fn service_javascript_crypto_asymmetric_op_sync_rpc(
14360 request: &JavascriptSyncRpcRequest,
14361) -> Result<Value, SidecarError> {
14362 let operation = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.asymmetricOp operation")?;
14363 let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.asymmetricOp key")?;
14364 let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.asymmetricOp data")?;
14365 let expect_kind = match operation {
14366 "publicEncrypt" | "publicDecrypt" => Some("public"),
14367 "privateEncrypt" | "privateDecrypt" => Some("private"),
14368 other => {
14369 return Err(SidecarError::InvalidState(format!(
14370 "Unsupported asymmetric crypto operation: {other}"
14371 )));
14372 }
14373 };
14374 let key_input =
14375 javascript_crypto_parse_direct_key_input(key_json, expect_kind, "crypto.asymmetricOp key")?;
14376 let padding = key_input.padding.unwrap_or(Padding::PKCS1);
14377 let mut output = vec![0_u8; javascript_crypto_rsa_output_size(&key_input.key)?];
14378 let written = match (operation, key_input.key) {
14379 ("publicEncrypt", JavascriptCryptoKeyMaterial::Public(key))
14380 | ("publicDecrypt", JavascriptCryptoKeyMaterial::Public(key)) => {
14381 let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
14382 if operation == "publicEncrypt" {
14383 rsa.public_encrypt(&data, &mut output, padding)
14384 .map_err(javascript_crypto_openssl_error)?
14385 } else {
14386 rsa.public_decrypt(&data, &mut output, padding)
14387 .map_err(javascript_crypto_openssl_error)?
14388 }
14389 }
14390 ("privateEncrypt", JavascriptCryptoKeyMaterial::Private(key))
14391 | ("privateDecrypt", JavascriptCryptoKeyMaterial::Private(key)) => {
14392 let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
14393 if operation == "privateEncrypt" {
14394 rsa.private_encrypt(&data, &mut output, padding)
14395 .map_err(javascript_crypto_openssl_error)?
14396 } else {
14397 rsa.private_decrypt(&data, &mut output, padding)
14398 .map_err(javascript_crypto_openssl_error)?
14399 }
14400 }
14401 _ => {
14402 return Err(SidecarError::InvalidState(format!(
14403 "{operation} requires an RSA {} key",
14404 expect_kind.unwrap_or("asymmetric")
14405 )));
14406 }
14407 };
14408 output.truncate(written);
14409 Ok(Value::String(
14410 base64::engine::general_purpose::STANDARD.encode(output),
14411 ))
14412}
14413
14414fn service_javascript_crypto_create_key_object_sync_rpc(
14415 request: &JavascriptSyncRpcRequest,
14416) -> Result<Value, SidecarError> {
14417 let operation =
14418 javascript_sync_rpc_arg_str(&request.args, 0, "crypto.createKeyObject operation")?;
14419 let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.createKeyObject key")?;
14420 let expected = match operation {
14421 "createPrivateKey" => Some("private"),
14422 "createPublicKey" => Some("public"),
14423 other => {
14424 return Err(SidecarError::InvalidState(format!(
14425 "Unsupported key creation operation: {other}"
14426 )));
14427 }
14428 };
14429 let key_input =
14430 javascript_crypto_parse_direct_key_input(key_json, expected, "crypto.createKeyObject key")?;
14431 Ok(Value::String(
14432 serde_json::to_string(&javascript_crypto_serialize_sandbox_key_object(
14433 &key_input.key,
14434 )?)
14435 .map_err(|error| {
14436 SidecarError::InvalidState(format!("serialize crypto key object: {error}"))
14437 })?,
14438 ))
14439}
14440
14441fn service_javascript_crypto_generate_key_pair_sync_rpc(
14442 request: &JavascriptSyncRpcRequest,
14443) -> Result<Value, SidecarError> {
14444 let key_type =
14445 javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeyPairSync type")?;
14446 let options = javascript_crypto_parse_serialized_options_arg(
14447 &request.args,
14448 1,
14449 "crypto.generateKeyPairSync options",
14450 )?
14451 .unwrap_or(Value::Object(Map::new()));
14452 let public_encoding = options.get("publicKeyEncoding").cloned();
14453 let private_encoding = options.get("privateKeyEncoding").cloned();
14454
14455 let private_key = match key_type {
14456 "rsa" => {
14457 let bits = options
14458 .get("modulusLength")
14459 .and_then(Value::as_u64)
14460 .unwrap_or(2048) as u32;
14461 let exponent = options
14462 .get("publicExponent")
14463 .map(|value| javascript_crypto_u32_from_bridge_value(value, "rsa publicExponent"))
14464 .transpose()?
14465 .unwrap_or(65_537);
14466 let exponent = BigNum::from_u32(exponent).map_err(javascript_crypto_openssl_error)?;
14467 let rsa =
14468 Rsa::generate_with_e(bits, &exponent).map_err(javascript_crypto_openssl_error)?;
14469 PKey::from_rsa(rsa).map_err(javascript_crypto_openssl_error)?
14470 }
14471 "ec" => {
14472 let named_curve = options
14473 .get("namedCurve")
14474 .and_then(Value::as_str)
14475 .ok_or_else(|| {
14476 SidecarError::InvalidState(String::from(
14477 "crypto.generateKeyPairSync ec requires namedCurve",
14478 ))
14479 })?;
14480 let group = EcGroup::from_curve_name(javascript_crypto_curve_nid(named_curve)?)
14481 .map_err(javascript_crypto_openssl_error)?;
14482 let key = EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?;
14483 PKey::from_ec_key(key).map_err(javascript_crypto_openssl_error)?
14484 }
14485 "ed25519" => PKey::generate_ed25519().map_err(javascript_crypto_openssl_error)?,
14486 "x25519" => PKey::generate_x25519().map_err(javascript_crypto_openssl_error)?,
14487 other => {
14488 return Err(SidecarError::InvalidState(format!(
14489 "unsupported crypto key pair type {other}"
14490 )));
14491 }
14492 };
14493 let public_key = PKey::public_key_from_pem(
14494 &private_key
14495 .public_key_to_pem()
14496 .map_err(javascript_crypto_openssl_error)?,
14497 )
14498 .map_err(javascript_crypto_openssl_error)?;
14499 let response = if public_encoding.is_some() || private_encoding.is_some() {
14500 json!({
14501 "publicKey": javascript_crypto_serialize_encoded_key_value_public(&public_key, public_encoding.as_ref())?,
14502 "privateKey": javascript_crypto_serialize_encoded_key_value_private(&private_key, private_encoding.as_ref())?,
14503 })
14504 } else {
14505 json!({
14506 "publicKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(public_key))?,
14507 "privateKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(private_key))?,
14508 })
14509 };
14510 Ok(Value::String(serde_json::to_string(&response).map_err(
14511 |error| SidecarError::InvalidState(format!("serialize generated key pair: {error}")),
14512 )?))
14513}
14514
14515fn service_javascript_crypto_generate_key_sync_rpc(
14516 request: &JavascriptSyncRpcRequest,
14517) -> Result<Value, SidecarError> {
14518 let key_type = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeySync type")?;
14519 let options = javascript_crypto_parse_serialized_options_arg(
14520 &request.args,
14521 1,
14522 "crypto.generateKeySync options",
14523 )?
14524 .unwrap_or(Value::Object(Map::new()));
14525 let bit_length = options
14526 .get("length")
14527 .and_then(Value::as_u64)
14528 .ok_or_else(|| {
14529 SidecarError::InvalidState(String::from(
14530 "crypto.generateKeySync options.length is required",
14531 ))
14532 })? as usize;
14533 let mut raw = vec![0_u8; bit_length.div_ceil(8)];
14534 rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
14535 let serialized = match key_type {
14536 "hmac" => javascript_crypto_serialize_sandbox_key_object(
14537 &JavascriptCryptoKeyMaterial::Secret(raw),
14538 )?,
14539 "aes" => javascript_crypto_serialize_sandbox_key_object(
14540 &JavascriptCryptoKeyMaterial::Secret(raw),
14541 )?,
14542 other => {
14543 return Err(SidecarError::InvalidState(format!(
14544 "unsupported crypto.generateKeySync type {other}"
14545 )));
14546 }
14547 };
14548 Ok(Value::String(serde_json::to_string(&serialized).map_err(
14549 |error| SidecarError::InvalidState(format!("serialize generated key: {error}")),
14550 )?))
14551}
14552
14553fn service_javascript_crypto_generate_prime_sync_rpc(
14554 request: &JavascriptSyncRpcRequest,
14555) -> Result<Value, SidecarError> {
14556 let bits =
14557 javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.generatePrimeSync size")? as i32;
14558 let options = javascript_crypto_parse_serialized_options_arg(
14559 &request.args,
14560 1,
14561 "crypto.generatePrimeSync options",
14562 )?
14563 .unwrap_or(Value::Object(Map::new()));
14564 let safe = options
14565 .get("safe")
14566 .and_then(Value::as_bool)
14567 .unwrap_or(false);
14568 let add = options
14569 .get("add")
14570 .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime add"))
14571 .transpose()?;
14572 let rem = options
14573 .get("rem")
14574 .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime rem"))
14575 .transpose()?;
14576 let mut prime = BigNum::new().map_err(javascript_crypto_openssl_error)?;
14577 prime
14578 .generate_prime(bits, safe, add.as_deref(), rem.as_deref())
14579 .map_err(javascript_crypto_openssl_error)?;
14580 let payload = if options
14581 .get("bigint")
14582 .and_then(Value::as_bool)
14583 .unwrap_or(false)
14584 {
14585 json!({
14586 "__type": "bigint",
14587 "value": prime.to_dec_str().map_err(javascript_crypto_openssl_error)?.to_string(),
14588 })
14589 } else {
14590 json!({
14591 "__type": "buffer",
14592 "value": base64::engine::general_purpose::STANDARD.encode(prime.to_vec()),
14593 })
14594 };
14595 Ok(Value::String(serde_json::to_string(&payload).map_err(
14596 |error| SidecarError::InvalidState(format!("serialize generated prime: {error}")),
14597 )?))
14598}
14599
14600fn service_javascript_crypto_diffie_hellman_sync_rpc(
14601 request: &JavascriptSyncRpcRequest,
14602) -> Result<Value, SidecarError> {
14603 let options = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellman options")?;
14604 let parsed: Value = serde_json::from_str(options).map_err(|error| {
14605 SidecarError::InvalidState(format!(
14606 "crypto.diffieHellman options must be valid JSON: {error}"
14607 ))
14608 })?;
14609 let private_key = javascript_crypto_parse_key_material_value(
14610 parsed.get("privateKey").ok_or_else(|| {
14611 SidecarError::InvalidState(String::from("crypto.diffieHellman missing privateKey"))
14612 })?,
14613 Some("private"),
14614 "crypto.diffieHellman privateKey",
14615 )?;
14616 let public_key = javascript_crypto_parse_key_material_value(
14617 parsed.get("publicKey").ok_or_else(|| {
14618 SidecarError::InvalidState(String::from("crypto.diffieHellman missing publicKey"))
14619 })?,
14620 Some("public"),
14621 "crypto.diffieHellman publicKey",
14622 )?;
14623 let private_key =
14624 javascript_crypto_expect_private_key(private_key, "crypto.diffieHellman privateKey")?;
14625 let public_key =
14626 javascript_crypto_expect_public_key(public_key, "crypto.diffieHellman publicKey")?;
14627 let mut deriver = Deriver::new(&private_key).map_err(javascript_crypto_openssl_error)?;
14628 deriver
14629 .set_peer(&public_key)
14630 .map_err(javascript_crypto_openssl_error)?;
14631 let secret = deriver
14632 .derive_to_vec()
14633 .map_err(javascript_crypto_openssl_error)?;
14634 Ok(Value::String(
14635 serde_json::to_string(&json!({
14636 "__type": "buffer",
14637 "value": base64::engine::general_purpose::STANDARD.encode(secret),
14638 }))
14639 .map_err(|error| {
14640 SidecarError::InvalidState(format!("serialize derived secret: {error}"))
14641 })?,
14642 ))
14643}
14644
14645fn service_javascript_crypto_diffie_hellman_group_sync_rpc(
14646 request: &JavascriptSyncRpcRequest,
14647) -> Result<Value, SidecarError> {
14648 let name = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellmanGroup name")?;
14649 let params = javascript_crypto_named_dh_group(name)?;
14650 let response = json!({
14651 "prime": {
14652 "__type": "buffer",
14653 "value": base64::engine::general_purpose::STANDARD.encode(params.prime_p().to_vec()),
14654 },
14655 "generator": {
14656 "__type": "buffer",
14657 "value": base64::engine::general_purpose::STANDARD.encode(params.generator().to_vec()),
14658 },
14659 });
14660 Ok(Value::String(serde_json::to_string(&response).map_err(
14661 |error| {
14662 SidecarError::InvalidState(format!("serialize diffieHellmanGroup response: {error}"))
14663 },
14664 )?))
14665}
14666
14667fn service_javascript_crypto_diffie_hellman_session_create_sync_rpc(
14668 process: &mut ActiveProcess,
14669 request: &JavascriptSyncRpcRequest,
14670) -> Result<Value, SidecarError> {
14671 ensure_per_process_state_handle_capacity(
14672 process.diffie_hellman_sessions.len(),
14673 "diffie-hellman session",
14674 )?;
14675 let raw = javascript_sync_rpc_arg_str(
14676 &request.args,
14677 0,
14678 "crypto.diffieHellmanSessionCreate request",
14679 )?;
14680 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14681 SidecarError::InvalidState(format!(
14682 "crypto.diffieHellmanSessionCreate request must be valid JSON: {error}"
14683 ))
14684 })?;
14685 let session = match parsed.get("type").and_then(Value::as_str) {
14686 Some("group") => {
14687 let name = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
14688 SidecarError::InvalidState(String::from(
14689 "crypto.diffieHellmanSessionCreate group requires name",
14690 ))
14691 })?;
14692 ActiveDiffieHellmanSession::Dh(ActiveDhSession {
14693 params: javascript_crypto_named_dh_group(name)?,
14694 key_pair: None,
14695 })
14696 }
14697 Some("dh") => {
14698 let args = parsed
14699 .get("args")
14700 .and_then(Value::as_array)
14701 .ok_or_else(|| {
14702 SidecarError::InvalidState(String::from(
14703 "crypto.diffieHellmanSessionCreate dh requires args",
14704 ))
14705 })?;
14706 let params = javascript_crypto_build_dh_params(args)?;
14707 ActiveDiffieHellmanSession::Dh(ActiveDhSession {
14708 params,
14709 key_pair: None,
14710 })
14711 }
14712 Some("ecdh") => {
14713 let curve = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
14714 SidecarError::InvalidState(String::from(
14715 "crypto.diffieHellmanSessionCreate ecdh requires name",
14716 ))
14717 })?;
14718 ActiveDiffieHellmanSession::Ecdh(ActiveEcdhSession {
14719 curve: curve.to_string(),
14720 key_pair: None,
14721 })
14722 }
14723 other => {
14724 return Err(SidecarError::InvalidState(format!(
14725 "Unsupported Diffie-Hellman session type: {}",
14726 other.unwrap_or("<missing>")
14727 )));
14728 }
14729 };
14730 process.next_diffie_hellman_session_id += 1;
14731 let session_id = process.next_diffie_hellman_session_id;
14732 process.diffie_hellman_sessions.insert(session_id, session);
14733 Ok(json!(session_id))
14734}
14735
14736fn service_javascript_crypto_diffie_hellman_session_call_sync_rpc(
14737 process: &mut ActiveProcess,
14738 request: &JavascriptSyncRpcRequest,
14739) -> Result<Value, SidecarError> {
14740 let session_id = javascript_sync_rpc_arg_u64(
14741 &request.args,
14742 0,
14743 "crypto.diffieHellmanSessionCall session id",
14744 )?;
14745 let raw =
14746 javascript_sync_rpc_arg_str(&request.args, 1, "crypto.diffieHellmanSessionCall request")?;
14747 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14748 SidecarError::InvalidState(format!(
14749 "crypto.diffieHellmanSessionCall request must be valid JSON: {error}"
14750 ))
14751 })?;
14752 let method = parsed
14753 .get("method")
14754 .and_then(Value::as_str)
14755 .ok_or_else(|| {
14756 SidecarError::InvalidState(String::from(
14757 "crypto.diffieHellmanSessionCall request missing method",
14758 ))
14759 })?;
14760 let args = parsed
14761 .get("args")
14762 .and_then(Value::as_array)
14763 .cloned()
14764 .unwrap_or_default();
14765 let session = process
14766 .diffie_hellman_sessions
14767 .get_mut(&session_id)
14768 .ok_or_else(|| {
14769 SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
14770 })?;
14771 let (result, has_result) = match session {
14772 ActiveDiffieHellmanSession::Dh(session) => {
14773 javascript_crypto_call_dh_session(session, method, &args)?
14774 }
14775 ActiveDiffieHellmanSession::Ecdh(session) => {
14776 javascript_crypto_call_ecdh_session(session, method, &args)?
14777 }
14778 };
14779 Ok(Value::String(
14780 serde_json::to_string(&json!({
14781 "result": result,
14782 "hasResult": has_result,
14783 }))
14784 .map_err(|error| {
14785 SidecarError::InvalidState(format!("serialize diffie session result: {error}"))
14786 })?,
14787 ))
14788}
14789
14790fn service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(
14791 process: &mut ActiveProcess,
14792 request: &JavascriptSyncRpcRequest,
14793) -> Result<Value, SidecarError> {
14794 let session_id = javascript_sync_rpc_arg_u64(
14795 &request.args,
14796 0,
14797 "crypto.diffieHellmanSessionDestroy session id",
14798 )?;
14799 process
14800 .diffie_hellman_sessions
14801 .remove(&session_id)
14802 .ok_or_else(|| {
14803 SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
14804 })?;
14805 Ok(Value::Null)
14806}
14807
14808fn service_javascript_crypto_subtle_sync_rpc(
14809 request: &JavascriptSyncRpcRequest,
14810) -> Result<Value, SidecarError> {
14811 let raw = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.subtle request")?;
14812 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14813 SidecarError::InvalidState(format!("crypto.subtle request must be valid JSON: {error}"))
14814 })?;
14815 let op = parsed.get("op").and_then(Value::as_str).ok_or_else(|| {
14816 SidecarError::InvalidState(String::from("crypto.subtle request missing op"))
14817 })?;
14818 match op {
14819 "digest" => {
14820 let algorithm = parsed
14821 .get("algorithm")
14822 .and_then(Value::as_str)
14823 .ok_or_else(|| {
14824 SidecarError::InvalidState(String::from(
14825 "crypto.subtle.digest missing algorithm",
14826 ))
14827 })?;
14828 let data = parsed.get("data").and_then(Value::as_str).ok_or_else(|| {
14829 SidecarError::InvalidState(String::from("crypto.subtle.digest missing data"))
14830 })?;
14831 let bytes = base64::engine::general_purpose::STANDARD
14832 .decode(data)
14833 .map_err(|error| {
14834 SidecarError::InvalidState(format!("crypto.subtle.digest data base64: {error}"))
14835 })?;
14836 let digest = JavascriptCryptoDigestAlgorithm::parse(algorithm)?.digest(&bytes);
14837 Ok(Value::String(
14838 serde_json::to_string(&json!({
14839 "data": base64::engine::general_purpose::STANDARD.encode(digest),
14840 }))
14841 .map_err(|error| {
14842 SidecarError::InvalidState(format!("serialize crypto.subtle digest: {error}"))
14843 })?,
14844 ))
14845 }
14846 "generateKey" => {
14847 let algorithm = parsed.get("algorithm").ok_or_else(|| {
14848 SidecarError::InvalidState(String::from(
14849 "crypto.subtle.generateKey missing algorithm",
14850 ))
14851 })?;
14852 let name =
14853 javascript_crypto_subtle_algorithm_name(algorithm, "crypto.subtle.generateKey")?;
14854 if !matches!(name, "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW") {
14855 return Err(SidecarError::InvalidState(format!(
14856 "Unsupported key algorithm: {name}"
14857 )));
14858 }
14859 let length_bits = algorithm
14860 .get("length")
14861 .and_then(Value::as_u64)
14862 .ok_or_else(|| {
14863 SidecarError::InvalidState(String::from(
14864 "crypto.subtle.generateKey AES algorithm requires length",
14865 ))
14866 })?;
14867 if length_bits % 8 != 0 {
14868 return Err(SidecarError::InvalidState(String::from(
14869 "crypto.subtle.generateKey length must be byte-aligned",
14870 )));
14871 }
14872 let length_bytes = usize::try_from(length_bits / 8).map_err(|_| {
14873 SidecarError::InvalidState(String::from(
14874 "crypto.subtle.generateKey length is too large",
14875 ))
14876 })?;
14877 let mut raw = vec![0_u8; length_bytes];
14878 rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
14879 let key = javascript_crypto_serialize_subtle_secret_key(
14880 &raw,
14881 javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
14882 parsed
14883 .get("extractable")
14884 .and_then(Value::as_bool)
14885 .unwrap_or(false),
14886 parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
14887 )?;
14888 Ok(Value::String(
14889 serde_json::to_string(&json!({ "key": key })).map_err(|error| {
14890 SidecarError::InvalidState(format!(
14891 "serialize crypto.subtle generated key: {error}"
14892 ))
14893 })?,
14894 ))
14895 }
14896 "importKey" => {
14897 let format = parsed
14898 .get("format")
14899 .and_then(Value::as_str)
14900 .ok_or_else(|| {
14901 SidecarError::InvalidState(String::from(
14902 "crypto.subtle.importKey missing format",
14903 ))
14904 })?;
14905 if format != "raw" {
14906 return Err(SidecarError::InvalidState(format!(
14907 "Unsupported import format: {format}"
14908 )));
14909 }
14910 let key_data = parsed
14911 .get("keyData")
14912 .and_then(Value::as_str)
14913 .ok_or_else(|| {
14914 SidecarError::InvalidState(String::from(
14915 "crypto.subtle.importKey missing keyData",
14916 ))
14917 })?;
14918 let raw = base64::engine::general_purpose::STANDARD
14919 .decode(key_data)
14920 .map_err(|error| {
14921 SidecarError::InvalidState(format!(
14922 "crypto.subtle.importKey keyData base64: {error}"
14923 ))
14924 })?;
14925 let algorithm = parsed.get("algorithm").ok_or_else(|| {
14926 SidecarError::InvalidState(String::from(
14927 "crypto.subtle.importKey missing algorithm",
14928 ))
14929 })?;
14930 let key = javascript_crypto_serialize_subtle_secret_key(
14931 &raw,
14932 javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
14933 parsed
14934 .get("extractable")
14935 .and_then(Value::as_bool)
14936 .unwrap_or(false),
14937 parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
14938 )?;
14939 Ok(Value::String(
14940 serde_json::to_string(&json!({ "key": key })).map_err(|error| {
14941 SidecarError::InvalidState(format!(
14942 "serialize crypto.subtle imported key: {error}"
14943 ))
14944 })?,
14945 ))
14946 }
14947 "exportKey" => {
14948 let format = parsed
14949 .get("format")
14950 .and_then(Value::as_str)
14951 .ok_or_else(|| {
14952 SidecarError::InvalidState(String::from(
14953 "crypto.subtle.exportKey missing format",
14954 ))
14955 })?;
14956 if format != "raw" {
14957 return Err(SidecarError::InvalidState(format!(
14958 "Unsupported export format: {format}"
14959 )));
14960 }
14961 let raw = javascript_crypto_subtle_key_raw(
14962 parsed.get("key").ok_or_else(|| {
14963 SidecarError::InvalidState(String::from("crypto.subtle.exportKey missing key"))
14964 })?,
14965 "crypto.subtle.exportKey key",
14966 )?;
14967 Ok(Value::String(
14968 serde_json::to_string(&json!({
14969 "data": base64::engine::general_purpose::STANDARD.encode(raw),
14970 }))
14971 .map_err(|error| {
14972 SidecarError::InvalidState(format!("serialize crypto.subtle export: {error}"))
14973 })?,
14974 ))
14975 }
14976 "encrypt" | "decrypt" => service_javascript_crypto_subtle_aes_crypt_sync_rpc(op, &parsed),
14977 _ => Err(SidecarError::InvalidState(format!(
14978 "Unsupported subtle operation: {op}"
14979 ))),
14980 }
14981}
14982
14983fn javascript_crypto_subtle_algorithm_name<'a>(
14984 algorithm: &'a Value,
14985 label: &str,
14986) -> Result<&'a str, SidecarError> {
14987 if let Some(name) = algorithm.as_str() {
14988 return Ok(name);
14989 }
14990 algorithm
14991 .get("name")
14992 .and_then(Value::as_str)
14993 .ok_or_else(|| SidecarError::InvalidState(format!("{label} algorithm missing name")))
14994}
14995
14996fn javascript_crypto_normalize_subtle_secret_algorithm(
14997 algorithm: Value,
14998 raw: &[u8],
14999) -> Result<Value, SidecarError> {
15000 let mut object = match algorithm {
15001 Value::String(name) => {
15002 let mut object = Map::new();
15003 object.insert(String::from("name"), Value::String(name));
15004 object
15005 }
15006 Value::Object(object) => object,
15007 _ => {
15008 return Err(SidecarError::InvalidState(String::from(
15009 "crypto.subtle secret algorithm must be a string or object",
15010 )));
15011 }
15012 };
15013 let name = object
15014 .get("name")
15015 .and_then(Value::as_str)
15016 .ok_or_else(|| {
15017 SidecarError::InvalidState(String::from("crypto.subtle secret algorithm missing name"))
15018 })?
15019 .to_string();
15020 if matches!(name.as_str(), "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW")
15021 && !object.contains_key("length")
15022 {
15023 object.insert(String::from("length"), json!(raw.len() * 8));
15024 }
15025 Ok(Value::Object(object))
15026}
15027
15028fn javascript_crypto_serialize_subtle_secret_key(
15029 raw: &[u8],
15030 algorithm: Value,
15031 extractable: bool,
15032 usages: Value,
15033) -> Result<Value, SidecarError> {
15034 let raw_base64 = base64::engine::general_purpose::STANDARD.encode(raw);
15035 let source_key_object_data = javascript_crypto_serialize_sandbox_key_object(
15036 &JavascriptCryptoKeyMaterial::Secret(raw.to_vec()),
15037 )?;
15038 Ok(json!({
15039 "type": "secret",
15040 "algorithm": algorithm,
15041 "extractable": extractable,
15042 "usages": usages,
15043 "_raw": raw_base64,
15044 "_sourceKeyObjectData": source_key_object_data,
15045 }))
15046}
15047
15048fn javascript_crypto_subtle_key_raw(key: &Value, label: &str) -> Result<Vec<u8>, SidecarError> {
15049 let raw = key.get("_raw").and_then(Value::as_str).ok_or_else(|| {
15050 SidecarError::InvalidState(format!("{label} must be a raw secret CryptoKey"))
15051 })?;
15052 base64::engine::general_purpose::STANDARD
15053 .decode(raw)
15054 .map_err(|error| SidecarError::InvalidState(format!("{label} raw base64: {error}")))
15055}
15056
15057fn service_javascript_crypto_subtle_aes_crypt_sync_rpc(
15058 op: &str,
15059 parsed: &Value,
15060) -> Result<Value, SidecarError> {
15061 let algorithm = parsed.get("algorithm").ok_or_else(|| {
15062 SidecarError::InvalidState(format!("crypto.subtle.{op} missing algorithm"))
15063 })?;
15064 let name = javascript_crypto_subtle_algorithm_name(algorithm, &format!("crypto.subtle.{op}"))?;
15065 if name != "AES-GCM" {
15066 return Err(SidecarError::InvalidState(format!(
15067 "Unsupported subtle AES operation algorithm: {name}"
15068 )));
15069 }
15070 let key = javascript_crypto_subtle_key_raw(
15071 parsed
15072 .get("key")
15073 .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing key")))?,
15074 &format!("crypto.subtle.{op} key"),
15075 )?;
15076 let iv = algorithm.get("iv").and_then(Value::as_str).ok_or_else(|| {
15077 SidecarError::InvalidState(format!("crypto.subtle.{op} AES-GCM missing iv"))
15078 })?;
15079 let iv = base64::engine::general_purpose::STANDARD
15080 .decode(iv)
15081 .map_err(|error| {
15082 SidecarError::InvalidState(format!("crypto.subtle.{op} iv base64: {error}"))
15083 })?;
15084 let data = parsed
15085 .get("data")
15086 .and_then(Value::as_str)
15087 .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing data")))?;
15088 let mut data = base64::engine::general_purpose::STANDARD
15089 .decode(data)
15090 .map_err(|error| {
15091 SidecarError::InvalidState(format!("crypto.subtle.{op} data base64: {error}"))
15092 })?;
15093 let tag_len = javascript_crypto_subtle_aes_gcm_tag_len(algorithm)?;
15094 let mut options = Map::new();
15095 options.insert(String::from("authTagLength"), json!(tag_len));
15096 if let Some(additional_data) = algorithm.get("additionalData").and_then(Value::as_str) {
15097 options.insert(
15098 String::from("aad"),
15099 Value::String(additional_data.to_string()),
15100 );
15101 }
15102 let decrypt = op == "decrypt";
15103 if decrypt {
15104 if data.len() < tag_len {
15105 return Err(SidecarError::InvalidState(String::from(
15106 "crypto.subtle.decrypt AES-GCM data shorter than auth tag",
15107 )));
15108 }
15109 let auth_tag = data.split_off(data.len() - tag_len);
15110 options.insert(
15111 String::from("authTag"),
15112 Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
15113 );
15114 }
15115 let cipher_name = format!("aes-{}-gcm", key.len() * 8);
15116 let mut context = javascript_crypto_build_cipher_context(
15117 &cipher_name,
15118 &key,
15119 Some(&iv),
15120 decrypt,
15121 Some(&Value::Object(options)),
15122 )?;
15123 let mut output = javascript_crypto_cipher_update(&mut context, &data)?;
15124 output.extend(javascript_crypto_cipher_finalize(&mut context)?);
15125 if !decrypt {
15126 let mut auth_tag = vec![0_u8; tag_len];
15127 context
15128 .get_tag(&mut auth_tag)
15129 .map_err(javascript_crypto_openssl_error)?;
15130 output.extend(auth_tag);
15131 }
15132 Ok(Value::String(
15133 serde_json::to_string(&json!({
15134 "data": base64::engine::general_purpose::STANDARD.encode(output),
15135 }))
15136 .map_err(|error| {
15137 SidecarError::InvalidState(format!("serialize crypto.subtle {op}: {error}"))
15138 })?,
15139 ))
15140}
15141
15142fn javascript_crypto_subtle_aes_gcm_tag_len(algorithm: &Value) -> Result<usize, SidecarError> {
15143 let tag_bits = algorithm
15144 .get("tagLength")
15145 .and_then(Value::as_u64)
15146 .unwrap_or(128);
15147 if !tag_bits.is_multiple_of(8) {
15148 return Err(SidecarError::InvalidState(String::from(
15149 "crypto.subtle AES-GCM tagLength must be byte-aligned",
15150 )));
15151 }
15152 usize::try_from(tag_bits / 8).map_err(|_| {
15153 SidecarError::InvalidState(String::from("crypto.subtle AES-GCM tagLength too large"))
15154 })
15155}
15156
15157fn service_javascript_crypto_cipheriv_inner(
15158 request: &JavascriptSyncRpcRequest,
15159 decrypt: bool,
15160) -> Result<Value, SidecarError> {
15161 let label = if decrypt {
15162 "crypto.decipheriv"
15163 } else {
15164 "crypto.cipheriv"
15165 };
15166 let algorithm = javascript_sync_rpc_arg_str(&request.args, 0, &format!("{label} algorithm"))?;
15167 let key = javascript_sync_rpc_base64_arg(&request.args, 1, &format!("{label} key"))?;
15168 let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 2, &format!("{label} iv"))?;
15169 let data = javascript_sync_rpc_base64_arg(&request.args, 3, &format!("{label} data"))?;
15170 let options =
15171 javascript_sync_rpc_json_arg_optional(&request.args, 4, &format!("{label} options"))?;
15172 let auth_tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options.as_ref())?;
15173 let mut context = javascript_crypto_build_cipher_context(
15174 algorithm,
15175 &key,
15176 iv.as_deref(),
15177 decrypt,
15178 options.as_ref(),
15179 )?;
15180 let payload = javascript_crypto_cipher_update(&mut context, &data)?;
15181 let final_bytes = javascript_crypto_cipher_finalize(&mut context)?;
15182 if decrypt {
15183 let mut output = payload;
15184 output.extend(final_bytes);
15185 return Ok(Value::String(
15186 base64::engine::general_purpose::STANDARD.encode(output),
15187 ));
15188 }
15189
15190 let mut response = Map::new();
15191 let mut encrypted = payload;
15192 encrypted.extend(final_bytes);
15193 response.insert(
15194 String::from("data"),
15195 Value::String(base64::engine::general_purpose::STANDARD.encode(encrypted)),
15196 );
15197 if javascript_crypto_is_aead(algorithm) {
15198 let mut auth_tag = vec![0_u8; auth_tag_len];
15199 context
15200 .get_tag(&mut auth_tag)
15201 .map_err(javascript_crypto_openssl_error)?;
15202 response.insert(
15203 String::from("authTag"),
15204 Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
15205 );
15206 }
15207 Ok(Value::String(serde_json::to_string(&response).map_err(
15208 |error| SidecarError::InvalidState(format!("serialize {label} response: {error}")),
15209 )?))
15210}
15211
15212fn javascript_sync_rpc_base64_arg_optional(
15213 args: &[Value],
15214 index: usize,
15215 label: &str,
15216) -> Result<Option<Vec<u8>>, SidecarError> {
15217 if args.get(index).is_none() || args[index].is_null() {
15218 return Ok(None);
15219 }
15220 javascript_sync_rpc_base64_arg(args, index, label).map(Some)
15221}
15222
15223fn javascript_sync_rpc_json_arg_optional(
15224 args: &[Value],
15225 index: usize,
15226 label: &str,
15227) -> Result<Option<Value>, SidecarError> {
15228 if args.get(index).is_none() || args[index].is_null() {
15229 return Ok(None);
15230 }
15231 let raw = javascript_sync_rpc_arg_str(args, index, label)?;
15232 serde_json::from_str(raw)
15233 .map(Some)
15234 .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
15235}
15236
15237fn javascript_crypto_parse_direct_key_input(
15238 raw: &str,
15239 expected: Option<&str>,
15240 label: &str,
15241) -> Result<JavascriptDirectKeyInput, SidecarError> {
15242 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
15243 SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
15244 })?;
15245 let padding = match parsed.as_object().and_then(|value| value.get("padding")) {
15246 Some(value) => javascript_crypto_padding_from_value(value)?,
15247 None => None,
15248 };
15249 Ok(JavascriptDirectKeyInput {
15250 key: javascript_crypto_parse_key_material_value(&parsed, expected, label)?,
15251 padding,
15252 })
15253}
15254
15255fn javascript_crypto_parse_key_material_value(
15256 value: &Value,
15257 expected: Option<&str>,
15258 label: &str,
15259) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15260 if let Some(object) = value.as_object() {
15261 if object.get("__type").and_then(Value::as_str) == Some("keyObject") {
15262 let serialized = object.get("value").ok_or_else(|| {
15263 SidecarError::InvalidState(format!("{label} keyObject is missing a value"))
15264 })?;
15265 return javascript_crypto_parse_serialized_key_object(serialized, expected, label);
15266 }
15267 if object.contains_key("type") && (object.contains_key("pem") || object.contains_key("raw"))
15268 {
15269 return javascript_crypto_parse_serialized_key_object(value, expected, label);
15270 }
15271 if let Some(source) = object.get("key") {
15272 return javascript_crypto_parse_key_source(
15273 source,
15274 object.get("format").and_then(Value::as_str),
15275 object.get("type").and_then(Value::as_str),
15276 expected,
15277 label,
15278 );
15279 }
15280 }
15281 javascript_crypto_parse_key_source(value, None, None, expected, label)
15282}
15283
15284fn javascript_crypto_parse_key_source(
15285 source: &Value,
15286 format: Option<&str>,
15287 kind: Option<&str>,
15288 expected: Option<&str>,
15289 label: &str,
15290) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15291 match source {
15292 Value::String(pem) => javascript_crypto_parse_key_from_pem(pem.as_bytes(), expected, label),
15293 Value::Object(object) if object.get("__type").and_then(Value::as_str) == Some("buffer") => {
15294 let data = javascript_crypto_decode_bridge_buffer(source, label)?;
15295 javascript_crypto_parse_key_from_bytes(&data, format, kind, expected, label)
15296 }
15297 Value::Object(_) => {
15298 if format == Some("jwk") {
15299 return Err(SidecarError::InvalidState(format!(
15300 "{label} jwk inputs are not supported yet"
15301 )));
15302 }
15303 Err(SidecarError::InvalidState(format!(
15304 "{label} has an unsupported key shape"
15305 )))
15306 }
15307 _ => Err(SidecarError::InvalidState(format!(
15308 "{label} has an unsupported key value"
15309 ))),
15310 }
15311}
15312
15313fn javascript_crypto_parse_key_from_pem(
15314 pem: &[u8],
15315 expected: Option<&str>,
15316 label: &str,
15317) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15318 match expected {
15319 Some("private") => PKey::private_key_from_pem(pem)
15320 .map(JavascriptCryptoKeyMaterial::Private)
15321 .map_err(|error| {
15322 SidecarError::InvalidState(format!("{label} private key is invalid: {error}"))
15323 }),
15324 Some("public") => PKey::public_key_from_pem(pem)
15325 .map(JavascriptCryptoKeyMaterial::Public)
15326 .map_err(|error| {
15327 SidecarError::InvalidState(format!("{label} public key is invalid: {error}"))
15328 }),
15329 _ => PKey::private_key_from_pem(pem)
15330 .map(JavascriptCryptoKeyMaterial::Private)
15331 .or_else(|_| PKey::public_key_from_pem(pem).map(JavascriptCryptoKeyMaterial::Public))
15332 .map_err(|error| {
15333 SidecarError::InvalidState(format!("{label} PEM key is invalid: {error}"))
15334 }),
15335 }
15336}
15337
15338fn javascript_crypto_parse_key_from_bytes(
15339 der: &[u8],
15340 format: Option<&str>,
15341 kind: Option<&str>,
15342 expected: Option<&str>,
15343 label: &str,
15344) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15345 match (format.unwrap_or("der"), kind.or(expected)) {
15346 ("der", Some("pkcs8")) | ("der", Some("private")) => PKey::private_key_from_der(der)
15347 .map(JavascriptCryptoKeyMaterial::Private)
15348 .map_err(|error| {
15349 SidecarError::InvalidState(format!("{label} private key DER is invalid: {error}"))
15350 }),
15351 ("der", Some("spki")) | ("der", Some("public")) => PKey::public_key_from_der(der)
15352 .map(JavascriptCryptoKeyMaterial::Public)
15353 .map_err(|error| {
15354 SidecarError::InvalidState(format!("{label} public key DER is invalid: {error}"))
15355 }),
15356 _ => Err(SidecarError::InvalidState(format!(
15357 "{label} unsupported key bytes format"
15358 ))),
15359 }
15360}
15361
15362fn javascript_crypto_parse_serialized_key_object(
15363 value: &Value,
15364 expected: Option<&str>,
15365 label: &str,
15366) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15367 let serialized: JavascriptSerializedSandboxKeyObject = serde_json::from_value(value.clone())
15368 .map_err(|error| {
15369 SidecarError::InvalidState(format!("{label} keyObject is invalid: {error}"))
15370 })?;
15371 match serialized.kind.as_str() {
15372 "secret" => {
15373 if expected == Some("public") || expected == Some("private") {
15374 return Err(SidecarError::InvalidState(format!(
15375 "{label} expected an asymmetric key"
15376 )));
15377 }
15378 Ok(JavascriptCryptoKeyMaterial::Secret(
15379 base64::engine::general_purpose::STANDARD
15380 .decode(serialized.raw.unwrap_or_default())
15381 .map_err(|error| {
15382 SidecarError::InvalidState(format!(
15383 "{label} secret key contains invalid base64: {error}"
15384 ))
15385 })?,
15386 ))
15387 }
15388 "private" => {
15389 let pem = serialized.pem.ok_or_else(|| {
15390 SidecarError::InvalidState(format!("{label} private keyObject is missing pem"))
15391 })?;
15392 javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("private"), label)
15393 }
15394 "public" => {
15395 let pem = serialized.pem.ok_or_else(|| {
15396 SidecarError::InvalidState(format!("{label} public keyObject is missing pem"))
15397 })?;
15398 javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("public"), label)
15399 }
15400 other => Err(SidecarError::InvalidState(format!(
15401 "{label} has unsupported keyObject type {other}"
15402 ))),
15403 }
15404}
15405
15406fn javascript_crypto_expect_private_key(
15407 key: JavascriptCryptoKeyMaterial,
15408 label: &str,
15409) -> Result<PKey<Private>, SidecarError> {
15410 match key {
15411 JavascriptCryptoKeyMaterial::Private(key) => Ok(key),
15412 _ => Err(SidecarError::InvalidState(format!(
15413 "{label} requires a private key"
15414 ))),
15415 }
15416}
15417
15418fn javascript_crypto_expect_public_key(
15419 key: JavascriptCryptoKeyMaterial,
15420 label: &str,
15421) -> Result<PKey<Public>, SidecarError> {
15422 match key {
15423 JavascriptCryptoKeyMaterial::Public(key) => Ok(key),
15424 JavascriptCryptoKeyMaterial::Private(key) => {
15425 let pem = key
15426 .public_key_to_pem()
15427 .map_err(javascript_crypto_openssl_error)?;
15428 PKey::public_key_from_pem(&pem).map_err(javascript_crypto_openssl_error)
15429 }
15430 _ => Err(SidecarError::InvalidState(format!(
15431 "{label} requires a public key"
15432 ))),
15433 }
15434}
15435
15436fn javascript_crypto_new_signer<'a>(
15437 algorithm: Option<&'a str>,
15438 key: &'a PKey<Private>,
15439) -> Result<Signer<'a>, SidecarError> {
15440 if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
15441 return Signer::new_without_digest(key).map_err(javascript_crypto_openssl_error);
15442 }
15443 Signer::new(
15444 javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
15445 SidecarError::InvalidState(String::from("crypto.sign requires a digest algorithm"))
15446 })?)?,
15447 key,
15448 )
15449 .map_err(javascript_crypto_openssl_error)
15450}
15451
15452fn javascript_crypto_new_verifier<'a>(
15453 algorithm: Option<&'a str>,
15454 key: &'a PKey<Public>,
15455) -> Result<Verifier<'a>, SidecarError> {
15456 if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
15457 return Verifier::new_without_digest(key).map_err(javascript_crypto_openssl_error);
15458 }
15459 Verifier::new(
15460 javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
15461 SidecarError::InvalidState(String::from("crypto.verify requires a digest algorithm"))
15462 })?)?,
15463 key,
15464 )
15465 .map_err(javascript_crypto_openssl_error)
15466}
15467
15468fn javascript_crypto_message_digest_from_name(name: &str) -> Result<MessageDigest, SidecarError> {
15469 match name.trim().to_ascii_lowercase().replace('-', "").as_str() {
15470 "md5" => Ok(MessageDigest::md5()),
15471 "sha1" => Ok(MessageDigest::sha1()),
15472 "sha256" => Ok(MessageDigest::sha256()),
15473 "sha384" => Ok(MessageDigest::sha384()),
15474 "sha512" => Ok(MessageDigest::sha512()),
15475 other => Err(SidecarError::InvalidState(format!(
15476 "unsupported crypto digest algorithm {other}"
15477 ))),
15478 }
15479}
15480
15481fn javascript_crypto_padding_from_value(value: &Value) -> Result<Option<Padding>, SidecarError> {
15482 let Some(number) = value.as_i64() else {
15483 return Ok(None);
15484 };
15485 let padding = match number {
15486 1 => Padding::PKCS1,
15487 3 => Padding::NONE,
15488 4 => Padding::PKCS1_OAEP,
15489 6 => Padding::PKCS1_PSS,
15490 other => {
15491 return Err(SidecarError::InvalidState(format!(
15492 "unsupported RSA padding constant {other}"
15493 )));
15494 }
15495 };
15496 Ok(Some(padding))
15497}
15498
15499fn javascript_crypto_decode_bridge_buffer(
15500 value: &Value,
15501 label: &str,
15502) -> Result<Vec<u8>, SidecarError> {
15503 let base64_value = value
15504 .as_object()
15505 .filter(|object| object.get("__type").and_then(Value::as_str) == Some("buffer"))
15506 .and_then(|object| object.get("value"))
15507 .and_then(Value::as_str)
15508 .ok_or_else(|| {
15509 SidecarError::InvalidState(format!("{label} must be a serialized bridge buffer"))
15510 })?;
15511 base64::engine::general_purpose::STANDARD
15512 .decode(base64_value)
15513 .map_err(|error| {
15514 SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
15515 })
15516}
15517
15518fn javascript_crypto_serialize_sandbox_key_object(
15519 key: &JavascriptCryptoKeyMaterial,
15520) -> Result<Value, SidecarError> {
15521 let serialized = match key {
15522 JavascriptCryptoKeyMaterial::Private(key) => JavascriptSerializedSandboxKeyObject {
15523 kind: String::from("private"),
15524 pem: Some(
15525 String::from_utf8(
15526 key.private_key_to_pem_pkcs8()
15527 .map_err(javascript_crypto_openssl_error)?,
15528 )
15529 .map_err(|error| {
15530 SidecarError::InvalidState(format!("private key PEM is not utf8: {error}"))
15531 })?,
15532 ),
15533 raw: None,
15534 asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
15535 asymmetric_key_details: None,
15536 jwk: None,
15537 },
15538 JavascriptCryptoKeyMaterial::Public(key) => JavascriptSerializedSandboxKeyObject {
15539 kind: String::from("public"),
15540 pem: Some(
15541 String::from_utf8(
15542 key.public_key_to_pem()
15543 .map_err(javascript_crypto_openssl_error)?,
15544 )
15545 .map_err(|error| {
15546 SidecarError::InvalidState(format!("public key PEM is not utf8: {error}"))
15547 })?,
15548 ),
15549 raw: None,
15550 asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
15551 asymmetric_key_details: None,
15552 jwk: None,
15553 },
15554 JavascriptCryptoKeyMaterial::Secret(raw) => JavascriptSerializedSandboxKeyObject {
15555 kind: String::from("secret"),
15556 pem: None,
15557 raw: Some(base64::engine::general_purpose::STANDARD.encode(raw)),
15558 asymmetric_key_type: None,
15559 asymmetric_key_details: None,
15560 jwk: None,
15561 },
15562 };
15563 serde_json::to_value(serialized)
15564 .map_err(|error| SidecarError::InvalidState(format!("serialize key object: {error}")))
15565}
15566
15567fn javascript_crypto_pkey_type_name(id: PKeyId) -> Option<String> {
15568 match id {
15569 PKeyId::RSA => Some(String::from("rsa")),
15570 PKeyId::EC => Some(String::from("ec")),
15571 PKeyId::ED25519 => Some(String::from("ed25519")),
15572 PKeyId::ED448 => Some(String::from("ed448")),
15573 PKeyId::X25519 => Some(String::from("x25519")),
15574 PKeyId::X448 => Some(String::from("x448")),
15575 PKeyId::DH => Some(String::from("dh")),
15576 _ => None,
15577 }
15578}
15579
15580fn javascript_crypto_rsa_output_size(
15581 key: &JavascriptCryptoKeyMaterial,
15582) -> Result<usize, SidecarError> {
15583 match key {
15584 JavascriptCryptoKeyMaterial::Private(key) => key
15585 .rsa()
15586 .map(|rsa| rsa.size() as usize)
15587 .map_err(javascript_crypto_openssl_error),
15588 JavascriptCryptoKeyMaterial::Public(key) => key
15589 .rsa()
15590 .map(|rsa| rsa.size() as usize)
15591 .map_err(javascript_crypto_openssl_error),
15592 JavascriptCryptoKeyMaterial::Secret(_) => Err(SidecarError::InvalidState(String::from(
15593 "RSA operations require an asymmetric key",
15594 ))),
15595 }
15596}
15597
15598fn javascript_crypto_parse_serialized_options_arg(
15599 args: &[Value],
15600 index: usize,
15601 label: &str,
15602) -> Result<Option<Value>, SidecarError> {
15603 let Some(raw) = args.get(index).and_then(Value::as_str) else {
15604 return Ok(None);
15605 };
15606 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
15607 SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
15608 })?;
15609 if parsed.get("hasOptions").and_then(Value::as_bool) == Some(true) {
15610 Ok(parsed.get("options").cloned())
15611 } else {
15612 Ok(None)
15613 }
15614}
15615
15616fn javascript_crypto_u32_from_bridge_value(
15617 value: &Value,
15618 label: &str,
15619) -> Result<u32, SidecarError> {
15620 if let Some(number) = value.as_u64() {
15621 return u32::try_from(number)
15622 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")));
15623 }
15624 let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
15625 if bytes.len() > 4 {
15626 return Err(SidecarError::InvalidState(format!(
15627 "{label} buffer is too large for u32"
15628 )));
15629 }
15630 Ok(bytes
15631 .into_iter()
15632 .fold(0_u32, |acc, byte| (acc << 8) | u32::from(byte)))
15633}
15634
15635fn javascript_crypto_bignum_from_bridge_value(
15636 value: &Value,
15637 label: &str,
15638) -> Result<BigNum, SidecarError> {
15639 if let Some(object) = value.as_object() {
15640 if object.get("__type").and_then(Value::as_str) == Some("bigint") {
15641 let decimal = object.get("value").and_then(Value::as_str).ok_or_else(|| {
15642 SidecarError::InvalidState(format!("{label} bigint is missing a value"))
15643 })?;
15644 return BigNum::from_dec_str(decimal).map_err(javascript_crypto_openssl_error);
15645 }
15646 }
15647 let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
15648 BigNum::from_slice(&bytes).map_err(javascript_crypto_openssl_error)
15649}
15650
15651fn javascript_crypto_curve_nid(name: &str) -> Result<Nid, SidecarError> {
15652 match name {
15653 "prime256v1" | "P-256" => Ok(Nid::X9_62_PRIME256V1),
15654 "secp384r1" | "P-384" => Ok(Nid::SECP384R1),
15655 "secp521r1" | "P-521" => Ok(Nid::SECP521R1),
15656 "secp256k1" => Ok(Nid::SECP256K1),
15657 other => Err(SidecarError::InvalidState(format!(
15658 "unsupported EC curve {other}"
15659 ))),
15660 }
15661}
15662
15663fn javascript_crypto_named_dh_group(name: &str) -> Result<Dh<Params>, SidecarError> {
15664 match name {
15665 "modp2" => Dh::get_1024_160().map_err(javascript_crypto_openssl_error),
15666 "modp14" | "modp15" | "modp16" | "modp17" | "modp18" => {
15667 Dh::get_2048_256().map_err(javascript_crypto_openssl_error)
15668 }
15669 other => Err(SidecarError::InvalidState(format!(
15670 "unsupported Diffie-Hellman group {other}"
15671 ))),
15672 }
15673}
15674
15675fn javascript_crypto_clone_dh_params(params: &Dh<Params>) -> Result<Dh<Params>, SidecarError> {
15676 Dh::from_pqg(
15677 params
15678 .prime_p()
15679 .to_owned()
15680 .map_err(javascript_crypto_openssl_error)?,
15681 params
15682 .prime_q()
15683 .map(|value| value.to_owned().map_err(javascript_crypto_openssl_error))
15684 .transpose()?,
15685 params
15686 .generator()
15687 .to_owned()
15688 .map_err(javascript_crypto_openssl_error)?,
15689 )
15690 .map_err(javascript_crypto_openssl_error)
15691}
15692
15693fn javascript_crypto_build_dh_params(args: &[Value]) -> Result<Dh<Params>, SidecarError> {
15694 let Some(first) = args.first() else {
15695 return Err(SidecarError::InvalidState(String::from(
15696 "Diffie-Hellman session args are required",
15697 )));
15698 };
15699 if let Some(bits) = first.as_u64() {
15700 let generator = args
15701 .get(1)
15702 .map(|value| javascript_crypto_u32_from_bridge_value(value, "Diffie-Hellman generator"))
15703 .transpose()?
15704 .unwrap_or(2);
15705 return Dh::generate_params(bits as u32, generator)
15706 .map_err(javascript_crypto_openssl_error);
15707 }
15708 let prime = javascript_crypto_bignum_from_bridge_value(first, "Diffie-Hellman prime")?;
15709 let generator = args
15710 .get(1)
15711 .map(|value| javascript_crypto_bignum_from_bridge_value(value, "Diffie-Hellman generator"))
15712 .transpose()?
15713 .unwrap_or(BigNum::from_u32(2).map_err(javascript_crypto_openssl_error)?);
15714 Dh::from_pqg(prime, None, generator).map_err(javascript_crypto_openssl_error)
15715}
15716
15717fn javascript_crypto_call_dh_session(
15718 session: &mut ActiveDhSession,
15719 method: &str,
15720 args: &[Value],
15721) -> Result<(Value, bool), SidecarError> {
15722 match method {
15723 "verifyError" => Ok((Value::Null, false)),
15724 "generateKeys" => {
15725 if session.key_pair.is_none() {
15726 session.key_pair = Some(
15727 javascript_crypto_clone_dh_params(&session.params)?
15728 .generate_key()
15729 .map_err(javascript_crypto_openssl_error)?,
15730 );
15731 }
15732 let public = session
15733 .key_pair
15734 .as_ref()
15735 .expect("dh key pair")
15736 .public_key()
15737 .to_vec();
15738 Ok((javascript_crypto_bridge_buffer_value(&public), true))
15739 }
15740 "computeSecret" => {
15741 if session.key_pair.is_none() {
15742 session.key_pair = Some(
15743 javascript_crypto_clone_dh_params(&session.params)?
15744 .generate_key()
15745 .map_err(javascript_crypto_openssl_error)?,
15746 );
15747 }
15748 let peer = javascript_crypto_bignum_from_bridge_value(
15749 args.first().ok_or_else(|| {
15750 SidecarError::InvalidState(String::from(
15751 "computeSecret requires peer public key",
15752 ))
15753 })?,
15754 "Diffie-Hellman peer public key",
15755 )?;
15756 let secret = session
15757 .key_pair
15758 .as_ref()
15759 .expect("dh key pair")
15760 .compute_key(&peer)
15761 .map_err(javascript_crypto_openssl_error)?;
15762 Ok((javascript_crypto_bridge_buffer_value(&secret), true))
15763 }
15764 "getPrime" => Ok((
15765 javascript_crypto_bridge_buffer_value(&session.params.prime_p().to_vec()),
15766 true,
15767 )),
15768 "getGenerator" => Ok((
15769 javascript_crypto_bridge_buffer_value(&session.params.generator().to_vec()),
15770 true,
15771 )),
15772 "getPublicKey" => {
15773 if session.key_pair.is_none() {
15774 session.key_pair = Some(
15775 javascript_crypto_clone_dh_params(&session.params)?
15776 .generate_key()
15777 .map_err(javascript_crypto_openssl_error)?,
15778 );
15779 }
15780 Ok((
15781 javascript_crypto_bridge_buffer_value(
15782 &session
15783 .key_pair
15784 .as_ref()
15785 .expect("dh key pair")
15786 .public_key()
15787 .to_vec(),
15788 ),
15789 true,
15790 ))
15791 }
15792 "getPrivateKey" => {
15793 if session.key_pair.is_none() {
15794 session.key_pair = Some(
15795 javascript_crypto_clone_dh_params(&session.params)?
15796 .generate_key()
15797 .map_err(javascript_crypto_openssl_error)?,
15798 );
15799 }
15800 Ok((
15801 javascript_crypto_bridge_buffer_value(
15802 &session
15803 .key_pair
15804 .as_ref()
15805 .expect("dh key pair")
15806 .private_key()
15807 .to_vec(),
15808 ),
15809 true,
15810 ))
15811 }
15812 other => Err(SidecarError::InvalidState(format!(
15813 "Unsupported Diffie-Hellman method: {other}"
15814 ))),
15815 }
15816}
15817
15818fn javascript_crypto_call_ecdh_session(
15819 session: &mut ActiveEcdhSession,
15820 method: &str,
15821 args: &[Value],
15822) -> Result<(Value, bool), SidecarError> {
15823 let nid = javascript_crypto_curve_nid(&session.curve)?;
15824 let group = EcGroup::from_curve_name(nid).map_err(javascript_crypto_openssl_error)?;
15825 match method {
15826 "verifyError" => Ok((Value::Null, false)),
15827 "generateKeys" => {
15828 if session.key_pair.is_none() {
15829 session.key_pair =
15830 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15831 }
15832 let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15833 let bytes = session
15834 .key_pair
15835 .as_ref()
15836 .expect("ecdh key pair")
15837 .public_key()
15838 .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
15839 .map_err(javascript_crypto_openssl_error)?;
15840 Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
15841 }
15842 "computeSecret" => {
15843 if session.key_pair.is_none() {
15844 session.key_pair =
15845 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15846 }
15847 let peer_bytes = javascript_crypto_decode_bridge_buffer(
15848 args.first().ok_or_else(|| {
15849 SidecarError::InvalidState(String::from(
15850 "computeSecret requires peer public key",
15851 ))
15852 })?,
15853 "ECDH peer public key",
15854 )?;
15855 let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15856 let peer_point = EcPoint::from_bytes(&group, &peer_bytes, &mut ctx)
15857 .map_err(javascript_crypto_openssl_error)?;
15858 let peer_key = EcKey::from_public_key(&group, &peer_point)
15859 .map_err(javascript_crypto_openssl_error)?;
15860 let private =
15861 PKey::from_ec_key(session.key_pair.as_ref().expect("ecdh key pair").to_owned())
15862 .map_err(javascript_crypto_openssl_error)?;
15863 let peer = PKey::from_ec_key(peer_key).map_err(javascript_crypto_openssl_error)?;
15864 let mut deriver = Deriver::new(&private).map_err(javascript_crypto_openssl_error)?;
15865 deriver
15866 .set_peer(&peer)
15867 .map_err(javascript_crypto_openssl_error)?;
15868 let secret = deriver
15869 .derive_to_vec()
15870 .map_err(javascript_crypto_openssl_error)?;
15871 Ok((javascript_crypto_bridge_buffer_value(&secret), true))
15872 }
15873 "getPublicKey" => {
15874 if session.key_pair.is_none() {
15875 session.key_pair =
15876 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15877 }
15878 let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15879 let bytes = session
15880 .key_pair
15881 .as_ref()
15882 .expect("ecdh key pair")
15883 .public_key()
15884 .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
15885 .map_err(javascript_crypto_openssl_error)?;
15886 Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
15887 }
15888 "getPrivateKey" => {
15889 if session.key_pair.is_none() {
15890 session.key_pair =
15891 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15892 }
15893 Ok((
15894 javascript_crypto_bridge_buffer_value(
15895 &session
15896 .key_pair
15897 .as_ref()
15898 .expect("ecdh key pair")
15899 .private_key()
15900 .to_vec(),
15901 ),
15902 true,
15903 ))
15904 }
15905 other => Err(SidecarError::InvalidState(format!(
15906 "Unsupported Diffie-Hellman method: {other}"
15907 ))),
15908 }
15909}
15910
15911fn javascript_crypto_serialize_encoded_key_value_public(
15912 key: &PKey<Public>,
15913 encoding: Option<&Value>,
15914) -> Result<Value, SidecarError> {
15915 if let Some(encoding) = encoding {
15916 let format = encoding
15917 .get("format")
15918 .and_then(Value::as_str)
15919 .unwrap_or("pem");
15920 return Ok(match format {
15921 "der" => json!({
15922 "kind": "buffer",
15923 "value": base64::engine::general_purpose::STANDARD
15924 .encode(key.public_key_to_der().map_err(javascript_crypto_openssl_error)?),
15925 }),
15926 _ => json!({
15927 "kind": "string",
15928 "value": String::from_utf8(
15929 key.public_key_to_pem().map_err(javascript_crypto_openssl_error)?,
15930 )
15931 .map_err(|error| SidecarError::InvalidState(format!("public key PEM utf8: {error}")))?,
15932 }),
15933 });
15934 }
15935 javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(
15936 key.to_owned(),
15937 ))
15938}
15939
15940fn javascript_crypto_serialize_encoded_key_value_private(
15941 key: &PKey<Private>,
15942 encoding: Option<&Value>,
15943) -> Result<Value, SidecarError> {
15944 if let Some(encoding) = encoding {
15945 let format = encoding
15946 .get("format")
15947 .and_then(Value::as_str)
15948 .unwrap_or("pem");
15949 return Ok(match format {
15950 "der" => json!({
15951 "kind": "buffer",
15952 "value": base64::engine::general_purpose::STANDARD
15953 .encode(key.private_key_to_der().map_err(javascript_crypto_openssl_error)?),
15954 }),
15955 _ => json!({
15956 "kind": "string",
15957 "value": String::from_utf8(
15958 key.private_key_to_pem_pkcs8().map_err(javascript_crypto_openssl_error)?,
15959 )
15960 .map_err(|error| SidecarError::InvalidState(format!("private key PEM utf8: {error}")))?,
15961 }),
15962 });
15963 }
15964 javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(
15965 key.to_owned(),
15966 ))
15967}
15968
15969fn javascript_crypto_bridge_buffer_value(bytes: &[u8]) -> Value {
15970 json!({
15971 "__type": "buffer",
15972 "value": base64::engine::general_purpose::STANDARD.encode(bytes),
15973 })
15974}
15975
15976fn javascript_crypto_build_cipher_context(
15977 algorithm: &str,
15978 key: &[u8],
15979 iv: Option<&[u8]>,
15980 decrypt: bool,
15981 options: Option<&Value>,
15982) -> Result<Crypter, SidecarError> {
15983 let cipher = javascript_crypto_cipher_from_name(algorithm)?;
15984 let mode = if decrypt {
15985 Mode::Decrypt
15986 } else {
15987 Mode::Encrypt
15988 };
15989 let mut context =
15990 Crypter::new(cipher, mode, key, iv).map_err(javascript_crypto_openssl_error)?;
15991 if let Some(auto_padding) = options
15992 .and_then(|value| value.get("autoPadding"))
15993 .and_then(Value::as_bool)
15994 {
15995 context.pad(auto_padding);
15996 }
15997 if javascript_crypto_is_aead(algorithm) {
15998 if let Some(aad) = options
15999 .and_then(|value| value.get("aad"))
16000 .and_then(Value::as_str)
16001 {
16002 context
16003 .aad_update(
16004 &base64::engine::general_purpose::STANDARD
16005 .decode(aad)
16006 .map_err(|error| {
16007 SidecarError::InvalidState(format!(
16008 "cipher aad contains invalid base64: {error}"
16009 ))
16010 })?,
16011 )
16012 .map_err(javascript_crypto_openssl_error)?;
16013 }
16014 if decrypt {
16015 if let Some(auth_tag) = options
16016 .and_then(|value| value.get("authTag"))
16017 .and_then(Value::as_str)
16018 {
16019 let decoded = base64::engine::general_purpose::STANDARD
16020 .decode(auth_tag)
16021 .map_err(|error| {
16022 SidecarError::InvalidState(format!(
16023 "cipher authTag contains invalid base64: {error}"
16024 ))
16025 })?;
16026 context
16027 .set_tag(&decoded)
16028 .map_err(javascript_crypto_openssl_error)?;
16029 }
16030 }
16031 }
16032 Ok(context)
16033}
16034
16035fn javascript_crypto_requested_aead_tag_len(
16036 algorithm: &str,
16037 options: Option<&Value>,
16038) -> Result<usize, SidecarError> {
16039 if !javascript_crypto_is_aead(algorithm) {
16040 return Ok(0);
16041 }
16042 let requested = options
16043 .and_then(|value| value.get("authTagLength"))
16044 .and_then(Value::as_u64)
16045 .unwrap_or(javascript_crypto_aead_tag_len(algorithm) as u64);
16046 usize::try_from(requested).map_err(|_| {
16047 SidecarError::InvalidState(String::from("cipher authTagLength must fit within usize"))
16048 })
16049}
16050
16051fn javascript_crypto_cipher_update(
16052 context: &mut Crypter,
16053 data: &[u8],
16054) -> Result<Vec<u8>, SidecarError> {
16055 let mut output = vec![0_u8; data.len() + 32];
16056 let written = context
16057 .update(data, &mut output)
16058 .map_err(javascript_crypto_openssl_error)?;
16059 output.truncate(written);
16060 Ok(output)
16061}
16062
16063fn javascript_crypto_cipher_finalize(context: &mut Crypter) -> Result<Vec<u8>, SidecarError> {
16064 let mut output = vec![0_u8; 32];
16065 let written = context
16066 .finalize(&mut output)
16067 .map_err(javascript_crypto_openssl_error)?;
16068 output.truncate(written);
16069 Ok(output)
16070}
16071
16072fn javascript_crypto_cipher_from_name(name: &str) -> Result<Cipher, SidecarError> {
16073 match name.to_ascii_lowercase().as_str() {
16074 "aes-128-cbc" => Ok(Cipher::aes_128_cbc()),
16075 "aes-192-cbc" => Ok(Cipher::aes_192_cbc()),
16076 "aes-256-cbc" => Ok(Cipher::aes_256_cbc()),
16077 "aes-128-ctr" => Ok(Cipher::aes_128_ctr()),
16078 "aes-192-ctr" => Ok(Cipher::aes_192_ctr()),
16079 "aes-256-ctr" => Ok(Cipher::aes_256_ctr()),
16080 "aes-128-gcm" => Ok(Cipher::aes_128_gcm()),
16081 "aes-192-gcm" => Ok(Cipher::aes_192_gcm()),
16082 "aes-256-gcm" => Ok(Cipher::aes_256_gcm()),
16083 other => Err(SidecarError::InvalidState(format!(
16084 "unsupported crypto cipher algorithm {other}"
16085 ))),
16086 }
16087}
16088
16089fn javascript_crypto_is_aead(algorithm: &str) -> bool {
16090 algorithm.to_ascii_lowercase().ends_with("-gcm")
16091}
16092
16093fn javascript_crypto_aead_tag_len(_algorithm: &str) -> usize {
16094 16
16095}
16096
16097fn javascript_crypto_openssl_error(error: openssl::error::ErrorStack) -> SidecarError {
16098 SidecarError::Execution(format!("crypto operation failed: {error}"))
16099}
16100
16101fn service_javascript_kernel_stdin_sync_rpc(
16102 kernel: &mut SidecarKernel,
16103 process: &mut ActiveProcess,
16104 request: &JavascriptSyncRpcRequest,
16105) -> Result<Value, SidecarError> {
16106 let max_bytes =
16107 javascript_sync_rpc_arg_u64_optional(&request.args, 0, "__kernel_stdin_read max bytes")?
16108 .map(|value| value.clamp(1, DEFAULT_KERNEL_STDIN_READ_MAX_BYTES as u64) as usize)
16109 .unwrap_or(DEFAULT_KERNEL_STDIN_READ_MAX_BYTES);
16110 let timeout_ms =
16111 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_stdin_read timeout ms")?
16112 .unwrap_or(DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS);
16113
16114 match kernel
16115 .fd_read_with_timeout_result(
16116 EXECUTION_DRIVER_NAME,
16117 process.kernel_pid,
16118 0,
16119 max_bytes,
16120 Some(Duration::from_millis(timeout_ms)),
16121 )
16122 .map_err(kernel_error)
16123 {
16124 Ok(Some(chunk)) if !chunk.is_empty() => Ok(json!({
16125 "dataBase64": base64::engine::general_purpose::STANDARD.encode(chunk),
16126 })),
16127 Ok(Some(_)) => Ok(Value::Null),
16128 Ok(None) => Ok(json!({
16129 "done": true,
16130 })),
16131 Err(SidecarError::Kernel(error)) if error.starts_with("EAGAIN:") => Ok(Value::Null),
16132 Err(error) => Err(error),
16133 }
16134}
16135
16136fn service_javascript_pty_set_raw_mode_sync_rpc(
16137 kernel: &mut SidecarKernel,
16138 process: &mut ActiveProcess,
16139 request: &JavascriptSyncRpcRequest,
16140) -> Result<Value, SidecarError> {
16141 let enabled = javascript_sync_rpc_arg_bool(&request.args, 0, "__pty_set_raw_mode enabled")?;
16142 kernel
16143 .pty_set_discipline(
16144 EXECUTION_DRIVER_NAME,
16145 process.kernel_pid,
16146 0,
16147 LineDisciplineConfig {
16148 canonical: Some(!enabled),
16149 echo: Some(!enabled),
16150 isig: Some(!enabled),
16151 },
16152 )
16153 .map_err(kernel_error)?;
16154 Ok(Value::Null)
16155}
16156
16157fn service_javascript_kernel_stdio_write_sync_rpc(
16158 kernel: &mut SidecarKernel,
16159 process: &mut ActiveProcess,
16160 request: &JavascriptSyncRpcRequest,
16161) -> Result<Value, SidecarError> {
16162 let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_stdio_write fd")?;
16163 let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "__kernel_stdio_write chunk")?;
16164
16165 let written = match fd {
16166 1 => kernel
16167 .write_process_stdout(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
16168 .map_err(kernel_error)?,
16169 2 => kernel
16170 .write_process_stderr(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
16171 .map_err(kernel_error)?,
16172 other => {
16173 return Err(SidecarError::InvalidState(format!(
16174 "__kernel_stdio_write only supports fd 1/2, got {other}"
16175 )));
16176 }
16177 };
16178
16179 let event = if fd == 1 {
16180 ActiveExecutionEvent::Stdout(chunk)
16181 } else {
16182 ActiveExecutionEvent::Stderr(chunk)
16183 };
16184 process.queue_pending_execution_event(event)?;
16185
16186 Ok(json!(written))
16187}
16188
16189fn service_javascript_kernel_poll_sync_rpc(
16190 kernel: &mut SidecarKernel,
16191 process: &ActiveProcess,
16192 request: &JavascriptSyncRpcRequest,
16193) -> Result<Value, SidecarError> {
16194 let fd_requests: Vec<KernelPollFdRequest> = serde_json::from_value(
16195 request
16196 .args
16197 .first()
16198 .cloned()
16199 .unwrap_or_else(|| Value::Array(Vec::new())),
16200 )
16201 .map_err(|error| {
16202 SidecarError::InvalidState(format!(
16203 "__kernel_poll fd list must be a JSON array of {{ fd, events }} objects: {error}"
16204 ))
16205 })?;
16206 let timeout_ms =
16207 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_poll timeout ms")?
16208 .unwrap_or_default();
16209 let timeout_ms = i32::try_from(timeout_ms).map_err(|_| {
16210 SidecarError::InvalidState(String::from("__kernel_poll timeout ms must fit within i32"))
16211 })?;
16212
16213 let poll_fds = fd_requests
16214 .iter()
16215 .map(|entry| PollFd {
16216 fd: entry.fd,
16217 events: PollEvents::from_bits(entry.events),
16218 revents: PollEvents::empty(),
16219 })
16220 .collect::<Vec<_>>();
16221 let result = kernel
16222 .poll_fds(
16223 EXECUTION_DRIVER_NAME,
16224 process.kernel_pid,
16225 poll_fds,
16226 timeout_ms,
16227 )
16228 .map_err(kernel_error)?;
16229
16230 Ok(json!({
16231 "readyCount": result.ready_count,
16232 "fds": result
16233 .fds
16234 .into_iter()
16235 .map(|entry| KernelPollFdResponse {
16236 fd: entry.fd,
16237 events: entry.events.bits(),
16238 revents: entry.revents.bits(),
16239 })
16240 .collect::<Vec<_>>(),
16241 }))
16242}
16243
16244fn install_kernel_stdin_pipe(kernel: &mut SidecarKernel, pid: u32) -> Result<u32, SidecarError> {
16245 let (read_fd, write_fd) = kernel
16246 .open_pipe(EXECUTION_DRIVER_NAME, pid)
16247 .map_err(kernel_error)?;
16248 kernel
16249 .fd_dup2(EXECUTION_DRIVER_NAME, pid, read_fd, 0)
16250 .map_err(kernel_error)?;
16251 kernel
16252 .fd_close(EXECUTION_DRIVER_NAME, pid, read_fd)
16253 .map_err(kernel_error)?;
16254 Ok(write_fd)
16255}
16256
16257fn javascript_child_process_stdin_mode(request: &JavascriptChildProcessSpawnRequest) -> &str {
16258 request
16259 .options
16260 .stdio
16261 .first()
16262 .map(String::as_str)
16263 .unwrap_or("pipe")
16264}
16265
16266pub(crate) fn write_kernel_process_stdin(
16267 kernel: &mut SidecarKernel,
16268 process: &mut ActiveProcess,
16269 chunk: &[u8],
16270) -> Result<(), SidecarError> {
16271 if process.runtime == GuestRuntimeKind::JavaScript {
16272 return Ok(());
16273 }
16274 let Some(writer_fd) = process.kernel_stdin_writer_fd else {
16275 return Ok(());
16276 };
16277 kernel
16278 .fd_write(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd, chunk)
16279 .map(|_| ())
16280 .map_err(kernel_error)
16281}
16282
16283pub(crate) fn close_kernel_process_stdin(
16284 kernel: &mut SidecarKernel,
16285 process: &mut ActiveProcess,
16286) -> Result<(), SidecarError> {
16287 let Some(writer_fd) = process.kernel_stdin_writer_fd.take() else {
16288 return Ok(());
16289 };
16290 kernel
16291 .fd_close(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd)
16292 .map_err(kernel_error)
16293}
16294
16295fn parse_http_header_collection(
16296 headers: &BTreeMap<String, Value>,
16297 label: &str,
16298) -> Result<HttpHeaderCollection, SidecarError> {
16299 let mut normalized = BTreeMap::<String, Vec<String>>::new();
16300 let mut raw_pairs = Vec::new();
16301
16302 for (raw_name, value) in headers {
16303 let normalized_name = raw_name.to_ascii_lowercase();
16304 let values = match value {
16305 Value::String(text) => vec![text.clone()],
16306 Value::Array(values) => values
16307 .iter()
16308 .map(|entry| {
16309 entry.as_str().map(str::to_owned).ok_or_else(|| {
16310 SidecarError::InvalidState(format!(
16311 "{label} header {raw_name} must contain only strings"
16312 ))
16313 })
16314 })
16315 .collect::<Result<Vec<_>, _>>()?,
16316 other => {
16317 return Err(SidecarError::InvalidState(format!(
16318 "{label} header {raw_name} must be a string or string array, received {other}"
16319 )));
16320 }
16321 };
16322 raw_pairs.extend(
16323 values
16324 .iter()
16325 .cloned()
16326 .map(|entry| (raw_name.clone(), entry)),
16327 );
16328 normalized
16329 .entry(normalized_name)
16330 .or_default()
16331 .extend(values);
16332 }
16333
16334 Ok(HttpHeaderCollection {
16335 normalized,
16336 raw_pairs,
16337 })
16338}
16339
16340fn http_headers_json(headers: &HttpHeaderCollection) -> Value {
16341 let map = headers
16342 .normalized
16343 .iter()
16344 .map(|(name, values)| {
16345 let value = if values.len() == 1 {
16346 Value::String(values[0].clone())
16347 } else {
16348 Value::Array(values.iter().cloned().map(Value::String).collect())
16349 };
16350 (name.clone(), value)
16351 })
16352 .collect::<Map<String, Value>>();
16353 Value::Object(map)
16354}
16355
16356fn http_raw_headers_json(headers: &HttpHeaderCollection) -> Value {
16357 Value::Array(
16358 headers
16359 .raw_pairs
16360 .iter()
16361 .flat_map(|(name, value)| [Value::String(name.clone()), Value::String(value.clone())])
16362 .collect(),
16363 )
16364}
16365
16366fn is_loopback_request_host(host: &str) -> bool {
16367 let bare = host
16368 .strip_prefix('[')
16369 .and_then(|value| value.strip_suffix(']'))
16370 .unwrap_or(host);
16371 matches!(bare, "localhost" | "127.0.0.1" | "::1")
16372}
16373
16374fn serialize_http_loopback_request(
16375 url: &Url,
16376 options: &JavascriptHttpRequestOptions,
16377 headers: &HttpHeaderCollection,
16378) -> Result<String, SidecarError> {
16379 let body_base64 = options
16380 .body
16381 .as_ref()
16382 .map(|body| base64::engine::general_purpose::STANDARD.encode(body.as_bytes()));
16383 serde_json::to_string(&json!({
16384 "method": options.method.clone().unwrap_or_else(|| String::from("GET")),
16385 "url": http_request_target(url),
16386 "headers": http_headers_json(headers),
16387 "rawHeaders": http_raw_headers_json(headers),
16388 "bodyBase64": body_base64,
16389 }))
16390 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
16391}
16392
16393fn http_request_target(url: &Url) -> String {
16394 let path = if url.path().is_empty() {
16395 "/"
16396 } else {
16397 url.path()
16398 };
16399 format!(
16400 "{path}{}",
16401 url.query()
16402 .map(|query| format!("?{query}"))
16403 .unwrap_or_default()
16404 )
16405}
16406
16407fn find_kernel_http_listener_process(vm: &VmState, port: u16) -> Option<String> {
16408 vm.active_processes
16409 .iter()
16410 .find_map(|(process_id, process)| {
16411 process.tcp_listeners.values().find_map(|listener| {
16412 let socket_id = listener.kernel_socket_id?;
16413 let record = vm.kernel.socket_get(socket_id)?;
16414 let local_addr = record
16415 .local_address()
16416 .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
16417 .unwrap_or_else(|| listener.guest_local_addr());
16418 if local_addr.port() == port && is_vm_local_http_listener_addr(local_addr.ip()) {
16419 Some(process_id.to_owned())
16420 } else {
16421 None
16422 }
16423 })
16424 })
16425}
16426
16427fn is_vm_local_http_listener_addr(ip: IpAddr) -> bool {
16428 ip.is_loopback() || ip.is_unspecified()
16429}
16430
16431fn serialize_kernel_http_fetch_request(
16432 port: u16,
16433 path: &str,
16434 options: &JavascriptHttpRequestOptions,
16435 headers: &HttpHeaderCollection,
16436) -> Vec<u8> {
16437 let method = options.method.as_deref().unwrap_or("GET");
16438 let mut lines = vec![format!("{method} {path} HTTP/1.1")];
16439 let mut has_host = false;
16440 let mut has_connection = false;
16441 let mut has_content_length = false;
16442 for (name, values) in &headers.normalized {
16443 match name.as_str() {
16444 "host" => has_host = true,
16445 "connection" => has_connection = true,
16446 "content-length" => has_content_length = true,
16447 _ => {}
16448 }
16449 lines.push(format!("{name}: {}", values.join(", ")));
16450 }
16451 if !has_host {
16452 lines.push(format!("Host: 127.0.0.1:{port}"));
16453 }
16454 if !has_connection {
16455 lines.push(String::from("Connection: close"));
16456 }
16457 let body = options.body.as_deref().unwrap_or("").as_bytes();
16458 if !has_content_length && !body.is_empty() {
16459 lines.push(format!("Content-Length: {}", body.len()));
16460 }
16461 lines.push(String::new());
16462 lines.push(String::new());
16463
16464 let mut request = lines.join("\r\n").into_bytes();
16465 request.extend_from_slice(body);
16466 request
16467}
16468
16469fn parse_kernel_http_fetch_response(
16470 buffer: &[u8],
16471 peer_closed: bool,
16472 url: &str,
16473) -> Result<Option<String>, SidecarError> {
16474 let Some(header_end) = find_http_header_end(buffer) else {
16475 return Ok(None);
16476 };
16477 let header_bytes = &buffer[..header_end];
16478 let head = String::from_utf8_lossy(header_bytes);
16479 let mut lines = head.split("\r\n");
16480 let status_line = lines.next().unwrap_or_default();
16481 let mut status_parts = status_line.splitn(3, ' ');
16482 let version = status_parts.next().unwrap_or_default();
16483 if !version.starts_with("HTTP/") {
16484 return Err(SidecarError::Execution(format!(
16485 "invalid vm.fetch HTTP response status line: {status_line}"
16486 )));
16487 }
16488 let status = status_parts
16489 .next()
16490 .ok_or_else(|| {
16491 SidecarError::Execution(format!(
16492 "invalid vm.fetch HTTP response status line: {status_line}"
16493 ))
16494 })?
16495 .parse::<u16>()
16496 .map_err(|error| {
16497 SidecarError::Execution(format!(
16498 "invalid vm.fetch HTTP response status code in {status_line:?}: {error}"
16499 ))
16500 })?;
16501 let status_text = status_parts.next().unwrap_or_default();
16502 let mut headers = Vec::new();
16503 let mut raw_headers = Vec::new();
16504 let mut content_length = None;
16505 let mut transfer_encoding_values = Vec::new();
16506 for line in lines {
16507 if line.is_empty() {
16508 continue;
16509 }
16510 let Some((name, value)) = line.split_once(':') else {
16511 return Err(SidecarError::Execution(format!(
16512 "invalid vm.fetch HTTP response header line: {line}"
16513 )));
16514 };
16515 let value = value.trim().to_owned();
16516 let normalized = name.to_ascii_lowercase();
16517 if normalized == "content-length" {
16518 content_length = Some(value.parse::<usize>().map_err(|error| {
16519 SidecarError::Execution(format!(
16520 "invalid vm.fetch Content-Length header {value:?}: {error}"
16521 ))
16522 })?);
16523 } else if normalized == "transfer-encoding" {
16524 transfer_encoding_values.push(value.clone());
16525 }
16526 headers.push(json!([normalized, value.clone()]));
16527 raw_headers.push(Value::String(name.to_owned()));
16528 raw_headers.push(Value::String(value));
16529 }
16530
16531 let body_start = header_end + 4;
16532 let transfer_encoding = transfer_encoding_tokens(&transfer_encoding_values);
16533 let is_chunked = transfer_encoding.iter().any(|token| token == "chunked");
16534 let body = if is_chunked {
16535 if content_length.is_some() {
16536 return Err(SidecarError::Execution(String::from(
16537 "vm.fetch HTTP response cannot include both Transfer-Encoding: chunked and Content-Length",
16538 )));
16539 }
16540 if transfer_encoding.len() != 1 {
16541 return Err(SidecarError::Execution(format!(
16542 "unsupported vm.fetch Transfer-Encoding: {}",
16543 transfer_encoding.join(", ")
16544 )));
16545 }
16546 let Some(decoded) = decode_kernel_http_chunked_body(&buffer[body_start..])? else {
16547 return Ok(None);
16548 };
16549 decoded
16550 } else if !transfer_encoding.is_empty() {
16551 return Err(SidecarError::Execution(format!(
16552 "unsupported vm.fetch Transfer-Encoding: {}",
16553 transfer_encoding.join(", ")
16554 )));
16555 } else if let Some(content_length) = content_length {
16556 let body_end = body_start.saturating_add(content_length);
16557 if buffer.len() < body_end {
16558 return Ok(None);
16559 }
16560 buffer[body_start..body_end].to_vec()
16561 } else if peer_closed {
16562 buffer[body_start..].to_vec()
16563 } else {
16564 return Ok(None);
16565 };
16566
16567 serde_json::to_string(&json!({
16568 "status": status,
16569 "statusText": status_text,
16570 "headers": headers,
16571 "rawHeaders": raw_headers,
16572 "body": base64::engine::general_purpose::STANDARD.encode(&body),
16573 "bodyEncoding": "base64",
16574 "url": url,
16575 }))
16576 .map(Some)
16577 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
16578}
16579
16580fn find_http_header_end(buffer: &[u8]) -> Option<usize> {
16581 buffer.windows(4).position(|window| window == b"\r\n\r\n")
16582}
16583
16584fn find_crlf(buffer: &[u8], start: usize) -> Option<usize> {
16585 buffer
16586 .get(start..)?
16587 .windows(2)
16588 .position(|window| window == b"\r\n")
16589 .map(|offset| start + offset)
16590}
16591
16592fn transfer_encoding_tokens(values: &[String]) -> Vec<String> {
16593 values
16594 .iter()
16595 .flat_map(|value| value.split(','))
16596 .map(|token| token.trim().to_ascii_lowercase())
16597 .filter(|token| !token.is_empty())
16598 .collect()
16599}
16600
16601fn decode_kernel_http_chunked_body(buffer: &[u8]) -> Result<Option<Vec<u8>>, SidecarError> {
16602 let mut offset = 0;
16603 let mut body = Vec::new();
16604 loop {
16605 let Some(line_end) = find_crlf(buffer, offset) else {
16606 return Ok(None);
16607 };
16608 let size_line = std::str::from_utf8(&buffer[offset..line_end]).map_err(|error| {
16609 SidecarError::Execution(format!(
16610 "invalid vm.fetch chunk size line encoding: {error}"
16611 ))
16612 })?;
16613 let size_part = size_line.split(';').next().unwrap_or_default();
16614 if size_part.is_empty() || !size_part.bytes().all(|byte| byte.is_ascii_hexdigit()) {
16615 return Err(SidecarError::Execution(format!(
16616 "invalid vm.fetch chunk size line: {size_line:?}"
16617 )));
16618 }
16619 let chunk_size = usize::from_str_radix(size_part, 16).map_err(|error| {
16620 SidecarError::Execution(format!(
16621 "invalid vm.fetch chunk size {size_part:?}: {error}"
16622 ))
16623 })?;
16624 let chunk_start = line_end + 2;
16625 let chunk_end = chunk_start
16626 .checked_add(chunk_size)
16627 .ok_or_else(|| SidecarError::Execution(String::from("vm.fetch chunk size overflow")))?;
16628 if chunk_size > 0 {
16629 let chunk_terminator_end = chunk_end.checked_add(2).ok_or_else(|| {
16630 SidecarError::Execution(String::from("vm.fetch chunk terminator overflow"))
16631 })?;
16632 if chunk_terminator_end > buffer.len() {
16633 return Ok(None);
16634 }
16635 if buffer.get(chunk_end..chunk_terminator_end) != Some(b"\r\n") {
16636 return Err(SidecarError::Execution(String::from(
16637 "invalid vm.fetch chunk terminator",
16638 )));
16639 }
16640 body.extend_from_slice(&buffer[chunk_start..chunk_end]);
16641 offset = chunk_terminator_end;
16642 continue;
16643 }
16644
16645 if buffer.get(chunk_start..chunk_start + 2) == Some(b"\r\n") {
16646 return Ok(Some(body));
16647 }
16648 let Some(trailer_end) = find_http_header_end(&buffer[chunk_start..]) else {
16649 return Ok(None);
16650 };
16651 let trailer_bytes = &buffer[chunk_start..chunk_start + trailer_end];
16652 let trailers = String::from_utf8_lossy(trailer_bytes);
16653 for line in trailers.split("\r\n") {
16654 if line.is_empty() {
16655 continue;
16656 }
16657 if line.starts_with(' ') || line.starts_with('\t') || !line.contains(':') {
16658 return Err(SidecarError::Execution(format!(
16659 "invalid vm.fetch chunk trailer line: {line}"
16660 )));
16661 }
16662 }
16663 return Ok(Some(body));
16664 }
16665}
16666
16667fn kernel_http_fetch_target_exit_code(error: &SidecarError) -> Option<i32> {
16668 let SidecarError::Execution(message) = error else {
16669 return None;
16670 };
16671 message
16672 .strip_prefix("vm.fetch target exited before responding (exit code ")?
16673 .strip_suffix(')')?
16674 .parse()
16675 .ok()
16676}
16677
16678#[allow(clippy::too_many_arguments)]
16679fn service_host_fetch_target_event<B>(
16680 bridge: &SharedBridge<B>,
16681 vm_id: &str,
16682 dns: &VmDnsConfig,
16683 socket_paths: &JavascriptSocketPathContext,
16684 kernel: &mut SidecarKernel,
16685 process: &mut ActiveProcess,
16686 resource_limits: &ResourceLimits,
16687 wait: Duration,
16688) -> Result<bool, SidecarError>
16689where
16690 B: NativeSidecarBridge + Send + 'static,
16691 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16692{
16693 let Some(event) = process
16694 .execution
16695 .poll_event_blocking(wait)
16696 .map_err(|error| SidecarError::Execution(error.to_string()))?
16697 else {
16698 return Ok(false);
16699 };
16700
16701 match event {
16702 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
16703 let network_counts = process.network_resource_counts();
16704 let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
16705 bridge,
16706 vm_id,
16707 dns,
16708 socket_paths,
16709 kernel,
16710 process,
16711 sync_request: &request,
16712 resource_limits,
16713 network_counts,
16714 });
16715 match response {
16716 Ok(result) => process
16717 .execution
16718 .respond_javascript_sync_rpc_success(request.id, result)
16719 .or_else(ignore_stale_javascript_sync_rpc_response)?,
16720 Err(error) => process
16721 .execution
16722 .respond_javascript_sync_rpc_error(
16723 request.id,
16724 javascript_sync_rpc_error_code(&error),
16725 error.to_string(),
16726 )
16727 .or_else(ignore_stale_javascript_sync_rpc_response)?,
16728 }
16729 }
16730 ActiveExecutionEvent::Exited(code) => {
16731 return Err(SidecarError::Execution(format!(
16732 "vm.fetch target exited before responding (exit code {code})"
16733 )));
16734 }
16735 other => {
16736 process.queue_pending_execution_event(other)?;
16737 }
16738 }
16739 Ok(true)
16740}
16741
16742fn drain_host_fetch_target_events<B>(
16743 bridge: &SharedBridge<B>,
16744 vm_id: &str,
16745 vm: &mut VmState,
16746 target_process_id: &str,
16747 socket_paths: &JavascriptSocketPathContext,
16748 resource_limits: &ResourceLimits,
16749) -> Result<(), SidecarError>
16750where
16751 B: NativeSidecarBridge + Send + 'static,
16752 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16753{
16754 for _ in 0..32 {
16755 let dns = vm.dns.clone();
16756 let Some(process) = vm.active_processes.get_mut(target_process_id) else {
16757 break;
16758 };
16759 let serviced = service_host_fetch_target_event(
16760 bridge,
16761 vm_id,
16762 &dns,
16763 socket_paths,
16764 &mut vm.kernel,
16765 process,
16766 resource_limits,
16767 Duration::from_millis(1),
16768 )?;
16769 if !serviced {
16770 break;
16771 }
16772 }
16773 Ok(())
16774}
16775
16776#[allow(clippy::too_many_arguments)]
16777fn dispatch_kernel_http_fetch<B>(
16778 bridge: &SharedBridge<B>,
16779 vm_id: &str,
16780 vm: &mut VmState,
16781 target_process_id: &str,
16782 port: u16,
16783 path: &str,
16784 options: &JavascriptHttpRequestOptions,
16785 headers: &HttpHeaderCollection,
16786 max_fetch_response_bytes: usize,
16787) -> Result<String, SidecarError>
16788where
16789 B: NativeSidecarBridge + Send + 'static,
16790 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16791{
16792 let socket_paths = build_javascript_socket_path_context(vm)?;
16793 let family = JavascriptSocketFamily::Ipv4;
16794 let local_port = allocate_guest_listen_port(
16795 0,
16796 family,
16797 &socket_paths.used_tcp_guest_ports,
16798 socket_paths.listen_policy,
16799 )?;
16800 let resource_limits = vm.kernel.resource_limits().clone();
16801 let network_counts = vm_network_resource_counts(vm);
16802 check_network_resource_limit(
16803 resource_limits.max_sockets,
16804 network_counts.sockets,
16805 2,
16806 "socket",
16807 )?;
16808 check_network_resource_limit(
16809 resource_limits.max_connections,
16810 network_counts.connections,
16811 2,
16812 "connection",
16813 )?;
16814
16815 let kernel_pid = vm
16816 .active_processes
16817 .get(target_process_id)
16818 .ok_or_else(|| {
16819 SidecarError::InvalidState(format!(
16820 "vm.fetch target process disappeared: {target_process_id}"
16821 ))
16822 })?
16823 .kernel_pid;
16824 let socket_id = vm
16825 .kernel
16826 .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, SocketSpec::tcp())
16827 .map_err(kernel_error)?;
16828
16829 let result = dispatch_kernel_http_fetch_with_socket(
16830 bridge,
16831 vm_id,
16832 vm,
16833 target_process_id,
16834 kernel_pid,
16835 socket_id,
16836 local_port,
16837 port,
16838 path,
16839 options,
16840 headers,
16841 &socket_paths,
16842 &resource_limits,
16843 max_fetch_response_bytes,
16844 );
16845 let close_result = vm
16846 .kernel
16847 .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
16848 .map_err(kernel_error);
16849 let cleanup_result = if result.is_err() {
16850 drain_host_fetch_target_events(
16851 bridge,
16852 vm_id,
16853 vm,
16854 target_process_id,
16855 &socket_paths,
16856 &resource_limits,
16857 )
16858 } else {
16859 Ok(())
16860 };
16861 match (result, close_result) {
16862 (Ok(response), Ok(())) => cleanup_result.map(|()| response),
16863 (Err(error), _) => Err(error),
16864 (Ok(_), Err(error)) => Err(error),
16865 }
16866}
16867
16868#[allow(clippy::too_many_arguments)]
16869fn dispatch_kernel_http_fetch_with_socket<B>(
16870 bridge: &SharedBridge<B>,
16871 vm_id: &str,
16872 vm: &mut VmState,
16873 target_process_id: &str,
16874 kernel_pid: u32,
16875 socket_id: SocketId,
16876 local_port: u16,
16877 port: u16,
16878 path: &str,
16879 options: &JavascriptHttpRequestOptions,
16880 headers: &HttpHeaderCollection,
16881 socket_paths: &JavascriptSocketPathContext,
16882 resource_limits: &ResourceLimits,
16883 max_fetch_response_bytes: usize,
16884) -> Result<String, SidecarError>
16885where
16886 B: NativeSidecarBridge + Send + 'static,
16887 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16888{
16889 vm.kernel
16890 .socket_bind_inet(
16891 EXECUTION_DRIVER_NAME,
16892 kernel_pid,
16893 socket_id,
16894 InetSocketAddress::new("127.0.0.1", local_port),
16895 )
16896 .map_err(kernel_error)?;
16897 vm.kernel
16898 .socket_connect_inet_loopback(
16899 EXECUTION_DRIVER_NAME,
16900 kernel_pid,
16901 socket_id,
16902 InetSocketAddress::new("127.0.0.1", port),
16903 )
16904 .map_err(kernel_error)?;
16905
16906 let request_bytes = serialize_kernel_http_fetch_request(port, path, options, headers);
16907 vm.kernel
16908 .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, &request_bytes)
16909 .map_err(kernel_error)?;
16910
16911 let mut response_buffer = Vec::new();
16912 let mut peer_closed = false;
16913 let url = format!("http://127.0.0.1:{port}{path}");
16914 let deadline = Instant::now() + http_loopback_request_timeout();
16915 loop {
16916 if let Some(response) =
16917 parse_kernel_http_fetch_response(&response_buffer, peer_closed, &url)?
16918 {
16919 ensure_vm_fetch_response_within_limit(&response, "vm.fetch", max_fetch_response_bytes)?;
16920 return Ok(response);
16921 }
16922 if Instant::now() >= deadline {
16923 let preview = String::from_utf8_lossy(&response_buffer);
16924 return Err(SidecarError::Execution(format!(
16925 "vm.fetch timed out waiting for kernel TCP HTTP response ({} buffered bytes: {:?})",
16926 response_buffer.len(),
16927 preview.chars().take(200).collect::<String>()
16928 )));
16929 }
16930
16931 {
16932 let dns = vm.dns.clone();
16933 let process = vm
16934 .active_processes
16935 .get_mut(target_process_id)
16936 .ok_or_else(|| {
16937 SidecarError::InvalidState(format!(
16938 "vm.fetch target process disappeared: {target_process_id}"
16939 ))
16940 })?;
16941 service_host_fetch_target_event(
16942 bridge,
16943 vm_id,
16944 &dns,
16945 socket_paths,
16946 &mut vm.kernel,
16947 process,
16948 resource_limits,
16949 Duration::from_millis(5),
16950 )?;
16951 }
16952
16953 let poll = vm
16954 .kernel
16955 .poll_targets(
16956 EXECUTION_DRIVER_NAME,
16957 kernel_pid,
16958 vec![PollTargetEntry::socket(
16959 socket_id,
16960 POLLIN | POLLHUP | POLLERR,
16961 )],
16962 5,
16963 )
16964 .map_err(kernel_error)?;
16965 let revents = poll
16966 .targets
16967 .first()
16968 .map(|entry| entry.revents)
16969 .unwrap_or_else(PollEvents::empty);
16970 if revents.intersects(POLLERR) {
16971 return Err(SidecarError::Execution(String::from(
16972 "vm.fetch kernel TCP socket reported POLLERR",
16973 )));
16974 }
16975 if revents.intersects(POLLIN) {
16976 match vm
16977 .kernel
16978 .socket_read(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, 64 * 1024)
16979 {
16980 Ok(Some(bytes)) if !bytes.is_empty() => {
16981 response_buffer.extend(bytes);
16982 ensure_vm_fetch_raw_response_buffer_within_limit(
16983 response_buffer.len(),
16984 "vm.fetch",
16985 )?;
16986 }
16987 Ok(Some(_)) => {}
16988 Ok(None) => peer_closed = true,
16989 Err(error) if error.code() == "EAGAIN" => {}
16990 Err(error) => return Err(kernel_error(error)),
16991 }
16992 }
16993 if revents.intersects(POLLHUP) {
16994 peer_closed = true;
16995 }
16996 }
16997}
16998
16999fn outbound_http_response_json(url: &Url, response: ureq::Response) -> Result<Value, SidecarError> {
17000 let status = response.status();
17001 let status_text = response.status_text().to_owned();
17002 let mut header_pairs = Vec::new();
17003 let mut raw_headers = Vec::new();
17004 for raw_name in response.headers_names() {
17005 for value in response.all(&raw_name) {
17006 header_pairs.push(json!([raw_name.to_ascii_lowercase(), value]));
17007 raw_headers.push(Value::String(raw_name.clone()));
17008 raw_headers.push(Value::String(value.to_owned()));
17009 }
17010 }
17011 let mut reader = response.into_reader();
17012 let mut body = Vec::new();
17013 reader.read_to_end(&mut body).map_err(|error| {
17014 SidecarError::Execution(format!("failed to read HTTP response: {error}"))
17015 })?;
17016 serde_json::to_string(&json!({
17017 "status": status,
17018 "statusText": status_text,
17019 "headers": header_pairs,
17020 "rawHeaders": raw_headers,
17021 "body": base64::engine::general_purpose::STANDARD.encode(body),
17022 "bodyEncoding": "base64",
17023 "url": url.as_str(),
17024 }))
17025 .map(Value::String)
17026 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17027}
17028
17029fn split_netloc(netloc: &str) -> Option<(&str, u16)> {
17033 let (host, port) = netloc.rsplit_once(':')?;
17034 let port: u16 = port.parse().ok()?;
17035 let host = host
17036 .strip_prefix('[')
17037 .and_then(|rest| rest.strip_suffix(']'))
17038 .unwrap_or(host);
17039 Some((host, port))
17040}
17041
17042fn issue_outbound_http_request(
17043 url: &Url,
17044 options: &JavascriptHttpRequestOptions,
17045 headers: &HttpHeaderCollection,
17046 pinned_addresses: &[IpAddr],
17047) -> Result<Value, SidecarError> {
17048 let method = options.method.as_deref().unwrap_or("GET");
17049 let pinned_host = url.host_str().map(str::to_owned);
17058 let pinned: Vec<IpAddr> = pinned_addresses.to_vec();
17059 let resolver = move |netloc: &str| -> std::io::Result<Vec<SocketAddr>> {
17060 let (host, port) = split_netloc(netloc).ok_or_else(|| {
17061 std::io::Error::new(
17062 std::io::ErrorKind::InvalidInput,
17063 format!("invalid network location: {netloc}"),
17064 )
17065 })?;
17066 let expected_host = pinned_host.as_deref();
17067 if expected_host != Some(host) {
17068 return Err(std::io::Error::new(
17069 std::io::ErrorKind::PermissionDenied,
17070 format!(
17071 "EACCES: outbound HTTP resolver pinned to {expected_host:?}, refusing {host}"
17072 ),
17073 ));
17074 }
17075 if pinned.is_empty() {
17076 return Err(std::io::Error::new(
17077 std::io::ErrorKind::PermissionDenied,
17078 "EACCES: no egress-vetted address available for outbound HTTP request",
17079 ));
17080 }
17081 Ok(pinned.iter().map(|ip| SocketAddr::new(*ip, port)).collect())
17082 };
17083 let mut agent_builder = ureq::AgentBuilder::new()
17084 .resolver(resolver)
17085 .timeout_connect(Duration::from_secs(5))
17086 .timeout_read(Duration::from_secs(15))
17087 .timeout_write(Duration::from_secs(15));
17088 if url.scheme() == "https" {
17089 let tls_options = JavascriptTlsBridgeOptions {
17090 is_server: false,
17091 servername: url.host_str().map(str::to_owned),
17092 alpn_protocols: Some(vec![String::from("http/1.1")]),
17093 reject_unauthorized: options.reject_unauthorized,
17094 ..JavascriptTlsBridgeOptions::default()
17095 };
17096 agent_builder = agent_builder.tls_config(Arc::new(build_client_tls_config(&tls_options)?));
17097 }
17098 let agent = agent_builder.build();
17099 let mut request = agent.request_url(method, url);
17100 for (name, values) in &headers.normalized {
17101 if name == "host" {
17102 continue;
17103 }
17104 let header_value = values.join(", ");
17105 request = request.set(name, &header_value);
17106 }
17107 let response = match options.body.as_deref() {
17108 Some(body) => request.send_string(body),
17109 None => request.call(),
17110 };
17111
17112 match response {
17113 Ok(response) => outbound_http_response_json(url, response),
17114 Err(ureq::Error::Status(_, response)) => outbound_http_response_json(url, response),
17115 Err(ureq::Error::Transport(error)) => Err(SidecarError::Execution(format!(
17116 "ERR_HTTP_REQUEST_FAILED: {error}"
17117 ))),
17118 }
17119}
17120
17121fn wait_for_loopback_http_response<B>(
17122 request: LoopbackHttpResponseWaitRequest<'_, B>,
17123) -> Result<String, SidecarError>
17124where
17125 B: NativeSidecarBridge + Send + 'static,
17126 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17127{
17128 let LoopbackHttpResponseWaitRequest {
17129 bridge,
17130 vm_id,
17131 dns,
17132 socket_paths,
17133 kernel,
17134 process,
17135 resource_limits,
17136 request_key,
17137 } = request;
17138 let deadline = Instant::now() + http_loopback_request_timeout();
17139 loop {
17140 if let Some(response) = process
17141 .pending_http_requests
17142 .get(&request_key)
17143 .and_then(|response| response.clone())
17144 {
17145 process.pending_http_requests.remove(&request_key);
17146 return Ok(response);
17147 }
17148
17149 if Instant::now() >= deadline {
17150 process.pending_http_requests.remove(&request_key);
17151 return Err(SidecarError::Execution(String::from(
17152 "HTTP loopback request timed out waiting for net.http_respond",
17153 )));
17154 }
17155
17156 let Some(event) = process
17157 .execution
17158 .poll_event_blocking(Duration::from_millis(10))
17159 .map_err(|error| SidecarError::Execution(error.to_string()))?
17160 else {
17161 continue;
17162 };
17163
17164 match event {
17165 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
17166 let network_counts = process.network_resource_counts();
17167 let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
17168 bridge,
17169 vm_id,
17170 dns,
17171 socket_paths,
17172 kernel,
17173 process,
17174 sync_request: &request,
17175 resource_limits,
17176 network_counts,
17177 });
17178 match response {
17179 Ok(result) => process
17180 .execution
17181 .respond_javascript_sync_rpc_success(request.id, result)
17182 .or_else(ignore_stale_javascript_sync_rpc_response)?,
17183 Err(error) => process
17184 .execution
17185 .respond_javascript_sync_rpc_error(
17186 request.id,
17187 javascript_sync_rpc_error_code(&error),
17188 error.to_string(),
17189 )
17190 .or_else(ignore_stale_javascript_sync_rpc_response)?,
17191 }
17192 }
17193 ActiveExecutionEvent::Exited(code) => {
17194 process.pending_http_requests.remove(&request_key);
17195 return Err(SidecarError::Execution(format!(
17196 "HTTP loopback server exited before responding (exit code {code})"
17197 )));
17198 }
17199 ActiveExecutionEvent::Stdout(_)
17200 | ActiveExecutionEvent::Stderr(_)
17201 | ActiveExecutionEvent::PythonVfsRpcRequest(_)
17202 | ActiveExecutionEvent::SignalState { .. } => {}
17203 }
17204 }
17205}
17206
17207pub(crate) fn dispatch_loopback_http_request<B>(
17208 request: LoopbackHttpDispatchRequest<'_, B>,
17209) -> Result<String, SidecarError>
17210where
17211 B: NativeSidecarBridge + Send + 'static,
17212 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17213{
17214 let LoopbackHttpDispatchRequest {
17215 bridge,
17216 vm_id,
17217 dns,
17218 socket_paths,
17219 kernel,
17220 process,
17221 resource_limits,
17222 server_id,
17223 request_json,
17224 } = request;
17225 let request_id = {
17226 let server = process.http_servers.get_mut(&server_id).ok_or_else(|| {
17227 SidecarError::InvalidState(format!("HTTP target server disappeared: {server_id}"))
17228 })?;
17229 server.next_request_id += 1;
17230 server.next_request_id
17231 };
17232 process
17233 .pending_http_requests
17234 .insert((server_id, request_id), None);
17235 process.execution.send_javascript_stream_event(
17236 "http_request",
17237 json!({
17238 "serverId": server_id,
17239 "requestId": request_id,
17240 "request": request_json,
17241 }),
17242 )?;
17243 wait_for_loopback_http_response(LoopbackHttpResponseWaitRequest {
17244 bridge,
17245 vm_id,
17246 dns,
17247 socket_paths,
17248 kernel,
17249 process,
17250 resource_limits,
17251 request_key: (server_id, request_id),
17252 })
17253}
17254
17255fn ensure_vm_fetch_response_within_limit(
17256 response_json: &str,
17257 operation: &str,
17258 limit: usize,
17259) -> Result<(), SidecarError> {
17260 let size = response_json.len();
17261 if size > limit {
17262 return Err(SidecarError::Execution(format!(
17263 "{operation} payload is {size} bytes, limit is {limit}"
17264 )));
17265 }
17266 Ok(())
17267}
17268
17269fn ensure_vm_fetch_raw_response_buffer_within_limit(
17270 size: usize,
17271 operation: &str,
17272) -> Result<(), SidecarError> {
17273 if size > VM_FETCH_BUFFER_LIMIT_BYTES {
17274 return Err(SidecarError::Execution(format!(
17275 "{operation} raw response buffer is {size} bytes, limit is {VM_FETCH_BUFFER_LIMIT_BYTES}"
17276 )));
17277 }
17278 Ok(())
17279}
17280
17281pub(crate) fn ensure_vm_fetch_response_frame_within_limit(
17282 response: &ResponseFrame,
17283 max_frame_bytes: usize,
17284) -> Result<(), SidecarError> {
17285 let max_frame_bytes = max_frame_bytes.min(VM_FETCH_BUFFER_LIMIT_BYTES);
17286 let frame = crate::protocol::to_generated_protocol_frame(
17287 &crate::protocol::ProtocolFrame::Response(response.clone()),
17288 )
17289 .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))?;
17290 let WireProtocolFrame::ResponseFrame(_) = &frame else {
17291 return Err(SidecarError::FrameTooLarge(String::from(
17292 "vm fetch response converted to non-response wire frame",
17293 )));
17294 };
17295 WireFrameCodec::new(max_frame_bytes)
17296 .encode(&frame)
17297 .map(|_| ())
17298 .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))
17299}
17300
17301fn service_javascript_dns_sync_rpc<B>(
17302 bridge: &SharedBridge<B>,
17303 kernel: &SidecarKernel,
17304 vm_id: &str,
17305 dns: &VmDnsConfig,
17306 request: &JavascriptSyncRpcRequest,
17307) -> Result<Value, SidecarError>
17308where
17309 B: NativeSidecarBridge + Send + 'static,
17310 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17311{
17312 match request.method.as_str() {
17313 "dns.lookup" => {
17314 let payload = request
17315 .args
17316 .first()
17317 .cloned()
17318 .ok_or_else(|| {
17319 SidecarError::InvalidState(String::from(
17320 "dns.lookup requires a request payload",
17321 ))
17322 })
17323 .and_then(|value| {
17324 serde_json::from_value::<JavascriptDnsLookupRequest>(value).map_err(|error| {
17325 SidecarError::InvalidState(format!("invalid dns.lookup payload: {error}"))
17326 })
17327 })?;
17328 let addresses = filter_dns_ip_addrs(
17329 resolve_dns_ip_addrs(
17330 bridge,
17331 kernel,
17332 vm_id,
17333 dns,
17334 &payload.hostname,
17335 DnsLookupPolicy::CheckPermissions,
17336 )?,
17337 payload.family,
17338 )?;
17339 let addresses = filter_dns_safe_ip_addrs(addresses, &payload.hostname)?;
17340 Ok(Value::Array(
17341 addresses
17342 .into_iter()
17343 .map(|ip| {
17344 json!({
17345 "address": ip.to_string(),
17346 "family": if ip.is_ipv6() { 6 } else { 4 },
17347 })
17348 })
17349 .collect(),
17350 ))
17351 }
17352 "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
17353 let payload = request
17354 .args
17355 .first()
17356 .cloned()
17357 .ok_or_else(|| {
17358 SidecarError::InvalidState(String::from(
17359 "dns.resolve requires a request payload",
17360 ))
17361 })
17362 .and_then(|value| {
17363 serde_json::from_value::<JavascriptDnsResolveRequest>(value).map_err(|error| {
17364 SidecarError::InvalidState(format!("invalid dns.resolve payload: {error}"))
17365 })
17366 })?;
17367 let requested_type = match request.method.as_str() {
17368 "dns.resolve4" => String::from("A"),
17369 "dns.resolve6" => String::from("AAAA"),
17370 _ => payload
17371 .rrtype
17372 .as_deref()
17373 .unwrap_or("A")
17374 .to_ascii_uppercase(),
17375 };
17376 let record_type = parse_dns_record_type(&requested_type)?;
17377 let resolution = resolve_dns_records(
17378 bridge,
17379 kernel,
17380 vm_id,
17381 dns,
17382 &payload.hostname,
17383 record_type,
17384 DnsLookupPolicy::CheckPermissions,
17385 )?;
17386 dns_resolution_to_node_value(&resolution, &requested_type)
17387 }
17388 other => Err(SidecarError::InvalidState(format!(
17389 "unsupported JavaScript dns sync RPC method {other}"
17390 ))),
17391 }
17392}
17393
17394fn service_javascript_dgram_sync_rpc<B>(
17395 request: JavascriptDgramSyncRpcServiceRequest<'_, B>,
17396) -> Result<Value, SidecarError>
17397where
17398 B: NativeSidecarBridge + Send + 'static,
17399 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17400{
17401 let JavascriptDgramSyncRpcServiceRequest {
17402 bridge,
17403 kernel,
17404 vm_id,
17405 dns,
17406 socket_paths,
17407 process,
17408 sync_request: request,
17409 resource_limits,
17410 network_counts,
17411 } = request;
17412 match request.method.as_str() {
17413 "dgram.createSocket" => {
17414 check_network_resource_limit(
17415 resource_limits.max_sockets,
17416 network_counts.sockets,
17417 1,
17418 "socket",
17419 )?;
17420 let payload = request
17421 .args
17422 .first()
17423 .cloned()
17424 .ok_or_else(|| {
17425 SidecarError::InvalidState(String::from(
17426 "dgram.createSocket requires a request payload",
17427 ))
17428 })
17429 .and_then(|value| {
17430 serde_json::from_value::<JavascriptDgramCreateSocketRequest>(value).map_err(
17431 |error| {
17432 SidecarError::InvalidState(format!(
17433 "invalid dgram.createSocket payload: {error}"
17434 ))
17435 },
17436 )
17437 })?;
17438 let family = JavascriptUdpFamily::from_socket_type(&payload.socket_type)?;
17439 let socket_id = process.allocate_udp_socket_id();
17440 process.udp_sockets.insert(
17441 socket_id.clone(),
17442 ActiveUdpSocket::new(kernel, process.kernel_pid, family)?,
17443 );
17444 Ok(json!({
17445 "socketId": socket_id,
17446 "type": family.socket_type(),
17447 }))
17448 }
17449 "dgram.bind" => {
17450 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.bind socket id")?;
17451 let payload = request
17452 .args
17453 .get(1)
17454 .cloned()
17455 .ok_or_else(|| {
17456 SidecarError::InvalidState(String::from(
17457 "dgram.bind requires a request payload",
17458 ))
17459 })
17460 .and_then(|value| {
17461 serde_json::from_value::<JavascriptDgramBindRequest>(value).map_err(|error| {
17462 SidecarError::InvalidState(format!("invalid dgram.bind payload: {error}"))
17463 })
17464 })?;
17465 let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17466 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17467 })?;
17468 let local_addr = socket.bind(
17469 kernel,
17470 process.kernel_pid,
17471 payload.address.as_deref(),
17472 payload.port,
17473 socket_paths,
17474 )?;
17475 Ok(json!({
17476 "localAddress": local_addr.ip().to_string(),
17477 "localPort": local_addr.port(),
17478 "family": socket_addr_family(&local_addr),
17479 }))
17480 }
17481 "dgram.send" => {
17482 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.send socket id")?;
17483 let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "dgram.send payload")?;
17484 let payload = request
17485 .args
17486 .get(2)
17487 .cloned()
17488 .ok_or_else(|| {
17489 SidecarError::InvalidState(String::from(
17490 "dgram.send requires a request payload",
17491 ))
17492 })
17493 .and_then(|value| {
17494 serde_json::from_value::<JavascriptDgramSendRequest>(value).map_err(|error| {
17495 SidecarError::InvalidState(format!("invalid dgram.send payload: {error}"))
17496 })
17497 })?;
17498 let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17499 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17500 })?;
17501 let (written, local_addr) = socket.send_to(ActiveUdpSendToRequest {
17502 bridge,
17503 kernel,
17504 kernel_pid: process.kernel_pid,
17505 vm_id,
17506 dns,
17507 host: payload.address.as_deref().unwrap_or("localhost"),
17508 port: payload.port,
17509 context: socket_paths,
17510 contents: &chunk,
17511 })?;
17512 Ok(json!({
17513 "bytes": written,
17514 "localAddress": local_addr.ip().to_string(),
17515 "localPort": local_addr.port(),
17516 "family": socket_addr_family(&local_addr),
17517 }))
17518 }
17519 "dgram.poll" => {
17520 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.poll socket id")?;
17521 let wait_ms =
17522 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "dgram.poll wait ms")?
17523 .unwrap_or_default();
17524 let event = {
17525 let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17526 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17527 })?;
17528 socket.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?
17529 };
17530
17531 match event {
17532 Some(JavascriptUdpSocketEvent::Message { data, remote_addr }) => {
17533 let family = JavascriptSocketFamily::from_ip(remote_addr.ip());
17534 let guest_remote_port = if is_loopback_ip(remote_addr.ip()) {
17535 socket_paths
17536 .guest_udp_port_for_host_port(family, remote_addr.port())
17537 .unwrap_or(remote_addr.port())
17538 } else {
17539 remote_addr.port()
17540 };
17541 Ok(json!({
17542 "type": "message",
17543 "data": javascript_sync_rpc_bytes_value(&data),
17544 "remoteAddress": remote_addr.ip().to_string(),
17545 "remotePort": guest_remote_port,
17546 "remoteFamily": socket_addr_family(&remote_addr),
17547 }))
17548 }
17549 Some(JavascriptUdpSocketEvent::Error { code, message }) => Ok(json!({
17550 "type": "error",
17551 "code": code,
17552 "message": message,
17553 })),
17554 None => Ok(Value::Null),
17555 }
17556 }
17557 "dgram.close" => {
17558 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.close socket id")?;
17559 let mut socket = process.udp_sockets.remove(socket_id).ok_or_else(|| {
17560 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17561 })?;
17562 socket.close(kernel, process.kernel_pid);
17563 Ok(Value::Null)
17564 }
17565 "dgram.address" => {
17566 let socket_id =
17567 javascript_sync_rpc_arg_str(&request.args, 0, "dgram.address socket id")?;
17568 let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17569 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17570 })?;
17571 let local_addr = socket.local_addr().ok_or_else(|| {
17572 SidecarError::Execution(String::from("EBADF: bad file descriptor"))
17573 })?;
17574 javascript_net_json_string(
17575 json!({
17576 "address": local_addr.ip().to_string(),
17577 "port": local_addr.port(),
17578 "family": socket_addr_family(&local_addr),
17579 }),
17580 "dgram.address",
17581 )
17582 }
17583 "dgram.setBufferSize" => {
17584 let socket_id =
17585 javascript_sync_rpc_arg_str(&request.args, 0, "dgram.setBufferSize socket id")?;
17586 let which =
17587 javascript_sync_rpc_arg_str(&request.args, 1, "dgram.setBufferSize buffer kind")?;
17588 let size = javascript_sync_rpc_arg_u64(&request.args, 2, "dgram.setBufferSize size")?;
17589 let size = usize::try_from(size).map_err(|_| {
17590 SidecarError::InvalidState(String::from(
17591 "dgram.setBufferSize size must fit within usize",
17592 ))
17593 })?;
17594 let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17595 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17596 })?;
17597 socket.set_buffer_size(which, size)?;
17598 Ok(Value::Null)
17599 }
17600 "dgram.getBufferSize" => {
17601 let socket_id =
17602 javascript_sync_rpc_arg_str(&request.args, 0, "dgram.getBufferSize socket id")?;
17603 let which =
17604 javascript_sync_rpc_arg_str(&request.args, 1, "dgram.getBufferSize buffer kind")?;
17605 let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17606 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17607 })?;
17608 let size = socket.get_buffer_size(which)?;
17609 Ok(json!(size))
17610 }
17611 other => Err(SidecarError::InvalidState(format!(
17612 "unsupported JavaScript dgram sync RPC method {other}"
17613 ))),
17614 }
17615}
17616
17617#[derive(Debug)]
17618struct ClientHttp2StreamState {
17619 send_stream: Option<h2::SendStream<Bytes>>,
17620}
17621
17622#[derive(Debug)]
17623struct ServerHttp2StreamState {
17624 send_response: Option<ServerHttp2Responder>,
17625 send_stream: Option<h2::SendStream<Bytes>>,
17626}
17627
17628#[derive(Debug)]
17629enum ServerHttp2Responder {
17630 Regular(server::SendResponse<Bytes>),
17631 Pushed(server::SendPushedResponse<Bytes>),
17632}
17633
17634const HTTP2_DEFAULT_WINDOW_SIZE: u32 = 65_535;
17635const HTTP2_POLL_DELAY: Duration = Duration::from_millis(10);
17636
17637fn http2_runtime_snapshot() -> Http2RuntimeSnapshot {
17638 Http2RuntimeSnapshot {
17639 effective_local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17640 local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17641 remote_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17642 next_stream_id: 1,
17643 outbound_queue_size: 1,
17644 deflate_dynamic_table_size: 0,
17645 inflate_dynamic_table_size: 0,
17646 }
17647}
17648
17649fn http2_snapshot_json(snapshot: &Http2SessionSnapshot) -> Result<String, SidecarError> {
17650 serde_json::to_string(snapshot)
17651 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17652}
17653
17654fn http2_event_value(event: &Http2BridgeEvent) -> Result<Value, SidecarError> {
17655 serde_json::to_string(event)
17656 .map(Value::String)
17657 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17658}
17659
17660fn push_http2_server_event(
17661 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17662 server_id: u64,
17663 event: Http2BridgeEvent,
17664) {
17665 if let Ok(mut state) = shared.lock() {
17666 state
17667 .server_events
17668 .entry(server_id)
17669 .or_default()
17670 .push_back(event);
17671 }
17672}
17673
17674fn push_http2_session_event(
17675 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17676 session_id: u64,
17677 event: Http2BridgeEvent,
17678) {
17679 if let Ok(mut state) = shared.lock() {
17680 state
17681 .session_events
17682 .entry(session_id)
17683 .or_default()
17684 .push_back(event);
17685 }
17686}
17687
17688fn pop_http2_event(
17689 queue: &mut BTreeMap<u64, VecDeque<Http2BridgeEvent>>,
17690 id: u64,
17691) -> Option<Http2BridgeEvent> {
17692 queue.get_mut(&id).and_then(VecDeque::pop_front)
17693}
17694
17695fn wait_for_http2_event(
17696 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17697 id: u64,
17698 is_server: bool,
17699 wait_ms: u64,
17700) -> Option<Http2BridgeEvent> {
17701 let deadline = Instant::now() + Duration::from_millis(wait_ms);
17702 loop {
17703 if let Ok(mut state) = shared.lock() {
17704 let queue = if is_server {
17705 &mut state.server_events
17706 } else {
17707 &mut state.session_events
17708 };
17709 if let Some(event) = pop_http2_event(queue, id) {
17710 return Some(event);
17711 }
17712 }
17713 if wait_ms == 0 || Instant::now() >= deadline {
17714 return None;
17715 }
17716 thread::sleep(HTTP2_POLL_DELAY);
17717 }
17718}
17719
17720fn next_http2_session_id(shared: &mut crate::state::Http2SharedState) -> u64 {
17721 shared.next_session_id += 1;
17722 shared.next_session_id
17723}
17724
17725fn next_http2_stream_id(shared: &mut crate::state::Http2SharedState) -> u64 {
17726 shared.next_stream_id += 1;
17727 shared.next_stream_id
17728}
17729
17730fn http2_reason(code: Option<u32>) -> Reason {
17731 code.unwrap_or(Reason::NO_ERROR.into()).into()
17732}
17733
17734fn http2_error_payload(message: impl Into<String>) -> String {
17735 serde_json::to_string(&json!({
17736 "name": "Error",
17737 "code": "ERR_HTTP2_ERROR",
17738 "message": message.into(),
17739 }))
17740 .unwrap_or_else(|_| {
17741 String::from(
17742 "{\"name\":\"Error\",\"code\":\"ERR_HTTP2_ERROR\",\"message\":\"HTTP/2 bridge error\"}",
17743 )
17744 })
17745}
17746
17747fn http2_socket_snapshot(local_addr: SocketAddr, remote_addr: SocketAddr) -> Http2SocketSnapshot {
17748 Http2SocketSnapshot {
17749 encrypted: false,
17750 allow_half_open: false,
17751 local_address: Some(local_addr.ip().to_string()),
17752 local_port: Some(local_addr.port()),
17753 local_family: Some(socket_addr_family(&local_addr).to_string()),
17754 remote_address: Some(remote_addr.ip().to_string()),
17755 remote_port: Some(remote_addr.port()),
17756 remote_family: Some(socket_addr_family(&remote_addr).to_string()),
17757 servername: None,
17758 alpn_protocol: Some(String::from("h2c")),
17759 }
17760}
17761
17762fn http2_wait_result(kind: &str, id: u64) -> Value {
17763 json!({
17764 "kind": kind,
17765 "id": id,
17766 })
17767}
17768
17769fn is_http2_terminal_event(event: &Http2BridgeEvent, is_server: bool, id: u64) -> bool {
17770 if is_server {
17771 event.kind == "serverClose" && event.id == id
17772 } else {
17773 event.kind == "sessionClose" && event.id == id
17774 }
17775}
17776
17777fn dispatch_http2_wait_loop(
17778 process: &ActiveProcess,
17779 id: u64,
17780 is_server: bool,
17781) -> Result<Value, SidecarError> {
17782 loop {
17783 if let Some(event) = wait_for_http2_event(&process.http2.shared, id, is_server, 50) {
17784 let payload = serde_json::to_value(&event).map_err(|error| {
17785 SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}"))
17786 })?;
17787 process
17788 .execution
17789 .send_javascript_stream_event("http2", payload.clone())?;
17790 if is_http2_terminal_event(&event, is_server, id) {
17791 return Ok(payload);
17792 }
17793 continue;
17794 }
17795
17796 let exists = process
17797 .http2
17798 .shared
17799 .lock()
17800 .map(|state| {
17801 if is_server {
17802 state.servers.contains_key(&id)
17803 } else {
17804 state.sessions.contains_key(&id)
17805 }
17806 })
17807 .unwrap_or(false);
17808 if !exists {
17809 return Ok(if is_server {
17810 http2_wait_result("serverClose", id)
17811 } else {
17812 http2_wait_result("sessionClose", id)
17813 });
17814 }
17815 }
17816}
17817
17818fn dispatch_http_wait_loop(process: &ActiveProcess, server_id: u64) -> Result<Value, SidecarError> {
17819 loop {
17820 if !process.http_servers.contains_key(&server_id) {
17821 return Ok(json!({
17822 "kind": "serverClose",
17823 "id": server_id,
17824 }));
17825 }
17826 thread::sleep(Duration::from_millis(25));
17827 }
17828}
17829
17830fn http2_settings_from_value(settings: &BTreeMap<String, Value>) -> BTreeMap<String, Value> {
17831 settings.clone()
17832}
17833
17834fn parse_http2_headers_json(
17835 headers_json: &str,
17836 label: &str,
17837) -> Result<BTreeMap<String, Value>, SidecarError> {
17838 serde_json::from_str::<BTreeMap<String, Value>>(headers_json)
17839 .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
17840}
17841
17842fn apply_http2_header_values(
17843 header_map: &mut HeaderMap,
17844 name: &str,
17845 value: &Value,
17846) -> Result<(), SidecarError> {
17847 let header_name = HeaderName::from_bytes(name.as_bytes()).map_err(|error| {
17848 SidecarError::InvalidState(format!("invalid HTTP/2 header name {name:?}: {error}"))
17849 })?;
17850 match value {
17851 Value::Array(values) => {
17852 for value in values {
17853 apply_http2_header_values(header_map, name, value)?;
17854 }
17855 }
17856 Value::String(text) => {
17857 let value = HeaderValue::from_str(text).map_err(|error| {
17858 SidecarError::InvalidState(format!(
17859 "invalid HTTP/2 header value for {name}: {error}"
17860 ))
17861 })?;
17862 header_map.append(header_name.clone(), value);
17863 }
17864 Value::Number(number) => {
17865 let value = HeaderValue::from_str(&number.to_string()).map_err(|error| {
17866 SidecarError::InvalidState(format!(
17867 "invalid HTTP/2 numeric header value for {name}: {error}"
17868 ))
17869 })?;
17870 header_map.append(header_name.clone(), value);
17871 }
17872 Value::Bool(boolean) => {
17873 let value = HeaderValue::from_str(if *boolean { "true" } else { "false" }).map_err(
17874 |error| {
17875 SidecarError::InvalidState(format!(
17876 "invalid HTTP/2 boolean header value for {name}: {error}"
17877 ))
17878 },
17879 )?;
17880 header_map.append(header_name.clone(), value);
17881 }
17882 Value::Null => {}
17883 Value::Object(_) => {
17884 return Err(SidecarError::InvalidState(format!(
17885 "unsupported HTTP/2 header object value for {name}"
17886 )));
17887 }
17888 }
17889 Ok(())
17890}
17891
17892fn build_http2_request(headers_json: &str) -> Result<Request<()>, SidecarError> {
17893 let headers = parse_http2_headers_json(headers_json, "HTTP/2 request headers")?;
17894 let method = headers
17895 .get(":method")
17896 .and_then(Value::as_str)
17897 .unwrap_or("GET");
17898 let path = headers.get(":path").and_then(Value::as_str).unwrap_or("/");
17899 let mut builder = Request::builder()
17900 .method(Method::from_bytes(method.as_bytes()).map_err(|error| {
17901 SidecarError::InvalidState(format!("invalid HTTP/2 method {method:?}: {error}"))
17902 })?)
17903 .uri(path.parse::<Uri>().map_err(|error| {
17904 SidecarError::InvalidState(format!("invalid HTTP/2 path {path:?}: {error}"))
17905 })?);
17906 {
17907 let header_map = builder.headers_mut().expect("request header map");
17908 for (name, value) in &headers {
17909 if name.starts_with(':') {
17910 continue;
17911 }
17912 apply_http2_header_values(header_map, name, value)?;
17913 }
17914 }
17915 builder
17916 .body(())
17917 .map_err(|error| SidecarError::InvalidState(format!("invalid HTTP/2 request: {error}")))
17918}
17919
17920fn build_http2_response(headers_json: &str) -> Result<Response<()>, SidecarError> {
17921 let headers = parse_http2_headers_json(headers_json, "HTTP/2 response headers")?;
17922 let status = headers
17923 .get(":status")
17924 .and_then(Value::as_u64)
17925 .or_else(|| {
17926 headers
17927 .get(":status")
17928 .and_then(Value::as_str)
17929 .and_then(|value| value.parse::<u16>().ok().map(u64::from))
17930 })
17931 .unwrap_or(200);
17932 let mut builder = Response::builder().status(status as u16);
17933 {
17934 let header_map = builder.headers_mut().expect("response header map");
17935 for (name, value) in &headers {
17936 if name.starts_with(':') {
17937 continue;
17938 }
17939 apply_http2_header_values(header_map, name, value)?;
17940 }
17941 }
17942 builder.body(()).map_err(|error| {
17943 SidecarError::InvalidState(format!("invalid HTTP/2 response headers: {error}"))
17944 })
17945}
17946
17947fn serialize_http2_headers_map(
17948 pseudo: BTreeMap<String, Value>,
17949 headers: &HeaderMap,
17950) -> Result<String, SidecarError> {
17951 let mut serialized = pseudo;
17952 for (name, value) in headers {
17953 let name = name.as_str().to_string();
17954 let value = Value::String(
17955 value
17956 .to_str()
17957 .map_err(|error| {
17958 SidecarError::Execution(format!("invalid HTTP/2 header value: {error}"))
17959 })?
17960 .to_owned(),
17961 );
17962 match serialized.get_mut(&name) {
17963 Some(Value::Array(values)) => values.push(value),
17964 Some(existing) => {
17965 let first = existing.clone();
17966 *existing = Value::Array(vec![first, value]);
17967 }
17968 None => {
17969 serialized.insert(name, value);
17970 }
17971 }
17972 }
17973 serde_json::to_string(&serialized)
17974 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17975}
17976
17977fn serialize_http2_request_headers(
17978 request: &Request<h2::RecvStream>,
17979) -> Result<String, SidecarError> {
17980 let mut pseudo = BTreeMap::new();
17981 pseudo.insert(
17982 String::from(":method"),
17983 Value::String(request.method().as_str().to_string()),
17984 );
17985 pseudo.insert(
17986 String::from(":path"),
17987 Value::String(
17988 request
17989 .uri()
17990 .path_and_query()
17991 .map(|value| value.as_str().to_string())
17992 .unwrap_or_else(|| String::from("/")),
17993 ),
17994 );
17995 serialize_http2_headers_map(pseudo, request.headers())
17996}
17997
17998fn serialize_http2_response_headers(
17999 response: &Response<h2::RecvStream>,
18000) -> Result<String, SidecarError> {
18001 let mut pseudo = BTreeMap::new();
18002 pseudo.insert(
18003 String::from(":status"),
18004 Value::Number(serde_json::Number::from(response.status().as_u16())),
18005 );
18006 serialize_http2_headers_map(pseudo, response.headers())
18007}
18008
18009fn remove_http2_session_resources(
18010 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
18011 session_id: u64,
18012) {
18013 if let Ok(mut state) = shared.lock() {
18014 state.sessions.remove(&session_id);
18015 state.session_events.remove(&session_id);
18016 let stream_ids = state
18017 .streams
18018 .iter()
18019 .filter_map(|(stream_id, stream)| {
18020 (stream.session_id == session_id).then_some(*stream_id)
18021 })
18022 .collect::<Vec<_>>();
18023 for stream_id in stream_ids {
18024 state.streams.remove(&stream_id);
18025 }
18026 }
18027}
18028
18029fn spawn_http2_client_session(
18030 shared: Arc<Mutex<crate::state::Http2SharedState>>,
18031 session_id: u64,
18032 remote_addr: SocketAddr,
18033 tls: Option<JavascriptTlsBridgeOptions>,
18034 snapshot: Arc<Mutex<Http2SessionSnapshot>>,
18035 mut command_rx: UnboundedReceiver<Http2SessionCommand>,
18036) {
18037 thread::spawn(move || {
18038 let runtime = match TokioRuntimeBuilder::new_current_thread()
18039 .enable_all()
18040 .build()
18041 {
18042 Ok(runtime) => runtime,
18043 Err(error) => {
18044 push_http2_session_event(
18045 &shared,
18046 session_id,
18047 Http2BridgeEvent {
18048 kind: String::from("sessionError"),
18049 id: session_id,
18050 data: Some(http2_error_payload(error.to_string())),
18051 ..Http2BridgeEvent::default()
18052 },
18053 );
18054 remove_http2_session_resources(&shared, session_id);
18055 return;
18056 }
18057 };
18058
18059 runtime.block_on(async move {
18060 let stream = match tokio::net::TcpStream::connect(remote_addr).await {
18061 Ok(stream) => stream,
18062 Err(error) => {
18063 push_http2_session_event(
18064 &shared,
18065 session_id,
18066 Http2BridgeEvent {
18067 kind: String::from("sessionError"),
18068 id: session_id,
18069 data: Some(http2_error_payload(error.to_string())),
18070 ..Http2BridgeEvent::default()
18071 },
18072 );
18073 remove_http2_session_resources(&shared, session_id);
18074 return;
18075 }
18076 };
18077
18078 let local_addr = match stream.local_addr() {
18079 Ok(addr) => addr,
18080 Err(error) => {
18081 push_http2_session_event(
18082 &shared,
18083 session_id,
18084 Http2BridgeEvent {
18085 kind: String::from("sessionError"),
18086 id: session_id,
18087 data: Some(http2_error_payload(error.to_string())),
18088 ..Http2BridgeEvent::default()
18089 },
18090 );
18091 remove_http2_session_resources(&shared, session_id);
18092 return;
18093 }
18094 };
18095
18096 {
18097 let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
18098 snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
18099 if let Some(options) = tls.as_ref() {
18100 snapshot_guard.encrypted = true;
18101 snapshot_guard.alpn_protocol = Some(String::from("h2"));
18102 snapshot_guard.socket.encrypted = true;
18103 snapshot_guard.socket.servername = options.servername.clone();
18104 snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
18105 }
18106 snapshot_guard.state = http2_runtime_snapshot();
18107 }
18108 if let Ok(snapshot_json) =
18109 http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
18110 {
18111 push_http2_session_event(
18112 &shared,
18113 session_id,
18114 Http2BridgeEvent {
18115 kind: String::from("sessionConnect"),
18116 id: session_id,
18117 data: Some(snapshot_json),
18118 ..Http2BridgeEvent::default()
18119 },
18120 );
18121 }
18122
18123 let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
18124 let server_name = match ServerName::try_from(
18125 options
18126 .servername
18127 .clone()
18128 .unwrap_or_else(|| String::from("localhost")),
18129 ) {
18130 Ok(server_name) => server_name,
18131 Err(_) => {
18132 push_http2_session_event(
18133 &shared,
18134 session_id,
18135 Http2BridgeEvent {
18136 kind: String::from("sessionError"),
18137 id: session_id,
18138 data: Some(http2_error_payload("invalid TLS servername")),
18139 ..Http2BridgeEvent::default()
18140 },
18141 );
18142 remove_http2_session_resources(&shared, session_id);
18143 return;
18144 }
18145 };
18146 let connector = match build_client_tls_config(options) {
18147 Ok(config) => TlsConnector::from(Arc::new(config)),
18148 Err(error) => {
18149 push_http2_session_event(
18150 &shared,
18151 session_id,
18152 Http2BridgeEvent {
18153 kind: String::from("sessionError"),
18154 id: session_id,
18155 data: Some(http2_error_payload(error.to_string())),
18156 ..Http2BridgeEvent::default()
18157 },
18158 );
18159 remove_http2_session_resources(&shared, session_id);
18160 return;
18161 }
18162 };
18163 match connector.connect(server_name, stream).await {
18164 Ok(tls_stream) => Box::pin(tls_stream),
18165 Err(error) => {
18166 push_http2_session_event(
18167 &shared,
18168 session_id,
18169 Http2BridgeEvent {
18170 kind: String::from("sessionError"),
18171 id: session_id,
18172 data: Some(http2_error_payload(error.to_string())),
18173 ..Http2BridgeEvent::default()
18174 },
18175 );
18176 remove_http2_session_resources(&shared, session_id);
18177 return;
18178 }
18179 }
18180 } else {
18181 Box::pin(stream)
18182 };
18183
18184 let (mut sender, connection) = match client::handshake(io).await {
18185 Ok(parts) => parts,
18186 Err(error) => {
18187 push_http2_session_event(
18188 &shared,
18189 session_id,
18190 Http2BridgeEvent {
18191 kind: String::from("sessionError"),
18192 id: session_id,
18193 data: Some(http2_error_payload(error.to_string())),
18194 ..Http2BridgeEvent::default()
18195 },
18196 );
18197 remove_http2_session_resources(&shared, session_id);
18198 return;
18199 }
18200 };
18201
18202 let (status_tx, mut status_rx) = unbounded_channel::<Result<(), String>>();
18203 tokio::spawn(async move {
18204 let _ = status_tx.send(connection.await.map_err(|error| error.to_string()));
18205 });
18206
18207 let streams: Arc<Mutex<BTreeMap<u64, ClientHttp2StreamState>>> =
18208 Arc::new(Mutex::new(BTreeMap::new()));
18209
18210 loop {
18211 tokio::select! {
18212 Some(result) = status_rx.recv() => {
18213 if let Err(message) = result {
18214 push_http2_session_event(
18215 &shared,
18216 session_id,
18217 Http2BridgeEvent {
18218 kind: String::from("sessionError"),
18219 id: session_id,
18220 data: Some(http2_error_payload(message)),
18221 ..Http2BridgeEvent::default()
18222 },
18223 );
18224 }
18225 push_http2_session_event(
18226 &shared,
18227 session_id,
18228 Http2BridgeEvent {
18229 kind: String::from("sessionClose"),
18230 id: session_id,
18231 ..Http2BridgeEvent::default()
18232 },
18233 );
18234 remove_http2_session_resources(&shared, session_id);
18235 break;
18236 }
18237 Some(command) = command_rx.recv() => {
18238 match command {
18239 Http2SessionCommand::Request { headers_json, options_json, respond_to } => {
18240 let request = match build_http2_request(&headers_json) {
18241 Ok(request) => request,
18242 Err(error) => {
18243 let _ = respond_to.send(Err(error.to_string()));
18244 continue;
18245 }
18246 };
18247 let options: JavascriptHttp2RequestOptions =
18248 serde_json::from_str(&options_json).unwrap_or_default();
18249 let stream_id = {
18250 let mut state = shared.lock().expect("http2 shared state");
18251 let stream_id = next_http2_stream_id(&mut state);
18252 state.streams.insert(
18253 stream_id,
18254 ActiveHttp2Stream {
18255 session_id,
18256 paused: Arc::new(AtomicBool::new(false)),
18257 },
18258 );
18259 stream_id
18260 };
18261 match sender.send_request(request, options.end_stream) {
18262 Ok((response_future, send_stream)) => {
18263 if !options.end_stream {
18264 streams
18265 .lock()
18266 .expect("http2 client streams")
18267 .insert(stream_id, ClientHttp2StreamState { send_stream: Some(send_stream) });
18268 }
18269 let shared_clone = Arc::clone(&shared);
18270 let snapshot_clone = Arc::clone(&snapshot);
18271 tokio::spawn(async move {
18272 match response_future.await {
18273 Ok(response) => {
18274 if let Ok(headers_json) = serialize_http2_response_headers(&response) {
18275 push_http2_session_event(
18276 &shared_clone,
18277 session_id,
18278 Http2BridgeEvent {
18279 kind: String::from("clientResponseHeaders"),
18280 id: stream_id,
18281 data: Some(headers_json),
18282 ..Http2BridgeEvent::default()
18283 },
18284 );
18285 }
18286 let mut body = response.into_body();
18287 while let Some(chunk) = body.data().await {
18288 match chunk {
18289 Ok(bytes) => {
18290 let paused = {
18291 let state = shared_clone.lock().expect("http2 shared state");
18292 state.streams.get(&stream_id).map(|stream| Arc::clone(&stream.paused))
18293 };
18294 if let Some(paused) = paused {
18295 while paused.load(Ordering::SeqCst) {
18296 tokio::time::sleep(HTTP2_POLL_DELAY).await;
18297 }
18298 }
18299 let _ = body.flow_control().release_capacity(bytes.len());
18300 push_http2_session_event(
18301 &shared_clone,
18302 session_id,
18303 Http2BridgeEvent {
18304 kind: String::from("clientData"),
18305 id: stream_id,
18306 data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
18307 ..Http2BridgeEvent::default()
18308 },
18309 );
18310 }
18311 Err(error) => {
18312 push_http2_session_event(
18313 &shared_clone,
18314 session_id,
18315 Http2BridgeEvent {
18316 kind: String::from("clientError"),
18317 id: stream_id,
18318 data: Some(http2_error_payload(error.to_string())),
18319 ..Http2BridgeEvent::default()
18320 },
18321 );
18322 break;
18323 }
18324 }
18325 }
18326 {
18327 let mut snapshot = snapshot_clone.lock().expect("http2 snapshot lock");
18328 snapshot.state.next_stream_id =
18329 snapshot.state.next_stream_id.saturating_add(2);
18330 }
18331 push_http2_session_event(
18332 &shared_clone,
18333 session_id,
18334 Http2BridgeEvent {
18335 kind: String::from("clientEnd"),
18336 id: stream_id,
18337 ..Http2BridgeEvent::default()
18338 },
18339 );
18340 push_http2_session_event(
18341 &shared_clone,
18342 session_id,
18343 Http2BridgeEvent {
18344 kind: String::from("clientClose"),
18345 id: stream_id,
18346 extra_number: Some(0),
18347 ..Http2BridgeEvent::default()
18348 },
18349 );
18350 if let Ok(mut state) = shared_clone.lock() {
18351 state.streams.remove(&stream_id);
18352 }
18353 }
18354 Err(error) => {
18355 push_http2_session_event(
18356 &shared_clone,
18357 session_id,
18358 Http2BridgeEvent {
18359 kind: String::from("clientError"),
18360 id: stream_id,
18361 data: Some(http2_error_payload(error.to_string())),
18362 ..Http2BridgeEvent::default()
18363 },
18364 );
18365 push_http2_session_event(
18366 &shared_clone,
18367 session_id,
18368 Http2BridgeEvent {
18369 kind: String::from("clientClose"),
18370 id: stream_id,
18371 extra_number: Some(u32::from(Reason::INTERNAL_ERROR) as u64),
18372 ..Http2BridgeEvent::default()
18373 },
18374 );
18375 if let Ok(mut state) = shared_clone.lock() {
18376 state.streams.remove(&stream_id);
18377 }
18378 }
18379 }
18380 });
18381 let _ = respond_to.send(Ok(json!(stream_id)));
18382 }
18383 Err(error) => {
18384 if let Ok(mut state) = shared.lock() {
18385 state.streams.remove(&stream_id);
18386 }
18387 let _ = respond_to.send(Err(error.to_string()));
18388 }
18389 }
18390 }
18391 Http2SessionCommand::Settings { settings_json, respond_to } => {
18392 let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
18393 .unwrap_or_default();
18394 {
18395 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18396 snapshot.local_settings = http2_settings_from_value(&settings);
18397 }
18398 if let Ok(headers_json) = serde_json::to_string(&settings) {
18399 push_http2_session_event(
18400 &shared,
18401 session_id,
18402 Http2BridgeEvent {
18403 kind: String::from("sessionLocalSettings"),
18404 id: session_id,
18405 data: Some(headers_json.clone()),
18406 ..Http2BridgeEvent::default()
18407 },
18408 );
18409 push_http2_session_event(
18410 &shared,
18411 session_id,
18412 Http2BridgeEvent {
18413 kind: String::from("sessionSettingsAck"),
18414 id: session_id,
18415 ..Http2BridgeEvent::default()
18416 },
18417 );
18418 }
18419 let _ = respond_to.send(Ok(Value::Null));
18420 }
18421 Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
18422 {
18423 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18424 snapshot.state.local_window_size = size;
18425 snapshot.state.effective_local_window_size = size;
18426 }
18427 let value = snapshot
18428 .lock()
18429 .ok()
18430 .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
18431 .map(Value::String)
18432 .unwrap_or(Value::Null);
18433 let _ = respond_to.send(Ok(value));
18434 }
18435 Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
18436 push_http2_session_event(
18437 &shared,
18438 session_id,
18439 Http2BridgeEvent {
18440 kind: String::from("sessionGoaway"),
18441 id: session_id,
18442 data: opaque_data.map(|value| {
18443 base64::engine::general_purpose::STANDARD.encode(value)
18444 }),
18445 extra_number: Some(error_code as u64),
18446 flags: Some(last_stream_id as u64),
18447 ..Http2BridgeEvent::default()
18448 },
18449 );
18450 let _ = respond_to.send(Ok(Value::Null));
18451 }
18452 Http2SessionCommand::Close { respond_to, .. } => {
18453 let _ = respond_to.send(Ok(Value::Null));
18454 push_http2_session_event(
18455 &shared,
18456 session_id,
18457 Http2BridgeEvent {
18458 kind: String::from("sessionClose"),
18459 id: session_id,
18460 ..Http2BridgeEvent::default()
18461 },
18462 );
18463 remove_http2_session_resources(&shared, session_id);
18464 break;
18465 }
18466 Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
18467 let result = streams
18468 .lock()
18469 .expect("http2 client streams")
18470 .get_mut(&stream_id)
18471 .and_then(|stream| stream.send_stream.as_mut())
18472 .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 client stream {stream_id}")))
18473 .and_then(|stream| stream.send_data(Bytes::from(chunk), end_stream).map_err(|error| SidecarError::Execution(error.to_string())));
18474 match result {
18475 Ok(()) => {
18476 if end_stream {
18477 streams.lock().expect("http2 client streams").remove(&stream_id);
18478 }
18479 let _ = respond_to.send(Ok(Value::Bool(true)));
18480 }
18481 Err(error) => {
18482 let _ = respond_to.send(Err(error.to_string()));
18483 }
18484 }
18485 }
18486 Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
18487 let mut streams = streams.lock().expect("http2 client streams");
18488 let Some(mut state) = streams.remove(&stream_id) else {
18489 let _ = respond_to.send(Err(format!("unknown HTTP/2 client stream {stream_id}")));
18490 continue;
18491 };
18492 if let Some(stream) = state.send_stream.as_mut() {
18493 stream.send_reset(http2_reason(error_code));
18494 }
18495 if let Ok(mut state) = shared.lock() {
18496 state.streams.remove(&stream_id);
18497 }
18498 push_http2_session_event(
18499 &shared,
18500 session_id,
18501 Http2BridgeEvent {
18502 kind: String::from("clientClose"),
18503 id: stream_id,
18504 extra_number: Some(u32::from(http2_reason(error_code)) as u64),
18505 ..Http2BridgeEvent::default()
18506 },
18507 );
18508 let _ = respond_to.send(Ok(Value::Null));
18509 }
18510 Http2SessionCommand::StreamRespond { respond_to, .. }
18511 | Http2SessionCommand::StreamPush { respond_to, .. }
18512 | Http2SessionCommand::StreamRespondWithFile { respond_to, .. } => {
18513 let _ = respond_to.send(Err(String::from("HTTP/2 client streams cannot send server responses")));
18514 }
18515 }
18516 }
18517 else => break,
18518 }
18519 }
18520 });
18521 });
18522}
18523
18524fn spawn_http2_server_session(
18525 shared: Arc<Mutex<crate::state::Http2SharedState>>,
18526 server_id: u64,
18527 session_id: u64,
18528 stream: TcpStream,
18529 tls: Option<JavascriptTlsBridgeOptions>,
18530 snapshot: Arc<Mutex<Http2SessionSnapshot>>,
18531 mut command_rx: UnboundedReceiver<Http2SessionCommand>,
18532) {
18533 thread::spawn(move || {
18534 let runtime = match TokioRuntimeBuilder::new_current_thread()
18535 .enable_all()
18536 .build()
18537 {
18538 Ok(runtime) => runtime,
18539 Err(error) => {
18540 push_http2_server_event(
18541 &shared,
18542 server_id,
18543 Http2BridgeEvent {
18544 kind: String::from("serverStreamError"),
18545 id: session_id,
18546 data: Some(http2_error_payload(error.to_string())),
18547 ..Http2BridgeEvent::default()
18548 },
18549 );
18550 remove_http2_session_resources(&shared, session_id);
18551 return;
18552 }
18553 };
18554
18555 runtime.block_on(async move {
18556 if let Err(error) = stream.set_nonblocking(true) {
18557 push_http2_server_event(
18558 &shared,
18559 server_id,
18560 Http2BridgeEvent {
18561 kind: String::from("serverStreamError"),
18562 id: session_id,
18563 data: Some(http2_error_payload(error.to_string())),
18564 ..Http2BridgeEvent::default()
18565 },
18566 );
18567 remove_http2_session_resources(&shared, session_id);
18568 return;
18569 }
18570 let stream = match tokio::net::TcpStream::from_std(stream) {
18571 Ok(stream) => stream,
18572 Err(error) => {
18573 push_http2_server_event(
18574 &shared,
18575 server_id,
18576 Http2BridgeEvent {
18577 kind: String::from("serverStreamError"),
18578 id: session_id,
18579 data: Some(http2_error_payload(error.to_string())),
18580 ..Http2BridgeEvent::default()
18581 },
18582 );
18583 remove_http2_session_resources(&shared, session_id);
18584 return;
18585 }
18586 };
18587 let local_addr = match stream.local_addr() {
18588 Ok(addr) => addr,
18589 Err(error) => {
18590 push_http2_server_event(
18591 &shared,
18592 server_id,
18593 Http2BridgeEvent {
18594 kind: String::from("serverStreamError"),
18595 id: session_id,
18596 data: Some(http2_error_payload(error.to_string())),
18597 ..Http2BridgeEvent::default()
18598 },
18599 );
18600 remove_http2_session_resources(&shared, session_id);
18601 return;
18602 }
18603 };
18604 let remote_addr = match stream.peer_addr() {
18605 Ok(addr) => addr,
18606 Err(error) => {
18607 push_http2_server_event(
18608 &shared,
18609 server_id,
18610 Http2BridgeEvent {
18611 kind: String::from("serverStreamError"),
18612 id: session_id,
18613 data: Some(http2_error_payload(error.to_string())),
18614 ..Http2BridgeEvent::default()
18615 },
18616 );
18617 remove_http2_session_resources(&shared, session_id);
18618 return;
18619 }
18620 };
18621 {
18622 let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
18623 snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
18624 if tls.is_some() {
18625 snapshot_guard.encrypted = true;
18626 snapshot_guard.alpn_protocol = Some(String::from("h2"));
18627 snapshot_guard.socket.encrypted = true;
18628 snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
18629 }
18630 snapshot_guard.state = http2_runtime_snapshot();
18631 }
18632 if let Ok(snapshot_json) =
18633 http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
18634 {
18635 push_http2_server_event(
18636 &shared,
18637 server_id,
18638 Http2BridgeEvent {
18639 kind: String::from(if tls.is_some() {
18640 "serverSecureConnection"
18641 } else {
18642 "serverConnection"
18643 }),
18644 id: server_id,
18645 data: Some(serde_json::to_string(&http2_socket_snapshot(local_addr, remote_addr)).unwrap_or_default()),
18646 ..Http2BridgeEvent::default()
18647 },
18648 );
18649 push_http2_server_event(
18650 &shared,
18651 server_id,
18652 Http2BridgeEvent {
18653 kind: String::from("serverSession"),
18654 id: server_id,
18655 data: Some(snapshot_json),
18656 extra_number: Some(session_id),
18657 ..Http2BridgeEvent::default()
18658 },
18659 );
18660 }
18661
18662 let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
18663 let acceptor = match build_server_tls_config(options) {
18664 Ok(config) => TlsAcceptor::from(Arc::new(config)),
18665 Err(error) => {
18666 push_http2_server_event(
18667 &shared,
18668 server_id,
18669 Http2BridgeEvent {
18670 kind: String::from("serverStreamError"),
18671 id: session_id,
18672 data: Some(http2_error_payload(error.to_string())),
18673 ..Http2BridgeEvent::default()
18674 },
18675 );
18676 remove_http2_session_resources(&shared, session_id);
18677 return;
18678 }
18679 };
18680 match acceptor.accept(stream).await {
18681 Ok(tls_stream) => Box::pin(tls_stream),
18682 Err(error) => {
18683 push_http2_server_event(
18684 &shared,
18685 server_id,
18686 Http2BridgeEvent {
18687 kind: String::from("serverStreamError"),
18688 id: session_id,
18689 data: Some(http2_error_payload(error.to_string())),
18690 ..Http2BridgeEvent::default()
18691 },
18692 );
18693 remove_http2_session_resources(&shared, session_id);
18694 return;
18695 }
18696 }
18697 } else {
18698 Box::pin(stream)
18699 };
18700
18701 let mut connection = match server::handshake(io).await {
18702 Ok(connection) => connection,
18703 Err(error) => {
18704 push_http2_server_event(
18705 &shared,
18706 server_id,
18707 Http2BridgeEvent {
18708 kind: String::from("serverStreamError"),
18709 id: session_id,
18710 data: Some(http2_error_payload(error.to_string())),
18711 ..Http2BridgeEvent::default()
18712 },
18713 );
18714 remove_http2_session_resources(&shared, session_id);
18715 return;
18716 }
18717 };
18718
18719 let streams: Arc<Mutex<BTreeMap<u64, ServerHttp2StreamState>>> =
18720 Arc::new(Mutex::new(BTreeMap::new()));
18721
18722 loop {
18723 tokio::select! {
18724 incoming = connection.accept() => {
18725 match incoming {
18726 Some(Ok((request, respond))) => {
18727 let headers_json = match serialize_http2_request_headers(&request) {
18728 Ok(headers) => headers,
18729 Err(error) => {
18730 push_http2_server_event(
18731 &shared,
18732 server_id,
18733 Http2BridgeEvent {
18734 kind: String::from("serverStreamError"),
18735 id: server_id,
18736 data: Some(http2_error_payload(error.to_string())),
18737 ..Http2BridgeEvent::default()
18738 },
18739 );
18740 continue;
18741 }
18742 };
18743 let stream_id = {
18744 let mut state = shared.lock().expect("http2 shared state");
18745 let stream_id = next_http2_stream_id(&mut state);
18746 state.streams.insert(
18747 stream_id,
18748 ActiveHttp2Stream {
18749 session_id,
18750 paused: Arc::new(AtomicBool::new(false)),
18751 },
18752 );
18753 stream_id
18754 };
18755 streams.lock().expect("http2 server streams").insert(
18756 stream_id,
18757 ServerHttp2StreamState {
18758 send_response: Some(ServerHttp2Responder::Regular(respond)),
18759 send_stream: None,
18760 },
18761 );
18762 let snapshot_json = snapshot
18763 .lock()
18764 .ok()
18765 .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok());
18766 push_http2_server_event(
18767 &shared,
18768 server_id,
18769 Http2BridgeEvent {
18770 kind: String::from("serverStream"),
18771 id: server_id,
18772 data: Some(stream_id.to_string()),
18773 extra: snapshot_json,
18774 extra_number: Some(session_id),
18775 extra_headers: Some(headers_json),
18776 flags: Some(0),
18777 },
18778 );
18779 let shared_clone = Arc::clone(&shared);
18780 tokio::spawn(async move {
18781 let mut body = request.into_body();
18782 while let Some(chunk) = body.data().await {
18783 match chunk {
18784 Ok(bytes) => {
18785 let paused = {
18786 let state = shared_clone.lock().expect("http2 shared state");
18787 state.streams.get(&stream_id).map(|stream| Arc::clone(&stream.paused))
18788 };
18789 if let Some(paused) = paused {
18790 while paused.load(Ordering::SeqCst) {
18791 tokio::time::sleep(HTTP2_POLL_DELAY).await;
18792 }
18793 }
18794 let _ = body.flow_control().release_capacity(bytes.len());
18795 push_http2_server_event(
18796 &shared_clone,
18797 server_id,
18798 Http2BridgeEvent {
18799 kind: String::from("serverStreamData"),
18800 id: stream_id,
18801 data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
18802 ..Http2BridgeEvent::default()
18803 },
18804 );
18805 }
18806 Err(error) => {
18807 push_http2_server_event(
18808 &shared_clone,
18809 server_id,
18810 Http2BridgeEvent {
18811 kind: String::from("serverStreamError"),
18812 id: stream_id,
18813 data: Some(http2_error_payload(error.to_string())),
18814 ..Http2BridgeEvent::default()
18815 },
18816 );
18817 break;
18818 }
18819 }
18820 }
18821 push_http2_server_event(
18822 &shared_clone,
18823 server_id,
18824 Http2BridgeEvent {
18825 kind: String::from("serverStreamEnd"),
18826 id: stream_id,
18827 ..Http2BridgeEvent::default()
18828 },
18829 );
18830 });
18831 }
18832 Some(Err(error)) => {
18833 push_http2_server_event(
18834 &shared,
18835 server_id,
18836 Http2BridgeEvent {
18837 kind: String::from("serverStreamError"),
18838 id: server_id,
18839 data: Some(http2_error_payload(error.to_string())),
18840 ..Http2BridgeEvent::default()
18841 },
18842 );
18843 break;
18844 }
18845 None => {
18846 push_http2_server_event(
18847 &shared,
18848 server_id,
18849 Http2BridgeEvent {
18850 kind: String::from("sessionClose"),
18851 id: session_id,
18852 ..Http2BridgeEvent::default()
18853 },
18854 );
18855 remove_http2_session_resources(&shared, session_id);
18856 break;
18857 }
18858 }
18859 }
18860 Some(command) = command_rx.recv() => {
18861 match command {
18862 Http2SessionCommand::Settings { settings_json, respond_to } => {
18863 let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
18864 .unwrap_or_default();
18865 if let Some(initial_window_size) = settings
18866 .get("initialWindowSize")
18867 .and_then(Value::as_u64)
18868 {
18869 let _ = connection.set_initial_window_size(initial_window_size as u32);
18870 }
18871 {
18872 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18873 snapshot.local_settings = http2_settings_from_value(&settings);
18874 }
18875 if let Ok(headers_json) = serde_json::to_string(&settings) {
18876 push_http2_session_event(
18877 &shared,
18878 session_id,
18879 Http2BridgeEvent {
18880 kind: String::from("sessionLocalSettings"),
18881 id: session_id,
18882 data: Some(headers_json),
18883 ..Http2BridgeEvent::default()
18884 },
18885 );
18886 }
18887 let _ = respond_to.send(Ok(Value::Null));
18888 }
18889 Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
18890 connection.set_target_window_size(size);
18891 {
18892 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18893 snapshot.state.local_window_size = size;
18894 snapshot.state.effective_local_window_size = size;
18895 }
18896 let value = snapshot
18897 .lock()
18898 .ok()
18899 .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
18900 .map(Value::String)
18901 .unwrap_or(Value::Null);
18902 let _ = respond_to.send(Ok(value));
18903 }
18904 Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
18905 connection.abrupt_shutdown(http2_reason(Some(error_code)));
18906 push_http2_session_event(
18907 &shared,
18908 session_id,
18909 Http2BridgeEvent {
18910 kind: String::from("sessionGoaway"),
18911 id: session_id,
18912 data: opaque_data.map(|value| {
18913 base64::engine::general_purpose::STANDARD.encode(value)
18914 }),
18915 extra_number: Some(error_code as u64),
18916 flags: Some(last_stream_id as u64),
18917 ..Http2BridgeEvent::default()
18918 },
18919 );
18920 let _ = respond_to.send(Ok(Value::Null));
18921 }
18922 Http2SessionCommand::Close { abrupt, respond_to } => {
18923 if abrupt {
18924 connection.abrupt_shutdown(Reason::NO_ERROR);
18925 } else {
18926 connection.graceful_shutdown();
18927 }
18928 let _ = respond_to.send(Ok(Value::Null));
18929 push_http2_session_event(
18930 &shared,
18931 session_id,
18932 Http2BridgeEvent {
18933 kind: String::from("sessionClose"),
18934 id: session_id,
18935 ..Http2BridgeEvent::default()
18936 },
18937 );
18938 remove_http2_session_resources(&shared, session_id);
18939 break;
18940 }
18941 Http2SessionCommand::StreamRespond { stream_id, headers_json, respond_to } => {
18942 let response = match build_http2_response(&headers_json) {
18943 Ok(response) => response,
18944 Err(error) => {
18945 let _ = respond_to.send(Err(error.to_string()));
18946 continue;
18947 }
18948 };
18949 let mut streams = streams.lock().expect("http2 server streams");
18950 let Some(state) = streams.get_mut(&stream_id) else {
18951 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18952 continue;
18953 };
18954 let Some(send_response) = state.send_response.as_mut() else {
18955 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
18956 continue;
18957 };
18958 match match send_response {
18959 ServerHttp2Responder::Regular(send_response) => {
18960 send_response.send_response(response, false)
18961 }
18962 ServerHttp2Responder::Pushed(send_response) => {
18963 send_response.send_response(response, false)
18964 }
18965 } {
18966 Ok(send_stream) => {
18967 state.send_stream = Some(send_stream);
18968 state.send_response = None;
18969 let _ = respond_to.send(Ok(Value::Null));
18970 }
18971 Err(error) => {
18972 let _ = respond_to.send(Err(error.to_string()));
18973 }
18974 }
18975 }
18976 Http2SessionCommand::StreamPush { stream_id, headers_json, respond_to } => {
18977 let request = match build_http2_request(&headers_json) {
18978 Ok(request) => request,
18979 Err(error) => {
18980 let _ = respond_to.send(Err(error.to_string()));
18981 continue;
18982 }
18983 };
18984 let mut streams_guard = streams.lock().expect("http2 server streams");
18985 let Some(state) = streams_guard.get_mut(&stream_id) else {
18986 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18987 continue;
18988 };
18989 let Some(send_response) = state.send_response.as_mut() else {
18990 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} cannot push after responding")));
18991 continue;
18992 };
18993 let ServerHttp2Responder::Regular(send_response) = send_response else {
18994 let _ = respond_to.send(Err(format!("HTTP/2 pushed stream {stream_id} cannot create nested push promises")));
18995 continue;
18996 };
18997 match send_response.push_request(request) {
18998 Ok(pushed) => {
18999 let pushed_stream_id = {
19000 let mut state = shared.lock().expect("http2 shared state");
19001 let pushed_stream_id = next_http2_stream_id(&mut state);
19002 state.streams.insert(
19003 pushed_stream_id,
19004 ActiveHttp2Stream {
19005 session_id,
19006 paused: Arc::new(AtomicBool::new(false)),
19007 },
19008 );
19009 pushed_stream_id
19010 };
19011 streams_guard.insert(
19012 pushed_stream_id,
19013 ServerHttp2StreamState {
19014 send_response: Some(ServerHttp2Responder::Pushed(pushed)),
19015 send_stream: None,
19016 },
19017 );
19018 let _ = respond_to.send(Ok(json!({
19019 "streamId": pushed_stream_id,
19020 "headers": headers_json,
19021 }).to_string().into()));
19022 }
19023 Err(error) => {
19024 let _ = respond_to.send(Err(error.to_string()));
19025 }
19026 }
19027 }
19028 Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
19029 let mut streams = streams.lock().expect("http2 server streams");
19030 let Some(state) = streams.get_mut(&stream_id) else {
19031 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19032 continue;
19033 };
19034 let Some(send_stream) = state.send_stream.as_mut() else {
19035 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} has not sent response headers")));
19036 continue;
19037 };
19038 match send_stream.send_data(Bytes::from(chunk), end_stream) {
19039 Ok(()) => {
19040 if end_stream {
19041 streams.remove(&stream_id);
19042 if let Ok(mut state) = shared.lock() {
19043 state.streams.remove(&stream_id);
19044 }
19045 push_http2_server_event(
19046 &shared,
19047 server_id,
19048 Http2BridgeEvent {
19049 kind: String::from("serverStreamClose"),
19050 id: stream_id,
19051 extra_number: Some(0),
19052 ..Http2BridgeEvent::default()
19053 },
19054 );
19055 }
19056 let _ = respond_to.send(Ok(Value::Bool(true)));
19057 }
19058 Err(error) => {
19059 let _ = respond_to.send(Err(error.to_string()));
19060 }
19061 }
19062 }
19063 Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
19064 let mut streams_guard = streams.lock().expect("http2 server streams");
19065 let Some(mut state) = streams_guard.remove(&stream_id) else {
19066 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19067 continue;
19068 };
19069 let reason = http2_reason(error_code);
19070 if let Some(send_stream) = state.send_stream.as_mut() {
19071 send_stream.send_reset(reason);
19072 }
19073 if let Some(send_response) = state.send_response.as_mut() {
19074 match send_response {
19075 ServerHttp2Responder::Regular(send_response) => {
19076 send_response.send_reset(reason)
19077 }
19078 ServerHttp2Responder::Pushed(send_response) => {
19079 send_response.send_reset(reason)
19080 }
19081 }
19082 }
19083 if let Ok(mut shared_guard) = shared.lock() {
19084 shared_guard.streams.remove(&stream_id);
19085 }
19086 push_http2_server_event(
19087 &shared,
19088 server_id,
19089 Http2BridgeEvent {
19090 kind: String::from("serverStreamClose"),
19091 id: stream_id,
19092 extra_number: Some(u32::from(reason) as u64),
19093 ..Http2BridgeEvent::default()
19094 },
19095 );
19096 let _ = respond_to.send(Ok(Value::Null));
19097 }
19098 Http2SessionCommand::StreamRespondWithFile { stream_id, body, headers_json, options_json, respond_to } => {
19099 let options: JavascriptHttp2FileResponseOptions =
19100 serde_json::from_str(&options_json).unwrap_or_default();
19101 let response = match build_http2_response(&headers_json) {
19102 Ok(response) => response,
19103 Err(error) => {
19104 let _ = respond_to.send(Err(error.to_string()));
19105 continue;
19106 }
19107 };
19108 let offset = usize::try_from(options.offset.unwrap_or_default()).unwrap_or(0);
19109 let body = if offset >= body.len() {
19110 Vec::new()
19111 } else {
19112 let body = &body[offset..];
19113 match options.length {
19114 Some(length) if length >= 0 => {
19115 body[..body.len().min(length as usize)].to_vec()
19116 }
19117 _ => body.to_vec(),
19118 }
19119 };
19120 let mut streams_guard = streams.lock().expect("http2 server streams");
19121 let Some(state) = streams_guard.get_mut(&stream_id) else {
19122 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19123 continue;
19124 };
19125 let Some(send_response) = state.send_response.as_mut() else {
19126 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
19127 continue;
19128 };
19129 match match send_response {
19130 ServerHttp2Responder::Regular(send_response) => {
19131 send_response.send_response(response, body.is_empty())
19132 }
19133 ServerHttp2Responder::Pushed(send_response) => {
19134 send_response.send_response(response, body.is_empty())
19135 }
19136 } {
19137 Ok(mut send_stream) => {
19138 state.send_response = None;
19139 if body.is_empty() {
19140 streams_guard.remove(&stream_id);
19141 if let Ok(mut shared_guard) = shared.lock() {
19142 shared_guard.streams.remove(&stream_id);
19143 }
19144 } else {
19145 if let Err(error) = send_stream.send_data(Bytes::from(body), true) {
19146 let _ = respond_to.send(Err(error.to_string()));
19147 continue;
19148 }
19149 streams_guard.remove(&stream_id);
19150 if let Ok(mut shared_guard) = shared.lock() {
19151 shared_guard.streams.remove(&stream_id);
19152 }
19153 }
19154 push_http2_server_event(
19155 &shared,
19156 server_id,
19157 Http2BridgeEvent {
19158 kind: String::from("serverStreamClose"),
19159 id: stream_id,
19160 extra_number: Some(0),
19161 ..Http2BridgeEvent::default()
19162 },
19163 );
19164 let _ = respond_to.send(Ok(Value::Null));
19165 }
19166 Err(error) => {
19167 let _ = respond_to.send(Err(error.to_string()));
19168 }
19169 }
19170 }
19171 Http2SessionCommand::Request { respond_to, .. } => {
19172 let _ = respond_to.send(Err(String::from("HTTP/2 server sessions cannot initiate client requests")));
19173 }
19174 }
19175 }
19176 else => break,
19177 }
19178 }
19179 });
19180 });
19181}
19182
19183fn spawn_http2_server_accept_loop(
19184 shared: Arc<Mutex<crate::state::Http2SharedState>>,
19185 server_id: u64,
19186 listener: TcpListener,
19187) {
19188 thread::spawn(move || {
19189 let listener = listener;
19190 loop {
19191 let closed = shared
19192 .lock()
19193 .ok()
19194 .and_then(|state| {
19195 state
19196 .servers
19197 .get(&server_id)
19198 .map(|server| server.closed.load(Ordering::SeqCst))
19199 })
19200 .unwrap_or(true);
19201 if closed {
19202 break;
19203 }
19204 match listener.accept() {
19205 Ok((stream, _)) => {
19206 let (command_tx, command_rx) = unbounded_channel();
19207 let (guest_local_addr, secure, tls) = {
19208 let state = shared.lock().expect("http2 shared state");
19209 let server = state.servers.get(&server_id).expect("http2 server state");
19210 (server.guest_local_addr, server.secure, server.tls.clone())
19211 };
19212 let (local_addr, remote_addr) = match (stream.local_addr(), stream.peer_addr())
19213 {
19214 (Ok(local_addr), Ok(remote_addr)) => (local_addr, remote_addr),
19215 _ => continue,
19216 };
19217 let session_snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
19218 encrypted: secure,
19219 alpn_protocol: Some(if secure {
19220 String::from("h2")
19221 } else {
19222 String::from("h2c")
19223 }),
19224 local_settings: BTreeMap::new(),
19225 remote_settings: BTreeMap::new(),
19226 state: http2_runtime_snapshot(),
19227 socket: Http2SocketSnapshot {
19228 local_address: Some(guest_local_addr.ip().to_string()),
19229 local_port: Some(guest_local_addr.port()),
19230 local_family: Some(socket_addr_family(&guest_local_addr).to_string()),
19231 remote_address: Some(remote_addr.ip().to_string()),
19232 remote_port: Some(remote_addr.port()),
19233 remote_family: Some(socket_addr_family(&remote_addr).to_string()),
19234 ..http2_socket_snapshot(local_addr, remote_addr)
19235 },
19236 ..Http2SessionSnapshot::default()
19237 }));
19238 let session_id = {
19239 let mut state = shared.lock().expect("http2 shared state");
19240 let session_id = next_http2_session_id(&mut state);
19241 state
19242 .sessions
19243 .insert(session_id, ActiveHttp2Session { command_tx });
19244 session_id
19245 };
19246 spawn_http2_server_session(
19247 Arc::clone(&shared),
19248 server_id,
19249 session_id,
19250 stream,
19251 tls,
19252 session_snapshot,
19253 command_rx,
19254 );
19255 }
19256 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
19257 thread::sleep(HTTP2_POLL_DELAY);
19258 }
19259 Err(error) => {
19260 push_http2_server_event(
19261 &shared,
19262 server_id,
19263 Http2BridgeEvent {
19264 kind: String::from("serverStreamError"),
19265 id: server_id,
19266 data: Some(http2_error_payload(error.to_string())),
19267 ..Http2BridgeEvent::default()
19268 },
19269 );
19270 thread::sleep(HTTP2_POLL_DELAY);
19271 }
19272 }
19273 }
19274 });
19275}
19276
19277fn send_http2_command(
19278 session: &ActiveHttp2Session,
19279 command: impl FnOnce(Sender<Result<Value, String>>) -> Http2SessionCommand,
19280) -> Result<Value, SidecarError> {
19281 let (respond_to, response_rx) = mpsc::channel();
19282 session.command_tx.send(command(respond_to)).map_err(|_| {
19283 SidecarError::InvalidState(String::from("HTTP/2 session command channel closed"))
19284 })?;
19285 response_rx
19286 .recv_timeout(Duration::from_secs(30))
19287 .map_err(|_| {
19288 SidecarError::Execution(String::from("timed out waiting for HTTP/2 session command"))
19289 })?
19290 .map_err(SidecarError::Execution)
19291}
19292
19293fn parse_http2_server_listen_payload(
19294 request: &JavascriptSyncRpcRequest,
19295) -> Result<JavascriptHttp2ServerListenRequest, SidecarError> {
19296 let payload_json =
19297 javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_server_listen payload")?;
19298 serde_json::from_str(payload_json).map_err(|error| {
19299 SidecarError::InvalidState(format!(
19300 "net.http2_server_listen payload must be valid JSON: {error}"
19301 ))
19302 })
19303}
19304
19305fn parse_http2_connect_payload(
19306 request: &JavascriptSyncRpcRequest,
19307) -> Result<JavascriptHttp2SessionConnectRequest, SidecarError> {
19308 let payload_json =
19309 javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_session_connect payload")?;
19310 serde_json::from_str(payload_json).map_err(|error| {
19311 SidecarError::InvalidState(format!(
19312 "net.http2_session_connect payload must be valid JSON: {error}"
19313 ))
19314 })
19315}
19316
19317fn http2_session_for_id(
19318 process: &ActiveProcess,
19319 session_id: u64,
19320) -> Result<ActiveHttp2Session, SidecarError> {
19321 let shared = process
19322 .http2
19323 .shared
19324 .lock()
19325 .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
19326 shared
19327 .sessions
19328 .get(&session_id)
19329 .cloned()
19330 .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 session {session_id}")))
19331}
19332
19333fn http2_stream_for_id(
19334 process: &ActiveProcess,
19335 stream_id: u64,
19336) -> Result<ActiveHttp2Stream, SidecarError> {
19337 let shared = process
19338 .http2
19339 .shared
19340 .lock()
19341 .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
19342 shared
19343 .streams
19344 .get(&stream_id)
19345 .cloned()
19346 .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 stream {stream_id}")))
19347}
19348
19349fn service_javascript_http2_sync_rpc<B>(
19350 request: JavascriptHttp2SyncRpcServiceRequest<'_, B>,
19351) -> Result<Value, SidecarError>
19352where
19353 B: NativeSidecarBridge + Send + 'static,
19354 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19355{
19356 let JavascriptHttp2SyncRpcServiceRequest {
19357 bridge,
19358 kernel,
19359 vm_id,
19360 dns,
19361 socket_paths,
19362 process,
19363 sync_request: request,
19364 resource_limits,
19365 network_counts,
19366 } = request;
19367 match request.method.as_str() {
19368 "net.http2_server_listen" => {
19369 check_network_resource_limit(
19370 resource_limits.max_sockets,
19371 network_counts.sockets,
19372 1,
19373 "socket",
19374 )?;
19375 let payload = parse_http2_server_listen_payload(request)?;
19376 let (family, bind_host, guest_host) =
19377 normalize_tcp_listen_host(payload.host.as_deref())?;
19378 let requested_port = payload.port.unwrap_or(0);
19379 bridge.require_network_access(
19380 vm_id,
19381 NetworkOperation::Listen,
19382 format_tcp_resource(bind_host, requested_port),
19383 )?;
19384 let port = allocate_guest_listen_port(
19385 requested_port,
19386 family,
19387 &socket_paths.used_tcp_guest_ports,
19388 socket_paths.listen_policy,
19389 )?;
19390 let mut listener =
19391 ActiveTcpListener::bind(bind_host, guest_host, port, payload.backlog)?;
19392 let guest_local_addr = listener.guest_local_addr();
19393 let closed = Arc::new(AtomicBool::new(false));
19394 {
19395 let mut state = process.http2.shared.lock().map_err(|_| {
19396 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19397 })?;
19398 state.servers.insert(
19399 payload.server_id,
19400 ActiveHttp2Server {
19401 actual_local_addr: listener.local_addr(),
19402 guest_local_addr,
19403 secure: payload.secure,
19404 tls: payload.tls.clone().map(|mut tls| {
19405 tls.is_server = payload.secure;
19406 if payload.secure && tls.alpn_protocols.is_none() {
19407 tls.alpn_protocols = Some(vec![String::from("h2")]);
19408 }
19409 tls
19410 }),
19411 closed: Arc::clone(&closed),
19412 },
19413 );
19414 state.server_events.entry(payload.server_id).or_default();
19415 }
19416 spawn_http2_server_accept_loop(
19417 Arc::clone(&process.http2.shared),
19418 payload.server_id,
19419 listener.listener.take().ok_or_else(|| {
19420 SidecarError::InvalidState(String::from(
19421 "HTTP/2 listener missing host TCP socket",
19422 ))
19423 })?,
19424 );
19425 javascript_net_json_string(
19426 json!({
19427 "address": {
19428 "address": guest_local_addr.ip().to_string(),
19429 "family": socket_addr_family(&guest_local_addr),
19430 "port": guest_local_addr.port(),
19431 }
19432 }),
19433 "net.http2_server_listen",
19434 )
19435 }
19436 "net.http2_server_poll" => {
19437 let server_id =
19438 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_poll server id")?;
19439 let wait_ms = javascript_sync_rpc_arg_u64_optional(
19440 &request.args,
19441 1,
19442 "net.http2_server_poll wait ms",
19443 )?
19444 .unwrap_or_default();
19445 match wait_for_http2_event(&process.http2.shared, server_id, true, wait_ms) {
19446 Some(event) => http2_event_value(&event),
19447 None => Ok(Value::Null),
19448 }
19449 }
19450 "net.http2_server_wait" => {
19451 let server_id =
19452 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_wait server id")?;
19453 dispatch_http2_wait_loop(process, server_id, true)
19454 }
19455 "net.http2_server_close" => {
19456 let server_id =
19457 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_close server id")?;
19458 let server = {
19459 let mut state = process.http2.shared.lock().map_err(|_| {
19460 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19461 })?;
19462 state.servers.remove(&server_id)
19463 }
19464 .ok_or_else(|| {
19465 SidecarError::InvalidState(format!("unknown HTTP/2 server {server_id}"))
19466 })?;
19467 server.closed.store(true, Ordering::SeqCst);
19468 push_http2_server_event(
19469 &process.http2.shared,
19470 server_id,
19471 Http2BridgeEvent {
19472 kind: String::from("serverClose"),
19473 id: server_id,
19474 ..Http2BridgeEvent::default()
19475 },
19476 );
19477 Ok(Value::Null)
19478 }
19479 "net.http2_server_respond" => {
19480 let server_id = javascript_sync_rpc_arg_u64(
19481 &request.args,
19482 0,
19483 "net.http2_server_respond server id",
19484 )?;
19485 let request_id = javascript_sync_rpc_arg_u64(
19486 &request.args,
19487 1,
19488 "net.http2_server_respond request id",
19489 )?;
19490 let response_json =
19491 javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_server_respond payload")?;
19492 ensure_vm_fetch_response_within_limit(
19493 response_json,
19494 "net.http2_server_respond",
19495 VM_FETCH_BUFFER_LIMIT_BYTES,
19496 )?;
19497 serde_json::from_str::<Value>(response_json).map_err(|error| {
19498 SidecarError::Execution(format!(
19499 "net.http2_server_respond payload must be valid JSON: {error}"
19500 ))
19501 })?;
19502 let Some(pending) = process
19503 .pending_http_requests
19504 .get_mut(&(server_id, request_id))
19505 else {
19506 return Err(SidecarError::InvalidState(format!(
19507 "unknown pending HTTP/2 request {request_id} for server {server_id}"
19508 )));
19509 };
19510 *pending = Some(response_json.to_owned());
19511 Ok(Value::Bool(true))
19512 }
19513 "net.http2_session_connect" => {
19514 check_network_resource_limit(
19515 resource_limits.max_sockets,
19516 network_counts.sockets,
19517 1,
19518 "socket",
19519 )?;
19520 check_network_resource_limit(
19521 resource_limits.max_connections,
19522 network_counts.connections,
19523 1,
19524 "connection",
19525 )?;
19526 let payload = parse_http2_connect_payload(request)?;
19527 let authority = payload.authority.clone().unwrap_or_else(|| {
19528 format!(
19529 "{}://{}:{}",
19530 payload.protocol.as_deref().unwrap_or("http"),
19531 payload.host.as_deref().unwrap_or("localhost"),
19532 payload.port.unwrap_or(80)
19533 )
19534 });
19535 let url = Url::parse(&authority).map_err(|error| {
19536 SidecarError::InvalidState(format!(
19537 "invalid HTTP/2 authority {authority:?}: {error}"
19538 ))
19539 })?;
19540 let secure = url.scheme() == "https" || payload.protocol.as_deref() == Some("https:");
19541 let host = payload
19542 .host
19543 .as_deref()
19544 .or_else(|| url.host_str())
19545 .unwrap_or("localhost");
19546 let port = payload.port.or_else(|| url.port()).unwrap_or(80);
19547 bridge.require_network_access(
19548 vm_id,
19549 NetworkOperation::Http,
19550 format_tcp_resource(host, port),
19551 )?;
19552 let resolved = {
19553 let shared = process.http2.shared.lock().map_err(|_| {
19554 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19555 })?;
19556 shared
19557 .servers
19558 .values()
19559 .find(|server| {
19560 is_loopback_request_host(host) && server.guest_local_addr.port() == port
19561 })
19562 .map(|server| ResolvedTcpConnectAddr {
19563 actual_addr: server.actual_local_addr,
19564 guest_remote_addr: server.guest_local_addr,
19565 use_kernel_loopback: false,
19566 })
19567 };
19568 let resolved = match resolved {
19569 Some(resolved) => resolved,
19570 None => {
19571 resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, socket_paths)?
19572 }
19573 };
19574 let (command_tx, command_rx) = unbounded_channel();
19575 let snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
19576 encrypted: secure,
19577 alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
19578 local_settings: http2_settings_from_value(&payload.settings),
19579 remote_settings: BTreeMap::new(),
19580 state: http2_runtime_snapshot(),
19581 socket: Http2SocketSnapshot {
19582 encrypted: secure,
19583 remote_address: Some(resolved.guest_remote_addr.ip().to_string()),
19584 remote_port: Some(resolved.guest_remote_addr.port()),
19585 remote_family: Some(
19586 socket_addr_family(&resolved.guest_remote_addr).to_string(),
19587 ),
19588 servername: if secure {
19589 payload
19590 .tls
19591 .as_ref()
19592 .and_then(|tls| tls.servername.clone())
19593 .or_else(|| Some(host.to_string()))
19594 } else {
19595 None
19596 },
19597 alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
19598 ..Http2SocketSnapshot::default()
19599 },
19600 ..Http2SessionSnapshot::default()
19601 }));
19602 let session_id = {
19603 let mut state = process.http2.shared.lock().map_err(|_| {
19604 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19605 })?;
19606 let session_id = next_http2_session_id(&mut state);
19607 state
19608 .sessions
19609 .insert(session_id, ActiveHttp2Session { command_tx });
19610 state.session_events.entry(session_id).or_default();
19611 session_id
19612 };
19613 spawn_http2_client_session(
19614 Arc::clone(&process.http2.shared),
19615 session_id,
19616 resolved.actual_addr,
19617 if secure {
19618 Some(payload.tls.unwrap_or(JavascriptTlsBridgeOptions {
19619 is_server: false,
19620 servername: Some(host.to_string()),
19621 alpn_protocols: Some(vec![String::from("h2")]),
19622 ..JavascriptTlsBridgeOptions::default()
19623 }))
19624 } else {
19625 None
19626 },
19627 Arc::clone(&snapshot),
19628 command_rx,
19629 );
19630 let snapshot_json =
19631 http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())?;
19632 javascript_net_json_string(
19633 json!({
19634 "sessionId": session_id,
19635 "state": snapshot_json,
19636 }),
19637 "net.http2_session_connect",
19638 )
19639 }
19640 "net.http2_session_request" => {
19641 let session_id = javascript_sync_rpc_arg_u64(
19642 &request.args,
19643 0,
19644 "net.http2_session_request session id",
19645 )?;
19646 let headers_json =
19647 javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_session_request headers")?;
19648 let options_json =
19649 javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_session_request options")?;
19650 let session = http2_session_for_id(process, session_id)?;
19651 send_http2_command(&session, |respond_to| Http2SessionCommand::Request {
19652 headers_json: headers_json.to_owned(),
19653 options_json: options_json.to_owned(),
19654 respond_to,
19655 })
19656 }
19657 "net.http2_session_settings" => {
19658 let session_id = javascript_sync_rpc_arg_u64(
19659 &request.args,
19660 0,
19661 "net.http2_session_settings session id",
19662 )?;
19663 let settings_json = javascript_sync_rpc_arg_str(
19664 &request.args,
19665 1,
19666 "net.http2_session_settings settings",
19667 )?;
19668 let session = http2_session_for_id(process, session_id)?;
19669 send_http2_command(&session, |respond_to| Http2SessionCommand::Settings {
19670 settings_json: settings_json.to_owned(),
19671 respond_to,
19672 })
19673 }
19674 "net.http2_session_set_local_window_size" => {
19675 let session_id = javascript_sync_rpc_arg_u64(
19676 &request.args,
19677 0,
19678 "net.http2_session_set_local_window_size session id",
19679 )?;
19680 let window_size = javascript_sync_rpc_arg_u64(
19681 &request.args,
19682 1,
19683 "net.http2_session_set_local_window_size window size",
19684 )?;
19685 let session = http2_session_for_id(process, session_id)?;
19686 send_http2_command(&session, |respond_to| {
19687 Http2SessionCommand::SetLocalWindowSize {
19688 size: window_size as u32,
19689 respond_to,
19690 }
19691 })
19692 }
19693 "net.http2_session_goaway" => {
19694 let session_id = javascript_sync_rpc_arg_u64(
19695 &request.args,
19696 0,
19697 "net.http2_session_goaway session id",
19698 )?;
19699 let error_code = javascript_sync_rpc_arg_u64(
19700 &request.args,
19701 1,
19702 "net.http2_session_goaway error code",
19703 )?;
19704 let last_stream_id = javascript_sync_rpc_arg_u64(
19705 &request.args,
19706 2,
19707 "net.http2_session_goaway last stream id",
19708 )?;
19709 let opaque_data = request
19710 .args
19711 .get(3)
19712 .and_then(Value::as_str)
19713 .map(|value| {
19714 base64::engine::general_purpose::STANDARD
19715 .decode(value)
19716 .map_err(|error| {
19717 SidecarError::InvalidState(format!("invalid GOAWAY payload: {error}"))
19718 })
19719 })
19720 .transpose()?;
19721 let session = http2_session_for_id(process, session_id)?;
19722 send_http2_command(&session, |respond_to| Http2SessionCommand::Goaway {
19723 error_code: error_code as u32,
19724 last_stream_id: last_stream_id as u32,
19725 opaque_data,
19726 respond_to,
19727 })
19728 }
19729 "net.http2_session_close" | "net.http2_session_destroy" => {
19730 let session_id = javascript_sync_rpc_arg_u64(
19731 &request.args,
19732 0,
19733 "net.http2_session_close session id",
19734 )?;
19735 let session = http2_session_for_id(process, session_id)?;
19736 send_http2_command(&session, |respond_to| Http2SessionCommand::Close {
19737 abrupt: request.method == "net.http2_session_destroy",
19738 respond_to,
19739 })
19740 }
19741 "net.http2_session_poll" => {
19742 let session_id =
19743 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_poll session id")?;
19744 let wait_ms = javascript_sync_rpc_arg_u64_optional(
19745 &request.args,
19746 1,
19747 "net.http2_session_poll wait ms",
19748 )?
19749 .unwrap_or_default();
19750 match wait_for_http2_event(&process.http2.shared, session_id, false, wait_ms) {
19751 Some(event) => http2_event_value(&event),
19752 None => Ok(Value::Null),
19753 }
19754 }
19755 "net.http2_session_wait" => {
19756 let session_id =
19757 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_wait session id")?;
19758 dispatch_http2_wait_loop(process, session_id, false)
19759 }
19760 "net.http2_stream_respond" => {
19761 let stream_id = javascript_sync_rpc_arg_u64(
19762 &request.args,
19763 0,
19764 "net.http2_stream_respond stream id",
19765 )?;
19766 let headers_json =
19767 javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_stream_respond headers")?;
19768 let stream = http2_stream_for_id(process, stream_id)?;
19769 let session = http2_session_for_id(process, stream.session_id)?;
19770 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamRespond {
19771 stream_id,
19772 headers_json: headers_json.to_owned(),
19773 respond_to,
19774 })
19775 }
19776 "net.http2_stream_push_stream" => {
19777 let stream_id = javascript_sync_rpc_arg_u64(
19778 &request.args,
19779 0,
19780 "net.http2_stream_push_stream stream id",
19781 )?;
19782 let headers_json = javascript_sync_rpc_arg_str(
19783 &request.args,
19784 1,
19785 "net.http2_stream_push_stream headers",
19786 )?;
19787 let _options_json = javascript_sync_rpc_arg_str(
19788 &request.args,
19789 2,
19790 "net.http2_stream_push_stream options",
19791 )?;
19792 let stream = http2_stream_for_id(process, stream_id)?;
19793 let session = http2_session_for_id(process, stream.session_id)?;
19794 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamPush {
19795 stream_id,
19796 headers_json: headers_json.to_owned(),
19797 respond_to,
19798 })
19799 }
19800 "net.http2_stream_write" => {
19801 let stream_id =
19802 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_write stream id")?;
19803 let chunk =
19804 javascript_sync_rpc_base64_arg(&request.args, 1, "net.http2_stream_write data")?;
19805 let stream = http2_stream_for_id(process, stream_id)?;
19806 let session = http2_session_for_id(process, stream.session_id)?;
19807 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
19808 stream_id,
19809 chunk,
19810 end_stream: false,
19811 respond_to,
19812 })
19813 }
19814 "net.http2_stream_end" => {
19815 let stream_id =
19816 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_end stream id")?;
19817 let chunk = request
19818 .args
19819 .get(1)
19820 .and_then(Value::as_str)
19821 .map(|value| {
19822 base64::engine::general_purpose::STANDARD
19823 .decode(value)
19824 .map_err(|error| {
19825 SidecarError::InvalidState(format!(
19826 "invalid HTTP/2 stream payload: {error}"
19827 ))
19828 })
19829 })
19830 .transpose()?
19831 .unwrap_or_default();
19832 let stream = http2_stream_for_id(process, stream_id)?;
19833 let session = http2_session_for_id(process, stream.session_id)?;
19834 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
19835 stream_id,
19836 chunk,
19837 end_stream: true,
19838 respond_to,
19839 })
19840 }
19841 "net.http2_stream_close" => {
19842 let stream_id =
19843 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_close stream id")?;
19844 let code = javascript_sync_rpc_arg_u64_optional(
19845 &request.args,
19846 1,
19847 "net.http2_stream_close error code",
19848 )?
19849 .map(|value| value as u32);
19850 let stream = http2_stream_for_id(process, stream_id)?;
19851 let session = http2_session_for_id(process, stream.session_id)?;
19852 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamClose {
19853 stream_id,
19854 error_code: code,
19855 respond_to,
19856 })
19857 }
19858 "net.http2_stream_pause" => {
19859 let stream_id =
19860 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_pause stream id")?;
19861 let stream = http2_stream_for_id(process, stream_id)?;
19862 stream.paused.store(true, Ordering::SeqCst);
19863 Ok(Value::Null)
19864 }
19865 "net.http2_stream_resume" => {
19866 let stream_id =
19867 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_resume stream id")?;
19868 let stream = http2_stream_for_id(process, stream_id)?;
19869 stream.paused.store(false, Ordering::SeqCst);
19870 Ok(Value::Null)
19871 }
19872 "net.http2_stream_respond_with_file" => {
19873 let stream_id = javascript_sync_rpc_arg_u64(
19874 &request.args,
19875 0,
19876 "net.http2_stream_respond_with_file stream id",
19877 )?;
19878 let path = javascript_sync_rpc_arg_str(
19879 &request.args,
19880 1,
19881 "net.http2_stream_respond_with_file path",
19882 )?;
19883 let headers_json = javascript_sync_rpc_arg_str(
19884 &request.args,
19885 2,
19886 "net.http2_stream_respond_with_file headers",
19887 )?;
19888 let options_json = javascript_sync_rpc_arg_str(
19889 &request.args,
19890 3,
19891 "net.http2_stream_respond_with_file options",
19892 )?;
19893 let stream = http2_stream_for_id(process, stream_id)?;
19894 let session = http2_session_for_id(process, stream.session_id)?;
19895 let guest_path = resolve_http2_file_response_guest_path(process, path);
19896 let body = kernel.read_file(&guest_path).map_err(kernel_error)?;
19897 send_http2_command(&session, |respond_to| {
19898 Http2SessionCommand::StreamRespondWithFile {
19899 stream_id,
19900 body,
19901 headers_json: headers_json.to_owned(),
19902 options_json: options_json.to_owned(),
19903 respond_to,
19904 }
19905 })
19906 }
19907 other => Err(SidecarError::InvalidState(format!(
19908 "unsupported JavaScript HTTP/2 sync RPC method {other}"
19909 ))),
19910 }
19911}
19912
19913const JAVASCRIPT_NET_POLL_MAX_WAIT: Duration = Duration::from_millis(50);
19914const EXITED_PROCESS_SNAPSHOT_RETENTION: Duration = Duration::from_secs(2);
19915
19916fn resolve_http2_file_response_guest_path(process: &ActiveProcess, path: &str) -> String {
19917 if Path::new(path).is_absolute() {
19918 normalize_path(path)
19919 } else {
19920 normalize_path(&format!("{}/{}", process.guest_cwd, path))
19921 }
19922}
19923
19924pub(crate) fn clamp_javascript_net_poll_wait(wait_ms: u64) -> Duration {
19925 if wait_ms == 0 {
19928 Duration::ZERO
19929 } else {
19930 Duration::from_millis(wait_ms).min(JAVASCRIPT_NET_POLL_MAX_WAIT)
19931 }
19932}
19933
19934pub(crate) fn service_javascript_net_sync_rpc<B>(
19935 request: JavascriptNetSyncRpcServiceRequest<'_, B>,
19936) -> Result<Value, SidecarError>
19937where
19938 B: NativeSidecarBridge + Send + 'static,
19939 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19940{
19941 let JavascriptNetSyncRpcServiceRequest {
19942 bridge,
19943 vm_id,
19944 dns,
19945 socket_paths,
19946 kernel,
19947 process,
19948 sync_request: request,
19949 resource_limits,
19950 network_counts,
19951 } = request;
19952 match request.method.as_str() {
19953 "net.http_listen" => {
19954 check_network_resource_limit(
19955 resource_limits.max_sockets,
19956 network_counts.sockets,
19957 1,
19958 "socket",
19959 )?;
19960 let payload_json =
19961 javascript_sync_rpc_arg_str(&request.args, 0, "net.http_listen payload")?;
19962 let payload: JavascriptHttpListenRequest =
19963 serde_json::from_str(payload_json).map_err(|error| {
19964 SidecarError::InvalidState(format!(
19965 "net.http_listen payload must be valid JSON: {error}"
19966 ))
19967 })?;
19968 let (family, bind_host, guest_host) =
19969 normalize_tcp_listen_host(payload.hostname.as_deref())?;
19970 let requested_port = payload.port.unwrap_or(0);
19971 bridge.require_network_access(
19972 vm_id,
19973 NetworkOperation::Listen,
19974 format_tcp_resource(bind_host, requested_port),
19975 )?;
19976 let port = allocate_guest_listen_port(
19977 requested_port,
19978 family,
19979 &socket_paths.used_tcp_guest_ports,
19980 socket_paths.listen_policy,
19981 )?;
19982 let mut listener = ActiveTcpListener::bind(
19983 bind_host,
19984 guest_host,
19985 port,
19986 Some(DEFAULT_JAVASCRIPT_NET_BACKLOG),
19987 )?;
19988 let guest_local_addr = listener.guest_local_addr();
19989 process.http_servers.insert(
19990 payload.server_id,
19991 ActiveHttpServer {
19992 listener: listener.listener.take().ok_or_else(|| {
19993 SidecarError::InvalidState(String::from(
19994 "HTTP listener missing host TCP socket",
19995 ))
19996 })?,
19997 guest_local_addr,
19998 next_request_id: 0,
19999 },
20000 );
20001 serde_json::to_string(&json!({
20002 "address": {
20003 "address": guest_local_addr.ip().to_string(),
20004 "family": socket_addr_family(&guest_local_addr),
20005 "port": guest_local_addr.port(),
20006 }
20007 }))
20008 .map(Value::String)
20009 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
20010 }
20011 "net.http_close" => {
20012 let server_id =
20013 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_close server id")?;
20014 let server = process.http_servers.remove(&server_id).ok_or_else(|| {
20015 SidecarError::InvalidState(format!("unknown HTTP server {server_id}"))
20016 })?;
20017 drop(server.listener);
20018 process
20019 .pending_http_requests
20020 .retain(|(pending_server_id, _), _| *pending_server_id != server_id);
20021 Ok(Value::Null)
20022 }
20023 "net.http_wait" => {
20024 let server_id =
20025 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_wait server id")?;
20026 dispatch_http_wait_loop(process, server_id)
20027 }
20028 "net.http_respond" => {
20029 let server_id =
20030 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_respond server id")?;
20031 let request_id =
20032 javascript_sync_rpc_arg_u64(&request.args, 1, "net.http_respond request id")?;
20033 let response_json =
20034 javascript_sync_rpc_arg_str(&request.args, 2, "net.http_respond payload")?;
20035 ensure_vm_fetch_response_within_limit(
20036 response_json,
20037 "net.http_respond",
20038 VM_FETCH_BUFFER_LIMIT_BYTES,
20039 )?;
20040 serde_json::from_str::<Value>(response_json).map_err(|error| {
20041 SidecarError::Execution(format!(
20042 "net.http_respond payload must be valid JSON: {error}"
20043 ))
20044 })?;
20045 let Some(pending) = process
20046 .pending_http_requests
20047 .get_mut(&(server_id, request_id))
20048 else {
20049 return Err(SidecarError::InvalidState(format!(
20050 "unknown pending HTTP request {request_id} for server {server_id}"
20051 )));
20052 };
20053 *pending = Some(response_json.to_owned());
20054 Ok(Value::Null)
20055 }
20056 "net.reserve_tcp_port" => {
20057 let payload = request
20058 .args
20059 .first()
20060 .cloned()
20061 .ok_or_else(|| {
20062 SidecarError::InvalidState(String::from(
20063 "net.reserve_tcp_port requires a request payload",
20064 ))
20065 })
20066 .and_then(|value| {
20067 serde_json::from_value::<JavascriptNetReserveTcpPortRequest>(value).map_err(
20068 |error| {
20069 SidecarError::InvalidState(format!(
20070 "invalid net.reserve_tcp_port payload: {error}"
20071 ))
20072 },
20073 )
20074 })?;
20075 let (family, _bind_host, guest_host) =
20076 normalize_tcp_listen_host(payload.host.as_deref())?;
20077 let requested_port = payload.port.unwrap_or(0);
20078 let port = allocate_guest_listen_port(
20079 requested_port,
20080 family,
20081 &socket_paths.used_tcp_guest_ports,
20082 socket_paths.listen_policy,
20083 )?;
20084 let reservation_id = process.allocate_tcp_port_reservation_id();
20085 process
20086 .tcp_port_reservations
20087 .insert(reservation_id.clone(), (family, port));
20088 Ok(json!({
20089 "reservationId": reservation_id,
20090 "localAddress": guest_host,
20091 "localPort": port,
20092 "family": match family {
20093 JavascriptSocketFamily::Ipv4 => "IPv4",
20094 JavascriptSocketFamily::Ipv6 => "IPv6",
20095 },
20096 }))
20097 }
20098 "net.release_tcp_port" => {
20099 let reservation_id =
20100 javascript_sync_rpc_arg_str(&request.args, 0, "net.release_tcp_port reservation")?;
20101 process.tcp_port_reservations.remove(reservation_id);
20102 Ok(Value::Null)
20103 }
20104 "net.connect" => {
20105 check_network_resource_limit(
20106 resource_limits.max_sockets,
20107 network_counts.sockets,
20108 1,
20109 "socket",
20110 )?;
20111 check_network_resource_limit(
20112 resource_limits.max_connections,
20113 network_counts.connections,
20114 1,
20115 "connection",
20116 )?;
20117 let payload = request
20118 .args
20119 .first()
20120 .cloned()
20121 .ok_or_else(|| {
20122 SidecarError::InvalidState(String::from(
20123 "net.connect requires a request payload",
20124 ))
20125 })
20126 .and_then(|value| {
20127 serde_json::from_value::<JavascriptNetConnectRequest>(value).map_err(|error| {
20128 SidecarError::InvalidState(format!("invalid net.connect payload: {error}"))
20129 })
20130 })?;
20131 if let Some(path) = payload.path.as_deref() {
20132 let guest_path = normalize_path(path);
20133 let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
20134 let socket = ActiveUnixSocket::connect(&host_path, &guest_path)?;
20135 let socket_id = process.allocate_unix_socket_id();
20136 process.unix_sockets.insert(socket_id.clone(), socket);
20137 Ok(json!({
20138 "socketId": socket_id,
20139 "remotePath": guest_path,
20140 }))
20141 } else {
20142 let port = payload.port.ok_or_else(|| {
20143 SidecarError::InvalidState(String::from(
20144 "net.connect requires either a path or port",
20145 ))
20146 })?;
20147 let host = payload.host.as_deref().unwrap_or("localhost");
20148 let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
20149 process
20150 .tcp_port_reservations
20151 .remove(id)
20152 .map(|reservation| (id.to_owned(), reservation))
20153 });
20154 bridge.require_network_access(
20155 vm_id,
20156 NetworkOperation::Http,
20157 format_tcp_resource(host, port),
20158 )?;
20159 if is_loopback_socket_host(host) {
20160 let families = [JavascriptSocketFamily::Ipv4, JavascriptSocketFamily::Ipv6];
20161 if let Some((family, target)) = families.iter().find_map(|family| {
20162 socket_paths
20163 .http_loopback_target(*family, port)
20164 .map(|target| (*family, target))
20165 }) {
20166 if let Some((reservation_id, reservation)) = local_reservation {
20167 process
20168 .tcp_port_reservations
20169 .insert(reservation_id, reservation);
20170 }
20171 let remote_address = match family {
20172 JavascriptSocketFamily::Ipv4 => "127.0.0.1",
20173 JavascriptSocketFamily::Ipv6 => "::1",
20174 };
20175 return Ok(json!({
20176 "loopbackHttpTarget": {
20177 "processId": target.process_id.clone(),
20178 "serverId": target.server_id,
20179 "host": remote_address,
20180 "port": port,
20181 },
20182 "localAddress": match family {
20183 JavascriptSocketFamily::Ipv4 => "127.0.0.1",
20184 JavascriptSocketFamily::Ipv6 => "::1",
20185 },
20186 "localPort": payload.local_port.unwrap_or(0),
20187 "remoteAddress": remote_address,
20188 "remotePort": port,
20189 "remoteFamily": match family {
20190 JavascriptSocketFamily::Ipv4 => "IPv4",
20191 JavascriptSocketFamily::Ipv6 => "IPv6",
20192 },
20193 }));
20194 }
20195 }
20196 let connect_result = ActiveTcpSocket::connect(ActiveTcpConnectRequest {
20197 bridge,
20198 kernel,
20199 kernel_pid: process.kernel_pid,
20200 vm_id,
20201 dns,
20202 host,
20203 port,
20204 local_address: payload.local_address.as_deref(),
20205 local_port: payload.local_port,
20206 local_reservation: local_reservation
20207 .as_ref()
20208 .map(|(_, reservation)| *reservation),
20209 context: socket_paths,
20210 });
20211 if let Err(error) = connect_result {
20212 if let Some((reservation_id, reservation)) = local_reservation {
20213 process
20214 .tcp_port_reservations
20215 .insert(reservation_id, reservation);
20216 }
20217 return Err(error);
20218 }
20219 let socket = connect_result?;
20220 let socket_id = process.allocate_tcp_socket_id();
20221 let local_addr = socket.guest_local_addr;
20222 let remote_addr = socket.guest_remote_addr;
20223 process.tcp_sockets.insert(socket_id.clone(), socket);
20224 Ok(json!({
20225 "socketId": socket_id,
20226 "localAddress": local_addr.ip().to_string(),
20227 "localPort": local_addr.port(),
20228 "remoteAddress": remote_addr.ip().to_string(),
20229 "remotePort": remote_addr.port(),
20230 "remoteFamily": socket_addr_family(&remote_addr),
20231 }))
20232 }
20233 }
20234 "net.listen" => {
20235 check_network_resource_limit(
20236 resource_limits.max_sockets,
20237 network_counts.sockets,
20238 1,
20239 "socket",
20240 )?;
20241 let payload = request
20242 .args
20243 .first()
20244 .cloned()
20245 .ok_or_else(|| {
20246 SidecarError::InvalidState(String::from(
20247 "net.listen requires a request payload",
20248 ))
20249 })
20250 .and_then(|value| match value {
20251 Value::String(json) => {
20252 serde_json::from_str::<JavascriptNetListenRequest>(&json).map_err(|error| {
20253 SidecarError::InvalidState(format!(
20254 "invalid net.listen payload: {error}"
20255 ))
20256 })
20257 }
20258 other => serde_json::from_value::<JavascriptNetListenRequest>(other).map_err(
20259 |error| {
20260 SidecarError::InvalidState(format!(
20261 "invalid net.listen payload: {error}"
20262 ))
20263 },
20264 ),
20265 })?;
20266 if let Some(path) = payload.path.as_deref() {
20267 let guest_path = normalize_path(path);
20268 if kernel.exists(&guest_path).map_err(kernel_error)? {
20269 return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
20270 libc::EADDRINUSE,
20271 )));
20272 }
20273
20274 let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
20275 let on_host_mount =
20276 host_mount_path_for_guest_path_from_mounts(&socket_paths.mounts, &guest_path)
20277 .is_some();
20278 let listener = ActiveUnixListener::bind(&host_path, &guest_path, payload.backlog)?;
20279 if !on_host_mount {
20280 ensure_kernel_parent_directories(kernel, &guest_path)?;
20281 kernel
20282 .write_file(&guest_path, Vec::new())
20283 .map_err(kernel_error)?;
20284 }
20285 let listener_id = process.allocate_unix_listener_id();
20286 process.unix_listeners.insert(listener_id.clone(), listener);
20287 Ok(json!({
20288 "serverId": listener_id,
20289 "path": guest_path,
20290 }))
20291 } else {
20292 let (family, bind_host, guest_host) =
20293 normalize_tcp_listen_host(payload.host.as_deref())?;
20294 let requested_port = payload.port.unwrap_or(0);
20295 bridge.require_network_access(
20296 vm_id,
20297 NetworkOperation::Listen,
20298 format_tcp_resource(bind_host, requested_port),
20299 )?;
20300 let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
20301 process
20302 .tcp_port_reservations
20303 .remove(id)
20304 .map(|reservation| (id.to_owned(), reservation))
20305 });
20306 let port = if requested_port != 0
20307 && local_reservation
20308 .as_ref()
20309 .map(|(_, reservation)| *reservation)
20310 == Some((family, requested_port))
20311 {
20312 requested_port
20313 } else {
20314 allocate_guest_listen_port(
20315 requested_port,
20316 family,
20317 &socket_paths.used_tcp_guest_ports,
20318 socket_paths.listen_policy,
20319 )?
20320 };
20321 let listener_result = ActiveTcpListener::bind_kernel(
20322 kernel,
20323 process.kernel_pid,
20324 guest_host,
20325 port,
20326 payload.backlog,
20327 );
20328 if let Err(error) = listener_result {
20329 if let Some((reservation_id, reservation)) = local_reservation {
20330 process
20331 .tcp_port_reservations
20332 .insert(reservation_id, reservation);
20333 }
20334 return Err(error);
20335 }
20336 let listener = listener_result?;
20337 let listener_id = process.allocate_tcp_listener_id();
20338 let local_addr = listener.guest_local_addr();
20339 process.tcp_listeners.insert(listener_id.clone(), listener);
20340 Ok(json!({
20341 "serverId": listener_id,
20342 "localAddress": local_addr.ip().to_string(),
20343 "localPort": local_addr.port(),
20344 "family": socket_addr_family(&local_addr),
20345 }))
20346 }
20347 }
20348 "net.poll" => {
20349 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.poll socket id")?;
20350 let wait_ms =
20351 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.poll wait ms")?
20352 .unwrap_or_default();
20353 let wait = clamp_javascript_net_poll_wait(wait_ms);
20354 let event = if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20355 socket.poll(kernel, process.kernel_pid, wait)?
20356 } else if let Some(socket) = process.unix_sockets.get_mut(socket_id) {
20357 socket.poll(wait)?
20358 } else {
20359 return Err(SidecarError::InvalidState(format!(
20360 "unknown net socket {socket_id}"
20361 )));
20362 };
20363
20364 match event {
20365 Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(json!({
20366 "type": "data",
20367 "data": javascript_sync_rpc_bytes_value(&chunk),
20368 })),
20369 Some(JavascriptTcpSocketEvent::End) => Ok(json!({
20370 "type": "end",
20371 })),
20372 Some(JavascriptTcpSocketEvent::Error { code, message }) => Ok(json!({
20373 "type": "error",
20374 "code": code,
20375 "message": message,
20376 })),
20377 Some(JavascriptTcpSocketEvent::Close { had_error }) => {
20378 if let Some(socket) = process.tcp_sockets.remove(socket_id) {
20379 if let Some(listener_id) = socket.listener_id.as_deref() {
20380 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20381 listener.release_connection(socket_id);
20382 }
20383 }
20384 } else if let Some(socket) = process.unix_sockets.remove(socket_id) {
20385 if let Some(listener_id) = socket.listener_id.as_deref() {
20386 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20387 listener.release_connection(socket_id);
20388 }
20389 }
20390 }
20391 Ok(json!({
20392 "type": "close",
20393 "hadError": had_error,
20394 }))
20395 }
20396 None => Ok(Value::Null),
20397 }
20398 }
20399 "net.socket_wait_connect" => {
20400 let socket_id =
20401 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_wait_connect socket id")?;
20402 if let Some(socket) = process.tcp_sockets.get(socket_id) {
20403 javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
20404 } else {
20405 let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20406 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20407 })?;
20408 javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
20409 }
20410 }
20411 "net.socket_read" => {
20412 let socket_id =
20413 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_read socket id")?;
20414 if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20415 javascript_net_read_value(socket.poll(
20416 kernel,
20417 process.kernel_pid,
20418 Duration::ZERO,
20419 )?)
20420 } else {
20421 let socket = process.unix_sockets.get_mut(socket_id).ok_or_else(|| {
20422 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20423 })?;
20424 javascript_net_read_value(socket.poll(Duration::ZERO)?)
20425 }
20426 }
20427 "net.socket_set_no_delay" => {
20428 let socket_id =
20429 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_set_no_delay socket id")?;
20430 let enable =
20431 javascript_sync_rpc_arg_bool(&request.args, 1, "net.socket_set_no_delay enabled")?;
20432 if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20433 socket.set_no_delay(enable)?;
20434 } else if !process.unix_sockets.contains_key(socket_id) {
20435 return Err(SidecarError::InvalidState(format!(
20436 "unknown net socket {socket_id}"
20437 )));
20438 }
20439 Ok(Value::Null)
20440 }
20441 "net.socket_set_keep_alive" => {
20442 let socket_id = javascript_sync_rpc_arg_str(
20443 &request.args,
20444 0,
20445 "net.socket_set_keep_alive socket id",
20446 )?;
20447 let enable = javascript_sync_rpc_arg_bool(
20448 &request.args,
20449 1,
20450 "net.socket_set_keep_alive enabled",
20451 )?;
20452 let initial_delay_secs = javascript_sync_rpc_arg_u64_optional(
20453 &request.args,
20454 2,
20455 "net.socket_set_keep_alive initial delay seconds",
20456 )?;
20457 if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20458 socket.set_keep_alive(enable, initial_delay_secs)?;
20459 } else if !process.unix_sockets.contains_key(socket_id) {
20460 return Err(SidecarError::InvalidState(format!(
20461 "unknown net socket {socket_id}"
20462 )));
20463 }
20464 Ok(Value::Null)
20465 }
20466 "net.socket_upgrade_tls" => {
20467 let socket_id =
20468 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_upgrade_tls socket id")?;
20469 let options_json =
20470 javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_upgrade_tls options")?;
20471 let options: JavascriptTlsBridgeOptions =
20472 serde_json::from_str(options_json).map_err(|error| {
20473 SidecarError::InvalidState(format!(
20474 "net.socket_upgrade_tls options must be valid JSON: {error}"
20475 ))
20476 })?;
20477 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20478 SidecarError::InvalidState(format!(
20479 "unknown TCP socket {socket_id} for TLS upgrade"
20480 ))
20481 })?;
20482 socket.upgrade_tls(vm_id, kernel, options)?;
20483 Ok(Value::Null)
20484 }
20485 "net.socket_get_tls_client_hello" => {
20486 let socket_id = javascript_sync_rpc_arg_str(
20487 &request.args,
20488 0,
20489 "net.socket_get_tls_client_hello socket id",
20490 )?;
20491 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20492 SidecarError::InvalidState(format!(
20493 "unknown TCP socket {socket_id} for TLS client hello query"
20494 ))
20495 })?;
20496 socket.tls_client_hello_json(vm_id, kernel)
20497 }
20498 "net.socket_tls_query" => {
20499 let socket_id =
20500 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_tls_query socket id")?;
20501 let query =
20502 javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_tls_query query")?;
20503 let detailed = request
20504 .args
20505 .get(2)
20506 .and_then(Value::as_bool)
20507 .unwrap_or(false);
20508 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20509 SidecarError::InvalidState(format!("unknown TCP socket {socket_id} for TLS query"))
20510 })?;
20511 socket.tls_query(query, detailed)
20512 }
20513 "net.server_poll" => {
20514 let listener_id =
20515 javascript_sync_rpc_arg_str(&request.args, 0, "net.server_poll listener id")?;
20516 let wait_ms =
20517 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.server_poll wait ms")?
20518 .unwrap_or_default();
20519 let tcp_event = if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20520 Some(listener.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?)
20521 } else {
20522 None
20523 };
20524
20525 if let Some(event) = tcp_event {
20526 return match event {
20527 Some(JavascriptTcpListenerEvent::Connection(pending)) => {
20528 let PendingTcpSocket {
20529 stream,
20530 kernel_socket_id,
20531 preallocated,
20532 guest_local_addr,
20533 guest_remote_addr,
20534 } = pending;
20535 if !preallocated {
20536 if let Err(error) = check_network_resource_limit(
20537 resource_limits.max_sockets,
20538 network_counts.sockets,
20539 1,
20540 "socket",
20541 )
20542 .and_then(|()| {
20543 check_network_resource_limit(
20544 resource_limits.max_connections,
20545 network_counts.connections,
20546 1,
20547 "connection",
20548 )
20549 }) {
20550 if let Some(stream) = stream {
20551 let _ = stream.shutdown(Shutdown::Both);
20552 }
20553 return Ok(json!({
20554 "type": "error",
20555 "code": "EAGAIN",
20556 "message": error.to_string(),
20557 }));
20558 }
20559 }
20560 let socket = if let Some(stream) = stream {
20561 ActiveTcpSocket::from_stream(
20562 stream,
20563 Some(listener_id.to_string()),
20564 guest_local_addr,
20565 guest_remote_addr,
20566 )?
20567 } else {
20568 ActiveTcpSocket::from_kernel(
20569 kernel_socket_id.ok_or_else(|| {
20570 SidecarError::InvalidState(String::from(
20571 "kernel TCP accept missing socket id",
20572 ))
20573 })?,
20574 Some(listener_id.to_string()),
20575 guest_local_addr,
20576 guest_remote_addr,
20577 )
20578 };
20579 let socket_id = process.allocate_tcp_socket_id();
20580 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20581 listener.register_connection(&socket_id);
20582 }
20583 process.tcp_sockets.insert(socket_id.clone(), socket);
20584 Ok(json!({
20585 "type": "connection",
20586 "socketId": socket_id,
20587 "localAddress": guest_local_addr.ip().to_string(),
20588 "localPort": guest_local_addr.port(),
20589 "remoteAddress": guest_remote_addr.ip().to_string(),
20590 "remotePort": guest_remote_addr.port(),
20591 "remoteFamily": socket_addr_family(&guest_remote_addr),
20592 }))
20593 }
20594 Some(JavascriptTcpListenerEvent::Error { code, message }) => Ok(json!({
20595 "type": "error",
20596 "code": code,
20597 "message": message,
20598 })),
20599 None => Ok(Value::Null),
20600 };
20601 }
20602
20603 let event = {
20604 let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
20605 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20606 })?;
20607 listener.poll(Duration::from_millis(wait_ms))?
20608 };
20609
20610 match event {
20611 Some(JavascriptUnixListenerEvent::Connection(pending)) => {
20612 if let Err(error) = check_network_resource_limit(
20613 resource_limits.max_sockets,
20614 network_counts.sockets,
20615 1,
20616 "socket",
20617 )
20618 .and_then(|()| {
20619 check_network_resource_limit(
20620 resource_limits.max_connections,
20621 network_counts.connections,
20622 1,
20623 "connection",
20624 )
20625 }) {
20626 let _ = pending.stream.shutdown(Shutdown::Both);
20627 return Ok(json!({
20628 "type": "error",
20629 "code": "EAGAIN",
20630 "message": error.to_string(),
20631 }));
20632 }
20633 let socket = ActiveUnixSocket::from_stream(
20634 pending.stream,
20635 Some(listener_id.to_string()),
20636 pending.local_path.clone(),
20637 pending.remote_path.clone(),
20638 )?;
20639 let socket_id = process.allocate_unix_socket_id();
20640 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20641 listener.register_connection(&socket_id);
20642 }
20643 process.unix_sockets.insert(socket_id.clone(), socket);
20644 Ok(json!({
20645 "type": "connection",
20646 "socketId": socket_id,
20647 "localPath": pending.local_path,
20648 "remotePath": pending.remote_path,
20649 }))
20650 }
20651 Some(JavascriptUnixListenerEvent::Error { code, message }) => Ok(json!({
20652 "type": "error",
20653 "code": code,
20654 "message": message,
20655 })),
20656 None => Ok(Value::Null),
20657 }
20658 }
20659 "net.server_accept" => {
20660 let listener_id =
20661 javascript_sync_rpc_arg_str(&request.args, 0, "net.server_accept listener id")?;
20662 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20663 return match listener.poll(kernel, process.kernel_pid, Duration::ZERO)? {
20664 Some(JavascriptTcpListenerEvent::Connection(pending)) => {
20665 let PendingTcpSocket {
20666 stream,
20667 kernel_socket_id,
20668 preallocated,
20669 guest_local_addr,
20670 guest_remote_addr,
20671 } = pending;
20672 if !preallocated {
20673 check_network_resource_limit(
20674 resource_limits.max_sockets,
20675 network_counts.sockets,
20676 1,
20677 "socket",
20678 )?;
20679 check_network_resource_limit(
20680 resource_limits.max_connections,
20681 network_counts.connections,
20682 1,
20683 "connection",
20684 )?;
20685 }
20686 let info = json!({
20687 "localAddress": guest_local_addr.ip().to_string(),
20688 "localPort": guest_local_addr.port(),
20689 "localFamily": socket_addr_family(&guest_local_addr),
20690 "remoteAddress": guest_remote_addr.ip().to_string(),
20691 "remotePort": guest_remote_addr.port(),
20692 "remoteFamily": socket_addr_family(&guest_remote_addr),
20693 });
20694 let socket = if let Some(stream) = stream {
20695 ActiveTcpSocket::from_stream(
20696 stream,
20697 Some(listener_id.to_string()),
20698 guest_local_addr,
20699 guest_remote_addr,
20700 )?
20701 } else {
20702 ActiveTcpSocket::from_kernel(
20703 kernel_socket_id.ok_or_else(|| {
20704 SidecarError::InvalidState(String::from(
20705 "kernel TCP accept missing socket id",
20706 ))
20707 })?,
20708 Some(listener_id.to_string()),
20709 guest_local_addr,
20710 guest_remote_addr,
20711 )
20712 };
20713 let socket_id = process.allocate_tcp_socket_id();
20714 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20715 listener.register_connection(&socket_id);
20716 }
20717 process.tcp_sockets.insert(socket_id.clone(), socket);
20718 javascript_net_json_string(
20719 json!({
20720 "socketId": socket_id,
20721 "info": info,
20722 }),
20723 "net.server_accept",
20724 )
20725 }
20726 Some(JavascriptTcpListenerEvent::Error { code, message }) => {
20727 let detail = code.unwrap_or_else(|| String::from("server accept"));
20728 Err(SidecarError::Execution(format!("{detail}: {message}")))
20729 }
20730 None => Ok(javascript_net_timeout_value()),
20731 };
20732 }
20733
20734 let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
20735 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20736 })?;
20737 match listener.poll(Duration::ZERO)? {
20738 Some(JavascriptUnixListenerEvent::Connection(pending)) => {
20739 check_network_resource_limit(
20740 resource_limits.max_sockets,
20741 network_counts.sockets,
20742 1,
20743 "socket",
20744 )?;
20745 check_network_resource_limit(
20746 resource_limits.max_connections,
20747 network_counts.connections,
20748 1,
20749 "connection",
20750 )?;
20751 let info = json!({
20752 "localPath": pending.local_path.clone(),
20753 "remotePath": pending.remote_path.clone(),
20754 });
20755 let socket = ActiveUnixSocket::from_stream(
20756 pending.stream,
20757 Some(listener_id.to_string()),
20758 pending.local_path,
20759 pending.remote_path,
20760 )?;
20761 let socket_id = process.allocate_unix_socket_id();
20762 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20763 listener.register_connection(&socket_id);
20764 }
20765 process.unix_sockets.insert(socket_id.clone(), socket);
20766 javascript_net_json_string(
20767 json!({
20768 "socketId": socket_id,
20769 "info": info,
20770 }),
20771 "net.server_accept",
20772 )
20773 }
20774 Some(JavascriptUnixListenerEvent::Error { code, message }) => {
20775 let detail = code.unwrap_or_else(|| String::from("server accept"));
20776 Err(SidecarError::Execution(format!("{detail}: {message}")))
20777 }
20778 None => Ok(javascript_net_timeout_value()),
20779 }
20780 }
20781 "net.server_connections" => {
20782 let listener_id = javascript_sync_rpc_arg_str(
20783 &request.args,
20784 0,
20785 "net.server_connections listener id",
20786 )?;
20787 if let Some(listener) = process.tcp_listeners.get(listener_id) {
20788 Ok(json!(listener.active_connection_count()))
20789 } else {
20790 let listener = process.unix_listeners.get(listener_id).ok_or_else(|| {
20791 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20792 })?;
20793 Ok(json!(listener.active_connection_count()))
20794 }
20795 }
20796 "net.upgrade_socket_write" => {
20797 let socket_id = javascript_sync_rpc_arg_str(
20798 &request.args,
20799 0,
20800 "net.upgrade_socket_write socket id",
20801 )?;
20802 let chunk =
20803 javascript_sync_rpc_base64_arg(&request.args, 1, "net.upgrade_socket_write chunk")?;
20804 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20805 SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20806 })?;
20807 socket
20808 .write_all(kernel, process.kernel_pid, &chunk)
20809 .map(|written| json!(written))
20810 }
20811 "net.upgrade_socket_end" => {
20812 let socket_id =
20813 javascript_sync_rpc_arg_str(&request.args, 0, "net.upgrade_socket_end socket id")?;
20814 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20815 SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20816 })?;
20817 socket.shutdown_write(kernel, process.kernel_pid)?;
20818 Ok(Value::Null)
20819 }
20820 "net.upgrade_socket_destroy" => {
20821 let socket_id = javascript_sync_rpc_arg_str(
20822 &request.args,
20823 0,
20824 "net.upgrade_socket_destroy socket id",
20825 )?;
20826 let socket = process.tcp_sockets.remove(socket_id).ok_or_else(|| {
20827 SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20828 })?;
20829 if let Some(listener_id) = socket.listener_id.as_deref() {
20830 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20831 listener.release_connection(socket_id);
20832 }
20833 }
20834 let _ = socket.close(kernel, process.kernel_pid);
20835 Ok(Value::Null)
20836 }
20837 "net.write" => {
20838 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.write socket id")?;
20839 let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "net.write chunk")?;
20840 if let Some(socket) = process.tcp_sockets.get(socket_id) {
20841 socket
20842 .write_all(kernel, process.kernel_pid, &chunk)
20843 .map(|written| json!(written))
20844 } else {
20845 let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20846 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20847 })?;
20848 socket.write_all(&chunk).map(|written| json!(written))
20849 }
20850 }
20851 "net.shutdown" => {
20852 let socket_id =
20853 javascript_sync_rpc_arg_str(&request.args, 0, "net.shutdown socket id")?;
20854 if let Some(socket) = process.tcp_sockets.get(socket_id) {
20855 socket.shutdown_write(kernel, process.kernel_pid)?;
20856 } else {
20857 let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20858 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20859 })?;
20860 socket.shutdown_write()?;
20861 }
20862 Ok(Value::Null)
20863 }
20864 "net.destroy" => {
20865 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.destroy socket id")?;
20866 if let Some(socket) = process.tcp_sockets.remove(socket_id) {
20867 if let Some(listener_id) = socket.listener_id.as_deref() {
20868 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20869 listener.release_connection(socket_id);
20870 }
20871 }
20872 let _ = socket.close(kernel, process.kernel_pid);
20873 Ok(Value::Null)
20874 } else {
20875 let socket = process.unix_sockets.remove(socket_id).ok_or_else(|| {
20876 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20877 })?;
20878 if let Some(listener_id) = socket.listener_id.as_deref() {
20879 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20880 listener.release_connection(socket_id);
20881 }
20882 }
20883 let _ = socket.close();
20884 Ok(Value::Null)
20885 }
20886 }
20887 "net.server_close" => {
20888 let listener_id =
20889 javascript_sync_rpc_arg_str(&request.args, 0, "net.server_close listener id")?;
20890 if let Some(listener) = process.tcp_listeners.remove(listener_id) {
20891 listener.close(kernel, process.kernel_pid)?;
20892 Ok(Value::Null)
20893 } else {
20894 let listener = process.unix_listeners.remove(listener_id).ok_or_else(|| {
20895 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20896 })?;
20897 listener.close()?;
20898 Ok(Value::Null)
20899 }
20900 }
20901 "tls.get_ciphers" => javascript_net_json_string(
20902 Value::Array(
20903 tls_provider()
20904 .cipher_suites
20905 .iter()
20906 .filter_map(|suite| {
20907 suite
20908 .suite()
20909 .as_str()
20910 .map(|value| Value::String(value.to_owned()))
20911 })
20912 .collect(),
20913 ),
20914 "tls.get_ciphers",
20915 ),
20916 _ => Err(SidecarError::InvalidState(format!(
20917 "unsupported JavaScript net sync RPC method {}",
20918 request.method
20919 ))),
20920 }
20921}
20922
20923fn signal_name_for_stream_event(signal: i32) -> Option<&'static str> {
20924 match signal {
20925 libc::SIGHUP => Some("SIGHUP"),
20926 libc::SIGINT => Some("SIGINT"),
20927 libc::SIGUSR1 => Some("SIGUSR1"),
20928 libc::SIGALRM => Some("SIGALRM"),
20929 libc::SIGCONT => Some("SIGCONT"),
20930 libc::SIGTERM => Some("SIGTERM"),
20931 libc::SIGCHLD => Some("SIGCHLD"),
20932 libc::SIGWINCH => Some("SIGWINCH"),
20933 _ => None,
20934 }
20935}
20936
20937pub(crate) fn canonical_signal_name(signal: i32) -> Option<&'static str> {
20938 match signal {
20939 1 => Some("SIGHUP"),
20940 2 => Some("SIGINT"),
20941 3 => Some("SIGQUIT"),
20942 4 => Some("SIGILL"),
20943 5 => Some("SIGTRAP"),
20944 6 => Some("SIGABRT"),
20945 7 => Some("SIGBUS"),
20946 8 => Some("SIGFPE"),
20947 9 => Some("SIGKILL"),
20948 10 => Some("SIGUSR1"),
20949 11 => Some("SIGSEGV"),
20950 12 => Some("SIGUSR2"),
20951 13 => Some("SIGPIPE"),
20952 14 => Some("SIGALRM"),
20953 15 => Some("SIGTERM"),
20954 17 => Some("SIGCHLD"),
20955 18 => Some("SIGCONT"),
20956 19 => Some("SIGSTOP"),
20957 20 => Some("SIGTSTP"),
20958 21 => Some("SIGTTIN"),
20959 22 => Some("SIGTTOU"),
20960 23 => Some("SIGURG"),
20961 24 => Some("SIGXCPU"),
20962 25 => Some("SIGXFSZ"),
20963 26 => Some("SIGVTALRM"),
20964 27 => Some("SIGPROF"),
20965 28 => Some("SIGWINCH"),
20966 29 => Some("SIGIO"),
20967 30 => Some("SIGPWR"),
20968 31 => Some("SIGSYS"),
20969 _ => None,
20970 }
20971}
20972
20973fn dispatch_v8_process_signal(process: &ActiveProcess, signal: i32) -> Result<bool, SidecarError> {
20974 let Some(signal_name) = signal_name_for_stream_event(signal) else {
20975 return Ok(false);
20976 };
20977 process.execution.send_javascript_stream_event(
20978 "signal",
20979 json!({
20980 "signal": signal_name,
20981 "number": signal,
20982 "action": "default",
20983 }),
20984 )?;
20985 Ok(true)
20986}
20987
20988fn dispatch_v8_session_signal_async(session: V8SessionHandle, signal: i32) {
20989 let Some(signal_name) = signal_name_for_stream_event(signal).map(str::to_owned) else {
20990 return;
20991 };
20992 thread::spawn(move || {
20993 thread::sleep(Duration::from_millis(1));
20994 let payload = v8_runtime::json_to_cbor_payload(&json!({
20995 "signal": signal_name,
20996 "number": signal,
20997 "action": "default",
20998 }))
20999 .unwrap_or_default();
21000 let _ = session.send_stream_event("signal", payload);
21001 });
21002}
21003
21004pub(crate) fn parse_signal(signal: &str) -> Result<i32, SidecarError> {
21005 let trimmed = signal.trim();
21006 if trimmed.is_empty() {
21007 return Err(SidecarError::InvalidState(String::from(
21008 "kill_process requires a non-empty signal",
21009 )));
21010 }
21011
21012 if let Ok(value) = trimmed.parse::<i32>() {
21013 return match value {
21014 0..=31 => Ok(value),
21015 _ => Err(SidecarError::InvalidState(format!(
21016 "unsupported kill_process signal {signal}"
21017 ))),
21018 };
21019 }
21020
21021 let upper = trimmed.to_ascii_uppercase();
21022 let normalized = upper.strip_prefix("SIG").unwrap_or(&upper);
21023
21024 signal_number_from_name(normalized).ok_or_else(|| {
21025 SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
21026 })
21027}
21028
21029fn signal_number_from_name(signal: &str) -> Option<i32> {
21030 match signal {
21031 "0" => Some(0),
21032 "HUP" => Some(1),
21033 "INT" => Some(2),
21034 "QUIT" => Some(3),
21035 "ILL" => Some(4),
21036 "TRAP" => Some(5),
21037 "ABRT" | "IOT" => Some(6),
21038 "BUS" => Some(7),
21039 "FPE" => Some(8),
21040 "KILL" => Some(9),
21041 "USR1" => Some(10),
21042 "SEGV" => Some(11),
21043 "USR2" => Some(12),
21044 "PIPE" => Some(13),
21045 "ALRM" => Some(14),
21046 "TERM" => Some(15),
21047 "STKFLT" => Some(16),
21048 "CHLD" => Some(17),
21049 "CONT" => Some(18),
21050 "STOP" => Some(19),
21051 "TSTP" => Some(20),
21052 "TTIN" => Some(21),
21053 "TTOU" => Some(22),
21054 "URG" => Some(23),
21055 "XCPU" => Some(24),
21056 "XFSZ" => Some(25),
21057 "VTALRM" => Some(26),
21058 "PROF" => Some(27),
21059 "WINCH" => Some(28),
21060 "IO" | "POLL" => Some(29),
21061 "PWR" => Some(30),
21062 "SYS" => Some(31),
21063 _ => None,
21064 }
21065}
21066
21067pub(crate) fn runtime_child_is_alive(child_pid: u32) -> Result<bool, SidecarError> {
21068 Ok(runtime_child_exit_status(child_pid)?.is_none())
21069}
21070
21071#[cfg(not(target_os = "macos"))]
21072fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
21073 if child_pid == 0 {
21074 return Ok(Some(0));
21075 }
21076
21077 let wait_flags = WaitPidFlag::WNOHANG
21078 | WaitPidFlag::WNOWAIT
21079 | WaitPidFlag::WEXITED
21080 | WaitPidFlag::WUNTRACED
21081 | WaitPidFlag::WCONTINUED;
21082 match wait_on_child(WaitId::Pid(Pid::from_raw(child_pid as i32)), wait_flags) {
21083 Ok(WaitStatus::StillAlive)
21084 | Ok(WaitStatus::Stopped(_, _))
21085 | Ok(WaitStatus::Continued(_)) => Ok(None),
21086 Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
21087 Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
21088 #[cfg(any(target_os = "linux", target_os = "android"))]
21089 Ok(WaitStatus::PtraceEvent(_, _, _) | WaitStatus::PtraceSyscall(_)) => Ok(None),
21090 Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
21091 Err(error) => Err(SidecarError::Execution(format!(
21092 "failed to inspect guest runtime process {child_pid}: {error}"
21093 ))),
21094 }
21095}
21096
21097#[cfg(target_os = "macos")]
21103fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
21104 if child_pid == 0 {
21105 return Ok(Some(0));
21106 }
21107
21108 match waitpid(Pid::from_raw(child_pid as i32), Some(WaitPidFlag::WNOHANG)) {
21109 Ok(WaitStatus::StillAlive)
21110 | Ok(WaitStatus::Stopped(_, _))
21111 | Ok(WaitStatus::Continued(_)) => Ok(None),
21112 Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
21113 Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
21114 Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
21115 Err(error) => Err(SidecarError::Execution(format!(
21116 "failed to inspect guest runtime process {child_pid}: {error}"
21117 ))),
21118 }
21119}
21120
21121pub(crate) fn signal_runtime_process(child_pid: u32, signal: i32) -> Result<(), SidecarError> {
21122 if child_pid == 0 {
21123 return Ok(());
21124 }
21125
21126 if !runtime_child_is_alive(child_pid)? {
21127 return Ok(());
21128 }
21129
21130 if signal == 0 {
21131 return Ok(());
21132 }
21133
21134 let parsed = Signal::try_from(signal).map_err(|_| {
21135 SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
21136 })?;
21137 let result = send_signal(Pid::from_raw(child_pid as i32), Some(parsed));
21138
21139 match result {
21140 Ok(()) => Ok(()),
21141 Err(nix::errno::Errno::ESRCH) => Ok(()),
21142 Err(error) => Err(SidecarError::Execution(format!(
21143 "failed to signal guest runtime process {child_pid}: {error}"
21144 ))),
21145 }
21146}
21147
21148pub(crate) fn error_code(error: &SidecarError) -> &'static str {
21149 match error {
21150 SidecarError::InvalidState(_) => "invalid_state",
21151 SidecarError::ProtocolVersionMismatch(_) => "protocol_version_mismatch",
21152 SidecarError::BridgeVersionMismatch(_) => "bridge_version_mismatch",
21153 SidecarError::Conflict(_) => "conflict",
21154 SidecarError::Unauthorized(_) => "unauthorized",
21155 SidecarError::Unsupported(_) => "unsupported",
21156 SidecarError::FrameTooLarge(_) => "frame_too_large",
21157 SidecarError::Kernel(_) => "kernel_error",
21158 SidecarError::Plugin(_) => "plugin_error",
21159 SidecarError::Execution(_) => "execution_error",
21160 SidecarError::Bridge(_) => "bridge_error",
21161 SidecarError::Io(_) => "io_error",
21162 }
21163}
21164
21165fn guest_errno_code(message: &str) -> Option<&str> {
21166 const TRUSTED_PREFIXES: &[&str] = &[
21167 "ERR_AGENTOS_NODE_SYNC_RPC",
21168 "ERR_AGENTOS_PYTHON_VFS_RPC",
21169 "ERR_AGENTOS_BRIDGE",
21170 ];
21171
21172 let mut segments = message.split(':').map(str::trim);
21173 let first = segments.next()?;
21174 if is_guest_errno_segment(first) {
21175 return Some(first);
21176 }
21177
21178 if TRUSTED_PREFIXES.contains(&first) {
21179 let second = segments.next()?;
21180 if is_guest_errno_segment(second) {
21181 return Some(second);
21182 }
21183 }
21184
21185 None
21186}
21187
21188fn is_guest_errno_segment(segment: &str) -> bool {
21189 segment.len() >= 2
21190 && segment.starts_with('E')
21191 && !segment.starts_with("ERR_")
21192 && segment[1..]
21193 .bytes()
21194 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
21195}
21196
21197pub(crate) fn javascript_sync_rpc_error_code(error: &SidecarError) -> String {
21198 let message = error.to_string();
21199 if let Some(code) = guest_errno_code(&message) {
21200 return code.to_owned();
21201 }
21202 if message.starts_with("ERR_NATIVE_BINARY_NOT_SUPPORTED:") {
21203 return String::from("ERR_NATIVE_BINARY_NOT_SUPPORTED");
21204 }
21205
21206 let lower = message.to_ascii_lowercase();
21207 if lower.contains("no such file or directory")
21208 || lower.contains("entry not found")
21209 || lower.contains("not found")
21210 {
21211 return String::from("ENOENT");
21212 }
21213 if lower.contains("permission denied") {
21214 return String::from("EACCES");
21215 }
21216 if lower.contains("already exists")
21217 || lower.contains("already registered")
21218 || lower.contains("file exists")
21219 {
21220 return String::from("EEXIST");
21221 }
21222 if lower.contains("invalid argument") {
21223 return String::from("EINVAL");
21224 }
21225
21226 String::from("ERR_AGENTOS_NODE_SYNC_RPC")
21227}
21228
21229pub(crate) fn ignore_stale_javascript_sync_rpc_response(
21230 error: SidecarError,
21231) -> Result<(), SidecarError> {
21232 match error {
21233 SidecarError::Execution(message)
21234 if message.ends_with("is no longer pending")
21235 && message.starts_with("sync RPC request ") =>
21236 {
21237 Ok(())
21238 }
21239 SidecarError::Execution(message) => {
21240 let lower = message.to_ascii_lowercase();
21241 if lower.contains("sync rpc response")
21242 && (lower.contains("broken pipe") || lower.contains("channel closed unexpectedly"))
21243 {
21244 Ok(())
21245 } else {
21246 Err(SidecarError::Execution(message))
21247 }
21248 }
21249 other => Err(other),
21250 }
21251}
21252
21253#[cfg(test)]
21254mod error_code_tests {
21255 use super::{guest_errno_code, javascript_sync_rpc_error_code, SidecarError};
21256
21257 #[test]
21258 fn guest_errno_code_rejects_guest_controlled_errno_segments() {
21259 assert_eq!(guest_errno_code("user said 'EACCES: denied'"), None);
21260 assert_eq!(
21261 guest_errno_code("prefix: user said 'EPERM': more text"),
21262 None
21263 );
21264 assert_eq!(guest_errno_code("ERR_AGENTOS_FAKE: EACCES: denied"), None);
21265 }
21266
21267 #[test]
21268 fn guest_errno_code_accepts_trusted_secure_exec_prefixes() {
21269 assert_eq!(
21270 guest_errno_code("ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo"),
21271 Some("EACCES")
21272 );
21273 assert_eq!(
21274 guest_errno_code("ERR_AGENTOS_PYTHON_VFS_RPC: ENOENT: missing file"),
21275 Some("ENOENT")
21276 );
21277 assert_eq!(guest_errno_code("EEXIST: already exists"), Some("EEXIST"));
21278 }
21279
21280 #[test]
21281 fn javascript_sync_rpc_error_code_ignores_spoofed_errnos() {
21282 let error = SidecarError::Execution(String::from("user said 'EACCES: denied'"));
21283 assert_eq!(
21284 javascript_sync_rpc_error_code(&error),
21285 "ERR_AGENTOS_NODE_SYNC_RPC"
21286 );
21287 }
21288
21289 #[test]
21290 fn javascript_sync_rpc_error_code_preserves_real_sidecar_errnos() {
21291 let error = SidecarError::Execution(String::from(
21292 "ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo",
21293 ));
21294 assert_eq!(javascript_sync_rpc_error_code(&error), "EACCES");
21295 }
21296
21297 #[test]
21298 fn javascript_sync_rpc_error_code_maps_file_exists_messages() {
21299 let error = SidecarError::Io(String::from(
21300 "failed to create mapped guest directory /.next/server: File exists (os error 17)",
21301 ));
21302 assert_eq!(javascript_sync_rpc_error_code(&error), "EEXIST");
21303 }
21304
21305 #[test]
21306 fn javascript_sync_rpc_error_code_preserves_native_binary_rejections() {
21307 let error = SidecarError::Execution(String::from(
21308 "ERR_NATIVE_BINARY_NOT_SUPPORTED: refused to execute native ELF guest binary at /tmp/fake-rg inside the VM",
21309 ));
21310 assert_eq!(
21311 javascript_sync_rpc_error_code(&error),
21312 "ERR_NATIVE_BINARY_NOT_SUPPORTED"
21313 );
21314 }
21315}
21316#[cfg(test)]
21317mod ssrf_egress_classifier_tests {
21318 use super::{
21328 filter_dns_safe_ip_addrs, is_loopback_ip, restricted_non_loopback_ip_range, SidecarError,
21329 };
21330 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
21331
21332 fn assert_restricted(ip: IpAddr, expected_label: &str) {
21333 let classification = restricted_non_loopback_ip_range(ip);
21334 assert!(
21335 classification.is_some(),
21336 "{ip} must be classified as a restricted egress target"
21337 );
21338 let (_cidr, label) = classification.unwrap();
21339 assert_eq!(
21340 label, expected_label,
21341 "{ip} should be labelled {expected_label}, got {label}"
21342 );
21343 }
21344
21345 fn assert_dns_denied(ip: IpAddr, label: &str) {
21346 match filter_dns_safe_ip_addrs(vec![ip], "attacker.example") {
21347 Err(SidecarError::Execution(message)) => assert!(
21348 message.starts_with("EACCES:"),
21349 "{label}: egress filter must deny with EACCES, got: {message}"
21350 ),
21351 other => panic!("{label}: expected EACCES denial, got {other:?}"),
21352 }
21353 }
21354
21355 #[test]
21357 fn classifier_denies_unspecified_and_cgnat_targets() {
21358 assert_restricted(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "unspecified");
21360 assert_restricted(IpAddr::V6(Ipv6Addr::UNSPECIFIED), "unspecified");
21362
21363 assert_restricted(
21365 IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
21366 "carrier-grade-nat",
21367 );
21368 assert_restricted(
21369 IpAddr::V4(Ipv4Addr::new(100, 127, 255, 254)),
21370 "carrier-grade-nat",
21371 );
21372
21373 assert!(
21375 restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 63, 255, 255)))
21376 .is_none(),
21377 "100.63.255.255 is outside CGNAT and must remain allowed"
21378 );
21379 assert!(
21380 restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 128, 0, 0))).is_none(),
21381 "100.128.0.0 is outside CGNAT and must remain allowed"
21382 );
21383
21384 assert_dns_denied(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "0.0.0.0 (unspecified)");
21386 assert_dns_denied(IpAddr::V6(Ipv6Addr::UNSPECIFIED), ":: (unspecified)");
21387 assert_dns_denied(
21388 IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
21389 "100.64.0.1 (CGNAT)",
21390 );
21391 }
21392
21393 #[test]
21395 fn classifier_denies_ipv6_spelled_metadata_addresses() {
21396 let mapped = "::ffff:169.254.169.254".parse::<Ipv6Addr>().unwrap();
21399 assert_restricted(IpAddr::V6(mapped), "link-local");
21400
21401 let compat = "::169.254.169.254".parse::<Ipv6Addr>().unwrap();
21402 assert_restricted(IpAddr::V6(compat), "link-local");
21403
21404 assert_restricted(
21406 IpAddr::V6("::10.0.0.1".parse::<Ipv6Addr>().unwrap()),
21407 "private",
21408 );
21409 assert_restricted(
21410 IpAddr::V6("::100.64.0.1".parse::<Ipv6Addr>().unwrap()),
21411 "carrier-grade-nat",
21412 );
21413
21414 assert_eq!(
21418 restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::UNSPECIFIED)),
21419 Some(("::/128", "unspecified")),
21420 ":: must classify as unspecified, not via the IPv4-compat path"
21421 );
21422 assert!(
21423 restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::LOCALHOST)).is_none()
21424 || is_loopback_ip(IpAddr::V6(Ipv6Addr::LOCALHOST)),
21425 "::1 must not be classified as a restricted IPv4-compatible target"
21426 );
21427 assert!(
21428 restricted_non_loopback_ip_range(IpAddr::V6("::8.8.8.8".parse::<Ipv6Addr>().unwrap()))
21429 .is_none(),
21430 "::8.8.8.8 (public IPv4-compatible) must remain allowed"
21431 );
21432
21433 assert_dns_denied(
21435 IpAddr::V6("::169.254.169.254".parse::<Ipv6Addr>().unwrap()),
21436 "::169.254.169.254 (IPv4-compat metadata)",
21437 );
21438 }
21439
21440 #[test]
21442 fn classifier_denies_reserved_and_multicast_targets() {
21443 assert_restricted(IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)), "multicast");
21447 assert_restricted(IpAddr::V4(Ipv4Addr::new(239, 255, 255, 255)), "multicast");
21448 assert_restricted(IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)), "reserved");
21449 assert_restricted(IpAddr::V4(Ipv4Addr::BROADCAST), "reserved");
21451
21452 assert_restricted(
21454 IpAddr::V6("::224.0.0.1".parse::<Ipv6Addr>().unwrap()),
21455 "multicast",
21456 );
21457 assert_restricted(
21458 IpAddr::V6("::240.0.0.1".parse::<Ipv6Addr>().unwrap()),
21459 "reserved",
21460 );
21461
21462 assert!(
21464 restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(223, 255, 255, 255)))
21465 .is_none(),
21466 "223.255.255.255 is outside 224/4 and must remain allowed"
21467 );
21468
21469 assert_dns_denied(
21471 IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)),
21472 "240.0.0.1 (reserved)",
21473 );
21474 assert_dns_denied(
21475 IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)),
21476 "224.0.0.1 (multicast)",
21477 );
21478 }
21479}
21480
21481#[cfg(test)]
21490mod dns_rebinding_pin_tests {
21491 use super::{issue_outbound_http_request, split_netloc, JavascriptHttpRequestOptions};
21492 use std::collections::BTreeMap;
21493 use std::io::{Read, Write};
21494 use std::net::{IpAddr, Ipv4Addr, TcpListener};
21495 use std::thread;
21496 use url::Url;
21497
21498 fn empty_headers() -> super::HttpHeaderCollection {
21499 super::parse_http_header_collection(&BTreeMap::new(), "test headers")
21500 .expect("empty header collection")
21501 }
21502
21503 fn options() -> JavascriptHttpRequestOptions {
21504 JavascriptHttpRequestOptions {
21505 method: Some(String::from("GET")),
21506 headers: BTreeMap::new(),
21507 body: None,
21508 reject_unauthorized: None,
21509 }
21510 }
21511
21512 #[test]
21513 fn split_netloc_handles_hostnames_and_bracketed_ipv6() {
21514 assert_eq!(
21515 split_netloc("attacker.example:80"),
21516 Some(("attacker.example", 80))
21517 );
21518 assert_eq!(split_netloc("[::1]:443"), Some(("::1", 443)));
21519 assert_eq!(split_netloc("10.0.0.1:8080"), Some(("10.0.0.1", 8080)));
21520 assert_eq!(split_netloc("no-port"), None);
21521 assert_eq!(split_netloc("host:notaport"), None);
21522 }
21523
21524 #[test]
21530 fn outbound_http_connect_is_pinned_to_vetted_ip() {
21531 let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind loopback server");
21532 let port = listener.local_addr().expect("local addr").port();
21533 let server = thread::spawn(move || {
21534 let (mut stream, _) = listener.accept().expect("accept");
21535 let mut buf = [0u8; 1024];
21536 let _ = stream.read(&mut buf);
21537 stream
21538 .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi")
21539 .expect("write response");
21540 let _ = stream.flush();
21541 });
21542
21543 let url = Url::parse(&format!("http://attacker.example:{port}/")).expect("url");
21544 let pinned = vec![IpAddr::V4(Ipv4Addr::LOCALHOST)];
21545 let result = issue_outbound_http_request(&url, &options(), &empty_headers(), &pinned)
21546 .expect("pinned request should reach the vetted loopback target");
21547 let payload = result.as_str().expect("string payload");
21548 assert!(
21549 payload.contains("\"status\":200"),
21550 "expected 200 from pinned target, got: {payload}"
21551 );
21552 server.join().expect("server thread");
21553 }
21554
21555 #[test]
21559 fn outbound_http_refuses_when_no_vetted_address() {
21560 let url = Url::parse("https://attacker.example/").expect("url");
21561 let error = issue_outbound_http_request(&url, &options(), &empty_headers(), &[])
21562 .expect_err("empty pinned set must be refused");
21563 let message = error.to_string();
21564 assert!(
21565 message.contains("EACCES") || message.contains("ERR_HTTP_REQUEST_FAILED"),
21566 "expected an egress refusal, got: {message}"
21567 );
21568 }
21569}