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
636#[cfg(test)]
637#[allow(dead_code)]
638pub(crate) fn loopback_tls_registry_len() -> usize {
639 loopback_tls_transport_registry()
640 .lock()
641 .expect("loopback TLS transport registry lock poisoned")
642 .len()
643}
644
645#[cfg(test)]
646pub(crate) fn loopback_tls_registry_contains(key: &str) -> bool {
647 loopback_tls_transport_registry()
648 .lock()
649 .expect("loopback TLS transport registry lock poisoned")
650 .contains_key(key)
651}
652
653fn loopback_tls_transport_key(
654 vm_id: &str,
655 socket_id: SocketId,
656 peer_socket_id: SocketId,
657) -> String {
658 let (lower, higher) = if socket_id <= peer_socket_id {
659 (socket_id, peer_socket_id)
660 } else {
661 (peer_socket_id, socket_id)
662 };
663 format!("{vm_id}:{lower}:{higher}")
664}
665
666fn loopback_tls_endpoint(
667 vm_id: &str,
668 socket_id: SocketId,
669 peer_socket_id: SocketId,
670) -> Result<crate::state::LoopbackTlsEndpoint, SidecarError> {
671 let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
672 let registry = loopback_tls_transport_registry();
673 let mut transports = registry.lock().map_err(|_| {
674 SidecarError::InvalidState(String::from(
675 "loopback TLS transport registry lock poisoned",
676 ))
677 })?;
678 transports.retain(|_, pair| pair.strong_count() > 0);
679 let pair = transports
680 .get(&key)
681 .and_then(Weak::upgrade)
682 .unwrap_or_else(|| {
683 let pair = Arc::new(crate::state::LoopbackTlsTransportPair {
684 state: Mutex::new(crate::state::LoopbackTlsTransportPairState::default()),
685 ready: std::sync::Condvar::new(),
686 });
687 transports.insert(key.clone(), Arc::downgrade(&pair));
688 pair
689 });
690 Ok(crate::state::LoopbackTlsEndpoint {
691 pair,
692 is_lower_socket: socket_id <= peer_socket_id,
693 registry_key: Some(key),
694 })
695}
696
697impl crate::state::LoopbackTlsEndpoint {
698 fn shutdown_write(&self) -> Result<(), SidecarError> {
699 let mut state = self.pair.state.lock().map_err(|_| {
700 SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
701 })?;
702 if self.is_lower_socket {
703 state.lower_write_closed = true;
704 } else {
705 state.higher_write_closed = true;
706 }
707 self.pair.ready.notify_all();
708 Ok(())
709 }
710
711 fn close_endpoint(&self) -> Result<(), SidecarError> {
712 let mut state = self.pair.state.lock().map_err(|_| {
713 SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
714 })?;
715 if self.is_lower_socket {
716 state.lower_write_closed = true;
717 state.lower_closed = true;
718 } else {
719 state.higher_write_closed = true;
720 state.higher_closed = true;
721 }
722 self.pair.ready.notify_all();
723 Ok(())
724 }
725}
726
727fn parse_tls_client_hello_from_bytes(
728 buffer: &[u8],
729) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
730 if buffer.is_empty() {
731 return Ok(None);
732 }
733
734 let mut acceptor = rustls::server::Acceptor::default();
735 let mut cursor = Cursor::new(buffer);
736 acceptor.read_tls(&mut cursor).map_err(sidecar_net_error)?;
737 let Some(accepted) = acceptor.accept().map_err(|(error, _)| {
738 SidecarError::Execution(format!("failed to parse TLS client hello: {error}"))
739 })?
740 else {
741 return Ok(None);
742 };
743 let client_hello = accepted.client_hello();
744 let alpn_protocols = client_hello.alpn().map(|protocols| {
745 protocols
746 .filter_map(|protocol| String::from_utf8(protocol.to_vec()).ok())
747 .collect::<Vec<_>>()
748 });
749 Ok(Some(JavascriptTlsClientHello {
750 servername: client_hello.server_name().map(str::to_owned),
751 alpn_protocols,
752 }))
753}
754
755fn peek_loopback_tls_client_hello(
756 vm_id: &str,
757 socket_id: SocketId,
758 peer_socket_id: SocketId,
759) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
760 let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
761 let registry = loopback_tls_transport_registry();
762 let pair = registry
763 .lock()
764 .map_err(|_| {
765 SidecarError::InvalidState(String::from(
766 "loopback TLS transport registry lock poisoned",
767 ))
768 })?
769 .get(&key)
770 .and_then(Weak::upgrade);
771 let Some(pair) = pair else {
772 return Ok(None);
773 };
774 let is_lower_socket = socket_id <= peer_socket_id;
775 let state = pair.state.lock().map_err(|_| {
776 SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
777 })?;
778 let buffered = if is_lower_socket {
779 state.higher_to_lower.iter().copied().collect::<Vec<_>>()
780 } else {
781 state.lower_to_higher.iter().copied().collect::<Vec<_>>()
782 };
783 drop(state);
784 parse_tls_client_hello_from_bytes(&buffered)
785}
786
787fn wait_for_loopback_peer_socket_id(
788 kernel: &SidecarKernel,
789 socket_id: SocketId,
790) -> Option<SocketId> {
791 for _ in 0..50 {
792 if let Some(peer_socket_id) = kernel
793 .socket_get(socket_id)
794 .and_then(|record| record.peer_socket_id())
795 {
796 return Some(peer_socket_id);
797 }
798 std::thread::sleep(Duration::from_millis(10));
799 }
800 None
801}
802
803impl Drop for crate::state::LoopbackTlsEndpoint {
804 fn drop(&mut self) {
805 let _ = self.close_endpoint();
806
807 let Some(key) = self.registry_key.take() else {
815 return;
816 };
817 let Ok(mut transports) = loopback_tls_transport_registry().lock() else {
818 return;
820 };
821 let should_remove = match transports.get(&key) {
822 Some(weak) => match weak.upgrade() {
827 Some(existing) => {
828 let same_pair = Arc::ptr_eq(&existing, &self.pair);
829 drop(existing);
830 same_pair && Arc::strong_count(&self.pair) <= 1
831 }
832 None => true,
833 },
834 None => false,
835 };
836 if should_remove {
837 transports.remove(&key);
838 }
839 }
840}
841
842impl Read for crate::state::LoopbackTlsEndpoint {
843 fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
844 let mut state = self
845 .pair
846 .state
847 .lock()
848 .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
849
850 loop {
851 let (peer_write_closed, peer_closed) = if self.is_lower_socket {
852 (state.higher_write_closed, state.higher_closed)
853 } else {
854 (state.lower_write_closed, state.lower_closed)
855 };
856
857 let incoming = if self.is_lower_socket {
858 &mut state.higher_to_lower
859 } else {
860 &mut state.lower_to_higher
861 };
862
863 if !incoming.is_empty() {
864 let mut count = 0;
865 while count < buffer.len() {
866 let Some(byte) = incoming.pop_front() else {
867 break;
868 };
869 buffer[count] = byte;
870 count += 1;
871 }
872 return Ok(count);
873 }
874
875 if peer_write_closed || peer_closed {
876 return Ok(0);
877 }
878
879 let (next_state, wait_result) = self
880 .pair
881 .ready
882 .wait_timeout(state, TCP_SOCKET_POLL_TIMEOUT)
883 .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
884 state = next_state;
885 if wait_result.timed_out() {
886 return Err(std::io::Error::new(
887 std::io::ErrorKind::WouldBlock,
888 "loopback TLS transport read timed out",
889 ));
890 }
891 }
892 }
893}
894
895impl Write for crate::state::LoopbackTlsEndpoint {
896 fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
897 let mut state = self
898 .pair
899 .state
900 .lock()
901 .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
902
903 let peer_closed = if self.is_lower_socket {
904 state.higher_closed
905 } else {
906 state.lower_closed
907 };
908 let outgoing = if self.is_lower_socket {
909 &mut state.lower_to_higher
910 } else {
911 &mut state.higher_to_lower
912 };
913 if peer_closed {
914 return Err(std::io::Error::new(
915 std::io::ErrorKind::BrokenPipe,
916 "loopback TLS peer is closed",
917 ));
918 }
919
920 outgoing.extend(buffer.iter().copied());
921 self.pair.ready.notify_all();
922 Ok(buffer.len())
923 }
924
925 fn flush(&mut self) -> std::io::Result<()> {
926 Ok(())
927 }
928}
929
930#[cfg(test)]
931mod loopback_tls_registry_tests {
932 use super::{
933 loopback_tls_endpoint, loopback_tls_registry_contains, loopback_tls_transport_key,
934 };
935
936 #[test]
940 fn dropping_endpoint_removes_its_registry_entry() {
941 let vm_id = "loopback-tls-drop-removes-entry";
942 let key = loopback_tls_transport_key(vm_id, 1, 2);
943
944 let endpoint = loopback_tls_endpoint(vm_id, 1, 2).expect("create endpoint");
945 assert!(
946 loopback_tls_registry_contains(&key),
947 "registry should contain the key while the endpoint is alive"
948 );
949
950 drop(endpoint);
951 assert!(
952 !loopback_tls_registry_contains(&key),
953 "registry entry must be pruned in the endpoint's Drop, not left for the lazy retain()"
954 );
955 }
956
957 #[test]
958 fn registry_entry_survives_until_last_peer_endpoint_drops() {
959 let vm_id = "loopback-tls-shared-pair";
960 let key = loopback_tls_transport_key(vm_id, 3, 4);
961
962 let lower = loopback_tls_endpoint(vm_id, 3, 4).expect("create lower endpoint");
964 let higher = loopback_tls_endpoint(vm_id, 4, 3).expect("create higher endpoint");
965 assert!(loopback_tls_registry_contains(&key));
966
967 drop(lower);
970 assert!(
971 loopback_tls_registry_contains(&key),
972 "entry must survive while a peer endpoint still shares the pair"
973 );
974
975 drop(higher);
976 assert!(
977 !loopback_tls_registry_contains(&key),
978 "entry must be pruned once the last peer endpoint drops"
979 );
980 }
981}
982
983struct ActiveTcpConnectRequest<'a, B> {
986 bridge: &'a SharedBridge<B>,
987 kernel: &'a mut SidecarKernel,
988 kernel_pid: u32,
989 vm_id: &'a str,
990 dns: &'a VmDnsConfig,
991 host: &'a str,
992 port: u16,
993 local_address: Option<&'a str>,
994 local_port: Option<u16>,
995 local_reservation: Option<(JavascriptSocketFamily, u16)>,
996 context: &'a JavascriptSocketPathContext,
997}
998
999struct ActiveUdpSendToRequest<'a, B> {
1000 bridge: &'a SharedBridge<B>,
1001 kernel: &'a mut SidecarKernel,
1002 kernel_pid: u32,
1003 vm_id: &'a str,
1004 dns: &'a VmDnsConfig,
1005 host: &'a str,
1006 port: u16,
1007 context: &'a JavascriptSocketPathContext,
1008 contents: &'a [u8],
1009}
1010
1011struct UdpRemoteAddrRequest<'a, B> {
1012 bridge: &'a SharedBridge<B>,
1013 kernel: &'a SidecarKernel,
1014 vm_id: &'a str,
1015 dns: &'a VmDnsConfig,
1016 host: &'a str,
1017 port: u16,
1018 family: JavascriptUdpFamily,
1019 context: &'a JavascriptSocketPathContext,
1020}
1021
1022pub(crate) struct JavascriptSyncRpcServiceRequest<'a, B> {
1023 pub(crate) bridge: &'a SharedBridge<B>,
1024 pub(crate) vm_id: &'a str,
1025 pub(crate) dns: &'a VmDnsConfig,
1026 pub(crate) socket_paths: &'a JavascriptSocketPathContext,
1027 pub(crate) kernel: &'a mut SidecarKernel,
1028 pub(crate) process: &'a mut ActiveProcess,
1029 pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
1030 pub(crate) resource_limits: &'a ResourceLimits,
1031 pub(crate) network_counts: NetworkResourceCounts,
1032}
1033
1034pub(crate) struct JavascriptNetSyncRpcServiceRequest<'a, B> {
1035 pub(crate) bridge: &'a SharedBridge<B>,
1036 pub(crate) vm_id: &'a str,
1037 pub(crate) dns: &'a VmDnsConfig,
1038 pub(crate) socket_paths: &'a JavascriptSocketPathContext,
1039 pub(crate) kernel: &'a mut SidecarKernel,
1040 pub(crate) process: &'a mut ActiveProcess,
1041 pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
1042 pub(crate) resource_limits: &'a ResourceLimits,
1043 pub(crate) network_counts: NetworkResourceCounts,
1044}
1045
1046struct LoopbackHttpResponseWaitRequest<'a, B> {
1047 bridge: &'a SharedBridge<B>,
1048 vm_id: &'a str,
1049 dns: &'a VmDnsConfig,
1050 socket_paths: &'a JavascriptSocketPathContext,
1051 kernel: &'a mut SidecarKernel,
1052 process: &'a mut ActiveProcess,
1053 resource_limits: &'a ResourceLimits,
1054 request_key: (u64, u64),
1055}
1056
1057pub(crate) struct LoopbackHttpDispatchRequest<'a, B> {
1058 pub(crate) bridge: &'a SharedBridge<B>,
1059 pub(crate) vm_id: &'a str,
1060 pub(crate) dns: &'a VmDnsConfig,
1061 pub(crate) socket_paths: &'a JavascriptSocketPathContext,
1062 pub(crate) kernel: &'a mut SidecarKernel,
1063 pub(crate) process: &'a mut ActiveProcess,
1064 pub(crate) resource_limits: &'a ResourceLimits,
1065 pub(crate) server_id: u64,
1066 pub(crate) request_json: &'a str,
1067}
1068
1069struct JavascriptDgramSyncRpcServiceRequest<'a, B> {
1070 bridge: &'a SharedBridge<B>,
1071 kernel: &'a mut SidecarKernel,
1072 vm_id: &'a str,
1073 dns: &'a VmDnsConfig,
1074 socket_paths: &'a JavascriptSocketPathContext,
1075 process: &'a mut ActiveProcess,
1076 sync_request: &'a JavascriptSyncRpcRequest,
1077 resource_limits: &'a ResourceLimits,
1078 network_counts: NetworkResourceCounts,
1079}
1080
1081struct JavascriptHttp2SyncRpcServiceRequest<'a, B> {
1082 bridge: &'a SharedBridge<B>,
1083 kernel: &'a mut SidecarKernel,
1084 vm_id: &'a str,
1085 dns: &'a VmDnsConfig,
1086 socket_paths: &'a JavascriptSocketPathContext,
1087 process: &'a mut ActiveProcess,
1088 sync_request: &'a JavascriptSyncRpcRequest,
1089 resource_limits: &'a ResourceLimits,
1090 network_counts: NetworkResourceCounts,
1091}
1092
1093impl ActiveTcpSocket {
1094 fn connect<B>(request: ActiveTcpConnectRequest<'_, B>) -> Result<Self, SidecarError>
1095 where
1096 B: NativeSidecarBridge + Send + 'static,
1097 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
1098 {
1099 let ActiveTcpConnectRequest {
1100 bridge,
1101 kernel,
1102 kernel_pid,
1103 vm_id,
1104 dns,
1105 host,
1106 port,
1107 local_address,
1108 local_port,
1109 local_reservation,
1110 context,
1111 } = request;
1112 let resolved = resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, context)?;
1113 if resolved.use_kernel_loopback {
1114 let family = JavascriptSocketFamily::from_ip(resolved.guest_remote_addr.ip());
1115 let requested_local_port = local_port.unwrap_or(0);
1116 let local_port = if requested_local_port != 0
1117 && local_reservation == Some((family, requested_local_port))
1118 {
1119 requested_local_port
1120 } else {
1121 allocate_guest_listen_port(
1122 requested_local_port,
1123 family,
1124 &context.used_tcp_guest_ports,
1125 context.listen_policy,
1126 )?
1127 };
1128 let local_ip = match (family, local_address) {
1129 (JavascriptSocketFamily::Ipv4, Some("0.0.0.0")) => {
1130 IpAddr::V4(Ipv4Addr::UNSPECIFIED)
1131 }
1132 (JavascriptSocketFamily::Ipv4, Some("127.0.0.1") | Some("localhost") | None) => {
1133 IpAddr::V4(Ipv4Addr::LOCALHOST)
1134 }
1135 (JavascriptSocketFamily::Ipv6, Some("::")) => IpAddr::V6(Ipv6Addr::UNSPECIFIED),
1136 (JavascriptSocketFamily::Ipv6, Some("::1") | Some("localhost") | None) => {
1137 IpAddr::V6(Ipv6Addr::LOCALHOST)
1138 }
1139 (JavascriptSocketFamily::Ipv4, Some(other)) => {
1140 return Err(SidecarError::Execution(format!(
1141 "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1142 )));
1143 }
1144 (JavascriptSocketFamily::Ipv6, Some(other)) => {
1145 return Err(SidecarError::Execution(format!(
1146 "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1147 )));
1148 }
1149 };
1150 let local_addr = SocketAddr::new(local_ip, local_port);
1151 let spec = match family {
1152 JavascriptSocketFamily::Ipv4 => SocketSpec::tcp(),
1153 JavascriptSocketFamily::Ipv6 => {
1154 SocketSpec::new(SocketDomain::Inet6, SocketType::Stream)
1155 }
1156 };
1157 let socket_id = kernel
1158 .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
1159 .map_err(kernel_error)?;
1160 kernel
1161 .socket_bind_inet(
1162 EXECUTION_DRIVER_NAME,
1163 kernel_pid,
1164 socket_id,
1165 InetSocketAddress::new(local_ip.to_string(), local_port),
1166 )
1167 .map_err(kernel_error)?;
1168 kernel
1169 .socket_connect_inet_loopback(
1170 EXECUTION_DRIVER_NAME,
1171 kernel_pid,
1172 socket_id,
1173 InetSocketAddress::new(
1174 resolved.guest_remote_addr.ip().to_string(),
1175 resolved.guest_remote_addr.port(),
1176 ),
1177 )
1178 .map_err(kernel_error)?;
1179 return Ok(Self::from_kernel(
1180 socket_id,
1181 None,
1182 local_addr,
1183 resolved.guest_remote_addr,
1184 ));
1185 }
1186
1187 let stream = TcpStream::connect_timeout(&resolved.actual_addr, Duration::from_secs(30))
1188 .map_err(sidecar_net_error)?;
1189 let guest_local_addr = stream.local_addr().map_err(sidecar_net_error)?;
1190 Self::from_stream(stream, None, guest_local_addr, resolved.guest_remote_addr)
1191 }
1192
1193 fn from_stream(
1194 stream: TcpStream,
1195 listener_id: Option<String>,
1196 guest_local_addr: SocketAddr,
1197 guest_remote_addr: SocketAddr,
1198 ) -> Result<Self, SidecarError> {
1199 let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
1200 read_stream
1201 .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
1202 .map_err(sidecar_net_error)?;
1203 let stream = Arc::new(Mutex::new(stream));
1204 let pending_read_stream = Arc::new(Mutex::new(Some(read_stream)));
1205 let (sender, events) = mpsc::channel();
1206 let tls_mode = Arc::new(AtomicBool::new(false));
1207 let tls_stream = Arc::new(Mutex::new(None));
1208 let tls_state = Arc::new(Mutex::new(None));
1209 let saw_local_shutdown = Arc::new(AtomicBool::new(false));
1210 let saw_remote_end = Arc::new(AtomicBool::new(false));
1211 let close_notified = Arc::new(AtomicBool::new(false));
1212
1213 Ok(Self {
1214 stream: Some(stream),
1215 pending_read_stream: Some(pending_read_stream),
1216 events: Some(events),
1217 event_sender: Some(sender),
1218 kernel_socket_id: None,
1219 no_delay: false,
1220 keep_alive: false,
1221 keep_alive_initial_delay_secs: None,
1222 guest_local_addr,
1223 guest_remote_addr,
1224 listener_id,
1225 tls_mode,
1226 tls_stream,
1227 tls_state,
1228 saw_local_shutdown,
1229 saw_remote_end,
1230 close_notified,
1231 })
1232 }
1233
1234 fn from_kernel(
1235 socket_id: SocketId,
1236 listener_id: Option<String>,
1237 guest_local_addr: SocketAddr,
1238 guest_remote_addr: SocketAddr,
1239 ) -> Self {
1240 let (sender, events) = mpsc::channel();
1241 Self {
1242 stream: None,
1243 pending_read_stream: None,
1244 events: Some(events),
1245 event_sender: Some(sender),
1246 kernel_socket_id: Some(socket_id),
1247 no_delay: false,
1248 keep_alive: false,
1249 keep_alive_initial_delay_secs: None,
1250 guest_local_addr,
1251 guest_remote_addr,
1252 listener_id,
1253 tls_mode: Arc::new(AtomicBool::new(false)),
1254 tls_stream: Arc::new(Mutex::new(None)),
1255 tls_state: Arc::new(Mutex::new(None)),
1256 saw_local_shutdown: Arc::new(AtomicBool::new(false)),
1257 saw_remote_end: Arc::new(AtomicBool::new(false)),
1258 close_notified: Arc::new(AtomicBool::new(false)),
1259 }
1260 }
1261
1262 fn poll(
1263 &mut self,
1264 kernel: &mut SidecarKernel,
1265 kernel_pid: u32,
1266 wait: Duration,
1267 ) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
1268 if self.tls_mode.load(Ordering::SeqCst) {
1269 self.ensure_tcp_reader()?;
1270 return match self
1271 .events
1272 .as_ref()
1273 .ok_or_else(|| {
1274 SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1275 })?
1276 .recv_timeout(wait)
1277 {
1278 Ok(event) => Ok(Some(event)),
1279 Err(RecvTimeoutError::Timeout) => Ok(None),
1280 Err(RecvTimeoutError::Disconnected) => Ok(None),
1281 };
1282 }
1283
1284 if let Some(socket_id) = self.kernel_socket_id {
1285 let result = kernel
1286 .poll_targets(
1287 EXECUTION_DRIVER_NAME,
1288 kernel_pid,
1289 vec![PollTargetEntry::socket(
1290 socket_id,
1291 POLLIN | POLLHUP | POLLERR,
1292 )],
1293 i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
1294 )
1295 .map_err(kernel_error)?;
1296 let revents = result
1297 .targets
1298 .first()
1299 .map(|entry| entry.revents)
1300 .unwrap_or_else(PollEvents::empty);
1301 if revents.is_empty() {
1302 return Ok(None);
1303 }
1304 if revents.intersects(POLLIN) {
1305 return match kernel.socket_read(
1306 EXECUTION_DRIVER_NAME,
1307 kernel_pid,
1308 socket_id,
1309 64 * 1024,
1310 ) {
1311 Ok(Some(bytes)) if !bytes.is_empty() => {
1312 Ok(Some(JavascriptTcpSocketEvent::Data(bytes)))
1313 }
1314 Ok(Some(_)) => Ok(Some(JavascriptTcpSocketEvent::Data(Vec::new()))),
1315 Ok(None) => Ok(Some(JavascriptTcpSocketEvent::End)),
1316 Err(error) if error.code() == "EAGAIN" => Ok(None),
1317 Err(error) => Ok(Some(JavascriptTcpSocketEvent::Error {
1318 code: Some(error.code().to_string()),
1319 message: error.to_string(),
1320 })),
1321 };
1322 }
1323 if revents.intersects(POLLHUP) {
1324 return Ok(Some(JavascriptTcpSocketEvent::End));
1325 }
1326 if revents.intersects(POLLERR) {
1327 return Ok(Some(JavascriptTcpSocketEvent::Error {
1328 code: Some(String::from("EPIPE")),
1329 message: String::from("kernel TCP socket reported POLLERR"),
1330 }));
1331 }
1332 return Ok(None);
1333 }
1334
1335 self.ensure_tcp_reader()?;
1336 match self
1337 .events
1338 .as_ref()
1339 .ok_or_else(|| {
1340 SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1341 })?
1342 .recv_timeout(wait)
1343 {
1344 Ok(event) => Ok(Some(event)),
1345 Err(RecvTimeoutError::Timeout) => Ok(None),
1346 Err(RecvTimeoutError::Disconnected) => Ok(None),
1347 }
1348 }
1349
1350 fn ensure_tcp_reader(&self) -> Result<(), SidecarError> {
1351 if self.kernel_socket_id.is_some() {
1352 return Ok(());
1353 }
1354 if self.tls_mode.load(Ordering::SeqCst) {
1355 return Ok(());
1356 }
1357 let read_stream = self
1358 .pending_read_stream
1359 .as_ref()
1360 .ok_or_else(|| {
1361 SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1362 })?
1363 .lock()
1364 .map_err(|_| {
1365 SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1366 })?
1367 .take();
1368 if let Some(read_stream) = read_stream {
1369 spawn_tcp_socket_reader(
1370 read_stream,
1371 self.event_sender
1372 .as_ref()
1373 .ok_or_else(|| {
1374 SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1375 })?
1376 .clone(),
1377 Arc::clone(&self.tls_mode),
1378 Arc::clone(&self.saw_local_shutdown),
1379 Arc::clone(&self.saw_remote_end),
1380 Arc::clone(&self.close_notified),
1381 );
1382 }
1383 Ok(())
1384 }
1385
1386 fn socket_info(&self) -> Value {
1387 json!({
1388 "localAddress": self.guest_local_addr.ip().to_string(),
1389 "localPort": self.guest_local_addr.port(),
1390 "localFamily": socket_addr_family(&self.guest_local_addr),
1391 "remoteAddress": self.guest_remote_addr.ip().to_string(),
1392 "remotePort": self.guest_remote_addr.port(),
1393 "remoteFamily": socket_addr_family(&self.guest_remote_addr),
1394 })
1395 }
1396
1397 fn set_no_delay(&mut self, enable: bool) -> Result<(), SidecarError> {
1398 self.no_delay = enable;
1399 if self.kernel_socket_id.is_some() {
1400 return Ok(());
1401 }
1402 let stream = self
1403 .stream
1404 .as_ref()
1405 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1406 .lock()
1407 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1408 stream.set_nodelay(enable).map_err(sidecar_net_error)
1409 }
1410
1411 fn set_keep_alive(
1412 &mut self,
1413 enable: bool,
1414 initial_delay_secs: Option<u64>,
1415 ) -> Result<(), SidecarError> {
1416 self.keep_alive = enable;
1417 self.keep_alive_initial_delay_secs = initial_delay_secs;
1418 if self.kernel_socket_id.is_some() {
1419 return Ok(());
1420 }
1421 let stream = self
1422 .stream
1423 .as_ref()
1424 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1425 .lock()
1426 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1427 let socket = SockRef::from(&*stream);
1428 socket.set_keepalive(enable).map_err(sidecar_net_error)?;
1429 if enable {
1430 if let Some(delay_secs) = initial_delay_secs.filter(|delay_secs| *delay_secs > 0) {
1431 socket
1432 .set_tcp_keepalive(
1433 &TcpKeepalive::new().with_time(Duration::from_secs(delay_secs)),
1434 )
1435 .map_err(sidecar_net_error)?;
1436 }
1437 }
1438 Ok(())
1439 }
1440
1441 fn upgrade_tls(
1442 &self,
1443 vm_id: &str,
1444 kernel: &SidecarKernel,
1445 options: JavascriptTlsBridgeOptions,
1446 ) -> Result<(), SidecarError> {
1447 if self.tls_mode.load(Ordering::SeqCst) {
1448 return Ok(());
1449 }
1450
1451 let client_hello = if options.is_server {
1452 self.peek_tls_client_hello(vm_id, kernel)?
1453 } else {
1454 None
1455 };
1456
1457 let tls_stream = if let Some(socket_id) = self.kernel_socket_id {
1458 let peer_socket_id = wait_for_loopback_peer_socket_id(kernel, socket_id)
1459 .ok_or_else(|| {
1460 SidecarError::Execution(format!(
1461 "ERR_NOT_IMPLEMENTED: kernel-backed loopback socket {socket_id} has no peer for TLS upgrade"
1462 ))
1463 })?;
1464 let endpoint = loopback_tls_endpoint(vm_id, socket_id, peer_socket_id)?;
1465 if options.is_server {
1466 ActiveTlsStream::LoopbackServer(build_server_loopback_tls_stream(
1467 endpoint, &options,
1468 )?)
1469 } else {
1470 ActiveTlsStream::LoopbackClient(build_client_loopback_tls_stream(
1471 endpoint, &options,
1472 )?)
1473 }
1474 } else {
1475 self.pending_read_stream
1476 .as_ref()
1477 .ok_or_else(|| {
1478 SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1479 })?
1480 .lock()
1481 .map_err(|_| {
1482 SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1483 })?
1484 .take();
1485 let stream = self
1486 .stream
1487 .as_ref()
1488 .ok_or_else(|| {
1489 SidecarError::InvalidState(String::from("TCP socket stream missing"))
1490 })?
1491 .lock()
1492 .map_err(|_| {
1493 SidecarError::InvalidState(String::from("TCP socket lock poisoned"))
1494 })?;
1495 let cloned = stream.try_clone().map_err(sidecar_net_error)?;
1496 drop(stream);
1497
1498 if options.is_server {
1499 ActiveTlsStream::Server(build_server_tls_stream(cloned, &options)?)
1500 } else {
1501 ActiveTlsStream::Client(build_client_tls_stream(cloned, &options)?)
1502 }
1503 };
1504
1505 let tls_state = ActiveTlsState {
1506 client_hello,
1507 local_certificates: tls_local_certificates(&options)?,
1508 session_reused: false,
1509 };
1510
1511 self.tls_mode.store(true, Ordering::SeqCst);
1512 {
1513 let mut state = self
1514 .tls_state
1515 .lock()
1516 .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?;
1517 *state = Some(tls_state);
1518 }
1519 {
1520 let mut stream = self.tls_stream.lock().map_err(|_| {
1521 SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
1522 })?;
1523 *stream = Some(tls_stream);
1524 }
1525
1526 spawn_tls_socket_reader(
1527 Arc::clone(&self.tls_stream),
1528 self.event_sender
1529 .as_ref()
1530 .ok_or_else(|| {
1531 SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1532 })?
1533 .clone(),
1534 Arc::clone(&self.saw_local_shutdown),
1535 Arc::clone(&self.saw_remote_end),
1536 Arc::clone(&self.close_notified),
1537 );
1538 Ok(())
1539 }
1540
1541 fn peek_tls_client_hello(
1542 &self,
1543 vm_id: &str,
1544 kernel: &SidecarKernel,
1545 ) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
1546 if let Some(socket_id) = self.kernel_socket_id {
1547 let Some(peer_socket_id) = kernel
1548 .socket_get(socket_id)
1549 .and_then(|record| record.peer_socket_id())
1550 else {
1551 return Ok(None);
1552 };
1553 return peek_loopback_tls_client_hello(vm_id, socket_id, peer_socket_id);
1554 }
1555
1556 let stream = self
1557 .stream
1558 .as_ref()
1559 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1560 .lock()
1561 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1562 let mut buffer = vec![0_u8; 16 * 1024];
1563 let bytes = match stream.peek(&mut buffer) {
1564 Ok(0) => return Ok(None),
1565 Ok(bytes) => bytes,
1566 Err(error)
1567 if matches!(
1568 error.kind(),
1569 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
1570 ) =>
1571 {
1572 return Ok(None);
1573 }
1574 Err(error) => return Err(sidecar_net_error(error)),
1575 };
1576 parse_tls_client_hello_from_bytes(&buffer[..bytes])
1577 }
1578
1579 fn tls_client_hello_json(
1580 &self,
1581 vm_id: &str,
1582 kernel: &SidecarKernel,
1583 ) -> Result<Value, SidecarError> {
1584 if let Some(client_hello) = self
1585 .tls_state
1586 .lock()
1587 .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
1588 .as_ref()
1589 .and_then(|state| state.client_hello.clone())
1590 {
1591 return javascript_net_json_string(
1592 serde_json::to_value(client_hello).map_err(|error| {
1593 SidecarError::InvalidState(format!(
1594 "failed to serialize TLS client hello: {error}"
1595 ))
1596 })?,
1597 "net.socket_get_tls_client_hello",
1598 );
1599 }
1600
1601 javascript_net_json_string(
1602 serde_json::to_value(
1603 self.peek_tls_client_hello(vm_id, kernel)?
1604 .unwrap_or_default(),
1605 )
1606 .map_err(|error| {
1607 SidecarError::InvalidState(format!("failed to serialize TLS client hello: {error}"))
1608 })?,
1609 "net.socket_get_tls_client_hello",
1610 )
1611 }
1612
1613 fn tls_query(&self, query: &str, detailed: bool) -> Result<Value, SidecarError> {
1614 let state = self
1615 .tls_state
1616 .lock()
1617 .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
1618 .clone();
1619 let mut tls_stream = self
1620 .tls_stream
1621 .lock()
1622 .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?;
1623 let Some(stream) = tls_stream.as_mut() else {
1624 return javascript_net_json_string(
1625 tls_bridge_undefined_value(),
1626 "net.socket_tls_query",
1627 );
1628 };
1629
1630 let payload = match query {
1631 "getSession" => tls_bridge_undefined_value(),
1632 "isSessionReused" => Value::Bool(
1633 state
1634 .as_ref()
1635 .is_some_and(|tls_state| tls_state.session_reused),
1636 ),
1637 "getPeerCertificate" => {
1638 let certificate = stream
1639 .peer_certificates()
1640 .and_then(|certificates| certificates.first())
1641 .map(|certificate| {
1642 tls_certificate_bridge_value(certificate.as_ref(), detailed)
1643 });
1644 certificate.unwrap_or_else(tls_bridge_undefined_value)
1645 }
1646 "getCertificate" => state
1647 .as_ref()
1648 .and_then(|tls_state| tls_state.local_certificates.first())
1649 .map(|certificate| tls_certificate_bridge_value(certificate, detailed))
1650 .unwrap_or_else(tls_bridge_undefined_value),
1651 "getProtocol" => stream
1652 .protocol_version()
1653 .map(tls_protocol_name)
1654 .map(Value::String)
1655 .unwrap_or(Value::Null),
1656 "getCipher" => stream
1657 .negotiated_cipher_suite()
1658 .map(tls_cipher_bridge_value)
1659 .unwrap_or_else(tls_bridge_undefined_value),
1660 other => {
1661 return Err(SidecarError::InvalidState(format!(
1662 "unsupported TLS query {other}"
1663 )));
1664 }
1665 };
1666 javascript_net_json_string(payload, "net.socket_tls_query")
1667 }
1668
1669 fn write_all(
1670 &self,
1671 kernel: &mut SidecarKernel,
1672 kernel_pid: u32,
1673 contents: &[u8],
1674 ) -> Result<usize, SidecarError> {
1675 if self.tls_mode.load(Ordering::SeqCst) {
1676 let mut tls_stream = self.tls_stream.lock().map_err(|_| {
1677 SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
1678 })?;
1679 let stream = tls_stream.as_mut().ok_or_else(|| {
1680 SidecarError::InvalidState(String::from("TLS stream missing for upgraded socket"))
1681 })?;
1682 stream.write_all(contents)?;
1683 return Ok(contents.len());
1684 }
1685 if let Some(socket_id) = self.kernel_socket_id {
1686 return kernel
1687 .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, contents)
1688 .map_err(kernel_error);
1689 }
1690
1691 let mut stream = self
1692 .stream
1693 .as_ref()
1694 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1695 .lock()
1696 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1697 stream.write_all(contents).map_err(sidecar_net_error)?;
1698 Ok(contents.len())
1699 }
1700
1701 fn shutdown_write(
1702 &self,
1703 kernel: &mut SidecarKernel,
1704 kernel_pid: u32,
1705 ) -> Result<(), SidecarError> {
1706 if self.tls_mode.load(Ordering::SeqCst) {
1707 if let Some(stream) = self
1708 .tls_stream
1709 .lock()
1710 .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
1711 .as_mut()
1712 {
1713 let _ = stream.send_close_notify();
1714 let _ = stream.shutdown_write();
1715 }
1716 if self.kernel_socket_id.is_some() {
1717 self.saw_local_shutdown.store(true, Ordering::SeqCst);
1718 return Ok(());
1719 }
1720 }
1721 if let Some(socket_id) = self.kernel_socket_id {
1722 return kernel
1723 .socket_shutdown(
1724 EXECUTION_DRIVER_NAME,
1725 kernel_pid,
1726 socket_id,
1727 KernelSocketShutdown::Write,
1728 )
1729 .map_err(kernel_error);
1730 }
1731 let stream = self
1732 .stream
1733 .as_ref()
1734 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1735 .lock()
1736 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1737 self.saw_local_shutdown.store(true, Ordering::SeqCst);
1738 match stream.shutdown(Shutdown::Write) {
1739 Ok(()) => {}
1740 Err(error) if error.kind() == std::io::ErrorKind::NotConnected => {}
1741 Err(error) => return Err(sidecar_net_error(error)),
1742 }
1743 if self.saw_remote_end.load(Ordering::SeqCst)
1744 && !self.close_notified.swap(true, Ordering::SeqCst)
1745 {
1746 let _ = self
1747 .event_sender
1748 .as_ref()
1749 .ok_or_else(|| {
1750 SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1751 })?
1752 .send(JavascriptTcpSocketEvent::Close { had_error: false });
1753 }
1754 Ok(())
1755 }
1756
1757 fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
1758 if self.tls_mode.load(Ordering::SeqCst) {
1759 if let Some(stream) = self
1760 .tls_stream
1761 .lock()
1762 .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
1763 .as_mut()
1764 {
1765 let _ = stream.send_close_notify();
1766 let _ = stream.close();
1767 }
1768 if self.kernel_socket_id.is_some() {
1769 return Ok(());
1770 }
1771 }
1772 if let Some(socket_id) = self.kernel_socket_id {
1773 return kernel
1774 .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
1775 .map_err(kernel_error);
1776 }
1777 let stream = self
1778 .stream
1779 .as_ref()
1780 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1781 .lock()
1782 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1783 stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
1784 }
1785}
1786
1787impl ActiveTlsStream {
1788 fn write_all(&mut self, contents: &[u8]) -> Result<(), SidecarError> {
1789 match self {
1790 Self::Client(stream) => {
1791 stream.write_all(contents).map_err(sidecar_net_error)?;
1792 stream.flush().map_err(sidecar_net_error)
1793 }
1794 Self::Server(stream) => {
1795 stream.write_all(contents).map_err(sidecar_net_error)?;
1796 stream.flush().map_err(sidecar_net_error)
1797 }
1798 Self::LoopbackClient(stream) => {
1799 stream.write_all(contents).map_err(sidecar_net_error)?;
1800 stream.flush().map_err(sidecar_net_error)
1801 }
1802 Self::LoopbackServer(stream) => {
1803 stream.write_all(contents).map_err(sidecar_net_error)?;
1804 stream.flush().map_err(sidecar_net_error)
1805 }
1806 }
1807 }
1808
1809 fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
1810 match self {
1811 Self::Client(stream) => stream.read(buffer),
1812 Self::Server(stream) => stream.read(buffer),
1813 Self::LoopbackClient(stream) => stream.read(buffer),
1814 Self::LoopbackServer(stream) => stream.read(buffer),
1815 }
1816 }
1817
1818 fn send_close_notify(&mut self) -> Result<(), SidecarError> {
1819 match self {
1820 Self::Client(stream) => {
1821 stream.conn.send_close_notify();
1822 let _ = stream.conn.complete_io(&mut stream.sock);
1823 }
1824 Self::Server(stream) => {
1825 stream.conn.send_close_notify();
1826 let _ = stream.conn.complete_io(&mut stream.sock);
1827 }
1828 Self::LoopbackClient(stream) => {
1829 stream.conn.send_close_notify();
1830 let _ = stream.conn.complete_io(&mut stream.sock);
1831 }
1832 Self::LoopbackServer(stream) => {
1833 stream.conn.send_close_notify();
1834 let _ = stream.conn.complete_io(&mut stream.sock);
1835 }
1836 }
1837 Ok(())
1838 }
1839
1840 fn shutdown_write(&mut self) -> Result<(), SidecarError> {
1841 match self {
1842 Self::Client(stream) => stream
1843 .sock
1844 .shutdown(Shutdown::Write)
1845 .map_err(sidecar_net_error),
1846 Self::Server(stream) => stream
1847 .sock
1848 .shutdown(Shutdown::Write)
1849 .map_err(sidecar_net_error),
1850 Self::LoopbackClient(stream) => stream.sock.shutdown_write(),
1851 Self::LoopbackServer(stream) => stream.sock.shutdown_write(),
1852 }
1853 }
1854
1855 fn close(&mut self) -> Result<(), SidecarError> {
1856 match self {
1857 Self::Client(stream) => stream
1858 .sock
1859 .shutdown(Shutdown::Both)
1860 .map_err(sidecar_net_error),
1861 Self::Server(stream) => stream
1862 .sock
1863 .shutdown(Shutdown::Both)
1864 .map_err(sidecar_net_error),
1865 Self::LoopbackClient(stream) => stream.sock.close_endpoint(),
1866 Self::LoopbackServer(stream) => stream.sock.close_endpoint(),
1867 }
1868 }
1869
1870 fn peer_certificates(&self) -> Option<&[CertificateDer<'static>]> {
1871 match self {
1872 Self::Client(stream) => stream.conn.peer_certificates(),
1873 Self::Server(stream) => stream.conn.peer_certificates(),
1874 Self::LoopbackClient(stream) => stream.conn.peer_certificates(),
1875 Self::LoopbackServer(stream) => stream.conn.peer_certificates(),
1876 }
1877 }
1878
1879 fn negotiated_cipher_suite(&self) -> Option<rustls::SupportedCipherSuite> {
1880 match self {
1881 Self::Client(stream) => stream.conn.negotiated_cipher_suite(),
1882 Self::Server(stream) => stream.conn.negotiated_cipher_suite(),
1883 Self::LoopbackClient(stream) => stream.conn.negotiated_cipher_suite(),
1884 Self::LoopbackServer(stream) => stream.conn.negotiated_cipher_suite(),
1885 }
1886 }
1887
1888 fn protocol_version(&self) -> Option<rustls::ProtocolVersion> {
1889 match self {
1890 Self::Client(stream) => stream.conn.protocol_version(),
1891 Self::Server(stream) => stream.conn.protocol_version(),
1892 Self::LoopbackClient(stream) => stream.conn.protocol_version(),
1893 Self::LoopbackServer(stream) => stream.conn.protocol_version(),
1894 }
1895 }
1896}
1897
1898impl ActiveUnixSocket {
1903 fn connect(host_path: &Path, guest_path: &str) -> Result<Self, SidecarError> {
1904 let stream = UnixStream::connect(host_path).map_err(sidecar_net_error)?;
1905 Self::from_stream(stream, None, None, Some(guest_path.to_owned()))
1906 }
1907
1908 fn from_stream(
1909 stream: UnixStream,
1910 listener_id: Option<String>,
1911 local_path: Option<String>,
1912 remote_path: Option<String>,
1913 ) -> Result<Self, SidecarError> {
1914 let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
1915 let stream = Arc::new(Mutex::new(stream));
1916 let (sender, events) = mpsc::channel();
1917 let saw_local_shutdown = Arc::new(AtomicBool::new(false));
1918 let saw_remote_end = Arc::new(AtomicBool::new(false));
1919 let close_notified = Arc::new(AtomicBool::new(false));
1920 spawn_unix_socket_reader(
1921 read_stream,
1922 sender.clone(),
1923 Arc::clone(&saw_local_shutdown),
1924 Arc::clone(&saw_remote_end),
1925 Arc::clone(&close_notified),
1926 );
1927
1928 Ok(Self {
1929 stream,
1930 events,
1931 event_sender: sender,
1932 listener_id,
1933 local_path,
1934 remote_path,
1935 saw_local_shutdown,
1936 saw_remote_end,
1937 close_notified,
1938 })
1939 }
1940
1941 fn poll(&mut self, wait: Duration) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
1942 match self.events.recv_timeout(wait) {
1943 Ok(event) => Ok(Some(event)),
1944 Err(RecvTimeoutError::Timeout) => Ok(None),
1945 Err(RecvTimeoutError::Disconnected) => Ok(None),
1946 }
1947 }
1948
1949 fn socket_info(&self) -> Value {
1950 json!({
1951 "localPath": self.local_path.clone(),
1952 "remotePath": self.remote_path.clone(),
1953 })
1954 }
1955
1956 fn write_all(&self, contents: &[u8]) -> Result<usize, SidecarError> {
1957 let mut stream = self
1958 .stream
1959 .lock()
1960 .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1961 stream.write_all(contents).map_err(sidecar_net_error)?;
1962 Ok(contents.len())
1963 }
1964
1965 fn shutdown_write(&self) -> Result<(), SidecarError> {
1966 let stream = self
1967 .stream
1968 .lock()
1969 .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1970 self.saw_local_shutdown.store(true, Ordering::SeqCst);
1971 stream
1972 .shutdown(Shutdown::Write)
1973 .map_err(sidecar_net_error)?;
1974 if self.saw_remote_end.load(Ordering::SeqCst)
1975 && !self.close_notified.swap(true, Ordering::SeqCst)
1976 {
1977 let _ = self
1978 .event_sender
1979 .send(JavascriptTcpSocketEvent::Close { had_error: false });
1980 }
1981 Ok(())
1982 }
1983
1984 fn close(&self) -> Result<(), SidecarError> {
1985 let stream = self
1986 .stream
1987 .lock()
1988 .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1989 stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
1990 }
1991}
1992
1993impl ActiveUnixListener {
1996 fn bind(
1997 host_path: &Path,
1998 guest_path: &str,
1999 backlog: Option<u32>,
2000 ) -> Result<Self, SidecarError> {
2001 if let Some(parent) = host_path.parent() {
2002 fs::create_dir_all(parent).map_err(sidecar_net_error)?;
2003 }
2004 let listener = UnixListener::bind(host_path).map_err(sidecar_net_error)?;
2005 listener.set_nonblocking(true).map_err(sidecar_net_error)?;
2006 Ok(Self {
2007 listener,
2008 path: guest_path.to_owned(),
2009 backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2010 .expect("default backlog fits within usize"),
2011 active_connection_ids: BTreeSet::new(),
2012 })
2013 }
2014
2015 fn path(&self) -> &str {
2016 &self.path
2017 }
2018
2019 fn poll(
2020 &mut self,
2021 wait: Duration,
2022 ) -> Result<Option<JavascriptUnixListenerEvent>, SidecarError> {
2023 let deadline = Instant::now() + wait;
2024 loop {
2025 match self.listener.accept() {
2026 Ok((stream, remote_addr)) => {
2027 if self.active_connection_ids.len() >= self.backlog {
2028 let _ = stream.shutdown(Shutdown::Both);
2029 if wait.is_zero() || Instant::now() >= deadline {
2030 return Ok(None);
2031 }
2032 continue;
2033 }
2034
2035 let local_path = Some(self.path.clone());
2036 let remote_path = unix_socket_path(&remote_addr);
2037 return Ok(Some(JavascriptUnixListenerEvent::Connection(
2038 PendingUnixSocket {
2039 stream,
2040 local_path,
2041 remote_path,
2042 },
2043 )));
2044 }
2045 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2046 if wait.is_zero() || Instant::now() >= deadline {
2047 return Ok(None);
2048 }
2049 thread::sleep(Duration::from_millis(10));
2050 }
2051 Err(error) => {
2052 return Ok(Some(JavascriptUnixListenerEvent::Error {
2053 code: io_error_code(&error),
2054 message: error.to_string(),
2055 }));
2056 }
2057 }
2058 }
2059 }
2060
2061 fn close(&self) -> Result<(), SidecarError> {
2062 Ok(())
2063 }
2064
2065 fn active_connection_count(&self) -> usize {
2066 self.active_connection_ids.len()
2067 }
2068
2069 fn register_connection(&mut self, socket_id: &str) {
2070 self.active_connection_ids.insert(socket_id.to_string());
2071 }
2072
2073 fn release_connection(&mut self, socket_id: &str) {
2074 self.active_connection_ids.remove(socket_id);
2075 }
2076}
2077
2078impl ActiveTcpListener {
2079 fn bind(
2080 bind_host: &str,
2081 guest_host: &str,
2082 guest_port: u16,
2083 backlog: Option<u32>,
2084 ) -> Result<Self, SidecarError> {
2085 let bind_addr = resolve_tcp_bind_addr(bind_host, 0)?;
2086 let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
2087 let listener = TcpListener::bind(bind_addr).map_err(sidecar_net_error)?;
2088 listener.set_nonblocking(true).map_err(sidecar_net_error)?;
2089 let local_addr = listener.local_addr().map_err(sidecar_net_error)?;
2090 Ok(Self {
2091 listener: Some(listener),
2092 kernel_socket_id: None,
2093 local_addr: Some(local_addr),
2094 guest_local_addr: guest_addr,
2095 backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2096 .expect("default backlog fits within usize"),
2097 active_connection_ids: BTreeSet::new(),
2098 })
2099 }
2100
2101 fn bind_kernel(
2102 kernel: &mut SidecarKernel,
2103 kernel_pid: u32,
2104 guest_host: &str,
2105 guest_port: u16,
2106 backlog: Option<u32>,
2107 ) -> Result<Self, SidecarError> {
2108 let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
2109 let spec = match guest_addr {
2110 SocketAddr::V4(_) => SocketSpec::tcp(),
2111 SocketAddr::V6(_) => SocketSpec::new(SocketDomain::Inet6, SocketType::Stream),
2112 };
2113 let socket_id = kernel
2114 .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
2115 .map_err(kernel_error)?;
2116 kernel
2117 .socket_bind_inet(
2118 EXECUTION_DRIVER_NAME,
2119 kernel_pid,
2120 socket_id,
2121 InetSocketAddress::new(guest_addr.ip().to_string(), guest_addr.port()),
2122 )
2123 .map_err(kernel_error)?;
2124 kernel
2125 .socket_listen(
2126 EXECUTION_DRIVER_NAME,
2127 kernel_pid,
2128 socket_id,
2129 usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2130 .expect("default backlog fits within usize"),
2131 )
2132 .map_err(kernel_error)?;
2133 Ok(Self {
2134 listener: None,
2135 kernel_socket_id: Some(socket_id),
2136 local_addr: Some(guest_addr),
2137 guest_local_addr: guest_addr,
2138 backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2139 .expect("default backlog fits within usize"),
2140 active_connection_ids: BTreeSet::new(),
2141 })
2142 }
2143
2144 pub(crate) fn local_addr(&self) -> SocketAddr {
2145 self.local_addr.unwrap_or(self.guest_local_addr)
2146 }
2147
2148 fn guest_local_addr(&self) -> SocketAddr {
2149 self.guest_local_addr
2150 }
2151
2152 fn poll(
2153 &mut self,
2154 kernel: &mut SidecarKernel,
2155 kernel_pid: u32,
2156 wait: Duration,
2157 ) -> Result<Option<JavascriptTcpListenerEvent>, SidecarError> {
2158 if let Some(socket_id) = self.kernel_socket_id {
2159 let result = kernel
2160 .poll_targets(
2161 EXECUTION_DRIVER_NAME,
2162 kernel_pid,
2163 vec![PollTargetEntry::socket(socket_id, POLLIN)],
2164 i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
2165 )
2166 .map_err(kernel_error)?;
2167 let revents = result
2168 .targets
2169 .first()
2170 .map(|entry| entry.revents)
2171 .unwrap_or_else(PollEvents::empty);
2172 if revents.is_empty() {
2173 return Ok(None);
2174 }
2175 let accepted_socket_id =
2176 match kernel.socket_accept(EXECUTION_DRIVER_NAME, kernel_pid, socket_id) {
2177 Ok(accepted_socket_id) => accepted_socket_id,
2178 Err(error) if error.code() == "EAGAIN" => return Ok(None),
2179 Err(error) => {
2180 return Ok(Some(JavascriptTcpListenerEvent::Error {
2181 code: Some(error.code().to_string()),
2182 message: error.to_string(),
2183 }));
2184 }
2185 };
2186 let accepted = kernel.socket_get(accepted_socket_id).ok_or_else(|| {
2187 SidecarError::InvalidState(format!(
2188 "accepted kernel TCP socket {accepted_socket_id} is missing"
2189 ))
2190 })?;
2191 let local_addr = accepted.local_address().ok_or_else(|| {
2192 SidecarError::InvalidState(format!(
2193 "accepted kernel TCP socket {accepted_socket_id} missing local address"
2194 ))
2195 })?;
2196 let remote_addr = accepted.peer_address().ok_or_else(|| {
2197 SidecarError::InvalidState(format!(
2198 "accepted kernel TCP socket {accepted_socket_id} missing peer address"
2199 ))
2200 })?;
2201 return Ok(Some(JavascriptTcpListenerEvent::Connection(
2202 PendingTcpSocket {
2203 stream: None,
2204 kernel_socket_id: Some(accepted_socket_id),
2205 preallocated: true,
2206 guest_local_addr: resolve_tcp_bind_addr(local_addr.host(), local_addr.port())?,
2207 guest_remote_addr: resolve_tcp_bind_addr(
2208 remote_addr.host(),
2209 remote_addr.port(),
2210 )?,
2211 },
2212 )));
2213 }
2214
2215 let deadline = Instant::now() + wait;
2216 loop {
2217 match self
2218 .listener
2219 .as_ref()
2220 .ok_or_else(|| {
2221 SidecarError::InvalidState(String::from("TCP listener socket missing"))
2222 })?
2223 .accept()
2224 {
2225 Ok((stream, remote_addr)) => {
2226 if self.active_connection_ids.len() >= self.backlog {
2227 let _ = stream.shutdown(Shutdown::Both);
2228 if wait.is_zero() || Instant::now() >= deadline {
2229 return Ok(None);
2230 }
2231 continue;
2232 }
2233 return Ok(Some(JavascriptTcpListenerEvent::Connection(
2234 PendingTcpSocket {
2235 stream: Some(stream),
2236 kernel_socket_id: None,
2237 preallocated: false,
2238 guest_local_addr: self.guest_local_addr,
2239 guest_remote_addr: SocketAddr::new(
2240 remote_addr.ip(),
2241 remote_addr.port(),
2242 ),
2243 },
2244 )));
2245 }
2246 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2247 if wait.is_zero() || Instant::now() >= deadline {
2248 return Ok(None);
2249 }
2250 thread::sleep(Duration::from_millis(10));
2251 }
2252 Err(error) => {
2253 return Ok(Some(JavascriptTcpListenerEvent::Error {
2254 code: io_error_code(&error),
2255 message: error.to_string(),
2256 }));
2257 }
2258 }
2259 }
2260 }
2261
2262 fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
2263 if let Some(socket_id) = self.kernel_socket_id {
2264 kernel
2265 .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
2266 .map_err(kernel_error)?;
2267 }
2268 Ok(())
2269 }
2270
2271 fn active_connection_count(&self) -> usize {
2272 self.active_connection_ids.len()
2273 }
2274
2275 fn register_connection(&mut self, socket_id: &str) {
2276 self.active_connection_ids.insert(socket_id.to_string());
2277 }
2278
2279 fn release_connection(&mut self, socket_id: &str) {
2280 self.active_connection_ids.remove(socket_id);
2281 }
2282}
2283
2284impl ActiveUdpSocket {
2287 fn new(
2288 kernel: &mut SidecarKernel,
2289 kernel_pid: u32,
2290 family: JavascriptUdpFamily,
2291 ) -> Result<Self, SidecarError> {
2292 let spec = match family {
2293 JavascriptUdpFamily::Ipv4 => SocketSpec::udp(),
2294 JavascriptUdpFamily::Ipv6 => SocketSpec::new(SocketDomain::Inet6, SocketType::Datagram),
2295 };
2296 let socket_id = kernel
2297 .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
2298 .map_err(kernel_error)?;
2299 Ok(Self {
2300 family,
2301 socket: None,
2302 kernel_socket_id: Some(socket_id),
2303 guest_local_addr: None,
2304 recv_buffer_size: 0,
2305 send_buffer_size: 0,
2306 })
2307 }
2308
2309 fn local_addr(&self) -> Option<SocketAddr> {
2310 self.guest_local_addr
2311 }
2312
2313 fn socket(&self) -> Result<&UdpSocket, SidecarError> {
2314 self.socket
2315 .as_ref()
2316 .ok_or_else(|| SidecarError::Execution(String::from("EBADF: bad file descriptor")))
2317 }
2318
2319 fn bind(
2320 &mut self,
2321 kernel: &mut SidecarKernel,
2322 kernel_pid: u32,
2323 host: Option<&str>,
2324 port: u16,
2325 context: &JavascriptSocketPathContext,
2326 ) -> Result<SocketAddr, SidecarError> {
2327 if self.socket.is_some() || self.guest_local_addr.is_some() {
2328 return Err(SidecarError::Execution(String::from(
2329 "EINVAL: secure-exec dgram socket is already bound",
2330 )));
2331 }
2332
2333 let (bind_host, guest_host, guest_family) = normalize_udp_bind_host(host, self.family)?;
2334 let guest_port = allocate_guest_listen_port(
2335 port,
2336 guest_family,
2337 &context.used_udp_guest_ports,
2338 context.listen_policy,
2339 )?;
2340 let local_addr = resolve_udp_bind_addr(guest_host, guest_port, self.family)?;
2341 if let Some(socket_id) = self.kernel_socket_id {
2342 kernel
2343 .socket_bind_inet(
2344 EXECUTION_DRIVER_NAME,
2345 kernel_pid,
2346 socket_id,
2347 InetSocketAddress::new(local_addr.ip().to_string(), local_addr.port()),
2348 )
2349 .map_err(kernel_error)?;
2350 } else {
2351 let bind_addr = resolve_udp_bind_addr(bind_host, 0, self.family)?;
2352 let socket = UdpSocket::bind(bind_addr).map_err(sidecar_net_error)?;
2353 socket.set_nonblocking(true).map_err(sidecar_net_error)?;
2354 self.socket = Some(socket);
2355 }
2356 self.guest_local_addr = Some(local_addr);
2357 Ok(local_addr)
2358 }
2359
2360 fn ensure_bound_for_send(
2361 &mut self,
2362 kernel: &mut SidecarKernel,
2363 kernel_pid: u32,
2364 context: &JavascriptSocketPathContext,
2365 ) -> Result<SocketAddr, SidecarError> {
2366 if let Some(local_addr) = self.local_addr() {
2367 return Ok(local_addr);
2368 }
2369
2370 self.bind(kernel, kernel_pid, None, 0, context)
2371 }
2372
2373 fn send_to<B>(
2374 &mut self,
2375 request: ActiveUdpSendToRequest<'_, B>,
2376 ) -> Result<(usize, SocketAddr), SidecarError>
2377 where
2378 B: NativeSidecarBridge + Send + 'static,
2379 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
2380 {
2381 let ActiveUdpSendToRequest {
2382 bridge,
2383 kernel,
2384 kernel_pid,
2385 vm_id,
2386 dns,
2387 host,
2388 port,
2389 context,
2390 contents,
2391 } = request;
2392 let remote_addr = resolve_udp_addr(UdpRemoteAddrRequest {
2393 bridge,
2394 kernel,
2395 vm_id,
2396 dns,
2397 host,
2398 port,
2399 family: self.family,
2400 context,
2401 })?;
2402 let local_addr = self.ensure_bound_for_send(kernel, kernel_pid, context)?;
2403 let written = if let Some(socket_id) = self.kernel_socket_id {
2404 if is_loopback_ip(remote_addr.ip()) && remote_addr.port() == port {
2405 kernel
2406 .socket_send_to_inet_loopback(
2407 EXECUTION_DRIVER_NAME,
2408 kernel_pid,
2409 socket_id,
2410 InetSocketAddress::new(remote_addr.ip().to_string(), remote_addr.port()),
2411 contents,
2412 )
2413 .map_err(kernel_error)?
2414 } else {
2415 return Err(SidecarError::Execution(String::from(
2416 "ERR_NOT_IMPLEMENTED: external UDP datagrams are not yet supported by the kernel-backed V8 bridge",
2417 )));
2418 }
2419 } else {
2420 let socket = self.socket.as_ref().ok_or_else(|| {
2421 SidecarError::InvalidState(String::from("UDP socket is not initialized"))
2422 })?;
2423 socket
2424 .send_to(contents, remote_addr)
2425 .map_err(sidecar_net_error)?
2426 };
2427 Ok((written, local_addr))
2428 }
2429
2430 fn poll(
2431 &self,
2432 kernel: &mut SidecarKernel,
2433 kernel_pid: u32,
2434 wait: Duration,
2435 ) -> Result<Option<JavascriptUdpSocketEvent>, SidecarError> {
2436 if let Some(socket_id) = self.kernel_socket_id {
2437 let result = kernel
2438 .poll_targets(
2439 EXECUTION_DRIVER_NAME,
2440 kernel_pid,
2441 vec![PollTargetEntry::socket(socket_id, POLLIN)],
2442 i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
2443 )
2444 .map_err(kernel_error)?;
2445 let revents = result
2446 .targets
2447 .first()
2448 .map(|entry| entry.revents)
2449 .unwrap_or_else(PollEvents::empty);
2450 if revents.is_empty() {
2451 return Ok(None);
2452 }
2453 return match kernel.socket_recv_datagram(
2454 EXECUTION_DRIVER_NAME,
2455 kernel_pid,
2456 socket_id,
2457 64 * 1024,
2458 ) {
2459 Ok(Some(datagram)) => {
2460 let (source_address, payload) = datagram.into_parts();
2461 let remote_addr = source_address
2462 .map(|source| {
2463 resolve_udp_bind_addr(source.host(), source.port(), self.family)
2464 })
2465 .transpose()?
2466 .unwrap_or_else(|| match self.family {
2467 JavascriptUdpFamily::Ipv4 => {
2468 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)
2469 }
2470 JavascriptUdpFamily::Ipv6 => {
2471 SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0)
2472 }
2473 });
2474 Ok(Some(JavascriptUdpSocketEvent::Message {
2475 data: payload,
2476 remote_addr,
2477 }))
2478 }
2479 Ok(None) => Ok(None),
2480 Err(error) if error.code() == "EAGAIN" => Ok(None),
2481 Err(error) => Ok(Some(JavascriptUdpSocketEvent::Error {
2482 code: Some(error.code().to_string()),
2483 message: error.to_string(),
2484 })),
2485 };
2486 }
2487 let socket = self.socket()?;
2488 let deadline = Instant::now() + wait;
2489 let mut buffer = vec![0_u8; 64 * 1024];
2490
2491 loop {
2492 match socket.recv_from(&mut buffer) {
2493 Ok((bytes_read, remote_addr)) => {
2494 return Ok(Some(JavascriptUdpSocketEvent::Message {
2495 data: buffer[..bytes_read].to_vec(),
2496 remote_addr,
2497 }));
2498 }
2499 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2500 if wait.is_zero() || Instant::now() >= deadline {
2501 return Ok(None);
2502 }
2503 thread::sleep(Duration::from_millis(10));
2504 }
2505 Err(error) => {
2506 return Ok(Some(JavascriptUdpSocketEvent::Error {
2507 code: io_error_code(&error),
2508 message: error.to_string(),
2509 }));
2510 }
2511 }
2512 }
2513 }
2514
2515 fn close(&mut self, kernel: &mut SidecarKernel, kernel_pid: u32) {
2516 if let Some(socket_id) = self.kernel_socket_id {
2517 let _ = kernel.socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id);
2518 }
2519 self.socket.take();
2520 self.guest_local_addr = None;
2521 }
2522
2523 fn set_buffer_size(&mut self, which: &str, size: usize) -> Result<(), SidecarError> {
2524 match which {
2525 "recv" => self.recv_buffer_size = size,
2526 "send" => self.send_buffer_size = size,
2527 other => {
2528 return Err(SidecarError::InvalidState(format!(
2529 "unsupported UDP buffer size kind {other}"
2530 )));
2531 }
2532 }
2533 if self.kernel_socket_id.is_some() {
2534 return Ok(());
2535 }
2536 let socket = self.socket()?;
2537 let socket = SockRef::from(socket);
2538 match which {
2539 "recv" => socket.set_recv_buffer_size(size).map_err(sidecar_net_error),
2540 "send" => socket.set_send_buffer_size(size).map_err(sidecar_net_error),
2541 other => Err(SidecarError::InvalidState(format!(
2542 "unsupported UDP buffer size kind {other}"
2543 ))),
2544 }
2545 }
2546
2547 fn get_buffer_size(&self, which: &str) -> Result<usize, SidecarError> {
2548 if self.kernel_socket_id.is_some() {
2549 return Ok(match which {
2550 "recv" => self.recv_buffer_size,
2551 "send" => self.send_buffer_size,
2552 other => {
2553 return Err(SidecarError::InvalidState(format!(
2554 "unsupported UDP buffer size kind {other}"
2555 )));
2556 }
2557 });
2558 }
2559 let socket = self.socket()?;
2560 let socket = SockRef::from(socket);
2561 match which {
2562 "recv" => socket.recv_buffer_size().map_err(sidecar_net_error),
2563 "send" => socket.send_buffer_size().map_err(sidecar_net_error),
2564 other => Err(SidecarError::InvalidState(format!(
2565 "unsupported UDP buffer size kind {other}"
2566 ))),
2567 }
2568 }
2569}
2570
2571impl ActiveExecution {
2574 pub(crate) fn uses_shared_v8_runtime(&self) -> bool {
2575 match self {
2576 Self::Javascript(execution) => execution.uses_shared_v8_runtime(),
2577 Self::Python(execution) => execution.uses_shared_v8_runtime(),
2578 Self::Wasm(execution) => execution.uses_shared_v8_runtime(),
2579 Self::Tool(_) => false,
2580 }
2581 }
2582
2583 pub(crate) fn child_pid(&self) -> u32 {
2584 match self {
2585 Self::Javascript(execution) => execution.child_pid(),
2586 Self::Python(execution) => execution.child_pid(),
2587 Self::Wasm(execution) => execution.child_pid(),
2588 Self::Tool(_) => 0,
2589 }
2590 }
2591
2592 pub(crate) fn write_stdin(&mut self, chunk: &[u8]) -> Result<(), SidecarError> {
2593 match self {
2594 Self::Javascript(execution) => execution
2595 .write_stdin(chunk)
2596 .map_err(|error| SidecarError::Execution(error.to_string())),
2597 Self::Python(execution) => execution
2598 .write_stdin(chunk)
2599 .map_err(|error| SidecarError::Execution(error.to_string())),
2600 Self::Wasm(execution) => execution
2601 .write_stdin(chunk)
2602 .map_err(|error| SidecarError::Execution(error.to_string())),
2603 Self::Tool(_) => Ok(()),
2604 }
2605 }
2606
2607 pub(crate) fn close_stdin(&mut self) -> Result<(), SidecarError> {
2608 match self {
2609 Self::Javascript(execution) => execution
2610 .close_stdin()
2611 .map_err(|error| SidecarError::Execution(error.to_string())),
2612 Self::Python(execution) => execution
2613 .close_stdin()
2614 .map_err(|error| SidecarError::Execution(error.to_string())),
2615 Self::Wasm(execution) => execution
2616 .close_stdin()
2617 .map_err(|error| SidecarError::Execution(error.to_string())),
2618 Self::Tool(_) => Ok(()),
2619 }
2620 }
2621
2622 pub(crate) fn respond_python_vfs_rpc_success(
2623 &mut self,
2624 id: u64,
2625 payload: PythonVfsRpcResponsePayload,
2626 ) -> Result<(), SidecarError> {
2627 match self {
2628 Self::Python(execution) => execution
2629 .respond_vfs_rpc_success(id, payload)
2630 .map_err(|error| SidecarError::Execution(error.to_string())),
2631 _ => Err(SidecarError::InvalidState(String::from(
2632 "only Python executions can service Python VFS RPC responses",
2633 ))),
2634 }
2635 }
2636
2637 pub(crate) fn respond_python_vfs_rpc_error(
2638 &mut self,
2639 id: u64,
2640 code: impl Into<String>,
2641 message: impl Into<String>,
2642 ) -> Result<(), SidecarError> {
2643 match self {
2644 Self::Python(execution) => execution
2645 .respond_vfs_rpc_error(id, code, message)
2646 .map_err(|error| SidecarError::Execution(error.to_string())),
2647 _ => Err(SidecarError::InvalidState(String::from(
2648 "only Python executions can service Python VFS RPC responses",
2649 ))),
2650 }
2651 }
2652
2653 pub(crate) fn send_javascript_stream_event(
2654 &self,
2655 event_type: &str,
2656 payload: Value,
2657 ) -> Result<(), SidecarError> {
2658 match self {
2659 Self::Javascript(execution) => execution
2660 .send_stream_event(event_type, payload)
2661 .map_err(|error| SidecarError::Execution(error.to_string())),
2662 Self::Wasm(execution) => execution
2663 .send_stream_event(event_type, payload)
2664 .map_err(|error| SidecarError::Execution(error.to_string())),
2665 _ => Err(SidecarError::InvalidState(String::from(
2666 "only embedded V8 executions can receive JavaScript stream events",
2667 ))),
2668 }
2669 }
2670
2671 pub(crate) fn javascript_v8_session_handle(&self) -> Option<V8SessionHandle> {
2672 match self {
2673 Self::Javascript(execution) => Some(execution.v8_session_handle()),
2674 Self::Wasm(execution) => Some(execution.v8_session_handle()),
2675 _ => None,
2676 }
2677 }
2678
2679 pub(crate) fn terminate(&mut self) -> Result<(), SidecarError> {
2680 match self {
2681 Self::Javascript(execution) => execution
2682 .terminate()
2683 .map_err(|error| SidecarError::Execution(error.to_string())),
2684 Self::Python(execution) => execution
2685 .kill()
2686 .map_err(|error| SidecarError::Execution(error.to_string())),
2687 Self::Wasm(execution) => execution
2688 .terminate()
2689 .map_err(|error| SidecarError::Execution(error.to_string())),
2690 Self::Tool(_) => Ok(()),
2691 }
2692 }
2693
2694 pub(crate) fn respond_javascript_sync_rpc_success(
2695 &mut self,
2696 id: u64,
2697 result: Value,
2698 ) -> Result<(), SidecarError> {
2699 match self {
2700 Self::Javascript(execution) => execution
2701 .respond_sync_rpc_success(id, result)
2702 .map_err(|error| SidecarError::Execution(error.to_string())),
2703 Self::Python(execution) => execution
2704 .respond_javascript_sync_rpc_success(id, result)
2705 .map_err(|error| SidecarError::Execution(error.to_string())),
2706 Self::Wasm(execution) => execution
2707 .respond_sync_rpc_success(id, result)
2708 .map_err(|error| SidecarError::Execution(error.to_string())),
2709 _ => Err(SidecarError::InvalidState(String::from(
2710 "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
2711 ))),
2712 }
2713 }
2714
2715 pub(crate) fn respond_javascript_sync_rpc_error(
2716 &mut self,
2717 id: u64,
2718 code: impl Into<String>,
2719 message: impl Into<String>,
2720 ) -> Result<(), SidecarError> {
2721 match self {
2722 Self::Javascript(execution) => execution
2723 .respond_sync_rpc_error(id, code, message)
2724 .map_err(|error| SidecarError::Execution(error.to_string())),
2725 Self::Python(execution) => execution
2726 .respond_javascript_sync_rpc_error(id, code, message)
2727 .map_err(|error| SidecarError::Execution(error.to_string())),
2728 Self::Wasm(execution) => execution
2729 .respond_sync_rpc_error(id, code, message)
2730 .map_err(|error| SidecarError::Execution(error.to_string())),
2731 _ => Err(SidecarError::InvalidState(String::from(
2732 "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
2733 ))),
2734 }
2735 }
2736
2737 pub(crate) async fn poll_event(
2738 &mut self,
2739 timeout: Duration,
2740 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
2741 match self {
2742 Self::Javascript(execution) => execution
2743 .poll_event(timeout)
2744 .await
2745 .map(|event| {
2746 event.map(|event| match event {
2747 JavascriptExecutionEvent::Stdout(chunk) => {
2748 ActiveExecutionEvent::Stdout(chunk)
2749 }
2750 JavascriptExecutionEvent::Stderr(chunk) => {
2751 ActiveExecutionEvent::Stderr(chunk)
2752 }
2753 JavascriptExecutionEvent::SyncRpcRequest(request) => {
2754 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2755 }
2756 JavascriptExecutionEvent::SignalState {
2757 signal,
2758 registration,
2759 } => ActiveExecutionEvent::SignalState {
2760 signal,
2761 registration: map_node_signal_registration(registration),
2762 },
2763 JavascriptExecutionEvent::Exited(code) => {
2764 ActiveExecutionEvent::Exited(code)
2765 }
2766 })
2767 })
2768 .map_err(|error| SidecarError::Execution(error.to_string())),
2769 Self::Python(execution) => execution
2770 .poll_event(timeout)
2771 .await
2772 .map(|event| {
2773 event.map(|event| match event {
2774 PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2775 PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2776 PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
2777 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2778 }
2779 PythonExecutionEvent::VfsRpcRequest(request) => {
2780 ActiveExecutionEvent::PythonVfsRpcRequest(request)
2781 }
2782 PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2783 })
2784 })
2785 .map_err(|error| SidecarError::Execution(error.to_string())),
2786 Self::Wasm(execution) => execution
2787 .poll_event(timeout)
2788 .await
2789 .map(|event| {
2790 event.map(|event| match event {
2791 WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2792 WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2793 WasmExecutionEvent::SyncRpcRequest(request) => {
2794 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2795 }
2796 WasmExecutionEvent::SignalState {
2797 signal,
2798 registration,
2799 } => ActiveExecutionEvent::SignalState {
2800 signal,
2801 registration: map_wasm_signal_registration(registration),
2802 },
2803 WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2804 })
2805 })
2806 .map_err(|error| SidecarError::Execution(error.to_string())),
2807 Self::Tool(execution) => {
2808 let _ = timeout;
2809 poll_tool_process_event(execution)
2810 }
2811 }
2812 }
2813
2814 pub(crate) fn poll_event_blocking(
2815 &mut self,
2816 timeout: Duration,
2817 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
2818 match self {
2819 Self::Javascript(execution) => execution
2820 .poll_event_blocking(timeout)
2821 .map(|event| {
2822 event.map(|event| match event {
2823 JavascriptExecutionEvent::Stdout(chunk) => {
2824 ActiveExecutionEvent::Stdout(chunk)
2825 }
2826 JavascriptExecutionEvent::Stderr(chunk) => {
2827 ActiveExecutionEvent::Stderr(chunk)
2828 }
2829 JavascriptExecutionEvent::SyncRpcRequest(request) => {
2830 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2831 }
2832 JavascriptExecutionEvent::SignalState {
2833 signal,
2834 registration,
2835 } => ActiveExecutionEvent::SignalState {
2836 signal,
2837 registration: map_node_signal_registration(registration),
2838 },
2839 JavascriptExecutionEvent::Exited(code) => {
2840 ActiveExecutionEvent::Exited(code)
2841 }
2842 })
2843 })
2844 .map_err(|error| SidecarError::Execution(error.to_string())),
2845 Self::Python(execution) => execution
2846 .poll_event_blocking(timeout)
2847 .map(|event| {
2848 event.map(|event| match event {
2849 PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2850 PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2851 PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
2852 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2853 }
2854 PythonExecutionEvent::VfsRpcRequest(request) => {
2855 ActiveExecutionEvent::PythonVfsRpcRequest(request)
2856 }
2857 PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2858 })
2859 })
2860 .map_err(|error| SidecarError::Execution(error.to_string())),
2861 Self::Wasm(execution) => execution
2862 .poll_event_blocking(timeout)
2863 .map(|event| {
2864 event.map(|event| match event {
2865 WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2866 WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2867 WasmExecutionEvent::SyncRpcRequest(request) => {
2868 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2869 }
2870 WasmExecutionEvent::SignalState {
2871 signal,
2872 registration,
2873 } => ActiveExecutionEvent::SignalState {
2874 signal,
2875 registration: map_wasm_signal_registration(registration),
2876 },
2877 WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2878 })
2879 })
2880 .map_err(|error| SidecarError::Execution(error.to_string())),
2881 Self::Tool(execution) => {
2882 let _ = timeout;
2883 poll_tool_process_event(execution)
2884 }
2885 }
2886 }
2887}
2888
2889struct ToolProcessEventRequest {
2890 sidecar_requests: SharedSidecarRequestClient,
2891 connection_id: String,
2892 session_id: String,
2893 vm_id: String,
2894 tool_resolution: ToolCommandResolution,
2895 cancelled: Arc<AtomicBool>,
2896 pending_events: Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
2897 events_overflowed: Arc<AtomicBool>,
2898}
2899
2900pub(crate) fn send_tool_process_event(
2901 pending_events: &Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
2902 events_overflowed: &AtomicBool,
2903 event: ActiveExecutionEvent,
2904) -> bool {
2905 let mut pending_events = pending_events
2906 .lock()
2907 .unwrap_or_else(|poisoned| poisoned.into_inner());
2908 if pending_events.len() >= MAX_PROCESS_EVENT_QUEUE {
2909 events_overflowed.store(true, Ordering::Relaxed);
2910 return false;
2911 }
2912 pending_events.push_back(event);
2913 true
2914}
2915
2916fn spawn_tool_process_events(request: ToolProcessEventRequest) {
2917 let ToolProcessEventRequest {
2918 sidecar_requests,
2919 connection_id,
2920 session_id,
2921 vm_id,
2922 tool_resolution,
2923 cancelled,
2924 pending_events,
2925 events_overflowed,
2926 } = request;
2927 std::thread::spawn(move || match tool_resolution {
2928 ToolCommandResolution::Failure(message) => {
2929 if !send_tool_process_event(
2930 &pending_events,
2931 &events_overflowed,
2932 ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
2933 ) {
2934 return;
2935 }
2936 let _ = send_tool_process_event(
2937 &pending_events,
2938 &events_overflowed,
2939 ActiveExecutionEvent::Exited(1),
2940 );
2941 }
2942 ToolCommandResolution::Invoke { request, timeout } => {
2943 let response = sidecar_requests.invoke(
2944 OwnershipScope::vm(connection_id.clone(), session_id.clone(), vm_id.clone()),
2945 SidecarRequestPayload::HostCallback(request.clone()),
2946 timeout,
2947 );
2948 if cancelled.load(Ordering::Relaxed) {
2949 return;
2950 }
2951
2952 match response {
2953 Ok(crate::protocol::SidecarResponsePayload::HostCallbackResult(result)) => {
2954 if let Some(value) = result.result {
2955 let value: serde_json::Value = serde_json::from_str(&value)
2956 .unwrap_or(serde_json::Value::String(value));
2957 let stdout = serde_json::to_vec(&json!({
2958 "ok": true,
2959 "result": value,
2960 }))
2961 .unwrap_or_else(|error| {
2962 format_tool_failure_output(&format!(
2963 "failed to serialize tool result: {error}"
2964 ))
2965 });
2966 if !send_tool_process_event(
2967 &pending_events,
2968 &events_overflowed,
2969 ActiveExecutionEvent::Stdout(stdout),
2970 ) {
2971 return;
2972 }
2973 let _ = send_tool_process_event(
2974 &pending_events,
2975 &events_overflowed,
2976 ActiveExecutionEvent::Exited(0),
2977 );
2978 } else {
2979 let message = result
2980 .error
2981 .unwrap_or_else(|| String::from("tool invocation returned no result"));
2982 if !send_tool_process_event(
2983 &pending_events,
2984 &events_overflowed,
2985 ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
2986 ) {
2987 return;
2988 }
2989 let _ = send_tool_process_event(
2990 &pending_events,
2991 &events_overflowed,
2992 ActiveExecutionEvent::Exited(1),
2993 );
2994 }
2995 }
2996 Ok(_) => {
2997 if !send_tool_process_event(
2998 &pending_events,
2999 &events_overflowed,
3000 ActiveExecutionEvent::Stderr(format_tool_failure_output(
3001 "unexpected sidecar tool response",
3002 )),
3003 ) {
3004 return;
3005 }
3006 let _ = send_tool_process_event(
3007 &pending_events,
3008 &events_overflowed,
3009 ActiveExecutionEvent::Exited(1),
3010 );
3011 }
3012 Err(error) => {
3013 if !send_tool_process_event(
3014 &pending_events,
3015 &events_overflowed,
3016 ActiveExecutionEvent::Stderr(format_tool_failure_output(
3017 &error.to_string(),
3018 )),
3019 ) {
3020 return;
3021 }
3022 let _ = send_tool_process_event(
3023 &pending_events,
3024 &events_overflowed,
3025 ActiveExecutionEvent::Exited(1),
3026 );
3027 }
3028 }
3029 }
3030 });
3031}
3032
3033impl<B> NativeSidecar<B>
3034where
3035 B: NativeSidecarBridge + Send + 'static,
3036 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
3037{
3038 pub(crate) async fn execute(
3039 &mut self,
3040 request: &RequestFrame,
3041 payload: ExecuteRequest,
3042 ) -> Result<DispatchResult, SidecarError> {
3043 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3044 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3045
3046 let vm = self
3047 .vms
3048 .get_mut(&vm_id)
3049 .ok_or_else(|| missing_vm_error(&vm_id))?;
3050 if vm.active_processes.contains_key(&payload.process_id) {
3051 return Err(SidecarError::InvalidState(format!(
3052 "VM {vm_id} already has an active process with id {}",
3053 payload.process_id
3054 )));
3055 }
3056
3057 if let Some(command) = payload.command.as_deref() {
3058 if let Some(tool_resolution) =
3059 resolve_tool_command(vm, command, &payload.args, payload.cwd.as_deref())?
3060 {
3061 let guest_cwd = payload
3062 .cwd
3063 .as_deref()
3064 .map(normalize_path)
3065 .unwrap_or_else(|| vm.guest_cwd.clone());
3066 let kernel_handle = vm
3067 .kernel
3068 .create_virtual_process(
3069 EXECUTION_DRIVER_NAME,
3070 TOOL_DRIVER_NAME,
3071 command,
3072 std::iter::once(command.to_owned())
3073 .chain(payload.args.iter().cloned())
3074 .collect(),
3075 VirtualProcessOptions {
3076 env: vm.guest_env.clone(),
3077 cwd: Some(guest_cwd.clone()),
3078 ..VirtualProcessOptions::default()
3079 },
3080 )
3081 .map_err(kernel_error)?;
3082 let kernel_pid = kernel_handle.pid();
3083 let tool_execution = ToolExecution::default();
3084 let cancelled = tool_execution.cancelled.clone();
3085 let pending_events = tool_execution.pending_events.clone();
3086 let events_overflowed = tool_execution.events_overflowed.clone();
3087 vm.active_processes.insert(
3088 payload.process_id.clone(),
3089 ActiveProcess::new(
3090 kernel_pid,
3091 kernel_handle,
3092 GuestRuntimeKind::JavaScript,
3093 ActiveExecution::Tool(tool_execution),
3094 )
3095 .with_guest_cwd(guest_cwd.clone())
3096 .with_host_cwd(resolve_vm_guest_path_to_host(vm, &guest_cwd)),
3097 );
3098 self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
3099 spawn_tool_process_events(ToolProcessEventRequest {
3100 sidecar_requests: self.sidecar_requests.clone(),
3101 connection_id: connection_id.clone(),
3102 session_id: session_id.clone(),
3103 vm_id: vm_id.clone(),
3104 tool_resolution,
3105 cancelled,
3106 pending_events,
3107 events_overflowed,
3108 });
3109
3110 return Ok(DispatchResult {
3111 response: self.respond(
3112 request,
3113 ResponsePayload::ProcessStarted(ProcessStartedResponse {
3114 process_id: payload.process_id,
3115 pid: Some(kernel_pid),
3116 }),
3117 ),
3118 events: Vec::new(),
3119 });
3120 }
3121 }
3122
3123 let resolved = resolve_execute_request(vm, &payload)?;
3124 let mut env = resolved.env.clone();
3125 let sandbox_root = normalize_host_path(&vm.cwd);
3126 env.insert(
3127 String::from(EXECUTION_SANDBOX_ROOT_ENV),
3128 sandbox_root.to_string_lossy().into_owned(),
3129 );
3130 if resolved.runtime == GuestRuntimeKind::JavaScript {
3131 env.insert(
3132 String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
3133 String::from("1"),
3134 );
3135 } else if resolved.runtime == GuestRuntimeKind::WebAssembly {
3136 env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
3137 }
3138 let argv = std::iter::once(resolved.entrypoint.clone())
3139 .chain(resolved.execution_args.iter().cloned())
3140 .collect::<Vec<_>>();
3141 let kernel_handle = vm
3142 .kernel
3143 .spawn_process(
3144 &resolved.command,
3145 argv,
3146 SpawnOptions {
3147 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
3148 cwd: Some(resolved.guest_cwd.clone()),
3149 ..SpawnOptions::default()
3150 },
3151 )
3152 .map_err(kernel_error)?;
3153 let kernel_pid = kernel_handle.pid();
3154
3155 let (execution, process_env) = match resolved.runtime {
3156 GuestRuntimeKind::JavaScript => {
3157 let inline_code = load_javascript_entrypoint_source(
3158 vm,
3159 &resolved.host_cwd,
3160 &resolved.entrypoint,
3161 &env,
3162 );
3163 prepare_javascript_shadow(vm, &resolved)?;
3164
3165 let context =
3166 self.javascript_engine
3167 .create_context(CreateJavascriptContextRequest {
3168 vm_id: vm_id.clone(),
3169 bootstrap_module: None,
3170 compile_cache_root: Some(self.cache_root.join("node-compile-cache")),
3171 });
3172 let built_reader = build_module_reader(vm, &resolved);
3173 let guest_reader = built_reader.clone().map(|reader| {
3174 Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
3175 as Box<dyn GuestModuleReader>
3176 });
3177 let module_reader =
3178 built_reader.map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
3179 let execution = self
3180 .javascript_engine
3181 .start_execution_with_module_reader(
3182 StartJavascriptExecutionRequest {
3183 guest_runtime: guest_runtime_identity(vm, None, None),
3184 vm_id: vm_id.clone(),
3185 context_id: context.context_id,
3186 argv: std::iter::once(resolved.entrypoint.clone())
3187 .chain(resolved.execution_args.iter().cloned())
3188 .collect(),
3189 env: env.clone(),
3190 cwd: resolved.host_cwd.clone(),
3191 limits: javascript_execution_limits(vm),
3192 inline_code,
3193 },
3194 module_reader,
3195 guest_reader,
3196 )
3197 .map_err(javascript_error)?;
3198 (ActiveExecution::Javascript(execution), env.clone())
3199 }
3200 GuestRuntimeKind::Python => {
3201 let python_file_path = python_file_entrypoint(&resolved.entrypoint);
3202 let pyodide_dist_path = self
3203 .python_engine
3204 .bundled_pyodide_dist_path_for_vm(&vm_id)
3205 .map_err(python_error)?;
3206 let pyodide_cache_path = pyodide_dist_path
3207 .parent()
3208 .and_then(Path::parent)
3209 .unwrap_or(pyodide_dist_path.as_path())
3210 .join("pyodide-package-cache");
3211 add_runtime_guest_path_mapping(
3212 &mut env,
3213 PYTHON_PYODIDE_GUEST_ROOT,
3214 &pyodide_dist_path,
3215 );
3216 add_runtime_guest_path_mapping(
3217 &mut env,
3218 PYTHON_PYODIDE_CACHE_GUEST_ROOT,
3219 &pyodide_cache_path,
3220 );
3221 add_runtime_host_access_path(
3222 &mut env,
3223 "AGENTOS_EXTRA_FS_READ_PATHS",
3224 &pyodide_dist_path,
3225 true,
3226 );
3227 add_runtime_host_access_path(
3228 &mut env,
3229 "AGENTOS_EXTRA_FS_READ_PATHS",
3230 &pyodide_cache_path,
3231 true,
3232 );
3233 add_runtime_host_access_path(
3234 &mut env,
3235 "AGENTOS_EXTRA_FS_WRITE_PATHS",
3236 &pyodide_cache_path,
3237 false,
3238 );
3239 let context = self
3240 .python_engine
3241 .create_context(CreatePythonContextRequest {
3242 vm_id: vm_id.clone(),
3243 pyodide_dist_path,
3244 });
3245 let execution = self
3246 .python_engine
3247 .start_execution(StartPythonExecutionRequest {
3248 vm_id: vm_id.clone(),
3249 context_id: context.context_id,
3250 code: resolved.entrypoint.clone(),
3251 file_path: python_file_path,
3252 env: env.clone(),
3253 cwd: resolved.host_cwd.clone(),
3254 limits: python_execution_limits(vm),
3255 guest_runtime: guest_runtime_identity(vm, None, None),
3256 })
3257 .map_err(python_error)?;
3258 (ActiveExecution::Python(execution), env.clone())
3259 }
3260 GuestRuntimeKind::WebAssembly => {
3261 let wasm_limits = wasm_execution_limits(vm);
3262 let wasm_guest_runtime =
3263 guest_runtime_identity(vm, Some(u64::from(kernel_pid)), Some(0));
3264 let wasm_permission_tier = resolved.wasm_permission_tier.unwrap_or_else(|| {
3265 resolve_wasm_permission_tier(
3266 vm,
3267 Some(&resolved.command),
3268 None,
3269 &resolved.entrypoint,
3270 )
3271 });
3272 let context = self.wasm_engine.create_context(CreateWasmContextRequest {
3273 vm_id: vm_id.clone(),
3274 module_path: Some(resolved.entrypoint.clone()),
3275 });
3276 let execution = self
3277 .wasm_engine
3278 .start_execution(StartWasmExecutionRequest {
3279 vm_id: vm_id.clone(),
3280 context_id: context.context_id,
3281 argv: resolved.process_args.clone(),
3282 env: env.clone(),
3283 cwd: resolved.host_cwd.clone(),
3284 permission_tier: execution_wasm_permission_tier(wasm_permission_tier),
3285 limits: wasm_limits,
3286 guest_runtime: wasm_guest_runtime,
3287 })
3288 .map_err(wasm_error)?;
3289 (ActiveExecution::Wasm(Box::new(execution)), env)
3290 }
3291 };
3292 let child_pid = execution.child_pid();
3293 let kernel_stdin_writer_fd = install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?;
3294 vm.active_processes.insert(
3295 payload.process_id.clone(),
3296 ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
3297 .with_kernel_stdin_writer_fd(kernel_stdin_writer_fd)
3298 .with_guest_cwd(resolved.guest_cwd.clone())
3299 .with_env(process_env)
3300 .with_host_cwd(resolved.host_cwd.clone()),
3301 );
3302 self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
3303
3304 Ok(DispatchResult {
3305 response: self.respond(
3306 request,
3307 ResponsePayload::ProcessStarted(ProcessStartedResponse {
3308 process_id: payload.process_id,
3309 pid: Some(if child_pid == 0 {
3310 kernel_pid
3311 } else {
3312 child_pid
3313 }),
3314 }),
3315 ),
3316 events: Vec::new(),
3317 })
3318 }
3319
3320 pub(crate) async fn write_stdin(
3321 &mut self,
3322 request: &RequestFrame,
3323 payload: WriteStdinRequest,
3324 ) -> Result<DispatchResult, SidecarError> {
3325 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3326 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3327
3328 let vm = self
3329 .vms
3330 .get_mut(&vm_id)
3331 .ok_or_else(|| missing_vm_error(&vm_id))?;
3332 let process = vm
3333 .active_processes
3334 .get_mut(&payload.process_id)
3335 .ok_or_else(|| {
3336 SidecarError::InvalidState(format!(
3337 "VM {vm_id} has no active process {}",
3338 payload.process_id
3339 ))
3340 })?;
3341 process.execution.write_stdin(&payload.chunk)?;
3342 write_kernel_process_stdin(&mut vm.kernel, process, &payload.chunk)?;
3343
3344 Ok(DispatchResult {
3345 response: self.respond(
3346 request,
3347 ResponsePayload::StdinWritten(StdinWrittenResponse {
3348 process_id: payload.process_id,
3349 accepted_bytes: payload.chunk.len() as u64,
3350 }),
3351 ),
3352 events: Vec::new(),
3353 })
3354 }
3355
3356 pub(crate) async fn close_stdin(
3357 &mut self,
3358 request: &RequestFrame,
3359 payload: CloseStdinRequest,
3360 ) -> Result<DispatchResult, SidecarError> {
3361 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3362 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3363
3364 let vm = self
3365 .vms
3366 .get_mut(&vm_id)
3367 .ok_or_else(|| missing_vm_error(&vm_id))?;
3368 let process = vm
3369 .active_processes
3370 .get_mut(&payload.process_id)
3371 .ok_or_else(|| {
3372 SidecarError::InvalidState(format!(
3373 "VM {vm_id} has no active process {}",
3374 payload.process_id
3375 ))
3376 })?;
3377 process.execution.close_stdin()?;
3378 close_kernel_process_stdin(&mut vm.kernel, process)?;
3379
3380 Ok(DispatchResult {
3381 response: self.respond(
3382 request,
3383 ResponsePayload::StdinClosed(StdinClosedResponse {
3384 process_id: payload.process_id,
3385 }),
3386 ),
3387 events: Vec::new(),
3388 })
3389 }
3390
3391 pub(crate) async fn kill_process(
3392 &mut self,
3393 request: &RequestFrame,
3394 payload: KillProcessRequest,
3395 ) -> Result<DispatchResult, SidecarError> {
3396 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3397 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3398 self.kill_process_internal(&vm_id, &payload.process_id, &payload.signal)?;
3399
3400 Ok(DispatchResult {
3401 response: self.respond(
3402 request,
3403 ResponsePayload::ProcessKilled(ProcessKilledResponse {
3404 process_id: payload.process_id,
3405 }),
3406 ),
3407 events: Vec::new(),
3408 })
3409 }
3410
3411 pub(crate) async fn find_listener(
3412 &mut self,
3413 request: &RequestFrame,
3414 payload: FindListenerRequest,
3415 ) -> Result<DispatchResult, SidecarError> {
3416 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3417 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3418 require_vm_inspection_permission(
3419 &self.bridge,
3420 &vm_id,
3421 "network.inspect",
3422 "network",
3423 &socket_query_resource(SocketQueryKind::TcpListener, &payload),
3424 )?;
3425
3426 let listener =
3427 find_socket_state_entry(self.vms.get(&vm_id), SocketQueryKind::TcpListener, &payload)?;
3428
3429 Ok(DispatchResult {
3430 response: self.respond(
3431 request,
3432 ResponsePayload::ListenerSnapshot(ListenerSnapshotResponse { listener }),
3433 ),
3434 events: Vec::new(),
3435 })
3436 }
3437
3438 pub(crate) async fn get_process_snapshot(
3439 &mut self,
3440 request: &RequestFrame,
3441 _payload: GetProcessSnapshotRequest,
3442 ) -> Result<DispatchResult, SidecarError> {
3443 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3444 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3445 require_vm_inspection_permission(
3446 &self.bridge,
3447 &vm_id,
3448 "process.inspect",
3449 "process",
3450 "process://snapshot",
3451 )?;
3452
3453 let processes = self
3454 .vms
3455 .get_mut(&vm_id)
3456 .map(|vm| {
3457 prune_exited_process_snapshots(vm);
3458 snapshot_vm_processes(vm)
3459 })
3460 .unwrap_or_default();
3461
3462 Ok(DispatchResult {
3463 response: self.respond(
3464 request,
3465 ResponsePayload::ProcessSnapshot(ProcessSnapshotResponse { processes }),
3466 ),
3467 events: Vec::new(),
3468 })
3469 }
3470
3471 pub(crate) async fn find_bound_udp(
3472 &mut self,
3473 request: &RequestFrame,
3474 payload: FindBoundUdpRequest,
3475 ) -> Result<DispatchResult, SidecarError> {
3476 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3477 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3478
3479 let lookup_request = FindListenerRequest {
3480 host: payload.host,
3481 port: payload.port,
3482 path: None,
3483 };
3484 require_vm_inspection_permission(
3485 &self.bridge,
3486 &vm_id,
3487 "network.inspect",
3488 "network",
3489 &socket_query_resource(SocketQueryKind::UdpBound, &lookup_request),
3490 )?;
3491 let socket = find_socket_state_entry(
3492 self.vms.get(&vm_id),
3493 SocketQueryKind::UdpBound,
3494 &lookup_request,
3495 )?;
3496
3497 Ok(DispatchResult {
3498 response: self.respond(
3499 request,
3500 ResponsePayload::BoundUdpSnapshot(BoundUdpSnapshotResponse { socket }),
3501 ),
3502 events: Vec::new(),
3503 })
3504 }
3505
3506 pub(crate) async fn vm_fetch(
3507 &mut self,
3508 request: &RequestFrame,
3509 payload: VmFetchRequest,
3510 ) -> Result<DispatchResult, SidecarError> {
3511 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3512 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3513
3514 let vm = self
3515 .vms
3516 .get_mut(&vm_id)
3517 .ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
3518 let target_path = if payload.path.starts_with('/') {
3519 payload.path.clone()
3520 } else {
3521 format!("/{}", payload.path)
3522 };
3523 let request_url = Url::parse(&format!("http://127.0.0.1:{}{target_path}", payload.port))
3524 .map_err(|error| {
3525 SidecarError::InvalidState(format!(
3526 "invalid vm.fetch target {target_path:?}: {error}"
3527 ))
3528 })?;
3529 let header_values: BTreeMap<String, Value> = serde_json::from_str(&payload.headers_json)
3530 .map_err(|error| {
3531 SidecarError::InvalidState(format!(
3532 "vm.fetch headers_json must be valid JSON: {error}"
3533 ))
3534 })?;
3535 let options = JavascriptHttpRequestOptions {
3536 method: Some(payload.method),
3537 headers: header_values,
3538 body: payload.body,
3539 reject_unauthorized: None,
3540 };
3541 let headers = parse_http_header_collection(&options.headers, "vm.fetch headers")?;
3542 let target_process_id = find_kernel_http_listener_process(vm, payload.port);
3543 if let Some(target_process_id) = target_process_id {
3544 let max_fetch_response_bytes = vm.limits.http.max_fetch_response_bytes;
3545 let response_json = match dispatch_kernel_http_fetch(
3546 &self.bridge,
3547 &vm_id,
3548 vm,
3549 &target_process_id,
3550 payload.port,
3551 &target_path,
3552 &options,
3553 &headers,
3554 max_fetch_response_bytes,
3555 ) {
3556 Ok(response_json) => response_json,
3557 Err(error) => {
3558 if let Some(exit_code) = kernel_http_fetch_target_exit_code(&error) {
3559 let _ = vm;
3560 self.finish_active_process_exit(&vm_id, &target_process_id, exit_code)?;
3561 }
3562 return Err(error);
3563 }
3564 };
3565 let response = self.respond(
3566 request,
3567 ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
3568 );
3569 ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
3570
3571 return Ok(DispatchResult {
3572 response,
3573 events: Vec::new(),
3574 });
3575 }
3576
3577 let Some((target_process_id, server_id)) =
3578 vm.active_processes
3579 .iter()
3580 .find_map(|(process_id, process)| {
3581 process
3582 .http_servers
3583 .iter()
3584 .find(|(_, server)| server.guest_local_addr.port() == payload.port)
3585 .map(|(server_id, _)| (process_id.clone(), *server_id))
3586 })
3587 else {
3588 return Err(SidecarError::Execution(format!(
3589 "vm.fetch could not find a guest HTTP listener on port {}",
3590 payload.port
3591 )));
3592 };
3593 let socket_paths = build_javascript_socket_path_context(vm)?;
3594 let resource_limits = vm.kernel.resource_limits().clone();
3595 let process = vm
3596 .active_processes
3597 .get_mut(&target_process_id)
3598 .ok_or_else(|| {
3599 SidecarError::InvalidState(format!(
3600 "vm.fetch target process disappeared: {target_process_id}"
3601 ))
3602 })?;
3603 let request_json = serialize_http_loopback_request(&request_url, &options, &headers)?;
3604 let response_json = dispatch_loopback_http_request(LoopbackHttpDispatchRequest {
3605 bridge: &self.bridge,
3606 vm_id: &vm_id,
3607 dns: &vm.dns,
3608 socket_paths: &socket_paths,
3609 kernel: &mut vm.kernel,
3610 process,
3611 resource_limits: &resource_limits,
3612 server_id,
3613 request_json: &request_json,
3614 })?;
3615
3616 let response = self.respond(
3617 request,
3618 ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
3619 );
3620 ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
3621
3622 Ok(DispatchResult {
3623 response,
3624 events: Vec::new(),
3625 })
3626 }
3627
3628 pub(crate) async fn get_signal_state(
3629 &mut self,
3630 request: &RequestFrame,
3631 payload: GetSignalStateRequest,
3632 ) -> Result<DispatchResult, SidecarError> {
3633 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3634 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3635
3636 let handlers = self
3637 .vms
3638 .get(&vm_id)
3639 .and_then(|vm| vm.signal_states.get(&payload.process_id))
3640 .cloned()
3641 .unwrap_or_default();
3642
3643 Ok(DispatchResult {
3644 response: self.respond(
3645 request,
3646 ResponsePayload::SignalState(SignalStateResponse {
3647 process_id: payload.process_id,
3648 handlers: handlers.into_iter().collect(),
3649 }),
3650 ),
3651 events: Vec::new(),
3652 })
3653 }
3654
3655 pub(crate) async fn get_zombie_timer_count(
3656 &mut self,
3657 request: &RequestFrame,
3658 _payload: GetZombieTimerCountRequest,
3659 ) -> Result<DispatchResult, SidecarError> {
3660 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3661 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3662
3663 let count = self
3664 .vms
3665 .get(&vm_id)
3666 .map(|vm| vm.kernel.zombie_timer_count() as u64)
3667 .unwrap_or_default();
3668
3669 Ok(DispatchResult {
3670 response: self.respond(
3671 request,
3672 ResponsePayload::ZombieTimerCount(ZombieTimerCountResponse { count }),
3673 ),
3674 events: Vec::new(),
3675 })
3676 }
3677
3678 pub(crate) fn kill_process_internal(
3679 &mut self,
3680 vm_id: &str,
3681 process_id: &str,
3682 signal: &str,
3683 ) -> Result<(), SidecarError> {
3684 let signal_name = signal.to_owned();
3685 let signal = parse_signal(signal)?;
3686 let vm = self
3687 .vms
3688 .get_mut(vm_id)
3689 .ok_or_else(|| SidecarError::InvalidState(format!("unknown sidecar VM {vm_id}")))?;
3690 let process = vm.active_processes.get_mut(process_id).ok_or_else(|| {
3691 SidecarError::InvalidState(format!("VM {vm_id} has no active process {process_id}"))
3692 })?;
3693 let kernel_pid = process.kernel_pid;
3694
3695 enum KillBehavior {
3696 Tool,
3697 SharedV8StateOnly,
3698 SharedV8Continue,
3699 SharedV8Terminate,
3700 SharedV8DispatchOrTerminate,
3701 Noop,
3702 HostPid(u32),
3703 }
3704
3705 let behavior = match &process.execution {
3706 ActiveExecution::Tool(_) => KillBehavior::Tool,
3707 ActiveExecution::Javascript(execution)
3708 if execution.uses_shared_v8_runtime() && matches!(signal, 0 | libc::SIGSTOP) =>
3709 {
3710 KillBehavior::SharedV8StateOnly
3711 }
3712 ActiveExecution::Javascript(execution)
3713 if execution.uses_shared_v8_runtime() && signal == libc::SIGCONT =>
3714 {
3715 KillBehavior::SharedV8Continue
3716 }
3717 ActiveExecution::Wasm(execution)
3718 if execution.uses_shared_v8_runtime()
3719 && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
3720 {
3721 KillBehavior::SharedV8StateOnly
3722 }
3723 ActiveExecution::Python(execution)
3724 if execution.uses_shared_v8_runtime()
3725 && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
3726 {
3727 KillBehavior::SharedV8StateOnly
3728 }
3729 ActiveExecution::Javascript(execution)
3730 if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
3731 {
3732 KillBehavior::SharedV8Terminate
3733 }
3734 ActiveExecution::Wasm(execution)
3735 if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
3736 {
3737 KillBehavior::SharedV8Terminate
3738 }
3739 ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime() => {
3740 KillBehavior::SharedV8DispatchOrTerminate
3741 }
3742 ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime() => {
3743 KillBehavior::SharedV8Terminate
3744 }
3745 ActiveExecution::Python(execution) if execution.uses_shared_v8_runtime() => {
3746 KillBehavior::SharedV8Terminate
3747 }
3748 ActiveExecution::Javascript(execution) if execution.child_pid() == 0 => {
3749 KillBehavior::Noop
3750 }
3751 _ => KillBehavior::HostPid(process.execution.child_pid()),
3752 };
3753
3754 match behavior {
3755 KillBehavior::Tool => {
3756 let ActiveExecution::Tool(execution) = &process.execution else {
3757 unreachable!("kill behavior must match tool execution");
3758 };
3759 if signal != 0 {
3760 execution.cancelled.store(true, Ordering::Relaxed);
3761 process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
3762 128 + signal,
3763 ))?;
3764 }
3765 }
3766 KillBehavior::SharedV8StateOnly => {
3767 if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
3768 vm.kernel
3769 .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
3770 .map_err(kernel_error)?;
3771 }
3772 }
3773 KillBehavior::SharedV8Continue => {
3774 vm.kernel
3775 .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
3776 .map_err(kernel_error)?;
3777 if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
3778 process.execution.terminate()?;
3779 }
3780 }
3781 KillBehavior::SharedV8Terminate => {
3782 if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
3783 close_kernel_process_stdin(&mut vm.kernel, process)?;
3784 }
3785 process.execution.terminate()?;
3786 let needs_synthetic_exit = matches!(process.execution, ActiveExecution::Wasm(_))
3787 || (signal == SIGKILL
3788 && matches!(process.execution, ActiveExecution::Javascript(_)));
3789 if signal != 0 && needs_synthetic_exit {
3790 process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
3791 128 + signal,
3792 ))?;
3793 }
3794 }
3795 KillBehavior::SharedV8DispatchOrTerminate => {
3796 if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
3797 process.execution.terminate()?;
3798 }
3799 }
3800 KillBehavior::Noop => {}
3801 KillBehavior::HostPid(pid) => {
3802 if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
3803 close_kernel_process_stdin(&mut vm.kernel, process)?;
3804 }
3805 signal_runtime_process(pid, signal)?;
3806 }
3807 }
3808 emit_security_audit_event(
3809 &self.bridge,
3810 vm_id,
3811 "security.process.kill",
3812 audit_fields([
3813 (String::from("source"), String::from("control_plane")),
3814 (String::from("source_pid"), String::from("0")),
3815 (String::from("target_pid"), process.kernel_pid.to_string()),
3816 (String::from("process_id"), process_id.to_owned()),
3817 (String::from("signal"), signal_name),
3818 (
3819 String::from("host_pid"),
3820 process.execution.child_pid().to_string(),
3821 ),
3822 ]),
3823 );
3824 Ok(())
3825 }
3826
3827 pub async fn pump_process_events(
3828 &mut self,
3829 ownership: &OwnershipScope,
3830 ) -> Result<bool, SidecarError> {
3831 let mut emitted_any = false;
3832
3833 let mut queued_envelopes = Vec::new();
3834 {
3835 let pending_capacity = self.pending_process_event_capacity();
3836 let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
3837 SidecarError::InvalidState(String::from("process event receiver unavailable"))
3838 })?;
3839 loop {
3840 if queued_envelopes.len() >= pending_capacity {
3841 if receiver.is_empty() {
3842 break;
3843 }
3844 return Err(process_event_queue_overflow_error());
3845 }
3846 match receiver.try_recv() {
3847 Ok(envelope) => {
3848 queued_envelopes.push(envelope);
3849 emitted_any = true;
3850 }
3851 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
3852 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
3853 }
3854 }
3855 }
3856 for envelope in queued_envelopes {
3857 self.queue_pending_process_event(envelope)?;
3858 }
3859
3860 let vm_ids = self.vm_ids_for_scope(ownership)?;
3861 for vm_id in vm_ids {
3862 while let Some(vm) = self.vms.get(&vm_id) {
3863 let connection_id = vm.connection_id.clone();
3864 let session_id = vm.session_id.clone();
3865 let process_ids = self
3866 .vms
3867 .get(&vm_id)
3868 .map(|vm| vm.active_processes.keys().cloned().collect::<Vec<_>>())
3869 .unwrap_or_default();
3870 let mut emitted_this_pass = false;
3871
3872 for process_id in process_ids {
3873 if self
3874 .vms
3875 .get(&vm_id)
3876 .is_some_and(|vm| vm.detached_child_processes.contains(&process_id))
3877 {
3878 continue;
3879 }
3880 enum ProcessPollResult {
3881 Event(Box<Option<ActiveExecutionEvent>>),
3882 RecoverClosedChannel,
3883 }
3884 let poll_result = {
3885 let Some(vm) = self.vms.get_mut(&vm_id) else {
3886 continue;
3887 };
3888 let Some(process) = vm.active_processes.get_mut(&process_id) else {
3889 continue;
3890 };
3891 if let Some(event) = process.pending_execution_events.pop_front() {
3892 ProcessPollResult::Event(Box::new(Some(event)))
3893 } else {
3894 match process.execution.poll_event(Duration::ZERO).await {
3895 Ok(event) => ProcessPollResult::Event(Box::new(event)),
3896 Err(SidecarError::Execution(message))
3897 if (process.runtime == GuestRuntimeKind::JavaScript
3898 && closed_javascript_event_channel(&message))
3899 || (process.runtime == GuestRuntimeKind::Python
3900 && closed_python_event_channel(&message))
3901 || (process.runtime == GuestRuntimeKind::WebAssembly
3902 && closed_wasm_event_channel(&message)) =>
3903 {
3904 ProcessPollResult::RecoverClosedChannel
3905 }
3906 Err(other) => return Err(other),
3907 }
3908 }
3909 };
3910 let event = match poll_result {
3911 ProcessPollResult::Event(event) => *event,
3912 ProcessPollResult::RecoverClosedChannel => {
3913 self.recover_closed_root_runtime_process_event(&vm_id, &process_id)?
3914 }
3915 };
3916
3917 let Some(event) = event else {
3918 continue;
3919 };
3920
3921 if Self::internal_execution_event(&event) {
3922 self.handle_execution_event(&vm_id, &process_id, event)?;
3927 } else {
3928 self.queue_pending_process_event(ProcessEventEnvelope {
3929 connection_id: connection_id.clone(),
3930 session_id: session_id.clone(),
3931 vm_id: vm_id.clone(),
3932 process_id: process_id.clone(),
3933 event,
3934 })?;
3935 }
3936 emitted_any = true;
3937 emitted_this_pass = true;
3938 }
3939
3940 if !emitted_this_pass {
3941 break;
3942 }
3943 }
3944
3945 if self.pump_detached_child_process_events(&vm_id)? {
3946 emitted_any = true;
3947 }
3948 }
3949
3950 Ok(emitted_any)
3951 }
3952
3953 fn internal_execution_event(event: &ActiveExecutionEvent) -> bool {
3954 matches!(
3955 event,
3956 ActiveExecutionEvent::JavascriptSyncRpcRequest(_)
3957 | ActiveExecutionEvent::PythonVfsRpcRequest(_)
3958 | ActiveExecutionEvent::SignalState { .. }
3959 )
3960 }
3961
3962 fn recover_closed_root_runtime_process_event(
3963 &mut self,
3964 vm_id: &str,
3965 process_id: &str,
3966 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
3967 let Some(vm) = self.vms.get_mut(vm_id) else {
3968 return Ok(None);
3969 };
3970 let Some(process) = vm.active_processes.get(process_id) else {
3971 return Ok(None);
3972 };
3973 if process.execution.uses_shared_v8_runtime() {
3974 return Ok(None);
3975 }
3976 if process.runtime != GuestRuntimeKind::JavaScript
3977 && process.runtime != GuestRuntimeKind::Python
3978 && process.runtime != GuestRuntimeKind::WebAssembly
3979 {
3980 return Ok(None);
3981 }
3982 let runtime_child_pid = process.execution.child_pid();
3983 if runtime_child_pid == 0 {
3984 return Ok(None);
3985 }
3986 if let Some(status) = runtime_child_exit_status(runtime_child_pid)? {
3987 return Ok(Some(ActiveExecutionEvent::Exited(status)));
3988 }
3989 if runtime_child_is_alive(runtime_child_pid)? {
3990 return Ok(None);
3991 }
3992 Ok(Some(ActiveExecutionEvent::Exited(0)))
3993 }
3994
3995 fn active_process_by_path<'a>(
3996 process: &'a ActiveProcess,
3997 child_path: &[&str],
3998 ) -> Option<&'a ActiveProcess> {
3999 let mut current = process;
4000 for child_id in child_path {
4001 current = current.child_processes.get(*child_id)?;
4002 }
4003 Some(current)
4004 }
4005
4006 fn active_process_by_path_mut<'a>(
4007 process: &'a mut ActiveProcess,
4008 child_path: &[&str],
4009 ) -> Option<&'a mut ActiveProcess> {
4010 let mut current = process;
4011 for child_id in child_path {
4012 current = current.child_processes.get_mut(*child_id)?;
4013 }
4014 Some(current)
4015 }
4016
4017 fn active_process_by_owned_path_mut<'a>(
4018 process: &'a mut ActiveProcess,
4019 child_path: &[String],
4020 ) -> Option<&'a mut ActiveProcess> {
4021 let mut current = process;
4022 for child_id in child_path {
4023 current = current.child_processes.get_mut(child_id)?;
4024 }
4025 Some(current)
4026 }
4027
4028 fn active_process_path_by_kernel_pid(
4029 process: &ActiveProcess,
4030 kernel_pid: u32,
4031 ) -> Option<Vec<String>> {
4032 if process.kernel_pid == kernel_pid {
4033 return Some(Vec::new());
4034 }
4035
4036 for (child_id, child) in &process.child_processes {
4037 let Some(mut path) = Self::active_process_path_by_kernel_pid(child, kernel_pid) else {
4038 continue;
4039 };
4040 path.insert(0, child_id.clone());
4041 return Some(path);
4042 }
4043
4044 None
4045 }
4046
4047 fn descendant_parent_process<'a>(
4048 vm: &'a VmState,
4049 process_id: &str,
4050 child_path: &[&str],
4051 ) -> Option<&'a ActiveProcess> {
4052 let root = vm.active_processes.get(process_id)?;
4053 Self::active_process_by_path(root, child_path)
4054 }
4055
4056 fn descendant_parent_process_mut<'a>(
4057 vm: &'a mut VmState,
4058 process_id: &str,
4059 child_path: &[&str],
4060 ) -> Option<&'a mut ActiveProcess> {
4061 let root = vm.active_processes.get_mut(process_id)?;
4062 Self::active_process_by_path_mut(root, child_path)
4063 }
4064
4065 fn child_process_path_label(process_id: &str, child_path: &[&str]) -> String {
4066 if child_path.is_empty() {
4067 process_id.to_owned()
4068 } else {
4069 format!("{process_id}/{}", child_path.join("/"))
4070 }
4071 }
4072
4073 fn adopt_detached_child_processes(
4074 current_process_id: &str,
4075 process: &mut ActiveProcess,
4076 ) -> Vec<(String, ActiveProcess)> {
4077 let mut adopted = Vec::new();
4078 let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
4079 for child_id in child_ids {
4080 let child_process_id = format!("{current_process_id}/{child_id}");
4081 let Some(mut child) = process.child_processes.remove(&child_id) else {
4082 continue;
4083 };
4084 if child.detached {
4085 adopted.push((child_process_id, child));
4086 continue;
4087 }
4088
4089 adopted.extend(Self::adopt_detached_child_processes(
4090 &child_process_id,
4091 &mut child,
4092 ));
4093 process.child_processes.insert(child_id, child);
4094 }
4095 adopted
4096 }
4097
4098 fn child_process_signal_key<'a>(process_id: &'a str, child_path: &[&'a str]) -> &'a str {
4099 child_path.last().copied().unwrap_or(process_id)
4100 }
4101
4102 fn resolve_detached_child_process_path(
4103 vm: &VmState,
4104 detached_process_id: &str,
4105 ) -> Option<(String, Vec<String>)> {
4106 let root_process_id = vm
4107 .active_processes
4108 .keys()
4109 .filter(|candidate| {
4110 detached_process_id == candidate.as_str()
4111 || detached_process_id
4112 .strip_prefix(candidate.as_str())
4113 .is_some_and(|remainder| remainder.starts_with('/'))
4114 })
4115 .max_by_key(|candidate| candidate.len())?
4116 .clone();
4117
4118 let remainder = detached_process_id
4119 .strip_prefix(root_process_id.as_str())
4120 .unwrap_or_default();
4121 if remainder.is_empty() {
4122 return Some((root_process_id, Vec::new()));
4123 }
4124
4125 Some((
4126 root_process_id,
4127 remainder
4128 .trim_start_matches('/')
4129 .split('/')
4130 .map(str::to_owned)
4131 .collect(),
4132 ))
4133 }
4134
4135 fn pump_detached_child_process_events(&mut self, vm_id: &str) -> Result<bool, SidecarError> {
4136 let detached_process_ids = self
4137 .vms
4138 .get(vm_id)
4139 .map(|vm| {
4140 vm.detached_child_processes
4141 .iter()
4142 .cloned()
4143 .collect::<Vec<_>>()
4144 })
4145 .unwrap_or_default();
4146 let mut emitted_any = false;
4147 for detached_process_id in detached_process_ids {
4148 let Some((root_process_id, child_path)) = self
4149 .vms
4150 .get(vm_id)
4151 .and_then(|vm| Self::resolve_detached_child_process_path(vm, &detached_process_id))
4152 else {
4153 if let Some(vm) = self.vms.get_mut(vm_id) {
4154 vm.detached_child_processes.remove(&detached_process_id);
4155 }
4156 continue;
4157 };
4158 if child_path.is_empty() {
4159 loop {
4160 enum ProcessPollResult {
4161 Event(Box<Option<ActiveExecutionEvent>>),
4162 RecoverClosedChannel,
4163 }
4164 let poll_result = {
4165 let Some(vm) = self.vms.get_mut(vm_id) else {
4166 break;
4167 };
4168 let Some(process) = vm.active_processes.get_mut(&root_process_id) else {
4169 break;
4170 };
4171 if let Some(event) = process.pending_execution_events.pop_front() {
4172 ProcessPollResult::Event(Box::new(Some(event)))
4173 } else {
4174 match process.execution.poll_event_blocking(Duration::ZERO) {
4175 Ok(event) => ProcessPollResult::Event(Box::new(event)),
4176 Err(SidecarError::Execution(message))
4177 if (process.runtime == GuestRuntimeKind::JavaScript
4178 && closed_javascript_event_channel(&message))
4179 || (process.runtime == GuestRuntimeKind::Python
4180 && closed_python_event_channel(&message))
4181 || (process.runtime == GuestRuntimeKind::WebAssembly
4182 && closed_wasm_event_channel(&message)) =>
4183 {
4184 ProcessPollResult::RecoverClosedChannel
4185 }
4186 Err(error) => return Err(error),
4187 }
4188 }
4189 };
4190 let event = match poll_result {
4191 ProcessPollResult::Event(event) => *event,
4192 ProcessPollResult::RecoverClosedChannel => {
4193 self.recover_closed_root_runtime_process_event(vm_id, &root_process_id)?
4194 }
4195 };
4196 let Some(event) = event else {
4197 break;
4198 };
4199 let Some((connection_id, session_id)) = self
4200 .vms
4201 .get(vm_id)
4202 .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
4203 else {
4204 break;
4205 };
4206 match event {
4207 ActiveExecutionEvent::Stdout(chunk) => {
4208 self.queue_pending_process_event(ProcessEventEnvelope {
4209 connection_id,
4210 session_id,
4211 vm_id: vm_id.to_owned(),
4212 process_id: detached_process_id.clone(),
4213 event: ActiveExecutionEvent::Stdout(chunk),
4214 })?;
4215 emitted_any = true;
4216 }
4217 ActiveExecutionEvent::Stderr(chunk) => {
4218 self.queue_pending_process_event(ProcessEventEnvelope {
4219 connection_id,
4220 session_id,
4221 vm_id: vm_id.to_owned(),
4222 process_id: detached_process_id.clone(),
4223 event: ActiveExecutionEvent::Stderr(chunk),
4224 })?;
4225 emitted_any = true;
4226 }
4227 ActiveExecutionEvent::Exited(exit_code) => {
4228 if let Some(vm) = self.vms.get_mut(vm_id) {
4229 vm.detached_child_processes.remove(&detached_process_id);
4230 }
4231 self.queue_pending_process_event(ProcessEventEnvelope {
4232 connection_id,
4233 session_id,
4234 vm_id: vm_id.to_owned(),
4235 process_id: detached_process_id.clone(),
4236 event: ActiveExecutionEvent::Exited(exit_code),
4237 })?;
4238 emitted_any = true;
4239 break;
4240 }
4241 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
4242 self.handle_javascript_sync_rpc_request(
4243 vm_id,
4244 &root_process_id,
4245 request,
4246 )?;
4247 }
4248 ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
4249 self.handle_python_vfs_rpc_request(vm_id, &root_process_id, *request)?;
4250 }
4251 ActiveExecutionEvent::SignalState {
4252 signal,
4253 registration,
4254 } => {
4255 if let Some(vm) = self.vms.get_mut(vm_id) {
4256 vm.signal_states
4257 .entry(root_process_id.clone())
4258 .or_default()
4259 .insert(signal, registration);
4260 }
4261 }
4262 }
4263 }
4264 continue;
4265 }
4266
4267 let parent_path = child_path[..child_path.len() - 1]
4268 .iter()
4269 .map(String::as_str)
4270 .collect::<Vec<_>>();
4271 let child_process_id = child_path.last().expect("child path cannot be empty");
4272
4273 loop {
4274 let event = match self.poll_descendant_javascript_child_process(
4275 vm_id,
4276 &root_process_id,
4277 &parent_path,
4278 child_process_id,
4279 0,
4280 ) {
4281 Ok(event) => event,
4282 Err(SidecarError::InvalidState(message))
4283 if message.contains("unknown child process")
4284 || message.contains("unknown child process path") =>
4285 {
4286 if let Some(vm) = self.vms.get_mut(vm_id) {
4287 vm.detached_child_processes.remove(&detached_process_id);
4288 }
4289 break;
4290 }
4291 Err(error) if is_javascript_child_process_gone_error(&error) => {
4292 if let Some(vm) = self.vms.get_mut(vm_id) {
4293 vm.detached_child_processes.remove(&detached_process_id);
4294 }
4295 break;
4296 }
4297 Err(error) => return Err(error),
4298 };
4299
4300 let Some(event_type) = event.get("type").and_then(Value::as_str) else {
4301 break;
4302 };
4303 let Some((connection_id, session_id)) = self
4304 .vms
4305 .get(vm_id)
4306 .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
4307 else {
4308 break;
4309 };
4310
4311 let envelope = match event_type {
4312 "stdout" => Some(ProcessEventEnvelope {
4313 connection_id: connection_id.clone(),
4314 session_id: session_id.clone(),
4315 vm_id: vm_id.to_owned(),
4316 process_id: detached_process_id.clone(),
4317 event: ActiveExecutionEvent::Stdout(javascript_sync_rpc_bytes_arg(
4318 &[event.get("data").cloned().unwrap_or(Value::Null)],
4319 0,
4320 "detached child_process stdout",
4321 )?),
4322 }),
4323 "stderr" => Some(ProcessEventEnvelope {
4324 connection_id: connection_id.clone(),
4325 session_id: session_id.clone(),
4326 vm_id: vm_id.to_owned(),
4327 process_id: detached_process_id.clone(),
4328 event: ActiveExecutionEvent::Stderr(javascript_sync_rpc_bytes_arg(
4329 &[event.get("data").cloned().unwrap_or(Value::Null)],
4330 0,
4331 "detached child_process stderr",
4332 )?),
4333 }),
4334 "exit" => {
4335 if let Some(vm) = self.vms.get_mut(vm_id) {
4336 vm.detached_child_processes.remove(&detached_process_id);
4337 }
4338 Some(ProcessEventEnvelope {
4339 connection_id,
4340 session_id,
4341 vm_id: vm_id.to_owned(),
4342 process_id: detached_process_id.clone(),
4343 event: ActiveExecutionEvent::Exited(
4344 event
4345 .get("exitCode")
4346 .and_then(Value::as_i64)
4347 .map(|value| value as i32)
4348 .unwrap_or(1),
4349 ),
4350 })
4351 }
4352 _ => None,
4353 };
4354
4355 let Some(envelope) = envelope else {
4356 break;
4357 };
4358 self.queue_pending_process_event(envelope)?;
4359 emitted_any = true;
4360
4361 if event_type == "exit" {
4362 break;
4363 }
4364 }
4365 }
4366
4367 Ok(emitted_any)
4368 }
4369 pub(crate) fn drain_queued_descendant_javascript_child_process_events(
4370 &mut self,
4371 vm_id: &str,
4372 process_id: &str,
4373 child_path: &[&str],
4374 ) -> Result<(), SidecarError> {
4375 if child_path.is_empty() {
4376 return Ok(());
4377 }
4378 let target_process_id = Self::child_process_path_label(process_id, child_path);
4379 let mut child_capacity = self
4380 .vms
4381 .get(vm_id)
4382 .and_then(|vm| vm.active_processes.get(process_id))
4383 .and_then(|root| descendant_pending_execution_event_capacity(root, child_path));
4384
4385 let mut deferred = VecDeque::new();
4386 while let Some(envelope) = self.pending_process_events.pop_front() {
4387 if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
4388 if matches!(child_capacity, Some(0)) {
4389 self.pending_process_events.push_front(envelope);
4390 while let Some(deferred_envelope) = deferred.pop_back() {
4391 self.pending_process_events.push_front(deferred_envelope);
4392 }
4393 return Err(process_event_queue_overflow_error());
4394 }
4395 if let Some(vm) = self.vms.get_mut(vm_id) {
4396 if let Some(root) = vm.active_processes.get_mut(process_id) {
4397 if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
4398 child.queue_pending_execution_event(envelope.event)?;
4399 child_capacity = child_capacity.map(|capacity| capacity - 1);
4400 continue;
4401 }
4402 }
4403 }
4404 }
4405 deferred.push_back(envelope);
4406 }
4407 self.pending_process_events = deferred;
4408
4409 let mut queued = Vec::new();
4410 {
4411 let transfer_capacity = self
4412 .pending_process_event_capacity()
4413 .min(child_capacity.unwrap_or(usize::MAX));
4414 let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
4415 SidecarError::InvalidState(String::from("process event receiver unavailable"))
4416 })?;
4417 loop {
4418 if queued.len() >= transfer_capacity {
4419 if receiver.is_empty() {
4420 break;
4421 }
4422 return Err(process_event_queue_overflow_error());
4423 }
4424 match receiver.try_recv() {
4425 Ok(envelope) => queued.push(envelope),
4426 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
4427 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
4428 }
4429 }
4430 }
4431 for envelope in queued {
4432 if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
4433 if let Some(vm) = self.vms.get_mut(vm_id) {
4434 if let Some(root) = vm.active_processes.get_mut(process_id) {
4435 if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
4436 child.queue_pending_execution_event(envelope.event)?;
4437 continue;
4438 }
4439 }
4440 }
4441 }
4442 self.queue_pending_process_event(envelope)?;
4443 }
4444
4445 Ok(())
4446 }
4447
4448 pub(crate) fn handle_execution_event(
4449 &mut self,
4450 vm_id: &str,
4451 process_id: &str,
4452 event: ActiveExecutionEvent,
4453 ) -> Result<Option<EventFrame>, SidecarError> {
4454 let Some(vm) = self.vms.get(vm_id) else {
4455 log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
4456 return Ok(None);
4457 };
4458 if !vm.active_processes.contains_key(process_id) {
4459 log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
4460 return Ok(None);
4461 }
4462 let (connection_id, session_id) = { (vm.connection_id.clone(), vm.session_id.clone()) };
4463 let ownership = OwnershipScope::vm(&connection_id, &session_id, vm_id);
4464
4465 if self.capture_extension_process_output_event(vm_id, process_id, &event) {
4466 return Ok(None);
4467 }
4468
4469 match event {
4470 ActiveExecutionEvent::Stdout(chunk) => Ok(Some(EventFrame::new(
4471 ownership,
4472 EventPayload::ProcessOutput(ProcessOutputEvent {
4473 process_id: process_id.to_owned(),
4474 channel: StreamChannel::Stdout,
4475 chunk,
4476 }),
4477 ))),
4478 ActiveExecutionEvent::Stderr(chunk) => Ok(Some(EventFrame::new(
4479 ownership,
4480 EventPayload::ProcessOutput(ProcessOutputEvent {
4481 process_id: process_id.to_owned(),
4482 channel: StreamChannel::Stderr,
4483 chunk,
4484 }),
4485 ))),
4486 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
4487 self.handle_javascript_sync_rpc_request(vm_id, process_id, request)?;
4488 Ok(None)
4489 }
4490 ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
4491 self.handle_python_vfs_rpc_request(vm_id, process_id, *request)?;
4492 Ok(None)
4493 }
4494 ActiveExecutionEvent::SignalState {
4495 signal,
4496 registration,
4497 } => {
4498 let Some(vm) = self.vms.get_mut(vm_id) else {
4499 return Ok(None);
4500 };
4501 if !vm.active_processes.contains_key(process_id) {
4502 return Ok(None);
4503 }
4504 vm.signal_states
4505 .entry(process_id.to_owned())
4506 .or_default()
4507 .insert(signal, registration);
4508 Ok(None)
4509 }
4510 ActiveExecutionEvent::Exited(exit_code) => {
4511 let became_idle = self
4512 .finish_active_process_exit(vm_id, process_id, exit_code)?
4513 .unwrap_or(false);
4514
4515 if became_idle {
4516 self.bridge.emit_lifecycle(vm_id, LifecycleState::Ready)?;
4517 }
4518
4519 Ok(Some(EventFrame::new(
4520 ownership,
4521 EventPayload::ProcessExited(ProcessExitedEvent {
4522 process_id: process_id.to_owned(),
4523 exit_code,
4524 }),
4525 )))
4526 }
4527 }
4528 }
4529
4530 pub(crate) fn finish_active_process_exit(
4531 &mut self,
4532 vm_id: &str,
4533 process_id: &str,
4534 exit_code: i32,
4535 ) -> Result<Option<bool>, SidecarError> {
4536 let Some(vm) = self.vms.get_mut(vm_id) else {
4537 log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
4538 return Ok(None);
4539 };
4540 if !vm.active_processes.contains_key(process_id) {
4541 log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
4542 return Ok(None);
4543 }
4544
4545 prune_exited_process_snapshots(vm);
4546 let process_table = vm.kernel.list_processes();
4547 let Some(mut process) = vm.active_processes.remove(process_id) else {
4548 return Ok(None);
4549 };
4550 if let Some(info) = process_table.get(&process.kernel_pid) {
4551 vm.exited_process_snapshots
4552 .push_back(ExitedProcessSnapshot {
4553 captured_at: Instant::now(),
4554 process: build_process_snapshot_entry(
4555 process_id,
4556 &process,
4557 info,
4558 Some(exit_code),
4559 ),
4560 });
4561 }
4562 let detached_children = Self::adopt_detached_child_processes(process_id, &mut process);
4563 sync_process_host_writes_to_kernel(vm, &process)?;
4564 terminate_child_process_tree(&mut vm.kernel, &mut process);
4565 process.kernel_handle.finish(exit_code);
4566 let _ = vm.kernel.wait_and_reap(process.kernel_pid);
4567 vm.signal_states.remove(process_id);
4568 for (detached_process_id, detached_child) in detached_children {
4569 vm.detached_child_processes
4570 .insert(detached_process_id.clone());
4571 vm.active_processes
4572 .insert(detached_process_id, detached_child);
4573 }
4574 let became_idle = vm.active_processes.is_empty();
4575 self.prune_extension_process_resource(process_id);
4576
4577 Ok(Some(became_idle))
4578 }
4579
4580 pub(crate) fn drain_process_events_blocking_with_limit(
4581 &mut self,
4582 vm_id: &str,
4583 process_id: &str,
4584 max_events: usize,
4585 ) -> Result<Vec<ActiveExecutionEvent>, SidecarError> {
4586 let mut events = Vec::new();
4587 if max_events == 0 {
4588 return Ok(events);
4589 }
4590 let mut deadline = Instant::now() + Duration::from_millis(150);
4591
4592 loop {
4593 if events.len() >= max_events {
4594 break;
4595 }
4596 let event = {
4597 let Some(vm) = self.vms.get_mut(vm_id) else {
4598 break;
4599 };
4600 let Some(process) = vm.active_processes.get_mut(process_id) else {
4601 break;
4602 };
4603 if let Some(event) = process.pending_execution_events.pop_front() {
4604 Some(event)
4605 } else {
4606 match process.execution.poll_event_blocking(Duration::ZERO) {
4607 Ok(event) => event,
4608 Err(SidecarError::Execution(_)) => None,
4609 Err(other) => return Err(other),
4610 }
4611 }
4612 };
4613
4614 let Some(event) = event else {
4615 if Instant::now() >= deadline {
4616 break;
4617 }
4618 let blocking_wait = deadline.saturating_duration_since(Instant::now());
4619 if blocking_wait.is_zero() {
4620 break;
4621 }
4622 if events.len() >= max_events {
4623 break;
4624 }
4625 let delayed_event = {
4626 let Some(vm) = self.vms.get_mut(vm_id) else {
4627 break;
4628 };
4629 let Some(process) = vm.active_processes.get_mut(process_id) else {
4630 break;
4631 };
4632 if let Some(event) = process.pending_execution_events.pop_front() {
4633 Some(event)
4634 } else {
4635 match process.execution.poll_event_blocking(blocking_wait) {
4636 Ok(event) => event,
4637 Err(SidecarError::Execution(_)) => None,
4638 Err(other) => return Err(other),
4639 }
4640 }
4641 };
4642 let Some(event) = delayed_event else {
4643 break;
4644 };
4645 events.push(event);
4646 deadline = Instant::now() + Duration::from_millis(150);
4647 continue;
4648 };
4649 events.push(event);
4650 deadline = Instant::now() + Duration::from_millis(150);
4651 }
4652
4653 Ok(events)
4654 }
4655
4656 pub(crate) fn handle_python_vfs_rpc_request(
4657 &mut self,
4658 vm_id: &str,
4659 process_id: &str,
4660 request: PythonVfsRpcRequest,
4661 ) -> Result<(), SidecarError> {
4662 match request.method {
4663 PythonVfsRpcMethod::Read
4664 | PythonVfsRpcMethod::Write
4665 | PythonVfsRpcMethod::Stat
4666 | PythonVfsRpcMethod::ReadDir
4667 | PythonVfsRpcMethod::Mkdir => {
4668 filesystem_handle_python_vfs_rpc_request(self, vm_id, process_id, request)
4669 }
4670 PythonVfsRpcMethod::HttpRequest => {
4671 self.handle_python_http_rpc_request(vm_id, process_id, request)
4672 }
4673 PythonVfsRpcMethod::DnsLookup => {
4674 self.handle_python_dns_rpc_request(vm_id, process_id, request)
4675 }
4676 PythonVfsRpcMethod::SubprocessRun => {
4677 self.handle_python_subprocess_rpc_request(vm_id, process_id, request)
4678 }
4679 }
4680 }
4681
4682 fn handle_python_http_rpc_request(
4683 &mut self,
4684 vm_id: &str,
4685 process_id: &str,
4686 request: PythonVfsRpcRequest,
4687 ) -> Result<(), SidecarError> {
4688 let Some(vm) = self.vms.get(vm_id) else {
4689 return Ok(());
4690 };
4691 if !vm.active_processes.contains_key(process_id) {
4692 return Ok(());
4693 }
4694 let response = (|| {
4695 let url_text = request.url.as_deref().ok_or_else(|| {
4696 SidecarError::InvalidState(String::from("python httpRequest requires a url"))
4697 })?;
4698 let url = Url::parse(url_text)
4699 .map_err(|error| SidecarError::Execution(format!("ERR_INVALID_URL: {error}")))?;
4700 let host = url.host_str().ok_or_else(|| {
4701 SidecarError::Execution(String::from("ERR_INVALID_URL: missing host"))
4702 })?;
4703 let port = url.port_or_known_default().ok_or_else(|| {
4704 SidecarError::Execution(String::from("ERR_INVALID_URL: missing port"))
4705 })?;
4706 self.bridge.require_network_access(
4707 vm_id,
4708 NetworkOperation::Http,
4709 format_tcp_resource(host, port),
4710 )?;
4711 let pinned_addresses = if let Ok(literal_ip) = host.parse::<IpAddr>() {
4718 filter_dns_safe_ip_addrs(vec![literal_ip], host)?
4719 } else {
4720 filter_dns_safe_ip_addrs(
4721 resolve_dns_ip_addrs(
4722 &self.bridge,
4723 &vm.kernel,
4724 vm_id,
4725 &vm.dns,
4726 host,
4727 DnsLookupPolicy::SkipPermissions,
4728 )?,
4729 host,
4730 )?
4731 };
4732 let mut headers = BTreeMap::new();
4733 for (name, value) in &request.headers {
4734 headers.insert(name.clone(), Value::String(value.clone()));
4735 }
4736 let options = JavascriptHttpRequestOptions {
4737 method: Some(
4738 request
4739 .http_method
4740 .clone()
4741 .unwrap_or_else(|| String::from("GET")),
4742 ),
4743 headers,
4744 body: request.body_base64.as_deref().map(|body| {
4745 String::from_utf8(
4746 base64::engine::general_purpose::STANDARD
4747 .decode(body)
4748 .unwrap_or_default(),
4749 )
4750 .unwrap_or_default()
4751 }),
4752 reject_unauthorized: None,
4753 };
4754 let headers =
4755 parse_http_header_collection(&options.headers, "python httpRequest headers")?;
4756 let response =
4757 issue_outbound_http_request(&url, &options, &headers, &pinned_addresses)?;
4758 let payload_json = response.as_str().ok_or_else(|| {
4759 SidecarError::Execution(String::from(
4760 "python httpRequest returned a non-string response payload",
4761 ))
4762 })?;
4763 let payload: Value = serde_json::from_str(payload_json).map_err(|error| {
4764 SidecarError::Execution(format!(
4765 "python httpRequest response must be valid JSON: {error}"
4766 ))
4767 })?;
4768 let header_map = payload
4769 .get("headers")
4770 .and_then(Value::as_array)
4771 .map(|entries| {
4772 let mut normalized = BTreeMap::<String, Vec<String>>::new();
4773 for entry in entries {
4774 let Some(pair) = entry.as_array() else {
4775 continue;
4776 };
4777 let Some(name) = pair.first().and_then(Value::as_str) else {
4778 continue;
4779 };
4780 let Some(value) = pair.get(1).and_then(Value::as_str) else {
4781 continue;
4782 };
4783 normalized
4784 .entry(name.to_owned())
4785 .or_default()
4786 .push(value.to_owned());
4787 }
4788 normalized
4789 })
4790 .unwrap_or_default();
4791 Ok(PythonVfsRpcResponsePayload::Http {
4792 status: payload
4793 .get("status")
4794 .and_then(Value::as_u64)
4795 .map(|value| value as u16)
4796 .unwrap_or_default(),
4797 reason: payload
4798 .get("statusText")
4799 .and_then(Value::as_str)
4800 .unwrap_or_default()
4801 .to_owned(),
4802 url: payload
4803 .get("url")
4804 .and_then(Value::as_str)
4805 .unwrap_or(url_text)
4806 .to_owned(),
4807 headers: header_map,
4808 body_base64: payload
4809 .get("body")
4810 .and_then(Value::as_str)
4811 .unwrap_or_default()
4812 .to_owned(),
4813 })
4814 })();
4815
4816 self.respond_python_rpc(vm_id, process_id, request.id, response)
4817 }
4818
4819 fn handle_python_dns_rpc_request(
4820 &mut self,
4821 vm_id: &str,
4822 process_id: &str,
4823 request: PythonVfsRpcRequest,
4824 ) -> Result<(), SidecarError> {
4825 let Some(vm) = self.vms.get(vm_id) else {
4826 return Ok(());
4827 };
4828 if !vm.active_processes.contains_key(process_id) {
4829 return Ok(());
4830 }
4831 let response = (|| {
4832 let hostname = request.hostname.as_deref().ok_or_else(|| {
4833 SidecarError::InvalidState(String::from("python dnsLookup requires a hostname"))
4834 })?;
4835 let mut addresses = filter_dns_safe_ip_addrs(
4836 resolve_dns_ip_addrs(
4837 &self.bridge,
4838 &vm.kernel,
4839 vm_id,
4840 &vm.dns,
4841 hostname,
4842 DnsLookupPolicy::CheckPermissions,
4843 )?,
4844 hostname,
4845 )?;
4846 if let Some(family) = request.family {
4847 addresses.retain(|address| {
4848 matches!((family, address), (4, IpAddr::V4(_)) | (6, IpAddr::V6(_)))
4849 });
4850 }
4851 Ok(PythonVfsRpcResponsePayload::DnsLookup {
4852 addresses: addresses
4853 .into_iter()
4854 .map(|address| address.to_string())
4855 .collect(),
4856 })
4857 })();
4858
4859 self.respond_python_rpc(vm_id, process_id, request.id, response)
4860 }
4861
4862 fn handle_python_subprocess_rpc_request(
4863 &mut self,
4864 vm_id: &str,
4865 process_id: &str,
4866 request: PythonVfsRpcRequest,
4867 ) -> Result<(), SidecarError> {
4868 let command = request.command.clone().ok_or_else(|| {
4869 SidecarError::InvalidState(String::from("python subprocessRun requires a command"))
4870 })?;
4871 let (internal_bootstrap_env, cwd) = {
4872 let Some(vm) = self.vms.get(vm_id) else {
4873 return Ok(());
4874 };
4875 let Some(process) = vm.active_processes.get(process_id) else {
4876 return Ok(());
4877 };
4878 let virtual_home = guest_virtual_home(vm);
4879 let cwd = request.cwd.clone().or_else(|| {
4880 guest_runtime_path_for_host_path(
4881 &vm.guest_env,
4882 &virtual_home,
4883 &vm.host_cwd,
4884 &process.host_cwd.to_string_lossy(),
4885 )
4886 });
4887 (
4888 sanitize_javascript_child_process_internal_bootstrap_env(&vm.guest_env),
4889 cwd,
4890 )
4891 };
4892 let response = self
4893 .spawn_javascript_child_process_sync(
4894 vm_id,
4895 process_id,
4896 JavascriptChildProcessSpawnRequest {
4897 command,
4898 args: request.args.clone(),
4899 options: JavascriptChildProcessSpawnOptions {
4900 cwd,
4901 env: request.env.clone(),
4902 input: None,
4903 internal_bootstrap_env,
4904 shell: request.shell,
4905 detached: false,
4906 stdio: vec![
4907 String::from("pipe"),
4908 String::from("pipe"),
4909 String::from("pipe"),
4910 ],
4911 timeout: None,
4912 kill_signal: None,
4913 },
4914 },
4915 request.max_buffer,
4916 )
4917 .map(|payload| PythonVfsRpcResponsePayload::SubprocessRun {
4918 exit_code: payload
4919 .get("code")
4920 .and_then(Value::as_i64)
4921 .map(|value| value as i32)
4922 .unwrap_or(1),
4923 stdout: payload
4924 .get("stdout")
4925 .and_then(Value::as_str)
4926 .unwrap_or_default()
4927 .to_owned(),
4928 stderr: payload
4929 .get("stderr")
4930 .and_then(Value::as_str)
4931 .unwrap_or_default()
4932 .to_owned(),
4933 max_buffer_exceeded: payload
4934 .get("maxBufferExceeded")
4935 .and_then(Value::as_bool)
4936 .unwrap_or(false),
4937 });
4938
4939 self.respond_python_rpc(vm_id, process_id, request.id, response)
4940 }
4941
4942 fn respond_python_rpc(
4943 &mut self,
4944 vm_id: &str,
4945 process_id: &str,
4946 request_id: u64,
4947 response: Result<PythonVfsRpcResponsePayload, SidecarError>,
4948 ) -> Result<(), SidecarError> {
4949 let Some(vm) = self.vms.get_mut(vm_id) else {
4950 return Ok(());
4951 };
4952 let Some(process) = vm.active_processes.get_mut(process_id) else {
4953 return Ok(());
4954 };
4955 let result = match response {
4956 Ok(payload) => process
4957 .execution
4958 .respond_python_vfs_rpc_success(request_id, payload),
4959 Err(error) => process.execution.respond_python_vfs_rpc_error(
4960 request_id,
4961 "ERR_AGENTOS_PYTHON_VFS_RPC",
4962 error.to_string(),
4963 ),
4964 };
4965 match result {
4966 Ok(()) => Ok(()),
4967 Err(error) if is_broken_pipe_error(&error) => Ok(()),
4968 Err(error) => Err(error),
4969 }
4970 }
4971
4972 pub(crate) fn resolve_javascript_child_process_execution(
4973 &self,
4974 vm: &VmState,
4975 parent_env: &BTreeMap<String, String>,
4976 parent_guest_cwd: &str,
4977 parent_host_cwd: &Path,
4978 request: &JavascriptChildProcessSpawnRequest,
4979 ) -> Result<ResolvedChildProcessExecution, SidecarError> {
4980 let mut runtime_env = parent_env.clone();
4981 runtime_env.extend(request.options.internal_bootstrap_env.clone());
4982 let (guest_cwd, host_cwd_override) = request
4983 .options
4984 .cwd
4985 .as_deref()
4986 .map(|cwd| {
4987 let normalized_parent_host_cwd = normalize_host_path(parent_host_cwd);
4988 let requested_host_cwd = normalize_host_path(Path::new(cwd));
4989 if path_is_within_root(&requested_host_cwd, &normalized_parent_host_cwd) {
4990 let relative = requested_host_cwd
4991 .strip_prefix(&normalized_parent_host_cwd)
4992 .unwrap_or_else(|_| Path::new(""));
4993 let relative = relative.to_string_lossy().replace('\\', "/");
4994 let guest_cwd = if relative.is_empty() {
4995 parent_guest_cwd.to_owned()
4996 } else {
4997 normalize_path(&format!("{parent_guest_cwd}/{relative}"))
4998 };
4999 (guest_cwd, Some(requested_host_cwd))
5000 } else if Path::new(cwd).is_relative() {
5001 (
5002 normalize_path(&format!("{parent_guest_cwd}/{cwd}")),
5003 Some(normalize_host_path(&parent_host_cwd.join(cwd))),
5004 )
5005 } else {
5006 (normalize_path(cwd), None)
5007 }
5008 })
5009 .unwrap_or_else(|| (parent_guest_cwd.to_owned(), None));
5010 let inherited_host_cwd = (host_cwd_override.is_none() && guest_cwd == parent_guest_cwd)
5011 .then(|| normalize_host_path(parent_host_cwd));
5012 let host_cwd = host_cwd_override
5013 .or(inherited_host_cwd)
5014 .or_else(|| {
5015 host_runtime_path_for_guest_path_with_env(
5016 vm,
5017 &runtime_env,
5018 &guest_cwd,
5019 parent_host_cwd,
5020 )
5021 })
5022 .unwrap_or_else(|| {
5023 let candidate = PathBuf::from(&guest_cwd);
5024 if guest_cwd == parent_guest_cwd {
5025 normalize_host_path(parent_host_cwd)
5026 } else if candidate.is_absolute() {
5027 shadow_path_for_guest(vm, &guest_cwd)
5028 } else {
5029 vm.host_cwd.clone()
5030 }
5031 });
5032 let mut env = parent_env.clone();
5033 env.extend(request.options.env.clone());
5034 env.remove("AGENTOS_GUEST_ENTRYPOINT");
5037 env.remove("AGENTOS_NODE_EVAL");
5038
5039 let (command, process_args) = if request.options.shell {
5040 let tokens = tokenize_shell_free_command(&request.command);
5041 let requires_shell = command_requires_shell(&request.command)
5042 || tokens.first().is_some_and(|command| {
5043 is_posix_shell_builtin(command) || shell_first_token_requires_shell(command)
5044 });
5045 if requires_shell {
5046 if !vm.command_guest_paths.contains_key("sh") {
5047 return Err(SidecarError::InvalidState(format!(
5048 "shell-mode child_process command requires /bin/sh, which is not \
5049 installed in this VM (install a software package that provides sh, \
5050 for example @secure-exec/coreutils): {}",
5051 request.command
5052 )));
5053 }
5054 (
5055 String::from("sh"),
5056 vec![String::from("-c"), request.command.clone()],
5057 )
5058 } else {
5059 let Some((command, args)) = tokens.split_first() else {
5060 return Err(SidecarError::InvalidState(String::from(
5061 "child_process shell command must not be empty",
5062 )));
5063 };
5064 (command.clone(), args.to_vec())
5065 }
5066 } else {
5067 (request.command.clone(), request.args.clone())
5068 };
5069 let process_args = apply_shell_cwd_prefix(&command, process_args, &guest_cwd);
5070 if is_tool_command(vm, &command) {
5071 let command = normalized_tool_command_name(&command).unwrap_or(command);
5072 return Ok(ResolvedChildProcessExecution {
5073 command: command.clone(),
5074 process_args: std::iter::once(command.clone())
5075 .chain(process_args.iter().cloned())
5076 .collect(),
5077 runtime: GuestRuntimeKind::JavaScript,
5078 entrypoint: command,
5079 execution_args: process_args,
5080 env,
5081 guest_cwd,
5082 host_cwd,
5083 wasm_permission_tier: None,
5084 tool_command: true,
5085 });
5086 }
5087
5088 if is_path_like_specifier(&command)
5089 && matches!(
5090 Path::new(&command).extension().and_then(|ext| ext.to_str()),
5091 Some("js" | "mjs" | "cjs" | "ts" | "mts" | "cts")
5092 )
5093 {
5094 let guest_entrypoint = if command.starts_with('/') {
5095 normalize_path(&command)
5096 } else if command.starts_with("file:") {
5097 normalize_path(command.trim_start_matches("file:"))
5098 } else {
5099 normalize_path(&format!("{guest_cwd}/{command}"))
5100 };
5101 let host_entrypoint = if command.starts_with("./") || command.starts_with("../") {
5102 normalize_host_path(&host_cwd.join(&command))
5103 } else {
5104 host_runtime_path_for_guest_path_with_env(
5105 vm,
5106 &runtime_env,
5107 &guest_entrypoint,
5108 parent_host_cwd,
5109 )
5110 .unwrap_or_else(|| {
5111 let candidate = PathBuf::from(&guest_entrypoint);
5112 if candidate.is_absolute() {
5113 candidate
5114 } else {
5115 host_cwd.join(&guest_entrypoint)
5116 }
5117 })
5118 };
5119 env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
5120 let guest_entrypoint = env.get("AGENTOS_GUEST_ENTRYPOINT").cloned();
5121 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
5122
5123 return Ok(ResolvedChildProcessExecution {
5124 command: command.clone(),
5125 process_args: std::iter::once(command)
5126 .chain(process_args.iter().cloned())
5127 .collect(),
5128 runtime: GuestRuntimeKind::JavaScript,
5129 entrypoint: host_entrypoint.to_string_lossy().into_owned(),
5130 execution_args: process_args,
5131 env,
5132 guest_cwd,
5133 host_cwd,
5134 wasm_permission_tier: None,
5135 tool_command: false,
5136 });
5137 }
5138
5139 if is_node_runtime_command(&command) {
5140 if let Some(cli) = resolve_host_node_cli_entrypoint(&command) {
5141 env.insert(
5142 String::from("AGENTOS_NODE_EVAL"),
5143 build_host_node_cli_eval(&cli),
5144 );
5145 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5146 add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
5147 add_runtime_host_access_path(
5148 &mut env,
5149 "AGENTOS_EXTRA_FS_READ_PATHS",
5150 &cli.package_root,
5151 true,
5152 );
5153
5154 return Ok(ResolvedChildProcessExecution {
5155 command: command.clone(),
5156 process_args: std::iter::once(command.clone())
5157 .chain(process_args.iter().cloned())
5158 .collect(),
5159 runtime: GuestRuntimeKind::JavaScript,
5160 entrypoint: String::from("-e"),
5161 execution_args: std::iter::once(cli.guest_entrypoint.clone())
5162 .chain(process_args.iter().cloned())
5163 .collect(),
5164 env,
5165 guest_cwd,
5166 host_cwd,
5167 wasm_permission_tier: None,
5168 tool_command: false,
5169 });
5170 }
5171
5172 if process_args.is_empty() {
5173 env.insert(String::from("AGENTOS_NODE_EVAL"), String::new());
5174 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5175
5176 return Ok(ResolvedChildProcessExecution {
5177 command: command.clone(),
5178 process_args: vec![command.clone()],
5179 runtime: GuestRuntimeKind::JavaScript,
5180 entrypoint: String::from("-e"),
5181 execution_args: Vec::new(),
5182 env,
5183 guest_cwd,
5184 host_cwd,
5185 wasm_permission_tier: None,
5186 tool_command: false,
5187 });
5188 }
5189
5190 if let Some((entrypoint, execution_args)) =
5191 resolve_special_node_cli_invocation(&process_args, &mut env)
5192 {
5193 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5194
5195 return Ok(ResolvedChildProcessExecution {
5196 command: command.clone(),
5197 process_args: std::iter::once(command.clone())
5198 .chain(process_args.iter().cloned())
5199 .collect(),
5200 runtime: GuestRuntimeKind::JavaScript,
5201 entrypoint,
5202 execution_args,
5203 env,
5204 guest_cwd,
5205 host_cwd,
5206 wasm_permission_tier: None,
5207 tool_command: false,
5208 });
5209 }
5210
5211 let Some(entrypoint_specifier) = process_args.first() else {
5212 return Err(SidecarError::InvalidState(format!(
5213 "{command} child_process spawn requires an entrypoint"
5214 )));
5215 };
5216
5217 let (entrypoint, execution_args) = if is_path_like_specifier(entrypoint_specifier) {
5218 let guest_entrypoint = if entrypoint_specifier.starts_with('/') {
5219 normalize_path(entrypoint_specifier)
5220 } else if entrypoint_specifier.starts_with("file:") {
5221 normalize_path(entrypoint_specifier.trim_start_matches("file:"))
5222 } else {
5223 normalize_path(&format!("{guest_cwd}/{entrypoint_specifier}"))
5224 };
5225 let host_entrypoint = if entrypoint_specifier.starts_with("./")
5226 || entrypoint_specifier.starts_with("../")
5227 {
5228 normalize_host_path(&host_cwd.join(entrypoint_specifier))
5229 } else {
5230 host_runtime_path_for_guest_path_with_env(
5231 vm,
5232 &runtime_env,
5233 &guest_entrypoint,
5234 parent_host_cwd,
5235 )
5236 .unwrap_or_else(|| {
5237 let candidate = PathBuf::from(&guest_entrypoint);
5238 if candidate.is_absolute() {
5239 candidate
5240 } else {
5241 host_cwd.join(&guest_entrypoint)
5242 }
5243 })
5244 };
5245 env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
5246 (
5247 host_entrypoint.to_string_lossy().into_owned(),
5248 process_args.iter().skip(1).cloned().collect(),
5249 )
5250 } else {
5251 (
5252 entrypoint_specifier.clone(),
5253 process_args.iter().skip(1).cloned().collect(),
5254 )
5255 };
5256 let guest_entrypoint = env.get("AGENTOS_GUEST_ENTRYPOINT").cloned();
5257 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
5258
5259 return Ok(ResolvedChildProcessExecution {
5260 command: command.clone(),
5261 process_args: std::iter::once(command)
5262 .chain(process_args.iter().cloned())
5263 .collect(),
5264 runtime: GuestRuntimeKind::JavaScript,
5265 entrypoint,
5266 execution_args,
5267 env,
5268 guest_cwd,
5269 host_cwd,
5270 wasm_permission_tier: None,
5271 tool_command: false,
5272 });
5273 }
5274
5275 if command == PYTHON_COMMAND {
5276 return Err(SidecarError::InvalidState(String::from(
5277 "nested python child_process execution is not supported yet",
5278 )));
5279 }
5280
5281 let guest_entrypoint = resolve_guest_command_entrypoint(
5282 vm,
5283 &guest_cwd,
5284 &command,
5285 env.get("PATH").map(String::as_str),
5286 )
5287 .ok_or_else(|| SidecarError::InvalidState(format!("command not found: {command}")))?;
5288 let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
5289 let wasm_permission_tier = vm.command_permissions.get(&command).copied().or_else(|| {
5290 Path::new(&guest_entrypoint)
5291 .file_name()
5292 .and_then(|name| name.to_str())
5293 .and_then(|name| vm.command_permissions.get(name).copied())
5294 });
5295 if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
5296 resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
5297 {
5298 prepare_guest_runtime_env(
5299 vm,
5300 &mut env,
5301 &guest_cwd,
5302 &host_cwd,
5303 Some(javascript_guest_entrypoint),
5304 )?;
5305
5306 return Ok(ResolvedChildProcessExecution {
5307 command: command.clone(),
5308 process_args: std::iter::once(command)
5309 .chain(process_args.iter().cloned())
5310 .collect(),
5311 runtime: GuestRuntimeKind::JavaScript,
5312 entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
5313 execution_args: process_args,
5314 env,
5315 guest_cwd,
5316 host_cwd,
5317 wasm_permission_tier: None,
5318 tool_command: false,
5319 });
5320 }
5321 prepare_guest_runtime_env(
5322 vm,
5323 &mut env,
5324 &guest_cwd,
5325 &host_cwd,
5326 Some(guest_entrypoint.clone()),
5327 )?;
5328
5329 Ok(ResolvedChildProcessExecution {
5330 command: command.clone(),
5331 process_args: std::iter::once(command)
5332 .chain(process_args.iter().cloned())
5333 .collect(),
5334 runtime: GuestRuntimeKind::WebAssembly,
5335 entrypoint: host_entrypoint.to_string_lossy().into_owned(),
5336 execution_args: process_args,
5337 env,
5338 guest_cwd,
5339 host_cwd,
5340 wasm_permission_tier,
5341 tool_command: false,
5342 })
5343 }
5344
5345 pub(crate) fn spawn_javascript_child_process(
5346 &mut self,
5347 vm_id: &str,
5348 process_id: &str,
5349 request: JavascriptChildProcessSpawnRequest,
5350 ) -> Result<Value, SidecarError> {
5351 let resolved = {
5352 let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
5353 let parent = vm
5354 .active_processes
5355 .get(process_id)
5356 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5357 self.resolve_javascript_child_process_execution(
5358 vm,
5359 &parent.env,
5360 &parent.guest_cwd,
5361 &parent.host_cwd,
5362 &request,
5363 )?
5364 };
5365 let (parent_kernel_pid, child_process_id) = {
5366 let vm = self
5367 .vms
5368 .get_mut(vm_id)
5369 .ok_or_else(|| missing_vm_error(vm_id))?;
5370 let process = vm
5371 .active_processes
5372 .get_mut(process_id)
5373 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5374 (process.kernel_pid, process.allocate_child_process_id())
5375 };
5376 let sidecar_requests = self.sidecar_requests.clone();
5377 let vm = self
5378 .vms
5379 .get_mut(vm_id)
5380 .ok_or_else(|| missing_vm_error(vm_id))?;
5381 let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
5382 .tool_command
5383 {
5384 let tool_resolution = resolve_tool_command(
5385 vm,
5386 &resolved.command,
5387 &resolved.execution_args,
5388 Some(&resolved.guest_cwd),
5389 )?
5390 .ok_or_else(|| {
5391 SidecarError::InvalidState(format!(
5392 "tool command no longer resolves: {}",
5393 resolved.command
5394 ))
5395 })?;
5396 let kernel_handle = vm
5397 .kernel
5398 .create_virtual_process(
5399 EXECUTION_DRIVER_NAME,
5400 TOOL_DRIVER_NAME,
5401 &resolved.command,
5402 resolved.process_args.clone(),
5403 VirtualProcessOptions {
5404 parent_pid: Some(parent_kernel_pid),
5405 env: resolved.env.clone(),
5406 cwd: Some(resolved.guest_cwd.clone()),
5407 },
5408 )
5409 .map_err(kernel_error)?;
5410 let kernel_pid = kernel_handle.pid();
5411 let tool_execution = ToolExecution::default();
5412 let cancelled = tool_execution.cancelled.clone();
5413 let pending_events = tool_execution.pending_events.clone();
5414 let events_overflowed = tool_execution.events_overflowed.clone();
5415 spawn_tool_process_events(ToolProcessEventRequest {
5416 sidecar_requests: sidecar_requests.clone(),
5417 connection_id: vm.connection_id.clone(),
5418 session_id: vm.session_id.clone(),
5419 vm_id: vm_id.to_owned(),
5420 tool_resolution,
5421 cancelled,
5422 pending_events,
5423 events_overflowed,
5424 });
5425 (
5426 kernel_pid,
5427 kernel_handle,
5428 ActiveExecution::Tool(tool_execution),
5429 None,
5430 )
5431 } else {
5432 let kernel_command = match resolved.runtime {
5433 GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
5434 GuestRuntimeKind::WebAssembly => WASM_COMMAND,
5435 GuestRuntimeKind::Python => {
5436 unreachable!("python child_process execution is rejected")
5437 }
5438 };
5439 let kernel_handle = vm
5440 .kernel
5441 .spawn_process(
5442 kernel_command,
5443 resolved.process_args.clone(),
5444 SpawnOptions {
5445 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
5446 parent_pid: Some(parent_kernel_pid),
5447 env: resolved.env.clone(),
5448 cwd: Some(resolved.guest_cwd.clone()),
5449 },
5450 )
5451 .map_err(kernel_error)?;
5452 let kernel_pid = kernel_handle.pid();
5453 if request.options.detached {
5454 vm.kernel
5455 .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
5456 .map_err(kernel_error)?;
5457 }
5458 let mut execution_env = resolved.env.clone();
5459 execution_env.insert(
5460 String::from(EXECUTION_SANDBOX_ROOT_ENV),
5461 normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
5462 );
5463
5464 let execution = match resolved.runtime {
5465 GuestRuntimeKind::JavaScript => {
5466 execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
5467 &request.options.internal_bootstrap_env,
5468 ));
5469 execution_env.insert(
5470 String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
5471 String::from("1"),
5472 );
5473 let context =
5474 self.javascript_engine
5475 .create_context(CreateJavascriptContextRequest {
5476 vm_id: vm_id.to_owned(),
5477 bootstrap_module: None,
5478 compile_cache_root: Some(
5479 self.cache_root.join("node-compile-cache"),
5480 ),
5481 });
5482 let inline_code = load_javascript_entrypoint_source(
5483 vm,
5484 &resolved.host_cwd,
5485 &resolved.entrypoint,
5486 &execution_env,
5487 );
5488 prepare_javascript_shadow(vm, &resolved)?;
5489
5490 let built_reader = build_module_reader(vm, &resolved);
5491 let guest_reader = built_reader.clone().map(|reader| {
5492 Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
5493 as Box<dyn GuestModuleReader>
5494 });
5495 let module_reader = built_reader
5496 .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
5497 let execution = self
5498 .javascript_engine
5499 .start_execution_with_module_reader(
5500 StartJavascriptExecutionRequest {
5501 guest_runtime: guest_runtime_identity(
5502 vm,
5503 Some(u64::from(kernel_pid)),
5504 Some(u64::from(parent_kernel_pid)),
5505 ),
5506 vm_id: vm_id.to_owned(),
5507 context_id: context.context_id,
5508 argv: std::iter::once(resolved.entrypoint.clone())
5509 .chain(resolved.execution_args.clone())
5510 .collect(),
5511 env: execution_env,
5512 cwd: resolved.host_cwd.clone(),
5513 limits: javascript_execution_limits(vm),
5514 inline_code,
5515 },
5516 module_reader,
5517 guest_reader,
5518 )
5519 .map_err(javascript_error)?;
5520 ActiveExecution::Javascript(execution)
5521 }
5522 GuestRuntimeKind::WebAssembly => {
5523 execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
5524 let wasm_limits = wasm_execution_limits(vm);
5525 let wasm_guest_runtime = guest_runtime_identity(
5526 vm,
5527 Some(u64::from(kernel_pid)),
5528 Some(u64::from(parent_kernel_pid)),
5529 );
5530 let context = self.wasm_engine.create_context(CreateWasmContextRequest {
5531 vm_id: vm_id.to_owned(),
5532 module_path: Some(resolved.entrypoint.clone()),
5533 });
5534 let execution = self
5535 .wasm_engine
5536 .start_execution(StartWasmExecutionRequest {
5537 vm_id: vm_id.to_owned(),
5538 context_id: context.context_id,
5539 argv: resolved.process_args.clone(),
5540 env: execution_env,
5541 cwd: resolved.host_cwd.clone(),
5542 permission_tier: execution_wasm_permission_tier(
5543 resolved
5544 .wasm_permission_tier
5545 .unwrap_or(WasmPermissionTier::Full),
5546 ),
5547 limits: wasm_limits,
5548 guest_runtime: wasm_guest_runtime,
5549 })
5550 .map_err(wasm_error)?;
5551 ActiveExecution::Wasm(Box::new(execution))
5552 }
5553 GuestRuntimeKind::Python => {
5554 unreachable!("python child_process execution is rejected")
5555 }
5556 };
5557 let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
5558 "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5559 "ignore" => {
5560 vm.kernel
5561 .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
5562 .map_err(kernel_error)?;
5563 None
5564 }
5565 "inherit" => None,
5566 _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5567 };
5568 (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
5569 };
5570
5571 let process = vm
5572 .active_processes
5573 .get_mut(process_id)
5574 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5575 process.child_processes.insert(
5576 child_process_id.clone(),
5577 ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
5578 .with_detached(request.options.detached)
5579 .with_guest_cwd(resolved.guest_cwd.clone())
5580 .with_env(resolved.env.clone())
5581 .with_host_cwd(resolved.host_cwd.clone()),
5582 );
5583 if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
5584 process
5585 .child_processes
5586 .get_mut(&child_process_id)
5587 .ok_or_else(|| {
5588 SidecarError::InvalidState(format!(
5589 "child process {child_process_id} disappeared during spawn"
5590 ))
5591 })?
5592 .kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
5593 }
5594 Ok(json!({
5595 "childId": child_process_id,
5596 "pid": kernel_pid,
5597 "command": resolved.command,
5598 "args": resolved.process_args,
5599 }))
5600 }
5601
5602 pub(crate) fn spawn_javascript_child_process_sync(
5603 &mut self,
5604 vm_id: &str,
5605 process_id: &str,
5606 request: JavascriptChildProcessSpawnRequest,
5607 max_buffer: Option<usize>,
5608 ) -> Result<Value, SidecarError> {
5609 let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
5610 let timeout_deadline = request
5611 .options
5612 .timeout
5613 .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
5614 let timeout_signal = request
5615 .options
5616 .kill_signal
5617 .clone()
5618 .unwrap_or_else(|| String::from("SIGTERM"));
5619 let spawned = self.spawn_javascript_child_process(vm_id, process_id, request)?;
5620 let child_process_id = spawned
5621 .get("childId")
5622 .and_then(Value::as_str)
5623 .ok_or_else(|| {
5624 SidecarError::InvalidState(String::from(
5625 "child_process.spawn_sync response is missing childId",
5626 ))
5627 })?
5628 .to_owned();
5629
5630 if let Some(input) = sync_input.as_deref() {
5631 self.write_javascript_child_process_stdin(vm_id, process_id, &child_process_id, input)?;
5632 }
5633 self.close_javascript_child_process_stdin(vm_id, process_id, &child_process_id)?;
5634
5635 let max_buffer = max_buffer.unwrap_or(1024 * 1024);
5636 let mut stdout = Vec::new();
5637 let mut stderr = Vec::new();
5638 let mut max_buffer_exceeded = false;
5639 let mut kill_sent = false;
5640 let mut timed_out = false;
5641
5642 let exit_code = loop {
5643 let wait_ms = if let Some(deadline) = timeout_deadline {
5644 let now = Instant::now();
5645 if now >= deadline {
5646 if !kill_sent {
5647 timed_out = true;
5648 self.kill_javascript_child_process(
5649 vm_id,
5650 process_id,
5651 &child_process_id,
5652 &timeout_signal,
5653 )?;
5654 kill_sent = true;
5655 }
5656 0
5657 } else {
5658 u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
5659 .unwrap_or(50)
5660 }
5661 } else {
5662 50
5663 };
5664 let event =
5665 self.poll_javascript_child_process(vm_id, process_id, &child_process_id, wait_ms)?;
5666 if event.is_null() {
5667 continue;
5668 }
5669
5670 match event.get("type").and_then(Value::as_str) {
5671 Some("stdout") => {
5672 let chunk = javascript_sync_rpc_bytes_arg(
5673 &[event.get("data").cloned().unwrap_or(Value::Null)],
5674 0,
5675 "child_process.spawn_sync stdout",
5676 )?;
5677 stdout.extend_from_slice(&chunk);
5678 if stdout.len() > max_buffer && !kill_sent {
5679 max_buffer_exceeded = true;
5680 self.kill_javascript_child_process(
5681 vm_id,
5682 process_id,
5683 &child_process_id,
5684 "SIGTERM",
5685 )?;
5686 kill_sent = true;
5687 }
5688 }
5689 Some("stderr") => {
5690 let chunk = javascript_sync_rpc_bytes_arg(
5691 &[event.get("data").cloned().unwrap_or(Value::Null)],
5692 0,
5693 "child_process.spawn_sync stderr",
5694 )?;
5695 stderr.extend_from_slice(&chunk);
5696 if stderr.len() > max_buffer && !kill_sent {
5697 max_buffer_exceeded = true;
5698 self.kill_javascript_child_process(
5699 vm_id,
5700 process_id,
5701 &child_process_id,
5702 "SIGTERM",
5703 )?;
5704 kill_sent = true;
5705 }
5706 }
5707 Some("exit") => {
5708 break event
5709 .get("exitCode")
5710 .and_then(Value::as_i64)
5711 .map(|value| value as i32)
5712 .unwrap_or(1);
5713 }
5714 _ => {}
5715 }
5716 };
5717
5718 Ok(json!({
5719 "stdout": String::from_utf8_lossy(&stdout),
5720 "stderr": String::from_utf8_lossy(&stderr),
5721 "code": exit_code,
5722 "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
5723 "timedOut": timed_out,
5724 "maxBufferExceeded": max_buffer_exceeded,
5725 }))
5726 }
5727
5728 fn spawn_descendant_javascript_child_process(
5729 &mut self,
5730 vm_id: &str,
5731 process_id: &str,
5732 current_process_path: &[&str],
5733 request: JavascriptChildProcessSpawnRequest,
5734 ) -> Result<Value, SidecarError> {
5735 let current_process_label =
5736 Self::child_process_path_label(process_id, current_process_path);
5737 let (resolved, parent_kernel_pid) = {
5738 let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
5739 let root = vm
5740 .active_processes
5741 .get(process_id)
5742 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5743 let parent =
5744 Self::active_process_by_path(root, current_process_path).ok_or_else(|| {
5745 SidecarError::InvalidState(format!(
5746 "unknown child process path {current_process_label} during nested spawn"
5747 ))
5748 })?;
5749 (
5750 self.resolve_javascript_child_process_execution(
5751 vm,
5752 &parent.env,
5753 &parent.guest_cwd,
5754 &parent.host_cwd,
5755 &request,
5756 )?,
5757 parent.kernel_pid,
5758 )
5759 };
5760
5761 let sidecar_requests = self.sidecar_requests.clone();
5762 let vm = self
5763 .vms
5764 .get_mut(vm_id)
5765 .ok_or_else(|| missing_vm_error(vm_id))?;
5766 let child_process_id = {
5767 let root = vm
5768 .active_processes
5769 .get_mut(process_id)
5770 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5771 let parent =
5772 Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
5773 SidecarError::InvalidState(format!(
5774 "unknown child process path {current_process_label} during nested spawn"
5775 ))
5776 })?;
5777 parent.allocate_child_process_id()
5778 };
5779 let mut child_path = current_process_path.to_vec();
5780 child_path.push(child_process_id.as_str());
5781 let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
5782 .tool_command
5783 {
5784 let tool_resolution = resolve_tool_command(
5785 vm,
5786 &resolved.command,
5787 &resolved.execution_args,
5788 Some(&resolved.guest_cwd),
5789 )?
5790 .ok_or_else(|| {
5791 SidecarError::InvalidState(format!(
5792 "tool command no longer resolves: {}",
5793 resolved.command
5794 ))
5795 })?;
5796 let kernel_handle = vm
5797 .kernel
5798 .create_virtual_process(
5799 EXECUTION_DRIVER_NAME,
5800 TOOL_DRIVER_NAME,
5801 &resolved.command,
5802 resolved.process_args.clone(),
5803 VirtualProcessOptions {
5804 parent_pid: Some(parent_kernel_pid),
5805 env: resolved.env.clone(),
5806 cwd: Some(resolved.guest_cwd.clone()),
5807 },
5808 )
5809 .map_err(kernel_error)?;
5810 let kernel_pid = kernel_handle.pid();
5811 let tool_execution = ToolExecution::default();
5812 let cancelled = tool_execution.cancelled.clone();
5813 let pending_events = tool_execution.pending_events.clone();
5814 let events_overflowed = tool_execution.events_overflowed.clone();
5815 spawn_tool_process_events(ToolProcessEventRequest {
5816 sidecar_requests: sidecar_requests.clone(),
5817 connection_id: vm.connection_id.clone(),
5818 session_id: vm.session_id.clone(),
5819 vm_id: vm_id.to_owned(),
5820 tool_resolution,
5821 cancelled,
5822 pending_events,
5823 events_overflowed,
5824 });
5825 (
5826 kernel_pid,
5827 kernel_handle,
5828 ActiveExecution::Tool(tool_execution),
5829 None,
5830 )
5831 } else {
5832 let kernel_command = match resolved.runtime {
5833 GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
5834 GuestRuntimeKind::WebAssembly => WASM_COMMAND,
5835 GuestRuntimeKind::Python => {
5836 unreachable!("python child_process execution is rejected")
5837 }
5838 };
5839 let kernel_handle = vm
5840 .kernel
5841 .spawn_process(
5842 kernel_command,
5843 resolved.process_args.clone(),
5844 SpawnOptions {
5845 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
5846 parent_pid: Some(parent_kernel_pid),
5847 env: resolved.env.clone(),
5848 cwd: Some(resolved.guest_cwd.clone()),
5849 },
5850 )
5851 .map_err(kernel_error)?;
5852 let kernel_pid = kernel_handle.pid();
5853 if request.options.detached {
5854 vm.kernel
5855 .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
5856 .map_err(kernel_error)?;
5857 }
5858 let mut execution_env = resolved.env.clone();
5859 execution_env.insert(
5860 String::from(EXECUTION_SANDBOX_ROOT_ENV),
5861 normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
5862 );
5863 let execution = match resolved.runtime {
5864 GuestRuntimeKind::JavaScript => {
5865 execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
5866 &request.options.internal_bootstrap_env,
5867 ));
5868 execution_env.insert(
5869 String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
5870 String::from("1"),
5871 );
5872 let context =
5873 self.javascript_engine
5874 .create_context(CreateJavascriptContextRequest {
5875 vm_id: vm_id.to_owned(),
5876 bootstrap_module: None,
5877 compile_cache_root: Some(
5878 self.cache_root.join("node-compile-cache"),
5879 ),
5880 });
5881 let inline_code = load_javascript_entrypoint_source(
5882 vm,
5883 &resolved.host_cwd,
5884 &resolved.entrypoint,
5885 &execution_env,
5886 );
5887 prepare_javascript_shadow(vm, &resolved)?;
5888
5889 let built_reader = build_module_reader(vm, &resolved);
5890 let guest_reader = built_reader.clone().map(|reader| {
5891 Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
5892 as Box<dyn GuestModuleReader>
5893 });
5894 let module_reader = built_reader
5895 .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
5896 let execution = self
5897 .javascript_engine
5898 .start_execution_with_module_reader(
5899 StartJavascriptExecutionRequest {
5900 guest_runtime: guest_runtime_identity(
5901 vm,
5902 Some(u64::from(kernel_pid)),
5903 Some(u64::from(parent_kernel_pid)),
5904 ),
5905 vm_id: vm_id.to_owned(),
5906 context_id: context.context_id,
5907 argv: std::iter::once(resolved.entrypoint.clone())
5908 .chain(resolved.execution_args.clone())
5909 .collect(),
5910 env: execution_env,
5911 cwd: resolved.host_cwd.clone(),
5912 limits: javascript_execution_limits(vm),
5913 inline_code,
5914 },
5915 module_reader,
5916 guest_reader,
5917 )
5918 .map_err(javascript_error)?;
5919 ActiveExecution::Javascript(execution)
5920 }
5921 GuestRuntimeKind::WebAssembly => {
5922 execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
5923 let wasm_limits = wasm_execution_limits(vm);
5924 let wasm_guest_runtime = guest_runtime_identity(
5925 vm,
5926 Some(u64::from(kernel_pid)),
5927 Some(u64::from(parent_kernel_pid)),
5928 );
5929 let context = self.wasm_engine.create_context(CreateWasmContextRequest {
5930 vm_id: vm_id.to_owned(),
5931 module_path: Some(resolved.entrypoint.clone()),
5932 });
5933 let execution = self
5934 .wasm_engine
5935 .start_execution(StartWasmExecutionRequest {
5936 vm_id: vm_id.to_owned(),
5937 context_id: context.context_id,
5938 argv: resolved.process_args.clone(),
5939 env: execution_env,
5940 cwd: resolved.host_cwd.clone(),
5941 permission_tier: execution_wasm_permission_tier(
5942 resolved
5943 .wasm_permission_tier
5944 .unwrap_or(WasmPermissionTier::Full),
5945 ),
5946 limits: wasm_limits,
5947 guest_runtime: wasm_guest_runtime,
5948 })
5949 .map_err(wasm_error)?;
5950 ActiveExecution::Wasm(Box::new(execution))
5951 }
5952 GuestRuntimeKind::Python => {
5953 unreachable!("python child_process execution is rejected")
5954 }
5955 };
5956 let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
5957 "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5958 "ignore" => {
5959 vm.kernel
5960 .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
5961 .map_err(kernel_error)?;
5962 None
5963 }
5964 "inherit" => None,
5965 _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5966 };
5967 (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
5968 };
5969
5970 let root = vm
5971 .active_processes
5972 .get_mut(process_id)
5973 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5974 let parent =
5975 Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
5976 SidecarError::InvalidState(format!(
5977 "unknown child process path {current_process_label} during nested spawn"
5978 ))
5979 })?;
5980 parent.child_processes.insert(
5981 child_process_id.clone(),
5982 ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
5983 .with_detached(request.options.detached)
5984 .with_guest_cwd(resolved.guest_cwd.clone())
5985 .with_env(resolved.env.clone())
5986 .with_host_cwd(resolved.host_cwd.clone()),
5987 );
5988 if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
5989 parent
5990 .child_processes
5991 .get_mut(&child_process_id)
5992 .ok_or_else(|| {
5993 SidecarError::InvalidState(format!(
5994 "child process {child_process_id} disappeared during nested spawn"
5995 ))
5996 })?
5997 .kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
5998 }
5999 Ok(json!({
6000 "childId": child_process_id,
6001 "pid": kernel_pid,
6002 "command": resolved.command,
6003 "args": resolved.process_args,
6004 }))
6005 }
6006
6007 fn spawn_descendant_javascript_child_process_sync(
6008 &mut self,
6009 vm_id: &str,
6010 process_id: &str,
6011 current_process_path: &[&str],
6012 request: JavascriptChildProcessSpawnRequest,
6013 max_buffer: Option<usize>,
6014 ) -> Result<Value, SidecarError> {
6015 let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
6016 let timeout_deadline = request
6017 .options
6018 .timeout
6019 .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
6020 let timeout_signal = request
6021 .options
6022 .kill_signal
6023 .clone()
6024 .unwrap_or_else(|| String::from("SIGTERM"));
6025 let spawned = self.spawn_descendant_javascript_child_process(
6026 vm_id,
6027 process_id,
6028 current_process_path,
6029 request,
6030 )?;
6031 let child_process_id = spawned
6032 .get("childId")
6033 .and_then(Value::as_str)
6034 .ok_or_else(|| {
6035 SidecarError::InvalidState(String::from(
6036 "child_process.spawn_sync response is missing childId",
6037 ))
6038 })?
6039 .to_owned();
6040
6041 if let Some(input) = sync_input.as_deref() {
6042 self.write_descendant_javascript_child_process_stdin(
6043 vm_id,
6044 process_id,
6045 current_process_path,
6046 &child_process_id,
6047 input,
6048 )?;
6049 }
6050 self.close_descendant_javascript_child_process_stdin(
6051 vm_id,
6052 process_id,
6053 current_process_path,
6054 &child_process_id,
6055 )?;
6056
6057 let max_buffer = max_buffer.unwrap_or(1024 * 1024);
6058 let mut stdout = Vec::new();
6059 let mut stderr = Vec::new();
6060 let mut max_buffer_exceeded = false;
6061 let mut kill_sent = false;
6062 let mut timed_out = false;
6063
6064 let exit_code = loop {
6065 let wait_ms = if let Some(deadline) = timeout_deadline {
6066 let now = Instant::now();
6067 if now >= deadline {
6068 if !kill_sent {
6069 timed_out = true;
6070 self.kill_descendant_javascript_child_process(
6071 vm_id,
6072 process_id,
6073 current_process_path,
6074 &child_process_id,
6075 &timeout_signal,
6076 )?;
6077 kill_sent = true;
6078 }
6079 0
6080 } else {
6081 u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
6082 .unwrap_or(50)
6083 }
6084 } else {
6085 50
6086 };
6087 let event = self.poll_descendant_javascript_child_process(
6088 vm_id,
6089 process_id,
6090 current_process_path,
6091 &child_process_id,
6092 wait_ms,
6093 )?;
6094 if event.is_null() {
6095 continue;
6096 }
6097
6098 match event.get("type").and_then(Value::as_str) {
6099 Some("stdout") => {
6100 let chunk = javascript_sync_rpc_bytes_arg(
6101 &[event.get("data").cloned().unwrap_or(Value::Null)],
6102 0,
6103 "child_process.spawn_sync stdout",
6104 )?;
6105 stdout.extend_from_slice(&chunk);
6106 if stdout.len() > max_buffer && !kill_sent {
6107 max_buffer_exceeded = true;
6108 self.kill_descendant_javascript_child_process(
6109 vm_id,
6110 process_id,
6111 current_process_path,
6112 &child_process_id,
6113 "SIGTERM",
6114 )?;
6115 kill_sent = true;
6116 }
6117 }
6118 Some("stderr") => {
6119 let chunk = javascript_sync_rpc_bytes_arg(
6120 &[event.get("data").cloned().unwrap_or(Value::Null)],
6121 0,
6122 "child_process.spawn_sync stderr",
6123 )?;
6124 stderr.extend_from_slice(&chunk);
6125 if stderr.len() > max_buffer && !kill_sent {
6126 max_buffer_exceeded = true;
6127 self.kill_descendant_javascript_child_process(
6128 vm_id,
6129 process_id,
6130 current_process_path,
6131 &child_process_id,
6132 "SIGTERM",
6133 )?;
6134 kill_sent = true;
6135 }
6136 }
6137 Some("exit") => {
6138 break event
6139 .get("exitCode")
6140 .and_then(Value::as_i64)
6141 .map(|value| value as i32)
6142 .unwrap_or(1);
6143 }
6144 _ => {}
6145 }
6146 };
6147
6148 Ok(json!({
6149 "stdout": String::from_utf8_lossy(&stdout),
6150 "stderr": String::from_utf8_lossy(&stderr),
6151 "code": exit_code,
6152 "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
6153 "timedOut": timed_out,
6154 "maxBufferExceeded": max_buffer_exceeded,
6155 }))
6156 }
6157
6158 fn handle_descendant_javascript_child_process_rpc(
6159 &mut self,
6160 vm_id: &str,
6161 process_id: &str,
6162 current_process_path: &[&str],
6163 request: &JavascriptSyncRpcRequest,
6164 ) -> Result<Value, SidecarError> {
6165 match request.method.as_str() {
6166 "child_process.spawn" => {
6167 let Some(vm) = self.vms.get(vm_id) else {
6168 return Ok(Value::Null);
6169 };
6170 let (payload, _) = parse_javascript_child_process_spawn_request(vm, &request.args)?;
6171 self.spawn_descendant_javascript_child_process(
6172 vm_id,
6173 process_id,
6174 current_process_path,
6175 payload,
6176 )
6177 }
6178 "child_process.spawn_sync" => {
6179 let Some(vm) = self.vms.get(vm_id) else {
6180 return Ok(Value::Null);
6181 };
6182 let (payload, max_buffer) =
6183 parse_javascript_child_process_spawn_request(vm, &request.args)?;
6184 self.spawn_descendant_javascript_child_process_sync(
6185 vm_id,
6186 process_id,
6187 current_process_path,
6188 payload,
6189 max_buffer,
6190 )
6191 }
6192 "child_process.poll" => {
6193 let child_process_id =
6194 javascript_sync_rpc_arg_str(&request.args, 0, "child_process.poll child id")?;
6195 let wait_ms = javascript_sync_rpc_arg_u64_optional(
6196 &request.args,
6197 1,
6198 "child_process.poll wait ms",
6199 )?
6200 .unwrap_or_default();
6201 self.poll_descendant_javascript_child_process(
6202 vm_id,
6203 process_id,
6204 current_process_path,
6205 child_process_id,
6206 wait_ms,
6207 )
6208 }
6209 "child_process.write_stdin" => {
6210 let child_process_id = javascript_sync_rpc_arg_str(
6211 &request.args,
6212 0,
6213 "child_process.write_stdin child id",
6214 )?;
6215 let chunk = javascript_sync_rpc_bytes_arg(
6216 &request.args,
6217 1,
6218 "child_process.write_stdin chunk",
6219 )?;
6220 self.write_descendant_javascript_child_process_stdin(
6221 vm_id,
6222 process_id,
6223 current_process_path,
6224 child_process_id,
6225 &chunk,
6226 )?;
6227 Ok(Value::Null)
6228 }
6229 "child_process.close_stdin" => {
6230 let child_process_id = javascript_sync_rpc_arg_str(
6231 &request.args,
6232 0,
6233 "child_process.close_stdin child id",
6234 )?;
6235 self.close_descendant_javascript_child_process_stdin(
6236 vm_id,
6237 process_id,
6238 current_process_path,
6239 child_process_id,
6240 )?;
6241 Ok(Value::Null)
6242 }
6243 "child_process.kill" => {
6244 let child_process_id =
6245 javascript_sync_rpc_arg_str(&request.args, 0, "child_process.kill child id")?;
6246 let signal =
6247 javascript_sync_rpc_arg_str(&request.args, 1, "child_process.kill signal")?;
6248 self.kill_descendant_javascript_child_process(
6249 vm_id,
6250 process_id,
6251 current_process_path,
6252 child_process_id,
6253 signal,
6254 )?;
6255 Ok(Value::Null)
6256 }
6257 _ => Err(SidecarError::InvalidState(format!(
6258 "unsupported nested child process RPC method {}",
6259 request.method
6260 ))),
6261 }
6262 }
6263
6264 fn poll_descendant_javascript_child_process(
6265 &mut self,
6266 vm_id: &str,
6267 process_id: &str,
6268 current_process_path: &[&str],
6269 child_process_id: &str,
6270 wait_ms: u64,
6271 ) -> Result<Value, SidecarError> {
6272 let mut child_path = current_process_path.to_vec();
6273 child_path.push(child_process_id);
6274 let child_gone_error = || javascript_child_process_gone_error(process_id, &child_path);
6275 let deadline = Instant::now() + Duration::from_millis(wait_ms);
6276 let mut polled_once = false;
6277
6278 loop {
6279 self.drain_queued_descendant_javascript_child_process_events(
6280 vm_id,
6281 process_id,
6282 &child_path,
6283 )?;
6284 enum ChildPollResult {
6285 Event(Box<Option<ActiveExecutionEvent>>),
6286 RecoverRuntimeExit,
6287 Timeout,
6288 }
6289 let wait = if wait_ms == 0 {
6290 Duration::ZERO
6291 } else {
6292 deadline.saturating_duration_since(Instant::now())
6293 };
6294 let poll_result = {
6295 let Some(vm) = self.vms.get_mut(vm_id) else {
6296 return Ok(Value::Null);
6297 };
6298 let Some(parent) =
6299 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6300 else {
6301 return Err(child_gone_error());
6302 };
6303 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6304 return Err(child_gone_error());
6305 };
6306 if let Some(event) = child.pending_execution_events.pop_front() {
6307 ChildPollResult::Event(Box::new(Some(event)))
6308 } else if polled_once && wait.is_zero() {
6309 ChildPollResult::Timeout
6310 } else {
6311 polled_once = true;
6312 match child.execution.poll_event_blocking(wait) {
6313 Ok(Some(event)) => ChildPollResult::Event(Box::new(Some(event))),
6314 Ok(None) => ChildPollResult::RecoverRuntimeExit,
6315 Err(SidecarError::Execution(message))
6316 if (child.runtime == GuestRuntimeKind::JavaScript
6317 && closed_javascript_event_channel(&message))
6318 || (child.runtime == GuestRuntimeKind::Python
6319 && closed_python_event_channel(&message))
6320 || (child.runtime == GuestRuntimeKind::WebAssembly
6321 && closed_wasm_event_channel(&message)) =>
6322 {
6323 ChildPollResult::RecoverRuntimeExit
6324 }
6325 Err(error) => return Err(error),
6326 }
6327 }
6328 };
6329 let event = match poll_result {
6330 ChildPollResult::Event(event) => *event,
6331 ChildPollResult::Timeout => return Ok(Value::Null),
6332 ChildPollResult::RecoverRuntimeExit => self
6333 .recover_descendant_runtime_child_process_event(
6334 vm_id,
6335 process_id,
6336 current_process_path,
6337 child_process_id,
6338 wait.as_millis().try_into().unwrap_or(u64::MAX),
6339 )?,
6340 };
6341
6342 let Some(event) = event else {
6343 return Ok(Value::Null);
6344 };
6345
6346 match event {
6347 ActiveExecutionEvent::Stdout(chunk) => {
6348 return Ok(json!({
6349 "type": "stdout",
6350 "data": javascript_sync_rpc_bytes_value(&chunk),
6351 }));
6352 }
6353 ActiveExecutionEvent::Stderr(chunk) => {
6354 return Ok(json!({
6355 "type": "stderr",
6356 "data": javascript_sync_rpc_bytes_value(&chunk),
6357 }));
6358 }
6359 ActiveExecutionEvent::Exited(exit_code) => {
6360 let had_trailing_events = {
6361 let Some(vm) = self.vms.get_mut(vm_id) else {
6362 return Ok(Value::Null);
6363 };
6364 let Some(parent) = Self::descendant_parent_process_mut(
6365 vm,
6366 process_id,
6367 current_process_path,
6368 ) else {
6369 return Ok(Value::Null);
6370 };
6371 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6372 return Ok(Value::Null);
6373 };
6374 let deadline = Instant::now() + Duration::from_millis(150);
6375 loop {
6376 let wait = deadline.saturating_duration_since(Instant::now());
6377 let next = poll_child_execution_after_exit(child, wait)?;
6378 let Some(next) = next else {
6379 break;
6380 };
6381 if matches!(next, ActiveExecutionEvent::Exited(_)) {
6382 continue;
6383 }
6384 child.queue_pending_execution_event(next)?;
6385 if Instant::now() >= deadline {
6386 break;
6387 }
6388 }
6389 if !child.pending_execution_events.is_empty() {
6390 child.queue_pending_execution_event(ActiveExecutionEvent::Exited(
6391 exit_code,
6392 ))?;
6393 true
6394 } else {
6395 false
6396 }
6397 };
6398 if had_trailing_events {
6399 continue;
6400 }
6401
6402 let parent_signal_key =
6403 Self::child_process_signal_key(process_id, current_process_path);
6404 let Some(vm) = self.vms.get_mut(vm_id) else {
6405 return Ok(Value::Null);
6406 };
6407 let signal_name = {
6408 let Some(parent) = Self::descendant_parent_process_mut(
6409 vm,
6410 process_id,
6411 current_process_path,
6412 ) else {
6413 return Ok(Value::Null);
6414 };
6415 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6416 return Ok(Value::Null);
6417 };
6418 child.pending_self_signal_exit.take().and_then(|signal| {
6419 if exit_code == 128 + signal {
6420 canonical_signal_name(signal).map(str::to_owned)
6421 } else {
6422 None
6423 }
6424 })
6425 };
6426 let (parent_runtime_pid, parent_v8_signal_session, should_signal_parent) = {
6427 let Some(parent) =
6428 Self::descendant_parent_process(vm, process_id, current_process_path)
6429 else {
6430 return Ok(Value::Null);
6431 };
6432 (
6433 parent.execution.child_pid(),
6434 parent.execution.javascript_v8_session_handle().filter(|_| {
6435 matches!(
6436 &parent.execution,
6437 ActiveExecution::Javascript(execution)
6438 if execution.uses_shared_v8_runtime()
6439 )
6440 }),
6441 vm.signal_states
6442 .get(parent_signal_key)
6443 .and_then(|handlers| handlers.get(&(libc::SIGCHLD as u32)))
6444 .is_some_and(|registration| {
6445 registration.action != SignalDispositionAction::Default
6446 }),
6447 )
6448 };
6449 let Some(parent) =
6450 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6451 else {
6452 return Ok(Value::Null);
6453 };
6454 let Some(mut child) = parent.child_processes.remove(child_process_id) else {
6455 return Ok(Value::Null);
6456 };
6457 let child_process_label =
6458 Self::child_process_path_label(process_id, &child_path);
6459 let detached_children =
6460 Self::adopt_detached_child_processes(&child_process_label, &mut child);
6461 sync_process_host_writes_to_kernel(vm, &child)?;
6462 terminate_child_process_tree(&mut vm.kernel, &mut child);
6463 child.kernel_handle.finish(exit_code);
6464 let _ = vm.kernel.wait_and_reap(child.kernel_pid);
6465 vm.signal_states.remove(child_process_id);
6466 for (detached_process_id, detached_child) in detached_children {
6467 vm.detached_child_processes
6468 .insert(detached_process_id.clone());
6469 vm.active_processes
6470 .insert(detached_process_id, detached_child);
6471 }
6472 if should_signal_parent {
6473 if let Some(session) = parent_v8_signal_session {
6474 dispatch_v8_session_signal_async(session, libc::SIGCHLD);
6475 } else {
6476 signal_runtime_process(parent_runtime_pid, libc::SIGCHLD)?;
6477 }
6478 }
6479 let mut payload = Map::new();
6480 payload.insert(String::from("type"), Value::String(String::from("exit")));
6481 payload.insert(String::from("exitCode"), Value::from(exit_code));
6482 if let Some(signal_name) = signal_name {
6483 payload.insert(String::from("signal"), Value::String(signal_name));
6484 }
6485 return Ok(Value::Object(payload));
6486 }
6487 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
6488 let mut current_child_path = current_process_path.to_vec();
6489 current_child_path.push(child_process_id);
6490 let response = if request.method == "process.signal_state" {
6491 let (signal, registration) =
6492 parse_process_signal_state_request(&request.args)?;
6493 let Some(vm) = self.vms.get_mut(vm_id) else {
6494 return Ok(Value::Null);
6495 };
6496 let signal_key =
6497 Self::child_process_signal_key(process_id, ¤t_child_path)
6498 .to_owned();
6499 apply_process_signal_state_update(
6500 &mut vm.signal_states,
6501 &signal_key,
6502 signal,
6503 registration,
6504 );
6505 Ok(Value::Null)
6506 } else if request.method == "process.kill" {
6507 self.handle_descendant_process_kill_rpc(
6508 vm_id,
6509 process_id,
6510 current_process_path,
6511 child_process_id,
6512 &request,
6513 )
6514 } else if request.method.starts_with("child_process.") {
6515 self.handle_descendant_javascript_child_process_rpc(
6516 vm_id,
6517 process_id,
6518 ¤t_child_path,
6519 &request,
6520 )
6521 } else {
6522 let Some(vm) = self.vms.get_mut(vm_id) else {
6523 return Ok(Value::Null);
6524 };
6525 let resource_limits = vm.kernel.resource_limits().clone();
6526 let network_counts = vm_network_resource_counts(vm);
6527 let socket_paths = build_javascript_socket_path_context(vm)?;
6528 let Some(root) = vm.active_processes.get_mut(process_id) else {
6529 return Ok(Value::Null);
6530 };
6531 let Some(parent) =
6532 Self::active_process_by_path_mut(root, current_process_path)
6533 else {
6534 return Ok(Value::Null);
6535 };
6536 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6537 return Ok(Value::Null);
6538 };
6539 service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
6540 bridge: &self.bridge,
6541 vm_id,
6542 dns: &vm.dns,
6543 socket_paths: &socket_paths,
6544 kernel: &mut vm.kernel,
6545 process: child,
6546 sync_request: &request,
6547 resource_limits: &resource_limits,
6548 network_counts,
6549 })
6550 };
6551
6552 let Some(vm) = self.vms.get_mut(vm_id) else {
6553 return Ok(Value::Null);
6554 };
6555 let Some(parent) =
6556 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6557 else {
6558 return Ok(Value::Null);
6559 };
6560 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6561 return Ok(Value::Null);
6562 };
6563 let parent_signal_event = response.as_ref().ok().and_then(|result| {
6564 let target_path_label =
6565 Self::child_process_path_label(process_id, current_process_path);
6566 if request.method != "process.kill"
6567 || result.get("action").and_then(Value::as_str) != Some("user")
6568 || result.get("targetProcessPath").and_then(Value::as_str)
6569 != Some(target_path_label.as_str())
6570 {
6571 return None;
6572 }
6573 Some(json!({
6574 "type": "signal",
6575 "signal": result.get("signal").and_then(Value::as_str).unwrap_or_default(),
6576 "number": result.get("number").and_then(Value::as_i64).unwrap_or_default(),
6577 }))
6578 });
6579 match response {
6580 Ok(result) => child
6581 .execution
6582 .respond_javascript_sync_rpc_success(request.id, result)
6583 .or_else(ignore_stale_javascript_sync_rpc_response)?,
6584 Err(error) => child
6585 .execution
6586 .respond_javascript_sync_rpc_error(
6587 request.id,
6588 javascript_sync_rpc_error_code(&error),
6589 error.to_string(),
6590 )
6591 .or_else(ignore_stale_javascript_sync_rpc_response)?,
6592 }
6593 if let Some(event) = parent_signal_event {
6594 return Ok(event);
6595 }
6596 }
6597 ActiveExecutionEvent::PythonVfsRpcRequest(_) => {
6598 return Err(SidecarError::InvalidState(String::from(
6599 "nested Python child_process execution is not supported yet",
6600 )));
6601 }
6602 ActiveExecutionEvent::SignalState {
6603 signal,
6604 registration,
6605 } => {
6606 let Some(vm) = self.vms.get_mut(vm_id) else {
6607 return Ok(Value::Null);
6608 };
6609 let signal_key =
6610 Self::child_process_signal_key(process_id, &child_path).to_owned();
6611 apply_process_signal_state_update(
6612 &mut vm.signal_states,
6613 &signal_key,
6614 signal,
6615 registration.clone(),
6616 );
6617 return Ok(json!({
6618 "type": "signal_state",
6619 "signal": signal,
6620 "registration": registration,
6621 }));
6622 }
6623 }
6624 }
6625 }
6626
6627 fn recover_descendant_runtime_child_process_event(
6628 &mut self,
6629 vm_id: &str,
6630 process_id: &str,
6631 current_process_path: &[&str],
6632 child_process_id: &str,
6633 wait_ms: u64,
6634 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
6635 let (
6636 parent_kernel_pid,
6637 child_kernel_pid,
6638 child_runtime_pid,
6639 child_runtime,
6640 child_shared_runtime,
6641 ) = {
6642 let mut child_path = current_process_path.to_vec();
6643 child_path.push(child_process_id);
6644 let Some(vm) = self.vms.get_mut(vm_id) else {
6645 return Ok(None);
6646 };
6647 let Some(parent) =
6648 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6649 else {
6650 return Err(javascript_child_process_gone_error(process_id, &child_path));
6651 };
6652 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6653 return Err(javascript_child_process_gone_error(process_id, &child_path));
6654 };
6655 (
6656 parent.kernel_pid,
6657 child.kernel_pid,
6658 child.execution.child_pid(),
6659 child.runtime.clone(),
6660 child.execution.uses_shared_v8_runtime(),
6661 )
6662 };
6663 if child_runtime != GuestRuntimeKind::JavaScript
6664 && child_runtime != GuestRuntimeKind::Python
6665 && child_runtime != GuestRuntimeKind::WebAssembly
6666 {
6667 return Ok(None);
6668 }
6669 let wait_deadline = Instant::now() + Duration::from_millis(wait_ms.min(25));
6670 loop {
6671 let Some(vm) = self.vms.get_mut(vm_id) else {
6672 return Ok(None);
6673 };
6674 if let Some(process_info) = vm.kernel.list_processes().get(&child_kernel_pid) {
6675 if process_info.status == ProcessStatus::Exited {
6676 return Ok(Some(ActiveExecutionEvent::Exited(
6677 process_info.exit_code.unwrap_or(0),
6678 )));
6679 }
6680 }
6681 if let Some(wait_result) = vm
6682 .kernel
6683 .waitpid_with_options(
6684 EXECUTION_DRIVER_NAME,
6685 parent_kernel_pid,
6686 child_kernel_pid as i32,
6687 WaitPidFlags::WNOHANG,
6688 )
6689 .map_err(kernel_error)?
6690 {
6691 return Ok(Some(ActiveExecutionEvent::Exited(wait_result.status)));
6692 }
6693
6694 if !child_shared_runtime && child_runtime_pid != 0 {
6695 if let Some(status) = runtime_child_exit_status(child_runtime_pid)? {
6696 return Ok(Some(ActiveExecutionEvent::Exited(status)));
6697 }
6698 if !runtime_child_is_alive(child_runtime_pid)? {
6699 return Ok(Some(ActiveExecutionEvent::Exited(0)));
6700 }
6701 }
6702 if Instant::now() >= wait_deadline {
6703 return Ok(None);
6704 }
6705 std::thread::sleep(Duration::from_millis(5));
6706 }
6707 }
6708
6709 fn write_descendant_javascript_child_process_stdin(
6710 &mut self,
6711 vm_id: &str,
6712 process_id: &str,
6713 current_process_path: &[&str],
6714 child_process_id: &str,
6715 chunk: &[u8],
6716 ) -> Result<(), SidecarError> {
6717 let mut child_path = current_process_path.to_vec();
6718 child_path.push(child_process_id);
6719 let Some(vm) = self.vms.get_mut(vm_id) else {
6720 return Err(javascript_child_process_gone_error(process_id, &child_path));
6721 };
6722 let Some(root) = vm.active_processes.get_mut(process_id) else {
6723 return Err(javascript_child_process_gone_error(process_id, &child_path));
6724 };
6725 let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6726 return Err(javascript_child_process_gone_error(process_id, &child_path));
6727 };
6728 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6729 return Err(javascript_child_process_gone_error(process_id, &child_path));
6730 };
6731 if let Err(error) = child.execution.write_stdin(chunk) {
6732 if is_broken_pipe_error(&error) {
6733 return Ok(());
6734 }
6735 return Err(error);
6736 }
6737 write_kernel_process_stdin(&mut vm.kernel, child, chunk)
6738 }
6739
6740 fn close_descendant_javascript_child_process_stdin(
6741 &mut self,
6742 vm_id: &str,
6743 process_id: &str,
6744 current_process_path: &[&str],
6745 child_process_id: &str,
6746 ) -> Result<(), SidecarError> {
6747 let mut child_path = current_process_path.to_vec();
6748 child_path.push(child_process_id);
6749 let Some(vm) = self.vms.get_mut(vm_id) else {
6750 return Err(javascript_child_process_gone_error(process_id, &child_path));
6751 };
6752 let Some(root) = vm.active_processes.get_mut(process_id) else {
6753 return Err(javascript_child_process_gone_error(process_id, &child_path));
6754 };
6755 let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6756 return Err(javascript_child_process_gone_error(process_id, &child_path));
6757 };
6758 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6759 return Err(javascript_child_process_gone_error(process_id, &child_path));
6760 };
6761 child.execution.close_stdin()?;
6762 close_kernel_process_stdin(&mut vm.kernel, child)
6763 }
6764
6765 fn kill_descendant_javascript_child_process(
6766 &mut self,
6767 vm_id: &str,
6768 process_id: &str,
6769 current_process_path: &[&str],
6770 child_process_id: &str,
6771 signal: &str,
6772 ) -> Result<(), SidecarError> {
6773 let signal_name = signal.to_owned();
6774 let signal = parse_signal(signal)?;
6775 let Some(vm) = self.vms.get_mut(vm_id) else {
6776 return Ok(());
6777 };
6778 let Some(root) = vm.active_processes.get_mut(process_id) else {
6779 return Ok(());
6780 };
6781 let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6782 return Ok(());
6783 };
6784 let source_pid = parent.kernel_pid;
6785 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6786 return Ok(());
6787 };
6788 terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
6789 let child_process_label = if current_process_path.is_empty() {
6790 child_process_id.to_owned()
6791 } else {
6792 format!("{}/{}", current_process_path.join("/"), child_process_id)
6793 };
6794 emit_security_audit_event(
6795 &self.bridge,
6796 vm_id,
6797 "security.process.kill",
6798 audit_fields([
6799 (String::from("source"), String::from("guest_child_process")),
6800 (String::from("source_pid"), source_pid.to_string()),
6801 (String::from("target_pid"), child.kernel_pid.to_string()),
6802 (String::from("process_id"), process_id.to_owned()),
6803 (String::from("child_process_id"), child_process_label),
6804 (String::from("signal"), signal_name),
6805 ]),
6806 );
6807 Ok(())
6808 }
6809
6810 fn handle_descendant_process_kill_rpc(
6811 &mut self,
6812 vm_id: &str,
6813 process_id: &str,
6814 current_process_path: &[&str],
6815 child_process_id: &str,
6816 request: &JavascriptSyncRpcRequest,
6817 ) -> Result<Value, SidecarError> {
6818 let target_pid = javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
6819 let signal_name = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
6820 let signal = parse_signal(signal_name)?;
6821
6822 let mut source_path = current_process_path.to_vec();
6823 source_path.push(child_process_id);
6824
6825 if signal != 0 && target_pid < 0 {
6826 let pgid = target_pid.unsigned_abs();
6827 let caller_kernel_pid = {
6828 let Some(vm) = self.vms.get(vm_id) else {
6829 return Err(SidecarError::InvalidState(String::from(
6830 "ESRCH: unknown VM during process.kill",
6831 )));
6832 };
6833 let Some(root) = vm.active_processes.get(process_id) else {
6834 return Err(SidecarError::InvalidState(format!(
6835 "ESRCH: unknown process {process_id} during process.kill",
6836 )));
6837 };
6838 let Some(source) = Self::active_process_by_path(root, &source_path) else {
6839 return Err(SidecarError::InvalidState(format!(
6840 "ESRCH: unknown child process {child_process_id} during process.kill",
6841 )));
6842 };
6843 source.kernel_pid
6844 };
6845 let caller_is_member =
6846 self.signal_vm_process_group(vm_id, caller_kernel_pid, pgid, signal_name)?;
6847 if !caller_is_member {
6848 return Ok(Value::Null);
6849 }
6850 let Some(vm) = self.vms.get_mut(vm_id) else {
6851 return Ok(Value::Null);
6852 };
6853 let Some(root) = vm.active_processes.get_mut(process_id) else {
6854 return Ok(Value::Null);
6855 };
6856 let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
6857 return Ok(Value::Null);
6858 };
6859 source.pending_self_signal_exit = None;
6860 if !matches!(
6861 canonical_signal_name(signal),
6862 Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
6863 ) {
6864 source.pending_self_signal_exit = Some(signal);
6865 }
6866 return Ok(json!({
6867 "self": true,
6868 "action": "default",
6869 }));
6870 }
6871
6872 let Some(vm) = self.vms.get_mut(vm_id) else {
6873 return Err(SidecarError::InvalidState(String::from(
6874 "ESRCH: unknown VM during process.kill",
6875 )));
6876 };
6877
6878 if signal == 0 {
6879 vm.kernel
6880 .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
6881 .map_err(kernel_error)?;
6882 return Ok(Value::Null);
6883 }
6884
6885 let target_kernel_pid = u32::try_from(target_pid).map_err(|_| {
6886 SidecarError::InvalidState(format!("EINVAL: invalid process pid {target_pid}"))
6887 })?;
6888 let (source_pid, located_target_path) = {
6889 let Some(root) = vm.active_processes.get(process_id) else {
6890 return Err(SidecarError::InvalidState(format!(
6891 "ESRCH: unknown process {process_id} during process.kill",
6892 )));
6893 };
6894 let Some(source) = Self::active_process_by_path(root, &source_path) else {
6895 return Err(SidecarError::InvalidState(format!(
6896 "ESRCH: unknown child process {child_process_id} during process.kill",
6897 )));
6898 };
6899 vm.kernel
6900 .signal_process(EXECUTION_DRIVER_NAME, target_pid, 0)
6901 .map_err(kernel_error)?;
6902 (
6903 source.kernel_pid,
6904 Self::active_process_path_by_kernel_pid(root, target_kernel_pid),
6905 )
6906 };
6907 let Some(target_path) = located_target_path else {
6908 self.signal_vm_kernel_pid(vm_id, target_kernel_pid, signal_name)?;
6912 return Ok(Value::Null);
6913 };
6914 let Some(vm) = self.vms.get_mut(vm_id) else {
6915 return Err(SidecarError::InvalidState(String::from(
6916 "ESRCH: unknown VM during process.kill",
6917 )));
6918 };
6919
6920 if source_pid == target_kernel_pid {
6921 let Some(root) = vm.active_processes.get_mut(process_id) else {
6922 return Ok(Value::Null);
6923 };
6924 let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
6925 return Ok(Value::Null);
6926 };
6927 source.pending_self_signal_exit = None;
6928 if !matches!(
6929 canonical_signal_name(signal),
6930 Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
6931 ) {
6932 source.pending_self_signal_exit = Some(signal);
6933 }
6934 return Ok(json!({
6935 "self": true,
6936 "action": "default",
6937 }));
6938 }
6939
6940 let signal_key = target_path.last().map(String::as_str).unwrap_or(process_id);
6941 let registration = vm
6942 .signal_states
6943 .get(signal_key)
6944 .and_then(|handlers| handlers.get(&(signal as u32)))
6945 .cloned();
6946
6947 let action = match registration
6948 .as_ref()
6949 .map(|registration| ®istration.action)
6950 {
6951 Some(SignalDispositionAction::Ignore) => "ignore",
6952 Some(SignalDispositionAction::User) => {
6953 let Some(root) = vm.active_processes.get_mut(process_id) else {
6954 return Ok(Value::Null);
6955 };
6956 let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
6957 else {
6958 return Err(SidecarError::InvalidState(format!(
6959 "ESRCH: unknown process pid {target_pid}"
6960 )));
6961 };
6962 if let Some(session) = target.execution.javascript_v8_session_handle().filter(
6963 |_| matches!(&target.execution, ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime())
6964 || matches!(&target.execution, ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime()),
6965 ) {
6966 dispatch_v8_session_signal_async(session, signal);
6967 } else if !dispatch_v8_process_signal(target, signal)? {
6968 return Err(SidecarError::InvalidState(format!(
6969 "unsupported guest signal delivery for pid {target_pid}"
6970 )));
6971 }
6972 "user"
6973 }
6974 Some(SignalDispositionAction::Default) | None
6975 if matches!(
6976 canonical_signal_name(signal),
6977 Some("SIGWINCH" | "SIGCHLD" | "SIGURG")
6978 ) =>
6979 {
6980 "ignore"
6981 }
6982 Some(SignalDispositionAction::Default) | None => {
6983 let Some(root) = vm.active_processes.get_mut(process_id) else {
6984 return Ok(Value::Null);
6985 };
6986 let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
6987 else {
6988 return Err(SidecarError::InvalidState(format!(
6989 "ESRCH: unknown process pid {target_pid}"
6990 )));
6991 };
6992 apply_active_process_default_signal(&mut vm.kernel, target, signal)?;
6993 "default"
6994 }
6995 };
6996
6997 let target_path_label = Self::child_process_path_label(
6998 process_id,
6999 &target_path.iter().map(String::as_str).collect::<Vec<_>>(),
7000 );
7001 emit_security_audit_event(
7002 &self.bridge,
7003 vm_id,
7004 "security.process.kill",
7005 audit_fields([
7006 (String::from("source"), String::from("guest_process")),
7007 (String::from("source_pid"), source_pid.to_string()),
7008 (String::from("target_pid"), target_pid.to_string()),
7009 (String::from("process_id"), process_id.to_owned()),
7010 (
7011 String::from("target_process_path"),
7012 target_path_label.clone(),
7013 ),
7014 (String::from("signal"), signal_name.to_owned()),
7015 ]),
7016 );
7017
7018 Ok(json!({
7019 "self": false,
7020 "action": action,
7021 "signal": signal_name,
7022 "number": signal,
7023 "targetProcessPath": target_path_label,
7024 }))
7025 }
7026
7027 pub(crate) fn poll_javascript_child_process(
7028 &mut self,
7029 vm_id: &str,
7030 process_id: &str,
7031 child_process_id: &str,
7032 wait_ms: u64,
7033 ) -> Result<Value, SidecarError> {
7034 self.poll_descendant_javascript_child_process(
7035 vm_id,
7036 process_id,
7037 &[],
7038 child_process_id,
7039 wait_ms,
7040 )
7041 }
7042
7043 pub(crate) fn write_javascript_child_process_stdin(
7044 &mut self,
7045 vm_id: &str,
7046 process_id: &str,
7047 child_process_id: &str,
7048 chunk: &[u8],
7049 ) -> Result<(), SidecarError> {
7050 let Some(vm) = self.vms.get_mut(vm_id) else {
7051 return Err(javascript_child_process_gone_error(
7052 process_id,
7053 &[child_process_id],
7054 ));
7055 };
7056 let Some(child) = vm
7057 .active_processes
7058 .get_mut(process_id)
7059 .ok_or_else(|| missing_process_error(vm_id, process_id))?
7060 .child_processes
7061 .get_mut(child_process_id)
7062 else {
7063 return Err(javascript_child_process_gone_error(
7064 process_id,
7065 &[child_process_id],
7066 ));
7067 };
7068 if let Err(error) = child.execution.write_stdin(chunk) {
7069 if is_broken_pipe_error(&error) {
7070 return Ok(());
7071 }
7072 return Err(error);
7073 }
7074 write_kernel_process_stdin(&mut vm.kernel, child, chunk)
7075 }
7076
7077 pub(crate) fn close_javascript_child_process_stdin(
7078 &mut self,
7079 vm_id: &str,
7080 process_id: &str,
7081 child_process_id: &str,
7082 ) -> Result<(), SidecarError> {
7083 let Some(vm) = self.vms.get_mut(vm_id) else {
7084 return Err(javascript_child_process_gone_error(
7085 process_id,
7086 &[child_process_id],
7087 ));
7088 };
7089 let Some(child) = vm
7090 .active_processes
7091 .get_mut(process_id)
7092 .ok_or_else(|| missing_process_error(vm_id, process_id))?
7093 .child_processes
7094 .get_mut(child_process_id)
7095 else {
7096 return Err(javascript_child_process_gone_error(
7097 process_id,
7098 &[child_process_id],
7099 ));
7100 };
7101 child.execution.close_stdin()?;
7102 close_kernel_process_stdin(&mut vm.kernel, child)
7103 }
7104
7105 pub(crate) fn kill_javascript_child_process(
7106 &mut self,
7107 vm_id: &str,
7108 process_id: &str,
7109 child_process_id: &str,
7110 signal: &str,
7111 ) -> Result<(), SidecarError> {
7112 let signal_name = signal.to_owned();
7113 let signal = parse_signal(signal)?;
7114 let Some(vm) = self.vms.get_mut(vm_id) else {
7115 return Ok(());
7116 };
7117 let process = vm
7118 .active_processes
7119 .get_mut(process_id)
7120 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
7121 let source_pid = process.kernel_pid;
7122 let child = process
7123 .child_processes
7124 .get_mut(child_process_id)
7125 .ok_or_else(|| {
7126 SidecarError::InvalidState(format!(
7127 "unknown child process {child_process_id} during kill"
7128 ))
7129 })?;
7130 terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
7131 emit_security_audit_event(
7132 &self.bridge,
7133 vm_id,
7134 "security.process.kill",
7135 audit_fields([
7136 (String::from("source"), String::from("guest_child_process")),
7137 (String::from("source_pid"), source_pid.to_string()),
7138 (String::from("target_pid"), child.kernel_pid.to_string()),
7139 (String::from("process_id"), process_id.to_owned()),
7140 (
7141 String::from("child_process_id"),
7142 child_process_id.to_owned(),
7143 ),
7144 (String::from("signal"), signal_name),
7145 ]),
7146 );
7147 Ok(())
7148 }
7149
7150 pub(crate) fn signal_vm_kernel_pid(
7156 &mut self,
7157 vm_id: &str,
7158 target_kernel_pid: u32,
7159 signal_name: &str,
7160 ) -> Result<(), SidecarError> {
7161 let signal = parse_signal(signal_name)?;
7162 let located = {
7163 let Some(vm) = self.vms.get(vm_id) else {
7164 return Err(SidecarError::InvalidState(String::from(
7165 "ESRCH: unknown VM during process.kill",
7166 )));
7167 };
7168 let alive = vm
7169 .kernel
7170 .list_processes()
7171 .get(&target_kernel_pid)
7172 .is_some_and(|info| info.status != ProcessStatus::Exited);
7173 if !alive {
7174 return Err(SidecarError::InvalidState(format!(
7175 "ESRCH: no such process {target_kernel_pid}"
7176 )));
7177 }
7178 vm.active_processes.iter().find_map(|(process_id, root)| {
7179 Self::active_process_path_by_kernel_pid(root, target_kernel_pid)
7180 .map(|path| (process_id.clone(), path))
7181 })
7182 };
7183
7184 match located {
7185 Some((process_id, path)) if path.is_empty() => {
7186 self.kill_process_internal(vm_id, &process_id, signal_name)
7187 }
7188 Some((process_id, path)) => {
7189 let Some(vm) = self.vms.get_mut(vm_id) else {
7190 return Ok(());
7191 };
7192 let Some(root) = vm.active_processes.get_mut(&process_id) else {
7193 return Ok(());
7194 };
7195 let Some(target) = Self::active_process_by_owned_path_mut(root, &path) else {
7196 return Err(SidecarError::InvalidState(format!(
7197 "ESRCH: no such process {target_kernel_pid}"
7198 )));
7199 };
7200 terminate_tracked_child_process_for_signal(&mut vm.kernel, target, signal)?;
7201 emit_security_audit_event(
7202 &self.bridge,
7203 vm_id,
7204 "security.process.kill",
7205 audit_fields([
7206 (String::from("source"), String::from("guest_process")),
7207 (String::from("target_pid"), target_kernel_pid.to_string()),
7208 (String::from("process_id"), process_id),
7209 (String::from("signal"), signal_name.to_owned()),
7210 ]),
7211 );
7212 Ok(())
7213 }
7214 None => {
7215 let Some(vm) = self.vms.get_mut(vm_id) else {
7216 return Ok(());
7217 };
7218 let target_pid = i32::try_from(target_kernel_pid).map_err(|_| {
7219 SidecarError::InvalidState(format!(
7220 "EINVAL: invalid process pid {target_kernel_pid}"
7221 ))
7222 })?;
7223 vm.kernel
7224 .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
7225 .map_err(kernel_error)?;
7226 emit_security_audit_event(
7227 &self.bridge,
7228 vm_id,
7229 "security.process.kill",
7230 audit_fields([
7231 (String::from("source"), String::from("guest_process")),
7232 (String::from("target_pid"), target_kernel_pid.to_string()),
7233 (String::from("signal"), signal_name.to_owned()),
7234 ]),
7235 );
7236 Ok(())
7237 }
7238 }
7239 }
7240
7241 pub(crate) fn signal_vm_process_group(
7246 &mut self,
7247 vm_id: &str,
7248 caller_kernel_pid: u32,
7249 pgid: u32,
7250 signal_name: &str,
7251 ) -> Result<bool, SidecarError> {
7252 parse_signal(signal_name)?;
7253 let members = {
7254 let Some(vm) = self.vms.get(vm_id) else {
7255 return Err(SidecarError::InvalidState(String::from(
7256 "ESRCH: unknown VM during process.kill",
7257 )));
7258 };
7259 vm.kernel
7260 .list_processes()
7261 .into_iter()
7262 .filter(|(_, info)| info.pgid == pgid && info.status != ProcessStatus::Exited)
7263 .map(|(pid, _)| pid)
7264 .collect::<Vec<_>>()
7265 };
7266 if members.is_empty() {
7267 return Err(SidecarError::InvalidState(format!(
7268 "ESRCH: no such process group {pgid}"
7269 )));
7270 }
7271
7272 let mut caller_is_member = false;
7273 for member_pid in members {
7274 if member_pid == caller_kernel_pid {
7275 caller_is_member = true;
7276 continue;
7277 }
7278 match self.signal_vm_kernel_pid(vm_id, member_pid, signal_name) {
7279 Ok(()) => {}
7280 Err(error) if sidecar_error_is_esrch(&error) => {}
7283 Err(error) => return Err(error),
7284 }
7285 }
7286 Ok(caller_is_member)
7287 }
7288}
7289
7290fn terminate_tracked_child_process_for_signal(
7295 kernel: &mut SidecarKernel,
7296 child: &mut ActiveProcess,
7297 signal: i32,
7298) -> Result<(), SidecarError> {
7299 let should_terminate_shared_runtime = child.execution.uses_shared_v8_runtime()
7300 && signal != 0
7301 && !matches!(
7302 signal,
7303 libc::SIGHUP
7304 | libc::SIGINT
7305 | libc::SIGTERM
7306 | libc::SIGCHLD
7307 | libc::SIGWINCH
7308 | libc::SIGSTOP
7309 | libc::SIGCONT
7310 );
7311 if should_terminate_shared_runtime {
7312 child.execution.terminate()?;
7313 child.pending_self_signal_exit = Some(signal);
7314 child.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
7315 } else {
7316 kernel
7317 .kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, signal)
7318 .map_err(kernel_error)?;
7319 }
7320 Ok(())
7321}
7322
7323fn sidecar_error_is_esrch(error: &SidecarError) -> bool {
7324 error.to_string().contains("ESRCH")
7325}
7326
7327fn apply_active_process_default_signal(
7328 kernel: &mut SidecarKernel,
7329 process: &mut ActiveProcess,
7330 signal: i32,
7331) -> Result<(), SidecarError> {
7332 if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
7333 return kernel
7334 .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
7335 .map_err(kernel_error);
7336 }
7337
7338 if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
7339 close_kernel_process_stdin(kernel, process)?;
7340 }
7341
7342 if process.execution.uses_shared_v8_runtime() {
7343 process.execution.terminate()?;
7344 if signal != 0 && matches!(process.execution, ActiveExecution::Wasm(_)) {
7345 process.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
7346 }
7347 return Ok(());
7348 }
7349
7350 kernel
7351 .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
7352 .map_err(kernel_error)
7353}
7354
7355fn map_wasm_signal_registration(
7356 registration: secure_exec_execution::wasm::WasmSignalHandlerRegistration,
7357) -> SignalHandlerRegistration {
7358 SignalHandlerRegistration {
7359 action: match registration.action {
7360 secure_exec_execution::wasm::WasmSignalDispositionAction::Default => {
7361 crate::protocol::SignalDispositionAction::Default
7362 }
7363 secure_exec_execution::wasm::WasmSignalDispositionAction::Ignore => {
7364 crate::protocol::SignalDispositionAction::Ignore
7365 }
7366 secure_exec_execution::wasm::WasmSignalDispositionAction::User => {
7367 crate::protocol::SignalDispositionAction::User
7368 }
7369 },
7370 mask: registration.mask,
7371 flags: registration.flags,
7372 }
7373}
7374
7375fn parse_process_signal_state_request(
7376 args: &[Value],
7377) -> Result<(u32, SignalHandlerRegistration), SidecarError> {
7378 let signal = javascript_sync_rpc_arg_u32(args, 0, "process.signal_state signal")?;
7379 let action = javascript_sync_rpc_arg_str(args, 1, "process.signal_state action")?;
7380 let mask_json = javascript_sync_rpc_arg_str(args, 2, "process.signal_state mask")?;
7381 let flags = javascript_sync_rpc_arg_u32(args, 3, "process.signal_state flags")?;
7382 let mask: Vec<u32> = serde_json::from_str(mask_json).map_err(|error| {
7383 SidecarError::InvalidState(format!(
7384 "process.signal_state mask must be valid JSON: {error}"
7385 ))
7386 })?;
7387 let action = match action.trim().to_ascii_lowercase().as_str() {
7388 "default" => SignalDispositionAction::Default,
7389 "ignore" => SignalDispositionAction::Ignore,
7390 "user" => SignalDispositionAction::User,
7391 other => {
7392 return Err(SidecarError::InvalidState(format!(
7393 "unsupported process.signal_state action {other}"
7394 )));
7395 }
7396 };
7397
7398 Ok((
7399 signal,
7400 SignalHandlerRegistration {
7401 action,
7402 mask,
7403 flags,
7404 },
7405 ))
7406}
7407
7408fn apply_process_signal_state_update(
7409 signal_states: &mut BTreeMap<String, BTreeMap<u32, SignalHandlerRegistration>>,
7410 process_id: &str,
7411 signal: u32,
7412 registration: SignalHandlerRegistration,
7413) {
7414 if registration.action == SignalDispositionAction::Default
7415 && registration.mask.is_empty()
7416 && registration.flags == 0
7417 {
7418 let remove_process_entry = signal_states
7419 .get_mut(process_id)
7420 .map(|handlers| {
7421 handlers.remove(&signal);
7422 handlers.is_empty()
7423 })
7424 .unwrap_or(false);
7425 if remove_process_entry {
7426 signal_states.remove(process_id);
7427 }
7428 return;
7429 }
7430
7431 signal_states
7432 .entry(process_id.to_owned())
7433 .or_default()
7434 .insert(signal, registration);
7435}
7436
7437fn map_node_signal_registration(
7438 registration: NodeSignalHandlerRegistration,
7439) -> SignalHandlerRegistration {
7440 SignalHandlerRegistration {
7441 action: match registration.action {
7442 NodeSignalDispositionAction::Default => SignalDispositionAction::Default,
7443 NodeSignalDispositionAction::Ignore => SignalDispositionAction::Ignore,
7444 NodeSignalDispositionAction::User => SignalDispositionAction::User,
7445 },
7446 mask: registration.mask,
7447 flags: registration.flags,
7448 }
7449}
7450
7451fn javascript_child_process_sync_input_bytes(
7452 value: Option<&Value>,
7453) -> Result<Option<Vec<u8>>, SidecarError> {
7454 let Some(value) = value else {
7455 return Ok(None);
7456 };
7457
7458 match value {
7459 Value::Null => Ok(None),
7460 Value::String(text) => Ok(Some(text.as_bytes().to_vec())),
7461 other => javascript_sync_rpc_bytes_arg(
7462 std::slice::from_ref(other),
7463 0,
7464 "child_process.spawn_sync input",
7465 )
7466 .map(Some),
7467 }
7468}
7469
7470fn resolve_execute_request(
7475 vm: &VmState,
7476 payload: &ExecuteRequest,
7477) -> Result<ResolvedChildProcessExecution, SidecarError> {
7478 let payload_env: BTreeMap<String, String> = payload
7479 .env
7480 .iter()
7481 .map(|(k, v)| (k.clone(), v.clone()))
7482 .collect();
7483 if let Some(command) = payload.command.as_deref() {
7484 return resolve_command_execution(
7485 vm,
7486 command,
7487 &payload.args,
7488 &payload_env,
7489 payload.cwd.as_deref(),
7490 payload.wasm_permission_tier,
7491 );
7492 }
7493
7494 let runtime = payload.runtime.clone().ok_or_else(|| {
7495 SidecarError::InvalidState(String::from("execute requires either command or runtime"))
7496 })?;
7497 let entrypoint = payload.entrypoint.clone().ok_or_else(|| {
7498 SidecarError::InvalidState(String::from(
7499 "execute requires either command or entrypoint",
7500 ))
7501 })?;
7502 let (guest_cwd, host_cwd, allow_host_path_overrides) =
7503 resolve_execution_cwds(vm, payload.cwd.as_deref());
7504 let mut env = vm.guest_env.clone();
7505 env.extend(payload_env.clone());
7506
7507 let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint);
7508 if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7509 let requested_cwd = payload.cwd.as_deref().unwrap_or(guest_cwd.as_str());
7510 return Err(SidecarError::InvalidState(format!(
7511 "execution cwd {requested_cwd} is outside sandbox root {}",
7512 vm.host_cwd.to_string_lossy()
7513 )));
7514 }
7515 let host_entrypoint_override = allow_host_path_overrides
7516 .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint))
7517 .flatten();
7518
7519 let guest_entrypoint = host_entrypoint_override
7520 .as_ref()
7521 .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7522 .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, &entrypoint));
7523 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7524
7525 Ok(ResolvedChildProcessExecution {
7526 command: match runtime {
7527 GuestRuntimeKind::JavaScript => String::from(JAVASCRIPT_COMMAND),
7528 GuestRuntimeKind::Python => String::from(PYTHON_COMMAND),
7529 GuestRuntimeKind::WebAssembly => String::from(WASM_COMMAND),
7530 },
7531 process_args: std::iter::once(entrypoint.clone())
7532 .chain(payload.args.iter().cloned())
7533 .collect(),
7534 runtime,
7535 entrypoint: host_entrypoint_override
7536 .map(|(_, host_entrypoint)| host_entrypoint)
7537 .unwrap_or(entrypoint),
7538 execution_args: payload.args.clone(),
7539 env,
7540 guest_cwd,
7541 host_cwd,
7542 wasm_permission_tier: payload.wasm_permission_tier,
7543 tool_command: false,
7544 })
7545}
7546
7547fn resolve_command_execution(
7548 vm: &VmState,
7549 command: &str,
7550 args: &[String],
7551 extra_env: &BTreeMap<String, String>,
7552 cwd: Option<&str>,
7553 explicit_wasm_permission_tier: Option<WasmPermissionTier>,
7554) -> Result<ResolvedChildProcessExecution, SidecarError> {
7555 let (guest_cwd, host_cwd, allow_host_path_overrides) = resolve_execution_cwds(vm, cwd);
7556 let mut env = vm.guest_env.clone();
7557 env.extend(extra_env.clone());
7558 let args = apply_shell_cwd_prefix(command, args.to_vec(), &guest_cwd);
7559
7560 if is_tool_command(vm, command) {
7561 let command = normalized_tool_command_name(command).unwrap_or_else(|| command.to_owned());
7562 return Ok(ResolvedChildProcessExecution {
7563 command: command.clone(),
7564 process_args: std::iter::once(command.clone())
7565 .chain(args.iter().cloned())
7566 .collect(),
7567 runtime: GuestRuntimeKind::JavaScript,
7568 entrypoint: command,
7569 execution_args: args,
7570 env,
7571 guest_cwd,
7572 host_cwd,
7573 wasm_permission_tier: None,
7574 tool_command: true,
7575 });
7576 }
7577
7578 if is_node_runtime_command(command) {
7579 if let Some(cli) = resolve_host_node_cli_entrypoint(command) {
7580 env.insert(
7581 String::from("AGENTOS_NODE_EVAL"),
7582 build_host_node_cli_eval(&cli),
7583 );
7584 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7585 add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
7586 add_runtime_host_access_path(
7587 &mut env,
7588 "AGENTOS_EXTRA_FS_READ_PATHS",
7589 &cli.package_root,
7590 true,
7591 );
7592
7593 return Ok(ResolvedChildProcessExecution {
7594 command: String::from(JAVASCRIPT_COMMAND),
7595 process_args: std::iter::once(command.to_owned())
7596 .chain(args.iter().cloned())
7597 .collect(),
7598 runtime: GuestRuntimeKind::JavaScript,
7599 entrypoint: String::from("-e"),
7600 execution_args: std::iter::once(cli.guest_entrypoint.clone())
7601 .chain(args.iter().cloned())
7602 .collect(),
7603 env,
7604 guest_cwd,
7605 host_cwd,
7606 wasm_permission_tier: None,
7607 tool_command: false,
7608 });
7609 }
7610
7611 if args.is_empty() {
7612 env.insert(String::from("AGENTOS_NODE_EVAL"), String::new());
7613 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7614
7615 return Ok(ResolvedChildProcessExecution {
7616 command: String::from(JAVASCRIPT_COMMAND),
7617 process_args: vec![command.to_owned()],
7618 runtime: GuestRuntimeKind::JavaScript,
7619 entrypoint: String::from("-e"),
7620 execution_args: Vec::new(),
7621 env,
7622 guest_cwd,
7623 host_cwd,
7624 wasm_permission_tier: None,
7625 tool_command: false,
7626 });
7627 }
7628
7629 if let Some((entrypoint, execution_args)) =
7630 resolve_special_node_cli_invocation(&args, &mut env)
7631 {
7632 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7633
7634 return Ok(ResolvedChildProcessExecution {
7635 command: String::from(JAVASCRIPT_COMMAND),
7636 process_args: std::iter::once(command.to_owned())
7637 .chain(args.iter().cloned())
7638 .collect(),
7639 runtime: GuestRuntimeKind::JavaScript,
7640 entrypoint,
7641 execution_args,
7642 env,
7643 guest_cwd,
7644 host_cwd,
7645 wasm_permission_tier: None,
7646 tool_command: false,
7647 });
7648 }
7649
7650 let Some(entrypoint_specifier) = args.first() else {
7651 return Err(SidecarError::InvalidState(format!(
7652 "{command} execution requires an entrypoint"
7653 )));
7654 };
7655
7656 let (entrypoint, execution_args, guest_entrypoint) = {
7657 let requested_host_entrypoint =
7658 resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier);
7659 if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7660 let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
7661 return Err(SidecarError::InvalidState(format!(
7662 "execution cwd {requested_cwd} is outside sandbox root {}",
7663 vm.host_cwd.to_string_lossy()
7664 )));
7665 }
7666 let host_entrypoint_override = allow_host_path_overrides
7667 .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier))
7668 .flatten();
7669 let guest_entrypoint = host_entrypoint_override
7670 .as_ref()
7671 .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7672 .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, entrypoint_specifier));
7673 let entrypoint = host_entrypoint_override.map_or_else(
7674 || {
7675 guest_entrypoint.as_ref().map_or_else(
7676 || entrypoint_specifier.clone(),
7677 |guest_entrypoint| {
7678 resolve_vm_guest_path_to_host(vm, guest_entrypoint)
7679 .to_string_lossy()
7680 .into_owned()
7681 },
7682 )
7683 },
7684 |(_, host_entrypoint)| host_entrypoint,
7685 );
7686 (
7687 entrypoint,
7688 args.iter().skip(1).cloned().collect(),
7689 guest_entrypoint,
7690 )
7691 };
7692
7693 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7694
7695 return Ok(ResolvedChildProcessExecution {
7696 command: String::from(JAVASCRIPT_COMMAND),
7697 process_args: std::iter::once(command.to_owned())
7698 .chain(args.iter().cloned())
7699 .collect(),
7700 runtime: GuestRuntimeKind::JavaScript,
7701 entrypoint,
7702 execution_args,
7703 env,
7704 guest_cwd,
7705 host_cwd,
7706 wasm_permission_tier: None,
7707 tool_command: false,
7708 });
7709 }
7710
7711 if command.ends_with(".js") || command.ends_with(".mjs") || command.ends_with(".cjs") {
7712 let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, command);
7713 if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7714 let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
7715 return Err(SidecarError::InvalidState(format!(
7716 "execution cwd {requested_cwd} is outside sandbox root {}",
7717 vm.host_cwd.to_string_lossy()
7718 )));
7719 }
7720 let host_entrypoint_override = allow_host_path_overrides
7721 .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, command))
7722 .flatten();
7723 let guest_entrypoint = host_entrypoint_override
7724 .as_ref()
7725 .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7726 .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, command));
7727 let entrypoint = host_entrypoint_override.map_or_else(
7728 || {
7729 guest_entrypoint.as_ref().map_or_else(
7730 || command.to_owned(),
7731 |guest_entrypoint| {
7732 resolve_vm_guest_path_to_host(vm, guest_entrypoint)
7733 .to_string_lossy()
7734 .into_owned()
7735 },
7736 )
7737 },
7738 |(_, host_entrypoint)| host_entrypoint,
7739 );
7740 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7741
7742 return Ok(ResolvedChildProcessExecution {
7743 command: String::from(JAVASCRIPT_COMMAND),
7744 process_args: std::iter::once(command.to_owned())
7745 .chain(args.iter().cloned())
7746 .collect(),
7747 runtime: GuestRuntimeKind::JavaScript,
7748 entrypoint,
7749 execution_args: args.to_vec(),
7750 env,
7751 guest_cwd,
7752 host_cwd,
7753 wasm_permission_tier: None,
7754 tool_command: false,
7755 });
7756 }
7757
7758 let guest_entrypoint = resolve_guest_command_entrypoint(
7759 vm,
7760 &guest_cwd,
7761 command,
7762 env.get("PATH").map(String::as_str),
7763 )
7764 .ok_or_else(|| {
7765 SidecarError::InvalidState(format!(
7766 "command not found on native sidecar path: {command}"
7767 ))
7768 })?;
7769 let wasm_permission_tier = explicit_wasm_permission_tier
7770 .or_else(|| vm.command_permissions.get(command).copied())
7771 .or_else(|| {
7772 Path::new(&guest_entrypoint)
7773 .file_name()
7774 .and_then(|name| name.to_str())
7775 .and_then(|name| vm.command_permissions.get(name).copied())
7776 });
7777
7778 let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
7779 if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
7780 resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
7781 {
7782 prepare_guest_runtime_env(
7783 vm,
7784 &mut env,
7785 &guest_cwd,
7786 &host_cwd,
7787 Some(javascript_guest_entrypoint),
7788 )?;
7789
7790 return Ok(ResolvedChildProcessExecution {
7791 command: command.to_owned(),
7792 process_args: std::iter::once(command.to_owned())
7793 .chain(args.iter().cloned())
7794 .collect(),
7795 runtime: GuestRuntimeKind::JavaScript,
7796 entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
7797 execution_args: args.to_vec(),
7798 env,
7799 guest_cwd,
7800 host_cwd,
7801 wasm_permission_tier: None,
7802 tool_command: false,
7803 });
7804 }
7805 prepare_guest_runtime_env(
7806 vm,
7807 &mut env,
7808 &guest_cwd,
7809 &host_cwd,
7810 Some(guest_entrypoint.clone()),
7811 )?;
7812
7813 Ok(ResolvedChildProcessExecution {
7814 command: command.to_owned(),
7815 process_args: std::iter::once(command.to_owned())
7816 .chain(args.iter().cloned())
7817 .collect(),
7818 runtime: GuestRuntimeKind::WebAssembly,
7819 entrypoint: host_entrypoint.to_string_lossy().into_owned(),
7820 execution_args: args.to_vec(),
7821 env,
7822 guest_cwd,
7823 host_cwd,
7824 wasm_permission_tier,
7825 tool_command: false,
7826 })
7827}
7828
7829const MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH: usize = 4;
7830
7831fn resolve_javascript_command_entrypoint(
7832 vm: &VmState,
7833 guest_entrypoint: &str,
7834 host_entrypoint: &Path,
7835) -> Option<(String, PathBuf)> {
7836 resolve_javascript_command_entrypoint_inner(
7837 vm,
7838 guest_entrypoint,
7839 host_entrypoint,
7840 MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH,
7841 )
7842}
7843
7844fn resolve_javascript_command_entrypoint_inner(
7845 vm: &VmState,
7846 guest_entrypoint: &str,
7847 host_entrypoint: &Path,
7848 redirects_remaining: usize,
7849) -> Option<(String, PathBuf)> {
7850 if redirects_remaining > 0 {
7851 let symlink_target = fs::symlink_metadata(host_entrypoint)
7852 .ok()
7853 .filter(|metadata| metadata.file_type().is_symlink())
7854 .and_then(|_| fs::read_link(host_entrypoint).ok());
7855 if let Some(symlink_target) = symlink_target {
7856 let guest_parent = Path::new(guest_entrypoint)
7857 .parent()
7858 .and_then(|path| path.to_str())
7859 .unwrap_or("/");
7860 let symlink_guest_entrypoint = if symlink_target.is_absolute() {
7861 normalize_path(&symlink_target.to_string_lossy())
7862 } else {
7863 normalize_path(&format!(
7864 "{guest_parent}/{}",
7865 symlink_target.to_string_lossy().replace('\\', "/")
7866 ))
7867 };
7868 let symlink_host_entrypoint =
7869 resolve_vm_guest_path_to_host(vm, &symlink_guest_entrypoint);
7870 return resolve_javascript_command_entrypoint_inner(
7871 vm,
7872 &symlink_guest_entrypoint,
7873 &symlink_host_entrypoint,
7874 redirects_remaining - 1,
7875 );
7876 }
7877 }
7878
7879 let script = load_executable_script_preview(host_entrypoint)?;
7880 let interpreter = parse_script_interpreter_name(&script);
7881
7882 if interpreter.is_none() && is_probable_javascript_entrypoint(host_entrypoint, &script) {
7883 return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
7884 }
7885
7886 let interpreter = interpreter?;
7887 if interpreter == "node" {
7888 return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
7889 }
7890
7891 if redirects_remaining == 0 || !matches!(interpreter.as_str(), "sh" | "bash" | "dash") {
7892 return None;
7893 }
7894
7895 let shim_target = parse_node_shell_shim_target(&script)?;
7896 let guest_parent = Path::new(guest_entrypoint)
7897 .parent()
7898 .and_then(|path| path.to_str())
7899 .unwrap_or("/");
7900 let shim_guest_entrypoint = normalize_path(&format!("{guest_parent}/{shim_target}"));
7901 let shim_host_entrypoint = resolve_vm_guest_path_to_host(vm, &shim_guest_entrypoint);
7902 resolve_javascript_command_entrypoint_inner(
7903 vm,
7904 &shim_guest_entrypoint,
7905 &shim_host_entrypoint,
7906 redirects_remaining - 1,
7907 )
7908}
7909
7910fn load_executable_script_preview(path: &Path) -> Option<String> {
7911 let bytes = fs::read(path).ok()?;
7912 let preview_len = bytes.len().min(16 * 1024);
7913 Some(String::from_utf8_lossy(&bytes[..preview_len]).into_owned())
7914}
7915
7916fn parse_script_interpreter_name(script: &str) -> Option<String> {
7917 let shebang = script.lines().next()?.strip_prefix("#!")?.trim();
7918 let mut tokens = shebang.split_whitespace();
7919 let command = tokens.next()?;
7920 let command_name = Path::new(command).file_name()?.to_str()?;
7921 if command_name == "env" {
7922 for token in tokens {
7923 if token.starts_with('-') {
7924 continue;
7925 }
7926 return Path::new(token)
7927 .file_name()
7928 .and_then(|name| name.to_str())
7929 .map(ToOwned::to_owned);
7930 }
7931 return None;
7932 }
7933
7934 Some(command_name.to_owned())
7935}
7936
7937fn parse_node_shell_shim_target(script: &str) -> Option<String> {
7938 for line in script.lines() {
7939 let trimmed = line.trim();
7940 if !trimmed.starts_with("exec ") {
7941 continue;
7942 }
7943
7944 let mut remaining = trimmed;
7945 while let Some(start) = remaining.find("\"$basedir/") {
7946 let after_prefix = &remaining[start + "\"$basedir/".len()..];
7947 let end = after_prefix.find('"')?;
7948 let candidate = &after_prefix[..end];
7949 remaining = &after_prefix[end + 1..];
7950
7951 if candidate.is_empty() || candidate == "node" || candidate.ends_with("/node") {
7952 continue;
7953 }
7954
7955 return Some(candidate.to_owned());
7956 }
7957 }
7958
7959 None
7960}
7961
7962fn is_probable_javascript_entrypoint(path: &Path, script: &str) -> bool {
7963 let extension = path
7964 .extension()
7965 .and_then(|value| value.to_str())
7966 .unwrap_or_default();
7967 if matches!(extension, "js" | "cjs" | "mjs") {
7968 return true;
7969 }
7970
7971 if !path
7972 .components()
7973 .any(|component| component.as_os_str() == "node_modules")
7974 {
7975 return false;
7976 }
7977
7978 let preview = script.trim_start_matches('\u{feff}').trim_start();
7979 !preview.is_empty()
7980 && !preview.starts_with("#!")
7981 && (preview.starts_with("\"use strict\"")
7982 || preview.starts_with("'use strict'")
7983 || preview.starts_with("import ")
7984 || preview.starts_with("export ")
7985 || preview.starts_with("const ")
7986 || preview.starts_with("let ")
7987 || preview.starts_with("var ")
7988 || preview.starts_with("Object.defineProperty(exports")
7989 || preview.starts_with("module.exports")
7990 || preview.starts_with("require("))
7991}
7992
7993fn resolve_guest_execution_cwd(vm: &VmState, value: Option<&str>) -> String {
7994 value
7995 .map(normalize_path)
7996 .unwrap_or_else(|| vm.guest_cwd.clone())
7997}
7998
7999fn resolve_execution_cwds(vm: &VmState, value: Option<&str>) -> (String, PathBuf, bool) {
8000 if let Some(raw_cwd) = value {
8001 let normalized_vm_host_cwd = normalize_host_path(&vm.host_cwd);
8002 let requested_host_cwd = normalize_host_path(Path::new(raw_cwd));
8003 if path_is_within_root(&requested_host_cwd, &normalized_vm_host_cwd) {
8004 let relative = requested_host_cwd
8005 .strip_prefix(&normalized_vm_host_cwd)
8006 .unwrap_or_else(|_| Path::new(""));
8007 let relative = relative.to_string_lossy().replace('\\', "/");
8008 let guest_cwd = if relative.is_empty() {
8009 String::from("/")
8010 } else {
8011 normalize_path(&format!("/{relative}"))
8012 };
8013 return (guest_cwd, requested_host_cwd, true);
8014 }
8015 }
8016
8017 let guest_cwd = resolve_guest_execution_cwd(vm, value);
8018 let host_cwd = if value.is_none() {
8019 vm.host_cwd.clone()
8020 } else {
8021 resolve_vm_guest_path_to_host(vm, &guest_cwd)
8022 };
8023 (guest_cwd, host_cwd, value.is_none())
8024}
8025
8026fn resolve_vm_guest_path_to_host(vm: &VmState, guest_path: &str) -> PathBuf {
8027 host_mount_path_for_guest_path(vm, guest_path)
8028 .unwrap_or_else(|| shadow_path_for_guest(vm, guest_path))
8029}
8030
8031fn shadow_path_for_guest(vm: &VmState, guest_path: &str) -> PathBuf {
8032 let normalized = normalize_path(guest_path);
8033 let relative = normalized.trim_start_matches('/');
8034 if relative.is_empty() {
8035 return vm.cwd.clone();
8036 }
8037 vm.cwd.join(relative)
8038}
8039
8040fn apply_shell_cwd_prefix(command: &str, mut args: Vec<String>, guest_cwd: &str) -> Vec<String> {
8041 if guest_cwd == "/" || !is_shell_command(command) {
8042 return args;
8043 }
8044
8045 let Some(flag) = args.first() else {
8046 return args;
8047 };
8048 if !matches!(flag.as_str(), "-c" | "-lc") || args.len() < 2 {
8049 return args;
8050 }
8051
8052 let command_text = args[1].clone();
8053 let quoted_cwd = shell_single_quote(guest_cwd);
8054 args[1] = format!("cd {quoted_cwd} && {command_text}");
8055 args
8056}
8057
8058fn is_shell_command(command: &str) -> bool {
8059 Path::new(command)
8060 .file_name()
8061 .and_then(|name| name.to_str())
8062 .unwrap_or(command)
8063 .trim_end_matches(".exe")
8064 .eq("sh")
8065 || Path::new(command)
8066 .file_name()
8067 .and_then(|name| name.to_str())
8068 .unwrap_or(command)
8069 .trim_end_matches(".exe")
8070 .eq("bash")
8071}
8072
8073fn shell_single_quote(value: &str) -> String {
8074 if value.is_empty() {
8075 return String::from("''");
8076 }
8077 format!("'{}'", value.replace('\'', "'\"'\"'"))
8078}
8079
8080pub(crate) fn sync_active_process_host_writes_to_kernel(
8081 vm: &mut VmState,
8082) -> Result<(), SidecarError> {
8083 if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
8084 let shadow_root = vm.cwd.clone();
8085 sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
8086 }
8087
8088 let normalized_vm_root = normalize_host_path(&vm.cwd);
8089 let extra_roots = collect_active_process_host_sync_roots(vm, &normalized_vm_root);
8090 for (host_cwd, guest_cwd) in extra_roots {
8091 sync_host_directory_tree_to_kernel(vm, &host_cwd, &guest_cwd)?;
8092 }
8093
8094 Ok(())
8095}
8096
8097fn collect_active_process_host_sync_roots(
8098 vm: &VmState,
8099 normalized_vm_root: &Path,
8100) -> Vec<(PathBuf, String)> {
8101 let mut roots = Vec::new();
8102 let mut seen = BTreeSet::new();
8103
8104 for process in vm.active_processes.values() {
8105 collect_process_host_sync_roots(process, normalized_vm_root, &mut seen, &mut roots);
8106 }
8107
8108 roots
8109}
8110
8111fn collect_process_host_sync_roots(
8112 process: &ActiveProcess,
8113 normalized_vm_root: &Path,
8114 seen: &mut BTreeSet<(PathBuf, String)>,
8115 roots: &mut Vec<(PathBuf, String)>,
8116) {
8117 let normalized_host_cwd = normalize_host_path(&process.host_cwd);
8118 if !path_is_within_root(&normalized_host_cwd, normalized_vm_root) {
8119 let guest_cwd = normalize_path(&process.guest_cwd);
8120 if seen.insert((normalized_host_cwd.clone(), guest_cwd.clone())) {
8121 roots.push((normalized_host_cwd, guest_cwd));
8122 }
8123 }
8124
8125 for child in process.child_processes.values() {
8126 collect_process_host_sync_roots(child, normalized_vm_root, seen, roots);
8127 }
8128}
8129
8130fn sync_process_host_writes_to_kernel(
8131 vm: &mut VmState,
8132 process: &ActiveProcess,
8133) -> Result<(), SidecarError> {
8134 if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
8135 let shadow_root = vm.cwd.clone();
8136 sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
8137 }
8138
8139 if !path_is_within_root(
8140 &normalize_host_path(&process.host_cwd),
8141 &normalize_host_path(&vm.cwd),
8142 ) {
8143 sync_host_directory_tree_to_kernel(vm, &process.host_cwd, &process.guest_cwd)?;
8144 }
8145
8146 Ok(())
8147}
8148
8149fn sync_host_directory_tree_to_kernel(
8150 vm: &mut VmState,
8151 host_root: &Path,
8152 guest_root: &str,
8153) -> Result<(), SidecarError> {
8154 let normalized_host_root = normalize_host_path(host_root);
8155 let normalized_guest_root = normalize_path(guest_root);
8156 let mut synced_file_times = BTreeMap::new();
8157 sync_host_directory_tree_to_kernel_inner(
8158 vm,
8159 &normalized_host_root,
8160 &normalized_host_root,
8161 &normalized_guest_root,
8162 &mut synced_file_times,
8163 )
8164}
8165
8166fn sync_host_directory_tree_to_kernel_inner(
8167 vm: &mut VmState,
8168 host_root: &Path,
8169 current_host_dir: &Path,
8170 guest_root: &str,
8171 synced_file_times: &mut BTreeMap<(u64, u64), (u64, u64)>,
8172) -> Result<(), SidecarError> {
8173 let entries = match fs::read_dir(current_host_dir) {
8174 Ok(entries) => entries,
8175 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
8176 Err(error) => {
8177 return Err(SidecarError::Io(format!(
8178 "failed to read host shadow directory {}: {error}",
8179 current_host_dir.display()
8180 )));
8181 }
8182 };
8183
8184 for entry in entries {
8185 let entry = entry.map_err(|error| {
8186 SidecarError::Io(format!(
8187 "failed to read host shadow entry in {}: {error}",
8188 current_host_dir.display()
8189 ))
8190 })?;
8191 let host_path = entry.path();
8192 let file_type = entry.file_type().map_err(|error| {
8193 SidecarError::Io(format!(
8194 "failed to stat host shadow entry {}: {error}",
8195 host_path.display()
8196 ))
8197 })?;
8198 let relative_path = host_path
8199 .strip_prefix(host_root)
8200 .map_err(|error| {
8201 SidecarError::InvalidState(format!(
8202 "failed to relativize host shadow path {} against {}: {error}",
8203 host_path.display(),
8204 host_root.display()
8205 ))
8206 })?
8207 .to_string_lossy()
8208 .replace('\\', "/");
8209 let guest_path = if guest_root == "/" {
8210 normalize_path(&format!("/{relative_path}"))
8211 } else {
8212 normalize_path(&format!(
8213 "{}/{}",
8214 guest_root.trim_end_matches('/'),
8215 relative_path
8216 ))
8217 };
8218
8219 if should_skip_shadow_sync_path(vm, &guest_path) {
8220 continue;
8221 }
8222
8223 if file_type.is_dir() {
8224 let metadata = entry.metadata().map_err(|error| {
8225 SidecarError::Io(format!(
8226 "failed to read host shadow metadata {}: {error}",
8227 host_path.display()
8228 ))
8229 })?;
8230 if !is_shadow_bootstrap_dir(&guest_path)
8231 && !vm.kernel.exists(&guest_path).unwrap_or(false)
8232 {
8233 vm.kernel.mkdir(&guest_path, true).map_err(|error| {
8234 SidecarError::InvalidState(format!(
8235 "failed to sync host shadow directory {} to guest {}: {}",
8236 host_path.display(),
8237 guest_path,
8238 kernel_error(error)
8239 ))
8240 })?;
8241 vm.kernel
8242 .chmod(&guest_path, host_shadow_mode(&metadata))
8243 .map_err(|error| {
8244 SidecarError::InvalidState(format!(
8245 "failed to sync host shadow directory mode {} to guest {}: {}",
8246 host_path.display(),
8247 guest_path,
8248 kernel_error(error)
8249 ))
8250 })?;
8251 }
8252 sync_host_directory_tree_to_kernel_inner(
8253 vm,
8254 host_root,
8255 &host_path,
8256 guest_root,
8257 synced_file_times,
8258 )?;
8259 continue;
8260 }
8261
8262 if file_type.is_file() {
8263 let metadata = entry.metadata().map_err(|error| {
8264 SidecarError::Io(format!(
8265 "failed to read host shadow metadata {}: {error}",
8266 host_path.display()
8267 ))
8268 })?;
8269 let timestamp_key = (metadata.dev(), metadata.ino());
8270 let (atime_ms, mtime_ms) =
8271 *synced_file_times.entry(timestamp_key).or_insert_with(|| {
8272 (
8273 metadata_time_ms(metadata.atime(), metadata.atime_nsec()),
8274 metadata_time_ms(metadata.mtime(), metadata.mtime_nsec()),
8275 )
8276 });
8277 let desired_mode = host_shadow_mode(&metadata);
8278 if let Ok(existing) = vm.kernel.lstat(&guest_path) {
8299 if !existing.is_directory
8300 && !existing.is_symbolic_link
8301 && existing.size == metadata.len()
8302 && (existing.mode & 0o7777) == (desired_mode & 0o7777)
8303 && existing.mtime_ms == mtime_ms
8304 {
8305 continue;
8306 }
8307 }
8308 let bytes = read_host_shadow_file(&host_path, desired_mode).map_err(|error| {
8309 SidecarError::Io(format!(
8310 "failed to read host shadow file {}: {error}",
8311 host_path.display()
8312 ))
8313 })?;
8314 vm.kernel.write_file(&guest_path, bytes).map_err(|error| {
8315 SidecarError::InvalidState(format!(
8316 "failed to sync host shadow file {} to guest {}: {}",
8317 host_path.display(),
8318 guest_path,
8319 kernel_error(error)
8320 ))
8321 })?;
8322 vm.kernel
8323 .chmod(&guest_path, desired_mode)
8324 .map_err(|error| {
8325 SidecarError::InvalidState(format!(
8326 "failed to sync host shadow file mode {} to guest {}: {}",
8327 host_path.display(),
8328 guest_path,
8329 kernel_error(error)
8330 ))
8331 })?;
8332 vm.kernel
8333 .utimes(&guest_path, atime_ms, mtime_ms)
8334 .map_err(|error| {
8335 SidecarError::InvalidState(format!(
8336 "failed to sync host shadow file times {} to guest {}: {}",
8337 host_path.display(),
8338 guest_path,
8339 kernel_error(error)
8340 ))
8341 })?;
8342 continue;
8343 }
8344
8345 if file_type.is_symlink() {
8346 let target = match fs::read_link(&host_path) {
8347 Ok(target) => target,
8348 Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
8349 Err(error) => {
8350 return Err(SidecarError::Io(format!(
8351 "failed to read host shadow symlink {}: {error}",
8352 host_path.display()
8353 )));
8354 }
8355 };
8356 replace_kernel_symlink(vm, &guest_path, &target.to_string_lossy())?;
8357 }
8358 }
8359
8360 Ok(())
8361}
8362
8363fn replace_kernel_symlink(
8364 vm: &mut VmState,
8365 guest_path: &str,
8366 target: &str,
8367) -> Result<(), SidecarError> {
8368 if vm.kernel.symlink(target, guest_path).is_ok() {
8369 return Ok(());
8370 }
8371
8372 if let Ok(existing_target) = vm.kernel.read_link(guest_path) {
8373 if existing_target == target {
8374 return Ok(());
8375 }
8376 }
8377
8378 let _ = vm.kernel.remove_file(guest_path);
8379 let _ = vm.kernel.remove_dir(guest_path);
8380 vm.kernel
8381 .symlink(target, guest_path)
8382 .map_err(kernel_error)?;
8383 Ok(())
8384}
8385
8386fn host_shadow_mode(metadata: &fs::Metadata) -> u32 {
8387 metadata.permissions().mode() & 0o7777
8388}
8389
8390fn read_host_shadow_file(host_path: &Path, mode: u32) -> std::io::Result<Vec<u8>> {
8396 match fs::read(host_path) {
8397 Ok(bytes) => Ok(bytes),
8398 Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
8399 fs::set_permissions(host_path, fs::Permissions::from_mode(mode | 0o400))?;
8400 let result = fs::read(host_path);
8401 fs::set_permissions(host_path, fs::Permissions::from_mode(mode))?;
8402 result
8403 }
8404 Err(error) => Err(error),
8405 }
8406}
8407
8408fn metadata_time_ms(seconds: i64, nanos: i64) -> u64 {
8409 let seconds = seconds.max(0) as u64;
8410 let nanos = nanos.max(0) as u64;
8411 seconds
8412 .saturating_mul(1_000)
8413 .saturating_add(nanos / 1_000_000)
8414}
8415
8416fn is_shadow_bootstrap_dir(path: &str) -> bool {
8417 matches!(
8418 path,
8419 "/dev"
8420 | "/proc"
8421 | "/tmp"
8422 | "/bin"
8423 | "/lib"
8424 | "/sbin"
8425 | "/boot"
8426 | "/etc"
8427 | "/root"
8428 | "/run"
8429 | "/srv"
8430 | "/sys"
8431 | "/opt"
8432 | "/mnt"
8433 | "/media"
8434 | "/home"
8435 | "/home/agentos"
8436 | "/usr"
8437 | "/usr/bin"
8438 | "/usr/games"
8439 | "/usr/include"
8440 | "/usr/lib"
8441 | "/usr/libexec"
8442 | "/usr/man"
8443 | "/usr/local"
8444 | "/usr/local/bin"
8445 | "/usr/sbin"
8446 | "/usr/share"
8447 | "/usr/share/man"
8448 | "/var"
8449 | "/var/cache"
8450 | "/var/empty"
8451 | "/var/lib"
8452 | "/var/lock"
8453 | "/var/log"
8454 | "/var/run"
8455 | "/var/spool"
8456 | "/var/tmp"
8457 | "/etc/agentos"
8458 | "/workspace"
8459 )
8460}
8461
8462#[cfg(test)]
8463mod shadow_sync_tests {
8464 use super::{is_protected_agentos_shadow_sync_path, is_shadow_bootstrap_dir};
8465
8466 #[test]
8467 fn shadow_bootstrap_sync_skips_virtual_home_tree() {
8468 assert!(is_shadow_bootstrap_dir("/home"));
8469 assert!(is_shadow_bootstrap_dir("/home/agentos"));
8470 }
8471
8472 #[test]
8473 fn protected_agentos_paths_are_not_shadow_synced() {
8474 assert!(is_protected_agentos_shadow_sync_path("/etc/agentos"));
8475 assert!(is_protected_agentos_shadow_sync_path(
8476 "/etc/agentos/instructions.md"
8477 ));
8478 assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos-copy"));
8479 assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos.md"));
8480 }
8481}
8482
8483fn is_kernel_owned_shadow_sync_path(path: &str) -> bool {
8484 matches!(path, "/dev" | "/proc" | "/sys")
8485 || path.starts_with("/dev/")
8486 || path.starts_with("/proc/")
8487 || path.starts_with("/sys/")
8488}
8489
8490pub(crate) fn is_protected_agentos_shadow_sync_path(path: &str) -> bool {
8491 path == "/etc/agentos" || path.starts_with("/etc/agentos/")
8492}
8493
8494fn should_skip_shadow_sync_path(vm: &VmState, guest_path: &str) -> bool {
8495 is_kernel_owned_shadow_sync_path(guest_path)
8496 || is_protected_agentos_shadow_sync_path(guest_path)
8497 || host_mount_path_for_guest_path_from_mounts(&vm.configuration.mounts, guest_path)
8498 .is_some()
8499}
8500
8501fn resolve_path_like_guest_specifier(cwd: &str, specifier: &str) -> String {
8502 if specifier.starts_with("file://") {
8503 normalize_path(specifier.trim_start_matches("file://"))
8504 } else if specifier.starts_with("file:") {
8505 normalize_path(specifier.trim_start_matches("file:"))
8506 } else if specifier.starts_with('/') {
8507 normalize_path(specifier)
8508 } else {
8509 normalize_path(&format!("{cwd}/{specifier}"))
8510 }
8511}
8512
8513fn guest_entrypoint_for_specifier(cwd: &str, specifier: &str) -> Option<String> {
8514 is_path_like_specifier(specifier).then(|| resolve_path_like_guest_specifier(cwd, specifier))
8515}
8516
8517fn is_node_runtime_command(command: &str) -> bool {
8518 matches!(command, "node" | "npm" | "npx")
8519 || Path::new(command)
8520 .file_name()
8521 .and_then(|name| name.to_str())
8522 .is_some_and(|name| matches!(name, "node" | "npm" | "npx"))
8523}
8524
8525fn resolve_special_node_cli_invocation(
8526 args: &[String],
8527 env: &mut BTreeMap<String, String>,
8528) -> Option<(String, Vec<String>)> {
8529 let first = args.first()?;
8530 match first.as_str() {
8531 "-e" | "--eval" => {
8532 env.insert(
8533 String::from("AGENTOS_NODE_EVAL"),
8534 args.get(1).cloned().unwrap_or_default(),
8535 );
8536 Some((first.clone(), args.iter().skip(2).cloned().collect()))
8537 }
8538 "-v" | "--version" => {
8539 env.insert(
8540 String::from("AGENTOS_NODE_EVAL"),
8541 String::from("console.log(process.version);"),
8542 );
8543 Some((String::from("-e"), args.to_vec()))
8544 }
8545 _ => None,
8546 }
8547}
8548
8549fn node_runtime_command_name(command: &str) -> Option<&str> {
8550 let name = Path::new(command)
8551 .file_name()
8552 .and_then(|name| name.to_str())?;
8553 matches!(name, "node" | "npm" | "npx").then_some(name)
8554}
8555
8556struct ResolvedHostNodeCliEntrypoint {
8557 command_name: String,
8558 guest_root: String,
8559 guest_entrypoint: String,
8560 package_root: PathBuf,
8561}
8562
8563fn resolve_host_node_cli_entrypoint(command: &str) -> Option<ResolvedHostNodeCliEntrypoint> {
8564 let command_name = node_runtime_command_name(command)?;
8565 if !matches!(command_name, "npm" | "npx") {
8566 return None;
8567 }
8568
8569 let path = std::env::var_os("PATH")?;
8570 for root in std::env::split_paths(&path) {
8571 let candidate = root.join(command_name);
8572 if !candidate.is_file() {
8573 continue;
8574 }
8575 let entrypoint = candidate.canonicalize().ok().unwrap_or(candidate);
8576 let package_root = entrypoint.parent()?.parent()?.to_path_buf();
8577 let guest_root = format!("/__secure_exec/node-runtime/{command_name}");
8578 let relative_entrypoint = entrypoint.strip_prefix(&package_root).ok()?;
8579 let guest_entrypoint = normalize_path(&format!(
8580 "{guest_root}/{}",
8581 relative_entrypoint.to_string_lossy().replace('\\', "/")
8582 ));
8583 return Some(ResolvedHostNodeCliEntrypoint {
8584 command_name: command_name.to_owned(),
8585 guest_root,
8586 guest_entrypoint,
8587 package_root,
8588 });
8589 }
8590
8591 None
8592}
8593
8594fn build_host_node_cli_eval(cli: &ResolvedHostNodeCliEntrypoint) -> String {
8595 let guest_npm_main = normalize_path(&format!("{}/lib/npm.js", cli.guest_root));
8596 let guest_npm_cli = normalize_path(&format!("{}/bin/npm-cli.js", cli.guest_root));
8597 let guest_package_json = normalize_path(&format!("{}/package.json", cli.guest_root));
8598 let guest_display_module = normalize_path(&format!("{}/lib/utils/display.js", cli.guest_root));
8599 let guest_log_file_module =
8600 normalize_path(&format!("{}/lib/utils/log-file.js", cli.guest_root));
8601 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); } } }";
8602 let display_stub = format!(
8603 "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 }};",
8604 display_module = serde_json::to_string(&guest_display_module)
8605 .unwrap_or_else(|_| format!("\"{guest_display_module}\"")),
8606 log_file_module = serde_json::to_string(&guest_log_file_module)
8607 .unwrap_or_else(|_| format!("\"{guest_log_file_module}\"")),
8608 );
8609 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)); }";
8610 match cli.command_name.as_str() {
8611 "npx" => format!(
8612 "{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); }});",
8613 debug_preamble = debug_preamble,
8614 display_stub = display_stub,
8615 registry_fetch_stub = registry_fetch_stub.replace(
8616 "__AGENTOS_NPM_MAIN__",
8617 &serde_json::to_string(&guest_npm_main)
8618 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8619 ),
8620 npm_main = serde_json::to_string(&guest_npm_main)
8621 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8622 npm_cli = serde_json::to_string(&guest_npm_cli)
8623 .unwrap_or_else(|_| format!("\"{guest_npm_cli}\"")),
8624 package_json = serde_json::to_string(&guest_package_json)
8625 .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
8626 ),
8627 _ => format!(
8628 "{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); }});",
8629 debug_preamble = debug_preamble,
8630 display_stub = display_stub,
8631 registry_fetch_stub = registry_fetch_stub.replace(
8632 "__AGENTOS_NPM_MAIN__",
8633 &serde_json::to_string(&guest_npm_main)
8634 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8635 ),
8636 npm_main = serde_json::to_string(&guest_npm_main)
8637 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8638 package_json = serde_json::to_string(&guest_package_json)
8639 .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
8640 ),
8641 }
8642}
8643
8644fn resolve_guest_command_entrypoint(
8645 vm: &VmState,
8646 guest_cwd: &str,
8647 command: &str,
8648 path_env: Option<&str>,
8649) -> Option<String> {
8650 if !is_path_like_specifier(command) {
8651 if let Some(entrypoint) = vm.command_guest_paths.get(command) {
8652 return Some(entrypoint.clone());
8653 }
8654
8655 for search_dir in guest_command_search_dirs(vm, guest_cwd, path_env) {
8656 let candidate = normalize_path(&format!("{search_dir}/{command}"));
8657 if let Some(entrypoint) = resolve_guest_command_path_candidate(vm, &candidate) {
8658 return Some(entrypoint);
8659 }
8660 }
8661
8662 return None;
8663 }
8664
8665 let normalized = resolve_path_like_guest_specifier(guest_cwd, command);
8666 resolve_guest_command_path_candidate(vm, &normalized).or_else(|| {
8667 let parent_dir = Path::new(&normalized).parent()?.to_str()?;
8671 if !guest_command_search_dirs(vm, guest_cwd, path_env)
8672 .iter()
8673 .any(|search_dir| normalize_path(search_dir) == normalize_path(parent_dir))
8674 {
8675 return None;
8676 }
8677
8678 let file_name = Path::new(&normalized).file_name()?.to_str()?;
8679 vm.command_guest_paths.get(file_name).cloned()
8680 })
8681}
8682
8683fn guest_command_search_dirs(vm: &VmState, guest_cwd: &str, path_env: Option<&str>) -> Vec<String> {
8684 let mut search_dirs = Vec::new();
8685 let mut seen = BTreeSet::new();
8686
8687 if let Some(path) = path_env.or_else(|| vm.guest_env.get("PATH").map(String::as_str)) {
8688 for segment in path.split(':') {
8689 let trimmed = segment.trim();
8690 if trimmed.is_empty() {
8691 continue;
8692 }
8693 let normalized = if trimmed.starts_with('/') {
8694 normalize_path(trimmed)
8695 } else {
8696 normalize_path(&format!("{guest_cwd}/{trimmed}"))
8697 };
8698 if seen.insert(normalized.clone()) {
8699 search_dirs.push(normalized);
8700 }
8701 }
8702 }
8703
8704 for fallback in ["/bin", "/usr/bin", "/usr/local/bin"] {
8705 let normalized = String::from(fallback);
8706 if seen.insert(normalized.clone()) {
8707 search_dirs.push(normalized);
8708 }
8709 }
8710
8711 search_dirs
8712}
8713
8714fn resolve_guest_command_path_candidate(vm: &VmState, candidate: &str) -> Option<String> {
8715 if candidate.starts_with("/bin/")
8716 || candidate.starts_with("/usr/bin/")
8717 || candidate.starts_with("/usr/local/bin/")
8718 || candidate.starts_with("/__secure_exec/commands/")
8719 {
8720 if let Some(file_name) = Path::new(candidate)
8721 .file_name()
8722 .and_then(|name| name.to_str())
8723 {
8724 if let Some(guest_entrypoint) = vm.command_guest_paths.get(file_name) {
8725 return Some(guest_entrypoint.clone());
8726 }
8727 }
8728 }
8729
8730 if vm
8731 .kernel
8732 .exists(candidate)
8733 .ok()
8734 .is_some_and(|exists| exists)
8735 {
8736 return Some(normalize_path(candidate));
8737 }
8738
8739 resolve_vm_guest_path_to_host(vm, candidate)
8740 .is_file()
8741 .then(|| normalize_path(candidate))
8742}
8743
8744fn resolve_host_entrypoint_within_vm_host_cwd(
8745 vm: &VmState,
8746 specifier: &str,
8747) -> Option<(String, String)> {
8748 let candidate = Path::new(specifier);
8749 if !candidate.is_absolute() {
8750 return None;
8751 }
8752
8753 let normalized_entrypoint = normalize_host_path(candidate);
8754 let normalized_host_cwd = normalize_host_path(&vm.host_cwd);
8755 if !path_is_within_root(&normalized_entrypoint, &normalized_host_cwd) {
8756 return None;
8757 }
8758
8759 let relative = normalized_entrypoint
8760 .strip_prefix(&normalized_host_cwd)
8761 .ok()?
8762 .to_string_lossy()
8763 .replace('\\', "/");
8764 let guest_entrypoint = if relative.is_empty() {
8765 String::from("/")
8766 } else {
8767 normalize_path(&format!("/{relative}"))
8768 };
8769 Some((
8770 guest_entrypoint,
8771 normalized_entrypoint.to_string_lossy().into_owned(),
8772 ))
8773}
8774
8775fn prepare_guest_runtime_env(
8776 vm: &VmState,
8777 env: &mut BTreeMap<String, String>,
8778 guest_cwd: &str,
8779 host_cwd: &Path,
8780 guest_entrypoint: Option<String>,
8781) -> Result<(), SidecarError> {
8782 let user = vm.kernel.user_profile();
8783 let path_mappings = runtime_guest_path_mappings(vm);
8784 let read_paths = expand_host_access_paths(
8785 std::iter::once(vm.cwd.clone())
8786 .chain(
8787 path_mappings
8788 .iter()
8789 .map(|mapping| PathBuf::from(&mapping.host_path)),
8790 )
8791 .chain(std::iter::once(host_cwd.to_path_buf()))
8792 .collect::<Vec<_>>()
8793 .as_slice(),
8794 );
8795 let write_paths = dedupe_host_paths(
8796 std::iter::once(vm.cwd.clone())
8797 .chain(std::iter::once(host_cwd.to_path_buf()))
8798 .chain(runtime_guest_writable_host_paths(vm))
8799 .collect::<Vec<_>>()
8800 .as_slice(),
8801 );
8802 let allowed_node_builtins = configured_allowed_node_builtins(vm);
8803 let loopback_exempt_ports = configured_loopback_exempt_ports(vm);
8804
8805 env.insert(
8806 String::from("AGENTOS_GUEST_PATH_MAPPINGS"),
8807 serde_json::to_string(&path_mappings).map_err(|error| {
8808 SidecarError::InvalidState(format!("failed to encode guest path mappings: {error}"))
8809 })?,
8810 );
8811 env.entry(String::from(EXECUTION_SANDBOX_ROOT_ENV))
8812 .or_insert_with(|| normalize_host_path(&vm.cwd).to_string_lossy().into_owned());
8813 env.insert(
8814 String::from("AGENTOS_EXTRA_FS_READ_PATHS"),
8815 serde_json::to_string(
8816 &read_paths
8817 .iter()
8818 .map(|path| path.to_string_lossy().into_owned())
8819 .collect::<Vec<_>>(),
8820 )
8821 .map_err(|error| {
8822 SidecarError::InvalidState(format!("failed to encode read paths: {error}"))
8823 })?,
8824 );
8825 env.insert(
8826 String::from("AGENTOS_EXTRA_FS_WRITE_PATHS"),
8827 serde_json::to_string(
8828 &write_paths
8829 .iter()
8830 .map(|path| path.to_string_lossy().into_owned())
8831 .collect::<Vec<_>>(),
8832 )
8833 .map_err(|error| {
8834 SidecarError::InvalidState(format!("failed to encode write paths: {error}"))
8835 })?,
8836 );
8837 env.insert(
8838 String::from("AGENTOS_ALLOWED_NODE_BUILTINS"),
8839 serde_json::to_string(&allowed_node_builtins).map_err(|error| {
8840 SidecarError::InvalidState(format!("failed to encode allowed builtins: {error}"))
8841 })?,
8842 );
8843 env.insert(
8846 String::from("AGENTOS_JS_PLATFORM"),
8847 js_runtime_platform_env(vm).to_owned(),
8848 );
8849 if let Some(resolution) = js_runtime_module_resolution_env(vm) {
8851 env.insert(
8852 String::from("AGENTOS_JS_MODULE_RESOLUTION"),
8853 resolution.to_owned(),
8854 );
8855 }
8856 if let Some(allowlist) = js_runtime_enforced_builtins(vm) {
8860 env.insert(
8861 String::from("AGENTOS_JS_BUILTIN_ALLOWLIST"),
8862 serde_json::to_string(&allowlist).map_err(|error| {
8863 SidecarError::InvalidState(format!(
8864 "failed to encode jsRuntime builtin allow-list: {error}"
8865 ))
8866 })?,
8867 );
8868 }
8869 env.entry(String::from("HOME"))
8876 .or_insert_with(|| user.homedir.clone());
8877 env.entry(String::from("USER"))
8878 .or_insert_with(|| user.username.clone());
8879 env.entry(String::from("LOGNAME"))
8880 .or_insert_with(|| user.username.clone());
8881 env.entry(String::from("SHELL"))
8882 .or_insert_with(|| user.shell.clone());
8883 env.entry(String::from("PATH")).or_insert_with(|| {
8884 vm.guest_env
8885 .get("PATH")
8886 .cloned()
8887 .unwrap_or_else(|| crate::vm::DEFAULT_GUEST_PATH_ENV.to_owned())
8888 });
8889 env.entry(String::from("TMPDIR"))
8890 .or_insert_with(|| String::from("/tmp"));
8891 env.insert(String::from("PWD"), guest_cwd.to_owned());
8892 if !loopback_exempt_ports.is_empty() {
8893 env.insert(
8894 String::from(LOOPBACK_EXEMPT_PORTS_ENV),
8895 serde_json::to_string(&loopback_exempt_ports).map_err(|error| {
8896 SidecarError::InvalidState(format!("failed to encode loopback exemptions: {error}"))
8897 })?,
8898 );
8899 }
8900 if let Some(guest_entrypoint) = guest_entrypoint {
8901 env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
8902 }
8903 Ok(())
8904}
8905
8906fn virtual_os_cpu_count(resource_limits: &ResourceLimits) -> usize {
8907 resource_limits.virtual_cpu_count.unwrap_or(1).max(1)
8908}
8909
8910fn virtual_os_totalmem_bytes(resource_limits: &ResourceLimits) -> u64 {
8911 resource_limits
8912 .max_wasm_memory_bytes
8913 .unwrap_or(1024 * 1024 * 1024)
8914}
8915
8916fn virtual_os_freemem_bytes(resource_limits: &ResourceLimits) -> u64 {
8917 resource_limits
8918 .max_wasm_memory_bytes
8919 .unwrap_or(512 * 1024 * 1024)
8920}
8921
8922fn javascript_execution_limits(vm: &VmState) -> JavascriptExecutionLimits {
8927 JavascriptExecutionLimits {
8928 v8_heap_limit_mb: vm.limits.js_runtime.v8_heap_limit_mb,
8929 sync_rpc_wait_timeout_ms: vm.limits.js_runtime.sync_rpc_wait_timeout_ms,
8930 }
8931}
8932
8933fn guest_runtime_identity(
8939 vm: &VmState,
8940 virtual_pid: Option<u64>,
8941 virtual_ppid: Option<u64>,
8942) -> GuestRuntimeConfig {
8943 let user = vm.kernel.user_profile();
8944 let resource_limits = vm.kernel.resource_limits();
8945 GuestRuntimeConfig {
8946 virtual_uid: Some(u64::from(user.uid)),
8947 virtual_gid: Some(u64::from(user.gid)),
8948 virtual_pid,
8949 virtual_ppid,
8950 virtual_exec_path: None,
8951 os_cpu_count: Some(virtual_os_cpu_count(resource_limits) as u64),
8952 os_totalmem: Some(virtual_os_totalmem_bytes(resource_limits)),
8953 os_freemem: Some(virtual_os_freemem_bytes(resource_limits)),
8954 os_homedir: Some(user.homedir.clone()),
8955 os_hostname: None,
8956 os_shell: Some(user.shell.clone()),
8957 os_user: Some(user.username.clone()),
8958 snapshot_userland_code: vm
8963 .configuration
8964 .js_runtime
8965 .as_ref()
8966 .and_then(|cfg| cfg.snapshot_userland_code.clone()),
8967 }
8968}
8969
8970fn guest_virtual_home(vm: &VmState) -> String {
8975 let homedir = vm.kernel.user_profile().homedir;
8976 if homedir.starts_with('/') {
8977 homedir
8978 } else {
8979 String::from("/root")
8980 }
8981}
8982
8983fn python_execution_limits(vm: &VmState) -> PythonExecutionLimits {
8985 PythonExecutionLimits {
8986 output_buffer_max_bytes: Some(vm.limits.python.output_buffer_max_bytes),
8987 execution_timeout_ms: Some(vm.limits.python.execution_timeout_ms),
8988 max_old_space_mb: Some(vm.limits.python.max_old_space_mb),
8989 vfs_rpc_timeout_ms: Some(vm.limits.python.vfs_rpc_timeout_ms),
8990 }
8991}
8992
8993fn wasm_execution_limits(vm: &VmState) -> WasmExecutionLimits {
8998 let resource_limits = vm.kernel.resource_limits();
8999 WasmExecutionLimits {
9000 max_fuel: resource_limits.max_wasm_fuel,
9001 max_memory_bytes: resource_limits.max_wasm_memory_bytes,
9002 max_stack_bytes: resource_limits
9003 .max_wasm_stack_bytes
9004 .map(|value| value as u64),
9005 }
9006}
9007
9008fn js_runtime_platform(vm: &VmState) -> vm_config::JsRuntimePlatform {
9011 vm.configuration
9012 .js_runtime
9013 .as_ref()
9014 .map(|cfg| cfg.platform)
9015 .unwrap_or(vm_config::JsRuntimePlatform::Node)
9016}
9017
9018fn js_runtime_platform_env(vm: &VmState) -> &'static str {
9021 match js_runtime_platform(vm) {
9022 vm_config::JsRuntimePlatform::Node => "node",
9023 vm_config::JsRuntimePlatform::Browser => "browser",
9024 vm_config::JsRuntimePlatform::Neutral => "neutral",
9025 vm_config::JsRuntimePlatform::Bare => "bare",
9026 }
9027}
9028
9029fn js_runtime_module_resolution_env(vm: &VmState) -> Option<&'static str> {
9032 let resolution = vm
9033 .configuration
9034 .js_runtime
9035 .as_ref()
9036 .map(|cfg| cfg.module_resolution)
9037 .unwrap_or(vm_config::JsModuleResolution::Node);
9038 match resolution {
9039 vm_config::JsModuleResolution::Node => None,
9040 vm_config::JsModuleResolution::Relative => Some("relative"),
9041 vm_config::JsModuleResolution::None => Some("none"),
9042 }
9043}
9044
9045fn js_runtime_enforced_builtins(vm: &VmState) -> Option<Vec<String>> {
9049 if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
9050 return Some(Vec::new());
9051 }
9052 vm.configuration
9053 .js_runtime
9054 .as_ref()
9055 .and_then(|cfg| cfg.allowed_builtins.clone())
9056}
9057
9058fn configured_allowed_node_builtins(vm: &VmState) -> Vec<String> {
9059 if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
9061 return Vec::new();
9062 }
9063 let configured = match vm
9066 .configuration
9067 .js_runtime
9068 .as_ref()
9069 .and_then(|cfg| cfg.allowed_builtins.as_ref())
9070 {
9071 Some(list) => list.clone(),
9072 None => DEFAULT_ALLOWED_NODE_BUILTINS
9073 .iter()
9074 .map(|value| (*value).to_owned())
9075 .collect::<Vec<_>>(),
9076 };
9077 dedupe_strings(&configured)
9078}
9079
9080fn configured_loopback_exempt_ports(vm: &VmState) -> Vec<String> {
9081 if !vm.configuration.loopback_exempt_ports.is_empty() {
9082 return vm
9083 .configuration
9084 .loopback_exempt_ports
9085 .iter()
9086 .map(ToString::to_string)
9087 .collect();
9088 }
9089
9090 vm.create_loopback_exempt_ports
9091 .iter()
9092 .map(ToString::to_string)
9093 .collect()
9094}
9095
9096fn mount_config_host_path(config: &str) -> Option<String> {
9098 serde_json::from_str::<Value>(config)
9099 .ok()?
9100 .get("hostPath")
9101 .and_then(Value::as_str)
9102 .map(str::to_owned)
9103}
9104
9105fn runtime_guest_writable_host_paths(vm: &VmState) -> Vec<PathBuf> {
9106 vm.configuration
9107 .mounts
9108 .iter()
9109 .filter(|mount| !mount.read_only)
9110 .filter_map(|mount| {
9111 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
9112 .then(|| mount_config_host_path(&mount.plugin.config))
9113 .flatten()
9114 .map(PathBuf::from)
9115 })
9116 .collect()
9117}
9118
9119fn runtime_guest_path_mappings(vm: &VmState) -> Vec<RuntimeGuestPathMapping> {
9120 let mut mappings = vm
9121 .configuration
9122 .mounts
9123 .iter()
9124 .filter_map(|mount| {
9125 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
9126 .then(|| {
9127 mount_config_host_path(&mount.plugin.config).map(|host_path| {
9128 RuntimeGuestPathMapping {
9129 guest_path: normalize_path(&mount.guest_path),
9130 host_path,
9131 read_only: mount.read_only,
9132 }
9133 })
9134 })
9135 .flatten()
9136 })
9137 .collect::<Vec<_>>();
9138 let mut command_root_mappings = vm
9139 .command_guest_paths
9140 .values()
9141 .filter_map(|guest_path| {
9142 Path::new(guest_path)
9143 .parent()
9144 .and_then(|parent| parent.to_str())
9145 .map(normalize_path)
9146 })
9147 .collect::<BTreeSet<_>>()
9148 .into_iter()
9149 .map(|guest_path| RuntimeGuestPathMapping {
9150 host_path: resolve_vm_guest_path_to_host(vm, &guest_path)
9151 .to_string_lossy()
9152 .into_owned(),
9153 guest_path,
9154 read_only: false,
9155 })
9156 .collect::<Vec<_>>();
9157 mappings.append(&mut command_root_mappings);
9158 let mut extra_node_modules_roots = mappings
9159 .iter()
9160 .filter(|mapping| mapping.guest_path.starts_with("/root/node_modules/"))
9161 .filter_map(|mapping| {
9162 host_node_modules_root(Path::new(&mapping.host_path)).map(|host_root| {
9163 RuntimeGuestPathMapping {
9164 guest_path: String::from("/root/node_modules"),
9165 host_path: host_root.to_string_lossy().into_owned(),
9166 read_only: mapping.read_only,
9167 }
9168 })
9169 })
9170 .collect::<Vec<_>>();
9171 mappings.append(&mut extra_node_modules_roots);
9172 mappings.push(RuntimeGuestPathMapping {
9173 guest_path: String::from("/"),
9174 host_path: vm.cwd.to_string_lossy().into_owned(),
9175 read_only: false,
9176 });
9177 mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.guest_path.len()));
9178 mappings.dedup_by(|left, right| {
9179 left.guest_path == right.guest_path && left.host_path == right.host_path
9180 });
9181 mappings
9182}
9183
9184fn build_module_reader(
9195 vm: &VmState,
9196 resolved: &ResolvedChildProcessExecution,
9197) -> Option<crate::plugins::host_dir::HostDirModuleReader> {
9198 let mut pairs: Vec<(String, PathBuf)> = vm
9199 .configuration
9200 .mounts
9201 .iter()
9202 .filter(|mount| mount.read_only)
9203 .filter(|mount| (mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
9204 .filter_map(|mount| {
9205 mount_config_host_path(&mount.plugin.config)
9206 .map(|host_path| (normalize_path(&mount.guest_path), PathBuf::from(host_path)))
9207 })
9208 .collect();
9209
9210 let guest_entrypoint = resolved
9211 .env
9212 .get("AGENTOS_GUEST_ENTRYPOINT")
9213 .map(|path| normalize_path(path));
9214 if let Some(guest_entrypoint) = guest_entrypoint.as_deref() {
9215 let entrypoint_in_read_only_mount = pairs.iter().any(|(guest_path, _)| {
9216 guest_entrypoint == guest_path
9217 || guest_entrypoint.starts_with(&format!("{guest_path}/"))
9218 });
9219 if !entrypoint_in_read_only_mount {
9220 return None;
9221 }
9222 }
9223
9224 let extra_roots: Vec<(String, PathBuf)> = pairs
9228 .iter()
9229 .filter(|(guest_path, _)| guest_path.starts_with("/root/node_modules/"))
9230 .filter_map(|(_, host_path)| {
9231 host_node_modules_root(host_path).map(|root| (String::from("/root/node_modules"), root))
9232 })
9233 .collect();
9234 pairs.extend(extra_roots);
9235
9236 crate::plugins::host_dir::HostDirModuleReader::from_mounts(pairs)
9237}
9238
9239fn host_node_modules_root(path: &Path) -> Option<PathBuf> {
9240 if let Some(root) = path
9241 .ancestors()
9242 .filter(|candidate| {
9243 candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
9244 })
9245 .last()
9246 .map(Path::to_path_buf)
9247 {
9248 return Some(root);
9249 }
9250
9251 fs::canonicalize(path)
9252 .ok()?
9253 .ancestors()
9254 .filter(|candidate| {
9255 candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
9256 })
9257 .last()
9258 .map(Path::to_path_buf)
9259}
9260
9261#[cfg(test)]
9262mod runtime_guest_path_mapping_tests {
9263 use super::{host_node_modules_root, javascript_sync_rpc_option_bool};
9264 use serde_json::json;
9265 use std::fs;
9266 use std::time::{SystemTime, UNIX_EPOCH};
9267
9268 #[test]
9269 fn host_node_modules_root_prefers_workspace_root_over_pnpm_package_node_modules() {
9270 let unique = SystemTime::now()
9271 .duration_since(UNIX_EPOCH)
9272 .expect("clock should be monotonic")
9273 .as_nanos();
9274 let temp = std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-{unique}"));
9275 let workspace_node_modules = temp.join("node_modules");
9276 let package_root = workspace_node_modules
9277 .join(".pnpm")
9278 .join("example@1.0.0")
9279 .join("node_modules")
9280 .join("@scope")
9281 .join("pkg");
9282 fs::create_dir_all(&package_root).expect("package root should be created");
9283
9284 let resolved =
9285 host_node_modules_root(&package_root).expect("node_modules root should resolve");
9286
9287 assert_eq!(resolved, workspace_node_modules);
9288
9289 fs::remove_dir_all(&temp).expect("temp tree should be removed");
9290 }
9291
9292 #[test]
9293 fn host_node_modules_root_preserves_symlinked_workspace_node_modules_path() {
9294 let unique = SystemTime::now()
9295 .duration_since(UNIX_EPOCH)
9296 .expect("clock should be monotonic")
9297 .as_nanos();
9298 let temp =
9299 std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-symlink-{unique}"));
9300 let workspace_node_modules = temp.join("node_modules");
9301 let package_link = workspace_node_modules.join("@scope").join("pkg");
9302 let real_package = temp.join("registry").join("agent").join("pkg");
9303 fs::create_dir_all(package_link.parent().expect("package parent should exist"))
9304 .expect("scoped parent should be created");
9305 fs::create_dir_all(&real_package).expect("real package root should be created");
9306 std::os::unix::fs::symlink(&real_package, &package_link)
9307 .expect("package symlink should be created");
9308
9309 let resolved =
9310 host_node_modules_root(&package_link).expect("node_modules root should resolve");
9311
9312 assert_eq!(resolved, workspace_node_modules);
9313
9314 fs::remove_dir_all(&temp).expect("temp tree should be removed");
9315 }
9316
9317 #[test]
9318 fn javascript_sync_rpc_option_bool_accepts_boolean_recursive_argument() {
9319 assert_eq!(
9320 javascript_sync_rpc_option_bool(&[json!("/workspace"), json!(true)], 1, "recursive"),
9321 Some(true)
9322 );
9323 assert_eq!(
9324 javascript_sync_rpc_option_bool(
9325 &[json!("/workspace"), json!({ "recursive": false })],
9326 1,
9327 "recursive"
9328 ),
9329 Some(false)
9330 );
9331 }
9332}
9333
9334#[cfg(test)]
9335mod kernel_poll_sync_rpc_tests {
9336 use super::{
9337 service_javascript_kernel_poll_sync_rpc, ActiveExecution, ActiveProcess,
9338 JavascriptSyncRpcRequest, KernelPollFdResponse, SidecarKernel, ToolExecution,
9339 EXECUTION_DRIVER_NAME, JAVASCRIPT_COMMAND,
9340 };
9341 use secure_exec_kernel::command_registry::CommandDriver;
9342 use secure_exec_kernel::kernel::{KernelVmConfig, SpawnOptions};
9343 use secure_exec_kernel::mount_table::MountTable;
9344 use secure_exec_kernel::permissions::Permissions;
9345 use secure_exec_kernel::poll::{POLLHUP, POLLIN};
9346 use secure_exec_kernel::vfs::MemoryFileSystem;
9347 use serde_json::{json, Value};
9348 #[test]
9349 fn javascript_kernel_poll_sync_rpc_reports_multiple_kernel_fds() {
9350 let mut config = KernelVmConfig::new("vm-js-kernel-poll");
9351 config.permissions = Permissions::allow_all();
9352 let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config);
9353 kernel
9354 .register_driver(CommandDriver::new(
9355 EXECUTION_DRIVER_NAME,
9356 [JAVASCRIPT_COMMAND],
9357 ))
9358 .expect("register execution driver");
9359
9360 let kernel_handle = kernel
9361 .spawn_process(
9362 JAVASCRIPT_COMMAND,
9363 Vec::new(),
9364 SpawnOptions {
9365 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
9366 ..SpawnOptions::default()
9367 },
9368 )
9369 .expect("spawn javascript kernel process");
9370 let pid = kernel_handle.pid();
9371
9372 let (stdin_read_fd, stdin_write_fd) = kernel
9373 .open_pipe(EXECUTION_DRIVER_NAME, pid)
9374 .expect("open kernel stdin pipe");
9375 kernel
9376 .fd_dup2(EXECUTION_DRIVER_NAME, pid, stdin_read_fd, 0)
9377 .expect("dup stdin pipe onto fd 0");
9378 kernel
9379 .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_read_fd)
9380 .expect("close original stdin read fd");
9381
9382 let process = ActiveProcess::new(
9383 pid,
9384 kernel_handle,
9385 super::GuestRuntimeKind::JavaScript,
9386 ActiveExecution::Tool(ToolExecution::default()),
9387 );
9388
9389 kernel
9390 .fd_write(EXECUTION_DRIVER_NAME, pid, stdin_write_fd, b"poll-ready")
9391 .expect("write kernel stdin payload");
9392 kernel
9393 .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_write_fd)
9394 .expect("close kernel stdin writer");
9395
9396 let response = service_javascript_kernel_poll_sync_rpc(
9397 &mut kernel,
9398 &process,
9399 &JavascriptSyncRpcRequest {
9400 id: 1,
9401 method: String::from("__kernel_poll"),
9402 args: vec![
9403 json!([
9404 { "fd": 0, "events": POLLIN.bits() },
9405 { "fd": 1, "events": POLLIN.bits() }
9406 ]),
9407 json!(250),
9408 ],
9409 },
9410 )
9411 .expect("poll kernel fds");
9412
9413 assert_eq!(response["readyCount"], Value::from(1));
9414 let fds: Vec<KernelPollFdResponse> =
9415 serde_json::from_value(response["fds"].clone()).expect("kernel poll fd response");
9416 assert_eq!(
9417 fds,
9418 vec![
9419 KernelPollFdResponse {
9420 fd: 0,
9421 events: POLLIN.bits(),
9422 revents: (POLLIN | POLLHUP).bits(),
9423 },
9424 KernelPollFdResponse {
9425 fd: 1,
9426 events: POLLIN.bits(),
9427 revents: 0,
9428 },
9429 ]
9430 );
9431
9432 process.kernel_handle.finish(0);
9433 kernel.waitpid(pid).expect("wait javascript kernel process");
9434 }
9435}
9436
9437fn dedupe_strings(values: &[String]) -> Vec<String> {
9438 let mut seen = BTreeSet::new();
9439 let mut deduped = Vec::new();
9440 for value in values {
9441 if seen.insert(value.clone()) {
9442 deduped.push(value.clone());
9443 }
9444 }
9445 deduped
9446}
9447
9448fn dedupe_host_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
9449 let mut seen = BTreeSet::new();
9450 let mut deduped = Vec::new();
9451 for path in paths {
9452 let normalized = normalize_host_path(path);
9453 let key = normalized.to_string_lossy().into_owned();
9454 if seen.insert(key) {
9455 deduped.push(normalized);
9456 }
9457 }
9458 deduped
9459}
9460
9461fn expand_host_access_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
9462 let mut expanded = Vec::new();
9463 let mut seen = BTreeSet::new();
9464
9465 let mut add_path = |candidate: PathBuf| {
9466 let normalized = normalize_host_path(&candidate);
9467 let key = normalized.to_string_lossy().into_owned();
9468 if seen.insert(key) {
9469 expanded.push(normalized);
9470 }
9471 };
9472
9473 for host_path in paths {
9474 add_path(host_path.clone());
9475 if let Ok(realpath) = fs::canonicalize(host_path) {
9476 add_path(realpath);
9477 }
9478
9479 if host_path.file_name().and_then(|name| name.to_str()) != Some("node_modules") {
9480 continue;
9481 }
9482
9483 let mut current = host_path.parent();
9484 while let Some(parent) = current {
9485 let candidate = parent.join("node_modules");
9486 if candidate.exists() {
9487 add_path(candidate.clone());
9488 if let Ok(realpath) = fs::canonicalize(&candidate) {
9489 add_path(realpath);
9490 }
9491 }
9492 current = parent.parent();
9493 }
9494 }
9495
9496 expanded
9497}
9498
9499fn prepare_javascript_shadow(
9500 vm: &mut VmState,
9501 resolved: &ResolvedChildProcessExecution,
9502) -> Result<(), SidecarError> {
9503 let guest_entrypoint = resolved
9504 .env
9505 .get("AGENTOS_GUEST_ENTRYPOINT")
9506 .cloned()
9507 .or_else(|| {
9515 resolve_host_entrypoint_within_vm_host_cwd(vm, &resolved.entrypoint)
9516 .map(|(guest_entrypoint, _)| guest_entrypoint)
9517 })
9518 .or_else(|| {
9519 resolved
9520 .entrypoint
9521 .starts_with('/')
9522 .then(|| normalize_path(&resolved.entrypoint))
9523 });
9524 let Some(guest_entrypoint) = guest_entrypoint else {
9525 return Ok(());
9526 };
9527 if host_mount_path_for_guest_path(vm, &guest_entrypoint).is_some() {
9528 return Ok(());
9529 }
9530 if vm.kernel.lstat(&guest_entrypoint).is_err() {
9531 let host_entrypoint = {
9532 let candidate = Path::new(&resolved.entrypoint);
9533 if candidate.is_absolute() {
9534 candidate.to_path_buf()
9535 } else {
9536 resolved.host_cwd.join(candidate)
9537 }
9538 };
9539 if host_entrypoint.exists() {
9540 materialize_host_path_to_shadow(vm, &guest_entrypoint, &host_entrypoint)?;
9541 return sync_shadow_entrypoint_into_kernel(vm, &guest_entrypoint);
9546 }
9547 }
9548 materialize_guest_path_to_shadow(vm, &guest_entrypoint)
9549}
9550
9551fn sync_shadow_entrypoint_into_kernel(
9556 vm: &mut VmState,
9557 guest_entrypoint: &str,
9558) -> Result<(), SidecarError> {
9559 if vm.kernel.exists(guest_entrypoint).unwrap_or(false) {
9560 return Ok(());
9561 }
9562 let shadow_path = shadow_path_for_guest(vm, guest_entrypoint);
9563 let bytes = match fs::read(&shadow_path) {
9564 Ok(bytes) => bytes,
9565 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
9566 Err(error) => {
9567 return Err(SidecarError::Io(format!(
9568 "failed to read staged shadow entrypoint {}: {error}",
9569 shadow_path.display()
9570 )));
9571 }
9572 };
9573 if let Some(parent) = guest_parent_path(guest_entrypoint) {
9574 if !vm.kernel.exists(&parent).unwrap_or(false) {
9575 vm.kernel.mkdir(&parent, true).map_err(kernel_error)?;
9576 }
9577 }
9578 vm.kernel
9579 .write_file(guest_entrypoint, bytes)
9580 .map_err(kernel_error)?;
9581 Ok(())
9582}
9583
9584fn guest_parent_path(guest_path: &str) -> Option<String> {
9585 let parent = Path::new(guest_path).parent()?;
9586 let parent = parent.to_string_lossy();
9587 if parent.is_empty() || parent == "/" {
9588 None
9589 } else {
9590 Some(parent.into_owned())
9591 }
9592}
9593
9594fn materialize_host_path_to_shadow(
9595 vm: &VmState,
9596 guest_path: &str,
9597 host_path: &Path,
9598) -> Result<(), SidecarError> {
9599 let shadow_path = shadow_path_for_guest(vm, guest_path);
9600 let metadata = fs::symlink_metadata(host_path)
9601 .map_err(|error| SidecarError::Io(format!("failed to stat host entrypoint: {error}")))?;
9602
9603 if metadata.file_type().is_symlink() {
9604 if let Some(parent) = shadow_path.parent() {
9605 fs::create_dir_all(parent).map_err(|error| {
9606 SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
9607 })?;
9608 }
9609 let _ = fs::remove_file(&shadow_path);
9610 let _ = fs::remove_dir_all(&shadow_path);
9611 let target = fs::read_link(host_path)
9612 .map_err(|error| SidecarError::Io(format!("failed to read host symlink: {error}")))?;
9613 std::os::unix::fs::symlink(&target, &shadow_path)
9614 .map_err(|error| SidecarError::Io(format!("failed to mirror host symlink: {error}")))?;
9615 return Ok(());
9616 }
9617
9618 if metadata.is_dir() {
9619 fs::create_dir_all(&shadow_path).map_err(|error| {
9620 SidecarError::Io(format!("failed to create shadow directory: {error}"))
9621 })?;
9622 fs::set_permissions(
9623 &shadow_path,
9624 fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
9625 )
9626 .map_err(|error| {
9627 SidecarError::Io(format!(
9628 "failed to set shadow directory mode on {}: {error}",
9629 shadow_path.display()
9630 ))
9631 })?;
9632 return Ok(());
9633 }
9634
9635 if let Some(parent) = shadow_path.parent() {
9636 fs::create_dir_all(parent).map_err(|error| {
9637 SidecarError::Io(format!("failed to create shadow parent: {error}"))
9638 })?;
9639 }
9640 let bytes = fs::read(host_path)
9641 .map_err(|error| SidecarError::Io(format!("failed to read host entrypoint: {error}")))?;
9642 fs::write(&shadow_path, bytes).map_err(|error| {
9643 SidecarError::Io(format!(
9644 "failed to mirror host file into shadow root: {error}"
9645 ))
9646 })?;
9647 fs::set_permissions(
9648 &shadow_path,
9649 fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
9650 )
9651 .map_err(|error| {
9652 SidecarError::Io(format!(
9653 "failed to set shadow file mode on {}: {error}",
9654 shadow_path.display()
9655 ))
9656 })?;
9657 Ok(())
9658}
9659
9660fn materialize_guest_path_to_shadow(
9661 vm: &mut VmState,
9662 guest_path: &str,
9663) -> Result<(), SidecarError> {
9664 let stat = vm.kernel.lstat(guest_path).map_err(kernel_error)?;
9665 let shadow_path = shadow_path_for_guest(vm, guest_path);
9666
9667 if stat.is_symbolic_link {
9668 if let Some(parent) = shadow_path.parent() {
9669 fs::create_dir_all(parent).map_err(|error| {
9670 SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
9671 })?;
9672 }
9673 let _ = fs::remove_file(&shadow_path);
9674 let _ = fs::remove_dir_all(&shadow_path);
9675 let target = vm.kernel.read_link(guest_path).map_err(kernel_error)?;
9676 std::os::unix::fs::symlink(&target, &shadow_path)
9677 .map_err(|error| SidecarError::Io(format!("failed to mirror symlink: {error}")))?;
9678 return Ok(());
9679 }
9680
9681 if stat.is_directory {
9682 fs::create_dir_all(&shadow_path).map_err(|error| {
9683 SidecarError::Io(format!("failed to create shadow directory: {error}"))
9684 })?;
9685 fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
9686 |error| {
9687 SidecarError::Io(format!(
9688 "failed to set shadow directory mode on {}: {error}",
9689 shadow_path.display()
9690 ))
9691 },
9692 )?;
9693 return Ok(());
9694 }
9695
9696 if let Some(parent) = shadow_path.parent() {
9697 fs::create_dir_all(parent).map_err(|error| {
9698 SidecarError::Io(format!("failed to create shadow parent: {error}"))
9699 })?;
9700 }
9701 let bytes = vm.kernel.read_file(guest_path).map_err(kernel_error)?;
9702 fs::write(&shadow_path, bytes).map_err(|error| {
9703 SidecarError::Io(format!(
9704 "failed to mirror guest file into shadow root: {error}"
9705 ))
9706 })?;
9707 fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
9708 |error| {
9709 SidecarError::Io(format!(
9710 "failed to set shadow file mode on {}: {error}",
9711 shadow_path.display()
9712 ))
9713 },
9714 )?;
9715 Ok(())
9716}
9717
9718fn load_javascript_entrypoint_source(
9719 vm: &mut VmState,
9720 host_cwd: &Path,
9721 entrypoint: &str,
9722 env: &BTreeMap<String, String>,
9723) -> Option<String> {
9724 let mut read_guest_file = |path: &str| {
9725 vm.kernel
9726 .read_file(path)
9727 .ok()
9728 .and_then(|bytes| String::from_utf8(bytes).ok())
9729 };
9730
9731 if let Some(source) = env
9732 .get("AGENTOS_GUEST_ENTRYPOINT")
9733 .filter(|path| path.starts_with('/'))
9734 .and_then(|path| read_guest_file(path))
9735 {
9736 return Some(source);
9737 }
9738
9739 if entrypoint.starts_with('/') {
9740 if let Some(source) = read_guest_file(entrypoint) {
9741 return Some(source);
9742 }
9743 }
9744
9745 let host_entrypoint = if Path::new(entrypoint).is_absolute() {
9746 PathBuf::from(entrypoint)
9747 } else {
9748 host_cwd.join(entrypoint)
9749 };
9750 let normalized_entrypoint = normalize_host_path(&host_entrypoint);
9751 let sandbox_root = normalize_host_path(&vm.cwd);
9752 let host_cwd = normalize_host_path(&vm.host_cwd);
9753 if !path_is_within_root(&normalized_entrypoint, &sandbox_root)
9754 && !path_is_within_root(&normalized_entrypoint, &host_cwd)
9755 {
9756 return None;
9757 }
9758
9759 fs::read_to_string(&normalized_entrypoint).ok()
9760}
9761
9762fn emit_dns_resolution_event<B>(
9763 bridge: &SharedBridge<B>,
9764 vm_id: &str,
9765 hostname: &str,
9766 source: KernelDnsResolutionSource,
9767 addresses: &[IpAddr],
9768 dns: &VmDnsConfig,
9769) where
9770 B: NativeSidecarBridge + Send + 'static,
9771 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9772{
9773 let _ = emit_structured_event(
9774 bridge,
9775 vm_id,
9776 "network.dns.resolved",
9777 audit_fields([
9778 ("hostname", hostname.to_owned()),
9779 ("source", source.as_str().to_owned()),
9780 (
9781 "addresses",
9782 addresses
9783 .iter()
9784 .map(ToString::to_string)
9785 .collect::<Vec<_>>()
9786 .join(","),
9787 ),
9788 ("address_count", addresses.len().to_string()),
9789 ("resolver_count", dns.name_servers.len().to_string()),
9790 (
9791 "resolvers",
9792 dns.name_servers
9793 .iter()
9794 .map(ToString::to_string)
9795 .collect::<Vec<_>>()
9796 .join(","),
9797 ),
9798 ]),
9799 );
9800}
9801
9802fn emit_dns_record_resolution_event<B>(
9803 bridge: &SharedBridge<B>,
9804 vm_id: &str,
9805 hostname: &str,
9806 resolution: &DnsRecordResolution,
9807 dns: &VmDnsConfig,
9808) where
9809 B: NativeSidecarBridge + Send + 'static,
9810 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9811{
9812 if let Some(addresses) = dns_resolution_ip_addrs(resolution.records()) {
9813 emit_dns_resolution_event(
9814 bridge,
9815 vm_id,
9816 hostname,
9817 resolution.source(),
9818 &addresses,
9819 dns,
9820 );
9821 return;
9822 }
9823
9824 let _ = emit_structured_event(
9825 bridge,
9826 vm_id,
9827 "network.dns.resolved",
9828 audit_fields([
9829 ("hostname", hostname.to_owned()),
9830 ("source", resolution.source().as_str().to_owned()),
9831 (
9832 "addresses",
9833 resolution
9834 .records()
9835 .iter()
9836 .map(summarize_dns_record)
9837 .collect::<Vec<_>>()
9838 .join(","),
9839 ),
9840 ("address_count", resolution.records().len().to_string()),
9841 ("resolver_count", dns.name_servers.len().to_string()),
9842 (
9843 "resolvers",
9844 dns.name_servers
9845 .iter()
9846 .map(ToString::to_string)
9847 .collect::<Vec<_>>()
9848 .join(","),
9849 ),
9850 ]),
9851 );
9852}
9853
9854fn emit_dns_resolution_failure_event<B>(
9855 bridge: &SharedBridge<B>,
9856 vm_id: &str,
9857 hostname: &str,
9858 dns: &VmDnsConfig,
9859 error: &SidecarError,
9860) where
9861 B: NativeSidecarBridge + Send + 'static,
9862 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9863{
9864 let _ = emit_structured_event(
9865 bridge,
9866 vm_id,
9867 "network.dns.resolve_failed",
9868 audit_fields([
9869 ("hostname", hostname.to_owned()),
9870 ("reason", error.to_string()),
9871 ("resolver_count", dns.name_servers.len().to_string()),
9872 (
9873 "resolvers",
9874 dns.name_servers
9875 .iter()
9876 .map(ToString::to_string)
9877 .collect::<Vec<_>>()
9878 .join(","),
9879 ),
9880 ]),
9881 );
9882}
9883
9884fn parse_dns_record_type(rrtype: &str) -> Result<RecordType, SidecarError> {
9885 match rrtype {
9886 "A" => Ok(RecordType::A),
9887 "AAAA" => Ok(RecordType::AAAA),
9888 "MX" => Ok(RecordType::MX),
9889 "TXT" => Ok(RecordType::TXT),
9890 "SRV" => Ok(RecordType::SRV),
9891 "CNAME" => Ok(RecordType::CNAME),
9892 "PTR" => Ok(RecordType::PTR),
9893 "NS" => Ok(RecordType::NS),
9894 "SOA" => Ok(RecordType::SOA),
9895 "NAPTR" => Ok(RecordType::NAPTR),
9896 "CAA" => Ok(RecordType::CAA),
9897 "ANY" => Ok(RecordType::ANY),
9898 other => Err(SidecarError::Execution(format!(
9899 "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
9900 ))),
9901 }
9902}
9903
9904fn dns_resolution_to_node_value(
9905 resolution: &DnsRecordResolution,
9906 requested_type: &str,
9907) -> Result<Value, SidecarError> {
9908 let safe_ips = dns_resolution_safe_ip_set(resolution.records(), resolution.hostname())?;
9909 match requested_type {
9910 "A" | "AAAA" => Ok(Value::Array(
9911 resolution
9912 .records()
9913 .iter()
9914 .filter_map(|record| dns_record_ip_string(record, &safe_ips))
9915 .map(Value::String)
9916 .collect(),
9917 )),
9918 "MX" => Ok(Value::Array(
9919 resolution
9920 .records()
9921 .iter()
9922 .filter_map(|record| match record.data() {
9923 RData::MX(mx) => Some(json!({
9924 "priority": mx.preference,
9925 "exchange": normalize_dns_name_for_node(&mx.exchange),
9926 "type": "MX",
9927 })),
9928 _ => None,
9929 })
9930 .collect(),
9931 )),
9932 "TXT" => Ok(Value::Array(
9933 resolution
9934 .records()
9935 .iter()
9936 .filter_map(|record| match record.data() {
9937 RData::TXT(txt) => Some(Value::Array(
9938 txt.txt_data
9939 .iter()
9940 .map(|entry| Value::String(String::from_utf8_lossy(entry).into_owned()))
9941 .collect(),
9942 )),
9943 _ => None,
9944 })
9945 .collect(),
9946 )),
9947 "SRV" => Ok(Value::Array(
9948 resolution
9949 .records()
9950 .iter()
9951 .filter_map(|record| match record.data() {
9952 RData::SRV(srv) => Some(json!({
9953 "priority": srv.priority,
9954 "weight": srv.weight,
9955 "port": srv.port,
9956 "name": normalize_dns_name_for_node(&srv.target),
9957 "type": "SRV",
9958 })),
9959 _ => None,
9960 })
9961 .collect(),
9962 )),
9963 "CNAME" => Ok(Value::Array(
9964 resolution
9965 .records()
9966 .iter()
9967 .filter_map(|record| match record.data() {
9968 RData::CNAME(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9969 _ => None,
9970 })
9971 .collect(),
9972 )),
9973 "PTR" => Ok(Value::Array(
9974 resolution
9975 .records()
9976 .iter()
9977 .filter_map(|record| match record.data() {
9978 RData::PTR(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9979 _ => None,
9980 })
9981 .collect(),
9982 )),
9983 "NS" => Ok(Value::Array(
9984 resolution
9985 .records()
9986 .iter()
9987 .filter_map(|record| match record.data() {
9988 RData::NS(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9989 _ => None,
9990 })
9991 .collect(),
9992 )),
9993 "SOA" => resolution
9994 .records()
9995 .iter()
9996 .find_map(|record| match record.data() {
9997 RData::SOA(soa) => Some(json!({
9998 "nsname": normalize_dns_name_for_node(&soa.mname),
9999 "hostmaster": normalize_dns_name_for_node(&soa.rname),
10000 "serial": soa.serial,
10001 "refresh": soa.refresh,
10002 "retry": soa.retry,
10003 "expire": soa.expire,
10004 "minttl": soa.minimum,
10005 })),
10006 _ => None,
10007 })
10008 .ok_or_else(|| {
10009 SidecarError::Execution(String::from("failed to resolve DNS SOA record"))
10010 }),
10011 "NAPTR" => Ok(Value::Array(
10012 resolution
10013 .records()
10014 .iter()
10015 .filter_map(|record| match record.data() {
10016 RData::NAPTR(naptr) => Some(json!({
10017 "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
10018 "service": String::from_utf8_lossy(&naptr.services).into_owned(),
10019 "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
10020 "replacement": normalize_dns_name_for_node(&naptr.replacement),
10021 "order": naptr.order,
10022 "preference": naptr.preference,
10023 })),
10024 _ => None,
10025 })
10026 .collect(),
10027 )),
10028 "CAA" => Ok(Value::Array(
10029 resolution
10030 .records()
10031 .iter()
10032 .filter_map(|record| match record.data() {
10033 RData::CAA(caa) => {
10034 let mut value = serde_json::Map::new();
10035 value.insert(
10036 "critical".to_owned(),
10037 Value::from(u8::from(caa.issuer_critical)),
10038 );
10039 value.insert("type".to_owned(), Value::String(String::from("CAA")));
10040 if caa.tag.eq_ignore_ascii_case("iodef") {
10041 value.insert(
10042 "iodef".to_owned(),
10043 Value::String(
10044 caa.value_as_iodef()
10045 .map(|url| url.to_string())
10046 .unwrap_or_else(|_| {
10047 String::from_utf8_lossy(&caa.value).into_owned()
10048 }),
10049 ),
10050 );
10051 } else if let Ok((issuer, _params)) = caa.value_as_issue() {
10052 let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
10053 "issuewild"
10054 } else {
10055 "issue"
10056 };
10057 value.insert(
10058 field.to_owned(),
10059 Value::String(
10060 issuer.as_ref().map(ToString::to_string).unwrap_or_else(|| {
10061 String::from_utf8_lossy(&caa.value).into_owned()
10062 }),
10063 ),
10064 );
10065 } else {
10066 value.insert(
10067 caa.tag.to_ascii_lowercase(),
10068 Value::String(String::from_utf8_lossy(&caa.value).into_owned()),
10069 );
10070 }
10071 Some(Value::Object(value))
10072 }
10073 _ => None,
10074 })
10075 .collect(),
10076 )),
10077 "ANY" => Ok(Value::Array(
10078 resolution
10079 .records()
10080 .iter()
10081 .filter_map(|record| dns_any_record_to_value(record, &safe_ips))
10082 .collect(),
10083 )),
10084 other => Err(SidecarError::Execution(format!(
10085 "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
10086 ))),
10087 }
10088}
10089
10090fn dns_resolution_safe_ip_set(
10091 records: &[Record],
10092 hostname: &str,
10093) -> Result<BTreeSet<IpAddr>, SidecarError> {
10094 let ips = records
10095 .iter()
10096 .filter_map(dns_record_ip_addr)
10097 .collect::<Vec<_>>();
10098 if ips.is_empty() {
10099 return Ok(BTreeSet::new());
10100 }
10101 Ok(filter_dns_safe_ip_addrs(ips, hostname)?
10102 .into_iter()
10103 .collect())
10104}
10105
10106fn dns_resolution_ip_addrs(records: &[Record]) -> Option<Vec<IpAddr>> {
10107 let ips = records
10108 .iter()
10109 .filter_map(dns_record_ip_addr)
10110 .collect::<Vec<_>>();
10111 if ips.is_empty() {
10112 return None;
10113 }
10114 Some(ips)
10115}
10116
10117fn dns_record_ip_addr(record: &Record) -> Option<IpAddr> {
10118 match record.data() {
10119 RData::A(address) => Some(IpAddr::V4(**address)),
10120 RData::AAAA(address) => Some(IpAddr::V6(**address)),
10121 _ => None,
10122 }
10123}
10124
10125fn dns_record_ip_string(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<String> {
10126 let ip = dns_record_ip_addr(record)?;
10127 safe_ips.contains(&ip).then(|| ip.to_string())
10128}
10129
10130fn dns_any_record_to_value(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<Value> {
10131 let value = match record.data() {
10132 RData::A(_) | RData::AAAA(_) => json!({
10133 "address": dns_record_ip_string(record, safe_ips)?,
10134 "ttl": record.ttl(),
10135 "type": record.record_type().to_string(),
10136 }),
10137 RData::MX(mx) => json!({
10138 "exchange": normalize_dns_name_for_node(&mx.exchange),
10139 "priority": mx.preference,
10140 "type": "MX",
10141 }),
10142 RData::TXT(txt) => json!({
10143 "entries": txt
10144 .txt_data
10145 .iter()
10146 .map(|entry| String::from_utf8_lossy(entry).into_owned())
10147 .collect::<Vec<_>>(),
10148 "type": "TXT",
10149 }),
10150 RData::SRV(srv) => json!({
10151 "name": normalize_dns_name_for_node(&srv.target),
10152 "port": srv.port,
10153 "priority": srv.priority,
10154 "weight": srv.weight,
10155 "type": "SRV",
10156 }),
10157 RData::CNAME(name) => json!({
10158 "value": normalize_dns_name_for_node(&name.0),
10159 "type": "CNAME",
10160 }),
10161 RData::PTR(name) => json!({
10162 "value": normalize_dns_name_for_node(&name.0),
10163 "type": "PTR",
10164 }),
10165 RData::NS(name) => json!({
10166 "value": normalize_dns_name_for_node(&name.0),
10167 "type": "NS",
10168 }),
10169 RData::SOA(soa) => json!({
10170 "nsname": normalize_dns_name_for_node(&soa.mname),
10171 "hostmaster": normalize_dns_name_for_node(&soa.rname),
10172 "serial": soa.serial,
10173 "refresh": soa.refresh,
10174 "retry": soa.retry,
10175 "expire": soa.expire,
10176 "minttl": soa.minimum,
10177 "type": "SOA",
10178 }),
10179 RData::NAPTR(naptr) => json!({
10180 "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
10181 "service": String::from_utf8_lossy(&naptr.services).into_owned(),
10182 "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
10183 "replacement": normalize_dns_name_for_node(&naptr.replacement),
10184 "order": naptr.order,
10185 "preference": naptr.preference,
10186 "type": "NAPTR",
10187 }),
10188 RData::CAA(caa) => {
10189 let mut value = serde_json::Map::new();
10190 value.insert(
10191 "critical".to_owned(),
10192 Value::from(u8::from(caa.issuer_critical)),
10193 );
10194 value.insert("type".to_owned(), Value::String(String::from("CAA")));
10195 if caa.tag.eq_ignore_ascii_case("iodef") {
10196 value.insert(
10197 "iodef".to_owned(),
10198 Value::String(
10199 caa.value_as_iodef()
10200 .map(|url| url.to_string())
10201 .unwrap_or_else(|_| String::from_utf8_lossy(&caa.value).into_owned()),
10202 ),
10203 );
10204 } else if let Ok((issuer, _params)) = caa.value_as_issue() {
10205 let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
10206 "issuewild"
10207 } else {
10208 "issue"
10209 };
10210 value.insert(
10211 field.to_owned(),
10212 Value::String(
10213 issuer
10214 .as_ref()
10215 .map(ToString::to_string)
10216 .unwrap_or_else(|| String::from_utf8_lossy(&caa.value).into_owned()),
10217 ),
10218 );
10219 }
10220 Value::Object(value)
10221 }
10222 _ => return None,
10223 };
10224 Some(value)
10225}
10226
10227fn normalize_dns_name_for_node(name: &impl ToString) -> String {
10228 name.to_string().trim_end_matches('.').to_owned()
10229}
10230
10231fn summarize_dns_record(record: &Record) -> String {
10232 match record.data() {
10233 RData::A(_) | RData::AAAA(_) => record.data().to_string(),
10234 _ => format!("{} {}", record.record_type(), record.data()),
10235 }
10236}
10237
10238fn find_socket_state_entry(
10246 vm: Option<&VmState>,
10247 kind: SocketQueryKind,
10248 request: &FindListenerRequest,
10249) -> Result<Option<SocketStateEntry>, SidecarError> {
10250 let vm = vm.ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
10251
10252 for (process_id, process) in &vm.active_processes {
10253 if let Some(path) = request.path.as_deref() {
10254 if matches!(kind, SocketQueryKind::TcpListener) {
10255 for listener in process.unix_listeners.values() {
10256 if listener.path() != path {
10257 continue;
10258 }
10259 return Ok(Some(SocketStateEntry {
10260 process_id: process_id.to_owned(),
10261 host: None,
10262 port: None,
10263 path: Some(path.to_owned()),
10264 }));
10265 }
10266 }
10267 }
10268
10269 if request.path.is_none() {
10270 if let Some(entry) =
10271 find_kernel_socket_state_entry(&vm.kernel, process_id, process, kind, request)?
10272 {
10273 return Ok(Some(entry));
10274 }
10275
10276 match kind {
10277 SocketQueryKind::TcpListener => {
10278 for server in process.http_servers.values() {
10279 let local_addr = server.guest_local_addr;
10280 let local_host = local_addr.ip().to_string();
10281 if !socket_host_matches(request.host.as_deref(), &local_host) {
10282 continue;
10283 }
10284 if let Some(port) = request.port {
10285 if local_addr.port() != port {
10286 continue;
10287 }
10288 }
10289 return Ok(Some(SocketStateEntry {
10290 process_id: process_id.to_owned(),
10291 host: Some(local_host),
10292 port: Some(local_addr.port()),
10293 path: None,
10294 }));
10295 }
10296
10297 for listener in process.tcp_listeners.values() {
10298 if listener.kernel_socket_id.is_some() {
10299 continue;
10300 }
10301 let local_addr = listener.guest_local_addr();
10302 let local_host = local_addr.ip().to_string();
10303 if !socket_host_matches(request.host.as_deref(), &local_host) {
10304 continue;
10305 }
10306 if let Some(port) = request.port {
10307 if local_addr.port() != port {
10308 continue;
10309 }
10310 }
10311 return Ok(Some(SocketStateEntry {
10312 process_id: process_id.to_owned(),
10313 host: Some(local_host),
10314 port: Some(local_addr.port()),
10315 path: None,
10316 }));
10317 }
10318 }
10319 SocketQueryKind::UdpBound => {
10320 for socket in process.udp_sockets.values() {
10321 if socket.kernel_socket_id.is_some() {
10322 continue;
10323 }
10324 let Some(local_addr) = socket.local_addr() else {
10325 continue;
10326 };
10327 let local_host = local_addr.ip().to_string();
10328 if !socket_host_matches(request.host.as_deref(), &local_host) {
10329 continue;
10330 }
10331 if let Some(port) = request.port {
10332 if local_addr.port() != port {
10333 continue;
10334 }
10335 }
10336 return Ok(Some(SocketStateEntry {
10337 process_id: process_id.to_owned(),
10338 host: Some(local_host),
10339 port: Some(local_addr.port()),
10340 path: None,
10341 }));
10342 }
10343 }
10344 }
10345 }
10346
10347 let child_pid = process.execution.child_pid();
10348 let inodes = socket_inodes_for_pid(child_pid)?;
10349 if inodes.is_empty() {
10350 continue;
10351 }
10352
10353 if let Some(path) = request.path.as_deref() {
10354 if let Some(listener) = find_unix_socket_for_pid(child_pid, &inodes, path, process_id)?
10355 {
10356 return Ok(Some(listener));
10357 }
10358 continue;
10359 }
10360
10361 let table_paths = match kind {
10362 SocketQueryKind::TcpListener => [
10363 format!("/proc/{child_pid}/net/tcp"),
10364 format!("/proc/{child_pid}/net/tcp6"),
10365 ],
10366 SocketQueryKind::UdpBound => [
10367 format!("/proc/{child_pid}/net/udp"),
10368 format!("/proc/{child_pid}/net/udp6"),
10369 ],
10370 };
10371 for table_path in table_paths {
10372 if let Some(entry) = find_inet_socket_for_pid(
10373 &table_path,
10374 &inodes,
10375 kind,
10376 request.host.as_deref(),
10377 request.port,
10378 process_id,
10379 )? {
10380 return Ok(Some(entry));
10381 }
10382 }
10383 }
10384
10385 Ok(None)
10386}
10387
10388fn require_vm_inspection_permission<B>(
10389 bridge: &SharedBridge<B>,
10390 vm_id: &str,
10391 capability: &str,
10392 domain: &str,
10393 resource: &str,
10394) -> Result<(), SidecarError>
10395where
10396 B: NativeSidecarBridge + Send + 'static,
10397 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
10398{
10399 let decision = bridge.static_permission_decision(vm_id, capability, domain, Some(resource));
10400 if decision.as_ref().is_some_and(|decision| decision.allow) {
10401 return Ok(());
10402 }
10403
10404 let reason = decision
10405 .and_then(|decision| decision.reason)
10406 .unwrap_or_else(|| format!("{capability} permission required"));
10407 Err(SidecarError::Execution(format!(
10408 "EACCES: permission denied, {resource}: {reason}"
10409 )))
10410}
10411
10412fn socket_query_resource(kind: SocketQueryKind, request: &FindListenerRequest) -> String {
10413 if let Some(path) = request.path.as_deref() {
10414 return format!("unix://{path}");
10415 }
10416
10417 let host = request.host.as_deref().unwrap_or("*");
10418 let port = request
10419 .port
10420 .map_or_else(|| String::from("*"), |port| port.to_string());
10421 match kind {
10422 SocketQueryKind::TcpListener => format!("tcp://{host}:{port}"),
10423 SocketQueryKind::UdpBound => format!("udp://{host}:{port}"),
10424 }
10425}
10426
10427fn snapshot_vm_processes(vm: &VmState) -> Vec<ProcessSnapshotEntry> {
10428 let process_table = vm.kernel.list_processes();
10429 snapshot_vm_processes_inner(vm, &process_table)
10430}
10431
10432fn snapshot_vm_processes_inner(
10433 vm: &VmState,
10434 process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
10435) -> Vec<ProcessSnapshotEntry> {
10436 let mut entries = Vec::new();
10437
10438 for (process_id, process) in &vm.active_processes {
10439 collect_process_snapshot_entries(process_id, process, process_table, &mut entries);
10440 }
10441
10442 for exited in &vm.exited_process_snapshots {
10443 entries.push(exited.process.clone());
10444 }
10445
10446 entries
10447}
10448
10449fn prune_exited_process_snapshots(vm: &mut VmState) {
10450 let cutoff = Instant::now() - EXITED_PROCESS_SNAPSHOT_RETENTION;
10451 while vm
10452 .exited_process_snapshots
10453 .front()
10454 .is_some_and(|snapshot| snapshot.captured_at < cutoff)
10455 {
10456 vm.exited_process_snapshots.pop_front();
10457 }
10458}
10459
10460fn build_process_snapshot_entry(
10461 process_id: &str,
10462 process: &ActiveProcess,
10463 info: &secure_exec_kernel::process_table::ProcessInfo,
10464 exit_code: Option<i32>,
10465) -> ProcessSnapshotEntry {
10466 ProcessSnapshotEntry {
10467 process_id: process_id.to_owned(),
10468 pid: info.pid,
10469 ppid: info.ppid,
10470 pgid: info.pgid,
10471 sid: info.sid,
10472 driver: info.driver.clone(),
10473 command: info.command.clone(),
10474 args: Vec::new(),
10475 cwd: process.guest_cwd.clone(),
10476 status: if exit_code.is_some() {
10477 ProcessSnapshotStatus::Exited
10478 } else {
10479 match info.status {
10480 ProcessStatus::Running => ProcessSnapshotStatus::Running,
10481 ProcessStatus::Stopped => ProcessSnapshotStatus::Stopped,
10482 ProcessStatus::Exited => ProcessSnapshotStatus::Exited,
10483 }
10484 },
10485 exit_code: exit_code.or(info.exit_code),
10486 }
10487}
10488
10489fn collect_process_snapshot_entries(
10490 process_id: &str,
10491 process: &ActiveProcess,
10492 process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
10493 entries: &mut Vec<ProcessSnapshotEntry>,
10494) {
10495 if let Some(info) = process_table.get(&process.kernel_pid) {
10496 entries.push(build_process_snapshot_entry(
10497 process_id, process, info, None,
10498 ));
10499 }
10500
10501 for (child_id, child) in &process.child_processes {
10502 let child_process_id = format!("{process_id}/{child_id}");
10503 collect_process_snapshot_entries(&child_process_id, child, process_table, entries);
10504 }
10505}
10506
10507fn find_kernel_socket_state_entry(
10508 kernel: &SidecarKernel,
10509 process_id: &str,
10510 process: &ActiveProcess,
10511 kind: SocketQueryKind,
10512 request: &FindListenerRequest,
10513) -> Result<Option<SocketStateEntry>, SidecarError> {
10514 let entry = match kind {
10515 SocketQueryKind::TcpListener => process
10516 .tcp_listeners
10517 .values()
10518 .filter_map(|listener| listener.kernel_socket_id)
10519 .find_map(|socket_id| {
10520 kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
10521 }),
10522 SocketQueryKind::UdpBound => process
10523 .udp_sockets
10524 .values()
10525 .filter_map(|socket| socket.kernel_socket_id)
10526 .find_map(|socket_id| {
10527 kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
10528 }),
10529 };
10530
10531 if entry.is_some() {
10532 return Ok(entry);
10533 }
10534
10535 for child in process.child_processes.values() {
10536 if let Some(entry) =
10537 find_kernel_socket_state_entry(kernel, process_id, child, kind, request)?
10538 {
10539 return Ok(Some(entry));
10540 }
10541 }
10542
10543 Ok(None)
10544}
10545
10546fn kernel_socket_state_entry(
10547 kernel: &SidecarKernel,
10548 process_id: &str,
10549 socket_id: SocketId,
10550 kind: SocketQueryKind,
10551 request: &FindListenerRequest,
10552) -> Option<SocketStateEntry> {
10553 let record = kernel.socket_get(socket_id)?;
10554 let local_address = record.local_address()?;
10555 match kind {
10556 SocketQueryKind::TcpListener if record.state() == SocketState::Listening => {}
10557 SocketQueryKind::TcpListener => return None,
10558 SocketQueryKind::UdpBound => {}
10559 }
10560
10561 if !socket_host_matches(request.host.as_deref(), local_address.host()) {
10562 return None;
10563 }
10564 if request
10565 .port
10566 .is_some_and(|port| local_address.port() != port)
10567 {
10568 return None;
10569 }
10570
10571 Some(SocketStateEntry {
10572 process_id: process_id.to_owned(),
10573 host: Some(local_address.host().to_owned()),
10574 port: Some(local_address.port()),
10575 path: None,
10576 })
10577}
10578
10579fn socket_inodes_for_pid(pid: u32) -> Result<BTreeSet<u64>, SidecarError> {
10580 let fd_dir = PathBuf::from(format!("/proc/{pid}/fd"));
10581 let entries = match fs::read_dir(&fd_dir) {
10582 Ok(entries) => entries,
10583 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(BTreeSet::new()),
10584 Err(error) => {
10585 return Err(SidecarError::Io(format!(
10586 "failed to read socket descriptors for process {pid}: {error}"
10587 )));
10588 }
10589 };
10590
10591 let mut inodes = BTreeSet::new();
10592 for entry in entries {
10593 let entry = entry.map_err(|error| {
10594 SidecarError::Io(format!(
10595 "failed to inspect fd entry for process {pid}: {error}"
10596 ))
10597 })?;
10598 let target = match fs::read_link(entry.path()) {
10599 Ok(target) => target,
10600 Err(_) => continue,
10601 };
10602 if let Some(inode) = parse_socket_inode(&target) {
10603 inodes.insert(inode);
10604 }
10605 }
10606
10607 Ok(inodes)
10608}
10609
10610fn parse_socket_inode(target: &Path) -> Option<u64> {
10611 let value = target.to_string_lossy();
10612 let trimmed = value.strip_prefix("socket:[")?.strip_suffix(']')?;
10613 trimmed.parse().ok()
10614}
10615
10616fn unix_socket_path(addr: &UnixSocketAddr) -> Option<String> {
10617 addr.as_pathname()
10618 .map(|path| path.to_string_lossy().into_owned())
10619}
10620
10621fn find_unix_socket_for_pid(
10622 pid: u32,
10623 inodes: &BTreeSet<u64>,
10624 path: &str,
10625 process_id: &str,
10626) -> Result<Option<SocketStateEntry>, SidecarError> {
10627 let table_path = format!("/proc/{pid}/net/unix");
10628 let contents = match fs::read_to_string(&table_path) {
10629 Ok(contents) => contents,
10630 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
10631 Err(error) => {
10632 return Err(SidecarError::Io(format!(
10633 "failed to inspect unix sockets for process {pid}: {error}"
10634 )));
10635 }
10636 };
10637
10638 for line in contents.lines().skip(1) {
10639 let columns = line.split_whitespace().collect::<Vec<_>>();
10640 if columns.len() < 8 {
10641 continue;
10642 }
10643 let Ok(inode) = columns[6].parse::<u64>() else {
10644 continue;
10645 };
10646 if !inodes.contains(&inode) || columns[7] != path {
10647 continue;
10648 }
10649 return Ok(Some(SocketStateEntry {
10650 process_id: process_id.to_owned(),
10651 host: None,
10652 port: None,
10653 path: Some(path.to_owned()),
10654 }));
10655 }
10656
10657 Ok(None)
10658}
10659
10660fn find_inet_socket_for_pid(
10661 table_path: &str,
10662 inodes: &BTreeSet<u64>,
10663 kind: SocketQueryKind,
10664 requested_host: Option<&str>,
10665 requested_port: Option<u16>,
10666 process_id: &str,
10667) -> Result<Option<SocketStateEntry>, SidecarError> {
10668 for entry in parse_proc_net_entries(table_path)? {
10669 if !inodes.contains(&entry.inode) {
10670 continue;
10671 }
10672 if matches!(kind, SocketQueryKind::TcpListener) && entry.state != "0A" {
10673 continue;
10674 }
10675 if !socket_host_matches(requested_host, &entry.local_host) {
10676 continue;
10677 }
10678 if let Some(port) = requested_port {
10679 if entry.local_port != port {
10680 continue;
10681 }
10682 }
10683 return Ok(Some(SocketStateEntry {
10684 process_id: process_id.to_owned(),
10685 host: Some(entry.local_host),
10686 port: Some(entry.local_port),
10687 path: None,
10688 }));
10689 }
10690
10691 Ok(None)
10692}
10693
10694fn is_unspecified_socket_host(host: &str) -> bool {
10695 host == "0.0.0.0" || host == "::"
10696}
10697
10698fn is_loopback_socket_host(host: &str) -> bool {
10699 host == "127.0.0.1" || host == "::1" || host.eq_ignore_ascii_case("localhost")
10700}
10701
10702pub(crate) fn vm_network_resource_counts(vm: &VmState) -> NetworkResourceCounts {
10703 let snapshot = vm.kernel.resource_snapshot();
10704 let mut counts = NetworkResourceCounts {
10705 sockets: snapshot.sockets,
10706 connections: snapshot.socket_connections,
10707 };
10708 for process in vm.active_processes.values() {
10709 let process_counts = process.sidecar_only_network_resource_counts();
10710 counts.sockets += process_counts.sockets;
10711 counts.connections += process_counts.connections;
10712 }
10713 counts
10714}
10715
10716#[allow(clippy::too_many_arguments)]
10717fn collect_javascript_socket_port_state(
10718 kernel: &SidecarKernel,
10719 process_id: &str,
10720 process: &ActiveProcess,
10721 tcp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10722 http_loopback_targets: &mut BTreeMap<
10723 (JavascriptSocketFamily, u16),
10724 JavascriptHttpLoopbackTarget,
10725 >,
10726 udp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10727 udp_host_to_guest: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10728 used_tcp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10729 used_udp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10730) {
10731 for (family, port) in process.tcp_port_reservations.values() {
10732 used_tcp_ports.entry(*family).or_default().insert(*port);
10733 }
10734
10735 let mut record_tcp_listener = |guest_addr: SocketAddr, host_port: u16| {
10736 let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10737 used_tcp_ports
10738 .entry(family)
10739 .or_default()
10740 .insert(guest_addr.port());
10741 tcp_guest_to_host.insert((family, guest_addr.port()), host_port);
10744 };
10745
10746 for listener in process.tcp_listeners.values() {
10747 let local_addr = listener
10748 .kernel_socket_id
10749 .and_then(|socket_id| kernel.socket_get(socket_id))
10750 .and_then(|record| record.local_address().cloned())
10751 .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
10752 .unwrap_or_else(|| listener.guest_local_addr());
10753 record_tcp_listener(local_addr, local_addr.port());
10754 }
10755
10756 for (server_id, server) in &process.http_servers {
10757 let host_port = match server.listener.local_addr() {
10758 Ok(addr) => addr.port(),
10759 Err(_) => continue,
10760 };
10761 record_tcp_listener(server.guest_local_addr, host_port);
10762 let family = JavascriptSocketFamily::from_ip(server.guest_local_addr.ip());
10763 http_loopback_targets.insert(
10764 (family, server.guest_local_addr.port()),
10765 JavascriptHttpLoopbackTarget {
10766 process_id: process_id.to_owned(),
10767 server_id: *server_id,
10768 },
10769 );
10770 }
10771
10772 if let Ok(http2) = process.http2.shared.lock() {
10773 for server in http2.servers.values() {
10774 record_tcp_listener(server.guest_local_addr, server.actual_local_addr.port());
10775 }
10776 }
10777
10778 for socket in process.tcp_sockets.values() {
10779 let guest_addr = socket
10780 .kernel_socket_id
10781 .and_then(|socket_id| kernel.socket_get(socket_id))
10782 .and_then(|record| record.local_address().cloned())
10783 .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
10784 .unwrap_or(socket.guest_local_addr);
10785 let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10786 used_tcp_ports
10787 .entry(family)
10788 .or_default()
10789 .insert(guest_addr.port());
10790 }
10791
10792 for socket in process.udp_sockets.values() {
10793 let guest_addr = socket
10794 .kernel_socket_id
10795 .and_then(|socket_id| kernel.socket_get(socket_id))
10796 .and_then(|record| record.local_address().cloned())
10797 .and_then(|address| {
10798 resolve_udp_bind_addr(address.host(), address.port(), socket.family).ok()
10799 })
10800 .or_else(|| socket.local_addr());
10801 let Some(guest_addr) = guest_addr else {
10802 continue;
10803 };
10804 let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10805 used_udp_ports
10806 .entry(family)
10807 .or_default()
10808 .insert(guest_addr.port());
10809 if let Some(host_addr) = socket
10810 .socket
10811 .as_ref()
10812 .and_then(|socket| socket.local_addr().ok())
10813 {
10814 if is_loopback_ip(guest_addr.ip()) {
10815 udp_guest_to_host.insert((family, guest_addr.port()), host_addr.port());
10816 udp_host_to_guest.insert((family, host_addr.port()), guest_addr.port());
10817 }
10818 } else if socket.kernel_socket_id.is_some() && is_loopback_ip(guest_addr.ip()) {
10819 udp_guest_to_host.insert((family, guest_addr.port()), guest_addr.port());
10820 udp_host_to_guest.insert((family, guest_addr.port()), guest_addr.port());
10821 }
10822 }
10823
10824 for (child_process_id, child) in &process.child_processes {
10825 let child_id = format!("{process_id}/{child_process_id}");
10826 collect_javascript_socket_port_state(
10827 kernel,
10828 &child_id,
10829 child,
10830 tcp_guest_to_host,
10831 http_loopback_targets,
10832 udp_guest_to_host,
10833 udp_host_to_guest,
10834 used_tcp_ports,
10835 used_udp_ports,
10836 );
10837 }
10838}
10839
10840pub(crate) fn build_javascript_socket_path_context(
10841 vm: &VmState,
10842) -> Result<JavascriptSocketPathContext, SidecarError> {
10843 let mut loopback_exempt_ports = vm.create_loopback_exempt_ports.clone();
10844 loopback_exempt_ports.extend(vm.configuration.loopback_exempt_ports.iter().copied());
10845 let mut tcp_loopback_guest_to_host_ports = BTreeMap::new();
10846 let mut http_loopback_targets = BTreeMap::new();
10847 let mut udp_loopback_guest_to_host_ports = BTreeMap::new();
10848 let mut udp_loopback_host_to_guest_ports = BTreeMap::new();
10849 let mut used_tcp_guest_ports = BTreeMap::new();
10850 let mut used_udp_guest_ports = BTreeMap::new();
10851 for (process_id, process) in &vm.active_processes {
10852 collect_javascript_socket_port_state(
10853 &vm.kernel,
10854 process_id,
10855 process,
10856 &mut tcp_loopback_guest_to_host_ports,
10857 &mut http_loopback_targets,
10858 &mut udp_loopback_guest_to_host_ports,
10859 &mut udp_loopback_host_to_guest_ports,
10860 &mut used_tcp_guest_ports,
10861 &mut used_udp_guest_ports,
10862 );
10863 }
10864 Ok(JavascriptSocketPathContext {
10865 sandbox_root: vm.cwd.clone(),
10866 mounts: vm.configuration.mounts.clone(),
10867 listen_policy: vm.listen_policy,
10868 loopback_exempt_ports,
10869 tcp_loopback_guest_to_host_ports,
10870 http_loopback_targets,
10871 udp_loopback_guest_to_host_ports,
10872 udp_loopback_host_to_guest_ports,
10873 used_tcp_guest_ports,
10874 used_udp_guest_ports,
10875 })
10876}
10877
10878fn check_network_resource_limit(
10879 limit: Option<usize>,
10880 current: usize,
10881 additional: usize,
10882 label: &str,
10883) -> Result<(), SidecarError> {
10884 if let Some(limit) = limit {
10885 if current.saturating_add(additional) > limit {
10886 return Err(SidecarError::Execution(format!(
10887 "EAGAIN: maximum {label} count reached"
10888 )));
10889 }
10890 }
10891 Ok(())
10892}
10893
10894fn normalize_tcp_listen_host(
10895 host: Option<&str>,
10896) -> Result<(JavascriptSocketFamily, &'static str, &'static str), SidecarError> {
10897 match host.unwrap_or("127.0.0.1") {
10898 "127.0.0.1" | "localhost" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "127.0.0.1")),
10899 "::1" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::1")),
10900 "0.0.0.0" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "0.0.0.0")),
10901 "::" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::")),
10902 other => Err(SidecarError::Execution(format!(
10903 "EACCES: TCP listeners must bind to loopback or unspecified addresses, got {other}"
10904 ))),
10905 }
10906}
10907
10908fn normalize_udp_bind_host(
10909 host: Option<&str>,
10910 family: JavascriptUdpFamily,
10911) -> Result<(&'static str, &'static str, JavascriptSocketFamily), SidecarError> {
10912 match (family, host) {
10913 (JavascriptUdpFamily::Ipv4, None) | (JavascriptUdpFamily::Ipv4, Some("0.0.0.0")) => {
10914 Ok(("127.0.0.1", "0.0.0.0", JavascriptSocketFamily::Ipv4))
10915 }
10916 (JavascriptUdpFamily::Ipv4, Some("127.0.0.1"))
10917 | (JavascriptUdpFamily::Ipv4, Some("localhost")) => {
10918 Ok(("127.0.0.1", "127.0.0.1", JavascriptSocketFamily::Ipv4))
10919 }
10920 (JavascriptUdpFamily::Ipv6, None) | (JavascriptUdpFamily::Ipv6, Some("::")) => {
10921 Ok(("::1", "::", JavascriptSocketFamily::Ipv6))
10922 }
10923 (JavascriptUdpFamily::Ipv6, Some("::1"))
10924 | (JavascriptUdpFamily::Ipv6, Some("localhost")) => {
10925 Ok(("::1", "::1", JavascriptSocketFamily::Ipv6))
10926 }
10927 (JavascriptUdpFamily::Ipv4, Some(other)) => Err(SidecarError::Execution(format!(
10928 "EACCES: udp4 sockets must bind to 127.0.0.1 or 0.0.0.0, got {other}"
10929 ))),
10930 (JavascriptUdpFamily::Ipv6, Some(other)) => Err(SidecarError::Execution(format!(
10931 "EACCES: udp6 sockets must bind to ::1 or ::, got {other}"
10932 ))),
10933 }
10934}
10935
10936fn allocate_guest_listen_port(
10937 requested_port: u16,
10938 family: JavascriptSocketFamily,
10939 used_ports: &BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10940 policy: VmListenPolicy,
10941) -> Result<u16, SidecarError> {
10942 let is_allowed = |port: u16| {
10943 port >= policy.port_min
10944 && port <= policy.port_max
10945 && (policy.allow_privileged || port >= 1024)
10946 };
10947 let used = used_ports.get(&family);
10948
10949 if requested_port != 0 {
10950 if !is_allowed(requested_port) {
10951 let reason = if requested_port < 1024 && !policy.allow_privileged {
10952 format!(
10953 "EACCES: privileged listen port {requested_port} requires {}=true",
10954 VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY
10955 )
10956 } else {
10957 format!(
10958 "EACCES: listen port {requested_port} is outside the allowed range {}-{}",
10959 policy.port_min, policy.port_max
10960 )
10961 };
10962 return Err(SidecarError::Execution(reason));
10963 }
10964 if used.is_some_and(|ports| ports.contains(&requested_port)) {
10965 return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
10966 libc::EADDRINUSE,
10967 )));
10968 }
10969 return Ok(requested_port);
10970 }
10971
10972 let allocation_start = policy
10973 .port_min
10974 .max(if policy.allow_privileged { 1 } else { 1024 });
10975 for candidate in allocation_start..=policy.port_max {
10976 if used.is_some_and(|ports| ports.contains(&candidate)) {
10977 continue;
10978 }
10979 return Ok(candidate);
10980 }
10981
10982 Err(sidecar_net_error(std::io::Error::from_raw_os_error(
10983 libc::EADDRINUSE,
10984 )))
10985}
10986
10987fn socket_host_matches(requested: Option<&str>, actual: &str) -> bool {
10988 match requested {
10989 None => true,
10990 Some(requested) if requested == actual => true,
10991 Some(requested)
10992 if is_unspecified_socket_host(requested) && is_unspecified_socket_host(actual) =>
10993 {
10994 true
10995 }
10996 Some(requested) if is_unspecified_socket_host(requested) => is_loopback_socket_host(actual),
10997 Some(requested) if requested.eq_ignore_ascii_case("localhost") => {
10998 is_loopback_socket_host(actual)
10999 }
11000 _ => false,
11001 }
11002}
11003
11004fn parse_proc_net_entries(table_path: &str) -> Result<Vec<ProcNetEntry>, SidecarError> {
11005 let contents = match fs::read_to_string(table_path) {
11006 Ok(contents) => contents,
11007 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
11008 Err(error) => {
11009 return Err(SidecarError::Io(format!(
11010 "failed to inspect socket table {table_path}: {error}"
11011 )));
11012 }
11013 };
11014
11015 let mut entries = Vec::new();
11016 for line in contents.lines().skip(1) {
11017 let columns = line.split_whitespace().collect::<Vec<_>>();
11018 if columns.len() < 10 {
11019 continue;
11020 }
11021 let Some((host, port)) = parse_proc_ip_port(columns[1]) else {
11022 continue;
11023 };
11024 let Ok(inode) = columns[9].parse::<u64>() else {
11025 continue;
11026 };
11027 entries.push(ProcNetEntry {
11028 local_host: host,
11029 local_port: port,
11030 state: columns[3].to_owned(),
11031 inode,
11032 });
11033 }
11034
11035 Ok(entries)
11036}
11037
11038fn parse_proc_ip_port(value: &str) -> Option<(String, u16)> {
11039 let (raw_ip, raw_port) = value.split_once(':')?;
11040 let port = u16::from_str_radix(raw_port, 16).ok()?;
11041 let host = match raw_ip.len() {
11042 8 => {
11043 let raw = u32::from_str_radix(raw_ip, 16).ok()?;
11044 Ipv4Addr::from(raw.to_le_bytes()).to_string()
11045 }
11046 32 => {
11047 let mut bytes = [0_u8; 16];
11048 for (index, chunk) in raw_ip.as_bytes().chunks(8).enumerate() {
11049 let word = u32::from_str_radix(std::str::from_utf8(chunk).ok()?, 16).ok()?;
11050 bytes[index * 4..(index + 1) * 4].copy_from_slice(&word.to_le_bytes());
11051 }
11052 Ipv6Addr::from(bytes).to_string()
11053 }
11054 _ => return None,
11055 };
11056 Some((host, port))
11057}
11058
11059fn python_file_entrypoint(entrypoint: &str) -> Option<PathBuf> {
11060 let path = Path::new(entrypoint);
11061 (path.extension().and_then(|extension| extension.to_str()) == Some("py"))
11062 .then(|| path.to_path_buf())
11063}
11064
11065fn add_runtime_guest_path_mapping(
11066 env: &mut BTreeMap<String, String>,
11067 guest_path: &str,
11068 host_path: &Path,
11069) {
11070 let mut mappings = env
11071 .get("AGENTOS_GUEST_PATH_MAPPINGS")
11072 .and_then(|value| serde_json::from_str::<Vec<Value>>(value).ok())
11073 .unwrap_or_default();
11074 mappings.retain(|mapping| {
11075 mapping
11076 .get("guestPath")
11077 .and_then(Value::as_str)
11078 .map(|existing| normalize_path(existing) != normalize_path(guest_path))
11079 .unwrap_or(true)
11080 });
11081 mappings.push(json!({
11082 "guestPath": normalize_path(guest_path),
11083 "hostPath": host_path.display().to_string(),
11084 }));
11085 if let Ok(serialized) = serde_json::to_string(&mappings) {
11086 env.insert(String::from("AGENTOS_GUEST_PATH_MAPPINGS"), serialized);
11087 }
11088}
11089
11090fn add_runtime_host_access_path(
11091 env: &mut BTreeMap<String, String>,
11092 key: &str,
11093 host_path: &Path,
11094 expand: bool,
11095) {
11096 let existing = env
11097 .get(key)
11098 .and_then(|value| serde_json::from_str::<Vec<String>>(value).ok())
11099 .unwrap_or_default()
11100 .into_iter()
11101 .map(PathBuf::from)
11102 .collect::<Vec<_>>();
11103 let mut paths = existing;
11104 paths.push(host_path.to_path_buf());
11105 let normalized = if expand {
11106 expand_host_access_paths(&paths)
11107 } else {
11108 dedupe_host_paths(&paths)
11109 };
11110 let serialized = normalized
11111 .iter()
11112 .map(|path| path.to_string_lossy().into_owned())
11113 .collect::<Vec<_>>();
11114 if let Ok(serialized) = serde_json::to_string(&serialized) {
11115 env.insert(key.to_owned(), serialized);
11116 }
11117}
11118
11119fn is_path_like_specifier(specifier: &str) -> bool {
11122 specifier.starts_with('/')
11123 || specifier.starts_with("./")
11124 || specifier.starts_with("../")
11125 || specifier.starts_with("file:")
11126}
11127
11128fn execution_wasm_permission_tier(tier: WasmPermissionTier) -> ExecutionWasmPermissionTier {
11129 match tier {
11130 WasmPermissionTier::Full => ExecutionWasmPermissionTier::Full,
11131 WasmPermissionTier::ReadWrite => ExecutionWasmPermissionTier::ReadWrite,
11132 WasmPermissionTier::ReadOnly => ExecutionWasmPermissionTier::ReadOnly,
11133 WasmPermissionTier::Isolated => ExecutionWasmPermissionTier::Isolated,
11134 }
11135}
11136
11137fn resolve_wasm_permission_tier(
11138 vm: &VmState,
11139 command_name: Option<&str>,
11140 explicit_tier: Option<WasmPermissionTier>,
11141 entrypoint: &str,
11142) -> WasmPermissionTier {
11143 explicit_tier
11144 .or_else(|| command_name.and_then(|command| vm.command_permissions.get(command).copied()))
11145 .or_else(|| {
11146 Path::new(entrypoint)
11147 .file_name()
11148 .and_then(|name| name.to_str())
11149 .and_then(|command| vm.command_permissions.get(command).copied())
11150 })
11151 .unwrap_or(WasmPermissionTier::Full)
11152}
11153
11154fn tokenize_shell_free_command(command: &str) -> Vec<String> {
11155 command
11156 .split_whitespace()
11157 .filter(|segment| !segment.is_empty())
11158 .map(str::to_owned)
11159 .collect()
11160}
11161
11162fn is_posix_shell_builtin(command: &str) -> bool {
11163 matches!(
11164 command,
11165 "." | ":"
11166 | "break"
11167 | "cd"
11168 | "continue"
11169 | "eval"
11170 | "exec"
11171 | "exit"
11172 | "export"
11173 | "readonly"
11174 | "return"
11175 | "set"
11176 | "shift"
11177 | "times"
11178 | "trap"
11179 | "umask"
11180 | "unset"
11181 )
11182}
11183
11184fn shell_first_token_requires_shell(token: &str) -> bool {
11190 token.contains('=') || is_shell_reserved_word(token)
11191}
11192
11193fn is_shell_reserved_word(token: &str) -> bool {
11194 matches!(
11195 token,
11196 "if" | "then"
11197 | "elif"
11198 | "else"
11199 | "fi"
11200 | "for"
11201 | "in"
11202 | "do"
11203 | "done"
11204 | "while"
11205 | "until"
11206 | "case"
11207 | "esac"
11208 | "{"
11209 | "}"
11210 | "!"
11211 )
11212}
11213
11214fn command_requires_shell(command: &str) -> bool {
11215 command.chars().any(|ch| {
11216 matches!(
11217 ch,
11218 '|' | '&'
11219 | ';'
11220 | '<'
11221 | '>'
11222 | '('
11223 | ')'
11224 | '$'
11225 | '`'
11226 | '*'
11227 | '?'
11228 | '['
11229 | ']'
11230 | '{'
11231 | '}'
11232 | '~'
11233 | '\''
11234 | '"'
11235 | '\\'
11236 | '\n'
11237 )
11238 })
11239}
11240
11241fn host_mount_path_for_guest_path(vm: &VmState, guest_path: &str) -> Option<PathBuf> {
11242 let normalized = normalize_path(guest_path);
11243
11244 let mut mounts = vm
11245 .configuration
11246 .mounts
11247 .iter()
11248 .filter_map(|mount| {
11249 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11250 .then(|| {
11251 mount_config_host_path(&mount.plugin.config)
11252 .map(|host_path| (mount.guest_path.as_str(), host_path))
11253 })
11254 .flatten()
11255 })
11256 .collect::<Vec<_>>();
11257 mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
11258
11259 for (guest_root, host_root) in mounts {
11260 if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
11261 continue;
11262 }
11263
11264 let suffix = normalized
11265 .strip_prefix(guest_root)
11266 .unwrap_or_default()
11267 .trim_start_matches('/');
11268 let mut path = PathBuf::from(host_root);
11269 if !suffix.is_empty() {
11270 path.push(suffix);
11271 }
11272 return Some(path);
11273 }
11274
11275 None
11276}
11277
11278fn host_runtime_path_for_guest_path_with_env(
11279 vm: &VmState,
11280 runtime_env: &BTreeMap<String, String>,
11281 guest_path: &str,
11282 default_host_cwd: &Path,
11283) -> Option<PathBuf> {
11284 if let Some(path) = host_mount_path_for_guest_path(vm, guest_path) {
11285 return Some(path);
11286 }
11287 if let Some(path) = host_path_from_runtime_guest_mappings(runtime_env, guest_path) {
11288 return Some(path);
11289 }
11290
11291 let normalized = normalize_path(guest_path);
11292 let virtual_home = guest_virtual_home(vm);
11293
11294 if normalized == virtual_home || normalized.starts_with(&format!("{virtual_home}/")) {
11295 let suffix = normalized
11296 .strip_prefix(&virtual_home)
11297 .unwrap_or_default()
11298 .trim_start_matches('/');
11299 let mut host_path = default_host_cwd.to_path_buf();
11300 if !suffix.is_empty() {
11301 host_path.push(suffix);
11302 }
11303 return Some(host_path);
11304 }
11305
11306 None
11307}
11308
11309#[derive(Deserialize, Serialize)]
11310struct RuntimeGuestPathMapping {
11311 #[serde(rename = "guestPath")]
11312 guest_path: String,
11313 #[serde(rename = "hostPath")]
11314 host_path: String,
11315 #[serde(rename = "readOnly", default)]
11316 read_only: bool,
11317}
11318
11319pub(crate) fn host_path_from_runtime_guest_mappings(
11320 runtime_env: &BTreeMap<String, String>,
11321 guest_path: &str,
11322) -> Option<PathBuf> {
11323 let mappings = runtime_env
11324 .get("AGENTOS_GUEST_PATH_MAPPINGS")
11325 .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
11326 let normalized = normalize_path(guest_path);
11327
11328 let mut sorted_mappings = mappings
11329 .into_iter()
11330 .filter_map(|mapping| {
11331 (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
11332 normalize_path(&mapping.guest_path),
11333 PathBuf::from(mapping.host_path),
11334 ))
11335 })
11336 .collect::<Vec<_>>();
11337 sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.0.len()));
11338
11339 for (guest_root, mut host_root) in sorted_mappings {
11340 if guest_root != "/"
11341 && normalized != guest_root
11342 && !normalized.starts_with(&format!("{guest_root}/"))
11343 {
11344 continue;
11345 }
11346 if guest_root == "/" && !normalized.starts_with('/') {
11347 continue;
11348 }
11349
11350 if host_root.is_relative() {
11351 host_root = std::env::current_dir().ok()?.join(host_root);
11352 }
11353
11354 let suffix = if guest_root == "/" {
11355 normalized.trim_start_matches('/')
11356 } else {
11357 normalized
11358 .strip_prefix(&guest_root)
11359 .unwrap_or_default()
11360 .trim_start_matches('/')
11361 };
11362 if !suffix.is_empty() {
11363 host_root.push(suffix);
11364 }
11365 return Some(host_root);
11366 }
11367
11368 None
11369}
11370
11371fn guest_runtime_path_for_host_path(
11372 runtime_env: &BTreeMap<String, String>,
11373 virtual_home: &str,
11374 cwd: &Path,
11375 host_path: &str,
11376) -> Option<String> {
11377 let resolved = if host_path.starts_with("file://") {
11378 PathBuf::from(host_path.trim_start_matches("file://"))
11379 } else if host_path.starts_with("file:") {
11380 PathBuf::from(host_path.trim_start_matches("file:"))
11381 } else {
11382 let candidate = PathBuf::from(host_path);
11383 if candidate.is_absolute() {
11384 candidate
11385 } else if host_path.starts_with("./") || host_path.starts_with("../") {
11386 cwd.join(candidate)
11387 } else {
11388 return None;
11389 }
11390 };
11391 let normalized = normalize_host_path(&resolved);
11392
11393 if let Some(path) = guest_path_from_runtime_host_mappings(runtime_env, &normalized) {
11394 return Some(path);
11395 }
11396
11397 let normalized_cwd = normalize_host_path(cwd);
11398 if !path_is_within_root(&normalized, &normalized_cwd) {
11399 return None;
11400 }
11401
11402 let virtual_home = if virtual_home.starts_with('/') {
11403 virtual_home.to_string()
11404 } else {
11405 String::from("/root")
11406 };
11407 let suffix = normalized
11408 .strip_prefix(&normalized_cwd)
11409 .ok()?
11410 .to_string_lossy()
11411 .replace('\\', "/")
11412 .trim_start_matches('/')
11413 .to_owned();
11414
11415 Some(if suffix.is_empty() {
11416 virtual_home
11417 } else {
11418 normalize_path(&format!("{virtual_home}/{suffix}"))
11419 })
11420}
11421
11422fn guest_path_from_runtime_host_mappings(
11423 runtime_env: &BTreeMap<String, String>,
11424 host_path: &Path,
11425) -> Option<String> {
11426 let mappings = runtime_env
11427 .get("AGENTOS_GUEST_PATH_MAPPINGS")
11428 .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
11429 let normalized = normalize_host_path(host_path);
11430
11431 let mut sorted_mappings = mappings
11432 .into_iter()
11433 .filter_map(|mapping| {
11434 (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
11435 normalize_path(&mapping.guest_path),
11436 normalize_host_path(Path::new(&mapping.host_path)),
11437 ))
11438 })
11439 .collect::<Vec<_>>();
11440 sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.1.as_os_str().len()));
11441
11442 for (guest_root, host_root) in sorted_mappings {
11443 if !path_is_within_root(&normalized, &host_root) {
11444 continue;
11445 }
11446 let suffix = normalized
11447 .strip_prefix(&host_root)
11448 .ok()?
11449 .to_string_lossy()
11450 .replace('\\', "/")
11451 .trim_start_matches('/')
11452 .to_owned();
11453
11454 return Some(if suffix.is_empty() {
11455 guest_root
11456 } else if guest_root == "/" {
11457 normalize_path(&format!("/{suffix}"))
11458 } else {
11459 normalize_path(&format!("{guest_root}/{suffix}"))
11460 });
11461 }
11462
11463 None
11464}
11465
11466fn host_mount_path_for_guest_path_from_mounts(
11467 mounts: &[crate::protocol::MountDescriptor],
11468 guest_path: &str,
11469) -> Option<PathBuf> {
11470 let normalized = normalize_path(guest_path);
11471
11472 let mut host_mounts = mounts
11473 .iter()
11474 .filter_map(|mount| {
11475 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11476 .then(|| {
11477 mount_config_host_path(&mount.plugin.config)
11478 .map(|host_path| (mount.guest_path.as_str(), host_path))
11479 })
11480 .flatten()
11481 })
11482 .collect::<Vec<_>>();
11483 host_mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
11484
11485 for (guest_root, host_root) in host_mounts {
11486 if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
11487 continue;
11488 }
11489
11490 let suffix = normalized
11491 .strip_prefix(guest_root)
11492 .unwrap_or_default()
11493 .trim_start_matches('/');
11494 let mut path = PathBuf::from(host_root);
11495 if !suffix.is_empty() {
11496 path.push(suffix);
11497 }
11498 return Some(path);
11499 }
11500
11501 None
11502}
11503
11504#[cfg(test)]
11505mod host_mount_path_for_guest_path_from_mounts_tests {
11506 use super::host_mount_path_for_guest_path_from_mounts;
11507 use crate::protocol::{MountDescriptor, MountPluginDescriptor};
11508 use serde_json::json;
11509 use std::path::PathBuf;
11510
11511 #[test]
11512 fn resolves_module_access_mount_paths() {
11513 let mounts = vec![MountDescriptor {
11514 guest_path: String::from("/root/node_modules"),
11515 read_only: true,
11516 plugin: MountPluginDescriptor {
11517 id: String::from("module_access"),
11518 config: json!({
11519 "hostPath": "/tmp/workspace/node_modules",
11520 })
11521 .to_string(),
11522 },
11523 }];
11524
11525 let resolved =
11526 host_mount_path_for_guest_path_from_mounts(&mounts, "/root/node_modules/pkg/index.js")
11527 .expect("module_access mount should resolve");
11528
11529 assert_eq!(
11530 resolved,
11531 PathBuf::from("/tmp/workspace/node_modules/pkg/index.js")
11532 );
11533 }
11534}
11535
11536fn resolve_guest_socket_host_path(
11537 context: &JavascriptSocketPathContext,
11538 guest_path: &str,
11539) -> PathBuf {
11540 if let Some(path) = host_mount_path_for_guest_path_from_mounts(&context.mounts, guest_path) {
11541 return path;
11542 }
11543
11544 let normalized = normalize_path(guest_path);
11545 let mut host_path = context.sandbox_root.clone();
11546 let suffix = normalized.trim_start_matches('/');
11547 if !suffix.is_empty() {
11548 host_path.push(suffix);
11549 }
11550 host_path
11551}
11552
11553fn ensure_kernel_parent_directories(
11554 kernel: &mut SidecarKernel,
11555 path: &str,
11556) -> Result<(), SidecarError> {
11557 let parent = dirname(path);
11558 if parent != "/" && !kernel.exists(&parent).map_err(kernel_error)? {
11559 kernel.mkdir(&parent, true).map_err(kernel_error)?;
11560 }
11561 Ok(())
11562}
11563
11564pub(crate) fn sanitize_javascript_child_process_internal_bootstrap_env(
11568 env: &BTreeMap<String, String>,
11569) -> BTreeMap<String, String> {
11570 const ALLOWED_KEYS: &[&str] = &[
11571 "AGENTOS_ALLOWED_NODE_BUILTINS",
11572 "AGENTOS_GUEST_PATH_MAPPINGS",
11573 "AGENTOS_LOOPBACK_EXEMPT_PORTS",
11574 "AGENTOS_VIRTUAL_PROCESS_EXEC_PATH",
11575 "AGENTOS_VIRTUAL_PROCESS_UID",
11576 "AGENTOS_VIRTUAL_PROCESS_GID",
11577 "AGENTOS_VIRTUAL_PROCESS_VERSION",
11578 ];
11579
11580 env.iter()
11581 .filter(|(key, _)| {
11582 ALLOWED_KEYS.contains(&key.as_str()) || key.starts_with("AGENTOS_VIRTUAL_OS_")
11583 })
11584 .map(|(key, value)| (key.clone(), value.clone()))
11585 .collect()
11586}
11587
11588fn resolve_tcp_bind_addr(host: &str, port: u16) -> Result<SocketAddr, SidecarError> {
11593 (host, port)
11594 .to_socket_addrs()
11595 .map_err(sidecar_net_error)?
11596 .next()
11597 .ok_or_else(|| {
11598 SidecarError::Execution(format!("failed to resolve TCP bind address {host}:{port}"))
11599 })
11600}
11601
11602pub(crate) fn format_dns_resource(hostname: &str) -> String {
11603 format!("dns://{hostname}")
11604}
11605
11606pub(crate) fn format_tcp_resource(host: &str, port: u16) -> String {
11607 format!("tcp://{host}:{port}")
11608}
11609
11610fn is_loopback_ip(ip: IpAddr) -> bool {
11611 match ip {
11612 IpAddr::V4(ip) => ip.is_loopback(),
11613 IpAddr::V6(ip) => {
11614 ip.is_loopback()
11615 || ip
11616 .to_ipv4_mapped()
11617 .is_some_and(|mapped| mapped.is_loopback())
11618 }
11619 }
11620}
11621
11622fn loopback_cidr(ip: IpAddr) -> &'static str {
11623 match ip {
11624 IpAddr::V4(ip) if ip.is_loopback() => "127.0.0.0/8",
11625 IpAddr::V6(ip)
11626 if ip
11627 .to_ipv4_mapped()
11628 .is_some_and(|mapped| mapped.is_loopback()) =>
11629 {
11630 "127.0.0.0/8"
11631 }
11632 IpAddr::V6(_) => "::1/128",
11633 IpAddr::V4(_) => "127.0.0.0/8",
11634 }
11635}
11636
11637fn ipv4_compatible_embedded(ip: Ipv6Addr) -> Option<Ipv4Addr> {
11643 let segments = ip.segments();
11644 if segments[0..6].iter().any(|&s| s != 0) {
11645 return None;
11646 }
11647 let embedded = (u32::from(segments[6]) << 16) | u32::from(segments[7]);
11648 if embedded == 0 || embedded == 1 {
11651 return None;
11652 }
11653 Some(Ipv4Addr::from(embedded))
11654}
11655
11656fn restricted_non_loopback_ip_range(ip: IpAddr) -> Option<(&'static str, &'static str)> {
11657 match ip {
11658 IpAddr::V4(ip) => {
11659 if ip.is_unspecified() {
11660 return Some(("0.0.0.0/32", "unspecified"));
11663 }
11664 let [first, second, ..] = ip.octets();
11665 match (first, second) {
11666 (10, _) => Some(("10.0.0.0/8", "private")),
11667 (100, 64..=127) => Some(("100.64.0.0/10", "carrier-grade-nat")),
11668 (172, 16..=31) => Some(("172.16.0.0/12", "private")),
11669 (192, 168) => Some(("192.168.0.0/16", "private")),
11670 (169, 254) => Some(("169.254.0.0/16", "link-local")),
11671 (224..=239, _) => Some(("224.0.0.0/4", "multicast")),
11676 (240..=255, _) => Some(("240.0.0.0/4", "reserved")),
11677 _ => None,
11678 }
11679 }
11680 IpAddr::V6(ip) => {
11681 if let Some(mapped) = ip.to_ipv4_mapped() {
11682 return restricted_non_loopback_ip_range(IpAddr::V4(mapped));
11683 }
11684 if let Some(compat) = ipv4_compatible_embedded(ip) {
11691 return restricted_non_loopback_ip_range(IpAddr::V4(compat));
11692 }
11693
11694 if ip.is_unspecified() {
11695 return Some(("::/128", "unspecified"));
11698 }
11699
11700 let segments = ip.segments();
11701 if (segments[0] & 0xfe00) == 0xfc00 {
11702 return Some(("fc00::/7", "unique-local"));
11703 }
11704 if (segments[0] & 0xffc0) == 0xfe80 {
11705 return Some(("fe80::/10", "link-local"));
11706 }
11707 None
11708 }
11709 }
11710}
11711
11712fn blocked_dns_resolution_error(
11713 resource: &str,
11714 ip: IpAddr,
11715 cidr: &str,
11716 label: &str,
11717) -> SidecarError {
11718 SidecarError::Execution(format!(
11719 "EACCES: blocked outbound network access to {resource}: {ip} is within restricted {label} range {cidr}"
11720 ))
11721}
11722
11723fn blocked_loopback_connect_error(resource: &str, ip: IpAddr, port: u16) -> SidecarError {
11724 SidecarError::Execution(format!(
11725 "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}",
11726 loopback_cidr(ip)
11727 ))
11728}
11729
11730fn filter_dns_safe_ip_addrs(
11731 addresses: Vec<IpAddr>,
11732 hostname: &str,
11733) -> Result<Vec<IpAddr>, SidecarError> {
11734 let resource = format_dns_resource(hostname);
11735 let mut allowed = Vec::new();
11736 let mut blocked = None;
11737
11738 for ip in addresses {
11739 if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
11740 blocked.get_or_insert((ip, cidr, label));
11741 continue;
11742 }
11743 allowed.push(ip);
11744 }
11745
11746 if allowed.is_empty() {
11747 let (ip, cidr, label) = blocked.expect("blocked DNS results should capture a reason");
11748 return Err(blocked_dns_resolution_error(&resource, ip, cidr, label));
11749 }
11750
11751 Ok(allowed)
11752}
11753
11754fn loopback_connect_allowed(context: &JavascriptSocketPathContext, port: u16) -> bool {
11755 context.loopback_port_allowed(port)
11756}
11757
11758fn filter_tcp_connect_ip_addrs(
11759 addresses: Vec<IpAddr>,
11760 host: &str,
11761 port: u16,
11762 context: &JavascriptSocketPathContext,
11763) -> Result<Vec<IpAddr>, SidecarError> {
11764 let resource = format_tcp_resource(host, port);
11765 let mut allowed = Vec::new();
11766 let mut blocked = None;
11767
11768 for ip in addresses {
11769 if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
11770 blocked.get_or_insert_with(|| blocked_dns_resolution_error(&resource, ip, cidr, label));
11771 continue;
11772 }
11773 if is_loopback_ip(ip) && !loopback_connect_allowed(context, port) {
11774 blocked.get_or_insert_with(|| blocked_loopback_connect_error(&resource, ip, port));
11775 continue;
11776 }
11777 allowed.push(ip);
11778 }
11779
11780 if allowed.is_empty() {
11781 return Err(blocked.expect("blocked TCP connect results should capture a reason"));
11782 }
11783
11784 Ok(allowed)
11785}
11786
11787fn resolve_tcp_connect_addr<B>(
11788 bridge: &SharedBridge<B>,
11789 kernel: &SidecarKernel,
11790 vm_id: &str,
11791 dns: &VmDnsConfig,
11792 host: &str,
11793 port: u16,
11794 context: &JavascriptSocketPathContext,
11795) -> Result<ResolvedTcpConnectAddr, SidecarError>
11796where
11797 B: NativeSidecarBridge + Send + 'static,
11798 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11799{
11800 let allowed = filter_tcp_connect_ip_addrs(
11801 resolve_dns_ip_addrs(
11802 bridge,
11803 kernel,
11804 vm_id,
11805 dns,
11806 host,
11807 DnsLookupPolicy::SkipPermissions,
11808 )?,
11809 host,
11810 port,
11811 context,
11812 )?;
11813 let ip = allowed
11814 .iter()
11815 .copied()
11816 .find(|candidate| {
11817 let family = JavascriptSocketFamily::from_ip(*candidate);
11818 context.translate_tcp_loopback_port(family, port).is_some()
11819 })
11820 .or_else(|| allowed.iter().copied().find(IpAddr::is_ipv4))
11823 .or_else(|| allowed.first().copied())
11824 .ok_or_else(|| {
11825 SidecarError::Execution(format!("failed to resolve TCP address {host}:{port}"))
11826 })?;
11827 let family = JavascriptSocketFamily::from_ip(ip);
11828 let translated_loopback_port = context.translate_tcp_loopback_port(family, port);
11829 let use_kernel_loopback = is_loopback_ip(ip) && translated_loopback_port == Some(port);
11830 let actual_port = if is_loopback_ip(ip) {
11831 translated_loopback_port.unwrap_or(port)
11832 } else {
11833 port
11834 };
11835 Ok(ResolvedTcpConnectAddr {
11836 actual_addr: SocketAddr::new(ip, actual_port),
11837 guest_remote_addr: SocketAddr::new(ip, port),
11838 use_kernel_loopback,
11839 })
11840}
11841
11842fn resolve_dns_ip_addrs<B>(
11843 bridge: &SharedBridge<B>,
11844 kernel: &SidecarKernel,
11845 vm_id: &str,
11846 dns: &VmDnsConfig,
11847 hostname: &str,
11848 policy: DnsLookupPolicy,
11849) -> Result<Vec<IpAddr>, SidecarError>
11850where
11851 B: NativeSidecarBridge + Send + 'static,
11852 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11853{
11854 let resolution = match kernel.resolve_dns(hostname, policy) {
11855 Ok(resolution) => resolution,
11856 Err(error) => {
11857 let sidecar_error = kernel_error(error.clone());
11858 if error.code() != "EACCES" {
11859 emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
11860 }
11861 return Err(sidecar_error);
11862 }
11863 };
11864 emit_dns_resolution_event(
11865 bridge,
11866 vm_id,
11867 hostname,
11868 resolution.source(),
11869 resolution.addresses(),
11870 dns,
11871 );
11872 Ok(resolution.addresses().to_vec())
11873}
11874
11875fn resolve_dns_records<B>(
11876 bridge: &SharedBridge<B>,
11877 kernel: &SidecarKernel,
11878 vm_id: &str,
11879 dns: &VmDnsConfig,
11880 hostname: &str,
11881 record_type: RecordType,
11882 policy: DnsLookupPolicy,
11883) -> Result<DnsRecordResolution, SidecarError>
11884where
11885 B: NativeSidecarBridge + Send + 'static,
11886 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11887{
11888 let resolution = match kernel.resolve_dns_records(hostname, record_type, policy) {
11889 Ok(resolution) => resolution,
11890 Err(error) => {
11891 let sidecar_error = kernel_error(error.clone());
11892 if error.code() != "EACCES" {
11893 emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
11894 }
11895 return Err(sidecar_error);
11896 }
11897 };
11898 emit_dns_record_resolution_event(bridge, vm_id, hostname, &resolution, dns);
11899 Ok(resolution)
11900}
11901
11902fn filter_dns_ip_addrs(
11903 addresses: Vec<IpAddr>,
11904 family: Option<u8>,
11905) -> Result<Vec<IpAddr>, SidecarError> {
11906 let filtered: Vec<_> = match family.unwrap_or(0) {
11907 0 => addresses,
11908 4 => addresses
11909 .into_iter()
11910 .filter(|ip| matches!(ip, IpAddr::V4(_)))
11911 .collect(),
11912 6 => addresses
11913 .into_iter()
11914 .filter(|ip| matches!(ip, IpAddr::V6(_)))
11915 .collect(),
11916 other => {
11917 return Err(SidecarError::InvalidState(format!(
11918 "unsupported dns family {other}"
11919 )));
11920 }
11921 };
11922
11923 if filtered.is_empty() {
11924 return Err(SidecarError::Execution(String::from(
11925 "failed to resolve DNS address for requested family",
11926 )));
11927 }
11928
11929 Ok(filtered)
11930}
11931
11932fn resolve_udp_bind_addr(
11933 host: &str,
11934 port: u16,
11935 family: JavascriptUdpFamily,
11936) -> Result<SocketAddr, SidecarError> {
11937 (host, port)
11938 .to_socket_addrs()
11939 .map_err(sidecar_net_error)?
11940 .find(|addr| family.matches_addr(addr))
11941 .ok_or_else(|| {
11942 SidecarError::Execution(format!(
11943 "failed to resolve {} UDP bind address {host}:{port}",
11944 family.socket_type()
11945 ))
11946 })
11947}
11948
11949fn resolve_udp_addr<B>(request: UdpRemoteAddrRequest<'_, B>) -> Result<SocketAddr, SidecarError>
11950where
11951 B: NativeSidecarBridge + Send + 'static,
11952 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11953{
11954 let UdpRemoteAddrRequest {
11955 bridge,
11956 kernel,
11957 vm_id,
11958 dns,
11959 host,
11960 port,
11961 family,
11962 context,
11963 } = request;
11964 resolve_dns_ip_addrs(
11965 bridge,
11966 kernel,
11967 vm_id,
11968 dns,
11969 host,
11970 DnsLookupPolicy::SkipPermissions,
11971 )?
11972 .into_iter()
11973 .map(|ip| {
11974 let family_key = JavascriptSocketFamily::from_ip(ip);
11975 let actual_port = if is_loopback_ip(ip) {
11976 context
11977 .translate_udp_loopback_port(family_key, port)
11978 .unwrap_or(port)
11979 } else {
11980 port
11981 };
11982 SocketAddr::new(ip, actual_port)
11983 })
11984 .find(|addr| family.matches_addr(addr))
11985 .ok_or_else(|| {
11986 SidecarError::Execution(format!(
11987 "failed to resolve {} UDP address {host}:{port}",
11988 family.socket_type()
11989 ))
11990 })
11991}
11992
11993fn socket_addr_family(addr: &SocketAddr) -> &'static str {
11994 match addr {
11995 SocketAddr::V4(_) => "IPv4",
11996 SocketAddr::V6(_) => "IPv6",
11997 }
11998}
11999
12000fn javascript_net_timeout_value() -> Value {
12001 Value::String(String::from(JAVASCRIPT_NET_TIMEOUT_SENTINEL))
12002}
12003
12004fn javascript_net_json_string(value: Value, label: &str) -> Result<Value, SidecarError> {
12005 serde_json::to_string(&value)
12006 .map(Value::String)
12007 .map_err(|error| {
12008 SidecarError::InvalidState(format!("failed to serialize {label} payload: {error}"))
12009 })
12010}
12011
12012fn javascript_net_read_value(
12013 event: Option<JavascriptTcpSocketEvent>,
12014) -> Result<Value, SidecarError> {
12015 match event {
12016 Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(Value::String(
12017 base64::engine::general_purpose::STANDARD.encode(chunk),
12018 )),
12019 Some(JavascriptTcpSocketEvent::End | JavascriptTcpSocketEvent::Close { .. }) => {
12020 Ok(Value::Null)
12021 }
12022 Some(JavascriptTcpSocketEvent::Error { code, message }) => {
12023 let detail = code.unwrap_or_else(|| String::from("socket read"));
12024 Err(SidecarError::Execution(format!("{detail}: {message}")))
12025 }
12026 None => Ok(javascript_net_timeout_value()),
12027 }
12028}
12029
12030fn io_error_code(error: &std::io::Error) -> Option<String> {
12031 match error.raw_os_error() {
12032 Some(libc::EADDRINUSE) => Some(String::from("EADDRINUSE")),
12033 Some(libc::EADDRNOTAVAIL) => Some(String::from("EADDRNOTAVAIL")),
12034 Some(libc::ECONNREFUSED) => Some(String::from("ECONNREFUSED")),
12035 Some(libc::ECONNRESET) => Some(String::from("ECONNRESET")),
12036 Some(libc::EINVAL) => Some(String::from("EINVAL")),
12037 Some(libc::EPIPE) => Some(String::from("EPIPE")),
12038 Some(libc::ETIMEDOUT) => Some(String::from("ETIMEDOUT")),
12039 Some(libc::EHOSTUNREACH) => Some(String::from("EHOSTUNREACH")),
12040 Some(libc::ENETUNREACH) => Some(String::from("ENETUNREACH")),
12041 _ => None,
12042 }
12043}
12044
12045fn sidecar_net_error(error: std::io::Error) -> SidecarError {
12046 let message = match io_error_code(&error) {
12047 Some(code) => format!("{code}: {error}"),
12048 None => error.to_string(),
12049 };
12050 SidecarError::Execution(message)
12051}
12052
12053fn tls_provider() -> Arc<rustls::crypto::CryptoProvider> {
12054 Arc::new(aws_lc_rs::default_provider())
12055}
12056
12057fn tls_local_certificates(
12058 options: &JavascriptTlsBridgeOptions,
12059) -> Result<Vec<Vec<u8>>, SidecarError> {
12060 let Some(certificates) = options.cert.as_ref() else {
12061 return Ok(Vec::new());
12062 };
12063 tls_material_entries(certificates)
12064}
12065
12066fn tls_material_entries(material: &JavascriptTlsMaterial) -> Result<Vec<Vec<u8>>, SidecarError> {
12067 match material {
12068 JavascriptTlsMaterial::Single(entry) => tls_data_value(entry).map(|value| vec![value]),
12069 JavascriptTlsMaterial::Many(entries) => entries.iter().map(tls_data_value).collect(),
12070 }
12071}
12072
12073fn tls_data_value(value: &JavascriptTlsDataValue) -> Result<Vec<u8>, SidecarError> {
12074 match value {
12075 JavascriptTlsDataValue::Buffer { data } => base64::engine::general_purpose::STANDARD
12076 .decode(data)
12077 .map_err(|error| {
12078 SidecarError::InvalidState(format!("TLS material contains invalid base64: {error}"))
12079 }),
12080 JavascriptTlsDataValue::String { data } => Ok(data.as_bytes().to_vec()),
12081 }
12082}
12083
12084fn tls_certificates_from_material(
12085 material: &JavascriptTlsMaterial,
12086) -> Result<Vec<CertificateDer<'static>>, SidecarError> {
12087 let mut certificates = Vec::new();
12088 for entry in tls_material_entries(material)? {
12089 let mut reader = std::io::BufReader::new(Cursor::new(entry.clone()));
12090 let parsed = rustls_pemfile::certs(&mut reader)
12091 .collect::<Result<Vec<_>, _>>()
12092 .map_err(sidecar_net_error)?;
12093 if parsed.is_empty() {
12094 certificates.push(CertificateDer::from(entry));
12095 } else {
12096 certificates.extend(parsed);
12097 }
12098 }
12099 if certificates.is_empty() {
12100 return Err(SidecarError::InvalidState(String::from(
12101 "TLS certificate material did not contain any certificates",
12102 )));
12103 }
12104 Ok(certificates)
12105}
12106
12107fn tls_private_key_from_material(
12108 material: &JavascriptTlsMaterial,
12109) -> Result<PrivateKeyDer<'static>, SidecarError> {
12110 for entry in tls_material_entries(material)? {
12111 let mut reader = std::io::BufReader::new(Cursor::new(entry));
12112 if let Some(key) = rustls_pemfile::private_key(&mut reader).map_err(sidecar_net_error)? {
12113 return Ok(key);
12114 }
12115 }
12116 Err(SidecarError::InvalidState(String::from(
12117 "TLS private key material did not contain a supported key",
12118 )))
12119}
12120
12121fn tls_root_store(options: &JavascriptTlsBridgeOptions) -> Result<RootCertStore, SidecarError> {
12122 let mut roots = RootCertStore::empty();
12123 if let Some(ca) = options.ca.as_ref() {
12124 for certificate in tls_certificates_from_material(ca)? {
12125 roots.add(certificate).map_err(|error| {
12126 SidecarError::InvalidState(format!("failed to add TLS CA certificate: {error}"))
12127 })?;
12128 }
12129 return Ok(roots);
12130 }
12131
12132 for certificate in rustls_native_certs::load_native_certs().certs {
12133 roots.add(certificate).map_err(|error| {
12134 SidecarError::InvalidState(format!(
12135 "failed to add native TLS certificate to root store: {error}"
12136 ))
12137 })?;
12138 }
12139 Ok(roots)
12140}
12141
12142fn build_client_tls_stream(
12143 stream: TcpStream,
12144 options: &JavascriptTlsBridgeOptions,
12145) -> Result<rustls::StreamOwned<ClientConnection, TcpStream>, SidecarError> {
12146 let config = build_client_tls_config(options)?;
12147 let server_name = options
12148 .servername
12149 .clone()
12150 .unwrap_or_else(|| String::from("localhost"));
12151 let server_name = ServerName::try_from(server_name)
12152 .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
12153 stream
12154 .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12155 .map_err(sidecar_net_error)?;
12156 stream
12157 .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12158 .map_err(sidecar_net_error)?;
12159 let mut tls_stream = rustls::StreamOwned::new(
12160 ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
12161 SidecarError::Execution(format!("failed to start TLS client: {error}"))
12162 })?,
12163 stream,
12164 );
12165 while tls_stream.conn.is_handshaking() {
12166 tls_stream
12167 .conn
12168 .complete_io(&mut tls_stream.sock)
12169 .map_err(sidecar_net_error)?;
12170 }
12171 tls_stream
12172 .sock
12173 .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
12174 .map_err(sidecar_net_error)?;
12175 tls_stream
12176 .sock
12177 .set_write_timeout(None)
12178 .map_err(sidecar_net_error)?;
12179 Ok(tls_stream)
12180}
12181
12182fn build_client_loopback_tls_stream(
12183 transport: crate::state::LoopbackTlsEndpoint,
12184 options: &JavascriptTlsBridgeOptions,
12185) -> Result<rustls::StreamOwned<ClientConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
12186{
12187 let config = build_client_tls_config(options)?;
12188 let server_name = options
12189 .servername
12190 .clone()
12191 .unwrap_or_else(|| String::from("localhost"));
12192 let server_name = ServerName::try_from(server_name)
12193 .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
12194 let mut tls_stream = rustls::StreamOwned::new(
12195 ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
12196 SidecarError::Execution(format!("failed to start TLS client: {error}"))
12197 })?,
12198 transport,
12199 );
12200 match tls_stream.conn.complete_io(&mut tls_stream.sock) {
12201 Ok(_) => {}
12202 Err(error)
12203 if matches!(
12204 error.kind(),
12205 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12206 ) => {}
12207 Err(error) => return Err(sidecar_net_error(error)),
12208 }
12209 Ok(tls_stream)
12210}
12211
12212fn build_client_tls_config(
12213 options: &JavascriptTlsBridgeOptions,
12214) -> Result<ClientConfig, SidecarError> {
12215 let provider = tls_provider();
12216 let builder = ClientConfig::builder_with_provider(provider.clone())
12217 .with_safe_default_protocol_versions()
12218 .map_err(|error| {
12219 SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
12220 })?;
12221
12222 let mut config = if options.reject_unauthorized == Some(false) {
12223 let verifier = Arc::new(InsecureTlsVerifier {
12224 supported_schemes: provider
12225 .signature_verification_algorithms
12226 .supported_schemes(),
12227 });
12228 builder
12229 .dangerous()
12230 .with_custom_certificate_verifier(verifier)
12231 .with_no_client_auth()
12232 } else {
12233 builder
12234 .with_root_certificates(tls_root_store(options)?)
12235 .with_no_client_auth()
12236 };
12237
12238 if let Some(protocols) = options.alpn_protocols.as_ref() {
12239 config.alpn_protocols = protocols
12240 .iter()
12241 .map(|protocol| protocol.as_bytes().to_vec())
12242 .collect();
12243 }
12244 Ok(config)
12245}
12246
12247fn build_server_tls_stream(
12248 stream: TcpStream,
12249 options: &JavascriptTlsBridgeOptions,
12250) -> Result<rustls::StreamOwned<ServerConnection, TcpStream>, SidecarError> {
12251 let config = build_server_tls_config(options)?;
12252 stream
12253 .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12254 .map_err(sidecar_net_error)?;
12255 stream
12256 .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12257 .map_err(sidecar_net_error)?;
12258 let mut tls_stream = rustls::StreamOwned::new(
12259 ServerConnection::new(Arc::new(config)).map_err(|error| {
12260 SidecarError::Execution(format!("failed to start TLS server: {error}"))
12261 })?,
12262 stream,
12263 );
12264 while tls_stream.conn.is_handshaking() {
12265 tls_stream
12266 .conn
12267 .complete_io(&mut tls_stream.sock)
12268 .map_err(sidecar_net_error)?;
12269 }
12270 tls_stream
12271 .sock
12272 .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
12273 .map_err(sidecar_net_error)?;
12274 tls_stream
12275 .sock
12276 .set_write_timeout(None)
12277 .map_err(sidecar_net_error)?;
12278 Ok(tls_stream)
12279}
12280
12281fn build_server_loopback_tls_stream(
12282 transport: crate::state::LoopbackTlsEndpoint,
12283 options: &JavascriptTlsBridgeOptions,
12284) -> Result<rustls::StreamOwned<ServerConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
12285{
12286 let config = build_server_tls_config(options)?;
12287 Ok(rustls::StreamOwned::new(
12288 ServerConnection::new(Arc::new(config)).map_err(|error| {
12289 SidecarError::Execution(format!("failed to start TLS server: {error}"))
12290 })?,
12291 transport,
12292 ))
12293}
12294
12295fn build_server_tls_config(
12296 options: &JavascriptTlsBridgeOptions,
12297) -> Result<ServerConfig, SidecarError> {
12298 let certificates = tls_certificates_from_material(options.cert.as_ref().ok_or_else(|| {
12299 SidecarError::InvalidState(String::from("TLS server upgrade requires a certificate"))
12300 })?)?;
12301 let key = tls_private_key_from_material(options.key.as_ref().ok_or_else(|| {
12302 SidecarError::InvalidState(String::from("TLS server upgrade requires a private key"))
12303 })?)?;
12304
12305 let mut config = ServerConfig::builder_with_provider(tls_provider())
12306 .with_safe_default_protocol_versions()
12307 .map_err(|error| {
12308 SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
12309 })?
12310 .with_no_client_auth()
12311 .with_single_cert(certificates, key)
12312 .map_err(|error| {
12313 SidecarError::InvalidState(format!("invalid TLS server config: {error}"))
12314 })?;
12315
12316 if let Some(protocols) = options.alpn_protocols.as_ref() {
12317 config.alpn_protocols = protocols
12318 .iter()
12319 .map(|protocol| protocol.as_bytes().to_vec())
12320 .collect();
12321 }
12322 Ok(config)
12323}
12324
12325fn tls_protocol_name(version: rustls::ProtocolVersion) -> String {
12326 match version {
12327 rustls::ProtocolVersion::TLSv1_2 => String::from("TLSv1.2"),
12328 rustls::ProtocolVersion::TLSv1_3 => String::from("TLSv1.3"),
12329 other => other
12330 .as_str()
12331 .map(str::to_owned)
12332 .unwrap_or_else(|| format!("{other:?}")),
12333 }
12334}
12335
12336fn tls_cipher_bridge_value(suite: rustls::SupportedCipherSuite) -> Value {
12337 tls_bridge_object(vec![
12338 (
12339 "name",
12340 suite
12341 .suite()
12342 .as_str()
12343 .map(|value| Value::String(value.to_owned()))
12344 .unwrap_or(Value::Null),
12345 ),
12346 (
12347 "standardName",
12348 suite
12349 .suite()
12350 .as_str()
12351 .map(|value| Value::String(value.to_owned()))
12352 .unwrap_or(Value::Null),
12353 ),
12354 (
12355 "version",
12356 Value::String(if suite.tls13().is_some() {
12357 String::from("TLSv1.3")
12358 } else {
12359 String::from("TLSv1.2")
12360 }),
12361 ),
12362 ])
12363}
12364
12365fn tls_certificate_bridge_value(certificate: &[u8], detailed: bool) -> Value {
12366 let mut fields = vec![("raw", tls_bridge_buffer_value(certificate))];
12367 if detailed {
12368 fields.push(("issuerCertificate", tls_bridge_undefined_value()));
12369 }
12370 tls_bridge_object(fields)
12371}
12372
12373fn tls_bridge_buffer_value(bytes: &[u8]) -> Value {
12374 json!({
12375 "type": "buffer",
12376 "data": base64::engine::general_purpose::STANDARD.encode(bytes),
12377 })
12378}
12379
12380fn tls_bridge_object(entries: Vec<(&str, Value)>) -> Value {
12381 let value = entries
12382 .into_iter()
12383 .map(|(key, value)| (key.to_owned(), value))
12384 .collect::<serde_json::Map<String, Value>>();
12385 json!({
12386 "type": "object",
12387 "id": 1,
12388 "value": value,
12389 })
12390}
12391
12392fn tls_bridge_undefined_value() -> Value {
12393 json!({
12394 "type": "undefined",
12395 })
12396}
12397
12398fn spawn_tcp_socket_reader(
12399 stream: TcpStream,
12400 sender: Sender<JavascriptTcpSocketEvent>,
12401 tls_mode: Arc<AtomicBool>,
12402 saw_local_shutdown: Arc<AtomicBool>,
12403 saw_remote_end: Arc<AtomicBool>,
12404 close_notified: Arc<AtomicBool>,
12405) {
12406 thread::spawn(move || {
12407 let mut stream = stream;
12408 let mut buffer = vec![0_u8; 64 * 1024];
12409 loop {
12410 if tls_mode.load(Ordering::SeqCst) {
12411 break;
12412 }
12413 match stream.read(&mut buffer) {
12414 Ok(0) => {
12415 saw_remote_end.store(true, Ordering::SeqCst);
12416 let _ = sender.send(JavascriptTcpSocketEvent::End);
12417 if saw_local_shutdown.load(Ordering::SeqCst)
12418 && !close_notified.swap(true, Ordering::SeqCst)
12419 {
12420 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12421 }
12422 break;
12423 }
12424 Ok(bytes_read) => {
12425 if sender
12426 .send(JavascriptTcpSocketEvent::Data(
12427 buffer[..bytes_read].to_vec(),
12428 ))
12429 .is_err()
12430 {
12431 break;
12432 }
12433 }
12434 Err(error)
12435 if matches!(
12436 error.kind(),
12437 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12438 ) =>
12439 {
12440 continue;
12441 }
12442 Err(error) => {
12443 let code = io_error_code(&error);
12444 let _ = sender.send(JavascriptTcpSocketEvent::Error {
12445 code,
12446 message: error.to_string(),
12447 });
12448 if !close_notified.swap(true, Ordering::SeqCst) {
12449 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12450 }
12451 break;
12452 }
12453 }
12454 }
12455 });
12456}
12457
12458fn spawn_tls_socket_reader(
12459 tls_stream: Arc<Mutex<Option<ActiveTlsStream>>>,
12460 sender: Sender<JavascriptTcpSocketEvent>,
12461 saw_local_shutdown: Arc<AtomicBool>,
12462 saw_remote_end: Arc<AtomicBool>,
12463 close_notified: Arc<AtomicBool>,
12464) {
12465 thread::spawn(move || {
12466 let mut buffer = vec![0_u8; 64 * 1024];
12467 loop {
12468 let read_result = {
12469 let mut guard = match tls_stream.lock() {
12470 Ok(guard) => guard,
12471 Err(_) => return,
12472 };
12473 let Some(stream) = guard.as_mut() else {
12474 return;
12475 };
12476 stream.read(&mut buffer)
12477 };
12478
12479 match read_result {
12480 Ok(0) => {
12481 saw_remote_end.store(true, Ordering::SeqCst);
12482 let _ = sender.send(JavascriptTcpSocketEvent::End);
12483 if saw_local_shutdown.load(Ordering::SeqCst)
12484 && !close_notified.swap(true, Ordering::SeqCst)
12485 {
12486 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12487 }
12488 break;
12489 }
12490 Ok(bytes_read) => {
12491 if sender
12492 .send(JavascriptTcpSocketEvent::Data(
12493 buffer[..bytes_read].to_vec(),
12494 ))
12495 .is_err()
12496 {
12497 break;
12498 }
12499 }
12500 Err(error)
12501 if matches!(
12502 error.kind(),
12503 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12504 ) =>
12505 {
12506 std::thread::sleep(Duration::from_millis(1));
12509 continue;
12510 }
12511 Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
12512 saw_remote_end.store(true, Ordering::SeqCst);
12513 let _ = sender.send(JavascriptTcpSocketEvent::End);
12514 if saw_local_shutdown.load(Ordering::SeqCst)
12515 && !close_notified.swap(true, Ordering::SeqCst)
12516 {
12517 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12518 }
12519 break;
12520 }
12521 Err(error) => {
12522 let code = io_error_code(&error);
12523 let _ = sender.send(JavascriptTcpSocketEvent::Error {
12524 code,
12525 message: error.to_string(),
12526 });
12527 if !close_notified.swap(true, Ordering::SeqCst) {
12528 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12529 }
12530 break;
12531 }
12532 }
12533 }
12534 });
12535}
12536
12537fn spawn_unix_socket_reader(
12538 stream: UnixStream,
12539 sender: Sender<JavascriptTcpSocketEvent>,
12540 saw_local_shutdown: Arc<AtomicBool>,
12541 saw_remote_end: Arc<AtomicBool>,
12542 close_notified: Arc<AtomicBool>,
12543) {
12544 thread::spawn(move || {
12545 let mut stream = stream;
12546 let mut buffer = vec![0_u8; 64 * 1024];
12547 loop {
12548 match stream.read(&mut buffer) {
12549 Ok(0) => {
12550 saw_remote_end.store(true, Ordering::SeqCst);
12551 let _ = sender.send(JavascriptTcpSocketEvent::End);
12552 if saw_local_shutdown.load(Ordering::SeqCst)
12553 && !close_notified.swap(true, Ordering::SeqCst)
12554 {
12555 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12556 }
12557 break;
12558 }
12559 Ok(bytes_read) => {
12560 if sender
12561 .send(JavascriptTcpSocketEvent::Data(
12562 buffer[..bytes_read].to_vec(),
12563 ))
12564 .is_err()
12565 {
12566 break;
12567 }
12568 }
12569 Err(error) => {
12570 let code = io_error_code(&error);
12571 let _ = sender.send(JavascriptTcpSocketEvent::Error {
12572 code,
12573 message: error.to_string(),
12574 });
12575 if !close_notified.swap(true, Ordering::SeqCst) {
12576 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12577 }
12578 break;
12579 }
12580 }
12581 }
12582 });
12583}
12584
12585fn terminate_child_process_tree(kernel: &mut SidecarKernel, process: &mut ActiveProcess) {
12586 let sqlite_database_ids = process.sqlite_databases.keys().copied().collect::<Vec<_>>();
12587 for database_id in sqlite_database_ids {
12588 let _ = close_sqlite_database(kernel, process, database_id);
12589 }
12590 process.sqlite_statements.clear();
12591 process.http_servers.clear();
12592 process.pending_http_requests.clear();
12593 if let Ok(mut http2) = process.http2.shared.lock() {
12594 let sessions = http2.sessions.values().cloned().collect::<Vec<_>>();
12595 http2.server_events.clear();
12596 http2.session_events.clear();
12597 http2.streams.clear();
12598 http2.servers.clear();
12599 http2.sessions.clear();
12600 drop(http2);
12601 for session in sessions {
12602 let (respond_to, _rx) = mpsc::channel();
12603 let _ = session.command_tx.send(Http2SessionCommand::Close {
12604 abrupt: true,
12605 respond_to,
12606 });
12607 }
12608 }
12609
12610 let listener_ids = process.tcp_listeners.keys().cloned().collect::<Vec<_>>();
12611 for listener_id in listener_ids {
12612 if let Some(listener) = process.tcp_listeners.remove(&listener_id) {
12613 let _ = listener.close(kernel, process.kernel_pid);
12614 }
12615 }
12616
12617 let sockets = process.tcp_sockets.keys().cloned().collect::<Vec<_>>();
12618 for socket_id in sockets {
12619 if let Some(socket) = process.tcp_sockets.remove(&socket_id) {
12620 let _ = socket.close(kernel, process.kernel_pid);
12621 }
12622 }
12623
12624 let unix_listener_ids = process.unix_listeners.keys().cloned().collect::<Vec<_>>();
12625 for listener_id in unix_listener_ids {
12626 if let Some(listener) = process.unix_listeners.remove(&listener_id) {
12627 let _ = listener.close();
12628 }
12629 }
12630
12631 let unix_sockets = process.unix_sockets.keys().cloned().collect::<Vec<_>>();
12632 for socket_id in unix_sockets {
12633 if let Some(socket) = process.unix_sockets.remove(&socket_id) {
12634 let _ = socket.close();
12635 }
12636 }
12637
12638 let udp_socket_ids = process.udp_sockets.keys().cloned().collect::<Vec<_>>();
12639 for socket_id in udp_socket_ids {
12640 if let Some(mut socket) = process.udp_sockets.remove(&socket_id) {
12641 socket.close(kernel, process.kernel_pid);
12642 }
12643 }
12644
12645 let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
12646 for child_id in child_ids {
12647 let Some(mut child) = process.child_processes.remove(&child_id) else {
12648 continue;
12649 };
12650 terminate_child_process_tree(kernel, &mut child);
12651 let _ = kernel.kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, SIGTERM);
12652 let _ = signal_runtime_process(child.execution.child_pid(), SIGTERM);
12653 child.kernel_handle.finish(0);
12654 let _ = kernel.wait_and_reap(child.kernel_pid);
12655 }
12656}
12657
12658fn service_javascript_sqlite_sync_rpc(
12659 kernel: &mut SidecarKernel,
12660 process: &mut ActiveProcess,
12661 request: &JavascriptSyncRpcRequest,
12662) -> Result<Value, SidecarError> {
12663 match request.method.as_str() {
12664 "sqlite.constants" => Ok(json!({})),
12665 "sqlite.open" => sqlite_open_database(kernel, process, request),
12666 "sqlite.close" => {
12667 let database_id =
12668 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.close database id")?;
12669 close_sqlite_database(kernel, process, database_id)?;
12670 Ok(Value::Null)
12671 }
12672 "sqlite.exec" => sqlite_exec_database(kernel, process, request),
12673 "sqlite.query" => sqlite_query_database(process, request),
12674 "sqlite.prepare" => sqlite_prepare_statement(process, request),
12675 "sqlite.location" => {
12676 let database_id =
12677 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.location database id")?;
12678 let database = sqlite_database(process, database_id)?;
12679 Ok(database
12680 .vm_path
12681 .as_ref()
12682 .map(|path| Value::String(path.clone()))
12683 .unwrap_or(Value::Null))
12684 }
12685 "sqlite.checkpoint" => {
12686 let database_id =
12687 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.checkpoint database id")?;
12688 let kernel_pid = process.kernel_pid;
12689 let database = sqlite_database_mut(process, database_id)?;
12690 sqlite_sync_database(kernel, kernel_pid, database)?;
12691 Ok(Value::Null)
12692 }
12693 "sqlite.statement.run" => sqlite_run_statement(kernel, process, request),
12694 "sqlite.statement.get" => sqlite_get_statement(process, request),
12695 "sqlite.statement.all" | "sqlite.statement.iterate" => {
12696 sqlite_all_statement(process, request)
12697 }
12698 "sqlite.statement.columns" => sqlite_statement_columns(process, request),
12699 "sqlite.statement.setReturnArrays" => {
12700 let statement_id = javascript_sync_rpc_arg_u64(
12701 &request.args,
12702 0,
12703 "sqlite.statement.setReturnArrays statement id",
12704 )?;
12705 let enabled = javascript_sync_rpc_arg_bool(
12706 &request.args,
12707 1,
12708 "sqlite.statement.setReturnArrays enabled",
12709 )?;
12710 sqlite_statement_mut(process, statement_id)?.return_arrays = enabled;
12711 Ok(Value::Null)
12712 }
12713 "sqlite.statement.setReadBigInts" => {
12714 let statement_id = javascript_sync_rpc_arg_u64(
12715 &request.args,
12716 0,
12717 "sqlite.statement.setReadBigInts statement id",
12718 )?;
12719 let enabled = javascript_sync_rpc_arg_bool(
12720 &request.args,
12721 1,
12722 "sqlite.statement.setReadBigInts enabled",
12723 )?;
12724 sqlite_statement_mut(process, statement_id)?.read_bigints = enabled;
12725 Ok(Value::Null)
12726 }
12727 "sqlite.statement.setAllowBareNamedParameters" => {
12728 let statement_id = javascript_sync_rpc_arg_u64(
12729 &request.args,
12730 0,
12731 "sqlite.statement.setAllowBareNamedParameters statement id",
12732 )?;
12733 let enabled = javascript_sync_rpc_arg_bool(
12734 &request.args,
12735 1,
12736 "sqlite.statement.setAllowBareNamedParameters enabled",
12737 )?;
12738 sqlite_statement_mut(process, statement_id)?.allow_bare_named_parameters = enabled;
12739 Ok(Value::Null)
12740 }
12741 "sqlite.statement.setAllowUnknownNamedParameters" => {
12742 let statement_id = javascript_sync_rpc_arg_u64(
12743 &request.args,
12744 0,
12745 "sqlite.statement.setAllowUnknownNamedParameters statement id",
12746 )?;
12747 let enabled = javascript_sync_rpc_arg_bool(
12748 &request.args,
12749 1,
12750 "sqlite.statement.setAllowUnknownNamedParameters enabled",
12751 )?;
12752 sqlite_statement_mut(process, statement_id)?.allow_unknown_named_parameters = enabled;
12753 Ok(Value::Null)
12754 }
12755 "sqlite.statement.finalize" => {
12756 let statement_id = javascript_sync_rpc_arg_u64(
12757 &request.args,
12758 0,
12759 "sqlite.statement.finalize statement id",
12760 )?;
12761 process
12762 .sqlite_statements
12763 .remove(&statement_id)
12764 .ok_or_else(|| {
12765 SidecarError::InvalidState(format!(
12766 "sqlite statement handle not found: {statement_id}"
12767 ))
12768 })?;
12769 Ok(Value::Null)
12770 }
12771 other => Err(SidecarError::InvalidState(format!(
12772 "unsupported JavaScript sqlite sync RPC method {other}"
12773 ))),
12774 }
12775}
12776
12777fn sqlite_open_database(
12778 kernel: &mut SidecarKernel,
12779 process: &mut ActiveProcess,
12780 request: &JavascriptSyncRpcRequest,
12781) -> Result<Value, SidecarError> {
12782 ensure_per_process_state_handle_capacity(process.sqlite_databases.len(), "sqlite database")?;
12783 let path = request.args.first().and_then(Value::as_str);
12784 let vm_path = path.filter(|value| !value.is_empty() && *value != ":memory:");
12785 let options = request.args.get(1);
12786 let read_only = sqlite_option_bool(options, "readOnly").unwrap_or(false);
12787 let create = sqlite_option_bool(options, "create").unwrap_or(!read_only);
12788 let timeout_ms = sqlite_option_u64(options, "timeout");
12789
12790 process.next_sqlite_database_id += 1;
12791 let database_id = process.next_sqlite_database_id;
12792
12793 let host_path = if vm_path.is_some() {
12794 Some(
12795 std::env::temp_dir()
12796 .join(format!(
12797 "secure-exec-sidecar-sqlite-{}-{database_id}",
12798 process.kernel_pid
12799 ))
12800 .join("database.sqlite"),
12801 )
12802 } else {
12803 None
12804 };
12805
12806 if let Some(host_path) = host_path.as_ref() {
12807 if let Some(parent) = host_path.parent() {
12808 fs::create_dir_all(parent).map_err(|error| {
12809 SidecarError::Io(format!(
12810 "failed to prepare sqlite temp directory {}: {error}",
12811 parent.display()
12812 ))
12813 })?;
12814 }
12815 }
12816
12817 if let (Some(vm_path), Some(host_path)) = (vm_path, host_path.as_ref()) {
12818 if kernel
12819 .exists_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
12820 .map_err(kernel_error)?
12821 {
12822 let contents = kernel
12823 .read_file_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
12824 .map_err(kernel_error)?;
12825 fs::write(host_path, contents).map_err(|error| {
12826 SidecarError::Io(format!(
12827 "failed to materialize sqlite database {}: {error}",
12828 host_path.display()
12829 ))
12830 })?;
12831 } else if read_only && !create {
12832 return Err(SidecarError::InvalidState(format!(
12833 "sqlite database does not exist: {vm_path}"
12834 )));
12835 }
12836 }
12837
12838 let target = host_path
12839 .as_ref()
12840 .map(|path| path.to_string_lossy().into_owned())
12841 .unwrap_or_else(|| String::from(":memory:"));
12842 let mut flags = if read_only {
12843 SqliteOpenFlags::SQLITE_OPEN_READ_ONLY
12844 } else {
12845 SqliteOpenFlags::SQLITE_OPEN_READ_WRITE
12846 };
12847 if create && !read_only {
12848 flags |= SqliteOpenFlags::SQLITE_OPEN_CREATE;
12849 }
12850
12851 let connection = SqliteConnection::open_with_flags(&target, flags).map_err(|error| {
12852 SidecarError::InvalidState(format!(
12853 "sqlite database open failed for {}: {error}",
12854 vm_path.unwrap_or(":memory:")
12855 ))
12856 })?;
12857 if let Some(timeout_ms) = timeout_ms {
12858 connection
12859 .busy_timeout(Duration::from_millis(timeout_ms))
12860 .map_err(sqlite_error)?;
12861 }
12862 if host_path.is_some() && !read_only {
12863 let _ = connection.pragma_update(None, "journal_mode", "WAL");
12864 }
12865
12866 process.sqlite_databases.insert(
12867 database_id,
12868 ActiveSqliteDatabase {
12869 connection,
12870 host_path,
12871 vm_path: vm_path.map(String::from),
12872 dirty: false,
12873 transaction_depth: 0,
12874 read_only,
12875 },
12876 );
12877
12878 Ok(json!(database_id))
12879}
12880
12881fn sqlite_exec_database(
12882 kernel: &mut SidecarKernel,
12883 process: &mut ActiveProcess,
12884 request: &JavascriptSyncRpcRequest,
12885) -> Result<Value, SidecarError> {
12886 let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.exec database id")?;
12887 let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.exec sql")?;
12888 let kernel_pid = process.kernel_pid;
12889 let database = sqlite_database_mut(process, database_id)?;
12890 let before = database.connection.total_changes();
12891 database
12892 .connection
12893 .execute_batch(sql)
12894 .map_err(sqlite_error)?;
12895 mark_sqlite_mutation(database, sql);
12896 sqlite_sync_database(kernel, kernel_pid, database)?;
12897 Ok(json!(database
12898 .connection
12899 .total_changes()
12900 .saturating_sub(before)))
12901}
12902
12903fn sqlite_query_database(
12904 process: &mut ActiveProcess,
12905 request: &JavascriptSyncRpcRequest,
12906) -> Result<Value, SidecarError> {
12907 let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.query database id")?;
12908 let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.query sql")?;
12909 let params = request.args.get(2);
12910 let options = request.args.get(3);
12911 let return_arrays = sqlite_option_bool(options, "returnArrays").unwrap_or(false);
12912 let read_bigints = sqlite_option_bool(options, "readBigInts").unwrap_or(false);
12913 let database = sqlite_database_mut(process, database_id)?;
12914 sqlite_query_rows(
12915 &mut database.connection,
12916 sql,
12917 params,
12918 return_arrays,
12919 read_bigints,
12920 true,
12921 false,
12922 )
12923}
12924
12925fn sqlite_prepare_statement(
12926 process: &mut ActiveProcess,
12927 request: &JavascriptSyncRpcRequest,
12928) -> Result<Value, SidecarError> {
12929 ensure_per_process_state_handle_capacity(process.sqlite_statements.len(), "sqlite statement")?;
12930 let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.prepare database id")?;
12931 let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.prepare sql")?;
12932 let _ = sqlite_database(process, database_id)?;
12933 process.next_sqlite_statement_id += 1;
12934 let statement_id = process.next_sqlite_statement_id;
12935 process.sqlite_statements.insert(
12936 statement_id,
12937 ActiveSqliteStatement {
12938 database_id,
12939 sql: sql.to_owned(),
12940 return_arrays: false,
12941 read_bigints: false,
12942 allow_bare_named_parameters: false,
12943 allow_unknown_named_parameters: false,
12944 },
12945 );
12946 Ok(json!(statement_id))
12947}
12948
12949fn sqlite_run_statement(
12950 kernel: &mut SidecarKernel,
12951 process: &mut ActiveProcess,
12952 request: &JavascriptSyncRpcRequest,
12953) -> Result<Value, SidecarError> {
12954 let statement_id =
12955 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.run statement id")?;
12956 let params = request.args.get(1);
12957 let statement_state = sqlite_statement(process, statement_id)?.clone();
12958 let kernel_pid = process.kernel_pid;
12959 let database = sqlite_database_mut(process, statement_state.database_id)?;
12960 let before = database.connection.total_changes();
12961 {
12962 let mut statement = database
12963 .connection
12964 .prepare(&statement_state.sql)
12965 .map_err(sqlite_error)?;
12966 bind_sqlite_parameters(
12967 &mut statement,
12968 params,
12969 statement_state.allow_bare_named_parameters,
12970 statement_state.allow_unknown_named_parameters,
12971 )?;
12972 statement.raw_execute().map_err(sqlite_error)?;
12973 }
12974 let changes = database.connection.total_changes().saturating_sub(before);
12975 let last_insert_rowid = database.connection.last_insert_rowid();
12976 mark_sqlite_mutation(database, &statement_state.sql);
12977 sqlite_sync_database(kernel, kernel_pid, database)?;
12978 let result = json!({
12979 "changes": changes,
12980 "lastInsertRowid": encode_sqlite_integer(last_insert_rowid, true),
12981 });
12982 Ok(result)
12983}
12984
12985fn sqlite_get_statement(
12986 process: &mut ActiveProcess,
12987 request: &JavascriptSyncRpcRequest,
12988) -> Result<Value, SidecarError> {
12989 let statement_id =
12990 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.get statement id")?;
12991 let params = request.args.get(1);
12992 let statement_state = sqlite_statement(process, statement_id)?.clone();
12993 let database = sqlite_database_mut(process, statement_state.database_id)?;
12994 let rows = sqlite_query_rows(
12995 &mut database.connection,
12996 &statement_state.sql,
12997 params,
12998 statement_state.return_arrays,
12999 statement_state.read_bigints,
13000 statement_state.allow_bare_named_parameters,
13001 statement_state.allow_unknown_named_parameters,
13002 )?;
13003 Ok(rows
13004 .as_array()
13005 .and_then(|rows| rows.first().cloned())
13006 .unwrap_or(Value::Null))
13007}
13008
13009fn sqlite_all_statement(
13010 process: &mut ActiveProcess,
13011 request: &JavascriptSyncRpcRequest,
13012) -> Result<Value, SidecarError> {
13013 let statement_id =
13014 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.all statement id")?;
13015 let params = request.args.get(1);
13016 let statement_state = sqlite_statement(process, statement_id)?.clone();
13017 let database = sqlite_database_mut(process, statement_state.database_id)?;
13018 sqlite_query_rows(
13019 &mut database.connection,
13020 &statement_state.sql,
13021 params,
13022 statement_state.return_arrays,
13023 statement_state.read_bigints,
13024 statement_state.allow_bare_named_parameters,
13025 statement_state.allow_unknown_named_parameters,
13026 )
13027}
13028
13029fn sqlite_statement_columns(
13030 process: &mut ActiveProcess,
13031 request: &JavascriptSyncRpcRequest,
13032) -> Result<Value, SidecarError> {
13033 let statement_id =
13034 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.columns statement id")?;
13035 let statement_state = sqlite_statement(process, statement_id)?.clone();
13036 let database = sqlite_database_mut(process, statement_state.database_id)?;
13037 let statement = database
13038 .connection
13039 .prepare(&statement_state.sql)
13040 .map_err(sqlite_error)?;
13041 Ok(Value::Array(
13042 statement
13043 .column_names()
13044 .iter()
13045 .map(|name| json!({ "name": name }))
13046 .collect(),
13047 ))
13048}
13049
13050fn sqlite_query_rows(
13051 connection: &mut SqliteConnection,
13052 sql: &str,
13053 params: Option<&Value>,
13054 return_arrays: bool,
13055 read_bigints: bool,
13056 allow_bare_named_parameters: bool,
13057 allow_unknown_named_parameters: bool,
13058) -> Result<Value, SidecarError> {
13059 let mut statement = connection.prepare(sql).map_err(sqlite_error)?;
13060 let column_names = statement
13061 .column_names()
13062 .iter()
13063 .map(|name| (*name).to_owned())
13064 .collect::<Vec<_>>();
13065 let column_count = statement.column_count();
13066 bind_sqlite_parameters(
13067 &mut statement,
13068 params,
13069 allow_bare_named_parameters,
13070 allow_unknown_named_parameters,
13071 )?;
13072 let mut rows = statement.raw_query();
13073 let mut encoded_rows = Vec::new();
13074 while let Some(row) = rows.next().map_err(sqlite_error)? {
13075 encoded_rows.push(encode_sqlite_row(
13076 row,
13077 &column_names,
13078 column_count,
13079 return_arrays,
13080 read_bigints,
13081 )?);
13082 }
13083 Ok(Value::Array(encoded_rows))
13084}
13085
13086fn encode_sqlite_row(
13087 row: &rusqlite::Row<'_>,
13088 column_names: &[String],
13089 column_count: usize,
13090 return_arrays: bool,
13091 read_bigints: bool,
13092) -> Result<Value, SidecarError> {
13093 if return_arrays {
13094 let mut values = Vec::with_capacity(column_count);
13095 for index in 0..column_count {
13096 values.push(encode_sqlite_value_ref(
13097 row.get_ref(index).map_err(sqlite_error)?,
13098 read_bigints,
13099 )?);
13100 }
13101 return Ok(Value::Array(values));
13102 }
13103
13104 let mut object = Map::with_capacity(column_count);
13105 for (index, name) in column_names.iter().enumerate() {
13106 object.insert(
13107 name.clone(),
13108 encode_sqlite_value_ref(row.get_ref(index).map_err(sqlite_error)?, read_bigints)?,
13109 );
13110 }
13111 Ok(Value::Object(object))
13112}
13113
13114fn encode_sqlite_value_ref(
13115 value: SqliteValueRef<'_>,
13116 read_bigints: bool,
13117) -> Result<Value, SidecarError> {
13118 Ok(match value {
13119 SqliteValueRef::Null => Value::Null,
13120 SqliteValueRef::Integer(number) => encode_sqlite_integer(number, read_bigints),
13121 SqliteValueRef::Real(number) => json!(number),
13122 SqliteValueRef::Text(text) => Value::String(String::from_utf8_lossy(text).into_owned()),
13123 SqliteValueRef::Blob(bytes) => json!({
13124 "__agentosSqliteType": "uint8array",
13125 "value": base64::engine::general_purpose::STANDARD.encode(bytes),
13126 }),
13127 })
13128}
13129
13130fn encode_sqlite_integer(number: i64, read_bigints: bool) -> Value {
13131 if read_bigints || number.abs() > SQLITE_JS_SAFE_INTEGER_MAX {
13132 json!({
13133 "__agentosSqliteType": "bigint",
13134 "value": number.to_string(),
13135 })
13136 } else {
13137 json!(number)
13138 }
13139}
13140
13141fn bind_sqlite_parameters(
13142 statement: &mut SqliteStatement<'_>,
13143 params: Option<&Value>,
13144 allow_bare_named_parameters: bool,
13145 allow_unknown_named_parameters: bool,
13146) -> Result<(), SidecarError> {
13147 let Some(params) = params else {
13148 return Ok(());
13149 };
13150 match params {
13151 Value::Null => Ok(()),
13152 Value::Array(values) => {
13153 for (index, value) in values.iter().enumerate() {
13154 statement
13155 .raw_bind_parameter(index + 1, decode_sqlite_parameter(value)?)
13156 .map_err(sqlite_error)?;
13157 }
13158 Ok(())
13159 }
13160 Value::Object(map)
13161 if map
13162 .get("__agentosSqliteType")
13163 .and_then(Value::as_str)
13164 .is_none() =>
13165 {
13166 for (key, value) in map {
13167 let index =
13168 resolve_sqlite_parameter_index(statement, key, allow_bare_named_parameters)?;
13169 let Some(index) = index else {
13170 if allow_unknown_named_parameters {
13171 continue;
13172 }
13173 return Err(SidecarError::InvalidState(format!(
13174 "sqlite named parameter not found: {key}"
13175 )));
13176 };
13177 statement
13178 .raw_bind_parameter(index, decode_sqlite_parameter(value)?)
13179 .map_err(sqlite_error)?;
13180 }
13181 Ok(())
13182 }
13183 other => statement
13184 .raw_bind_parameter(1, decode_sqlite_parameter(other)?)
13185 .map_err(sqlite_error),
13186 }
13187}
13188
13189fn resolve_sqlite_parameter_index(
13190 statement: &mut SqliteStatement<'_>,
13191 key: &str,
13192 allow_bare_named_parameters: bool,
13193) -> Result<Option<usize>, SidecarError> {
13194 let mut candidates = vec![key.to_owned()];
13195 if allow_bare_named_parameters
13196 && !key.starts_with(':')
13197 && !key.starts_with('@')
13198 && !key.starts_with('$')
13199 {
13200 candidates.push(format!(":{key}"));
13201 candidates.push(format!("@{key}"));
13202 candidates.push(format!("${key}"));
13203 }
13204 for candidate in candidates {
13205 if let Some(index) = statement
13206 .parameter_index(&candidate)
13207 .map_err(sqlite_error)?
13208 {
13209 return Ok(Some(index));
13210 }
13211 }
13212 Ok(None)
13213}
13214
13215fn decode_sqlite_parameter(value: &Value) -> Result<rusqlite::types::Value, SidecarError> {
13216 Ok(match value {
13217 Value::Null => rusqlite::types::Value::Null,
13218 Value::Bool(value) => rusqlite::types::Value::Integer(i64::from(*value)),
13219 Value::Number(value) => match (value.as_i64(), value.as_f64()) {
13220 (Some(integer), _) => rusqlite::types::Value::Integer(integer),
13221 (_, Some(real)) => rusqlite::types::Value::Real(real),
13222 _ => {
13223 return Err(SidecarError::InvalidState(String::from(
13224 "sqlite parameter number is not representable",
13225 )));
13226 }
13227 },
13228 Value::String(value) => rusqlite::types::Value::Text(value.clone()),
13229 Value::Array(_) => {
13230 return Err(SidecarError::InvalidState(String::from(
13231 "sqlite parameters do not support nested arrays",
13232 )));
13233 }
13234 Value::Object(map) => match map.get("__agentosSqliteType").and_then(Value::as_str) {
13235 Some("bigint") => rusqlite::types::Value::Integer(
13236 map.get("value")
13237 .and_then(Value::as_str)
13238 .ok_or_else(|| {
13239 SidecarError::InvalidState(String::from(
13240 "sqlite bigint parameter missing string value",
13241 ))
13242 })?
13243 .parse::<i64>()
13244 .map_err(|error| {
13245 SidecarError::InvalidState(format!(
13246 "sqlite bigint parameter is not a signed 64-bit integer: {error}"
13247 ))
13248 })?,
13249 ),
13250 Some("uint8array") => rusqlite::types::Value::Blob(
13251 base64::engine::general_purpose::STANDARD
13252 .decode(map.get("value").and_then(Value::as_str).ok_or_else(|| {
13253 SidecarError::InvalidState(String::from(
13254 "sqlite blob parameter missing base64 value",
13255 ))
13256 })?)
13257 .map_err(|error| {
13258 SidecarError::InvalidState(format!(
13259 "sqlite blob parameter contains invalid base64: {error}"
13260 ))
13261 })?,
13262 ),
13263 Some(other) => {
13264 return Err(SidecarError::InvalidState(format!(
13265 "unsupported sqlite tagged parameter type {other}"
13266 )));
13267 }
13268 None => {
13269 return Err(SidecarError::InvalidState(String::from(
13270 "sqlite named parameter objects must be passed as the top-level params object",
13271 )));
13272 }
13273 },
13274 })
13275}
13276
13277fn close_sqlite_database(
13278 kernel: &mut SidecarKernel,
13279 process: &mut ActiveProcess,
13280 database_id: u64,
13281) -> Result<(), SidecarError> {
13282 let mut database = process
13283 .sqlite_databases
13284 .remove(&database_id)
13285 .ok_or_else(|| {
13286 SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13287 })?;
13288 process
13289 .sqlite_statements
13290 .retain(|_, statement| statement.database_id != database_id);
13291 sqlite_sync_database(kernel, process.kernel_pid, &mut database)?;
13292 let host_path = database.host_path.clone();
13293 drop(database);
13294 cleanup_sqlite_host_artifacts(host_path.as_deref())?;
13295 Ok(())
13296}
13297
13298fn ensure_per_process_state_handle_capacity(len: usize, label: &str) -> Result<(), SidecarError> {
13299 if len >= MAX_PER_PROCESS_STATE_HANDLES {
13300 return Err(SidecarError::InvalidState(format!(
13301 "{label} handle limit exceeded: limit is {MAX_PER_PROCESS_STATE_HANDLES}"
13302 )));
13303 }
13304 Ok(())
13305}
13306
13307fn sqlite_sync_database(
13308 kernel: &mut SidecarKernel,
13309 kernel_pid: u32,
13310 database: &mut ActiveSqliteDatabase,
13311) -> Result<(), SidecarError> {
13312 if !database.dirty
13313 || database.transaction_depth > 0
13314 || database.read_only
13315 || database.host_path.is_none()
13316 || database.vm_path.is_none()
13317 {
13318 return Ok(());
13319 }
13320
13321 let _ = database
13322 .connection
13323 .execute_batch("PRAGMA wal_checkpoint(TRUNCATE)");
13324 let host_path = database.host_path.as_ref().expect("sqlite host path");
13325 if !host_path.exists() {
13326 return Ok(());
13327 }
13328 ensure_vm_parent_dir(
13329 kernel,
13330 kernel_pid,
13331 database.vm_path.as_deref().expect("sqlite vm path"),
13332 )?;
13333 let contents = fs::read(host_path).map_err(|error| {
13334 SidecarError::Io(format!(
13335 "failed to read sqlite temp database {}: {error}",
13336 host_path.display()
13337 ))
13338 })?;
13339 kernel
13340 .write_file_for_process(
13341 EXECUTION_DRIVER_NAME,
13342 kernel_pid,
13343 database.vm_path.as_deref().expect("sqlite vm path"),
13344 contents,
13345 None,
13346 )
13347 .map_err(kernel_error)?;
13348 database.dirty = false;
13349 Ok(())
13350}
13351
13352fn cleanup_sqlite_host_artifacts(host_path: Option<&Path>) -> Result<(), SidecarError> {
13353 let Some(host_path) = host_path else {
13354 return Ok(());
13355 };
13356 let parent = host_path.parent().map(PathBuf::from);
13357 for suffix in ["", "-wal", "-shm"] {
13358 let path = PathBuf::from(format!("{}{}", host_path.display(), suffix));
13359 if path.exists() {
13360 fs::remove_file(&path).map_err(|error| {
13361 SidecarError::Io(format!(
13362 "failed to remove sqlite temp artifact {}: {error}",
13363 path.display()
13364 ))
13365 })?;
13366 }
13367 }
13368 if let Some(parent) = parent {
13369 let _ = fs::remove_dir_all(parent);
13370 }
13371 Ok(())
13372}
13373
13374fn ensure_vm_parent_dir(
13375 kernel: &mut SidecarKernel,
13376 kernel_pid: u32,
13377 path: &str,
13378) -> Result<(), SidecarError> {
13379 let parent = dirname(path);
13380 if parent == "/" || parent == "." {
13381 return Ok(());
13382 }
13383 let mut current = String::new();
13384 for segment in parent.split('/').filter(|segment| !segment.is_empty()) {
13385 current.push('/');
13386 current.push_str(segment);
13387 if !kernel
13388 .exists_for_process(EXECUTION_DRIVER_NAME, kernel_pid, ¤t)
13389 .map_err(kernel_error)?
13390 {
13391 kernel
13392 .mkdir_for_process(EXECUTION_DRIVER_NAME, kernel_pid, ¤t, false, None)
13393 .map_err(kernel_error)?;
13394 }
13395 }
13396 Ok(())
13397}
13398
13399fn sqlite_database(
13400 process: &ActiveProcess,
13401 database_id: u64,
13402) -> Result<&ActiveSqliteDatabase, SidecarError> {
13403 process.sqlite_databases.get(&database_id).ok_or_else(|| {
13404 SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13405 })
13406}
13407
13408fn sqlite_database_mut(
13409 process: &mut ActiveProcess,
13410 database_id: u64,
13411) -> Result<&mut ActiveSqliteDatabase, SidecarError> {
13412 process
13413 .sqlite_databases
13414 .get_mut(&database_id)
13415 .ok_or_else(|| {
13416 SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13417 })
13418}
13419
13420fn sqlite_statement(
13421 process: &ActiveProcess,
13422 statement_id: u64,
13423) -> Result<&ActiveSqliteStatement, SidecarError> {
13424 process.sqlite_statements.get(&statement_id).ok_or_else(|| {
13425 SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
13426 })
13427}
13428
13429fn sqlite_statement_mut(
13430 process: &mut ActiveProcess,
13431 statement_id: u64,
13432) -> Result<&mut ActiveSqliteStatement, SidecarError> {
13433 process
13434 .sqlite_statements
13435 .get_mut(&statement_id)
13436 .ok_or_else(|| {
13437 SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
13438 })
13439}
13440
13441fn mark_sqlite_mutation(database: &mut ActiveSqliteDatabase, sql: &str) {
13442 let normalized = sql.trim_start().to_ascii_lowercase();
13443 if normalized.starts_with("begin") || normalized.starts_with("savepoint") {
13444 database.dirty = true;
13445 database.transaction_depth += 1;
13446 return;
13447 }
13448 if normalized.starts_with("commit") || normalized.starts_with("release savepoint") {
13449 database.dirty = true;
13450 database.transaction_depth = database.transaction_depth.saturating_sub(1);
13451 return;
13452 }
13453 if normalized.starts_with("rollback") && !normalized.starts_with("rollback to") {
13454 database.dirty = true;
13455 database.transaction_depth = database.transaction_depth.saturating_sub(1);
13456 return;
13457 }
13458 if normalized.starts_with("insert")
13459 || normalized.starts_with("update")
13460 || normalized.starts_with("delete")
13461 || normalized.starts_with("replace")
13462 || normalized.starts_with("create")
13463 || normalized.starts_with("alter")
13464 || normalized.starts_with("drop")
13465 || normalized.starts_with("vacuum")
13466 || normalized.starts_with("reindex")
13467 || normalized.starts_with("analyze")
13468 || normalized.starts_with("attach")
13469 || normalized.starts_with("detach")
13470 || normalized.starts_with("pragma")
13471 {
13472 database.dirty = true;
13473 }
13474}
13475
13476fn sqlite_option_bool(options: Option<&Value>, key: &str) -> Option<bool> {
13477 options
13478 .and_then(|value| value.get(key))
13479 .and_then(Value::as_bool)
13480}
13481
13482fn sqlite_option_u64(options: Option<&Value>, key: &str) -> Option<u64> {
13483 options
13484 .and_then(|value| value.get(key))
13485 .and_then(Value::as_u64)
13486}
13487
13488fn sqlite_error(error: rusqlite::Error) -> SidecarError {
13489 SidecarError::InvalidState(format!("sqlite error: {error}"))
13490}
13491
13492pub(crate) fn javascript_sync_rpc_arg_str<'a>(
13493 args: &'a [Value],
13494 index: usize,
13495 label: &str,
13496) -> Result<&'a str, SidecarError> {
13497 args.get(index)
13498 .and_then(Value::as_str)
13499 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a string argument")))
13500}
13501
13502pub(crate) fn javascript_sync_rpc_arg_bool(
13503 args: &[Value],
13504 index: usize,
13505 label: &str,
13506) -> Result<bool, SidecarError> {
13507 args.get(index)
13508 .and_then(Value::as_bool)
13509 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a boolean argument")))
13510}
13511
13512pub(crate) fn javascript_sync_rpc_encoding(args: &[Value]) -> Option<String> {
13513 args.get(1).and_then(|value| {
13514 value.as_str().map(str::to_owned).or_else(|| {
13515 value
13516 .get("encoding")
13517 .and_then(Value::as_str)
13518 .map(str::to_owned)
13519 })
13520 })
13521}
13522
13523pub(crate) fn javascript_sync_rpc_option_bool(
13524 args: &[Value],
13525 index: usize,
13526 key: &str,
13527) -> Option<bool> {
13528 let value = args.get(index)?;
13529 if key == "recursive" {
13530 if let Some(boolean) = value.as_bool() {
13531 return Some(boolean);
13532 }
13533 }
13534 value.get(key).and_then(Value::as_bool)
13535}
13536
13537pub(crate) fn javascript_sync_rpc_option_u32(
13538 args: &[Value],
13539 index: usize,
13540 key: &str,
13541) -> Result<Option<u32>, SidecarError> {
13542 let Some(value) = args.get(index).and_then(|value| {
13543 if value.is_object() {
13544 value.get(key)
13545 } else if key == "mode" && value.is_number() {
13546 Some(value)
13547 } else {
13548 None
13549 }
13550 }) else {
13551 return Ok(None);
13552 };
13553 if value.is_null() {
13554 return Ok(None);
13555 }
13556
13557 let numeric = value
13558 .as_u64()
13559 .or_else(|| {
13560 value
13561 .as_f64()
13562 .filter(|number| number.is_finite() && *number >= 0.0)
13563 .map(|number| number as u64)
13564 })
13565 .ok_or_else(|| SidecarError::InvalidState(format!("{key} must be numeric")))?;
13566
13567 u32::try_from(numeric)
13568 .map(Some)
13569 .map_err(|_| SidecarError::InvalidState(format!("{key} must fit within u32")))
13570}
13571
13572pub(crate) fn javascript_sync_rpc_arg_u32(
13573 args: &[Value],
13574 index: usize,
13575 label: &str,
13576) -> Result<u32, SidecarError> {
13577 let value = javascript_sync_rpc_arg_u64(args, index, label)?;
13578 u32::try_from(value)
13579 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
13580}
13581
13582pub(crate) fn javascript_sync_rpc_arg_i32(
13583 args: &[Value],
13584 index: usize,
13585 label: &str,
13586) -> Result<i32, SidecarError> {
13587 let Some(value) = args.get(index) else {
13588 return Err(SidecarError::InvalidState(format!("{label} is required")));
13589 };
13590
13591 let numeric = value
13592 .as_i64()
13593 .or_else(|| {
13594 value
13595 .as_f64()
13596 .filter(|number| number.is_finite())
13597 .map(|number| number as i64)
13598 })
13599 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))?;
13600
13601 i32::try_from(numeric)
13602 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within i32")))
13603}
13604
13605pub(crate) fn javascript_sync_rpc_arg_u32_optional(
13606 args: &[Value],
13607 index: usize,
13608 label: &str,
13609) -> Result<Option<u32>, SidecarError> {
13610 javascript_sync_rpc_arg_u64_optional(args, index, label)?
13611 .map(|value| {
13612 u32::try_from(value)
13613 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
13614 })
13615 .transpose()
13616}
13617
13618pub(crate) fn javascript_sync_rpc_arg_u64(
13619 args: &[Value],
13620 index: usize,
13621 label: &str,
13622) -> Result<u64, SidecarError> {
13623 let Some(value) = args.get(index) else {
13624 return Err(SidecarError::InvalidState(format!("{label} is required")));
13625 };
13626
13627 value
13628 .as_u64()
13629 .or_else(|| {
13630 value
13631 .as_f64()
13632 .filter(|number| number.is_finite() && *number >= 0.0)
13633 .map(|number| number as u64)
13634 })
13635 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))
13636}
13637
13638pub(crate) fn javascript_sync_rpc_arg_u64_optional(
13639 args: &[Value],
13640 index: usize,
13641 label: &str,
13642) -> Result<Option<u64>, SidecarError> {
13643 let Some(value) = args.get(index) else {
13644 return Ok(None);
13645 };
13646 if value.is_null() {
13647 return Ok(None);
13648 }
13649 javascript_sync_rpc_arg_u64(args, index, label).map(Some)
13650}
13651
13652pub(crate) fn javascript_sync_rpc_bytes_arg(
13653 args: &[Value],
13654 index: usize,
13655 label: &str,
13656) -> Result<Vec<u8>, SidecarError> {
13657 let Some(value) = args.get(index) else {
13658 return Err(SidecarError::InvalidState(format!("{label} is required")));
13659 };
13660
13661 if let Some(text) = value.as_str() {
13662 return Ok(text.as_bytes().to_vec());
13663 }
13664
13665 let Some(base64_value) = value
13666 .get("__agentOSType")
13667 .and_then(Value::as_str)
13668 .filter(|kind| *kind == "bytes")
13669 .and_then(|_| value.get("base64"))
13670 .and_then(Value::as_str)
13671 else {
13672 return Err(SidecarError::InvalidState(format!(
13673 "{label} must be a string or encoded bytes payload"
13674 )));
13675 };
13676
13677 base64::engine::general_purpose::STANDARD
13678 .decode(base64_value)
13679 .map_err(|error| {
13680 SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
13681 })
13682}
13683
13684pub(crate) fn javascript_sync_rpc_bytes_value(bytes: &[u8]) -> Value {
13685 json!({
13686 "__agentOSType": "bytes",
13687 "base64": base64::engine::general_purpose::STANDARD.encode(bytes),
13688 })
13689}
13690
13691#[derive(Debug, Deserialize)]
13692struct KernelPollFdRequest {
13693 fd: u32,
13694 events: u16,
13695}
13696
13697#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
13698struct KernelPollFdResponse {
13699 fd: u32,
13700 events: u16,
13701 revents: u16,
13702}
13703
13704fn javascript_sync_rpc_base64_arg(
13705 args: &[Value],
13706 index: usize,
13707 label: &str,
13708) -> Result<Vec<u8>, SidecarError> {
13709 let value = javascript_sync_rpc_arg_str(args, index, label)?;
13710 base64::engine::general_purpose::STANDARD
13711 .decode(value)
13712 .map_err(|error| {
13713 SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
13714 })
13715}
13716
13717static SYNC_RPC_STATS: std::sync::OnceLock<
13724 std::sync::Mutex<std::collections::BTreeMap<String, u64>>,
13725> = std::sync::OnceLock::new();
13726
13727fn sync_rpc_trace_enabled() -> bool {
13728 std::env::var("AGENTOS_SYNC_RPC_TRACE").as_deref() == Ok("1")
13729}
13730
13731fn record_sync_rpc(method: &str) {
13732 let stats =
13733 SYNC_RPC_STATS.get_or_init(|| std::sync::Mutex::new(std::collections::BTreeMap::new()));
13734 let Ok(mut map) = stats.lock() else {
13735 return;
13736 };
13737 *map.entry(method.to_string()).or_insert(0) += 1;
13738 let total: u64 = map.values().sum();
13739 if total == 1 || total.is_multiple_of(50) {
13740 let mut top: Vec<(&String, &u64)> = map.iter().collect();
13741 top.sort_by(|a, b| b.1.cmp(a.1));
13742 let breakdown = top
13743 .iter()
13744 .take(8)
13745 .map(|(m, c)| format!("{m}={c}"))
13746 .collect::<Vec<_>>()
13747 .join(" ");
13748 tracing::info!(target: "secure_exec_sidecar::perf", total, %breakdown, "sync_rpc count");
13749 }
13750}
13751
13752pub(crate) fn service_javascript_sync_rpc<B>(
13753 request: JavascriptSyncRpcServiceRequest<'_, B>,
13754) -> Result<Value, SidecarError>
13755where
13756 B: NativeSidecarBridge + Send + 'static,
13757 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
13758{
13759 if sync_rpc_trace_enabled() {
13760 record_sync_rpc(request.sync_request.method.as_str());
13761 }
13762 let JavascriptSyncRpcServiceRequest {
13763 bridge,
13764 vm_id,
13765 dns,
13766 socket_paths,
13767 kernel,
13768 process,
13769 sync_request: request,
13770 resource_limits,
13771 network_counts,
13772 } = request;
13773 match request.method.as_str() {
13774 "_resolveModule"
13777 | "_resolveModuleSync"
13778 | "__resolve_module"
13779 | "_batchResolveModules"
13780 | "__batch_resolve_modules"
13781 | "_loadFile"
13782 | "_loadFileSync"
13783 | "__load_file"
13784 | "_moduleFormat"
13785 | "__module_format" => service_javascript_module_sync_rpc(kernel, process, request),
13786 "_loadPolyfill" | "__load_polyfill" => {
13788 service_javascript_internal_bridge_sync_rpc(process, request)
13789 }
13790 "__kernel_stdin_read" => match &process.execution {
13791 ActiveExecution::Javascript(execution) => execution
13792 .read_kernel_stdin_sync_rpc(request)
13793 .map_err(|error| SidecarError::Execution(error.to_string())),
13794 ActiveExecution::Python(_) | ActiveExecution::Wasm(_) | ActiveExecution::Tool(_) => {
13795 service_javascript_kernel_stdin_sync_rpc(kernel, process, request)
13796 }
13797 },
13798 "__kernel_stdio_write" => {
13799 service_javascript_kernel_stdio_write_sync_rpc(kernel, process, request)
13800 }
13801 "__kernel_poll" => service_javascript_kernel_poll_sync_rpc(kernel, process, request),
13802 "__pty_set_raw_mode" => {
13803 service_javascript_pty_set_raw_mode_sync_rpc(kernel, process, request)
13804 }
13805 "crypto.hashDigest"
13806 | "crypto.hmacDigest"
13807 | "crypto.pbkdf2"
13808 | "crypto.scrypt"
13809 | "crypto.cipheriv"
13810 | "crypto.decipheriv"
13811 | "crypto.cipherivCreate"
13812 | "crypto.cipherivUpdate"
13813 | "crypto.cipherivFinal"
13814 | "crypto.sign"
13815 | "crypto.verify"
13816 | "crypto.asymmetricOp"
13817 | "crypto.createKeyObject"
13818 | "crypto.generateKeyPairSync"
13819 | "crypto.generateKeySync"
13820 | "crypto.generatePrimeSync"
13821 | "crypto.diffieHellman"
13822 | "crypto.diffieHellmanGroup"
13823 | "crypto.diffieHellmanSessionCreate"
13824 | "crypto.diffieHellmanSessionCall"
13825 | "crypto.diffieHellmanSessionDestroy"
13826 | "crypto.subtle" => service_javascript_crypto_sync_rpc(process, request),
13827 "dns.lookup" | "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
13828 service_javascript_dns_sync_rpc(bridge, kernel, vm_id, dns, request)
13829 }
13830 "net.http_listen" | "net.http_close" | "net.http_wait" | "net.http_respond" => {
13831 service_javascript_net_sync_rpc(JavascriptNetSyncRpcServiceRequest {
13832 bridge,
13833 vm_id,
13834 dns,
13835 socket_paths,
13836 kernel,
13837 process,
13838 sync_request: request,
13839 resource_limits,
13840 network_counts,
13841 })
13842 }
13843 "net.http2_server_listen"
13844 | "net.http2_server_poll"
13845 | "net.http2_server_close"
13846 | "net.http2_server_respond"
13847 | "net.http2_server_wait"
13848 | "net.http2_session_connect"
13849 | "net.http2_session_request"
13850 | "net.http2_session_settings"
13851 | "net.http2_session_set_local_window_size"
13852 | "net.http2_session_goaway"
13853 | "net.http2_session_close"
13854 | "net.http2_session_destroy"
13855 | "net.http2_session_poll"
13856 | "net.http2_session_wait"
13857 | "net.http2_stream_respond"
13858 | "net.http2_stream_push_stream"
13859 | "net.http2_stream_write"
13860 | "net.http2_stream_end"
13861 | "net.http2_stream_close"
13862 | "net.http2_stream_pause"
13863 | "net.http2_stream_resume"
13864 | "net.http2_stream_respond_with_file" => {
13865 service_javascript_http2_sync_rpc(JavascriptHttp2SyncRpcServiceRequest {
13866 bridge,
13867 kernel,
13868 vm_id,
13869 dns,
13870 socket_paths,
13871 process,
13872 sync_request: request,
13873 resource_limits,
13874 network_counts,
13875 })
13876 }
13877 "net.connect"
13878 | "net.reserve_tcp_port"
13879 | "net.release_tcp_port"
13880 | "net.listen"
13881 | "net.poll"
13882 | "net.socket_wait_connect"
13883 | "net.socket_read"
13884 | "net.socket_set_no_delay"
13885 | "net.socket_set_keep_alive"
13886 | "net.socket_upgrade_tls"
13887 | "net.socket_get_tls_client_hello"
13888 | "net.socket_tls_query"
13889 | "net.server_poll"
13890 | "net.server_accept"
13891 | "net.server_connections"
13892 | "net.upgrade_socket_write"
13893 | "net.upgrade_socket_end"
13894 | "net.upgrade_socket_destroy"
13895 | "net.write"
13896 | "net.shutdown"
13897 | "net.destroy"
13898 | "net.server_close"
13899 | "tls.get_ciphers" => {
13900 service_javascript_net_sync_rpc(JavascriptNetSyncRpcServiceRequest {
13901 bridge,
13902 vm_id,
13903 dns,
13904 socket_paths,
13905 kernel,
13906 process,
13907 sync_request: request,
13908 resource_limits,
13909 network_counts,
13910 })
13911 }
13912 "dgram.createSocket"
13913 | "dgram.bind"
13914 | "dgram.send"
13915 | "dgram.poll"
13916 | "dgram.close"
13917 | "dgram.address"
13918 | "dgram.setBufferSize"
13919 | "dgram.getBufferSize" => {
13920 service_javascript_dgram_sync_rpc(JavascriptDgramSyncRpcServiceRequest {
13921 bridge,
13922 kernel,
13923 vm_id,
13924 dns,
13925 socket_paths,
13926 process,
13927 sync_request: request,
13928 resource_limits,
13929 network_counts,
13930 })
13931 }
13932 "sqlite.constants"
13933 | "sqlite.open"
13934 | "sqlite.close"
13935 | "sqlite.exec"
13936 | "sqlite.query"
13937 | "sqlite.prepare"
13938 | "sqlite.location"
13939 | "sqlite.checkpoint"
13940 | "sqlite.statement.run"
13941 | "sqlite.statement.get"
13942 | "sqlite.statement.all"
13943 | "sqlite.statement.iterate"
13944 | "sqlite.statement.columns"
13945 | "sqlite.statement.setReturnArrays"
13946 | "sqlite.statement.setReadBigInts"
13947 | "sqlite.statement.setAllowBareNamedParameters"
13948 | "sqlite.statement.setAllowUnknownNamedParameters"
13949 | "sqlite.statement.finalize" => {
13950 service_javascript_sqlite_sync_rpc(kernel, process, request)
13951 }
13952 "process.kill" => {
13953 let target_pid =
13954 javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
13955 let signal = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
13956 let parsed_signal = parse_signal(signal)?;
13957 if parsed_signal == 0 {
13958 kernel
13959 .signal_process(EXECUTION_DRIVER_NAME, target_pid, parsed_signal)
13960 .map_err(kernel_error)?;
13961 return Ok(Value::Null);
13962 }
13963 let process_pid = i32::try_from(process.kernel_pid)
13964 .map_err(|_| SidecarError::InvalidState("process pid exceeds i32".into()))?;
13965 if target_pid != process_pid {
13966 return Err(SidecarError::InvalidState(format!(
13967 "unknown process pid {target_pid}"
13968 )));
13969 }
13970 process.pending_self_signal_exit = None;
13971 if parsed_signal != 0
13972 && !matches!(
13973 canonical_signal_name(parsed_signal),
13974 Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
13975 )
13976 {
13977 process.pending_self_signal_exit = Some(parsed_signal);
13978 }
13979 Ok(json!({
13980 "self": true,
13981 "action": "default",
13982 }))
13983 }
13984 "process.umask" => {
13985 let new_mask = javascript_sync_rpc_arg_u32_optional(&request.args, 0, "process umask")?;
13986 kernel
13987 .umask(EXECUTION_DRIVER_NAME, process.kernel_pid, new_mask)
13988 .map(|mask| json!(mask))
13989 .map_err(kernel_error)
13990 }
13991 "fs.chmodSync" | "fs.promises.chmod" => {
13992 let response =
13993 service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request)?;
13994 mirror_process_chmod_to_host(process, request)?;
13995 Ok(response)
13996 }
13997 _ => service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request),
13998 }
13999}
14000
14001fn service_javascript_internal_bridge_sync_rpc(
14002 process: &ActiveProcess,
14003 request: &JavascriptSyncRpcRequest,
14004) -> Result<Value, SidecarError> {
14005 let method = match request.method.as_str() {
14009 "_loadPolyfill" | "__load_polyfill" => "_loadPolyfill",
14010 other => {
14011 return Err(SidecarError::InvalidState(format!(
14012 "unsupported JavaScript internal bridge method {other}"
14013 )));
14014 }
14015 };
14016
14017 handle_internal_bridge_call_from_host_context(
14018 &process.host_cwd,
14019 &process.guest_cwd,
14020 &process.env,
14021 method,
14022 &request.args,
14023 )
14024 .ok_or_else(|| {
14025 SidecarError::InvalidState(format!(
14026 "JavaScript internal bridge method {method} returned no value"
14027 ))
14028 })
14029}
14030
14031fn mirror_process_chmod_to_host(
14032 process: &ActiveProcess,
14033 request: &JavascriptSyncRpcRequest,
14034) -> Result<(), SidecarError> {
14035 let guest_path = javascript_sync_rpc_arg_str(&request.args, 0, "filesystem chmod path")?;
14036 let mode = javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem chmod mode")? & 0o7777;
14037 let Some(host_path) = resolve_process_guest_path_to_host(process, guest_path) else {
14038 return Ok(());
14039 };
14040 if !host_path.exists() {
14041 return Ok(());
14042 }
14043 fs::set_permissions(&host_path, fs::Permissions::from_mode(mode)).map_err(|error| {
14044 SidecarError::Io(format!(
14045 "failed to mirror chmod to host path {}: {error}",
14046 host_path.display()
14047 ))
14048 })
14049}
14050
14051fn resolve_process_guest_path_to_host(
14052 process: &ActiveProcess,
14053 guest_path: &str,
14054) -> Option<PathBuf> {
14055 let normalized_guest_path = if guest_path.starts_with('/') {
14056 normalize_path(guest_path)
14057 } else {
14058 normalize_path(&format!(
14059 "{}/{}",
14060 process.guest_cwd.trim_end_matches('/'),
14061 guest_path
14062 ))
14063 };
14064 if let Some(host_path) =
14065 host_path_from_runtime_guest_mappings(&process.env, &normalized_guest_path)
14066 {
14067 return Some(host_path);
14068 }
14069 let normalized_guest_cwd = normalize_path(&process.guest_cwd);
14070 let mut host_root = normalize_host_path(&process.host_cwd);
14071 for _ in normalized_guest_cwd
14072 .trim_start_matches('/')
14073 .split('/')
14074 .filter(|segment| !segment.is_empty())
14075 {
14076 host_root = host_root.parent()?.to_path_buf();
14077 }
14078 if normalized_guest_path == "/" {
14079 Some(host_root)
14080 } else {
14081 Some(host_root.join(normalized_guest_path.trim_start_matches('/')))
14082 }
14083}
14084
14085pub(crate) fn service_javascript_crypto_sync_rpc(
14086 process: &mut ActiveProcess,
14087 request: &JavascriptSyncRpcRequest,
14088) -> Result<Value, SidecarError> {
14089 match request.method.as_str() {
14090 "crypto.hashDigest" => {
14091 let algorithm = javascript_crypto_digest_algorithm(
14092 &request.args,
14093 0,
14094 "crypto.hashDigest algorithm",
14095 )?;
14096 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hashDigest data")?;
14097 Ok(Value::String(
14098 base64::engine::general_purpose::STANDARD.encode(algorithm.digest(&data)),
14099 ))
14100 }
14101 "crypto.hmacDigest" => {
14102 let algorithm = javascript_crypto_digest_algorithm(
14103 &request.args,
14104 0,
14105 "crypto.hmacDigest algorithm",
14106 )?;
14107 let key = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hmacDigest key")?;
14108 let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.hmacDigest data")?;
14109 Ok(Value::String(
14110 base64::engine::general_purpose::STANDARD.encode(algorithm.hmac(&key, &data)?),
14111 ))
14112 }
14113 "crypto.pbkdf2" => {
14114 let password =
14115 javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.pbkdf2 password")?;
14116 let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.pbkdf2 salt")?;
14117 let iterations =
14118 javascript_sync_rpc_arg_u32(&request.args, 2, "crypto.pbkdf2 iterations")?;
14119 if iterations == 0 {
14120 return Err(SidecarError::InvalidState(String::from(
14121 "crypto.pbkdf2 iterations must be greater than zero",
14122 )));
14123 }
14124 let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
14125 &request.args,
14126 3,
14127 "crypto.pbkdf2 key length",
14128 )?)
14129 .map_err(|_| {
14130 SidecarError::InvalidState(String::from(
14131 "crypto.pbkdf2 key length must fit within usize",
14132 ))
14133 })?;
14134 let algorithm =
14135 javascript_crypto_digest_algorithm(&request.args, 4, "crypto.pbkdf2 digest")?;
14136 let mut output = vec![0u8; key_len];
14137 algorithm.pbkdf2(&password, &salt, iterations, &mut output);
14138 Ok(Value::String(
14139 base64::engine::general_purpose::STANDARD.encode(output),
14140 ))
14141 }
14142 "crypto.scrypt" => {
14143 let password =
14144 javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.scrypt password")?;
14145 let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.scrypt salt")?;
14146 let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
14147 &request.args,
14148 2,
14149 "crypto.scrypt key length",
14150 )?)
14151 .map_err(|_| {
14152 SidecarError::InvalidState(String::from(
14153 "crypto.scrypt key length must fit within usize",
14154 ))
14155 })?;
14156 let options_json =
14157 javascript_sync_rpc_arg_str(&request.args, 3, "crypto.scrypt options")?;
14158 let options: JavascriptScryptOptions =
14159 serde_json::from_str(options_json).map_err(|error| {
14160 SidecarError::InvalidState(format!(
14161 "crypto.scrypt options must be valid JSON: {error}"
14162 ))
14163 })?;
14164 let cost = options.cost.unwrap_or(DEFAULT_SCRYPT_COST);
14165 if cost == 0 || !cost.is_power_of_two() {
14166 return Err(SidecarError::InvalidState(String::from(
14167 "crypto.scrypt cost must be a positive power of two",
14168 )));
14169 }
14170 let log_n = u8::try_from(cost.ilog2()).map_err(|_| {
14171 SidecarError::InvalidState(String::from(
14172 "crypto.scrypt cost exceeds supported parameter range",
14173 ))
14174 })?;
14175 let params = ScryptParams::new(
14176 log_n,
14177 options.block_size.unwrap_or(DEFAULT_SCRYPT_BLOCK_SIZE),
14178 options
14179 .parallelization
14180 .unwrap_or(DEFAULT_SCRYPT_PARALLELIZATION),
14181 key_len,
14182 )
14183 .map_err(|error| {
14184 SidecarError::InvalidState(format!("crypto.scrypt options are invalid: {error}"))
14185 })?;
14186 let mut output = vec![0u8; key_len];
14187 scrypt(&password, &salt, ¶ms, &mut output).map_err(|error| {
14188 SidecarError::Execution(format!("crypto.scrypt failed: {error}"))
14189 })?;
14190 Ok(Value::String(
14191 base64::engine::general_purpose::STANDARD.encode(output),
14192 ))
14193 }
14194 "crypto.cipheriv" => service_javascript_crypto_cipheriv_sync_rpc(request),
14195 "crypto.decipheriv" => service_javascript_crypto_decipheriv_sync_rpc(request),
14196 "crypto.cipherivCreate" => {
14197 service_javascript_crypto_cipheriv_create_sync_rpc(process, request)
14198 }
14199 "crypto.cipherivUpdate" => {
14200 service_javascript_crypto_cipheriv_update_sync_rpc(process, request)
14201 }
14202 "crypto.cipherivFinal" => {
14203 service_javascript_crypto_cipheriv_final_sync_rpc(process, request)
14204 }
14205 "crypto.sign" => service_javascript_crypto_sign_sync_rpc(request),
14206 "crypto.verify" => service_javascript_crypto_verify_sync_rpc(request),
14207 "crypto.asymmetricOp" => service_javascript_crypto_asymmetric_op_sync_rpc(request),
14208 "crypto.createKeyObject" => service_javascript_crypto_create_key_object_sync_rpc(request),
14209 "crypto.generateKeyPairSync" => {
14210 service_javascript_crypto_generate_key_pair_sync_rpc(request)
14211 }
14212 "crypto.generateKeySync" => service_javascript_crypto_generate_key_sync_rpc(request),
14213 "crypto.generatePrimeSync" => service_javascript_crypto_generate_prime_sync_rpc(request),
14214 "crypto.diffieHellman" => service_javascript_crypto_diffie_hellman_sync_rpc(request),
14215 "crypto.diffieHellmanGroup" => {
14216 service_javascript_crypto_diffie_hellman_group_sync_rpc(request)
14217 }
14218 "crypto.diffieHellmanSessionCreate" => {
14219 service_javascript_crypto_diffie_hellman_session_create_sync_rpc(process, request)
14220 }
14221 "crypto.diffieHellmanSessionCall" => {
14222 service_javascript_crypto_diffie_hellman_session_call_sync_rpc(process, request)
14223 }
14224 "crypto.diffieHellmanSessionDestroy" => {
14225 service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(process, request)
14226 }
14227 "crypto.subtle" => service_javascript_crypto_subtle_sync_rpc(request),
14228 _ => Err(SidecarError::InvalidState(format!(
14229 "unsupported JavaScript crypto sync RPC method {}",
14230 request.method
14231 ))),
14232 }
14233}
14234
14235fn javascript_crypto_digest_algorithm(
14236 args: &[Value],
14237 index: usize,
14238 label: &str,
14239) -> Result<JavascriptCryptoDigestAlgorithm, SidecarError> {
14240 JavascriptCryptoDigestAlgorithm::parse(javascript_sync_rpc_arg_str(args, index, label)?)
14241}
14242
14243impl JavascriptCryptoDigestAlgorithm {
14244 fn parse(value: &str) -> Result<Self, SidecarError> {
14245 match value.trim().to_ascii_lowercase().replace('-', "").as_str() {
14246 "md5" => Ok(Self::Md5),
14247 "sha1" => Ok(Self::Sha1),
14248 "sha256" => Ok(Self::Sha256),
14249 "sha512" => Ok(Self::Sha512),
14250 _ => Err(SidecarError::InvalidState(format!(
14251 "unsupported crypto digest algorithm {value}"
14252 ))),
14253 }
14254 }
14255
14256 fn digest(self, data: &[u8]) -> Vec<u8> {
14257 match self {
14258 Self::Md5 => Md5::digest(data).to_vec(),
14259 Self::Sha1 => Sha1::digest(data).to_vec(),
14260 Self::Sha256 => Sha256::digest(data).to_vec(),
14261 Self::Sha512 => Sha512::digest(data).to_vec(),
14262 }
14263 }
14264
14265 fn hmac(self, key: &[u8], data: &[u8]) -> Result<Vec<u8>, SidecarError> {
14266 match self {
14267 Self::Md5 => {
14268 let mut mac = Hmac::<Md5>::new_from_slice(key).map_err(|error| {
14269 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14270 })?;
14271 mac.update(data);
14272 Ok(mac.finalize().into_bytes().to_vec())
14273 }
14274 Self::Sha1 => {
14275 let mut mac = Hmac::<Sha1>::new_from_slice(key).map_err(|error| {
14276 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14277 })?;
14278 mac.update(data);
14279 Ok(mac.finalize().into_bytes().to_vec())
14280 }
14281 Self::Sha256 => {
14282 let mut mac = Hmac::<Sha256>::new_from_slice(key).map_err(|error| {
14283 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14284 })?;
14285 mac.update(data);
14286 Ok(mac.finalize().into_bytes().to_vec())
14287 }
14288 Self::Sha512 => {
14289 let mut mac = Hmac::<Sha512>::new_from_slice(key).map_err(|error| {
14290 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14291 })?;
14292 mac.update(data);
14293 Ok(mac.finalize().into_bytes().to_vec())
14294 }
14295 }
14296 }
14297
14298 fn pbkdf2(self, password: &[u8], salt: &[u8], iterations: u32, output: &mut [u8]) {
14299 match self {
14300 Self::Md5 => pbkdf2_hmac::<Md5>(password, salt, iterations, output),
14301 Self::Sha1 => pbkdf2_hmac::<Sha1>(password, salt, iterations, output),
14302 Self::Sha256 => pbkdf2_hmac::<Sha256>(password, salt, iterations, output),
14303 Self::Sha512 => pbkdf2_hmac::<Sha512>(password, salt, iterations, output),
14304 }
14305 }
14306}
14307
14308#[derive(Debug, Clone)]
14309enum JavascriptCryptoKeyMaterial {
14310 Private(PKey<Private>),
14311 Public(PKey<Public>),
14312 Secret(Vec<u8>),
14313}
14314
14315#[derive(Debug, Clone, Deserialize, Serialize)]
14316struct JavascriptSerializedSandboxKeyObject {
14317 #[serde(rename = "type")]
14318 kind: String,
14319 #[serde(skip_serializing_if = "Option::is_none")]
14320 pem: Option<String>,
14321 #[serde(skip_serializing_if = "Option::is_none")]
14322 raw: Option<String>,
14323 #[serde(skip_serializing_if = "Option::is_none", rename = "asymmetricKeyType")]
14324 asymmetric_key_type: Option<String>,
14325 #[serde(
14326 skip_serializing_if = "Option::is_none",
14327 rename = "asymmetricKeyDetails"
14328 )]
14329 asymmetric_key_details: Option<Map<String, Value>>,
14330 #[serde(skip_serializing_if = "Option::is_none")]
14331 jwk: Option<Value>,
14332}
14333
14334#[derive(Debug, Clone)]
14335struct JavascriptDirectKeyInput {
14336 key: JavascriptCryptoKeyMaterial,
14337 padding: Option<Padding>,
14338}
14339
14340fn service_javascript_crypto_cipheriv_sync_rpc(
14341 request: &JavascriptSyncRpcRequest,
14342) -> Result<Value, SidecarError> {
14343 service_javascript_crypto_cipheriv_inner(request, false)
14344}
14345
14346fn service_javascript_crypto_decipheriv_sync_rpc(
14347 request: &JavascriptSyncRpcRequest,
14348) -> Result<Value, SidecarError> {
14349 service_javascript_crypto_cipheriv_inner(request, true)
14350}
14351
14352fn service_javascript_crypto_cipheriv_create_sync_rpc(
14353 process: &mut ActiveProcess,
14354 request: &JavascriptSyncRpcRequest,
14355) -> Result<Value, SidecarError> {
14356 ensure_per_process_state_handle_capacity(process.cipher_sessions.len(), "cipher session")?;
14357 let mode = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.cipherivCreate mode")?;
14358 let decrypt = mode == "decipher";
14359 let algorithm =
14360 javascript_sync_rpc_arg_str(&request.args, 1, "crypto.cipherivCreate algorithm")?;
14361 let key = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.cipherivCreate key")?;
14362 let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 3, "crypto.cipherivCreate iv")?;
14363 let options =
14364 javascript_sync_rpc_json_arg_optional(&request.args, 4, "crypto.cipherivCreate options")?;
14365 let auth_tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options.as_ref())?;
14366 let context = javascript_crypto_build_cipher_context(
14367 algorithm,
14368 &key,
14369 iv.as_deref(),
14370 decrypt,
14371 options.as_ref(),
14372 )?;
14373 process.next_cipher_session_id += 1;
14374 let session_id = process.next_cipher_session_id;
14375 process.cipher_sessions.insert(
14376 session_id,
14377 ActiveCipherSession {
14378 algorithm: algorithm.to_string(),
14379 auth_tag_len,
14380 context,
14381 },
14382 );
14383 Ok(json!(session_id))
14384}
14385
14386fn service_javascript_crypto_cipheriv_update_sync_rpc(
14387 process: &mut ActiveProcess,
14388 request: &JavascriptSyncRpcRequest,
14389) -> Result<Value, SidecarError> {
14390 let session_id =
14391 javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivUpdate session id")?;
14392 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.cipherivUpdate data")?;
14393 let session = process
14394 .cipher_sessions
14395 .get_mut(&session_id)
14396 .ok_or_else(|| {
14397 SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
14398 })?;
14399 let result = javascript_crypto_cipher_update(&mut session.context, &data)?;
14400 Ok(Value::String(
14401 base64::engine::general_purpose::STANDARD.encode(result),
14402 ))
14403}
14404
14405fn service_javascript_crypto_cipheriv_final_sync_rpc(
14406 process: &mut ActiveProcess,
14407 request: &JavascriptSyncRpcRequest,
14408) -> Result<Value, SidecarError> {
14409 let session_id =
14410 javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivFinal session id")?;
14411 let mut session = process.cipher_sessions.remove(&session_id).ok_or_else(|| {
14412 SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
14413 })?;
14414 let data = javascript_crypto_cipher_finalize(&mut session.context)?;
14415 let mut response = Map::new();
14416 response.insert(
14417 String::from("data"),
14418 Value::String(base64::engine::general_purpose::STANDARD.encode(data)),
14419 );
14420 if javascript_crypto_is_aead(&session.algorithm) {
14421 let mut auth_tag = vec![0_u8; session.auth_tag_len];
14422 session
14423 .context
14424 .get_tag(&mut auth_tag)
14425 .map_err(javascript_crypto_openssl_error)?;
14426 response.insert(
14427 String::from("authTag"),
14428 Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
14429 );
14430 }
14431 Ok(Value::String(serde_json::to_string(&response).map_err(
14432 |error| SidecarError::InvalidState(format!("serialize cipher final response: {error}")),
14433 )?))
14434}
14435
14436fn service_javascript_crypto_sign_sync_rpc(
14437 request: &JavascriptSyncRpcRequest,
14438) -> Result<Value, SidecarError> {
14439 let algorithm = request.args.first().and_then(Value::as_str);
14440 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.sign data")?;
14441 let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.sign key")?;
14442 let key_input =
14443 javascript_crypto_parse_direct_key_input(key_json, Some("private"), "crypto.sign key")?;
14444 let private_key = javascript_crypto_expect_private_key(key_input.key, "crypto.sign key")?;
14445 let mut signer = javascript_crypto_new_signer(algorithm, &private_key)?;
14446 if let Some(padding) = key_input.padding {
14447 signer
14448 .set_rsa_padding(padding)
14449 .map_err(javascript_crypto_openssl_error)?;
14450 }
14451 signer
14452 .update(&data)
14453 .map_err(javascript_crypto_openssl_error)?;
14454 Ok(Value::String(
14455 base64::engine::general_purpose::STANDARD.encode(
14456 signer
14457 .sign_to_vec()
14458 .map_err(javascript_crypto_openssl_error)?,
14459 ),
14460 ))
14461}
14462
14463fn service_javascript_crypto_verify_sync_rpc(
14464 request: &JavascriptSyncRpcRequest,
14465) -> Result<Value, SidecarError> {
14466 let algorithm = request.args.first().and_then(Value::as_str);
14467 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.verify data")?;
14468 let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.verify key")?;
14469 let signature = javascript_sync_rpc_base64_arg(&request.args, 3, "crypto.verify signature")?;
14470 let key_input =
14471 javascript_crypto_parse_direct_key_input(key_json, Some("public"), "crypto.verify key")?;
14472 let public_key = javascript_crypto_expect_public_key(key_input.key, "crypto.verify key")?;
14473 let mut verifier = javascript_crypto_new_verifier(algorithm, &public_key)?;
14474 if let Some(padding) = key_input.padding {
14475 verifier
14476 .set_rsa_padding(padding)
14477 .map_err(javascript_crypto_openssl_error)?;
14478 }
14479 verifier
14480 .update(&data)
14481 .map_err(javascript_crypto_openssl_error)?;
14482 Ok(json!(verifier
14483 .verify(&signature)
14484 .map_err(javascript_crypto_openssl_error)?))
14485}
14486
14487fn service_javascript_crypto_asymmetric_op_sync_rpc(
14488 request: &JavascriptSyncRpcRequest,
14489) -> Result<Value, SidecarError> {
14490 let operation = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.asymmetricOp operation")?;
14491 let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.asymmetricOp key")?;
14492 let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.asymmetricOp data")?;
14493 let expect_kind = match operation {
14494 "publicEncrypt" | "publicDecrypt" => Some("public"),
14495 "privateEncrypt" | "privateDecrypt" => Some("private"),
14496 other => {
14497 return Err(SidecarError::InvalidState(format!(
14498 "Unsupported asymmetric crypto operation: {other}"
14499 )));
14500 }
14501 };
14502 let key_input =
14503 javascript_crypto_parse_direct_key_input(key_json, expect_kind, "crypto.asymmetricOp key")?;
14504 let padding = key_input.padding.unwrap_or(Padding::PKCS1);
14505 let mut output = vec![0_u8; javascript_crypto_rsa_output_size(&key_input.key)?];
14506 let written = match (operation, key_input.key) {
14507 ("publicEncrypt", JavascriptCryptoKeyMaterial::Public(key))
14508 | ("publicDecrypt", JavascriptCryptoKeyMaterial::Public(key)) => {
14509 let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
14510 if operation == "publicEncrypt" {
14511 rsa.public_encrypt(&data, &mut output, padding)
14512 .map_err(javascript_crypto_openssl_error)?
14513 } else {
14514 rsa.public_decrypt(&data, &mut output, padding)
14515 .map_err(javascript_crypto_openssl_error)?
14516 }
14517 }
14518 ("privateEncrypt", JavascriptCryptoKeyMaterial::Private(key))
14519 | ("privateDecrypt", JavascriptCryptoKeyMaterial::Private(key)) => {
14520 let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
14521 if operation == "privateEncrypt" {
14522 rsa.private_encrypt(&data, &mut output, padding)
14523 .map_err(javascript_crypto_openssl_error)?
14524 } else {
14525 rsa.private_decrypt(&data, &mut output, padding)
14526 .map_err(javascript_crypto_openssl_error)?
14527 }
14528 }
14529 _ => {
14530 return Err(SidecarError::InvalidState(format!(
14531 "{operation} requires an RSA {} key",
14532 expect_kind.unwrap_or("asymmetric")
14533 )));
14534 }
14535 };
14536 output.truncate(written);
14537 Ok(Value::String(
14538 base64::engine::general_purpose::STANDARD.encode(output),
14539 ))
14540}
14541
14542fn service_javascript_crypto_create_key_object_sync_rpc(
14543 request: &JavascriptSyncRpcRequest,
14544) -> Result<Value, SidecarError> {
14545 let operation =
14546 javascript_sync_rpc_arg_str(&request.args, 0, "crypto.createKeyObject operation")?;
14547 let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.createKeyObject key")?;
14548 let expected = match operation {
14549 "createPrivateKey" => Some("private"),
14550 "createPublicKey" => Some("public"),
14551 other => {
14552 return Err(SidecarError::InvalidState(format!(
14553 "Unsupported key creation operation: {other}"
14554 )));
14555 }
14556 };
14557 let key_input =
14558 javascript_crypto_parse_direct_key_input(key_json, expected, "crypto.createKeyObject key")?;
14559 Ok(Value::String(
14560 serde_json::to_string(&javascript_crypto_serialize_sandbox_key_object(
14561 &key_input.key,
14562 )?)
14563 .map_err(|error| {
14564 SidecarError::InvalidState(format!("serialize crypto key object: {error}"))
14565 })?,
14566 ))
14567}
14568
14569fn service_javascript_crypto_generate_key_pair_sync_rpc(
14570 request: &JavascriptSyncRpcRequest,
14571) -> Result<Value, SidecarError> {
14572 let key_type =
14573 javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeyPairSync type")?;
14574 let options = javascript_crypto_parse_serialized_options_arg(
14575 &request.args,
14576 1,
14577 "crypto.generateKeyPairSync options",
14578 )?
14579 .unwrap_or(Value::Object(Map::new()));
14580 let public_encoding = options.get("publicKeyEncoding").cloned();
14581 let private_encoding = options.get("privateKeyEncoding").cloned();
14582
14583 let private_key = match key_type {
14584 "rsa" => {
14585 let bits = options
14586 .get("modulusLength")
14587 .and_then(Value::as_u64)
14588 .unwrap_or(2048) as u32;
14589 let exponent = options
14590 .get("publicExponent")
14591 .map(|value| javascript_crypto_u32_from_bridge_value(value, "rsa publicExponent"))
14592 .transpose()?
14593 .unwrap_or(65_537);
14594 let exponent = BigNum::from_u32(exponent).map_err(javascript_crypto_openssl_error)?;
14595 let rsa =
14596 Rsa::generate_with_e(bits, &exponent).map_err(javascript_crypto_openssl_error)?;
14597 PKey::from_rsa(rsa).map_err(javascript_crypto_openssl_error)?
14598 }
14599 "ec" => {
14600 let named_curve = options
14601 .get("namedCurve")
14602 .and_then(Value::as_str)
14603 .ok_or_else(|| {
14604 SidecarError::InvalidState(String::from(
14605 "crypto.generateKeyPairSync ec requires namedCurve",
14606 ))
14607 })?;
14608 let group = EcGroup::from_curve_name(javascript_crypto_curve_nid(named_curve)?)
14609 .map_err(javascript_crypto_openssl_error)?;
14610 let key = EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?;
14611 PKey::from_ec_key(key).map_err(javascript_crypto_openssl_error)?
14612 }
14613 "ed25519" => PKey::generate_ed25519().map_err(javascript_crypto_openssl_error)?,
14614 "x25519" => PKey::generate_x25519().map_err(javascript_crypto_openssl_error)?,
14615 other => {
14616 return Err(SidecarError::InvalidState(format!(
14617 "unsupported crypto key pair type {other}"
14618 )));
14619 }
14620 };
14621 let public_key = PKey::public_key_from_pem(
14622 &private_key
14623 .public_key_to_pem()
14624 .map_err(javascript_crypto_openssl_error)?,
14625 )
14626 .map_err(javascript_crypto_openssl_error)?;
14627 let response = if public_encoding.is_some() || private_encoding.is_some() {
14628 json!({
14629 "publicKey": javascript_crypto_serialize_encoded_key_value_public(&public_key, public_encoding.as_ref())?,
14630 "privateKey": javascript_crypto_serialize_encoded_key_value_private(&private_key, private_encoding.as_ref())?,
14631 })
14632 } else {
14633 json!({
14634 "publicKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(public_key))?,
14635 "privateKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(private_key))?,
14636 })
14637 };
14638 Ok(Value::String(serde_json::to_string(&response).map_err(
14639 |error| SidecarError::InvalidState(format!("serialize generated key pair: {error}")),
14640 )?))
14641}
14642
14643fn service_javascript_crypto_generate_key_sync_rpc(
14644 request: &JavascriptSyncRpcRequest,
14645) -> Result<Value, SidecarError> {
14646 let key_type = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeySync type")?;
14647 let options = javascript_crypto_parse_serialized_options_arg(
14648 &request.args,
14649 1,
14650 "crypto.generateKeySync options",
14651 )?
14652 .unwrap_or(Value::Object(Map::new()));
14653 let bit_length = options
14654 .get("length")
14655 .and_then(Value::as_u64)
14656 .ok_or_else(|| {
14657 SidecarError::InvalidState(String::from(
14658 "crypto.generateKeySync options.length is required",
14659 ))
14660 })? as usize;
14661 let mut raw = vec![0_u8; bit_length.div_ceil(8)];
14662 rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
14663 let serialized = match key_type {
14664 "hmac" => javascript_crypto_serialize_sandbox_key_object(
14665 &JavascriptCryptoKeyMaterial::Secret(raw),
14666 )?,
14667 "aes" => javascript_crypto_serialize_sandbox_key_object(
14668 &JavascriptCryptoKeyMaterial::Secret(raw),
14669 )?,
14670 other => {
14671 return Err(SidecarError::InvalidState(format!(
14672 "unsupported crypto.generateKeySync type {other}"
14673 )));
14674 }
14675 };
14676 Ok(Value::String(serde_json::to_string(&serialized).map_err(
14677 |error| SidecarError::InvalidState(format!("serialize generated key: {error}")),
14678 )?))
14679}
14680
14681fn service_javascript_crypto_generate_prime_sync_rpc(
14682 request: &JavascriptSyncRpcRequest,
14683) -> Result<Value, SidecarError> {
14684 let bits =
14685 javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.generatePrimeSync size")? as i32;
14686 let options = javascript_crypto_parse_serialized_options_arg(
14687 &request.args,
14688 1,
14689 "crypto.generatePrimeSync options",
14690 )?
14691 .unwrap_or(Value::Object(Map::new()));
14692 let safe = options
14693 .get("safe")
14694 .and_then(Value::as_bool)
14695 .unwrap_or(false);
14696 let add = options
14697 .get("add")
14698 .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime add"))
14699 .transpose()?;
14700 let rem = options
14701 .get("rem")
14702 .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime rem"))
14703 .transpose()?;
14704 let mut prime = BigNum::new().map_err(javascript_crypto_openssl_error)?;
14705 prime
14706 .generate_prime(bits, safe, add.as_deref(), rem.as_deref())
14707 .map_err(javascript_crypto_openssl_error)?;
14708 let payload = if options
14709 .get("bigint")
14710 .and_then(Value::as_bool)
14711 .unwrap_or(false)
14712 {
14713 json!({
14714 "__type": "bigint",
14715 "value": prime.to_dec_str().map_err(javascript_crypto_openssl_error)?.to_string(),
14716 })
14717 } else {
14718 json!({
14719 "__type": "buffer",
14720 "value": base64::engine::general_purpose::STANDARD.encode(prime.to_vec()),
14721 })
14722 };
14723 Ok(Value::String(serde_json::to_string(&payload).map_err(
14724 |error| SidecarError::InvalidState(format!("serialize generated prime: {error}")),
14725 )?))
14726}
14727
14728fn service_javascript_crypto_diffie_hellman_sync_rpc(
14729 request: &JavascriptSyncRpcRequest,
14730) -> Result<Value, SidecarError> {
14731 let options = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellman options")?;
14732 let parsed: Value = serde_json::from_str(options).map_err(|error| {
14733 SidecarError::InvalidState(format!(
14734 "crypto.diffieHellman options must be valid JSON: {error}"
14735 ))
14736 })?;
14737 let private_key = javascript_crypto_parse_key_material_value(
14738 parsed.get("privateKey").ok_or_else(|| {
14739 SidecarError::InvalidState(String::from("crypto.diffieHellman missing privateKey"))
14740 })?,
14741 Some("private"),
14742 "crypto.diffieHellman privateKey",
14743 )?;
14744 let public_key = javascript_crypto_parse_key_material_value(
14745 parsed.get("publicKey").ok_or_else(|| {
14746 SidecarError::InvalidState(String::from("crypto.diffieHellman missing publicKey"))
14747 })?,
14748 Some("public"),
14749 "crypto.diffieHellman publicKey",
14750 )?;
14751 let private_key =
14752 javascript_crypto_expect_private_key(private_key, "crypto.diffieHellman privateKey")?;
14753 let public_key =
14754 javascript_crypto_expect_public_key(public_key, "crypto.diffieHellman publicKey")?;
14755 let mut deriver = Deriver::new(&private_key).map_err(javascript_crypto_openssl_error)?;
14756 deriver
14757 .set_peer(&public_key)
14758 .map_err(javascript_crypto_openssl_error)?;
14759 let secret = deriver
14760 .derive_to_vec()
14761 .map_err(javascript_crypto_openssl_error)?;
14762 Ok(Value::String(
14763 serde_json::to_string(&json!({
14764 "__type": "buffer",
14765 "value": base64::engine::general_purpose::STANDARD.encode(secret),
14766 }))
14767 .map_err(|error| {
14768 SidecarError::InvalidState(format!("serialize derived secret: {error}"))
14769 })?,
14770 ))
14771}
14772
14773fn service_javascript_crypto_diffie_hellman_group_sync_rpc(
14774 request: &JavascriptSyncRpcRequest,
14775) -> Result<Value, SidecarError> {
14776 let name = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellmanGroup name")?;
14777 let params = javascript_crypto_named_dh_group(name)?;
14778 let response = json!({
14779 "prime": {
14780 "__type": "buffer",
14781 "value": base64::engine::general_purpose::STANDARD.encode(params.prime_p().to_vec()),
14782 },
14783 "generator": {
14784 "__type": "buffer",
14785 "value": base64::engine::general_purpose::STANDARD.encode(params.generator().to_vec()),
14786 },
14787 });
14788 Ok(Value::String(serde_json::to_string(&response).map_err(
14789 |error| {
14790 SidecarError::InvalidState(format!("serialize diffieHellmanGroup response: {error}"))
14791 },
14792 )?))
14793}
14794
14795fn service_javascript_crypto_diffie_hellman_session_create_sync_rpc(
14796 process: &mut ActiveProcess,
14797 request: &JavascriptSyncRpcRequest,
14798) -> Result<Value, SidecarError> {
14799 ensure_per_process_state_handle_capacity(
14800 process.diffie_hellman_sessions.len(),
14801 "diffie-hellman session",
14802 )?;
14803 let raw = javascript_sync_rpc_arg_str(
14804 &request.args,
14805 0,
14806 "crypto.diffieHellmanSessionCreate request",
14807 )?;
14808 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14809 SidecarError::InvalidState(format!(
14810 "crypto.diffieHellmanSessionCreate request must be valid JSON: {error}"
14811 ))
14812 })?;
14813 let session = match parsed.get("type").and_then(Value::as_str) {
14814 Some("group") => {
14815 let name = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
14816 SidecarError::InvalidState(String::from(
14817 "crypto.diffieHellmanSessionCreate group requires name",
14818 ))
14819 })?;
14820 ActiveDiffieHellmanSession::Dh(ActiveDhSession {
14821 params: javascript_crypto_named_dh_group(name)?,
14822 key_pair: None,
14823 })
14824 }
14825 Some("dh") => {
14826 let args = parsed
14827 .get("args")
14828 .and_then(Value::as_array)
14829 .ok_or_else(|| {
14830 SidecarError::InvalidState(String::from(
14831 "crypto.diffieHellmanSessionCreate dh requires args",
14832 ))
14833 })?;
14834 let params = javascript_crypto_build_dh_params(args)?;
14835 ActiveDiffieHellmanSession::Dh(ActiveDhSession {
14836 params,
14837 key_pair: None,
14838 })
14839 }
14840 Some("ecdh") => {
14841 let curve = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
14842 SidecarError::InvalidState(String::from(
14843 "crypto.diffieHellmanSessionCreate ecdh requires name",
14844 ))
14845 })?;
14846 ActiveDiffieHellmanSession::Ecdh(ActiveEcdhSession {
14847 curve: curve.to_string(),
14848 key_pair: None,
14849 })
14850 }
14851 other => {
14852 return Err(SidecarError::InvalidState(format!(
14853 "Unsupported Diffie-Hellman session type: {}",
14854 other.unwrap_or("<missing>")
14855 )));
14856 }
14857 };
14858 process.next_diffie_hellman_session_id += 1;
14859 let session_id = process.next_diffie_hellman_session_id;
14860 process.diffie_hellman_sessions.insert(session_id, session);
14861 Ok(json!(session_id))
14862}
14863
14864fn service_javascript_crypto_diffie_hellman_session_call_sync_rpc(
14865 process: &mut ActiveProcess,
14866 request: &JavascriptSyncRpcRequest,
14867) -> Result<Value, SidecarError> {
14868 let session_id = javascript_sync_rpc_arg_u64(
14869 &request.args,
14870 0,
14871 "crypto.diffieHellmanSessionCall session id",
14872 )?;
14873 let raw =
14874 javascript_sync_rpc_arg_str(&request.args, 1, "crypto.diffieHellmanSessionCall request")?;
14875 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14876 SidecarError::InvalidState(format!(
14877 "crypto.diffieHellmanSessionCall request must be valid JSON: {error}"
14878 ))
14879 })?;
14880 let method = parsed
14881 .get("method")
14882 .and_then(Value::as_str)
14883 .ok_or_else(|| {
14884 SidecarError::InvalidState(String::from(
14885 "crypto.diffieHellmanSessionCall request missing method",
14886 ))
14887 })?;
14888 let args = parsed
14889 .get("args")
14890 .and_then(Value::as_array)
14891 .cloned()
14892 .unwrap_or_default();
14893 let session = process
14894 .diffie_hellman_sessions
14895 .get_mut(&session_id)
14896 .ok_or_else(|| {
14897 SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
14898 })?;
14899 let (result, has_result) = match session {
14900 ActiveDiffieHellmanSession::Dh(session) => {
14901 javascript_crypto_call_dh_session(session, method, &args)?
14902 }
14903 ActiveDiffieHellmanSession::Ecdh(session) => {
14904 javascript_crypto_call_ecdh_session(session, method, &args)?
14905 }
14906 };
14907 Ok(Value::String(
14908 serde_json::to_string(&json!({
14909 "result": result,
14910 "hasResult": has_result,
14911 }))
14912 .map_err(|error| {
14913 SidecarError::InvalidState(format!("serialize diffie session result: {error}"))
14914 })?,
14915 ))
14916}
14917
14918fn service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(
14919 process: &mut ActiveProcess,
14920 request: &JavascriptSyncRpcRequest,
14921) -> Result<Value, SidecarError> {
14922 let session_id = javascript_sync_rpc_arg_u64(
14923 &request.args,
14924 0,
14925 "crypto.diffieHellmanSessionDestroy session id",
14926 )?;
14927 process
14928 .diffie_hellman_sessions
14929 .remove(&session_id)
14930 .ok_or_else(|| {
14931 SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
14932 })?;
14933 Ok(Value::Null)
14934}
14935
14936fn service_javascript_crypto_subtle_sync_rpc(
14937 request: &JavascriptSyncRpcRequest,
14938) -> Result<Value, SidecarError> {
14939 let raw = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.subtle request")?;
14940 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14941 SidecarError::InvalidState(format!("crypto.subtle request must be valid JSON: {error}"))
14942 })?;
14943 let op = parsed.get("op").and_then(Value::as_str).ok_or_else(|| {
14944 SidecarError::InvalidState(String::from("crypto.subtle request missing op"))
14945 })?;
14946 match op {
14947 "digest" => {
14948 let algorithm = parsed
14949 .get("algorithm")
14950 .and_then(Value::as_str)
14951 .ok_or_else(|| {
14952 SidecarError::InvalidState(String::from(
14953 "crypto.subtle.digest missing algorithm",
14954 ))
14955 })?;
14956 let data = parsed.get("data").and_then(Value::as_str).ok_or_else(|| {
14957 SidecarError::InvalidState(String::from("crypto.subtle.digest missing data"))
14958 })?;
14959 let bytes = base64::engine::general_purpose::STANDARD
14960 .decode(data)
14961 .map_err(|error| {
14962 SidecarError::InvalidState(format!("crypto.subtle.digest data base64: {error}"))
14963 })?;
14964 let digest = JavascriptCryptoDigestAlgorithm::parse(algorithm)?.digest(&bytes);
14965 Ok(Value::String(
14966 serde_json::to_string(&json!({
14967 "data": base64::engine::general_purpose::STANDARD.encode(digest),
14968 }))
14969 .map_err(|error| {
14970 SidecarError::InvalidState(format!("serialize crypto.subtle digest: {error}"))
14971 })?,
14972 ))
14973 }
14974 "generateKey" => {
14975 let algorithm = parsed.get("algorithm").ok_or_else(|| {
14976 SidecarError::InvalidState(String::from(
14977 "crypto.subtle.generateKey missing algorithm",
14978 ))
14979 })?;
14980 let name =
14981 javascript_crypto_subtle_algorithm_name(algorithm, "crypto.subtle.generateKey")?;
14982 if !matches!(name, "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW") {
14983 return Err(SidecarError::InvalidState(format!(
14984 "Unsupported key algorithm: {name}"
14985 )));
14986 }
14987 let length_bits = algorithm
14988 .get("length")
14989 .and_then(Value::as_u64)
14990 .ok_or_else(|| {
14991 SidecarError::InvalidState(String::from(
14992 "crypto.subtle.generateKey AES algorithm requires length",
14993 ))
14994 })?;
14995 if length_bits % 8 != 0 {
14996 return Err(SidecarError::InvalidState(String::from(
14997 "crypto.subtle.generateKey length must be byte-aligned",
14998 )));
14999 }
15000 let length_bytes = usize::try_from(length_bits / 8).map_err(|_| {
15001 SidecarError::InvalidState(String::from(
15002 "crypto.subtle.generateKey length is too large",
15003 ))
15004 })?;
15005 let mut raw = vec![0_u8; length_bytes];
15006 rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
15007 let key = javascript_crypto_serialize_subtle_secret_key(
15008 &raw,
15009 javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
15010 parsed
15011 .get("extractable")
15012 .and_then(Value::as_bool)
15013 .unwrap_or(false),
15014 parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
15015 )?;
15016 Ok(Value::String(
15017 serde_json::to_string(&json!({ "key": key })).map_err(|error| {
15018 SidecarError::InvalidState(format!(
15019 "serialize crypto.subtle generated key: {error}"
15020 ))
15021 })?,
15022 ))
15023 }
15024 "importKey" => {
15025 let format = parsed
15026 .get("format")
15027 .and_then(Value::as_str)
15028 .ok_or_else(|| {
15029 SidecarError::InvalidState(String::from(
15030 "crypto.subtle.importKey missing format",
15031 ))
15032 })?;
15033 if format != "raw" {
15034 return Err(SidecarError::InvalidState(format!(
15035 "Unsupported import format: {format}"
15036 )));
15037 }
15038 let key_data = parsed
15039 .get("keyData")
15040 .and_then(Value::as_str)
15041 .ok_or_else(|| {
15042 SidecarError::InvalidState(String::from(
15043 "crypto.subtle.importKey missing keyData",
15044 ))
15045 })?;
15046 let raw = base64::engine::general_purpose::STANDARD
15047 .decode(key_data)
15048 .map_err(|error| {
15049 SidecarError::InvalidState(format!(
15050 "crypto.subtle.importKey keyData base64: {error}"
15051 ))
15052 })?;
15053 let algorithm = parsed.get("algorithm").ok_or_else(|| {
15054 SidecarError::InvalidState(String::from(
15055 "crypto.subtle.importKey missing algorithm",
15056 ))
15057 })?;
15058 let key = javascript_crypto_serialize_subtle_secret_key(
15059 &raw,
15060 javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
15061 parsed
15062 .get("extractable")
15063 .and_then(Value::as_bool)
15064 .unwrap_or(false),
15065 parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
15066 )?;
15067 Ok(Value::String(
15068 serde_json::to_string(&json!({ "key": key })).map_err(|error| {
15069 SidecarError::InvalidState(format!(
15070 "serialize crypto.subtle imported key: {error}"
15071 ))
15072 })?,
15073 ))
15074 }
15075 "exportKey" => {
15076 let format = parsed
15077 .get("format")
15078 .and_then(Value::as_str)
15079 .ok_or_else(|| {
15080 SidecarError::InvalidState(String::from(
15081 "crypto.subtle.exportKey missing format",
15082 ))
15083 })?;
15084 if format != "raw" {
15085 return Err(SidecarError::InvalidState(format!(
15086 "Unsupported export format: {format}"
15087 )));
15088 }
15089 let raw = javascript_crypto_subtle_key_raw(
15090 parsed.get("key").ok_or_else(|| {
15091 SidecarError::InvalidState(String::from("crypto.subtle.exportKey missing key"))
15092 })?,
15093 "crypto.subtle.exportKey key",
15094 )?;
15095 Ok(Value::String(
15096 serde_json::to_string(&json!({
15097 "data": base64::engine::general_purpose::STANDARD.encode(raw),
15098 }))
15099 .map_err(|error| {
15100 SidecarError::InvalidState(format!("serialize crypto.subtle export: {error}"))
15101 })?,
15102 ))
15103 }
15104 "encrypt" | "decrypt" => service_javascript_crypto_subtle_aes_crypt_sync_rpc(op, &parsed),
15105 _ => Err(SidecarError::InvalidState(format!(
15106 "Unsupported subtle operation: {op}"
15107 ))),
15108 }
15109}
15110
15111fn javascript_crypto_subtle_algorithm_name<'a>(
15112 algorithm: &'a Value,
15113 label: &str,
15114) -> Result<&'a str, SidecarError> {
15115 if let Some(name) = algorithm.as_str() {
15116 return Ok(name);
15117 }
15118 algorithm
15119 .get("name")
15120 .and_then(Value::as_str)
15121 .ok_or_else(|| SidecarError::InvalidState(format!("{label} algorithm missing name")))
15122}
15123
15124fn javascript_crypto_normalize_subtle_secret_algorithm(
15125 algorithm: Value,
15126 raw: &[u8],
15127) -> Result<Value, SidecarError> {
15128 let mut object = match algorithm {
15129 Value::String(name) => {
15130 let mut object = Map::new();
15131 object.insert(String::from("name"), Value::String(name));
15132 object
15133 }
15134 Value::Object(object) => object,
15135 _ => {
15136 return Err(SidecarError::InvalidState(String::from(
15137 "crypto.subtle secret algorithm must be a string or object",
15138 )));
15139 }
15140 };
15141 let name = object
15142 .get("name")
15143 .and_then(Value::as_str)
15144 .ok_or_else(|| {
15145 SidecarError::InvalidState(String::from("crypto.subtle secret algorithm missing name"))
15146 })?
15147 .to_string();
15148 if matches!(name.as_str(), "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW")
15149 && !object.contains_key("length")
15150 {
15151 object.insert(String::from("length"), json!(raw.len() * 8));
15152 }
15153 Ok(Value::Object(object))
15154}
15155
15156fn javascript_crypto_serialize_subtle_secret_key(
15157 raw: &[u8],
15158 algorithm: Value,
15159 extractable: bool,
15160 usages: Value,
15161) -> Result<Value, SidecarError> {
15162 let raw_base64 = base64::engine::general_purpose::STANDARD.encode(raw);
15163 let source_key_object_data = javascript_crypto_serialize_sandbox_key_object(
15164 &JavascriptCryptoKeyMaterial::Secret(raw.to_vec()),
15165 )?;
15166 Ok(json!({
15167 "type": "secret",
15168 "algorithm": algorithm,
15169 "extractable": extractable,
15170 "usages": usages,
15171 "_raw": raw_base64,
15172 "_sourceKeyObjectData": source_key_object_data,
15173 }))
15174}
15175
15176fn javascript_crypto_subtle_key_raw(key: &Value, label: &str) -> Result<Vec<u8>, SidecarError> {
15177 let raw = key.get("_raw").and_then(Value::as_str).ok_or_else(|| {
15178 SidecarError::InvalidState(format!("{label} must be a raw secret CryptoKey"))
15179 })?;
15180 base64::engine::general_purpose::STANDARD
15181 .decode(raw)
15182 .map_err(|error| SidecarError::InvalidState(format!("{label} raw base64: {error}")))
15183}
15184
15185fn service_javascript_crypto_subtle_aes_crypt_sync_rpc(
15186 op: &str,
15187 parsed: &Value,
15188) -> Result<Value, SidecarError> {
15189 let algorithm = parsed.get("algorithm").ok_or_else(|| {
15190 SidecarError::InvalidState(format!("crypto.subtle.{op} missing algorithm"))
15191 })?;
15192 let name = javascript_crypto_subtle_algorithm_name(algorithm, &format!("crypto.subtle.{op}"))?;
15193 if name != "AES-GCM" {
15194 return Err(SidecarError::InvalidState(format!(
15195 "Unsupported subtle AES operation algorithm: {name}"
15196 )));
15197 }
15198 let key = javascript_crypto_subtle_key_raw(
15199 parsed
15200 .get("key")
15201 .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing key")))?,
15202 &format!("crypto.subtle.{op} key"),
15203 )?;
15204 let iv = algorithm.get("iv").and_then(Value::as_str).ok_or_else(|| {
15205 SidecarError::InvalidState(format!("crypto.subtle.{op} AES-GCM missing iv"))
15206 })?;
15207 let iv = base64::engine::general_purpose::STANDARD
15208 .decode(iv)
15209 .map_err(|error| {
15210 SidecarError::InvalidState(format!("crypto.subtle.{op} iv base64: {error}"))
15211 })?;
15212 let data = parsed
15213 .get("data")
15214 .and_then(Value::as_str)
15215 .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing data")))?;
15216 let mut data = base64::engine::general_purpose::STANDARD
15217 .decode(data)
15218 .map_err(|error| {
15219 SidecarError::InvalidState(format!("crypto.subtle.{op} data base64: {error}"))
15220 })?;
15221 let tag_len = javascript_crypto_subtle_aes_gcm_tag_len(algorithm)?;
15222 let mut options = Map::new();
15223 options.insert(String::from("authTagLength"), json!(tag_len));
15224 if let Some(additional_data) = algorithm.get("additionalData").and_then(Value::as_str) {
15225 options.insert(
15226 String::from("aad"),
15227 Value::String(additional_data.to_string()),
15228 );
15229 }
15230 let decrypt = op == "decrypt";
15231 if decrypt {
15232 if data.len() < tag_len {
15233 return Err(SidecarError::InvalidState(String::from(
15234 "crypto.subtle.decrypt AES-GCM data shorter than auth tag",
15235 )));
15236 }
15237 let auth_tag = data.split_off(data.len() - tag_len);
15238 options.insert(
15239 String::from("authTag"),
15240 Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
15241 );
15242 }
15243 let cipher_name = format!("aes-{}-gcm", key.len() * 8);
15244 let mut context = javascript_crypto_build_cipher_context(
15245 &cipher_name,
15246 &key,
15247 Some(&iv),
15248 decrypt,
15249 Some(&Value::Object(options)),
15250 )?;
15251 let mut output = javascript_crypto_cipher_update(&mut context, &data)?;
15252 output.extend(javascript_crypto_cipher_finalize(&mut context)?);
15253 if !decrypt {
15254 let mut auth_tag = vec![0_u8; tag_len];
15255 context
15256 .get_tag(&mut auth_tag)
15257 .map_err(javascript_crypto_openssl_error)?;
15258 output.extend(auth_tag);
15259 }
15260 Ok(Value::String(
15261 serde_json::to_string(&json!({
15262 "data": base64::engine::general_purpose::STANDARD.encode(output),
15263 }))
15264 .map_err(|error| {
15265 SidecarError::InvalidState(format!("serialize crypto.subtle {op}: {error}"))
15266 })?,
15267 ))
15268}
15269
15270fn javascript_crypto_subtle_aes_gcm_tag_len(algorithm: &Value) -> Result<usize, SidecarError> {
15271 let tag_bits = algorithm
15272 .get("tagLength")
15273 .and_then(Value::as_u64)
15274 .unwrap_or(128);
15275 if !tag_bits.is_multiple_of(8) {
15276 return Err(SidecarError::InvalidState(String::from(
15277 "crypto.subtle AES-GCM tagLength must be byte-aligned",
15278 )));
15279 }
15280 usize::try_from(tag_bits / 8).map_err(|_| {
15281 SidecarError::InvalidState(String::from("crypto.subtle AES-GCM tagLength too large"))
15282 })
15283}
15284
15285fn service_javascript_crypto_cipheriv_inner(
15286 request: &JavascriptSyncRpcRequest,
15287 decrypt: bool,
15288) -> Result<Value, SidecarError> {
15289 let label = if decrypt {
15290 "crypto.decipheriv"
15291 } else {
15292 "crypto.cipheriv"
15293 };
15294 let algorithm = javascript_sync_rpc_arg_str(&request.args, 0, &format!("{label} algorithm"))?;
15295 let key = javascript_sync_rpc_base64_arg(&request.args, 1, &format!("{label} key"))?;
15296 let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 2, &format!("{label} iv"))?;
15297 let data = javascript_sync_rpc_base64_arg(&request.args, 3, &format!("{label} data"))?;
15298 let options =
15299 javascript_sync_rpc_json_arg_optional(&request.args, 4, &format!("{label} options"))?;
15300 let auth_tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options.as_ref())?;
15301 let mut context = javascript_crypto_build_cipher_context(
15302 algorithm,
15303 &key,
15304 iv.as_deref(),
15305 decrypt,
15306 options.as_ref(),
15307 )?;
15308 let payload = javascript_crypto_cipher_update(&mut context, &data)?;
15309 let final_bytes = javascript_crypto_cipher_finalize(&mut context)?;
15310 if decrypt {
15311 let mut output = payload;
15312 output.extend(final_bytes);
15313 return Ok(Value::String(
15314 base64::engine::general_purpose::STANDARD.encode(output),
15315 ));
15316 }
15317
15318 let mut response = Map::new();
15319 let mut encrypted = payload;
15320 encrypted.extend(final_bytes);
15321 response.insert(
15322 String::from("data"),
15323 Value::String(base64::engine::general_purpose::STANDARD.encode(encrypted)),
15324 );
15325 if javascript_crypto_is_aead(algorithm) {
15326 let mut auth_tag = vec![0_u8; auth_tag_len];
15327 context
15328 .get_tag(&mut auth_tag)
15329 .map_err(javascript_crypto_openssl_error)?;
15330 response.insert(
15331 String::from("authTag"),
15332 Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
15333 );
15334 }
15335 Ok(Value::String(serde_json::to_string(&response).map_err(
15336 |error| SidecarError::InvalidState(format!("serialize {label} response: {error}")),
15337 )?))
15338}
15339
15340fn javascript_sync_rpc_base64_arg_optional(
15341 args: &[Value],
15342 index: usize,
15343 label: &str,
15344) -> Result<Option<Vec<u8>>, SidecarError> {
15345 if args.get(index).is_none() || args[index].is_null() {
15346 return Ok(None);
15347 }
15348 javascript_sync_rpc_base64_arg(args, index, label).map(Some)
15349}
15350
15351fn javascript_sync_rpc_json_arg_optional(
15352 args: &[Value],
15353 index: usize,
15354 label: &str,
15355) -> Result<Option<Value>, SidecarError> {
15356 if args.get(index).is_none() || args[index].is_null() {
15357 return Ok(None);
15358 }
15359 let raw = javascript_sync_rpc_arg_str(args, index, label)?;
15360 serde_json::from_str(raw)
15361 .map(Some)
15362 .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
15363}
15364
15365fn javascript_crypto_parse_direct_key_input(
15366 raw: &str,
15367 expected: Option<&str>,
15368 label: &str,
15369) -> Result<JavascriptDirectKeyInput, SidecarError> {
15370 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
15371 SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
15372 })?;
15373 let padding = match parsed.as_object().and_then(|value| value.get("padding")) {
15374 Some(value) => javascript_crypto_padding_from_value(value)?,
15375 None => None,
15376 };
15377 Ok(JavascriptDirectKeyInput {
15378 key: javascript_crypto_parse_key_material_value(&parsed, expected, label)?,
15379 padding,
15380 })
15381}
15382
15383fn javascript_crypto_parse_key_material_value(
15384 value: &Value,
15385 expected: Option<&str>,
15386 label: &str,
15387) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15388 if let Some(object) = value.as_object() {
15389 if object.get("__type").and_then(Value::as_str) == Some("keyObject") {
15390 let serialized = object.get("value").ok_or_else(|| {
15391 SidecarError::InvalidState(format!("{label} keyObject is missing a value"))
15392 })?;
15393 return javascript_crypto_parse_serialized_key_object(serialized, expected, label);
15394 }
15395 if object.contains_key("type") && (object.contains_key("pem") || object.contains_key("raw"))
15396 {
15397 return javascript_crypto_parse_serialized_key_object(value, expected, label);
15398 }
15399 if let Some(source) = object.get("key") {
15400 return javascript_crypto_parse_key_source(
15401 source,
15402 object.get("format").and_then(Value::as_str),
15403 object.get("type").and_then(Value::as_str),
15404 expected,
15405 label,
15406 );
15407 }
15408 }
15409 javascript_crypto_parse_key_source(value, None, None, expected, label)
15410}
15411
15412fn javascript_crypto_parse_key_source(
15413 source: &Value,
15414 format: Option<&str>,
15415 kind: Option<&str>,
15416 expected: Option<&str>,
15417 label: &str,
15418) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15419 match source {
15420 Value::String(pem) => javascript_crypto_parse_key_from_pem(pem.as_bytes(), expected, label),
15421 Value::Object(object) if object.get("__type").and_then(Value::as_str) == Some("buffer") => {
15422 let data = javascript_crypto_decode_bridge_buffer(source, label)?;
15423 javascript_crypto_parse_key_from_bytes(&data, format, kind, expected, label)
15424 }
15425 Value::Object(_) => {
15426 if format == Some("jwk") {
15427 return Err(SidecarError::InvalidState(format!(
15428 "{label} jwk inputs are not supported yet"
15429 )));
15430 }
15431 Err(SidecarError::InvalidState(format!(
15432 "{label} has an unsupported key shape"
15433 )))
15434 }
15435 _ => Err(SidecarError::InvalidState(format!(
15436 "{label} has an unsupported key value"
15437 ))),
15438 }
15439}
15440
15441fn javascript_crypto_parse_key_from_pem(
15442 pem: &[u8],
15443 expected: Option<&str>,
15444 label: &str,
15445) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15446 match expected {
15447 Some("private") => PKey::private_key_from_pem(pem)
15448 .map(JavascriptCryptoKeyMaterial::Private)
15449 .map_err(|error| {
15450 SidecarError::InvalidState(format!("{label} private key is invalid: {error}"))
15451 }),
15452 Some("public") => PKey::public_key_from_pem(pem)
15453 .map(JavascriptCryptoKeyMaterial::Public)
15454 .map_err(|error| {
15455 SidecarError::InvalidState(format!("{label} public key is invalid: {error}"))
15456 }),
15457 _ => PKey::private_key_from_pem(pem)
15458 .map(JavascriptCryptoKeyMaterial::Private)
15459 .or_else(|_| PKey::public_key_from_pem(pem).map(JavascriptCryptoKeyMaterial::Public))
15460 .map_err(|error| {
15461 SidecarError::InvalidState(format!("{label} PEM key is invalid: {error}"))
15462 }),
15463 }
15464}
15465
15466fn javascript_crypto_parse_key_from_bytes(
15467 der: &[u8],
15468 format: Option<&str>,
15469 kind: Option<&str>,
15470 expected: Option<&str>,
15471 label: &str,
15472) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15473 match (format.unwrap_or("der"), kind.or(expected)) {
15474 ("der", Some("pkcs8")) | ("der", Some("private")) => PKey::private_key_from_der(der)
15475 .map(JavascriptCryptoKeyMaterial::Private)
15476 .map_err(|error| {
15477 SidecarError::InvalidState(format!("{label} private key DER is invalid: {error}"))
15478 }),
15479 ("der", Some("spki")) | ("der", Some("public")) => PKey::public_key_from_der(der)
15480 .map(JavascriptCryptoKeyMaterial::Public)
15481 .map_err(|error| {
15482 SidecarError::InvalidState(format!("{label} public key DER is invalid: {error}"))
15483 }),
15484 _ => Err(SidecarError::InvalidState(format!(
15485 "{label} unsupported key bytes format"
15486 ))),
15487 }
15488}
15489
15490fn javascript_crypto_parse_serialized_key_object(
15491 value: &Value,
15492 expected: Option<&str>,
15493 label: &str,
15494) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15495 let serialized: JavascriptSerializedSandboxKeyObject = serde_json::from_value(value.clone())
15496 .map_err(|error| {
15497 SidecarError::InvalidState(format!("{label} keyObject is invalid: {error}"))
15498 })?;
15499 match serialized.kind.as_str() {
15500 "secret" => {
15501 if expected == Some("public") || expected == Some("private") {
15502 return Err(SidecarError::InvalidState(format!(
15503 "{label} expected an asymmetric key"
15504 )));
15505 }
15506 Ok(JavascriptCryptoKeyMaterial::Secret(
15507 base64::engine::general_purpose::STANDARD
15508 .decode(serialized.raw.unwrap_or_default())
15509 .map_err(|error| {
15510 SidecarError::InvalidState(format!(
15511 "{label} secret key contains invalid base64: {error}"
15512 ))
15513 })?,
15514 ))
15515 }
15516 "private" => {
15517 let pem = serialized.pem.ok_or_else(|| {
15518 SidecarError::InvalidState(format!("{label} private keyObject is missing pem"))
15519 })?;
15520 javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("private"), label)
15521 }
15522 "public" => {
15523 let pem = serialized.pem.ok_or_else(|| {
15524 SidecarError::InvalidState(format!("{label} public keyObject is missing pem"))
15525 })?;
15526 javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("public"), label)
15527 }
15528 other => Err(SidecarError::InvalidState(format!(
15529 "{label} has unsupported keyObject type {other}"
15530 ))),
15531 }
15532}
15533
15534fn javascript_crypto_expect_private_key(
15535 key: JavascriptCryptoKeyMaterial,
15536 label: &str,
15537) -> Result<PKey<Private>, SidecarError> {
15538 match key {
15539 JavascriptCryptoKeyMaterial::Private(key) => Ok(key),
15540 _ => Err(SidecarError::InvalidState(format!(
15541 "{label} requires a private key"
15542 ))),
15543 }
15544}
15545
15546fn javascript_crypto_expect_public_key(
15547 key: JavascriptCryptoKeyMaterial,
15548 label: &str,
15549) -> Result<PKey<Public>, SidecarError> {
15550 match key {
15551 JavascriptCryptoKeyMaterial::Public(key) => Ok(key),
15552 JavascriptCryptoKeyMaterial::Private(key) => {
15553 let pem = key
15554 .public_key_to_pem()
15555 .map_err(javascript_crypto_openssl_error)?;
15556 PKey::public_key_from_pem(&pem).map_err(javascript_crypto_openssl_error)
15557 }
15558 _ => Err(SidecarError::InvalidState(format!(
15559 "{label} requires a public key"
15560 ))),
15561 }
15562}
15563
15564fn javascript_crypto_new_signer<'a>(
15565 algorithm: Option<&'a str>,
15566 key: &'a PKey<Private>,
15567) -> Result<Signer<'a>, SidecarError> {
15568 if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
15569 return Signer::new_without_digest(key).map_err(javascript_crypto_openssl_error);
15570 }
15571 Signer::new(
15572 javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
15573 SidecarError::InvalidState(String::from("crypto.sign requires a digest algorithm"))
15574 })?)?,
15575 key,
15576 )
15577 .map_err(javascript_crypto_openssl_error)
15578}
15579
15580fn javascript_crypto_new_verifier<'a>(
15581 algorithm: Option<&'a str>,
15582 key: &'a PKey<Public>,
15583) -> Result<Verifier<'a>, SidecarError> {
15584 if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
15585 return Verifier::new_without_digest(key).map_err(javascript_crypto_openssl_error);
15586 }
15587 Verifier::new(
15588 javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
15589 SidecarError::InvalidState(String::from("crypto.verify requires a digest algorithm"))
15590 })?)?,
15591 key,
15592 )
15593 .map_err(javascript_crypto_openssl_error)
15594}
15595
15596fn javascript_crypto_message_digest_from_name(name: &str) -> Result<MessageDigest, SidecarError> {
15597 match name.trim().to_ascii_lowercase().replace('-', "").as_str() {
15598 "md5" => Ok(MessageDigest::md5()),
15599 "sha1" => Ok(MessageDigest::sha1()),
15600 "sha256" => Ok(MessageDigest::sha256()),
15601 "sha384" => Ok(MessageDigest::sha384()),
15602 "sha512" => Ok(MessageDigest::sha512()),
15603 other => Err(SidecarError::InvalidState(format!(
15604 "unsupported crypto digest algorithm {other}"
15605 ))),
15606 }
15607}
15608
15609fn javascript_crypto_padding_from_value(value: &Value) -> Result<Option<Padding>, SidecarError> {
15610 let Some(number) = value.as_i64() else {
15611 return Ok(None);
15612 };
15613 let padding = match number {
15614 1 => Padding::PKCS1,
15615 3 => Padding::NONE,
15616 4 => Padding::PKCS1_OAEP,
15617 6 => Padding::PKCS1_PSS,
15618 other => {
15619 return Err(SidecarError::InvalidState(format!(
15620 "unsupported RSA padding constant {other}"
15621 )));
15622 }
15623 };
15624 Ok(Some(padding))
15625}
15626
15627fn javascript_crypto_decode_bridge_buffer(
15628 value: &Value,
15629 label: &str,
15630) -> Result<Vec<u8>, SidecarError> {
15631 let base64_value = value
15632 .as_object()
15633 .filter(|object| object.get("__type").and_then(Value::as_str) == Some("buffer"))
15634 .and_then(|object| object.get("value"))
15635 .and_then(Value::as_str)
15636 .ok_or_else(|| {
15637 SidecarError::InvalidState(format!("{label} must be a serialized bridge buffer"))
15638 })?;
15639 base64::engine::general_purpose::STANDARD
15640 .decode(base64_value)
15641 .map_err(|error| {
15642 SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
15643 })
15644}
15645
15646fn javascript_crypto_serialize_sandbox_key_object(
15647 key: &JavascriptCryptoKeyMaterial,
15648) -> Result<Value, SidecarError> {
15649 let serialized = match key {
15650 JavascriptCryptoKeyMaterial::Private(key) => JavascriptSerializedSandboxKeyObject {
15651 kind: String::from("private"),
15652 pem: Some(
15653 String::from_utf8(
15654 key.private_key_to_pem_pkcs8()
15655 .map_err(javascript_crypto_openssl_error)?,
15656 )
15657 .map_err(|error| {
15658 SidecarError::InvalidState(format!("private key PEM is not utf8: {error}"))
15659 })?,
15660 ),
15661 raw: None,
15662 asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
15663 asymmetric_key_details: None,
15664 jwk: None,
15665 },
15666 JavascriptCryptoKeyMaterial::Public(key) => JavascriptSerializedSandboxKeyObject {
15667 kind: String::from("public"),
15668 pem: Some(
15669 String::from_utf8(
15670 key.public_key_to_pem()
15671 .map_err(javascript_crypto_openssl_error)?,
15672 )
15673 .map_err(|error| {
15674 SidecarError::InvalidState(format!("public key PEM is not utf8: {error}"))
15675 })?,
15676 ),
15677 raw: None,
15678 asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
15679 asymmetric_key_details: None,
15680 jwk: None,
15681 },
15682 JavascriptCryptoKeyMaterial::Secret(raw) => JavascriptSerializedSandboxKeyObject {
15683 kind: String::from("secret"),
15684 pem: None,
15685 raw: Some(base64::engine::general_purpose::STANDARD.encode(raw)),
15686 asymmetric_key_type: None,
15687 asymmetric_key_details: None,
15688 jwk: None,
15689 },
15690 };
15691 serde_json::to_value(serialized)
15692 .map_err(|error| SidecarError::InvalidState(format!("serialize key object: {error}")))
15693}
15694
15695fn javascript_crypto_pkey_type_name(id: PKeyId) -> Option<String> {
15696 match id {
15697 PKeyId::RSA => Some(String::from("rsa")),
15698 PKeyId::EC => Some(String::from("ec")),
15699 PKeyId::ED25519 => Some(String::from("ed25519")),
15700 PKeyId::ED448 => Some(String::from("ed448")),
15701 PKeyId::X25519 => Some(String::from("x25519")),
15702 PKeyId::X448 => Some(String::from("x448")),
15703 PKeyId::DH => Some(String::from("dh")),
15704 _ => None,
15705 }
15706}
15707
15708fn javascript_crypto_rsa_output_size(
15709 key: &JavascriptCryptoKeyMaterial,
15710) -> Result<usize, SidecarError> {
15711 match key {
15712 JavascriptCryptoKeyMaterial::Private(key) => key
15713 .rsa()
15714 .map(|rsa| rsa.size() as usize)
15715 .map_err(javascript_crypto_openssl_error),
15716 JavascriptCryptoKeyMaterial::Public(key) => key
15717 .rsa()
15718 .map(|rsa| rsa.size() as usize)
15719 .map_err(javascript_crypto_openssl_error),
15720 JavascriptCryptoKeyMaterial::Secret(_) => Err(SidecarError::InvalidState(String::from(
15721 "RSA operations require an asymmetric key",
15722 ))),
15723 }
15724}
15725
15726fn javascript_crypto_parse_serialized_options_arg(
15727 args: &[Value],
15728 index: usize,
15729 label: &str,
15730) -> Result<Option<Value>, SidecarError> {
15731 let Some(raw) = args.get(index).and_then(Value::as_str) else {
15732 return Ok(None);
15733 };
15734 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
15735 SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
15736 })?;
15737 if parsed.get("hasOptions").and_then(Value::as_bool) == Some(true) {
15738 Ok(parsed.get("options").cloned())
15739 } else {
15740 Ok(None)
15741 }
15742}
15743
15744fn javascript_crypto_u32_from_bridge_value(
15745 value: &Value,
15746 label: &str,
15747) -> Result<u32, SidecarError> {
15748 if let Some(number) = value.as_u64() {
15749 return u32::try_from(number)
15750 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")));
15751 }
15752 let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
15753 if bytes.len() > 4 {
15754 return Err(SidecarError::InvalidState(format!(
15755 "{label} buffer is too large for u32"
15756 )));
15757 }
15758 Ok(bytes
15759 .into_iter()
15760 .fold(0_u32, |acc, byte| (acc << 8) | u32::from(byte)))
15761}
15762
15763fn javascript_crypto_bignum_from_bridge_value(
15764 value: &Value,
15765 label: &str,
15766) -> Result<BigNum, SidecarError> {
15767 if let Some(object) = value.as_object() {
15768 if object.get("__type").and_then(Value::as_str) == Some("bigint") {
15769 let decimal = object.get("value").and_then(Value::as_str).ok_or_else(|| {
15770 SidecarError::InvalidState(format!("{label} bigint is missing a value"))
15771 })?;
15772 return BigNum::from_dec_str(decimal).map_err(javascript_crypto_openssl_error);
15773 }
15774 }
15775 let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
15776 BigNum::from_slice(&bytes).map_err(javascript_crypto_openssl_error)
15777}
15778
15779fn javascript_crypto_curve_nid(name: &str) -> Result<Nid, SidecarError> {
15780 match name {
15781 "prime256v1" | "P-256" => Ok(Nid::X9_62_PRIME256V1),
15782 "secp384r1" | "P-384" => Ok(Nid::SECP384R1),
15783 "secp521r1" | "P-521" => Ok(Nid::SECP521R1),
15784 "secp256k1" => Ok(Nid::SECP256K1),
15785 other => Err(SidecarError::InvalidState(format!(
15786 "unsupported EC curve {other}"
15787 ))),
15788 }
15789}
15790
15791fn javascript_crypto_named_dh_group(name: &str) -> Result<Dh<Params>, SidecarError> {
15792 match name {
15793 "modp2" => Dh::get_1024_160().map_err(javascript_crypto_openssl_error),
15794 "modp14" | "modp15" | "modp16" | "modp17" | "modp18" => {
15795 Dh::get_2048_256().map_err(javascript_crypto_openssl_error)
15796 }
15797 other => Err(SidecarError::InvalidState(format!(
15798 "unsupported Diffie-Hellman group {other}"
15799 ))),
15800 }
15801}
15802
15803fn javascript_crypto_clone_dh_params(params: &Dh<Params>) -> Result<Dh<Params>, SidecarError> {
15804 Dh::from_pqg(
15805 params
15806 .prime_p()
15807 .to_owned()
15808 .map_err(javascript_crypto_openssl_error)?,
15809 params
15810 .prime_q()
15811 .map(|value| value.to_owned().map_err(javascript_crypto_openssl_error))
15812 .transpose()?,
15813 params
15814 .generator()
15815 .to_owned()
15816 .map_err(javascript_crypto_openssl_error)?,
15817 )
15818 .map_err(javascript_crypto_openssl_error)
15819}
15820
15821fn javascript_crypto_build_dh_params(args: &[Value]) -> Result<Dh<Params>, SidecarError> {
15822 let Some(first) = args.first() else {
15823 return Err(SidecarError::InvalidState(String::from(
15824 "Diffie-Hellman session args are required",
15825 )));
15826 };
15827 if let Some(bits) = first.as_u64() {
15828 let generator = args
15829 .get(1)
15830 .map(|value| javascript_crypto_u32_from_bridge_value(value, "Diffie-Hellman generator"))
15831 .transpose()?
15832 .unwrap_or(2);
15833 return Dh::generate_params(bits as u32, generator)
15834 .map_err(javascript_crypto_openssl_error);
15835 }
15836 let prime = javascript_crypto_bignum_from_bridge_value(first, "Diffie-Hellman prime")?;
15837 let generator = args
15838 .get(1)
15839 .map(|value| javascript_crypto_bignum_from_bridge_value(value, "Diffie-Hellman generator"))
15840 .transpose()?
15841 .unwrap_or(BigNum::from_u32(2).map_err(javascript_crypto_openssl_error)?);
15842 Dh::from_pqg(prime, None, generator).map_err(javascript_crypto_openssl_error)
15843}
15844
15845fn javascript_crypto_call_dh_session(
15846 session: &mut ActiveDhSession,
15847 method: &str,
15848 args: &[Value],
15849) -> Result<(Value, bool), SidecarError> {
15850 match method {
15851 "verifyError" => Ok((Value::Null, false)),
15852 "generateKeys" => {
15853 if session.key_pair.is_none() {
15854 session.key_pair = Some(
15855 javascript_crypto_clone_dh_params(&session.params)?
15856 .generate_key()
15857 .map_err(javascript_crypto_openssl_error)?,
15858 );
15859 }
15860 let public = session
15861 .key_pair
15862 .as_ref()
15863 .expect("dh key pair")
15864 .public_key()
15865 .to_vec();
15866 Ok((javascript_crypto_bridge_buffer_value(&public), true))
15867 }
15868 "computeSecret" => {
15869 if session.key_pair.is_none() {
15870 session.key_pair = Some(
15871 javascript_crypto_clone_dh_params(&session.params)?
15872 .generate_key()
15873 .map_err(javascript_crypto_openssl_error)?,
15874 );
15875 }
15876 let peer = javascript_crypto_bignum_from_bridge_value(
15877 args.first().ok_or_else(|| {
15878 SidecarError::InvalidState(String::from(
15879 "computeSecret requires peer public key",
15880 ))
15881 })?,
15882 "Diffie-Hellman peer public key",
15883 )?;
15884 let secret = session
15885 .key_pair
15886 .as_ref()
15887 .expect("dh key pair")
15888 .compute_key(&peer)
15889 .map_err(javascript_crypto_openssl_error)?;
15890 Ok((javascript_crypto_bridge_buffer_value(&secret), true))
15891 }
15892 "getPrime" => Ok((
15893 javascript_crypto_bridge_buffer_value(&session.params.prime_p().to_vec()),
15894 true,
15895 )),
15896 "getGenerator" => Ok((
15897 javascript_crypto_bridge_buffer_value(&session.params.generator().to_vec()),
15898 true,
15899 )),
15900 "getPublicKey" => {
15901 if session.key_pair.is_none() {
15902 session.key_pair = Some(
15903 javascript_crypto_clone_dh_params(&session.params)?
15904 .generate_key()
15905 .map_err(javascript_crypto_openssl_error)?,
15906 );
15907 }
15908 Ok((
15909 javascript_crypto_bridge_buffer_value(
15910 &session
15911 .key_pair
15912 .as_ref()
15913 .expect("dh key pair")
15914 .public_key()
15915 .to_vec(),
15916 ),
15917 true,
15918 ))
15919 }
15920 "getPrivateKey" => {
15921 if session.key_pair.is_none() {
15922 session.key_pair = Some(
15923 javascript_crypto_clone_dh_params(&session.params)?
15924 .generate_key()
15925 .map_err(javascript_crypto_openssl_error)?,
15926 );
15927 }
15928 Ok((
15929 javascript_crypto_bridge_buffer_value(
15930 &session
15931 .key_pair
15932 .as_ref()
15933 .expect("dh key pair")
15934 .private_key()
15935 .to_vec(),
15936 ),
15937 true,
15938 ))
15939 }
15940 other => Err(SidecarError::InvalidState(format!(
15941 "Unsupported Diffie-Hellman method: {other}"
15942 ))),
15943 }
15944}
15945
15946fn javascript_crypto_call_ecdh_session(
15947 session: &mut ActiveEcdhSession,
15948 method: &str,
15949 args: &[Value],
15950) -> Result<(Value, bool), SidecarError> {
15951 let nid = javascript_crypto_curve_nid(&session.curve)?;
15952 let group = EcGroup::from_curve_name(nid).map_err(javascript_crypto_openssl_error)?;
15953 match method {
15954 "verifyError" => Ok((Value::Null, false)),
15955 "generateKeys" => {
15956 if session.key_pair.is_none() {
15957 session.key_pair =
15958 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15959 }
15960 let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15961 let bytes = session
15962 .key_pair
15963 .as_ref()
15964 .expect("ecdh key pair")
15965 .public_key()
15966 .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
15967 .map_err(javascript_crypto_openssl_error)?;
15968 Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
15969 }
15970 "computeSecret" => {
15971 if session.key_pair.is_none() {
15972 session.key_pair =
15973 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15974 }
15975 let peer_bytes = javascript_crypto_decode_bridge_buffer(
15976 args.first().ok_or_else(|| {
15977 SidecarError::InvalidState(String::from(
15978 "computeSecret requires peer public key",
15979 ))
15980 })?,
15981 "ECDH peer public key",
15982 )?;
15983 let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15984 let peer_point = EcPoint::from_bytes(&group, &peer_bytes, &mut ctx)
15985 .map_err(javascript_crypto_openssl_error)?;
15986 let peer_key = EcKey::from_public_key(&group, &peer_point)
15987 .map_err(javascript_crypto_openssl_error)?;
15988 let private =
15989 PKey::from_ec_key(session.key_pair.as_ref().expect("ecdh key pair").to_owned())
15990 .map_err(javascript_crypto_openssl_error)?;
15991 let peer = PKey::from_ec_key(peer_key).map_err(javascript_crypto_openssl_error)?;
15992 let mut deriver = Deriver::new(&private).map_err(javascript_crypto_openssl_error)?;
15993 deriver
15994 .set_peer(&peer)
15995 .map_err(javascript_crypto_openssl_error)?;
15996 let secret = deriver
15997 .derive_to_vec()
15998 .map_err(javascript_crypto_openssl_error)?;
15999 Ok((javascript_crypto_bridge_buffer_value(&secret), true))
16000 }
16001 "getPublicKey" => {
16002 if session.key_pair.is_none() {
16003 session.key_pair =
16004 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
16005 }
16006 let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
16007 let bytes = session
16008 .key_pair
16009 .as_ref()
16010 .expect("ecdh key pair")
16011 .public_key()
16012 .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
16013 .map_err(javascript_crypto_openssl_error)?;
16014 Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
16015 }
16016 "getPrivateKey" => {
16017 if session.key_pair.is_none() {
16018 session.key_pair =
16019 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
16020 }
16021 Ok((
16022 javascript_crypto_bridge_buffer_value(
16023 &session
16024 .key_pair
16025 .as_ref()
16026 .expect("ecdh key pair")
16027 .private_key()
16028 .to_vec(),
16029 ),
16030 true,
16031 ))
16032 }
16033 other => Err(SidecarError::InvalidState(format!(
16034 "Unsupported Diffie-Hellman method: {other}"
16035 ))),
16036 }
16037}
16038
16039fn javascript_crypto_serialize_encoded_key_value_public(
16040 key: &PKey<Public>,
16041 encoding: Option<&Value>,
16042) -> Result<Value, SidecarError> {
16043 if let Some(encoding) = encoding {
16044 let format = encoding
16045 .get("format")
16046 .and_then(Value::as_str)
16047 .unwrap_or("pem");
16048 return Ok(match format {
16049 "der" => json!({
16050 "kind": "buffer",
16051 "value": base64::engine::general_purpose::STANDARD
16052 .encode(key.public_key_to_der().map_err(javascript_crypto_openssl_error)?),
16053 }),
16054 _ => json!({
16055 "kind": "string",
16056 "value": String::from_utf8(
16057 key.public_key_to_pem().map_err(javascript_crypto_openssl_error)?,
16058 )
16059 .map_err(|error| SidecarError::InvalidState(format!("public key PEM utf8: {error}")))?,
16060 }),
16061 });
16062 }
16063 javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(
16064 key.to_owned(),
16065 ))
16066}
16067
16068fn javascript_crypto_serialize_encoded_key_value_private(
16069 key: &PKey<Private>,
16070 encoding: Option<&Value>,
16071) -> Result<Value, SidecarError> {
16072 if let Some(encoding) = encoding {
16073 let format = encoding
16074 .get("format")
16075 .and_then(Value::as_str)
16076 .unwrap_or("pem");
16077 return Ok(match format {
16078 "der" => json!({
16079 "kind": "buffer",
16080 "value": base64::engine::general_purpose::STANDARD
16081 .encode(key.private_key_to_der().map_err(javascript_crypto_openssl_error)?),
16082 }),
16083 _ => json!({
16084 "kind": "string",
16085 "value": String::from_utf8(
16086 key.private_key_to_pem_pkcs8().map_err(javascript_crypto_openssl_error)?,
16087 )
16088 .map_err(|error| SidecarError::InvalidState(format!("private key PEM utf8: {error}")))?,
16089 }),
16090 });
16091 }
16092 javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(
16093 key.to_owned(),
16094 ))
16095}
16096
16097fn javascript_crypto_bridge_buffer_value(bytes: &[u8]) -> Value {
16098 json!({
16099 "__type": "buffer",
16100 "value": base64::engine::general_purpose::STANDARD.encode(bytes),
16101 })
16102}
16103
16104fn javascript_crypto_build_cipher_context(
16105 algorithm: &str,
16106 key: &[u8],
16107 iv: Option<&[u8]>,
16108 decrypt: bool,
16109 options: Option<&Value>,
16110) -> Result<Crypter, SidecarError> {
16111 let cipher = javascript_crypto_cipher_from_name(algorithm)?;
16112 let mode = if decrypt {
16113 Mode::Decrypt
16114 } else {
16115 Mode::Encrypt
16116 };
16117 let mut context =
16118 Crypter::new(cipher, mode, key, iv).map_err(javascript_crypto_openssl_error)?;
16119 if let Some(auto_padding) = options
16120 .and_then(|value| value.get("autoPadding"))
16121 .and_then(Value::as_bool)
16122 {
16123 context.pad(auto_padding);
16124 }
16125 if javascript_crypto_is_aead(algorithm) {
16126 if let Some(aad) = options
16127 .and_then(|value| value.get("aad"))
16128 .and_then(Value::as_str)
16129 {
16130 context
16131 .aad_update(
16132 &base64::engine::general_purpose::STANDARD
16133 .decode(aad)
16134 .map_err(|error| {
16135 SidecarError::InvalidState(format!(
16136 "cipher aad contains invalid base64: {error}"
16137 ))
16138 })?,
16139 )
16140 .map_err(javascript_crypto_openssl_error)?;
16141 }
16142 if decrypt {
16143 if let Some(auth_tag) = options
16144 .and_then(|value| value.get("authTag"))
16145 .and_then(Value::as_str)
16146 {
16147 let decoded = base64::engine::general_purpose::STANDARD
16148 .decode(auth_tag)
16149 .map_err(|error| {
16150 SidecarError::InvalidState(format!(
16151 "cipher authTag contains invalid base64: {error}"
16152 ))
16153 })?;
16154 context
16155 .set_tag(&decoded)
16156 .map_err(javascript_crypto_openssl_error)?;
16157 }
16158 }
16159 }
16160 Ok(context)
16161}
16162
16163fn javascript_crypto_requested_aead_tag_len(
16164 algorithm: &str,
16165 options: Option<&Value>,
16166) -> Result<usize, SidecarError> {
16167 if !javascript_crypto_is_aead(algorithm) {
16168 return Ok(0);
16169 }
16170 let requested = options
16171 .and_then(|value| value.get("authTagLength"))
16172 .and_then(Value::as_u64)
16173 .unwrap_or(javascript_crypto_aead_tag_len(algorithm) as u64);
16174 usize::try_from(requested).map_err(|_| {
16175 SidecarError::InvalidState(String::from("cipher authTagLength must fit within usize"))
16176 })
16177}
16178
16179fn javascript_crypto_cipher_update(
16180 context: &mut Crypter,
16181 data: &[u8],
16182) -> Result<Vec<u8>, SidecarError> {
16183 let mut output = vec![0_u8; data.len() + 32];
16184 let written = context
16185 .update(data, &mut output)
16186 .map_err(javascript_crypto_openssl_error)?;
16187 output.truncate(written);
16188 Ok(output)
16189}
16190
16191fn javascript_crypto_cipher_finalize(context: &mut Crypter) -> Result<Vec<u8>, SidecarError> {
16192 let mut output = vec![0_u8; 32];
16193 let written = context
16194 .finalize(&mut output)
16195 .map_err(javascript_crypto_openssl_error)?;
16196 output.truncate(written);
16197 Ok(output)
16198}
16199
16200fn javascript_crypto_cipher_from_name(name: &str) -> Result<Cipher, SidecarError> {
16201 match name.to_ascii_lowercase().as_str() {
16202 "aes-128-cbc" => Ok(Cipher::aes_128_cbc()),
16203 "aes-192-cbc" => Ok(Cipher::aes_192_cbc()),
16204 "aes-256-cbc" => Ok(Cipher::aes_256_cbc()),
16205 "aes-128-ctr" => Ok(Cipher::aes_128_ctr()),
16206 "aes-192-ctr" => Ok(Cipher::aes_192_ctr()),
16207 "aes-256-ctr" => Ok(Cipher::aes_256_ctr()),
16208 "aes-128-gcm" => Ok(Cipher::aes_128_gcm()),
16209 "aes-192-gcm" => Ok(Cipher::aes_192_gcm()),
16210 "aes-256-gcm" => Ok(Cipher::aes_256_gcm()),
16211 other => Err(SidecarError::InvalidState(format!(
16212 "unsupported crypto cipher algorithm {other}"
16213 ))),
16214 }
16215}
16216
16217fn javascript_crypto_is_aead(algorithm: &str) -> bool {
16218 algorithm.to_ascii_lowercase().ends_with("-gcm")
16219}
16220
16221fn javascript_crypto_aead_tag_len(_algorithm: &str) -> usize {
16222 16
16223}
16224
16225fn javascript_crypto_openssl_error(error: openssl::error::ErrorStack) -> SidecarError {
16226 SidecarError::Execution(format!("crypto operation failed: {error}"))
16227}
16228
16229fn service_javascript_kernel_stdin_sync_rpc(
16230 kernel: &mut SidecarKernel,
16231 process: &mut ActiveProcess,
16232 request: &JavascriptSyncRpcRequest,
16233) -> Result<Value, SidecarError> {
16234 let max_bytes =
16235 javascript_sync_rpc_arg_u64_optional(&request.args, 0, "__kernel_stdin_read max bytes")?
16236 .map(|value| value.clamp(1, DEFAULT_KERNEL_STDIN_READ_MAX_BYTES as u64) as usize)
16237 .unwrap_or(DEFAULT_KERNEL_STDIN_READ_MAX_BYTES);
16238 let timeout_ms =
16239 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_stdin_read timeout ms")?
16240 .unwrap_or(DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS);
16241
16242 match kernel
16243 .fd_read_with_timeout_result(
16244 EXECUTION_DRIVER_NAME,
16245 process.kernel_pid,
16246 0,
16247 max_bytes,
16248 Some(Duration::from_millis(timeout_ms)),
16249 )
16250 .map_err(kernel_error)
16251 {
16252 Ok(Some(chunk)) if !chunk.is_empty() => Ok(json!({
16253 "dataBase64": base64::engine::general_purpose::STANDARD.encode(chunk),
16254 })),
16255 Ok(Some(_)) => Ok(Value::Null),
16256 Ok(None) => Ok(json!({
16257 "done": true,
16258 })),
16259 Err(SidecarError::Kernel(error)) if error.starts_with("EAGAIN:") => Ok(Value::Null),
16260 Err(error) => Err(error),
16261 }
16262}
16263
16264fn service_javascript_pty_set_raw_mode_sync_rpc(
16265 kernel: &mut SidecarKernel,
16266 process: &mut ActiveProcess,
16267 request: &JavascriptSyncRpcRequest,
16268) -> Result<Value, SidecarError> {
16269 let enabled = javascript_sync_rpc_arg_bool(&request.args, 0, "__pty_set_raw_mode enabled")?;
16270 kernel
16271 .pty_set_discipline(
16272 EXECUTION_DRIVER_NAME,
16273 process.kernel_pid,
16274 0,
16275 LineDisciplineConfig {
16276 canonical: Some(!enabled),
16277 echo: Some(!enabled),
16278 isig: Some(!enabled),
16279 },
16280 )
16281 .map_err(kernel_error)?;
16282 Ok(Value::Null)
16283}
16284
16285fn service_javascript_kernel_stdio_write_sync_rpc(
16286 kernel: &mut SidecarKernel,
16287 process: &mut ActiveProcess,
16288 request: &JavascriptSyncRpcRequest,
16289) -> Result<Value, SidecarError> {
16290 let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_stdio_write fd")?;
16291 let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "__kernel_stdio_write chunk")?;
16292
16293 let written = match fd {
16294 1 => kernel
16295 .write_process_stdout(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
16296 .map_err(kernel_error)?,
16297 2 => kernel
16298 .write_process_stderr(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
16299 .map_err(kernel_error)?,
16300 other => {
16301 return Err(SidecarError::InvalidState(format!(
16302 "__kernel_stdio_write only supports fd 1/2, got {other}"
16303 )));
16304 }
16305 };
16306
16307 let event = if fd == 1 {
16308 ActiveExecutionEvent::Stdout(chunk)
16309 } else {
16310 ActiveExecutionEvent::Stderr(chunk)
16311 };
16312 process.queue_pending_execution_event(event)?;
16313
16314 Ok(json!(written))
16315}
16316
16317fn service_javascript_kernel_poll_sync_rpc(
16318 kernel: &mut SidecarKernel,
16319 process: &ActiveProcess,
16320 request: &JavascriptSyncRpcRequest,
16321) -> Result<Value, SidecarError> {
16322 let fd_requests: Vec<KernelPollFdRequest> = serde_json::from_value(
16323 request
16324 .args
16325 .first()
16326 .cloned()
16327 .unwrap_or_else(|| Value::Array(Vec::new())),
16328 )
16329 .map_err(|error| {
16330 SidecarError::InvalidState(format!(
16331 "__kernel_poll fd list must be a JSON array of {{ fd, events }} objects: {error}"
16332 ))
16333 })?;
16334 let timeout_ms =
16335 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_poll timeout ms")?
16336 .unwrap_or_default();
16337 let timeout_ms = i32::try_from(timeout_ms).map_err(|_| {
16338 SidecarError::InvalidState(String::from("__kernel_poll timeout ms must fit within i32"))
16339 })?;
16340
16341 let poll_fds = fd_requests
16342 .iter()
16343 .map(|entry| PollFd {
16344 fd: entry.fd,
16345 events: PollEvents::from_bits(entry.events),
16346 revents: PollEvents::empty(),
16347 })
16348 .collect::<Vec<_>>();
16349 let result = kernel
16350 .poll_fds(
16351 EXECUTION_DRIVER_NAME,
16352 process.kernel_pid,
16353 poll_fds,
16354 timeout_ms,
16355 )
16356 .map_err(kernel_error)?;
16357
16358 Ok(json!({
16359 "readyCount": result.ready_count,
16360 "fds": result
16361 .fds
16362 .into_iter()
16363 .map(|entry| KernelPollFdResponse {
16364 fd: entry.fd,
16365 events: entry.events.bits(),
16366 revents: entry.revents.bits(),
16367 })
16368 .collect::<Vec<_>>(),
16369 }))
16370}
16371
16372fn install_kernel_stdin_pipe(kernel: &mut SidecarKernel, pid: u32) -> Result<u32, SidecarError> {
16373 let (read_fd, write_fd) = kernel
16374 .open_pipe(EXECUTION_DRIVER_NAME, pid)
16375 .map_err(kernel_error)?;
16376 kernel
16377 .fd_dup2(EXECUTION_DRIVER_NAME, pid, read_fd, 0)
16378 .map_err(kernel_error)?;
16379 kernel
16380 .fd_close(EXECUTION_DRIVER_NAME, pid, read_fd)
16381 .map_err(kernel_error)?;
16382 Ok(write_fd)
16383}
16384
16385fn javascript_child_process_stdin_mode(request: &JavascriptChildProcessSpawnRequest) -> &str {
16386 request
16387 .options
16388 .stdio
16389 .first()
16390 .map(String::as_str)
16391 .unwrap_or("pipe")
16392}
16393
16394pub(crate) fn write_kernel_process_stdin(
16395 kernel: &mut SidecarKernel,
16396 process: &mut ActiveProcess,
16397 chunk: &[u8],
16398) -> Result<(), SidecarError> {
16399 if process.runtime == GuestRuntimeKind::JavaScript {
16400 return Ok(());
16401 }
16402 let Some(writer_fd) = process.kernel_stdin_writer_fd else {
16403 return Ok(());
16404 };
16405 kernel
16406 .fd_write(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd, chunk)
16407 .map(|_| ())
16408 .map_err(kernel_error)
16409}
16410
16411pub(crate) fn close_kernel_process_stdin(
16412 kernel: &mut SidecarKernel,
16413 process: &mut ActiveProcess,
16414) -> Result<(), SidecarError> {
16415 let Some(writer_fd) = process.kernel_stdin_writer_fd.take() else {
16416 return Ok(());
16417 };
16418 kernel
16419 .fd_close(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd)
16420 .map_err(kernel_error)
16421}
16422
16423fn parse_http_header_collection(
16424 headers: &BTreeMap<String, Value>,
16425 label: &str,
16426) -> Result<HttpHeaderCollection, SidecarError> {
16427 let mut normalized = BTreeMap::<String, Vec<String>>::new();
16428 let mut raw_pairs = Vec::new();
16429
16430 for (raw_name, value) in headers {
16431 let normalized_name = raw_name.to_ascii_lowercase();
16432 let values = match value {
16433 Value::String(text) => vec![text.clone()],
16434 Value::Array(values) => values
16435 .iter()
16436 .map(|entry| {
16437 entry.as_str().map(str::to_owned).ok_or_else(|| {
16438 SidecarError::InvalidState(format!(
16439 "{label} header {raw_name} must contain only strings"
16440 ))
16441 })
16442 })
16443 .collect::<Result<Vec<_>, _>>()?,
16444 other => {
16445 return Err(SidecarError::InvalidState(format!(
16446 "{label} header {raw_name} must be a string or string array, received {other}"
16447 )));
16448 }
16449 };
16450 raw_pairs.extend(
16451 values
16452 .iter()
16453 .cloned()
16454 .map(|entry| (raw_name.clone(), entry)),
16455 );
16456 normalized
16457 .entry(normalized_name)
16458 .or_default()
16459 .extend(values);
16460 }
16461
16462 Ok(HttpHeaderCollection {
16463 normalized,
16464 raw_pairs,
16465 })
16466}
16467
16468fn http_headers_json(headers: &HttpHeaderCollection) -> Value {
16469 let map = headers
16470 .normalized
16471 .iter()
16472 .map(|(name, values)| {
16473 let value = if values.len() == 1 {
16474 Value::String(values[0].clone())
16475 } else {
16476 Value::Array(values.iter().cloned().map(Value::String).collect())
16477 };
16478 (name.clone(), value)
16479 })
16480 .collect::<Map<String, Value>>();
16481 Value::Object(map)
16482}
16483
16484fn http_raw_headers_json(headers: &HttpHeaderCollection) -> Value {
16485 Value::Array(
16486 headers
16487 .raw_pairs
16488 .iter()
16489 .flat_map(|(name, value)| [Value::String(name.clone()), Value::String(value.clone())])
16490 .collect(),
16491 )
16492}
16493
16494fn is_loopback_request_host(host: &str) -> bool {
16495 let bare = host
16496 .strip_prefix('[')
16497 .and_then(|value| value.strip_suffix(']'))
16498 .unwrap_or(host);
16499 matches!(bare, "localhost" | "127.0.0.1" | "::1")
16500}
16501
16502fn serialize_http_loopback_request(
16503 url: &Url,
16504 options: &JavascriptHttpRequestOptions,
16505 headers: &HttpHeaderCollection,
16506) -> Result<String, SidecarError> {
16507 let body_base64 = options
16508 .body
16509 .as_ref()
16510 .map(|body| base64::engine::general_purpose::STANDARD.encode(body.as_bytes()));
16511 serde_json::to_string(&json!({
16512 "method": options.method.clone().unwrap_or_else(|| String::from("GET")),
16513 "url": http_request_target(url),
16514 "headers": http_headers_json(headers),
16515 "rawHeaders": http_raw_headers_json(headers),
16516 "bodyBase64": body_base64,
16517 }))
16518 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
16519}
16520
16521fn http_request_target(url: &Url) -> String {
16522 let path = if url.path().is_empty() {
16523 "/"
16524 } else {
16525 url.path()
16526 };
16527 format!(
16528 "{path}{}",
16529 url.query()
16530 .map(|query| format!("?{query}"))
16531 .unwrap_or_default()
16532 )
16533}
16534
16535fn find_kernel_http_listener_process(vm: &VmState, port: u16) -> Option<String> {
16536 vm.active_processes
16537 .iter()
16538 .find_map(|(process_id, process)| {
16539 process.tcp_listeners.values().find_map(|listener| {
16540 let socket_id = listener.kernel_socket_id?;
16541 let record = vm.kernel.socket_get(socket_id)?;
16542 let local_addr = record
16543 .local_address()
16544 .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
16545 .unwrap_or_else(|| listener.guest_local_addr());
16546 if local_addr.port() == port && is_vm_local_http_listener_addr(local_addr.ip()) {
16547 Some(process_id.to_owned())
16548 } else {
16549 None
16550 }
16551 })
16552 })
16553}
16554
16555fn is_vm_local_http_listener_addr(ip: IpAddr) -> bool {
16556 ip.is_loopback() || ip.is_unspecified()
16557}
16558
16559fn serialize_kernel_http_fetch_request(
16560 port: u16,
16561 path: &str,
16562 options: &JavascriptHttpRequestOptions,
16563 headers: &HttpHeaderCollection,
16564) -> Vec<u8> {
16565 let method = options.method.as_deref().unwrap_or("GET");
16566 let mut lines = vec![format!("{method} {path} HTTP/1.1")];
16567 let mut has_host = false;
16568 let mut has_connection = false;
16569 let mut has_content_length = false;
16570 for (name, values) in &headers.normalized {
16571 match name.as_str() {
16572 "host" => has_host = true,
16573 "connection" => has_connection = true,
16574 "content-length" => has_content_length = true,
16575 _ => {}
16576 }
16577 lines.push(format!("{name}: {}", values.join(", ")));
16578 }
16579 if !has_host {
16580 lines.push(format!("Host: 127.0.0.1:{port}"));
16581 }
16582 if !has_connection {
16583 lines.push(String::from("Connection: close"));
16584 }
16585 let body = options.body.as_deref().unwrap_or("").as_bytes();
16586 if !has_content_length && !body.is_empty() {
16587 lines.push(format!("Content-Length: {}", body.len()));
16588 }
16589 lines.push(String::new());
16590 lines.push(String::new());
16591
16592 let mut request = lines.join("\r\n").into_bytes();
16593 request.extend_from_slice(body);
16594 request
16595}
16596
16597fn parse_kernel_http_fetch_response(
16598 buffer: &[u8],
16599 peer_closed: bool,
16600 url: &str,
16601) -> Result<Option<String>, SidecarError> {
16602 let Some(header_end) = find_http_header_end(buffer) else {
16603 return Ok(None);
16604 };
16605 let header_bytes = &buffer[..header_end];
16606 let head = String::from_utf8_lossy(header_bytes);
16607 let mut lines = head.split("\r\n");
16608 let status_line = lines.next().unwrap_or_default();
16609 let mut status_parts = status_line.splitn(3, ' ');
16610 let version = status_parts.next().unwrap_or_default();
16611 if !version.starts_with("HTTP/") {
16612 return Err(SidecarError::Execution(format!(
16613 "invalid vm.fetch HTTP response status line: {status_line}"
16614 )));
16615 }
16616 let status = status_parts
16617 .next()
16618 .ok_or_else(|| {
16619 SidecarError::Execution(format!(
16620 "invalid vm.fetch HTTP response status line: {status_line}"
16621 ))
16622 })?
16623 .parse::<u16>()
16624 .map_err(|error| {
16625 SidecarError::Execution(format!(
16626 "invalid vm.fetch HTTP response status code in {status_line:?}: {error}"
16627 ))
16628 })?;
16629 let status_text = status_parts.next().unwrap_or_default();
16630 let mut headers = Vec::new();
16631 let mut raw_headers = Vec::new();
16632 let mut content_length = None;
16633 let mut transfer_encoding_values = Vec::new();
16634 for line in lines {
16635 if line.is_empty() {
16636 continue;
16637 }
16638 let Some((name, value)) = line.split_once(':') else {
16639 return Err(SidecarError::Execution(format!(
16640 "invalid vm.fetch HTTP response header line: {line}"
16641 )));
16642 };
16643 let value = value.trim().to_owned();
16644 let normalized = name.to_ascii_lowercase();
16645 if normalized == "content-length" {
16646 content_length = Some(value.parse::<usize>().map_err(|error| {
16647 SidecarError::Execution(format!(
16648 "invalid vm.fetch Content-Length header {value:?}: {error}"
16649 ))
16650 })?);
16651 } else if normalized == "transfer-encoding" {
16652 transfer_encoding_values.push(value.clone());
16653 }
16654 headers.push(json!([normalized, value.clone()]));
16655 raw_headers.push(Value::String(name.to_owned()));
16656 raw_headers.push(Value::String(value));
16657 }
16658
16659 let body_start = header_end + 4;
16660 let transfer_encoding = transfer_encoding_tokens(&transfer_encoding_values);
16661 let is_chunked = transfer_encoding.iter().any(|token| token == "chunked");
16662 let body = if is_chunked {
16663 if content_length.is_some() {
16664 return Err(SidecarError::Execution(String::from(
16665 "vm.fetch HTTP response cannot include both Transfer-Encoding: chunked and Content-Length",
16666 )));
16667 }
16668 if transfer_encoding.len() != 1 {
16669 return Err(SidecarError::Execution(format!(
16670 "unsupported vm.fetch Transfer-Encoding: {}",
16671 transfer_encoding.join(", ")
16672 )));
16673 }
16674 let Some(decoded) = decode_kernel_http_chunked_body(&buffer[body_start..])? else {
16675 return Ok(None);
16676 };
16677 decoded
16678 } else if !transfer_encoding.is_empty() {
16679 return Err(SidecarError::Execution(format!(
16680 "unsupported vm.fetch Transfer-Encoding: {}",
16681 transfer_encoding.join(", ")
16682 )));
16683 } else if let Some(content_length) = content_length {
16684 let body_end = body_start.saturating_add(content_length);
16685 if body_end > VM_FETCH_BUFFER_LIMIT_BYTES {
16693 return Err(SidecarError::Execution(format!(
16694 "vm.fetch raw response buffer is {body_end} bytes, limit is {VM_FETCH_BUFFER_LIMIT_BYTES}"
16695 )));
16696 }
16697 if buffer.len() < body_end {
16698 return Ok(None);
16699 }
16700 buffer[body_start..body_end].to_vec()
16701 } else if peer_closed {
16702 buffer[body_start..].to_vec()
16703 } else {
16704 return Ok(None);
16705 };
16706
16707 serde_json::to_string(&json!({
16708 "status": status,
16709 "statusText": status_text,
16710 "headers": headers,
16711 "rawHeaders": raw_headers,
16712 "body": base64::engine::general_purpose::STANDARD.encode(&body),
16713 "bodyEncoding": "base64",
16714 "url": url,
16715 }))
16716 .map(Some)
16717 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
16718}
16719
16720fn find_http_header_end(buffer: &[u8]) -> Option<usize> {
16721 buffer.windows(4).position(|window| window == b"\r\n\r\n")
16722}
16723
16724fn find_crlf(buffer: &[u8], start: usize) -> Option<usize> {
16725 buffer
16726 .get(start..)?
16727 .windows(2)
16728 .position(|window| window == b"\r\n")
16729 .map(|offset| start + offset)
16730}
16731
16732fn transfer_encoding_tokens(values: &[String]) -> Vec<String> {
16733 values
16734 .iter()
16735 .flat_map(|value| value.split(','))
16736 .map(|token| token.trim().to_ascii_lowercase())
16737 .filter(|token| !token.is_empty())
16738 .collect()
16739}
16740
16741fn decode_kernel_http_chunked_body(buffer: &[u8]) -> Result<Option<Vec<u8>>, SidecarError> {
16742 let mut offset = 0;
16743 let mut body = Vec::new();
16744 loop {
16745 let Some(line_end) = find_crlf(buffer, offset) else {
16746 return Ok(None);
16747 };
16748 let size_line = std::str::from_utf8(&buffer[offset..line_end]).map_err(|error| {
16749 SidecarError::Execution(format!(
16750 "invalid vm.fetch chunk size line encoding: {error}"
16751 ))
16752 })?;
16753 let size_part = size_line.split(';').next().unwrap_or_default();
16754 if size_part.is_empty() || !size_part.bytes().all(|byte| byte.is_ascii_hexdigit()) {
16755 return Err(SidecarError::Execution(format!(
16756 "invalid vm.fetch chunk size line: {size_line:?}"
16757 )));
16758 }
16759 let chunk_size = usize::from_str_radix(size_part, 16).map_err(|error| {
16760 SidecarError::Execution(format!(
16761 "invalid vm.fetch chunk size {size_part:?}: {error}"
16762 ))
16763 })?;
16764 let chunk_start = line_end + 2;
16765 let chunk_end = chunk_start
16766 .checked_add(chunk_size)
16767 .ok_or_else(|| SidecarError::Execution(String::from("vm.fetch chunk size overflow")))?;
16768 if chunk_size > 0 {
16769 let chunk_terminator_end = chunk_end.checked_add(2).ok_or_else(|| {
16770 SidecarError::Execution(String::from("vm.fetch chunk terminator overflow"))
16771 })?;
16772 if chunk_terminator_end > buffer.len() {
16773 return Ok(None);
16774 }
16775 if buffer.get(chunk_end..chunk_terminator_end) != Some(b"\r\n") {
16776 return Err(SidecarError::Execution(String::from(
16777 "invalid vm.fetch chunk terminator",
16778 )));
16779 }
16780 body.extend_from_slice(&buffer[chunk_start..chunk_end]);
16781 offset = chunk_terminator_end;
16782 continue;
16783 }
16784
16785 if buffer.get(chunk_start..chunk_start + 2) == Some(b"\r\n") {
16786 return Ok(Some(body));
16787 }
16788 let Some(trailer_end) = find_http_header_end(&buffer[chunk_start..]) else {
16789 return Ok(None);
16790 };
16791 let trailer_bytes = &buffer[chunk_start..chunk_start + trailer_end];
16792 let trailers = String::from_utf8_lossy(trailer_bytes);
16793 for line in trailers.split("\r\n") {
16794 if line.is_empty() {
16795 continue;
16796 }
16797 if line.starts_with(' ') || line.starts_with('\t') || !line.contains(':') {
16798 return Err(SidecarError::Execution(format!(
16799 "invalid vm.fetch chunk trailer line: {line}"
16800 )));
16801 }
16802 }
16803 return Ok(Some(body));
16804 }
16805}
16806
16807fn kernel_http_fetch_target_exit_code(error: &SidecarError) -> Option<i32> {
16808 let SidecarError::Execution(message) = error else {
16809 return None;
16810 };
16811 message
16812 .strip_prefix("vm.fetch target exited before responding (exit code ")?
16813 .strip_suffix(')')?
16814 .parse()
16815 .ok()
16816}
16817
16818#[allow(clippy::too_many_arguments)]
16819fn service_host_fetch_target_event<B>(
16820 bridge: &SharedBridge<B>,
16821 vm_id: &str,
16822 dns: &VmDnsConfig,
16823 socket_paths: &JavascriptSocketPathContext,
16824 kernel: &mut SidecarKernel,
16825 process: &mut ActiveProcess,
16826 resource_limits: &ResourceLimits,
16827 wait: Duration,
16828) -> Result<bool, SidecarError>
16829where
16830 B: NativeSidecarBridge + Send + 'static,
16831 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16832{
16833 let Some(event) = process
16834 .execution
16835 .poll_event_blocking(wait)
16836 .map_err(|error| SidecarError::Execution(error.to_string()))?
16837 else {
16838 return Ok(false);
16839 };
16840
16841 match event {
16842 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
16843 let network_counts = process.network_resource_counts();
16844 let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
16845 bridge,
16846 vm_id,
16847 dns,
16848 socket_paths,
16849 kernel,
16850 process,
16851 sync_request: &request,
16852 resource_limits,
16853 network_counts,
16854 });
16855 match response {
16856 Ok(result) => process
16857 .execution
16858 .respond_javascript_sync_rpc_success(request.id, result)
16859 .or_else(ignore_stale_javascript_sync_rpc_response)?,
16860 Err(error) => process
16861 .execution
16862 .respond_javascript_sync_rpc_error(
16863 request.id,
16864 javascript_sync_rpc_error_code(&error),
16865 error.to_string(),
16866 )
16867 .or_else(ignore_stale_javascript_sync_rpc_response)?,
16868 }
16869 }
16870 ActiveExecutionEvent::Exited(code) => {
16871 return Err(SidecarError::Execution(format!(
16872 "vm.fetch target exited before responding (exit code {code})"
16873 )));
16874 }
16875 other => {
16876 process.queue_pending_execution_event(other)?;
16877 }
16878 }
16879 Ok(true)
16880}
16881
16882fn drain_host_fetch_target_events<B>(
16883 bridge: &SharedBridge<B>,
16884 vm_id: &str,
16885 vm: &mut VmState,
16886 target_process_id: &str,
16887 socket_paths: &JavascriptSocketPathContext,
16888 resource_limits: &ResourceLimits,
16889) -> Result<(), SidecarError>
16890where
16891 B: NativeSidecarBridge + Send + 'static,
16892 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16893{
16894 for _ in 0..32 {
16895 let dns = vm.dns.clone();
16896 let Some(process) = vm.active_processes.get_mut(target_process_id) else {
16897 break;
16898 };
16899 let serviced = service_host_fetch_target_event(
16900 bridge,
16901 vm_id,
16902 &dns,
16903 socket_paths,
16904 &mut vm.kernel,
16905 process,
16906 resource_limits,
16907 Duration::from_millis(1),
16908 )?;
16909 if !serviced {
16910 break;
16911 }
16912 }
16913 Ok(())
16914}
16915
16916#[allow(clippy::too_many_arguments)]
16917fn dispatch_kernel_http_fetch<B>(
16918 bridge: &SharedBridge<B>,
16919 vm_id: &str,
16920 vm: &mut VmState,
16921 target_process_id: &str,
16922 port: u16,
16923 path: &str,
16924 options: &JavascriptHttpRequestOptions,
16925 headers: &HttpHeaderCollection,
16926 max_fetch_response_bytes: usize,
16927) -> Result<String, SidecarError>
16928where
16929 B: NativeSidecarBridge + Send + 'static,
16930 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16931{
16932 let socket_paths = build_javascript_socket_path_context(vm)?;
16933 let family = JavascriptSocketFamily::Ipv4;
16934 let local_port = allocate_guest_listen_port(
16935 0,
16936 family,
16937 &socket_paths.used_tcp_guest_ports,
16938 socket_paths.listen_policy,
16939 )?;
16940 let resource_limits = vm.kernel.resource_limits().clone();
16941 let network_counts = vm_network_resource_counts(vm);
16942 check_network_resource_limit(
16943 resource_limits.max_sockets,
16944 network_counts.sockets,
16945 2,
16946 "socket",
16947 )?;
16948 check_network_resource_limit(
16949 resource_limits.max_connections,
16950 network_counts.connections,
16951 2,
16952 "connection",
16953 )?;
16954
16955 let kernel_pid = vm
16956 .active_processes
16957 .get(target_process_id)
16958 .ok_or_else(|| {
16959 SidecarError::InvalidState(format!(
16960 "vm.fetch target process disappeared: {target_process_id}"
16961 ))
16962 })?
16963 .kernel_pid;
16964 let socket_id = vm
16965 .kernel
16966 .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, SocketSpec::tcp())
16967 .map_err(kernel_error)?;
16968
16969 let result = dispatch_kernel_http_fetch_with_socket(
16970 bridge,
16971 vm_id,
16972 vm,
16973 target_process_id,
16974 kernel_pid,
16975 socket_id,
16976 local_port,
16977 port,
16978 path,
16979 options,
16980 headers,
16981 &socket_paths,
16982 &resource_limits,
16983 max_fetch_response_bytes,
16984 );
16985 let close_result = vm
16986 .kernel
16987 .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
16988 .map_err(kernel_error);
16989 let cleanup_result = if result.is_err() {
16990 drain_host_fetch_target_events(
16991 bridge,
16992 vm_id,
16993 vm,
16994 target_process_id,
16995 &socket_paths,
16996 &resource_limits,
16997 )
16998 } else {
16999 Ok(())
17000 };
17001 match (result, close_result) {
17002 (Ok(response), Ok(())) => cleanup_result.map(|()| response),
17003 (Err(error), _) => Err(error),
17004 (Ok(_), Err(error)) => Err(error),
17005 }
17006}
17007
17008#[allow(clippy::too_many_arguments)]
17009fn dispatch_kernel_http_fetch_with_socket<B>(
17010 bridge: &SharedBridge<B>,
17011 vm_id: &str,
17012 vm: &mut VmState,
17013 target_process_id: &str,
17014 kernel_pid: u32,
17015 socket_id: SocketId,
17016 local_port: u16,
17017 port: u16,
17018 path: &str,
17019 options: &JavascriptHttpRequestOptions,
17020 headers: &HttpHeaderCollection,
17021 socket_paths: &JavascriptSocketPathContext,
17022 resource_limits: &ResourceLimits,
17023 max_fetch_response_bytes: usize,
17024) -> Result<String, SidecarError>
17025where
17026 B: NativeSidecarBridge + Send + 'static,
17027 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17028{
17029 vm.kernel
17030 .socket_bind_inet(
17031 EXECUTION_DRIVER_NAME,
17032 kernel_pid,
17033 socket_id,
17034 InetSocketAddress::new("127.0.0.1", local_port),
17035 )
17036 .map_err(kernel_error)?;
17037 vm.kernel
17038 .socket_connect_inet_loopback(
17039 EXECUTION_DRIVER_NAME,
17040 kernel_pid,
17041 socket_id,
17042 InetSocketAddress::new("127.0.0.1", port),
17043 )
17044 .map_err(kernel_error)?;
17045
17046 let request_bytes = serialize_kernel_http_fetch_request(port, path, options, headers);
17047 vm.kernel
17048 .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, &request_bytes)
17049 .map_err(kernel_error)?;
17050
17051 let mut response_buffer = Vec::new();
17052 let mut peer_closed = false;
17053 let url = format!("http://127.0.0.1:{port}{path}");
17054 let deadline = Instant::now() + http_loopback_request_timeout();
17055 loop {
17056 if let Some(response) =
17057 parse_kernel_http_fetch_response(&response_buffer, peer_closed, &url)?
17058 {
17059 ensure_vm_fetch_response_within_limit(&response, "vm.fetch", max_fetch_response_bytes)?;
17060 return Ok(response);
17061 }
17062 if Instant::now() >= deadline {
17063 let preview = String::from_utf8_lossy(&response_buffer);
17064 return Err(SidecarError::Execution(format!(
17065 "vm.fetch timed out waiting for kernel TCP HTTP response ({} buffered bytes: {:?})",
17066 response_buffer.len(),
17067 preview.chars().take(200).collect::<String>()
17068 )));
17069 }
17070
17071 {
17072 let dns = vm.dns.clone();
17073 let process = vm
17074 .active_processes
17075 .get_mut(target_process_id)
17076 .ok_or_else(|| {
17077 SidecarError::InvalidState(format!(
17078 "vm.fetch target process disappeared: {target_process_id}"
17079 ))
17080 })?;
17081 service_host_fetch_target_event(
17082 bridge,
17083 vm_id,
17084 &dns,
17085 socket_paths,
17086 &mut vm.kernel,
17087 process,
17088 resource_limits,
17089 Duration::from_millis(5),
17090 )?;
17091 }
17092
17093 let poll = vm
17094 .kernel
17095 .poll_targets(
17096 EXECUTION_DRIVER_NAME,
17097 kernel_pid,
17098 vec![PollTargetEntry::socket(
17099 socket_id,
17100 POLLIN | POLLHUP | POLLERR,
17101 )],
17102 5,
17103 )
17104 .map_err(kernel_error)?;
17105 let revents = poll
17106 .targets
17107 .first()
17108 .map(|entry| entry.revents)
17109 .unwrap_or_else(PollEvents::empty);
17110 if revents.intersects(POLLERR) {
17111 return Err(SidecarError::Execution(String::from(
17112 "vm.fetch kernel TCP socket reported POLLERR",
17113 )));
17114 }
17115 if revents.intersects(POLLIN) {
17116 match vm
17117 .kernel
17118 .socket_read(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, 64 * 1024)
17119 {
17120 Ok(Some(bytes)) if !bytes.is_empty() => {
17121 response_buffer.extend(bytes);
17122 ensure_vm_fetch_raw_response_buffer_within_limit(
17123 response_buffer.len(),
17124 "vm.fetch",
17125 )?;
17126 }
17127 Ok(Some(_)) => {}
17128 Ok(None) => peer_closed = true,
17129 Err(error) if error.code() == "EAGAIN" => {}
17130 Err(error) => return Err(kernel_error(error)),
17131 }
17132 }
17133 if revents.intersects(POLLHUP) {
17134 peer_closed = true;
17135 }
17136 }
17137}
17138
17139fn outbound_http_response_json(url: &Url, response: ureq::Response) -> Result<Value, SidecarError> {
17140 let status = response.status();
17141 let status_text = response.status_text().to_owned();
17142 let mut header_pairs = Vec::new();
17143 let mut raw_headers = Vec::new();
17144 for raw_name in response.headers_names() {
17145 for value in response.all(&raw_name) {
17146 header_pairs.push(json!([raw_name.to_ascii_lowercase(), value]));
17147 raw_headers.push(Value::String(raw_name.clone()));
17148 raw_headers.push(Value::String(value.to_owned()));
17149 }
17150 }
17151 let mut reader = response.into_reader();
17152 let mut body = Vec::new();
17153 reader.read_to_end(&mut body).map_err(|error| {
17154 SidecarError::Execution(format!("failed to read HTTP response: {error}"))
17155 })?;
17156 serde_json::to_string(&json!({
17157 "status": status,
17158 "statusText": status_text,
17159 "headers": header_pairs,
17160 "rawHeaders": raw_headers,
17161 "body": base64::engine::general_purpose::STANDARD.encode(body),
17162 "bodyEncoding": "base64",
17163 "url": url.as_str(),
17164 }))
17165 .map(Value::String)
17166 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17167}
17168
17169fn split_netloc(netloc: &str) -> Option<(&str, u16)> {
17173 let (host, port) = netloc.rsplit_once(':')?;
17174 let port: u16 = port.parse().ok()?;
17175 let host = host
17176 .strip_prefix('[')
17177 .and_then(|rest| rest.strip_suffix(']'))
17178 .unwrap_or(host);
17179 Some((host, port))
17180}
17181
17182fn issue_outbound_http_request(
17183 url: &Url,
17184 options: &JavascriptHttpRequestOptions,
17185 headers: &HttpHeaderCollection,
17186 pinned_addresses: &[IpAddr],
17187) -> Result<Value, SidecarError> {
17188 let method = options.method.as_deref().unwrap_or("GET");
17189 let pinned_host = url.host_str().map(str::to_owned);
17198 let pinned: Vec<IpAddr> = pinned_addresses.to_vec();
17199 let resolver = move |netloc: &str| -> std::io::Result<Vec<SocketAddr>> {
17200 let (host, port) = split_netloc(netloc).ok_or_else(|| {
17201 std::io::Error::new(
17202 std::io::ErrorKind::InvalidInput,
17203 format!("invalid network location: {netloc}"),
17204 )
17205 })?;
17206 let expected_host = pinned_host.as_deref();
17207 if expected_host != Some(host) {
17208 return Err(std::io::Error::new(
17209 std::io::ErrorKind::PermissionDenied,
17210 format!(
17211 "EACCES: outbound HTTP resolver pinned to {expected_host:?}, refusing {host}"
17212 ),
17213 ));
17214 }
17215 if pinned.is_empty() {
17216 return Err(std::io::Error::new(
17217 std::io::ErrorKind::PermissionDenied,
17218 "EACCES: no egress-vetted address available for outbound HTTP request",
17219 ));
17220 }
17221 Ok(pinned.iter().map(|ip| SocketAddr::new(*ip, port)).collect())
17222 };
17223 let mut agent_builder = ureq::AgentBuilder::new()
17224 .resolver(resolver)
17225 .timeout_connect(Duration::from_secs(5))
17226 .timeout_read(Duration::from_secs(15))
17227 .timeout_write(Duration::from_secs(15));
17228 if url.scheme() == "https" {
17229 let tls_options = JavascriptTlsBridgeOptions {
17230 is_server: false,
17231 servername: url.host_str().map(str::to_owned),
17232 alpn_protocols: Some(vec![String::from("http/1.1")]),
17233 reject_unauthorized: options.reject_unauthorized,
17234 ..JavascriptTlsBridgeOptions::default()
17235 };
17236 agent_builder = agent_builder.tls_config(Arc::new(build_client_tls_config(&tls_options)?));
17237 }
17238 let agent = agent_builder.build();
17239 let mut request = agent.request_url(method, url);
17240 for (name, values) in &headers.normalized {
17241 if name == "host" {
17242 continue;
17243 }
17244 let header_value = values.join(", ");
17245 request = request.set(name, &header_value);
17246 }
17247 let response = match options.body.as_deref() {
17248 Some(body) => request.send_string(body),
17249 None => request.call(),
17250 };
17251
17252 match response {
17253 Ok(response) => outbound_http_response_json(url, response),
17254 Err(ureq::Error::Status(_, response)) => outbound_http_response_json(url, response),
17255 Err(ureq::Error::Transport(error)) => Err(SidecarError::Execution(format!(
17256 "ERR_HTTP_REQUEST_FAILED: {error}"
17257 ))),
17258 }
17259}
17260
17261fn wait_for_loopback_http_response<B>(
17262 request: LoopbackHttpResponseWaitRequest<'_, B>,
17263) -> Result<String, SidecarError>
17264where
17265 B: NativeSidecarBridge + Send + 'static,
17266 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17267{
17268 let LoopbackHttpResponseWaitRequest {
17269 bridge,
17270 vm_id,
17271 dns,
17272 socket_paths,
17273 kernel,
17274 process,
17275 resource_limits,
17276 request_key,
17277 } = request;
17278 let deadline = Instant::now() + http_loopback_request_timeout();
17279 loop {
17280 if let Some(response) = process
17281 .pending_http_requests
17282 .get(&request_key)
17283 .and_then(|response| response.clone())
17284 {
17285 process.pending_http_requests.remove(&request_key);
17286 return Ok(response);
17287 }
17288
17289 if Instant::now() >= deadline {
17290 process.pending_http_requests.remove(&request_key);
17291 return Err(SidecarError::Execution(String::from(
17292 "HTTP loopback request timed out waiting for net.http_respond",
17293 )));
17294 }
17295
17296 let Some(event) = process
17297 .execution
17298 .poll_event_blocking(Duration::from_millis(10))
17299 .map_err(|error| SidecarError::Execution(error.to_string()))?
17300 else {
17301 continue;
17302 };
17303
17304 match event {
17305 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
17306 let network_counts = process.network_resource_counts();
17307 let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
17308 bridge,
17309 vm_id,
17310 dns,
17311 socket_paths,
17312 kernel,
17313 process,
17314 sync_request: &request,
17315 resource_limits,
17316 network_counts,
17317 });
17318 match response {
17319 Ok(result) => process
17320 .execution
17321 .respond_javascript_sync_rpc_success(request.id, result)
17322 .or_else(ignore_stale_javascript_sync_rpc_response)?,
17323 Err(error) => process
17324 .execution
17325 .respond_javascript_sync_rpc_error(
17326 request.id,
17327 javascript_sync_rpc_error_code(&error),
17328 error.to_string(),
17329 )
17330 .or_else(ignore_stale_javascript_sync_rpc_response)?,
17331 }
17332 }
17333 ActiveExecutionEvent::Exited(code) => {
17334 process.pending_http_requests.remove(&request_key);
17335 return Err(SidecarError::Execution(format!(
17336 "HTTP loopback server exited before responding (exit code {code})"
17337 )));
17338 }
17339 ActiveExecutionEvent::Stdout(_)
17340 | ActiveExecutionEvent::Stderr(_)
17341 | ActiveExecutionEvent::PythonVfsRpcRequest(_)
17342 | ActiveExecutionEvent::SignalState { .. } => {}
17343 }
17344 }
17345}
17346
17347pub(crate) fn dispatch_loopback_http_request<B>(
17348 request: LoopbackHttpDispatchRequest<'_, B>,
17349) -> Result<String, SidecarError>
17350where
17351 B: NativeSidecarBridge + Send + 'static,
17352 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17353{
17354 let LoopbackHttpDispatchRequest {
17355 bridge,
17356 vm_id,
17357 dns,
17358 socket_paths,
17359 kernel,
17360 process,
17361 resource_limits,
17362 server_id,
17363 request_json,
17364 } = request;
17365 let request_id = {
17366 let server = process.http_servers.get_mut(&server_id).ok_or_else(|| {
17367 SidecarError::InvalidState(format!("HTTP target server disappeared: {server_id}"))
17368 })?;
17369 server.next_request_id += 1;
17370 server.next_request_id
17371 };
17372 process
17373 .pending_http_requests
17374 .insert((server_id, request_id), None);
17375 process.execution.send_javascript_stream_event(
17376 "http_request",
17377 json!({
17378 "serverId": server_id,
17379 "requestId": request_id,
17380 "request": request_json,
17381 }),
17382 )?;
17383 wait_for_loopback_http_response(LoopbackHttpResponseWaitRequest {
17384 bridge,
17385 vm_id,
17386 dns,
17387 socket_paths,
17388 kernel,
17389 process,
17390 resource_limits,
17391 request_key: (server_id, request_id),
17392 })
17393}
17394
17395fn ensure_vm_fetch_response_within_limit(
17396 response_json: &str,
17397 operation: &str,
17398 limit: usize,
17399) -> Result<(), SidecarError> {
17400 let size = response_json.len();
17401 if size > limit {
17402 return Err(SidecarError::Execution(format!(
17403 "{operation} payload is {size} bytes, limit is {limit}"
17404 )));
17405 }
17406 Ok(())
17407}
17408
17409fn ensure_vm_fetch_raw_response_buffer_within_limit(
17410 size: usize,
17411 operation: &str,
17412) -> Result<(), SidecarError> {
17413 if size > VM_FETCH_BUFFER_LIMIT_BYTES {
17414 return Err(SidecarError::Execution(format!(
17415 "{operation} raw response buffer is {size} bytes, limit is {VM_FETCH_BUFFER_LIMIT_BYTES}"
17416 )));
17417 }
17418 Ok(())
17419}
17420
17421pub(crate) fn ensure_vm_fetch_response_frame_within_limit(
17422 response: &ResponseFrame,
17423 max_frame_bytes: usize,
17424) -> Result<(), SidecarError> {
17425 let max_frame_bytes = max_frame_bytes.min(VM_FETCH_BUFFER_LIMIT_BYTES);
17426 let frame = crate::protocol::to_generated_protocol_frame(
17427 &crate::protocol::ProtocolFrame::Response(response.clone()),
17428 )
17429 .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))?;
17430 let WireProtocolFrame::ResponseFrame(_) = &frame else {
17431 return Err(SidecarError::FrameTooLarge(String::from(
17432 "vm fetch response converted to non-response wire frame",
17433 )));
17434 };
17435 WireFrameCodec::new(max_frame_bytes)
17436 .encode(&frame)
17437 .map(|_| ())
17438 .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))
17439}
17440
17441fn service_javascript_dns_sync_rpc<B>(
17442 bridge: &SharedBridge<B>,
17443 kernel: &SidecarKernel,
17444 vm_id: &str,
17445 dns: &VmDnsConfig,
17446 request: &JavascriptSyncRpcRequest,
17447) -> Result<Value, SidecarError>
17448where
17449 B: NativeSidecarBridge + Send + 'static,
17450 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17451{
17452 match request.method.as_str() {
17453 "dns.lookup" => {
17454 let payload = request
17455 .args
17456 .first()
17457 .cloned()
17458 .ok_or_else(|| {
17459 SidecarError::InvalidState(String::from(
17460 "dns.lookup requires a request payload",
17461 ))
17462 })
17463 .and_then(|value| {
17464 serde_json::from_value::<JavascriptDnsLookupRequest>(value).map_err(|error| {
17465 SidecarError::InvalidState(format!("invalid dns.lookup payload: {error}"))
17466 })
17467 })?;
17468 let addresses = filter_dns_ip_addrs(
17469 resolve_dns_ip_addrs(
17470 bridge,
17471 kernel,
17472 vm_id,
17473 dns,
17474 &payload.hostname,
17475 DnsLookupPolicy::CheckPermissions,
17476 )?,
17477 payload.family,
17478 )?;
17479 let addresses = filter_dns_safe_ip_addrs(addresses, &payload.hostname)?;
17480 Ok(Value::Array(
17481 addresses
17482 .into_iter()
17483 .map(|ip| {
17484 json!({
17485 "address": ip.to_string(),
17486 "family": if ip.is_ipv6() { 6 } else { 4 },
17487 })
17488 })
17489 .collect(),
17490 ))
17491 }
17492 "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
17493 let payload = request
17494 .args
17495 .first()
17496 .cloned()
17497 .ok_or_else(|| {
17498 SidecarError::InvalidState(String::from(
17499 "dns.resolve requires a request payload",
17500 ))
17501 })
17502 .and_then(|value| {
17503 serde_json::from_value::<JavascriptDnsResolveRequest>(value).map_err(|error| {
17504 SidecarError::InvalidState(format!("invalid dns.resolve payload: {error}"))
17505 })
17506 })?;
17507 let requested_type = match request.method.as_str() {
17508 "dns.resolve4" => String::from("A"),
17509 "dns.resolve6" => String::from("AAAA"),
17510 _ => payload
17511 .rrtype
17512 .as_deref()
17513 .unwrap_or("A")
17514 .to_ascii_uppercase(),
17515 };
17516 let record_type = parse_dns_record_type(&requested_type)?;
17517 let resolution = resolve_dns_records(
17518 bridge,
17519 kernel,
17520 vm_id,
17521 dns,
17522 &payload.hostname,
17523 record_type,
17524 DnsLookupPolicy::CheckPermissions,
17525 )?;
17526 dns_resolution_to_node_value(&resolution, &requested_type)
17527 }
17528 other => Err(SidecarError::InvalidState(format!(
17529 "unsupported JavaScript dns sync RPC method {other}"
17530 ))),
17531 }
17532}
17533
17534fn service_javascript_dgram_sync_rpc<B>(
17535 request: JavascriptDgramSyncRpcServiceRequest<'_, B>,
17536) -> Result<Value, SidecarError>
17537where
17538 B: NativeSidecarBridge + Send + 'static,
17539 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17540{
17541 let JavascriptDgramSyncRpcServiceRequest {
17542 bridge,
17543 kernel,
17544 vm_id,
17545 dns,
17546 socket_paths,
17547 process,
17548 sync_request: request,
17549 resource_limits,
17550 network_counts,
17551 } = request;
17552 match request.method.as_str() {
17553 "dgram.createSocket" => {
17554 check_network_resource_limit(
17555 resource_limits.max_sockets,
17556 network_counts.sockets,
17557 1,
17558 "socket",
17559 )?;
17560 let payload = request
17561 .args
17562 .first()
17563 .cloned()
17564 .ok_or_else(|| {
17565 SidecarError::InvalidState(String::from(
17566 "dgram.createSocket requires a request payload",
17567 ))
17568 })
17569 .and_then(|value| {
17570 serde_json::from_value::<JavascriptDgramCreateSocketRequest>(value).map_err(
17571 |error| {
17572 SidecarError::InvalidState(format!(
17573 "invalid dgram.createSocket payload: {error}"
17574 ))
17575 },
17576 )
17577 })?;
17578 let family = JavascriptUdpFamily::from_socket_type(&payload.socket_type)?;
17579 let socket_id = process.allocate_udp_socket_id();
17580 process.udp_sockets.insert(
17581 socket_id.clone(),
17582 ActiveUdpSocket::new(kernel, process.kernel_pid, family)?,
17583 );
17584 Ok(json!({
17585 "socketId": socket_id,
17586 "type": family.socket_type(),
17587 }))
17588 }
17589 "dgram.bind" => {
17590 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.bind socket id")?;
17591 let payload = request
17592 .args
17593 .get(1)
17594 .cloned()
17595 .ok_or_else(|| {
17596 SidecarError::InvalidState(String::from(
17597 "dgram.bind requires a request payload",
17598 ))
17599 })
17600 .and_then(|value| {
17601 serde_json::from_value::<JavascriptDgramBindRequest>(value).map_err(|error| {
17602 SidecarError::InvalidState(format!("invalid dgram.bind payload: {error}"))
17603 })
17604 })?;
17605 let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17606 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17607 })?;
17608 let local_addr = socket.bind(
17609 kernel,
17610 process.kernel_pid,
17611 payload.address.as_deref(),
17612 payload.port,
17613 socket_paths,
17614 )?;
17615 Ok(json!({
17616 "localAddress": local_addr.ip().to_string(),
17617 "localPort": local_addr.port(),
17618 "family": socket_addr_family(&local_addr),
17619 }))
17620 }
17621 "dgram.send" => {
17622 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.send socket id")?;
17623 let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "dgram.send payload")?;
17624 let payload = request
17625 .args
17626 .get(2)
17627 .cloned()
17628 .ok_or_else(|| {
17629 SidecarError::InvalidState(String::from(
17630 "dgram.send requires a request payload",
17631 ))
17632 })
17633 .and_then(|value| {
17634 serde_json::from_value::<JavascriptDgramSendRequest>(value).map_err(|error| {
17635 SidecarError::InvalidState(format!("invalid dgram.send payload: {error}"))
17636 })
17637 })?;
17638 let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17639 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17640 })?;
17641 let (written, local_addr) = socket.send_to(ActiveUdpSendToRequest {
17642 bridge,
17643 kernel,
17644 kernel_pid: process.kernel_pid,
17645 vm_id,
17646 dns,
17647 host: payload.address.as_deref().unwrap_or("localhost"),
17648 port: payload.port,
17649 context: socket_paths,
17650 contents: &chunk,
17651 })?;
17652 Ok(json!({
17653 "bytes": written,
17654 "localAddress": local_addr.ip().to_string(),
17655 "localPort": local_addr.port(),
17656 "family": socket_addr_family(&local_addr),
17657 }))
17658 }
17659 "dgram.poll" => {
17660 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.poll socket id")?;
17661 let wait_ms =
17662 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "dgram.poll wait ms")?
17663 .unwrap_or_default();
17664 let event = {
17665 let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17666 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17667 })?;
17668 socket.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?
17669 };
17670
17671 match event {
17672 Some(JavascriptUdpSocketEvent::Message { data, remote_addr }) => {
17673 let family = JavascriptSocketFamily::from_ip(remote_addr.ip());
17674 let guest_remote_port = if is_loopback_ip(remote_addr.ip()) {
17675 socket_paths
17676 .guest_udp_port_for_host_port(family, remote_addr.port())
17677 .unwrap_or(remote_addr.port())
17678 } else {
17679 remote_addr.port()
17680 };
17681 Ok(json!({
17682 "type": "message",
17683 "data": javascript_sync_rpc_bytes_value(&data),
17684 "remoteAddress": remote_addr.ip().to_string(),
17685 "remotePort": guest_remote_port,
17686 "remoteFamily": socket_addr_family(&remote_addr),
17687 }))
17688 }
17689 Some(JavascriptUdpSocketEvent::Error { code, message }) => Ok(json!({
17690 "type": "error",
17691 "code": code,
17692 "message": message,
17693 })),
17694 None => Ok(Value::Null),
17695 }
17696 }
17697 "dgram.close" => {
17698 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.close socket id")?;
17699 let mut socket = process.udp_sockets.remove(socket_id).ok_or_else(|| {
17700 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17701 })?;
17702 socket.close(kernel, process.kernel_pid);
17703 Ok(Value::Null)
17704 }
17705 "dgram.address" => {
17706 let socket_id =
17707 javascript_sync_rpc_arg_str(&request.args, 0, "dgram.address socket id")?;
17708 let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17709 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17710 })?;
17711 let local_addr = socket.local_addr().ok_or_else(|| {
17712 SidecarError::Execution(String::from("EBADF: bad file descriptor"))
17713 })?;
17714 javascript_net_json_string(
17715 json!({
17716 "address": local_addr.ip().to_string(),
17717 "port": local_addr.port(),
17718 "family": socket_addr_family(&local_addr),
17719 }),
17720 "dgram.address",
17721 )
17722 }
17723 "dgram.setBufferSize" => {
17724 let socket_id =
17725 javascript_sync_rpc_arg_str(&request.args, 0, "dgram.setBufferSize socket id")?;
17726 let which =
17727 javascript_sync_rpc_arg_str(&request.args, 1, "dgram.setBufferSize buffer kind")?;
17728 let size = javascript_sync_rpc_arg_u64(&request.args, 2, "dgram.setBufferSize size")?;
17729 let size = usize::try_from(size).map_err(|_| {
17730 SidecarError::InvalidState(String::from(
17731 "dgram.setBufferSize size must fit within usize",
17732 ))
17733 })?;
17734 let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17735 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17736 })?;
17737 socket.set_buffer_size(which, size)?;
17738 Ok(Value::Null)
17739 }
17740 "dgram.getBufferSize" => {
17741 let socket_id =
17742 javascript_sync_rpc_arg_str(&request.args, 0, "dgram.getBufferSize socket id")?;
17743 let which =
17744 javascript_sync_rpc_arg_str(&request.args, 1, "dgram.getBufferSize buffer kind")?;
17745 let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17746 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17747 })?;
17748 let size = socket.get_buffer_size(which)?;
17749 Ok(json!(size))
17750 }
17751 other => Err(SidecarError::InvalidState(format!(
17752 "unsupported JavaScript dgram sync RPC method {other}"
17753 ))),
17754 }
17755}
17756
17757#[derive(Debug)]
17758struct ClientHttp2StreamState {
17759 send_stream: Option<h2::SendStream<Bytes>>,
17760}
17761
17762#[derive(Debug)]
17763struct ServerHttp2StreamState {
17764 send_response: Option<ServerHttp2Responder>,
17765 send_stream: Option<h2::SendStream<Bytes>>,
17766}
17767
17768#[derive(Debug)]
17769enum ServerHttp2Responder {
17770 Regular(server::SendResponse<Bytes>),
17771 Pushed(server::SendPushedResponse<Bytes>),
17772}
17773
17774const HTTP2_DEFAULT_WINDOW_SIZE: u32 = 65_535;
17775const HTTP2_POLL_DELAY: Duration = Duration::from_millis(10);
17776
17777fn http2_runtime_snapshot() -> Http2RuntimeSnapshot {
17778 Http2RuntimeSnapshot {
17779 effective_local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17780 local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17781 remote_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17782 next_stream_id: 1,
17783 outbound_queue_size: 1,
17784 deflate_dynamic_table_size: 0,
17785 inflate_dynamic_table_size: 0,
17786 }
17787}
17788
17789fn http2_snapshot_json(snapshot: &Http2SessionSnapshot) -> Result<String, SidecarError> {
17790 serde_json::to_string(snapshot)
17791 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17792}
17793
17794fn http2_event_value(event: &Http2BridgeEvent) -> Result<Value, SidecarError> {
17795 serde_json::to_string(event)
17796 .map(Value::String)
17797 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17798}
17799
17800fn push_http2_server_event(
17801 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17802 server_id: u64,
17803 event: Http2BridgeEvent,
17804) {
17805 if let Ok(mut state) = shared.lock() {
17806 state
17807 .server_events
17808 .entry(server_id)
17809 .or_default()
17810 .push_back(event);
17811 }
17812}
17813
17814fn push_http2_session_event(
17815 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17816 session_id: u64,
17817 event: Http2BridgeEvent,
17818) {
17819 if let Ok(mut state) = shared.lock() {
17820 state
17821 .session_events
17822 .entry(session_id)
17823 .or_default()
17824 .push_back(event);
17825 }
17826}
17827
17828fn pop_http2_event(
17829 queue: &mut BTreeMap<u64, VecDeque<Http2BridgeEvent>>,
17830 id: u64,
17831) -> Option<Http2BridgeEvent> {
17832 queue.get_mut(&id).and_then(VecDeque::pop_front)
17833}
17834
17835fn wait_for_http2_event(
17836 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17837 id: u64,
17838 is_server: bool,
17839 wait_ms: u64,
17840) -> Option<Http2BridgeEvent> {
17841 let deadline = Instant::now() + Duration::from_millis(wait_ms);
17842 loop {
17843 if let Ok(mut state) = shared.lock() {
17844 let queue = if is_server {
17845 &mut state.server_events
17846 } else {
17847 &mut state.session_events
17848 };
17849 if let Some(event) = pop_http2_event(queue, id) {
17850 return Some(event);
17851 }
17852 }
17853 if wait_ms == 0 || Instant::now() >= deadline {
17854 return None;
17855 }
17856 thread::sleep(HTTP2_POLL_DELAY);
17857 }
17858}
17859
17860fn next_http2_session_id(shared: &mut crate::state::Http2SharedState) -> u64 {
17861 shared.next_session_id += 1;
17862 shared.next_session_id
17863}
17864
17865fn next_http2_stream_id(shared: &mut crate::state::Http2SharedState) -> u64 {
17866 shared.next_stream_id += 1;
17867 shared.next_stream_id
17868}
17869
17870fn http2_reason(code: Option<u32>) -> Reason {
17871 code.unwrap_or(Reason::NO_ERROR.into()).into()
17872}
17873
17874fn http2_error_payload(message: impl Into<String>) -> String {
17875 serde_json::to_string(&json!({
17876 "name": "Error",
17877 "code": "ERR_HTTP2_ERROR",
17878 "message": message.into(),
17879 }))
17880 .unwrap_or_else(|_| {
17881 String::from(
17882 "{\"name\":\"Error\",\"code\":\"ERR_HTTP2_ERROR\",\"message\":\"HTTP/2 bridge error\"}",
17883 )
17884 })
17885}
17886
17887fn http2_socket_snapshot(local_addr: SocketAddr, remote_addr: SocketAddr) -> Http2SocketSnapshot {
17888 Http2SocketSnapshot {
17889 encrypted: false,
17890 allow_half_open: false,
17891 local_address: Some(local_addr.ip().to_string()),
17892 local_port: Some(local_addr.port()),
17893 local_family: Some(socket_addr_family(&local_addr).to_string()),
17894 remote_address: Some(remote_addr.ip().to_string()),
17895 remote_port: Some(remote_addr.port()),
17896 remote_family: Some(socket_addr_family(&remote_addr).to_string()),
17897 servername: None,
17898 alpn_protocol: Some(String::from("h2c")),
17899 }
17900}
17901
17902fn http2_wait_result(kind: &str, id: u64) -> Value {
17903 json!({
17904 "kind": kind,
17905 "id": id,
17906 })
17907}
17908
17909fn is_http2_terminal_event(event: &Http2BridgeEvent, is_server: bool, id: u64) -> bool {
17910 if is_server {
17911 event.kind == "serverClose" && event.id == id
17912 } else {
17913 event.kind == "sessionClose" && event.id == id
17914 }
17915}
17916
17917fn dispatch_http2_wait_loop(
17918 process: &ActiveProcess,
17919 id: u64,
17920 is_server: bool,
17921) -> Result<Value, SidecarError> {
17922 loop {
17923 if let Some(event) = wait_for_http2_event(&process.http2.shared, id, is_server, 50) {
17924 let payload = serde_json::to_value(&event).map_err(|error| {
17925 SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}"))
17926 })?;
17927 process
17928 .execution
17929 .send_javascript_stream_event("http2", payload.clone())?;
17930 if is_http2_terminal_event(&event, is_server, id) {
17931 return Ok(payload);
17932 }
17933 continue;
17934 }
17935
17936 let exists = process
17937 .http2
17938 .shared
17939 .lock()
17940 .map(|state| {
17941 if is_server {
17942 state.servers.contains_key(&id)
17943 } else {
17944 state.sessions.contains_key(&id)
17945 }
17946 })
17947 .unwrap_or(false);
17948 if !exists {
17949 return Ok(if is_server {
17950 http2_wait_result("serverClose", id)
17951 } else {
17952 http2_wait_result("sessionClose", id)
17953 });
17954 }
17955 }
17956}
17957
17958fn dispatch_http_wait_loop(process: &ActiveProcess, server_id: u64) -> Result<Value, SidecarError> {
17959 loop {
17960 if !process.http_servers.contains_key(&server_id) {
17961 return Ok(json!({
17962 "kind": "serverClose",
17963 "id": server_id,
17964 }));
17965 }
17966 thread::sleep(Duration::from_millis(25));
17967 }
17968}
17969
17970fn http2_settings_from_value(settings: &BTreeMap<String, Value>) -> BTreeMap<String, Value> {
17971 settings.clone()
17972}
17973
17974fn parse_http2_headers_json(
17975 headers_json: &str,
17976 label: &str,
17977) -> Result<BTreeMap<String, Value>, SidecarError> {
17978 serde_json::from_str::<BTreeMap<String, Value>>(headers_json)
17979 .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
17980}
17981
17982fn apply_http2_header_values(
17983 header_map: &mut HeaderMap,
17984 name: &str,
17985 value: &Value,
17986) -> Result<(), SidecarError> {
17987 let header_name = HeaderName::from_bytes(name.as_bytes()).map_err(|error| {
17988 SidecarError::InvalidState(format!("invalid HTTP/2 header name {name:?}: {error}"))
17989 })?;
17990 match value {
17991 Value::Array(values) => {
17992 for value in values {
17993 apply_http2_header_values(header_map, name, value)?;
17994 }
17995 }
17996 Value::String(text) => {
17997 let value = HeaderValue::from_str(text).map_err(|error| {
17998 SidecarError::InvalidState(format!(
17999 "invalid HTTP/2 header value for {name}: {error}"
18000 ))
18001 })?;
18002 header_map.append(header_name.clone(), value);
18003 }
18004 Value::Number(number) => {
18005 let value = HeaderValue::from_str(&number.to_string()).map_err(|error| {
18006 SidecarError::InvalidState(format!(
18007 "invalid HTTP/2 numeric header value for {name}: {error}"
18008 ))
18009 })?;
18010 header_map.append(header_name.clone(), value);
18011 }
18012 Value::Bool(boolean) => {
18013 let value = HeaderValue::from_str(if *boolean { "true" } else { "false" }).map_err(
18014 |error| {
18015 SidecarError::InvalidState(format!(
18016 "invalid HTTP/2 boolean header value for {name}: {error}"
18017 ))
18018 },
18019 )?;
18020 header_map.append(header_name.clone(), value);
18021 }
18022 Value::Null => {}
18023 Value::Object(_) => {
18024 return Err(SidecarError::InvalidState(format!(
18025 "unsupported HTTP/2 header object value for {name}"
18026 )));
18027 }
18028 }
18029 Ok(())
18030}
18031
18032fn build_http2_request(headers_json: &str) -> Result<Request<()>, SidecarError> {
18033 let headers = parse_http2_headers_json(headers_json, "HTTP/2 request headers")?;
18034 let method = headers
18035 .get(":method")
18036 .and_then(Value::as_str)
18037 .unwrap_or("GET");
18038 let path = headers.get(":path").and_then(Value::as_str).unwrap_or("/");
18039 let mut builder = Request::builder()
18040 .method(Method::from_bytes(method.as_bytes()).map_err(|error| {
18041 SidecarError::InvalidState(format!("invalid HTTP/2 method {method:?}: {error}"))
18042 })?)
18043 .uri(path.parse::<Uri>().map_err(|error| {
18044 SidecarError::InvalidState(format!("invalid HTTP/2 path {path:?}: {error}"))
18045 })?);
18046 {
18047 let header_map = builder.headers_mut().expect("request header map");
18048 for (name, value) in &headers {
18049 if name.starts_with(':') {
18050 continue;
18051 }
18052 apply_http2_header_values(header_map, name, value)?;
18053 }
18054 }
18055 builder
18056 .body(())
18057 .map_err(|error| SidecarError::InvalidState(format!("invalid HTTP/2 request: {error}")))
18058}
18059
18060fn build_http2_response(headers_json: &str) -> Result<Response<()>, SidecarError> {
18061 let headers = parse_http2_headers_json(headers_json, "HTTP/2 response headers")?;
18062 let status = headers
18063 .get(":status")
18064 .and_then(Value::as_u64)
18065 .or_else(|| {
18066 headers
18067 .get(":status")
18068 .and_then(Value::as_str)
18069 .and_then(|value| value.parse::<u16>().ok().map(u64::from))
18070 })
18071 .unwrap_or(200);
18072 let mut builder = Response::builder().status(status as u16);
18073 {
18074 let header_map = builder.headers_mut().expect("response header map");
18075 for (name, value) in &headers {
18076 if name.starts_with(':') {
18077 continue;
18078 }
18079 apply_http2_header_values(header_map, name, value)?;
18080 }
18081 }
18082 builder.body(()).map_err(|error| {
18083 SidecarError::InvalidState(format!("invalid HTTP/2 response headers: {error}"))
18084 })
18085}
18086
18087fn serialize_http2_headers_map(
18088 pseudo: BTreeMap<String, Value>,
18089 headers: &HeaderMap,
18090) -> Result<String, SidecarError> {
18091 let mut serialized = pseudo;
18092 for (name, value) in headers {
18093 let name = name.as_str().to_string();
18094 let value = Value::String(
18095 value
18096 .to_str()
18097 .map_err(|error| {
18098 SidecarError::Execution(format!("invalid HTTP/2 header value: {error}"))
18099 })?
18100 .to_owned(),
18101 );
18102 match serialized.get_mut(&name) {
18103 Some(Value::Array(values)) => values.push(value),
18104 Some(existing) => {
18105 let first = existing.clone();
18106 *existing = Value::Array(vec![first, value]);
18107 }
18108 None => {
18109 serialized.insert(name, value);
18110 }
18111 }
18112 }
18113 serde_json::to_string(&serialized)
18114 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
18115}
18116
18117fn serialize_http2_request_headers(
18118 request: &Request<h2::RecvStream>,
18119) -> Result<String, SidecarError> {
18120 let mut pseudo = BTreeMap::new();
18121 pseudo.insert(
18122 String::from(":method"),
18123 Value::String(request.method().as_str().to_string()),
18124 );
18125 pseudo.insert(
18126 String::from(":path"),
18127 Value::String(
18128 request
18129 .uri()
18130 .path_and_query()
18131 .map(|value| value.as_str().to_string())
18132 .unwrap_or_else(|| String::from("/")),
18133 ),
18134 );
18135 serialize_http2_headers_map(pseudo, request.headers())
18136}
18137
18138fn serialize_http2_response_headers(
18139 response: &Response<h2::RecvStream>,
18140) -> Result<String, SidecarError> {
18141 let mut pseudo = BTreeMap::new();
18142 pseudo.insert(
18143 String::from(":status"),
18144 Value::Number(serde_json::Number::from(response.status().as_u16())),
18145 );
18146 serialize_http2_headers_map(pseudo, response.headers())
18147}
18148
18149fn remove_http2_session_resources(
18150 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
18151 session_id: u64,
18152) {
18153 if let Ok(mut state) = shared.lock() {
18154 state.sessions.remove(&session_id);
18155 state.session_events.remove(&session_id);
18156 let stream_ids = state
18157 .streams
18158 .iter()
18159 .filter_map(|(stream_id, stream)| {
18160 (stream.session_id == session_id).then_some(*stream_id)
18161 })
18162 .collect::<Vec<_>>();
18163 for stream_id in stream_ids {
18164 state.streams.remove(&stream_id);
18165 }
18166 }
18167}
18168
18169fn spawn_http2_client_session(
18170 shared: Arc<Mutex<crate::state::Http2SharedState>>,
18171 session_id: u64,
18172 remote_addr: SocketAddr,
18173 tls: Option<JavascriptTlsBridgeOptions>,
18174 snapshot: Arc<Mutex<Http2SessionSnapshot>>,
18175 mut command_rx: UnboundedReceiver<Http2SessionCommand>,
18176) {
18177 thread::spawn(move || {
18178 let runtime = match TokioRuntimeBuilder::new_current_thread()
18179 .enable_all()
18180 .build()
18181 {
18182 Ok(runtime) => runtime,
18183 Err(error) => {
18184 push_http2_session_event(
18185 &shared,
18186 session_id,
18187 Http2BridgeEvent {
18188 kind: String::from("sessionError"),
18189 id: session_id,
18190 data: Some(http2_error_payload(error.to_string())),
18191 ..Http2BridgeEvent::default()
18192 },
18193 );
18194 remove_http2_session_resources(&shared, session_id);
18195 return;
18196 }
18197 };
18198
18199 runtime.block_on(async move {
18200 let stream = match tokio::net::TcpStream::connect(remote_addr).await {
18201 Ok(stream) => stream,
18202 Err(error) => {
18203 push_http2_session_event(
18204 &shared,
18205 session_id,
18206 Http2BridgeEvent {
18207 kind: String::from("sessionError"),
18208 id: session_id,
18209 data: Some(http2_error_payload(error.to_string())),
18210 ..Http2BridgeEvent::default()
18211 },
18212 );
18213 remove_http2_session_resources(&shared, session_id);
18214 return;
18215 }
18216 };
18217
18218 let local_addr = match stream.local_addr() {
18219 Ok(addr) => addr,
18220 Err(error) => {
18221 push_http2_session_event(
18222 &shared,
18223 session_id,
18224 Http2BridgeEvent {
18225 kind: String::from("sessionError"),
18226 id: session_id,
18227 data: Some(http2_error_payload(error.to_string())),
18228 ..Http2BridgeEvent::default()
18229 },
18230 );
18231 remove_http2_session_resources(&shared, session_id);
18232 return;
18233 }
18234 };
18235
18236 {
18237 let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
18238 snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
18239 if let Some(options) = tls.as_ref() {
18240 snapshot_guard.encrypted = true;
18241 snapshot_guard.alpn_protocol = Some(String::from("h2"));
18242 snapshot_guard.socket.encrypted = true;
18243 snapshot_guard.socket.servername = options.servername.clone();
18244 snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
18245 }
18246 snapshot_guard.state = http2_runtime_snapshot();
18247 }
18248 if let Ok(snapshot_json) =
18249 http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
18250 {
18251 push_http2_session_event(
18252 &shared,
18253 session_id,
18254 Http2BridgeEvent {
18255 kind: String::from("sessionConnect"),
18256 id: session_id,
18257 data: Some(snapshot_json),
18258 ..Http2BridgeEvent::default()
18259 },
18260 );
18261 }
18262
18263 let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
18264 let server_name = match ServerName::try_from(
18265 options
18266 .servername
18267 .clone()
18268 .unwrap_or_else(|| String::from("localhost")),
18269 ) {
18270 Ok(server_name) => server_name,
18271 Err(_) => {
18272 push_http2_session_event(
18273 &shared,
18274 session_id,
18275 Http2BridgeEvent {
18276 kind: String::from("sessionError"),
18277 id: session_id,
18278 data: Some(http2_error_payload("invalid TLS servername")),
18279 ..Http2BridgeEvent::default()
18280 },
18281 );
18282 remove_http2_session_resources(&shared, session_id);
18283 return;
18284 }
18285 };
18286 let connector = match build_client_tls_config(options) {
18287 Ok(config) => TlsConnector::from(Arc::new(config)),
18288 Err(error) => {
18289 push_http2_session_event(
18290 &shared,
18291 session_id,
18292 Http2BridgeEvent {
18293 kind: String::from("sessionError"),
18294 id: session_id,
18295 data: Some(http2_error_payload(error.to_string())),
18296 ..Http2BridgeEvent::default()
18297 },
18298 );
18299 remove_http2_session_resources(&shared, session_id);
18300 return;
18301 }
18302 };
18303 match connector.connect(server_name, stream).await {
18304 Ok(tls_stream) => Box::pin(tls_stream),
18305 Err(error) => {
18306 push_http2_session_event(
18307 &shared,
18308 session_id,
18309 Http2BridgeEvent {
18310 kind: String::from("sessionError"),
18311 id: session_id,
18312 data: Some(http2_error_payload(error.to_string())),
18313 ..Http2BridgeEvent::default()
18314 },
18315 );
18316 remove_http2_session_resources(&shared, session_id);
18317 return;
18318 }
18319 }
18320 } else {
18321 Box::pin(stream)
18322 };
18323
18324 let (mut sender, connection) = match client::handshake(io).await {
18325 Ok(parts) => parts,
18326 Err(error) => {
18327 push_http2_session_event(
18328 &shared,
18329 session_id,
18330 Http2BridgeEvent {
18331 kind: String::from("sessionError"),
18332 id: session_id,
18333 data: Some(http2_error_payload(error.to_string())),
18334 ..Http2BridgeEvent::default()
18335 },
18336 );
18337 remove_http2_session_resources(&shared, session_id);
18338 return;
18339 }
18340 };
18341
18342 let (status_tx, mut status_rx) = unbounded_channel::<Result<(), String>>();
18343 tokio::spawn(async move {
18344 let _ = status_tx.send(connection.await.map_err(|error| error.to_string()));
18345 });
18346
18347 let streams: Arc<Mutex<BTreeMap<u64, ClientHttp2StreamState>>> =
18348 Arc::new(Mutex::new(BTreeMap::new()));
18349
18350 loop {
18351 tokio::select! {
18352 Some(result) = status_rx.recv() => {
18353 if let Err(message) = result {
18354 push_http2_session_event(
18355 &shared,
18356 session_id,
18357 Http2BridgeEvent {
18358 kind: String::from("sessionError"),
18359 id: session_id,
18360 data: Some(http2_error_payload(message)),
18361 ..Http2BridgeEvent::default()
18362 },
18363 );
18364 }
18365 push_http2_session_event(
18366 &shared,
18367 session_id,
18368 Http2BridgeEvent {
18369 kind: String::from("sessionClose"),
18370 id: session_id,
18371 ..Http2BridgeEvent::default()
18372 },
18373 );
18374 remove_http2_session_resources(&shared, session_id);
18375 break;
18376 }
18377 Some(command) = command_rx.recv() => {
18378 match command {
18379 Http2SessionCommand::Request { headers_json, options_json, respond_to } => {
18380 let request = match build_http2_request(&headers_json) {
18381 Ok(request) => request,
18382 Err(error) => {
18383 let _ = respond_to.send(Err(error.to_string()));
18384 continue;
18385 }
18386 };
18387 let options: JavascriptHttp2RequestOptions =
18388 serde_json::from_str(&options_json).unwrap_or_default();
18389 let stream_id = {
18390 let mut state = shared.lock().expect("http2 shared state");
18391 let stream_id = next_http2_stream_id(&mut state);
18392 state.streams.insert(
18393 stream_id,
18394 ActiveHttp2Stream {
18395 session_id,
18396 paused: Arc::new(AtomicBool::new(false)),
18397 },
18398 );
18399 stream_id
18400 };
18401 match sender.send_request(request, options.end_stream) {
18402 Ok((response_future, send_stream)) => {
18403 if !options.end_stream {
18404 streams
18405 .lock()
18406 .expect("http2 client streams")
18407 .insert(stream_id, ClientHttp2StreamState { send_stream: Some(send_stream) });
18408 }
18409 let shared_clone = Arc::clone(&shared);
18410 let snapshot_clone = Arc::clone(&snapshot);
18411 tokio::spawn(async move {
18412 match response_future.await {
18413 Ok(response) => {
18414 if let Ok(headers_json) = serialize_http2_response_headers(&response) {
18415 push_http2_session_event(
18416 &shared_clone,
18417 session_id,
18418 Http2BridgeEvent {
18419 kind: String::from("clientResponseHeaders"),
18420 id: stream_id,
18421 data: Some(headers_json),
18422 ..Http2BridgeEvent::default()
18423 },
18424 );
18425 }
18426 let mut body = response.into_body();
18427 while let Some(chunk) = body.data().await {
18428 match chunk {
18429 Ok(bytes) => {
18430 let paused = {
18431 let state = shared_clone.lock().expect("http2 shared state");
18432 state.streams.get(&stream_id).map(|stream| Arc::clone(&stream.paused))
18433 };
18434 if let Some(paused) = paused {
18435 while paused.load(Ordering::SeqCst) {
18436 tokio::time::sleep(HTTP2_POLL_DELAY).await;
18437 }
18438 }
18439 let _ = body.flow_control().release_capacity(bytes.len());
18440 push_http2_session_event(
18441 &shared_clone,
18442 session_id,
18443 Http2BridgeEvent {
18444 kind: String::from("clientData"),
18445 id: stream_id,
18446 data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
18447 ..Http2BridgeEvent::default()
18448 },
18449 );
18450 }
18451 Err(error) => {
18452 push_http2_session_event(
18453 &shared_clone,
18454 session_id,
18455 Http2BridgeEvent {
18456 kind: String::from("clientError"),
18457 id: stream_id,
18458 data: Some(http2_error_payload(error.to_string())),
18459 ..Http2BridgeEvent::default()
18460 },
18461 );
18462 break;
18463 }
18464 }
18465 }
18466 {
18467 let mut snapshot = snapshot_clone.lock().expect("http2 snapshot lock");
18468 snapshot.state.next_stream_id =
18469 snapshot.state.next_stream_id.saturating_add(2);
18470 }
18471 push_http2_session_event(
18472 &shared_clone,
18473 session_id,
18474 Http2BridgeEvent {
18475 kind: String::from("clientEnd"),
18476 id: stream_id,
18477 ..Http2BridgeEvent::default()
18478 },
18479 );
18480 push_http2_session_event(
18481 &shared_clone,
18482 session_id,
18483 Http2BridgeEvent {
18484 kind: String::from("clientClose"),
18485 id: stream_id,
18486 extra_number: Some(0),
18487 ..Http2BridgeEvent::default()
18488 },
18489 );
18490 if let Ok(mut state) = shared_clone.lock() {
18491 state.streams.remove(&stream_id);
18492 }
18493 }
18494 Err(error) => {
18495 push_http2_session_event(
18496 &shared_clone,
18497 session_id,
18498 Http2BridgeEvent {
18499 kind: String::from("clientError"),
18500 id: stream_id,
18501 data: Some(http2_error_payload(error.to_string())),
18502 ..Http2BridgeEvent::default()
18503 },
18504 );
18505 push_http2_session_event(
18506 &shared_clone,
18507 session_id,
18508 Http2BridgeEvent {
18509 kind: String::from("clientClose"),
18510 id: stream_id,
18511 extra_number: Some(u32::from(Reason::INTERNAL_ERROR) as u64),
18512 ..Http2BridgeEvent::default()
18513 },
18514 );
18515 if let Ok(mut state) = shared_clone.lock() {
18516 state.streams.remove(&stream_id);
18517 }
18518 }
18519 }
18520 });
18521 let _ = respond_to.send(Ok(json!(stream_id)));
18522 }
18523 Err(error) => {
18524 if let Ok(mut state) = shared.lock() {
18525 state.streams.remove(&stream_id);
18526 }
18527 let _ = respond_to.send(Err(error.to_string()));
18528 }
18529 }
18530 }
18531 Http2SessionCommand::Settings { settings_json, respond_to } => {
18532 let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
18533 .unwrap_or_default();
18534 {
18535 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18536 snapshot.local_settings = http2_settings_from_value(&settings);
18537 }
18538 if let Ok(headers_json) = serde_json::to_string(&settings) {
18539 push_http2_session_event(
18540 &shared,
18541 session_id,
18542 Http2BridgeEvent {
18543 kind: String::from("sessionLocalSettings"),
18544 id: session_id,
18545 data: Some(headers_json.clone()),
18546 ..Http2BridgeEvent::default()
18547 },
18548 );
18549 push_http2_session_event(
18550 &shared,
18551 session_id,
18552 Http2BridgeEvent {
18553 kind: String::from("sessionSettingsAck"),
18554 id: session_id,
18555 ..Http2BridgeEvent::default()
18556 },
18557 );
18558 }
18559 let _ = respond_to.send(Ok(Value::Null));
18560 }
18561 Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
18562 {
18563 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18564 snapshot.state.local_window_size = size;
18565 snapshot.state.effective_local_window_size = size;
18566 }
18567 let value = snapshot
18568 .lock()
18569 .ok()
18570 .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
18571 .map(Value::String)
18572 .unwrap_or(Value::Null);
18573 let _ = respond_to.send(Ok(value));
18574 }
18575 Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
18576 push_http2_session_event(
18577 &shared,
18578 session_id,
18579 Http2BridgeEvent {
18580 kind: String::from("sessionGoaway"),
18581 id: session_id,
18582 data: opaque_data.map(|value| {
18583 base64::engine::general_purpose::STANDARD.encode(value)
18584 }),
18585 extra_number: Some(error_code as u64),
18586 flags: Some(last_stream_id as u64),
18587 ..Http2BridgeEvent::default()
18588 },
18589 );
18590 let _ = respond_to.send(Ok(Value::Null));
18591 }
18592 Http2SessionCommand::Close { respond_to, .. } => {
18593 let _ = respond_to.send(Ok(Value::Null));
18594 push_http2_session_event(
18595 &shared,
18596 session_id,
18597 Http2BridgeEvent {
18598 kind: String::from("sessionClose"),
18599 id: session_id,
18600 ..Http2BridgeEvent::default()
18601 },
18602 );
18603 remove_http2_session_resources(&shared, session_id);
18604 break;
18605 }
18606 Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
18607 let result = streams
18608 .lock()
18609 .expect("http2 client streams")
18610 .get_mut(&stream_id)
18611 .and_then(|stream| stream.send_stream.as_mut())
18612 .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 client stream {stream_id}")))
18613 .and_then(|stream| stream.send_data(Bytes::from(chunk), end_stream).map_err(|error| SidecarError::Execution(error.to_string())));
18614 match result {
18615 Ok(()) => {
18616 if end_stream {
18617 streams.lock().expect("http2 client streams").remove(&stream_id);
18618 }
18619 let _ = respond_to.send(Ok(Value::Bool(true)));
18620 }
18621 Err(error) => {
18622 let _ = respond_to.send(Err(error.to_string()));
18623 }
18624 }
18625 }
18626 Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
18627 let mut streams = streams.lock().expect("http2 client streams");
18628 let Some(mut state) = streams.remove(&stream_id) else {
18629 let _ = respond_to.send(Err(format!("unknown HTTP/2 client stream {stream_id}")));
18630 continue;
18631 };
18632 if let Some(stream) = state.send_stream.as_mut() {
18633 stream.send_reset(http2_reason(error_code));
18634 }
18635 if let Ok(mut state) = shared.lock() {
18636 state.streams.remove(&stream_id);
18637 }
18638 push_http2_session_event(
18639 &shared,
18640 session_id,
18641 Http2BridgeEvent {
18642 kind: String::from("clientClose"),
18643 id: stream_id,
18644 extra_number: Some(u32::from(http2_reason(error_code)) as u64),
18645 ..Http2BridgeEvent::default()
18646 },
18647 );
18648 let _ = respond_to.send(Ok(Value::Null));
18649 }
18650 Http2SessionCommand::StreamRespond { respond_to, .. }
18651 | Http2SessionCommand::StreamPush { respond_to, .. }
18652 | Http2SessionCommand::StreamRespondWithFile { respond_to, .. } => {
18653 let _ = respond_to.send(Err(String::from("HTTP/2 client streams cannot send server responses")));
18654 }
18655 }
18656 }
18657 else => break,
18658 }
18659 }
18660 });
18661 });
18662}
18663
18664fn spawn_http2_server_session(
18665 shared: Arc<Mutex<crate::state::Http2SharedState>>,
18666 server_id: u64,
18667 session_id: u64,
18668 stream: TcpStream,
18669 tls: Option<JavascriptTlsBridgeOptions>,
18670 snapshot: Arc<Mutex<Http2SessionSnapshot>>,
18671 mut command_rx: UnboundedReceiver<Http2SessionCommand>,
18672) {
18673 thread::spawn(move || {
18674 let runtime = match TokioRuntimeBuilder::new_current_thread()
18675 .enable_all()
18676 .build()
18677 {
18678 Ok(runtime) => runtime,
18679 Err(error) => {
18680 push_http2_server_event(
18681 &shared,
18682 server_id,
18683 Http2BridgeEvent {
18684 kind: String::from("serverStreamError"),
18685 id: session_id,
18686 data: Some(http2_error_payload(error.to_string())),
18687 ..Http2BridgeEvent::default()
18688 },
18689 );
18690 remove_http2_session_resources(&shared, session_id);
18691 return;
18692 }
18693 };
18694
18695 runtime.block_on(async move {
18696 if let Err(error) = stream.set_nonblocking(true) {
18697 push_http2_server_event(
18698 &shared,
18699 server_id,
18700 Http2BridgeEvent {
18701 kind: String::from("serverStreamError"),
18702 id: session_id,
18703 data: Some(http2_error_payload(error.to_string())),
18704 ..Http2BridgeEvent::default()
18705 },
18706 );
18707 remove_http2_session_resources(&shared, session_id);
18708 return;
18709 }
18710 let stream = match tokio::net::TcpStream::from_std(stream) {
18711 Ok(stream) => stream,
18712 Err(error) => {
18713 push_http2_server_event(
18714 &shared,
18715 server_id,
18716 Http2BridgeEvent {
18717 kind: String::from("serverStreamError"),
18718 id: session_id,
18719 data: Some(http2_error_payload(error.to_string())),
18720 ..Http2BridgeEvent::default()
18721 },
18722 );
18723 remove_http2_session_resources(&shared, session_id);
18724 return;
18725 }
18726 };
18727 let local_addr = match stream.local_addr() {
18728 Ok(addr) => addr,
18729 Err(error) => {
18730 push_http2_server_event(
18731 &shared,
18732 server_id,
18733 Http2BridgeEvent {
18734 kind: String::from("serverStreamError"),
18735 id: session_id,
18736 data: Some(http2_error_payload(error.to_string())),
18737 ..Http2BridgeEvent::default()
18738 },
18739 );
18740 remove_http2_session_resources(&shared, session_id);
18741 return;
18742 }
18743 };
18744 let remote_addr = match stream.peer_addr() {
18745 Ok(addr) => addr,
18746 Err(error) => {
18747 push_http2_server_event(
18748 &shared,
18749 server_id,
18750 Http2BridgeEvent {
18751 kind: String::from("serverStreamError"),
18752 id: session_id,
18753 data: Some(http2_error_payload(error.to_string())),
18754 ..Http2BridgeEvent::default()
18755 },
18756 );
18757 remove_http2_session_resources(&shared, session_id);
18758 return;
18759 }
18760 };
18761 {
18762 let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
18763 snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
18764 if tls.is_some() {
18765 snapshot_guard.encrypted = true;
18766 snapshot_guard.alpn_protocol = Some(String::from("h2"));
18767 snapshot_guard.socket.encrypted = true;
18768 snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
18769 }
18770 snapshot_guard.state = http2_runtime_snapshot();
18771 }
18772 if let Ok(snapshot_json) =
18773 http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
18774 {
18775 push_http2_server_event(
18776 &shared,
18777 server_id,
18778 Http2BridgeEvent {
18779 kind: String::from(if tls.is_some() {
18780 "serverSecureConnection"
18781 } else {
18782 "serverConnection"
18783 }),
18784 id: server_id,
18785 data: Some(serde_json::to_string(&http2_socket_snapshot(local_addr, remote_addr)).unwrap_or_default()),
18786 ..Http2BridgeEvent::default()
18787 },
18788 );
18789 push_http2_server_event(
18790 &shared,
18791 server_id,
18792 Http2BridgeEvent {
18793 kind: String::from("serverSession"),
18794 id: server_id,
18795 data: Some(snapshot_json),
18796 extra_number: Some(session_id),
18797 ..Http2BridgeEvent::default()
18798 },
18799 );
18800 }
18801
18802 let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
18803 let acceptor = match build_server_tls_config(options) {
18804 Ok(config) => TlsAcceptor::from(Arc::new(config)),
18805 Err(error) => {
18806 push_http2_server_event(
18807 &shared,
18808 server_id,
18809 Http2BridgeEvent {
18810 kind: String::from("serverStreamError"),
18811 id: session_id,
18812 data: Some(http2_error_payload(error.to_string())),
18813 ..Http2BridgeEvent::default()
18814 },
18815 );
18816 remove_http2_session_resources(&shared, session_id);
18817 return;
18818 }
18819 };
18820 match acceptor.accept(stream).await {
18821 Ok(tls_stream) => Box::pin(tls_stream),
18822 Err(error) => {
18823 push_http2_server_event(
18824 &shared,
18825 server_id,
18826 Http2BridgeEvent {
18827 kind: String::from("serverStreamError"),
18828 id: session_id,
18829 data: Some(http2_error_payload(error.to_string())),
18830 ..Http2BridgeEvent::default()
18831 },
18832 );
18833 remove_http2_session_resources(&shared, session_id);
18834 return;
18835 }
18836 }
18837 } else {
18838 Box::pin(stream)
18839 };
18840
18841 let mut connection = match server::handshake(io).await {
18842 Ok(connection) => connection,
18843 Err(error) => {
18844 push_http2_server_event(
18845 &shared,
18846 server_id,
18847 Http2BridgeEvent {
18848 kind: String::from("serverStreamError"),
18849 id: session_id,
18850 data: Some(http2_error_payload(error.to_string())),
18851 ..Http2BridgeEvent::default()
18852 },
18853 );
18854 remove_http2_session_resources(&shared, session_id);
18855 return;
18856 }
18857 };
18858
18859 let streams: Arc<Mutex<BTreeMap<u64, ServerHttp2StreamState>>> =
18860 Arc::new(Mutex::new(BTreeMap::new()));
18861
18862 loop {
18863 tokio::select! {
18864 incoming = connection.accept() => {
18865 match incoming {
18866 Some(Ok((request, respond))) => {
18867 let headers_json = match serialize_http2_request_headers(&request) {
18868 Ok(headers) => headers,
18869 Err(error) => {
18870 push_http2_server_event(
18871 &shared,
18872 server_id,
18873 Http2BridgeEvent {
18874 kind: String::from("serverStreamError"),
18875 id: server_id,
18876 data: Some(http2_error_payload(error.to_string())),
18877 ..Http2BridgeEvent::default()
18878 },
18879 );
18880 continue;
18881 }
18882 };
18883 let stream_id = {
18884 let mut state = shared.lock().expect("http2 shared state");
18885 let stream_id = next_http2_stream_id(&mut state);
18886 state.streams.insert(
18887 stream_id,
18888 ActiveHttp2Stream {
18889 session_id,
18890 paused: Arc::new(AtomicBool::new(false)),
18891 },
18892 );
18893 stream_id
18894 };
18895 streams.lock().expect("http2 server streams").insert(
18896 stream_id,
18897 ServerHttp2StreamState {
18898 send_response: Some(ServerHttp2Responder::Regular(respond)),
18899 send_stream: None,
18900 },
18901 );
18902 let snapshot_json = snapshot
18903 .lock()
18904 .ok()
18905 .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok());
18906 push_http2_server_event(
18907 &shared,
18908 server_id,
18909 Http2BridgeEvent {
18910 kind: String::from("serverStream"),
18911 id: server_id,
18912 data: Some(stream_id.to_string()),
18913 extra: snapshot_json,
18914 extra_number: Some(session_id),
18915 extra_headers: Some(headers_json),
18916 flags: Some(0),
18917 },
18918 );
18919 let shared_clone = Arc::clone(&shared);
18920 tokio::spawn(async move {
18921 let mut body = request.into_body();
18922 while let Some(chunk) = body.data().await {
18923 match chunk {
18924 Ok(bytes) => {
18925 let paused = {
18926 let state = shared_clone.lock().expect("http2 shared state");
18927 state.streams.get(&stream_id).map(|stream| Arc::clone(&stream.paused))
18928 };
18929 if let Some(paused) = paused {
18930 while paused.load(Ordering::SeqCst) {
18931 tokio::time::sleep(HTTP2_POLL_DELAY).await;
18932 }
18933 }
18934 let _ = body.flow_control().release_capacity(bytes.len());
18935 push_http2_server_event(
18936 &shared_clone,
18937 server_id,
18938 Http2BridgeEvent {
18939 kind: String::from("serverStreamData"),
18940 id: stream_id,
18941 data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
18942 ..Http2BridgeEvent::default()
18943 },
18944 );
18945 }
18946 Err(error) => {
18947 push_http2_server_event(
18948 &shared_clone,
18949 server_id,
18950 Http2BridgeEvent {
18951 kind: String::from("serverStreamError"),
18952 id: stream_id,
18953 data: Some(http2_error_payload(error.to_string())),
18954 ..Http2BridgeEvent::default()
18955 },
18956 );
18957 break;
18958 }
18959 }
18960 }
18961 push_http2_server_event(
18962 &shared_clone,
18963 server_id,
18964 Http2BridgeEvent {
18965 kind: String::from("serverStreamEnd"),
18966 id: stream_id,
18967 ..Http2BridgeEvent::default()
18968 },
18969 );
18970 });
18971 }
18972 Some(Err(error)) => {
18973 push_http2_server_event(
18974 &shared,
18975 server_id,
18976 Http2BridgeEvent {
18977 kind: String::from("serverStreamError"),
18978 id: server_id,
18979 data: Some(http2_error_payload(error.to_string())),
18980 ..Http2BridgeEvent::default()
18981 },
18982 );
18983 break;
18984 }
18985 None => {
18986 push_http2_server_event(
18987 &shared,
18988 server_id,
18989 Http2BridgeEvent {
18990 kind: String::from("sessionClose"),
18991 id: session_id,
18992 ..Http2BridgeEvent::default()
18993 },
18994 );
18995 remove_http2_session_resources(&shared, session_id);
18996 break;
18997 }
18998 }
18999 }
19000 Some(command) = command_rx.recv() => {
19001 match command {
19002 Http2SessionCommand::Settings { settings_json, respond_to } => {
19003 let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
19004 .unwrap_or_default();
19005 if let Some(initial_window_size) = settings
19006 .get("initialWindowSize")
19007 .and_then(Value::as_u64)
19008 {
19009 let _ = connection.set_initial_window_size(initial_window_size as u32);
19010 }
19011 {
19012 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
19013 snapshot.local_settings = http2_settings_from_value(&settings);
19014 }
19015 if let Ok(headers_json) = serde_json::to_string(&settings) {
19016 push_http2_session_event(
19017 &shared,
19018 session_id,
19019 Http2BridgeEvent {
19020 kind: String::from("sessionLocalSettings"),
19021 id: session_id,
19022 data: Some(headers_json),
19023 ..Http2BridgeEvent::default()
19024 },
19025 );
19026 }
19027 let _ = respond_to.send(Ok(Value::Null));
19028 }
19029 Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
19030 connection.set_target_window_size(size);
19031 {
19032 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
19033 snapshot.state.local_window_size = size;
19034 snapshot.state.effective_local_window_size = size;
19035 }
19036 let value = snapshot
19037 .lock()
19038 .ok()
19039 .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
19040 .map(Value::String)
19041 .unwrap_or(Value::Null);
19042 let _ = respond_to.send(Ok(value));
19043 }
19044 Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
19045 connection.abrupt_shutdown(http2_reason(Some(error_code)));
19046 push_http2_session_event(
19047 &shared,
19048 session_id,
19049 Http2BridgeEvent {
19050 kind: String::from("sessionGoaway"),
19051 id: session_id,
19052 data: opaque_data.map(|value| {
19053 base64::engine::general_purpose::STANDARD.encode(value)
19054 }),
19055 extra_number: Some(error_code as u64),
19056 flags: Some(last_stream_id as u64),
19057 ..Http2BridgeEvent::default()
19058 },
19059 );
19060 let _ = respond_to.send(Ok(Value::Null));
19061 }
19062 Http2SessionCommand::Close { abrupt, respond_to } => {
19063 if abrupt {
19064 connection.abrupt_shutdown(Reason::NO_ERROR);
19065 } else {
19066 connection.graceful_shutdown();
19067 }
19068 let _ = respond_to.send(Ok(Value::Null));
19069 push_http2_session_event(
19070 &shared,
19071 session_id,
19072 Http2BridgeEvent {
19073 kind: String::from("sessionClose"),
19074 id: session_id,
19075 ..Http2BridgeEvent::default()
19076 },
19077 );
19078 remove_http2_session_resources(&shared, session_id);
19079 break;
19080 }
19081 Http2SessionCommand::StreamRespond { stream_id, headers_json, respond_to } => {
19082 let response = match build_http2_response(&headers_json) {
19083 Ok(response) => response,
19084 Err(error) => {
19085 let _ = respond_to.send(Err(error.to_string()));
19086 continue;
19087 }
19088 };
19089 let mut streams = streams.lock().expect("http2 server streams");
19090 let Some(state) = streams.get_mut(&stream_id) else {
19091 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19092 continue;
19093 };
19094 let Some(send_response) = state.send_response.as_mut() else {
19095 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
19096 continue;
19097 };
19098 match match send_response {
19099 ServerHttp2Responder::Regular(send_response) => {
19100 send_response.send_response(response, false)
19101 }
19102 ServerHttp2Responder::Pushed(send_response) => {
19103 send_response.send_response(response, false)
19104 }
19105 } {
19106 Ok(send_stream) => {
19107 state.send_stream = Some(send_stream);
19108 state.send_response = None;
19109 let _ = respond_to.send(Ok(Value::Null));
19110 }
19111 Err(error) => {
19112 let _ = respond_to.send(Err(error.to_string()));
19113 }
19114 }
19115 }
19116 Http2SessionCommand::StreamPush { stream_id, headers_json, respond_to } => {
19117 let request = match build_http2_request(&headers_json) {
19118 Ok(request) => request,
19119 Err(error) => {
19120 let _ = respond_to.send(Err(error.to_string()));
19121 continue;
19122 }
19123 };
19124 let mut streams_guard = streams.lock().expect("http2 server streams");
19125 let Some(state) = streams_guard.get_mut(&stream_id) else {
19126 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19127 continue;
19128 };
19129 let Some(send_response) = state.send_response.as_mut() else {
19130 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} cannot push after responding")));
19131 continue;
19132 };
19133 let ServerHttp2Responder::Regular(send_response) = send_response else {
19134 let _ = respond_to.send(Err(format!("HTTP/2 pushed stream {stream_id} cannot create nested push promises")));
19135 continue;
19136 };
19137 match send_response.push_request(request) {
19138 Ok(pushed) => {
19139 let pushed_stream_id = {
19140 let mut state = shared.lock().expect("http2 shared state");
19141 let pushed_stream_id = next_http2_stream_id(&mut state);
19142 state.streams.insert(
19143 pushed_stream_id,
19144 ActiveHttp2Stream {
19145 session_id,
19146 paused: Arc::new(AtomicBool::new(false)),
19147 },
19148 );
19149 pushed_stream_id
19150 };
19151 streams_guard.insert(
19152 pushed_stream_id,
19153 ServerHttp2StreamState {
19154 send_response: Some(ServerHttp2Responder::Pushed(pushed)),
19155 send_stream: None,
19156 },
19157 );
19158 let _ = respond_to.send(Ok(json!({
19159 "streamId": pushed_stream_id,
19160 "headers": headers_json,
19161 }).to_string().into()));
19162 }
19163 Err(error) => {
19164 let _ = respond_to.send(Err(error.to_string()));
19165 }
19166 }
19167 }
19168 Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
19169 let mut streams = streams.lock().expect("http2 server streams");
19170 let Some(state) = streams.get_mut(&stream_id) else {
19171 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19172 continue;
19173 };
19174 let Some(send_stream) = state.send_stream.as_mut() else {
19175 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} has not sent response headers")));
19176 continue;
19177 };
19178 match send_stream.send_data(Bytes::from(chunk), end_stream) {
19179 Ok(()) => {
19180 if end_stream {
19181 streams.remove(&stream_id);
19182 if let Ok(mut state) = shared.lock() {
19183 state.streams.remove(&stream_id);
19184 }
19185 push_http2_server_event(
19186 &shared,
19187 server_id,
19188 Http2BridgeEvent {
19189 kind: String::from("serverStreamClose"),
19190 id: stream_id,
19191 extra_number: Some(0),
19192 ..Http2BridgeEvent::default()
19193 },
19194 );
19195 }
19196 let _ = respond_to.send(Ok(Value::Bool(true)));
19197 }
19198 Err(error) => {
19199 let _ = respond_to.send(Err(error.to_string()));
19200 }
19201 }
19202 }
19203 Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
19204 let mut streams_guard = streams.lock().expect("http2 server streams");
19205 let Some(mut state) = streams_guard.remove(&stream_id) else {
19206 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19207 continue;
19208 };
19209 let reason = http2_reason(error_code);
19210 if let Some(send_stream) = state.send_stream.as_mut() {
19211 send_stream.send_reset(reason);
19212 }
19213 if let Some(send_response) = state.send_response.as_mut() {
19214 match send_response {
19215 ServerHttp2Responder::Regular(send_response) => {
19216 send_response.send_reset(reason)
19217 }
19218 ServerHttp2Responder::Pushed(send_response) => {
19219 send_response.send_reset(reason)
19220 }
19221 }
19222 }
19223 if let Ok(mut shared_guard) = shared.lock() {
19224 shared_guard.streams.remove(&stream_id);
19225 }
19226 push_http2_server_event(
19227 &shared,
19228 server_id,
19229 Http2BridgeEvent {
19230 kind: String::from("serverStreamClose"),
19231 id: stream_id,
19232 extra_number: Some(u32::from(reason) as u64),
19233 ..Http2BridgeEvent::default()
19234 },
19235 );
19236 let _ = respond_to.send(Ok(Value::Null));
19237 }
19238 Http2SessionCommand::StreamRespondWithFile { stream_id, body, headers_json, options_json, respond_to } => {
19239 let options: JavascriptHttp2FileResponseOptions =
19240 serde_json::from_str(&options_json).unwrap_or_default();
19241 let response = match build_http2_response(&headers_json) {
19242 Ok(response) => response,
19243 Err(error) => {
19244 let _ = respond_to.send(Err(error.to_string()));
19245 continue;
19246 }
19247 };
19248 let offset = usize::try_from(options.offset.unwrap_or_default()).unwrap_or(0);
19249 let body = if offset >= body.len() {
19250 Vec::new()
19251 } else {
19252 let body = &body[offset..];
19253 match options.length {
19254 Some(length) if length >= 0 => {
19255 body[..body.len().min(length as usize)].to_vec()
19256 }
19257 _ => body.to_vec(),
19258 }
19259 };
19260 let mut streams_guard = streams.lock().expect("http2 server streams");
19261 let Some(state) = streams_guard.get_mut(&stream_id) else {
19262 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19263 continue;
19264 };
19265 let Some(send_response) = state.send_response.as_mut() else {
19266 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
19267 continue;
19268 };
19269 match match send_response {
19270 ServerHttp2Responder::Regular(send_response) => {
19271 send_response.send_response(response, body.is_empty())
19272 }
19273 ServerHttp2Responder::Pushed(send_response) => {
19274 send_response.send_response(response, body.is_empty())
19275 }
19276 } {
19277 Ok(mut send_stream) => {
19278 state.send_response = None;
19279 if body.is_empty() {
19280 streams_guard.remove(&stream_id);
19281 if let Ok(mut shared_guard) = shared.lock() {
19282 shared_guard.streams.remove(&stream_id);
19283 }
19284 } else {
19285 if let Err(error) = send_stream.send_data(Bytes::from(body), true) {
19286 let _ = respond_to.send(Err(error.to_string()));
19287 continue;
19288 }
19289 streams_guard.remove(&stream_id);
19290 if let Ok(mut shared_guard) = shared.lock() {
19291 shared_guard.streams.remove(&stream_id);
19292 }
19293 }
19294 push_http2_server_event(
19295 &shared,
19296 server_id,
19297 Http2BridgeEvent {
19298 kind: String::from("serverStreamClose"),
19299 id: stream_id,
19300 extra_number: Some(0),
19301 ..Http2BridgeEvent::default()
19302 },
19303 );
19304 let _ = respond_to.send(Ok(Value::Null));
19305 }
19306 Err(error) => {
19307 let _ = respond_to.send(Err(error.to_string()));
19308 }
19309 }
19310 }
19311 Http2SessionCommand::Request { respond_to, .. } => {
19312 let _ = respond_to.send(Err(String::from("HTTP/2 server sessions cannot initiate client requests")));
19313 }
19314 }
19315 }
19316 else => break,
19317 }
19318 }
19319 });
19320 });
19321}
19322
19323fn spawn_http2_server_accept_loop(
19324 shared: Arc<Mutex<crate::state::Http2SharedState>>,
19325 server_id: u64,
19326 listener: TcpListener,
19327) {
19328 thread::spawn(move || {
19329 let listener = listener;
19330 loop {
19331 let closed = shared
19332 .lock()
19333 .ok()
19334 .and_then(|state| {
19335 state
19336 .servers
19337 .get(&server_id)
19338 .map(|server| server.closed.load(Ordering::SeqCst))
19339 })
19340 .unwrap_or(true);
19341 if closed {
19342 break;
19343 }
19344 match listener.accept() {
19345 Ok((stream, _)) => {
19346 let (command_tx, command_rx) = unbounded_channel();
19347 let (guest_local_addr, secure, tls) = {
19348 let state = shared.lock().expect("http2 shared state");
19349 let server = state.servers.get(&server_id).expect("http2 server state");
19350 (server.guest_local_addr, server.secure, server.tls.clone())
19351 };
19352 let (local_addr, remote_addr) = match (stream.local_addr(), stream.peer_addr())
19353 {
19354 (Ok(local_addr), Ok(remote_addr)) => (local_addr, remote_addr),
19355 _ => continue,
19356 };
19357 let session_snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
19358 encrypted: secure,
19359 alpn_protocol: Some(if secure {
19360 String::from("h2")
19361 } else {
19362 String::from("h2c")
19363 }),
19364 local_settings: BTreeMap::new(),
19365 remote_settings: BTreeMap::new(),
19366 state: http2_runtime_snapshot(),
19367 socket: Http2SocketSnapshot {
19368 local_address: Some(guest_local_addr.ip().to_string()),
19369 local_port: Some(guest_local_addr.port()),
19370 local_family: Some(socket_addr_family(&guest_local_addr).to_string()),
19371 remote_address: Some(remote_addr.ip().to_string()),
19372 remote_port: Some(remote_addr.port()),
19373 remote_family: Some(socket_addr_family(&remote_addr).to_string()),
19374 ..http2_socket_snapshot(local_addr, remote_addr)
19375 },
19376 ..Http2SessionSnapshot::default()
19377 }));
19378 let session_id = {
19379 let mut state = shared.lock().expect("http2 shared state");
19380 let session_id = next_http2_session_id(&mut state);
19381 state
19382 .sessions
19383 .insert(session_id, ActiveHttp2Session { command_tx });
19384 session_id
19385 };
19386 spawn_http2_server_session(
19387 Arc::clone(&shared),
19388 server_id,
19389 session_id,
19390 stream,
19391 tls,
19392 session_snapshot,
19393 command_rx,
19394 );
19395 }
19396 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
19397 thread::sleep(HTTP2_POLL_DELAY);
19398 }
19399 Err(error) => {
19400 push_http2_server_event(
19401 &shared,
19402 server_id,
19403 Http2BridgeEvent {
19404 kind: String::from("serverStreamError"),
19405 id: server_id,
19406 data: Some(http2_error_payload(error.to_string())),
19407 ..Http2BridgeEvent::default()
19408 },
19409 );
19410 thread::sleep(HTTP2_POLL_DELAY);
19411 }
19412 }
19413 }
19414 });
19415}
19416
19417fn send_http2_command(
19418 session: &ActiveHttp2Session,
19419 command: impl FnOnce(Sender<Result<Value, String>>) -> Http2SessionCommand,
19420) -> Result<Value, SidecarError> {
19421 let (respond_to, response_rx) = mpsc::channel();
19422 session.command_tx.send(command(respond_to)).map_err(|_| {
19423 SidecarError::InvalidState(String::from("HTTP/2 session command channel closed"))
19424 })?;
19425 response_rx
19426 .recv_timeout(Duration::from_secs(30))
19427 .map_err(|_| {
19428 SidecarError::Execution(String::from("timed out waiting for HTTP/2 session command"))
19429 })?
19430 .map_err(SidecarError::Execution)
19431}
19432
19433fn parse_http2_server_listen_payload(
19434 request: &JavascriptSyncRpcRequest,
19435) -> Result<JavascriptHttp2ServerListenRequest, SidecarError> {
19436 let payload_json =
19437 javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_server_listen payload")?;
19438 serde_json::from_str(payload_json).map_err(|error| {
19439 SidecarError::InvalidState(format!(
19440 "net.http2_server_listen payload must be valid JSON: {error}"
19441 ))
19442 })
19443}
19444
19445fn parse_http2_connect_payload(
19446 request: &JavascriptSyncRpcRequest,
19447) -> Result<JavascriptHttp2SessionConnectRequest, SidecarError> {
19448 let payload_json =
19449 javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_session_connect payload")?;
19450 serde_json::from_str(payload_json).map_err(|error| {
19451 SidecarError::InvalidState(format!(
19452 "net.http2_session_connect payload must be valid JSON: {error}"
19453 ))
19454 })
19455}
19456
19457fn http2_session_for_id(
19458 process: &ActiveProcess,
19459 session_id: u64,
19460) -> Result<ActiveHttp2Session, SidecarError> {
19461 let shared = process
19462 .http2
19463 .shared
19464 .lock()
19465 .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
19466 shared
19467 .sessions
19468 .get(&session_id)
19469 .cloned()
19470 .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 session {session_id}")))
19471}
19472
19473fn http2_stream_for_id(
19474 process: &ActiveProcess,
19475 stream_id: u64,
19476) -> Result<ActiveHttp2Stream, SidecarError> {
19477 let shared = process
19478 .http2
19479 .shared
19480 .lock()
19481 .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
19482 shared
19483 .streams
19484 .get(&stream_id)
19485 .cloned()
19486 .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 stream {stream_id}")))
19487}
19488
19489fn service_javascript_http2_sync_rpc<B>(
19490 request: JavascriptHttp2SyncRpcServiceRequest<'_, B>,
19491) -> Result<Value, SidecarError>
19492where
19493 B: NativeSidecarBridge + Send + 'static,
19494 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19495{
19496 let JavascriptHttp2SyncRpcServiceRequest {
19497 bridge,
19498 kernel,
19499 vm_id,
19500 dns,
19501 socket_paths,
19502 process,
19503 sync_request: request,
19504 resource_limits,
19505 network_counts,
19506 } = request;
19507 match request.method.as_str() {
19508 "net.http2_server_listen" => {
19509 check_network_resource_limit(
19510 resource_limits.max_sockets,
19511 network_counts.sockets,
19512 1,
19513 "socket",
19514 )?;
19515 let payload = parse_http2_server_listen_payload(request)?;
19516 let (family, bind_host, guest_host) =
19517 normalize_tcp_listen_host(payload.host.as_deref())?;
19518 let requested_port = payload.port.unwrap_or(0);
19519 bridge.require_network_access(
19520 vm_id,
19521 NetworkOperation::Listen,
19522 format_tcp_resource(bind_host, requested_port),
19523 )?;
19524 let port = allocate_guest_listen_port(
19525 requested_port,
19526 family,
19527 &socket_paths.used_tcp_guest_ports,
19528 socket_paths.listen_policy,
19529 )?;
19530 let mut listener =
19531 ActiveTcpListener::bind(bind_host, guest_host, port, payload.backlog)?;
19532 let guest_local_addr = listener.guest_local_addr();
19533 let closed = Arc::new(AtomicBool::new(false));
19534 {
19535 let mut state = process.http2.shared.lock().map_err(|_| {
19536 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19537 })?;
19538 state.servers.insert(
19539 payload.server_id,
19540 ActiveHttp2Server {
19541 actual_local_addr: listener.local_addr(),
19542 guest_local_addr,
19543 secure: payload.secure,
19544 tls: payload.tls.clone().map(|mut tls| {
19545 tls.is_server = payload.secure;
19546 if payload.secure && tls.alpn_protocols.is_none() {
19547 tls.alpn_protocols = Some(vec![String::from("h2")]);
19548 }
19549 tls
19550 }),
19551 closed: Arc::clone(&closed),
19552 },
19553 );
19554 state.server_events.entry(payload.server_id).or_default();
19555 }
19556 spawn_http2_server_accept_loop(
19557 Arc::clone(&process.http2.shared),
19558 payload.server_id,
19559 listener.listener.take().ok_or_else(|| {
19560 SidecarError::InvalidState(String::from(
19561 "HTTP/2 listener missing host TCP socket",
19562 ))
19563 })?,
19564 );
19565 javascript_net_json_string(
19566 json!({
19567 "address": {
19568 "address": guest_local_addr.ip().to_string(),
19569 "family": socket_addr_family(&guest_local_addr),
19570 "port": guest_local_addr.port(),
19571 }
19572 }),
19573 "net.http2_server_listen",
19574 )
19575 }
19576 "net.http2_server_poll" => {
19577 let server_id =
19578 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_poll server id")?;
19579 let wait_ms = javascript_sync_rpc_arg_u64_optional(
19580 &request.args,
19581 1,
19582 "net.http2_server_poll wait ms",
19583 )?
19584 .unwrap_or_default();
19585 match wait_for_http2_event(&process.http2.shared, server_id, true, wait_ms) {
19586 Some(event) => http2_event_value(&event),
19587 None => Ok(Value::Null),
19588 }
19589 }
19590 "net.http2_server_wait" => {
19591 let server_id =
19592 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_wait server id")?;
19593 dispatch_http2_wait_loop(process, server_id, true)
19594 }
19595 "net.http2_server_close" => {
19596 let server_id =
19597 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_close server id")?;
19598 let server = {
19599 let mut state = process.http2.shared.lock().map_err(|_| {
19600 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19601 })?;
19602 state.servers.remove(&server_id)
19603 }
19604 .ok_or_else(|| {
19605 SidecarError::InvalidState(format!("unknown HTTP/2 server {server_id}"))
19606 })?;
19607 server.closed.store(true, Ordering::SeqCst);
19608 push_http2_server_event(
19609 &process.http2.shared,
19610 server_id,
19611 Http2BridgeEvent {
19612 kind: String::from("serverClose"),
19613 id: server_id,
19614 ..Http2BridgeEvent::default()
19615 },
19616 );
19617 Ok(Value::Null)
19618 }
19619 "net.http2_server_respond" => {
19620 let server_id = javascript_sync_rpc_arg_u64(
19621 &request.args,
19622 0,
19623 "net.http2_server_respond server id",
19624 )?;
19625 let request_id = javascript_sync_rpc_arg_u64(
19626 &request.args,
19627 1,
19628 "net.http2_server_respond request id",
19629 )?;
19630 let response_json =
19631 javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_server_respond payload")?;
19632 ensure_vm_fetch_response_within_limit(
19633 response_json,
19634 "net.http2_server_respond",
19635 VM_FETCH_BUFFER_LIMIT_BYTES,
19636 )?;
19637 serde_json::from_str::<Value>(response_json).map_err(|error| {
19638 SidecarError::Execution(format!(
19639 "net.http2_server_respond payload must be valid JSON: {error}"
19640 ))
19641 })?;
19642 let Some(pending) = process
19643 .pending_http_requests
19644 .get_mut(&(server_id, request_id))
19645 else {
19646 return Err(SidecarError::InvalidState(format!(
19647 "unknown pending HTTP/2 request {request_id} for server {server_id}"
19648 )));
19649 };
19650 *pending = Some(response_json.to_owned());
19651 Ok(Value::Bool(true))
19652 }
19653 "net.http2_session_connect" => {
19654 check_network_resource_limit(
19655 resource_limits.max_sockets,
19656 network_counts.sockets,
19657 1,
19658 "socket",
19659 )?;
19660 check_network_resource_limit(
19661 resource_limits.max_connections,
19662 network_counts.connections,
19663 1,
19664 "connection",
19665 )?;
19666 let payload = parse_http2_connect_payload(request)?;
19667 let authority = payload.authority.clone().unwrap_or_else(|| {
19668 format!(
19669 "{}://{}:{}",
19670 payload.protocol.as_deref().unwrap_or("http"),
19671 payload.host.as_deref().unwrap_or("localhost"),
19672 payload.port.unwrap_or(80)
19673 )
19674 });
19675 let url = Url::parse(&authority).map_err(|error| {
19676 SidecarError::InvalidState(format!(
19677 "invalid HTTP/2 authority {authority:?}: {error}"
19678 ))
19679 })?;
19680 let secure = url.scheme() == "https" || payload.protocol.as_deref() == Some("https:");
19681 let host = payload
19682 .host
19683 .as_deref()
19684 .or_else(|| url.host_str())
19685 .unwrap_or("localhost");
19686 let port = payload.port.or_else(|| url.port()).unwrap_or(80);
19687 bridge.require_network_access(
19688 vm_id,
19689 NetworkOperation::Http,
19690 format_tcp_resource(host, port),
19691 )?;
19692 let resolved = {
19693 let shared = process.http2.shared.lock().map_err(|_| {
19694 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19695 })?;
19696 shared
19697 .servers
19698 .values()
19699 .find(|server| {
19700 is_loopback_request_host(host) && server.guest_local_addr.port() == port
19701 })
19702 .map(|server| ResolvedTcpConnectAddr {
19703 actual_addr: server.actual_local_addr,
19704 guest_remote_addr: server.guest_local_addr,
19705 use_kernel_loopback: false,
19706 })
19707 };
19708 let resolved = match resolved {
19709 Some(resolved) => resolved,
19710 None => {
19711 resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, socket_paths)?
19712 }
19713 };
19714 let (command_tx, command_rx) = unbounded_channel();
19715 let snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
19716 encrypted: secure,
19717 alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
19718 local_settings: http2_settings_from_value(&payload.settings),
19719 remote_settings: BTreeMap::new(),
19720 state: http2_runtime_snapshot(),
19721 socket: Http2SocketSnapshot {
19722 encrypted: secure,
19723 remote_address: Some(resolved.guest_remote_addr.ip().to_string()),
19724 remote_port: Some(resolved.guest_remote_addr.port()),
19725 remote_family: Some(
19726 socket_addr_family(&resolved.guest_remote_addr).to_string(),
19727 ),
19728 servername: if secure {
19729 payload
19730 .tls
19731 .as_ref()
19732 .and_then(|tls| tls.servername.clone())
19733 .or_else(|| Some(host.to_string()))
19734 } else {
19735 None
19736 },
19737 alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
19738 ..Http2SocketSnapshot::default()
19739 },
19740 ..Http2SessionSnapshot::default()
19741 }));
19742 let session_id = {
19743 let mut state = process.http2.shared.lock().map_err(|_| {
19744 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19745 })?;
19746 let session_id = next_http2_session_id(&mut state);
19747 state
19748 .sessions
19749 .insert(session_id, ActiveHttp2Session { command_tx });
19750 state.session_events.entry(session_id).or_default();
19751 session_id
19752 };
19753 spawn_http2_client_session(
19754 Arc::clone(&process.http2.shared),
19755 session_id,
19756 resolved.actual_addr,
19757 if secure {
19758 Some(payload.tls.unwrap_or(JavascriptTlsBridgeOptions {
19759 is_server: false,
19760 servername: Some(host.to_string()),
19761 alpn_protocols: Some(vec![String::from("h2")]),
19762 ..JavascriptTlsBridgeOptions::default()
19763 }))
19764 } else {
19765 None
19766 },
19767 Arc::clone(&snapshot),
19768 command_rx,
19769 );
19770 let snapshot_json =
19771 http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())?;
19772 javascript_net_json_string(
19773 json!({
19774 "sessionId": session_id,
19775 "state": snapshot_json,
19776 }),
19777 "net.http2_session_connect",
19778 )
19779 }
19780 "net.http2_session_request" => {
19781 let session_id = javascript_sync_rpc_arg_u64(
19782 &request.args,
19783 0,
19784 "net.http2_session_request session id",
19785 )?;
19786 let headers_json =
19787 javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_session_request headers")?;
19788 let options_json =
19789 javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_session_request options")?;
19790 let session = http2_session_for_id(process, session_id)?;
19791 send_http2_command(&session, |respond_to| Http2SessionCommand::Request {
19792 headers_json: headers_json.to_owned(),
19793 options_json: options_json.to_owned(),
19794 respond_to,
19795 })
19796 }
19797 "net.http2_session_settings" => {
19798 let session_id = javascript_sync_rpc_arg_u64(
19799 &request.args,
19800 0,
19801 "net.http2_session_settings session id",
19802 )?;
19803 let settings_json = javascript_sync_rpc_arg_str(
19804 &request.args,
19805 1,
19806 "net.http2_session_settings settings",
19807 )?;
19808 let session = http2_session_for_id(process, session_id)?;
19809 send_http2_command(&session, |respond_to| Http2SessionCommand::Settings {
19810 settings_json: settings_json.to_owned(),
19811 respond_to,
19812 })
19813 }
19814 "net.http2_session_set_local_window_size" => {
19815 let session_id = javascript_sync_rpc_arg_u64(
19816 &request.args,
19817 0,
19818 "net.http2_session_set_local_window_size session id",
19819 )?;
19820 let window_size = javascript_sync_rpc_arg_u64(
19821 &request.args,
19822 1,
19823 "net.http2_session_set_local_window_size window size",
19824 )?;
19825 let session = http2_session_for_id(process, session_id)?;
19826 send_http2_command(&session, |respond_to| {
19827 Http2SessionCommand::SetLocalWindowSize {
19828 size: window_size as u32,
19829 respond_to,
19830 }
19831 })
19832 }
19833 "net.http2_session_goaway" => {
19834 let session_id = javascript_sync_rpc_arg_u64(
19835 &request.args,
19836 0,
19837 "net.http2_session_goaway session id",
19838 )?;
19839 let error_code = javascript_sync_rpc_arg_u64(
19840 &request.args,
19841 1,
19842 "net.http2_session_goaway error code",
19843 )?;
19844 let last_stream_id = javascript_sync_rpc_arg_u64(
19845 &request.args,
19846 2,
19847 "net.http2_session_goaway last stream id",
19848 )?;
19849 let opaque_data = request
19850 .args
19851 .get(3)
19852 .and_then(Value::as_str)
19853 .map(|value| {
19854 base64::engine::general_purpose::STANDARD
19855 .decode(value)
19856 .map_err(|error| {
19857 SidecarError::InvalidState(format!("invalid GOAWAY payload: {error}"))
19858 })
19859 })
19860 .transpose()?;
19861 let session = http2_session_for_id(process, session_id)?;
19862 send_http2_command(&session, |respond_to| Http2SessionCommand::Goaway {
19863 error_code: error_code as u32,
19864 last_stream_id: last_stream_id as u32,
19865 opaque_data,
19866 respond_to,
19867 })
19868 }
19869 "net.http2_session_close" | "net.http2_session_destroy" => {
19870 let session_id = javascript_sync_rpc_arg_u64(
19871 &request.args,
19872 0,
19873 "net.http2_session_close session id",
19874 )?;
19875 let session = http2_session_for_id(process, session_id)?;
19876 send_http2_command(&session, |respond_to| Http2SessionCommand::Close {
19877 abrupt: request.method == "net.http2_session_destroy",
19878 respond_to,
19879 })
19880 }
19881 "net.http2_session_poll" => {
19882 let session_id =
19883 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_poll session id")?;
19884 let wait_ms = javascript_sync_rpc_arg_u64_optional(
19885 &request.args,
19886 1,
19887 "net.http2_session_poll wait ms",
19888 )?
19889 .unwrap_or_default();
19890 match wait_for_http2_event(&process.http2.shared, session_id, false, wait_ms) {
19891 Some(event) => http2_event_value(&event),
19892 None => Ok(Value::Null),
19893 }
19894 }
19895 "net.http2_session_wait" => {
19896 let session_id =
19897 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_wait session id")?;
19898 dispatch_http2_wait_loop(process, session_id, false)
19899 }
19900 "net.http2_stream_respond" => {
19901 let stream_id = javascript_sync_rpc_arg_u64(
19902 &request.args,
19903 0,
19904 "net.http2_stream_respond stream id",
19905 )?;
19906 let headers_json =
19907 javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_stream_respond headers")?;
19908 let stream = http2_stream_for_id(process, stream_id)?;
19909 let session = http2_session_for_id(process, stream.session_id)?;
19910 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamRespond {
19911 stream_id,
19912 headers_json: headers_json.to_owned(),
19913 respond_to,
19914 })
19915 }
19916 "net.http2_stream_push_stream" => {
19917 let stream_id = javascript_sync_rpc_arg_u64(
19918 &request.args,
19919 0,
19920 "net.http2_stream_push_stream stream id",
19921 )?;
19922 let headers_json = javascript_sync_rpc_arg_str(
19923 &request.args,
19924 1,
19925 "net.http2_stream_push_stream headers",
19926 )?;
19927 let _options_json = javascript_sync_rpc_arg_str(
19928 &request.args,
19929 2,
19930 "net.http2_stream_push_stream options",
19931 )?;
19932 let stream = http2_stream_for_id(process, stream_id)?;
19933 let session = http2_session_for_id(process, stream.session_id)?;
19934 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamPush {
19935 stream_id,
19936 headers_json: headers_json.to_owned(),
19937 respond_to,
19938 })
19939 }
19940 "net.http2_stream_write" => {
19941 let stream_id =
19942 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_write stream id")?;
19943 let chunk =
19944 javascript_sync_rpc_base64_arg(&request.args, 1, "net.http2_stream_write data")?;
19945 let stream = http2_stream_for_id(process, stream_id)?;
19946 let session = http2_session_for_id(process, stream.session_id)?;
19947 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
19948 stream_id,
19949 chunk,
19950 end_stream: false,
19951 respond_to,
19952 })
19953 }
19954 "net.http2_stream_end" => {
19955 let stream_id =
19956 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_end stream id")?;
19957 let chunk = request
19958 .args
19959 .get(1)
19960 .and_then(Value::as_str)
19961 .map(|value| {
19962 base64::engine::general_purpose::STANDARD
19963 .decode(value)
19964 .map_err(|error| {
19965 SidecarError::InvalidState(format!(
19966 "invalid HTTP/2 stream payload: {error}"
19967 ))
19968 })
19969 })
19970 .transpose()?
19971 .unwrap_or_default();
19972 let stream = http2_stream_for_id(process, stream_id)?;
19973 let session = http2_session_for_id(process, stream.session_id)?;
19974 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
19975 stream_id,
19976 chunk,
19977 end_stream: true,
19978 respond_to,
19979 })
19980 }
19981 "net.http2_stream_close" => {
19982 let stream_id =
19983 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_close stream id")?;
19984 let code = javascript_sync_rpc_arg_u64_optional(
19985 &request.args,
19986 1,
19987 "net.http2_stream_close error code",
19988 )?
19989 .map(|value| value as u32);
19990 let stream = http2_stream_for_id(process, stream_id)?;
19991 let session = http2_session_for_id(process, stream.session_id)?;
19992 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamClose {
19993 stream_id,
19994 error_code: code,
19995 respond_to,
19996 })
19997 }
19998 "net.http2_stream_pause" => {
19999 let stream_id =
20000 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_pause stream id")?;
20001 let stream = http2_stream_for_id(process, stream_id)?;
20002 stream.paused.store(true, Ordering::SeqCst);
20003 Ok(Value::Null)
20004 }
20005 "net.http2_stream_resume" => {
20006 let stream_id =
20007 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_resume stream id")?;
20008 let stream = http2_stream_for_id(process, stream_id)?;
20009 stream.paused.store(false, Ordering::SeqCst);
20010 Ok(Value::Null)
20011 }
20012 "net.http2_stream_respond_with_file" => {
20013 let stream_id = javascript_sync_rpc_arg_u64(
20014 &request.args,
20015 0,
20016 "net.http2_stream_respond_with_file stream id",
20017 )?;
20018 let path = javascript_sync_rpc_arg_str(
20019 &request.args,
20020 1,
20021 "net.http2_stream_respond_with_file path",
20022 )?;
20023 let headers_json = javascript_sync_rpc_arg_str(
20024 &request.args,
20025 2,
20026 "net.http2_stream_respond_with_file headers",
20027 )?;
20028 let options_json = javascript_sync_rpc_arg_str(
20029 &request.args,
20030 3,
20031 "net.http2_stream_respond_with_file options",
20032 )?;
20033 let stream = http2_stream_for_id(process, stream_id)?;
20034 let session = http2_session_for_id(process, stream.session_id)?;
20035 let guest_path = resolve_http2_file_response_guest_path(process, path);
20036 let body = kernel.read_file(&guest_path).map_err(kernel_error)?;
20037 send_http2_command(&session, |respond_to| {
20038 Http2SessionCommand::StreamRespondWithFile {
20039 stream_id,
20040 body,
20041 headers_json: headers_json.to_owned(),
20042 options_json: options_json.to_owned(),
20043 respond_to,
20044 }
20045 })
20046 }
20047 other => Err(SidecarError::InvalidState(format!(
20048 "unsupported JavaScript HTTP/2 sync RPC method {other}"
20049 ))),
20050 }
20051}
20052
20053const JAVASCRIPT_NET_POLL_MAX_WAIT: Duration = Duration::from_millis(50);
20054const EXITED_PROCESS_SNAPSHOT_RETENTION: Duration = Duration::from_secs(2);
20055
20056fn resolve_http2_file_response_guest_path(process: &ActiveProcess, path: &str) -> String {
20057 if Path::new(path).is_absolute() {
20058 normalize_path(path)
20059 } else {
20060 normalize_path(&format!("{}/{}", process.guest_cwd, path))
20061 }
20062}
20063
20064pub(crate) fn clamp_javascript_net_poll_wait(wait_ms: u64) -> Duration {
20065 if wait_ms == 0 {
20068 Duration::ZERO
20069 } else {
20070 Duration::from_millis(wait_ms).min(JAVASCRIPT_NET_POLL_MAX_WAIT)
20071 }
20072}
20073
20074pub(crate) fn service_javascript_net_sync_rpc<B>(
20075 request: JavascriptNetSyncRpcServiceRequest<'_, B>,
20076) -> Result<Value, SidecarError>
20077where
20078 B: NativeSidecarBridge + Send + 'static,
20079 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
20080{
20081 let JavascriptNetSyncRpcServiceRequest {
20082 bridge,
20083 vm_id,
20084 dns,
20085 socket_paths,
20086 kernel,
20087 process,
20088 sync_request: request,
20089 resource_limits,
20090 network_counts,
20091 } = request;
20092 match request.method.as_str() {
20093 "net.http_listen" => {
20094 check_network_resource_limit(
20095 resource_limits.max_sockets,
20096 network_counts.sockets,
20097 1,
20098 "socket",
20099 )?;
20100 let payload_json =
20101 javascript_sync_rpc_arg_str(&request.args, 0, "net.http_listen payload")?;
20102 let payload: JavascriptHttpListenRequest =
20103 serde_json::from_str(payload_json).map_err(|error| {
20104 SidecarError::InvalidState(format!(
20105 "net.http_listen payload must be valid JSON: {error}"
20106 ))
20107 })?;
20108 let (family, bind_host, guest_host) =
20109 normalize_tcp_listen_host(payload.hostname.as_deref())?;
20110 let requested_port = payload.port.unwrap_or(0);
20111 bridge.require_network_access(
20112 vm_id,
20113 NetworkOperation::Listen,
20114 format_tcp_resource(bind_host, requested_port),
20115 )?;
20116 let port = allocate_guest_listen_port(
20117 requested_port,
20118 family,
20119 &socket_paths.used_tcp_guest_ports,
20120 socket_paths.listen_policy,
20121 )?;
20122 let mut listener = ActiveTcpListener::bind(
20123 bind_host,
20124 guest_host,
20125 port,
20126 Some(DEFAULT_JAVASCRIPT_NET_BACKLOG),
20127 )?;
20128 let guest_local_addr = listener.guest_local_addr();
20129 process.http_servers.insert(
20130 payload.server_id,
20131 ActiveHttpServer {
20132 listener: listener.listener.take().ok_or_else(|| {
20133 SidecarError::InvalidState(String::from(
20134 "HTTP listener missing host TCP socket",
20135 ))
20136 })?,
20137 guest_local_addr,
20138 next_request_id: 0,
20139 },
20140 );
20141 serde_json::to_string(&json!({
20142 "address": {
20143 "address": guest_local_addr.ip().to_string(),
20144 "family": socket_addr_family(&guest_local_addr),
20145 "port": guest_local_addr.port(),
20146 }
20147 }))
20148 .map(Value::String)
20149 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
20150 }
20151 "net.http_close" => {
20152 let server_id =
20153 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_close server id")?;
20154 let server = process.http_servers.remove(&server_id).ok_or_else(|| {
20155 SidecarError::InvalidState(format!("unknown HTTP server {server_id}"))
20156 })?;
20157 drop(server.listener);
20158 process
20159 .pending_http_requests
20160 .retain(|(pending_server_id, _), _| *pending_server_id != server_id);
20161 Ok(Value::Null)
20162 }
20163 "net.http_wait" => {
20164 let server_id =
20165 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_wait server id")?;
20166 dispatch_http_wait_loop(process, server_id)
20167 }
20168 "net.http_respond" => {
20169 let server_id =
20170 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_respond server id")?;
20171 let request_id =
20172 javascript_sync_rpc_arg_u64(&request.args, 1, "net.http_respond request id")?;
20173 let response_json =
20174 javascript_sync_rpc_arg_str(&request.args, 2, "net.http_respond payload")?;
20175 ensure_vm_fetch_response_within_limit(
20176 response_json,
20177 "net.http_respond",
20178 VM_FETCH_BUFFER_LIMIT_BYTES,
20179 )?;
20180 serde_json::from_str::<Value>(response_json).map_err(|error| {
20181 SidecarError::Execution(format!(
20182 "net.http_respond payload must be valid JSON: {error}"
20183 ))
20184 })?;
20185 let Some(pending) = process
20186 .pending_http_requests
20187 .get_mut(&(server_id, request_id))
20188 else {
20189 return Err(SidecarError::InvalidState(format!(
20190 "unknown pending HTTP request {request_id} for server {server_id}"
20191 )));
20192 };
20193 *pending = Some(response_json.to_owned());
20194 Ok(Value::Null)
20195 }
20196 "net.reserve_tcp_port" => {
20197 let payload = request
20198 .args
20199 .first()
20200 .cloned()
20201 .ok_or_else(|| {
20202 SidecarError::InvalidState(String::from(
20203 "net.reserve_tcp_port requires a request payload",
20204 ))
20205 })
20206 .and_then(|value| {
20207 serde_json::from_value::<JavascriptNetReserveTcpPortRequest>(value).map_err(
20208 |error| {
20209 SidecarError::InvalidState(format!(
20210 "invalid net.reserve_tcp_port payload: {error}"
20211 ))
20212 },
20213 )
20214 })?;
20215 let (family, _bind_host, guest_host) =
20216 normalize_tcp_listen_host(payload.host.as_deref())?;
20217 let requested_port = payload.port.unwrap_or(0);
20218 let port = allocate_guest_listen_port(
20219 requested_port,
20220 family,
20221 &socket_paths.used_tcp_guest_ports,
20222 socket_paths.listen_policy,
20223 )?;
20224 let reservation_id = process.allocate_tcp_port_reservation_id();
20225 process
20226 .tcp_port_reservations
20227 .insert(reservation_id.clone(), (family, port));
20228 Ok(json!({
20229 "reservationId": reservation_id,
20230 "localAddress": guest_host,
20231 "localPort": port,
20232 "family": match family {
20233 JavascriptSocketFamily::Ipv4 => "IPv4",
20234 JavascriptSocketFamily::Ipv6 => "IPv6",
20235 },
20236 }))
20237 }
20238 "net.release_tcp_port" => {
20239 let reservation_id =
20240 javascript_sync_rpc_arg_str(&request.args, 0, "net.release_tcp_port reservation")?;
20241 process.tcp_port_reservations.remove(reservation_id);
20242 Ok(Value::Null)
20243 }
20244 "net.connect" => {
20245 check_network_resource_limit(
20246 resource_limits.max_sockets,
20247 network_counts.sockets,
20248 1,
20249 "socket",
20250 )?;
20251 check_network_resource_limit(
20252 resource_limits.max_connections,
20253 network_counts.connections,
20254 1,
20255 "connection",
20256 )?;
20257 let payload = request
20258 .args
20259 .first()
20260 .cloned()
20261 .ok_or_else(|| {
20262 SidecarError::InvalidState(String::from(
20263 "net.connect requires a request payload",
20264 ))
20265 })
20266 .and_then(|value| {
20267 serde_json::from_value::<JavascriptNetConnectRequest>(value).map_err(|error| {
20268 SidecarError::InvalidState(format!("invalid net.connect payload: {error}"))
20269 })
20270 })?;
20271 if let Some(path) = payload.path.as_deref() {
20272 let guest_path = normalize_path(path);
20273 let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
20274 let socket = ActiveUnixSocket::connect(&host_path, &guest_path)?;
20275 let socket_id = process.allocate_unix_socket_id();
20276 process.unix_sockets.insert(socket_id.clone(), socket);
20277 Ok(json!({
20278 "socketId": socket_id,
20279 "remotePath": guest_path,
20280 }))
20281 } else {
20282 let port = payload.port.ok_or_else(|| {
20283 SidecarError::InvalidState(String::from(
20284 "net.connect requires either a path or port",
20285 ))
20286 })?;
20287 let host = payload.host.as_deref().unwrap_or("localhost");
20288 let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
20289 process
20290 .tcp_port_reservations
20291 .remove(id)
20292 .map(|reservation| (id.to_owned(), reservation))
20293 });
20294 bridge.require_network_access(
20295 vm_id,
20296 NetworkOperation::Http,
20297 format_tcp_resource(host, port),
20298 )?;
20299 if is_loopback_socket_host(host) {
20300 let families = [JavascriptSocketFamily::Ipv4, JavascriptSocketFamily::Ipv6];
20301 if let Some((family, target)) = families.iter().find_map(|family| {
20302 socket_paths
20303 .http_loopback_target(*family, port)
20304 .map(|target| (*family, target))
20305 }) {
20306 if let Some((reservation_id, reservation)) = local_reservation {
20307 process
20308 .tcp_port_reservations
20309 .insert(reservation_id, reservation);
20310 }
20311 let remote_address = match family {
20312 JavascriptSocketFamily::Ipv4 => "127.0.0.1",
20313 JavascriptSocketFamily::Ipv6 => "::1",
20314 };
20315 return Ok(json!({
20316 "loopbackHttpTarget": {
20317 "processId": target.process_id.clone(),
20318 "serverId": target.server_id,
20319 "host": remote_address,
20320 "port": port,
20321 },
20322 "localAddress": match family {
20323 JavascriptSocketFamily::Ipv4 => "127.0.0.1",
20324 JavascriptSocketFamily::Ipv6 => "::1",
20325 },
20326 "localPort": payload.local_port.unwrap_or(0),
20327 "remoteAddress": remote_address,
20328 "remotePort": port,
20329 "remoteFamily": match family {
20330 JavascriptSocketFamily::Ipv4 => "IPv4",
20331 JavascriptSocketFamily::Ipv6 => "IPv6",
20332 },
20333 }));
20334 }
20335 }
20336 let connect_result = ActiveTcpSocket::connect(ActiveTcpConnectRequest {
20337 bridge,
20338 kernel,
20339 kernel_pid: process.kernel_pid,
20340 vm_id,
20341 dns,
20342 host,
20343 port,
20344 local_address: payload.local_address.as_deref(),
20345 local_port: payload.local_port,
20346 local_reservation: local_reservation
20347 .as_ref()
20348 .map(|(_, reservation)| *reservation),
20349 context: socket_paths,
20350 });
20351 if let Err(error) = connect_result {
20352 if let Some((reservation_id, reservation)) = local_reservation {
20353 process
20354 .tcp_port_reservations
20355 .insert(reservation_id, reservation);
20356 }
20357 return Err(error);
20358 }
20359 let socket = connect_result?;
20360 let socket_id = process.allocate_tcp_socket_id();
20361 let local_addr = socket.guest_local_addr;
20362 let remote_addr = socket.guest_remote_addr;
20363 process.tcp_sockets.insert(socket_id.clone(), socket);
20364 Ok(json!({
20365 "socketId": socket_id,
20366 "localAddress": local_addr.ip().to_string(),
20367 "localPort": local_addr.port(),
20368 "remoteAddress": remote_addr.ip().to_string(),
20369 "remotePort": remote_addr.port(),
20370 "remoteFamily": socket_addr_family(&remote_addr),
20371 }))
20372 }
20373 }
20374 "net.listen" => {
20375 check_network_resource_limit(
20376 resource_limits.max_sockets,
20377 network_counts.sockets,
20378 1,
20379 "socket",
20380 )?;
20381 let payload = request
20382 .args
20383 .first()
20384 .cloned()
20385 .ok_or_else(|| {
20386 SidecarError::InvalidState(String::from(
20387 "net.listen requires a request payload",
20388 ))
20389 })
20390 .and_then(|value| match value {
20391 Value::String(json) => {
20392 serde_json::from_str::<JavascriptNetListenRequest>(&json).map_err(|error| {
20393 SidecarError::InvalidState(format!(
20394 "invalid net.listen payload: {error}"
20395 ))
20396 })
20397 }
20398 other => serde_json::from_value::<JavascriptNetListenRequest>(other).map_err(
20399 |error| {
20400 SidecarError::InvalidState(format!(
20401 "invalid net.listen payload: {error}"
20402 ))
20403 },
20404 ),
20405 })?;
20406 if let Some(path) = payload.path.as_deref() {
20407 let guest_path = normalize_path(path);
20408 if kernel.exists(&guest_path).map_err(kernel_error)? {
20409 return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
20410 libc::EADDRINUSE,
20411 )));
20412 }
20413
20414 let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
20415 let on_host_mount =
20416 host_mount_path_for_guest_path_from_mounts(&socket_paths.mounts, &guest_path)
20417 .is_some();
20418 let listener = ActiveUnixListener::bind(&host_path, &guest_path, payload.backlog)?;
20419 if !on_host_mount {
20420 ensure_kernel_parent_directories(kernel, &guest_path)?;
20421 kernel
20422 .write_file(&guest_path, Vec::new())
20423 .map_err(kernel_error)?;
20424 }
20425 let listener_id = process.allocate_unix_listener_id();
20426 process.unix_listeners.insert(listener_id.clone(), listener);
20427 Ok(json!({
20428 "serverId": listener_id,
20429 "path": guest_path,
20430 }))
20431 } else {
20432 let (family, bind_host, guest_host) =
20433 normalize_tcp_listen_host(payload.host.as_deref())?;
20434 let requested_port = payload.port.unwrap_or(0);
20435 bridge.require_network_access(
20436 vm_id,
20437 NetworkOperation::Listen,
20438 format_tcp_resource(bind_host, requested_port),
20439 )?;
20440 let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
20441 process
20442 .tcp_port_reservations
20443 .remove(id)
20444 .map(|reservation| (id.to_owned(), reservation))
20445 });
20446 let port = if requested_port != 0
20447 && local_reservation
20448 .as_ref()
20449 .map(|(_, reservation)| *reservation)
20450 == Some((family, requested_port))
20451 {
20452 requested_port
20453 } else {
20454 allocate_guest_listen_port(
20455 requested_port,
20456 family,
20457 &socket_paths.used_tcp_guest_ports,
20458 socket_paths.listen_policy,
20459 )?
20460 };
20461 let listener_result = ActiveTcpListener::bind_kernel(
20462 kernel,
20463 process.kernel_pid,
20464 guest_host,
20465 port,
20466 payload.backlog,
20467 );
20468 if let Err(error) = listener_result {
20469 if let Some((reservation_id, reservation)) = local_reservation {
20470 process
20471 .tcp_port_reservations
20472 .insert(reservation_id, reservation);
20473 }
20474 return Err(error);
20475 }
20476 let listener = listener_result?;
20477 let listener_id = process.allocate_tcp_listener_id();
20478 let local_addr = listener.guest_local_addr();
20479 process.tcp_listeners.insert(listener_id.clone(), listener);
20480 Ok(json!({
20481 "serverId": listener_id,
20482 "localAddress": local_addr.ip().to_string(),
20483 "localPort": local_addr.port(),
20484 "family": socket_addr_family(&local_addr),
20485 }))
20486 }
20487 }
20488 "net.poll" => {
20489 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.poll socket id")?;
20490 let wait_ms =
20491 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.poll wait ms")?
20492 .unwrap_or_default();
20493 let wait = clamp_javascript_net_poll_wait(wait_ms);
20494 let event = if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20495 socket.poll(kernel, process.kernel_pid, wait)?
20496 } else if let Some(socket) = process.unix_sockets.get_mut(socket_id) {
20497 socket.poll(wait)?
20498 } else {
20499 return Err(SidecarError::InvalidState(format!(
20500 "unknown net socket {socket_id}"
20501 )));
20502 };
20503
20504 match event {
20505 Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(json!({
20506 "type": "data",
20507 "data": javascript_sync_rpc_bytes_value(&chunk),
20508 })),
20509 Some(JavascriptTcpSocketEvent::End) => Ok(json!({
20510 "type": "end",
20511 })),
20512 Some(JavascriptTcpSocketEvent::Error { code, message }) => Ok(json!({
20513 "type": "error",
20514 "code": code,
20515 "message": message,
20516 })),
20517 Some(JavascriptTcpSocketEvent::Close { had_error }) => {
20518 if let Some(socket) = process.tcp_sockets.remove(socket_id) {
20519 if let Some(listener_id) = socket.listener_id.as_deref() {
20520 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20521 listener.release_connection(socket_id);
20522 }
20523 }
20524 } else if let Some(socket) = process.unix_sockets.remove(socket_id) {
20525 if let Some(listener_id) = socket.listener_id.as_deref() {
20526 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20527 listener.release_connection(socket_id);
20528 }
20529 }
20530 }
20531 Ok(json!({
20532 "type": "close",
20533 "hadError": had_error,
20534 }))
20535 }
20536 None => Ok(Value::Null),
20537 }
20538 }
20539 "net.socket_wait_connect" => {
20540 let socket_id =
20541 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_wait_connect socket id")?;
20542 if let Some(socket) = process.tcp_sockets.get(socket_id) {
20543 javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
20544 } else {
20545 let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20546 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20547 })?;
20548 javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
20549 }
20550 }
20551 "net.socket_read" => {
20552 let socket_id =
20553 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_read socket id")?;
20554 if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20555 javascript_net_read_value(socket.poll(
20556 kernel,
20557 process.kernel_pid,
20558 Duration::ZERO,
20559 )?)
20560 } else {
20561 let socket = process.unix_sockets.get_mut(socket_id).ok_or_else(|| {
20562 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20563 })?;
20564 javascript_net_read_value(socket.poll(Duration::ZERO)?)
20565 }
20566 }
20567 "net.socket_set_no_delay" => {
20568 let socket_id =
20569 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_set_no_delay socket id")?;
20570 let enable =
20571 javascript_sync_rpc_arg_bool(&request.args, 1, "net.socket_set_no_delay enabled")?;
20572 if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20573 socket.set_no_delay(enable)?;
20574 } else if !process.unix_sockets.contains_key(socket_id) {
20575 return Err(SidecarError::InvalidState(format!(
20576 "unknown net socket {socket_id}"
20577 )));
20578 }
20579 Ok(Value::Null)
20580 }
20581 "net.socket_set_keep_alive" => {
20582 let socket_id = javascript_sync_rpc_arg_str(
20583 &request.args,
20584 0,
20585 "net.socket_set_keep_alive socket id",
20586 )?;
20587 let enable = javascript_sync_rpc_arg_bool(
20588 &request.args,
20589 1,
20590 "net.socket_set_keep_alive enabled",
20591 )?;
20592 let initial_delay_secs = javascript_sync_rpc_arg_u64_optional(
20593 &request.args,
20594 2,
20595 "net.socket_set_keep_alive initial delay seconds",
20596 )?;
20597 if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20598 socket.set_keep_alive(enable, initial_delay_secs)?;
20599 } else if !process.unix_sockets.contains_key(socket_id) {
20600 return Err(SidecarError::InvalidState(format!(
20601 "unknown net socket {socket_id}"
20602 )));
20603 }
20604 Ok(Value::Null)
20605 }
20606 "net.socket_upgrade_tls" => {
20607 let socket_id =
20608 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_upgrade_tls socket id")?;
20609 let options_json =
20610 javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_upgrade_tls options")?;
20611 let options: JavascriptTlsBridgeOptions =
20612 serde_json::from_str(options_json).map_err(|error| {
20613 SidecarError::InvalidState(format!(
20614 "net.socket_upgrade_tls options must be valid JSON: {error}"
20615 ))
20616 })?;
20617 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20618 SidecarError::InvalidState(format!(
20619 "unknown TCP socket {socket_id} for TLS upgrade"
20620 ))
20621 })?;
20622 socket.upgrade_tls(vm_id, kernel, options)?;
20623 Ok(Value::Null)
20624 }
20625 "net.socket_get_tls_client_hello" => {
20626 let socket_id = javascript_sync_rpc_arg_str(
20627 &request.args,
20628 0,
20629 "net.socket_get_tls_client_hello socket id",
20630 )?;
20631 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20632 SidecarError::InvalidState(format!(
20633 "unknown TCP socket {socket_id} for TLS client hello query"
20634 ))
20635 })?;
20636 socket.tls_client_hello_json(vm_id, kernel)
20637 }
20638 "net.socket_tls_query" => {
20639 let socket_id =
20640 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_tls_query socket id")?;
20641 let query =
20642 javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_tls_query query")?;
20643 let detailed = request
20644 .args
20645 .get(2)
20646 .and_then(Value::as_bool)
20647 .unwrap_or(false);
20648 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20649 SidecarError::InvalidState(format!("unknown TCP socket {socket_id} for TLS query"))
20650 })?;
20651 socket.tls_query(query, detailed)
20652 }
20653 "net.server_poll" => {
20654 let listener_id =
20655 javascript_sync_rpc_arg_str(&request.args, 0, "net.server_poll listener id")?;
20656 let wait_ms =
20657 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.server_poll wait ms")?
20658 .unwrap_or_default();
20659 let tcp_event = if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20660 Some(listener.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?)
20661 } else {
20662 None
20663 };
20664
20665 if let Some(event) = tcp_event {
20666 return match event {
20667 Some(JavascriptTcpListenerEvent::Connection(pending)) => {
20668 let PendingTcpSocket {
20669 stream,
20670 kernel_socket_id,
20671 preallocated,
20672 guest_local_addr,
20673 guest_remote_addr,
20674 } = pending;
20675 if !preallocated {
20676 if let Err(error) = check_network_resource_limit(
20677 resource_limits.max_sockets,
20678 network_counts.sockets,
20679 1,
20680 "socket",
20681 )
20682 .and_then(|()| {
20683 check_network_resource_limit(
20684 resource_limits.max_connections,
20685 network_counts.connections,
20686 1,
20687 "connection",
20688 )
20689 }) {
20690 if let Some(stream) = stream {
20691 let _ = stream.shutdown(Shutdown::Both);
20692 }
20693 return Ok(json!({
20694 "type": "error",
20695 "code": "EAGAIN",
20696 "message": error.to_string(),
20697 }));
20698 }
20699 }
20700 let socket = if let Some(stream) = stream {
20701 ActiveTcpSocket::from_stream(
20702 stream,
20703 Some(listener_id.to_string()),
20704 guest_local_addr,
20705 guest_remote_addr,
20706 )?
20707 } else {
20708 ActiveTcpSocket::from_kernel(
20709 kernel_socket_id.ok_or_else(|| {
20710 SidecarError::InvalidState(String::from(
20711 "kernel TCP accept missing socket id",
20712 ))
20713 })?,
20714 Some(listener_id.to_string()),
20715 guest_local_addr,
20716 guest_remote_addr,
20717 )
20718 };
20719 let socket_id = process.allocate_tcp_socket_id();
20720 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20721 listener.register_connection(&socket_id);
20722 }
20723 process.tcp_sockets.insert(socket_id.clone(), socket);
20724 Ok(json!({
20725 "type": "connection",
20726 "socketId": socket_id,
20727 "localAddress": guest_local_addr.ip().to_string(),
20728 "localPort": guest_local_addr.port(),
20729 "remoteAddress": guest_remote_addr.ip().to_string(),
20730 "remotePort": guest_remote_addr.port(),
20731 "remoteFamily": socket_addr_family(&guest_remote_addr),
20732 }))
20733 }
20734 Some(JavascriptTcpListenerEvent::Error { code, message }) => Ok(json!({
20735 "type": "error",
20736 "code": code,
20737 "message": message,
20738 })),
20739 None => Ok(Value::Null),
20740 };
20741 }
20742
20743 let event = {
20744 let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
20745 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20746 })?;
20747 listener.poll(Duration::from_millis(wait_ms))?
20748 };
20749
20750 match event {
20751 Some(JavascriptUnixListenerEvent::Connection(pending)) => {
20752 if let Err(error) = check_network_resource_limit(
20753 resource_limits.max_sockets,
20754 network_counts.sockets,
20755 1,
20756 "socket",
20757 )
20758 .and_then(|()| {
20759 check_network_resource_limit(
20760 resource_limits.max_connections,
20761 network_counts.connections,
20762 1,
20763 "connection",
20764 )
20765 }) {
20766 let _ = pending.stream.shutdown(Shutdown::Both);
20767 return Ok(json!({
20768 "type": "error",
20769 "code": "EAGAIN",
20770 "message": error.to_string(),
20771 }));
20772 }
20773 let socket = ActiveUnixSocket::from_stream(
20774 pending.stream,
20775 Some(listener_id.to_string()),
20776 pending.local_path.clone(),
20777 pending.remote_path.clone(),
20778 )?;
20779 let socket_id = process.allocate_unix_socket_id();
20780 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20781 listener.register_connection(&socket_id);
20782 }
20783 process.unix_sockets.insert(socket_id.clone(), socket);
20784 Ok(json!({
20785 "type": "connection",
20786 "socketId": socket_id,
20787 "localPath": pending.local_path,
20788 "remotePath": pending.remote_path,
20789 }))
20790 }
20791 Some(JavascriptUnixListenerEvent::Error { code, message }) => Ok(json!({
20792 "type": "error",
20793 "code": code,
20794 "message": message,
20795 })),
20796 None => Ok(Value::Null),
20797 }
20798 }
20799 "net.server_accept" => {
20800 let listener_id =
20801 javascript_sync_rpc_arg_str(&request.args, 0, "net.server_accept listener id")?;
20802 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20803 return match listener.poll(kernel, process.kernel_pid, Duration::ZERO)? {
20804 Some(JavascriptTcpListenerEvent::Connection(pending)) => {
20805 let PendingTcpSocket {
20806 stream,
20807 kernel_socket_id,
20808 preallocated,
20809 guest_local_addr,
20810 guest_remote_addr,
20811 } = pending;
20812 if !preallocated {
20813 check_network_resource_limit(
20814 resource_limits.max_sockets,
20815 network_counts.sockets,
20816 1,
20817 "socket",
20818 )?;
20819 check_network_resource_limit(
20820 resource_limits.max_connections,
20821 network_counts.connections,
20822 1,
20823 "connection",
20824 )?;
20825 }
20826 let info = json!({
20827 "localAddress": guest_local_addr.ip().to_string(),
20828 "localPort": guest_local_addr.port(),
20829 "localFamily": socket_addr_family(&guest_local_addr),
20830 "remoteAddress": guest_remote_addr.ip().to_string(),
20831 "remotePort": guest_remote_addr.port(),
20832 "remoteFamily": socket_addr_family(&guest_remote_addr),
20833 });
20834 let socket = if let Some(stream) = stream {
20835 ActiveTcpSocket::from_stream(
20836 stream,
20837 Some(listener_id.to_string()),
20838 guest_local_addr,
20839 guest_remote_addr,
20840 )?
20841 } else {
20842 ActiveTcpSocket::from_kernel(
20843 kernel_socket_id.ok_or_else(|| {
20844 SidecarError::InvalidState(String::from(
20845 "kernel TCP accept missing socket id",
20846 ))
20847 })?,
20848 Some(listener_id.to_string()),
20849 guest_local_addr,
20850 guest_remote_addr,
20851 )
20852 };
20853 let socket_id = process.allocate_tcp_socket_id();
20854 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20855 listener.register_connection(&socket_id);
20856 }
20857 process.tcp_sockets.insert(socket_id.clone(), socket);
20858 javascript_net_json_string(
20859 json!({
20860 "socketId": socket_id,
20861 "info": info,
20862 }),
20863 "net.server_accept",
20864 )
20865 }
20866 Some(JavascriptTcpListenerEvent::Error { code, message }) => {
20867 let detail = code.unwrap_or_else(|| String::from("server accept"));
20868 Err(SidecarError::Execution(format!("{detail}: {message}")))
20869 }
20870 None => Ok(javascript_net_timeout_value()),
20871 };
20872 }
20873
20874 let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
20875 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20876 })?;
20877 match listener.poll(Duration::ZERO)? {
20878 Some(JavascriptUnixListenerEvent::Connection(pending)) => {
20879 check_network_resource_limit(
20880 resource_limits.max_sockets,
20881 network_counts.sockets,
20882 1,
20883 "socket",
20884 )?;
20885 check_network_resource_limit(
20886 resource_limits.max_connections,
20887 network_counts.connections,
20888 1,
20889 "connection",
20890 )?;
20891 let info = json!({
20892 "localPath": pending.local_path.clone(),
20893 "remotePath": pending.remote_path.clone(),
20894 });
20895 let socket = ActiveUnixSocket::from_stream(
20896 pending.stream,
20897 Some(listener_id.to_string()),
20898 pending.local_path,
20899 pending.remote_path,
20900 )?;
20901 let socket_id = process.allocate_unix_socket_id();
20902 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20903 listener.register_connection(&socket_id);
20904 }
20905 process.unix_sockets.insert(socket_id.clone(), socket);
20906 javascript_net_json_string(
20907 json!({
20908 "socketId": socket_id,
20909 "info": info,
20910 }),
20911 "net.server_accept",
20912 )
20913 }
20914 Some(JavascriptUnixListenerEvent::Error { code, message }) => {
20915 let detail = code.unwrap_or_else(|| String::from("server accept"));
20916 Err(SidecarError::Execution(format!("{detail}: {message}")))
20917 }
20918 None => Ok(javascript_net_timeout_value()),
20919 }
20920 }
20921 "net.server_connections" => {
20922 let listener_id = javascript_sync_rpc_arg_str(
20923 &request.args,
20924 0,
20925 "net.server_connections listener id",
20926 )?;
20927 if let Some(listener) = process.tcp_listeners.get(listener_id) {
20928 Ok(json!(listener.active_connection_count()))
20929 } else {
20930 let listener = process.unix_listeners.get(listener_id).ok_or_else(|| {
20931 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20932 })?;
20933 Ok(json!(listener.active_connection_count()))
20934 }
20935 }
20936 "net.upgrade_socket_write" => {
20937 let socket_id = javascript_sync_rpc_arg_str(
20938 &request.args,
20939 0,
20940 "net.upgrade_socket_write socket id",
20941 )?;
20942 let chunk =
20943 javascript_sync_rpc_base64_arg(&request.args, 1, "net.upgrade_socket_write chunk")?;
20944 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20945 SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20946 })?;
20947 socket
20948 .write_all(kernel, process.kernel_pid, &chunk)
20949 .map(|written| json!(written))
20950 }
20951 "net.upgrade_socket_end" => {
20952 let socket_id =
20953 javascript_sync_rpc_arg_str(&request.args, 0, "net.upgrade_socket_end socket id")?;
20954 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20955 SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20956 })?;
20957 socket.shutdown_write(kernel, process.kernel_pid)?;
20958 Ok(Value::Null)
20959 }
20960 "net.upgrade_socket_destroy" => {
20961 let socket_id = javascript_sync_rpc_arg_str(
20962 &request.args,
20963 0,
20964 "net.upgrade_socket_destroy socket id",
20965 )?;
20966 let socket = process.tcp_sockets.remove(socket_id).ok_or_else(|| {
20967 SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20968 })?;
20969 if let Some(listener_id) = socket.listener_id.as_deref() {
20970 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20971 listener.release_connection(socket_id);
20972 }
20973 }
20974 let _ = socket.close(kernel, process.kernel_pid);
20975 Ok(Value::Null)
20976 }
20977 "net.write" => {
20978 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.write socket id")?;
20979 let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "net.write chunk")?;
20980 if let Some(socket) = process.tcp_sockets.get(socket_id) {
20981 socket
20982 .write_all(kernel, process.kernel_pid, &chunk)
20983 .map(|written| json!(written))
20984 } else {
20985 let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20986 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20987 })?;
20988 socket.write_all(&chunk).map(|written| json!(written))
20989 }
20990 }
20991 "net.shutdown" => {
20992 let socket_id =
20993 javascript_sync_rpc_arg_str(&request.args, 0, "net.shutdown socket id")?;
20994 if let Some(socket) = process.tcp_sockets.get(socket_id) {
20995 socket.shutdown_write(kernel, process.kernel_pid)?;
20996 } else {
20997 let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20998 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20999 })?;
21000 socket.shutdown_write()?;
21001 }
21002 Ok(Value::Null)
21003 }
21004 "net.destroy" => {
21005 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.destroy socket id")?;
21006 if let Some(socket) = process.tcp_sockets.remove(socket_id) {
21007 if let Some(listener_id) = socket.listener_id.as_deref() {
21008 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
21009 listener.release_connection(socket_id);
21010 }
21011 }
21012 let _ = socket.close(kernel, process.kernel_pid);
21013 Ok(Value::Null)
21014 } else {
21015 let socket = process.unix_sockets.remove(socket_id).ok_or_else(|| {
21016 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
21017 })?;
21018 if let Some(listener_id) = socket.listener_id.as_deref() {
21019 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
21020 listener.release_connection(socket_id);
21021 }
21022 }
21023 let _ = socket.close();
21024 Ok(Value::Null)
21025 }
21026 }
21027 "net.server_close" => {
21028 let listener_id =
21029 javascript_sync_rpc_arg_str(&request.args, 0, "net.server_close listener id")?;
21030 if let Some(listener) = process.tcp_listeners.remove(listener_id) {
21031 listener.close(kernel, process.kernel_pid)?;
21032 Ok(Value::Null)
21033 } else {
21034 let listener = process.unix_listeners.remove(listener_id).ok_or_else(|| {
21035 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
21036 })?;
21037 listener.close()?;
21038 Ok(Value::Null)
21039 }
21040 }
21041 "tls.get_ciphers" => javascript_net_json_string(
21042 Value::Array(
21043 tls_provider()
21044 .cipher_suites
21045 .iter()
21046 .filter_map(|suite| {
21047 suite
21048 .suite()
21049 .as_str()
21050 .map(|value| Value::String(value.to_owned()))
21051 })
21052 .collect(),
21053 ),
21054 "tls.get_ciphers",
21055 ),
21056 _ => Err(SidecarError::InvalidState(format!(
21057 "unsupported JavaScript net sync RPC method {}",
21058 request.method
21059 ))),
21060 }
21061}
21062
21063fn signal_name_for_stream_event(signal: i32) -> Option<&'static str> {
21064 match signal {
21065 libc::SIGHUP => Some("SIGHUP"),
21066 libc::SIGINT => Some("SIGINT"),
21067 libc::SIGUSR1 => Some("SIGUSR1"),
21068 libc::SIGALRM => Some("SIGALRM"),
21069 libc::SIGCONT => Some("SIGCONT"),
21070 libc::SIGTERM => Some("SIGTERM"),
21071 libc::SIGCHLD => Some("SIGCHLD"),
21072 libc::SIGWINCH => Some("SIGWINCH"),
21073 _ => None,
21074 }
21075}
21076
21077pub(crate) fn canonical_signal_name(signal: i32) -> Option<&'static str> {
21078 match signal {
21079 1 => Some("SIGHUP"),
21080 2 => Some("SIGINT"),
21081 3 => Some("SIGQUIT"),
21082 4 => Some("SIGILL"),
21083 5 => Some("SIGTRAP"),
21084 6 => Some("SIGABRT"),
21085 7 => Some("SIGBUS"),
21086 8 => Some("SIGFPE"),
21087 9 => Some("SIGKILL"),
21088 10 => Some("SIGUSR1"),
21089 11 => Some("SIGSEGV"),
21090 12 => Some("SIGUSR2"),
21091 13 => Some("SIGPIPE"),
21092 14 => Some("SIGALRM"),
21093 15 => Some("SIGTERM"),
21094 17 => Some("SIGCHLD"),
21095 18 => Some("SIGCONT"),
21096 19 => Some("SIGSTOP"),
21097 20 => Some("SIGTSTP"),
21098 21 => Some("SIGTTIN"),
21099 22 => Some("SIGTTOU"),
21100 23 => Some("SIGURG"),
21101 24 => Some("SIGXCPU"),
21102 25 => Some("SIGXFSZ"),
21103 26 => Some("SIGVTALRM"),
21104 27 => Some("SIGPROF"),
21105 28 => Some("SIGWINCH"),
21106 29 => Some("SIGIO"),
21107 30 => Some("SIGPWR"),
21108 31 => Some("SIGSYS"),
21109 _ => None,
21110 }
21111}
21112
21113fn dispatch_v8_process_signal(process: &ActiveProcess, signal: i32) -> Result<bool, SidecarError> {
21114 let Some(signal_name) = signal_name_for_stream_event(signal) else {
21115 return Ok(false);
21116 };
21117 process.execution.send_javascript_stream_event(
21118 "signal",
21119 json!({
21120 "signal": signal_name,
21121 "number": signal,
21122 "action": "default",
21123 }),
21124 )?;
21125 Ok(true)
21126}
21127
21128fn dispatch_v8_session_signal_async(session: V8SessionHandle, signal: i32) {
21129 let Some(signal_name) = signal_name_for_stream_event(signal).map(str::to_owned) else {
21130 return;
21131 };
21132 thread::spawn(move || {
21133 thread::sleep(Duration::from_millis(1));
21134 let payload = v8_runtime::json_to_cbor_payload(&json!({
21135 "signal": signal_name,
21136 "number": signal,
21137 "action": "default",
21138 }))
21139 .unwrap_or_default();
21140 let _ = session.send_stream_event("signal", payload);
21141 });
21142}
21143
21144pub(crate) fn parse_signal(signal: &str) -> Result<i32, SidecarError> {
21145 let trimmed = signal.trim();
21146 if trimmed.is_empty() {
21147 return Err(SidecarError::InvalidState(String::from(
21148 "kill_process requires a non-empty signal",
21149 )));
21150 }
21151
21152 if let Ok(value) = trimmed.parse::<i32>() {
21153 return match value {
21154 0..=31 => Ok(value),
21155 _ => Err(SidecarError::InvalidState(format!(
21156 "unsupported kill_process signal {signal}"
21157 ))),
21158 };
21159 }
21160
21161 let upper = trimmed.to_ascii_uppercase();
21162 let normalized = upper.strip_prefix("SIG").unwrap_or(&upper);
21163
21164 signal_number_from_name(normalized).ok_or_else(|| {
21165 SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
21166 })
21167}
21168
21169fn signal_number_from_name(signal: &str) -> Option<i32> {
21170 match signal {
21171 "0" => Some(0),
21172 "HUP" => Some(1),
21173 "INT" => Some(2),
21174 "QUIT" => Some(3),
21175 "ILL" => Some(4),
21176 "TRAP" => Some(5),
21177 "ABRT" | "IOT" => Some(6),
21178 "BUS" => Some(7),
21179 "FPE" => Some(8),
21180 "KILL" => Some(9),
21181 "USR1" => Some(10),
21182 "SEGV" => Some(11),
21183 "USR2" => Some(12),
21184 "PIPE" => Some(13),
21185 "ALRM" => Some(14),
21186 "TERM" => Some(15),
21187 "STKFLT" => Some(16),
21188 "CHLD" => Some(17),
21189 "CONT" => Some(18),
21190 "STOP" => Some(19),
21191 "TSTP" => Some(20),
21192 "TTIN" => Some(21),
21193 "TTOU" => Some(22),
21194 "URG" => Some(23),
21195 "XCPU" => Some(24),
21196 "XFSZ" => Some(25),
21197 "VTALRM" => Some(26),
21198 "PROF" => Some(27),
21199 "WINCH" => Some(28),
21200 "IO" | "POLL" => Some(29),
21201 "PWR" => Some(30),
21202 "SYS" => Some(31),
21203 _ => None,
21204 }
21205}
21206
21207pub(crate) fn runtime_child_is_alive(child_pid: u32) -> Result<bool, SidecarError> {
21208 Ok(runtime_child_exit_status(child_pid)?.is_none())
21209}
21210
21211#[cfg(not(target_os = "macos"))]
21212fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
21213 if child_pid == 0 {
21214 return Ok(Some(0));
21215 }
21216
21217 let wait_flags = WaitPidFlag::WNOHANG
21218 | WaitPidFlag::WNOWAIT
21219 | WaitPidFlag::WEXITED
21220 | WaitPidFlag::WUNTRACED
21221 | WaitPidFlag::WCONTINUED;
21222 match wait_on_child(WaitId::Pid(Pid::from_raw(child_pid as i32)), wait_flags) {
21223 Ok(WaitStatus::StillAlive)
21224 | Ok(WaitStatus::Stopped(_, _))
21225 | Ok(WaitStatus::Continued(_)) => Ok(None),
21226 Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
21227 Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
21228 #[cfg(any(target_os = "linux", target_os = "android"))]
21229 Ok(WaitStatus::PtraceEvent(_, _, _) | WaitStatus::PtraceSyscall(_)) => Ok(None),
21230 Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
21231 Err(error) => Err(SidecarError::Execution(format!(
21232 "failed to inspect guest runtime process {child_pid}: {error}"
21233 ))),
21234 }
21235}
21236
21237#[cfg(target_os = "macos")]
21243fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
21244 if child_pid == 0 {
21245 return Ok(Some(0));
21246 }
21247
21248 match waitpid(Pid::from_raw(child_pid as i32), Some(WaitPidFlag::WNOHANG)) {
21249 Ok(WaitStatus::StillAlive)
21250 | Ok(WaitStatus::Stopped(_, _))
21251 | Ok(WaitStatus::Continued(_)) => Ok(None),
21252 Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
21253 Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
21254 Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
21255 Err(error) => Err(SidecarError::Execution(format!(
21256 "failed to inspect guest runtime process {child_pid}: {error}"
21257 ))),
21258 }
21259}
21260
21261pub(crate) fn signal_runtime_process(child_pid: u32, signal: i32) -> Result<(), SidecarError> {
21262 if child_pid == 0 {
21263 return Ok(());
21264 }
21265
21266 if !runtime_child_is_alive(child_pid)? {
21267 return Ok(());
21268 }
21269
21270 if signal == 0 {
21271 return Ok(());
21272 }
21273
21274 let parsed = Signal::try_from(signal).map_err(|_| {
21275 SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
21276 })?;
21277 let result = send_signal(Pid::from_raw(child_pid as i32), Some(parsed));
21278
21279 match result {
21280 Ok(()) => Ok(()),
21281 Err(nix::errno::Errno::ESRCH) => Ok(()),
21282 Err(error) => Err(SidecarError::Execution(format!(
21283 "failed to signal guest runtime process {child_pid}: {error}"
21284 ))),
21285 }
21286}
21287
21288pub(crate) fn error_code(error: &SidecarError) -> &'static str {
21289 match error {
21290 SidecarError::InvalidState(_) => "invalid_state",
21291 SidecarError::ProtocolVersionMismatch(_) => "protocol_version_mismatch",
21292 SidecarError::BridgeVersionMismatch(_) => "bridge_version_mismatch",
21293 SidecarError::Conflict(_) => "conflict",
21294 SidecarError::Unauthorized(_) => "unauthorized",
21295 SidecarError::Unsupported(_) => "unsupported",
21296 SidecarError::FrameTooLarge(_) => "frame_too_large",
21297 SidecarError::Kernel(_) => "kernel_error",
21298 SidecarError::Plugin(_) => "plugin_error",
21299 SidecarError::Execution(_) => "execution_error",
21300 SidecarError::Bridge(_) => "bridge_error",
21301 SidecarError::Io(_) => "io_error",
21302 }
21303}
21304
21305fn guest_errno_code(message: &str) -> Option<&str> {
21306 const TRUSTED_PREFIXES: &[&str] = &[
21307 "ERR_AGENTOS_NODE_SYNC_RPC",
21308 "ERR_AGENTOS_PYTHON_VFS_RPC",
21309 "ERR_AGENTOS_BRIDGE",
21310 ];
21311
21312 let mut segments = message.split(':').map(str::trim);
21313 let first = segments.next()?;
21314 if is_guest_errno_segment(first) {
21315 return Some(first);
21316 }
21317
21318 if TRUSTED_PREFIXES.contains(&first) {
21319 let second = segments.next()?;
21320 if is_guest_errno_segment(second) {
21321 return Some(second);
21322 }
21323 }
21324
21325 None
21326}
21327
21328fn is_guest_errno_segment(segment: &str) -> bool {
21329 segment.len() >= 2
21330 && segment.starts_with('E')
21331 && !segment.starts_with("ERR_")
21332 && segment[1..]
21333 .bytes()
21334 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
21335}
21336
21337pub(crate) fn javascript_sync_rpc_error_code(error: &SidecarError) -> String {
21338 let message = error.to_string();
21339 if let Some(code) = guest_errno_code(&message) {
21340 return code.to_owned();
21341 }
21342 if message.starts_with("ERR_NATIVE_BINARY_NOT_SUPPORTED:") {
21343 return String::from("ERR_NATIVE_BINARY_NOT_SUPPORTED");
21344 }
21345
21346 let lower = message.to_ascii_lowercase();
21347 if lower.contains("no such file or directory")
21348 || lower.contains("entry not found")
21349 || lower.contains("not found")
21350 {
21351 return String::from("ENOENT");
21352 }
21353 if lower.contains("permission denied") {
21354 return String::from("EACCES");
21355 }
21356 if lower.contains("already exists")
21357 || lower.contains("already registered")
21358 || lower.contains("file exists")
21359 {
21360 return String::from("EEXIST");
21361 }
21362 if lower.contains("invalid argument") {
21363 return String::from("EINVAL");
21364 }
21365
21366 String::from("ERR_AGENTOS_NODE_SYNC_RPC")
21367}
21368
21369pub(crate) fn ignore_stale_javascript_sync_rpc_response(
21370 error: SidecarError,
21371) -> Result<(), SidecarError> {
21372 match error {
21373 SidecarError::Execution(message)
21374 if message.ends_with("is no longer pending")
21375 && message.starts_with("sync RPC request ") =>
21376 {
21377 Ok(())
21378 }
21379 SidecarError::Execution(message) => {
21380 let lower = message.to_ascii_lowercase();
21381 if lower.contains("sync rpc response")
21382 && (lower.contains("broken pipe") || lower.contains("channel closed unexpectedly"))
21383 {
21384 Ok(())
21385 } else {
21386 Err(SidecarError::Execution(message))
21387 }
21388 }
21389 other => Err(other),
21390 }
21391}
21392
21393#[cfg(test)]
21394mod error_code_tests {
21395 use super::{guest_errno_code, javascript_sync_rpc_error_code, SidecarError};
21396
21397 #[test]
21398 fn guest_errno_code_rejects_guest_controlled_errno_segments() {
21399 assert_eq!(guest_errno_code("user said 'EACCES: denied'"), None);
21400 assert_eq!(
21401 guest_errno_code("prefix: user said 'EPERM': more text"),
21402 None
21403 );
21404 assert_eq!(guest_errno_code("ERR_AGENTOS_FAKE: EACCES: denied"), None);
21405 }
21406
21407 #[test]
21408 fn guest_errno_code_accepts_trusted_secure_exec_prefixes() {
21409 assert_eq!(
21410 guest_errno_code("ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo"),
21411 Some("EACCES")
21412 );
21413 assert_eq!(
21414 guest_errno_code("ERR_AGENTOS_PYTHON_VFS_RPC: ENOENT: missing file"),
21415 Some("ENOENT")
21416 );
21417 assert_eq!(guest_errno_code("EEXIST: already exists"), Some("EEXIST"));
21418 }
21419
21420 #[test]
21421 fn javascript_sync_rpc_error_code_ignores_spoofed_errnos() {
21422 let error = SidecarError::Execution(String::from("user said 'EACCES: denied'"));
21423 assert_eq!(
21424 javascript_sync_rpc_error_code(&error),
21425 "ERR_AGENTOS_NODE_SYNC_RPC"
21426 );
21427 }
21428
21429 #[test]
21430 fn javascript_sync_rpc_error_code_preserves_real_sidecar_errnos() {
21431 let error = SidecarError::Execution(String::from(
21432 "ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo",
21433 ));
21434 assert_eq!(javascript_sync_rpc_error_code(&error), "EACCES");
21435 }
21436
21437 #[test]
21438 fn javascript_sync_rpc_error_code_maps_file_exists_messages() {
21439 let error = SidecarError::Io(String::from(
21440 "failed to create mapped guest directory /.next/server: File exists (os error 17)",
21441 ));
21442 assert_eq!(javascript_sync_rpc_error_code(&error), "EEXIST");
21443 }
21444
21445 #[test]
21446 fn javascript_sync_rpc_error_code_preserves_native_binary_rejections() {
21447 let error = SidecarError::Execution(String::from(
21448 "ERR_NATIVE_BINARY_NOT_SUPPORTED: refused to execute native ELF guest binary at /tmp/fake-rg inside the VM",
21449 ));
21450 assert_eq!(
21451 javascript_sync_rpc_error_code(&error),
21452 "ERR_NATIVE_BINARY_NOT_SUPPORTED"
21453 );
21454 }
21455}
21456#[cfg(test)]
21457mod ssrf_egress_classifier_tests {
21458 use super::{
21468 filter_dns_safe_ip_addrs, is_loopback_ip, restricted_non_loopback_ip_range, SidecarError,
21469 };
21470 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
21471
21472 fn assert_restricted(ip: IpAddr, expected_label: &str) {
21473 let classification = restricted_non_loopback_ip_range(ip);
21474 assert!(
21475 classification.is_some(),
21476 "{ip} must be classified as a restricted egress target"
21477 );
21478 let (_cidr, label) = classification.unwrap();
21479 assert_eq!(
21480 label, expected_label,
21481 "{ip} should be labelled {expected_label}, got {label}"
21482 );
21483 }
21484
21485 fn assert_dns_denied(ip: IpAddr, label: &str) {
21486 match filter_dns_safe_ip_addrs(vec![ip], "attacker.example") {
21487 Err(SidecarError::Execution(message)) => assert!(
21488 message.starts_with("EACCES:"),
21489 "{label}: egress filter must deny with EACCES, got: {message}"
21490 ),
21491 other => panic!("{label}: expected EACCES denial, got {other:?}"),
21492 }
21493 }
21494
21495 #[test]
21497 fn classifier_denies_unspecified_and_cgnat_targets() {
21498 assert_restricted(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "unspecified");
21500 assert_restricted(IpAddr::V6(Ipv6Addr::UNSPECIFIED), "unspecified");
21502
21503 assert_restricted(
21505 IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
21506 "carrier-grade-nat",
21507 );
21508 assert_restricted(
21509 IpAddr::V4(Ipv4Addr::new(100, 127, 255, 254)),
21510 "carrier-grade-nat",
21511 );
21512
21513 assert!(
21515 restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 63, 255, 255)))
21516 .is_none(),
21517 "100.63.255.255 is outside CGNAT and must remain allowed"
21518 );
21519 assert!(
21520 restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 128, 0, 0))).is_none(),
21521 "100.128.0.0 is outside CGNAT and must remain allowed"
21522 );
21523
21524 assert_dns_denied(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "0.0.0.0 (unspecified)");
21526 assert_dns_denied(IpAddr::V6(Ipv6Addr::UNSPECIFIED), ":: (unspecified)");
21527 assert_dns_denied(
21528 IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
21529 "100.64.0.1 (CGNAT)",
21530 );
21531 }
21532
21533 #[test]
21535 fn classifier_denies_ipv6_spelled_metadata_addresses() {
21536 let mapped = "::ffff:169.254.169.254".parse::<Ipv6Addr>().unwrap();
21539 assert_restricted(IpAddr::V6(mapped), "link-local");
21540
21541 let compat = "::169.254.169.254".parse::<Ipv6Addr>().unwrap();
21542 assert_restricted(IpAddr::V6(compat), "link-local");
21543
21544 assert_restricted(
21546 IpAddr::V6("::10.0.0.1".parse::<Ipv6Addr>().unwrap()),
21547 "private",
21548 );
21549 assert_restricted(
21550 IpAddr::V6("::100.64.0.1".parse::<Ipv6Addr>().unwrap()),
21551 "carrier-grade-nat",
21552 );
21553
21554 assert_eq!(
21558 restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::UNSPECIFIED)),
21559 Some(("::/128", "unspecified")),
21560 ":: must classify as unspecified, not via the IPv4-compat path"
21561 );
21562 assert!(
21563 restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::LOCALHOST)).is_none()
21564 || is_loopback_ip(IpAddr::V6(Ipv6Addr::LOCALHOST)),
21565 "::1 must not be classified as a restricted IPv4-compatible target"
21566 );
21567 assert!(
21568 restricted_non_loopback_ip_range(IpAddr::V6("::8.8.8.8".parse::<Ipv6Addr>().unwrap()))
21569 .is_none(),
21570 "::8.8.8.8 (public IPv4-compatible) must remain allowed"
21571 );
21572
21573 assert_dns_denied(
21575 IpAddr::V6("::169.254.169.254".parse::<Ipv6Addr>().unwrap()),
21576 "::169.254.169.254 (IPv4-compat metadata)",
21577 );
21578 }
21579
21580 #[test]
21582 fn classifier_denies_reserved_and_multicast_targets() {
21583 assert_restricted(IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)), "multicast");
21587 assert_restricted(IpAddr::V4(Ipv4Addr::new(239, 255, 255, 255)), "multicast");
21588 assert_restricted(IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)), "reserved");
21589 assert_restricted(IpAddr::V4(Ipv4Addr::BROADCAST), "reserved");
21591
21592 assert_restricted(
21594 IpAddr::V6("::224.0.0.1".parse::<Ipv6Addr>().unwrap()),
21595 "multicast",
21596 );
21597 assert_restricted(
21598 IpAddr::V6("::240.0.0.1".parse::<Ipv6Addr>().unwrap()),
21599 "reserved",
21600 );
21601
21602 assert!(
21604 restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(223, 255, 255, 255)))
21605 .is_none(),
21606 "223.255.255.255 is outside 224/4 and must remain allowed"
21607 );
21608
21609 assert_dns_denied(
21611 IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)),
21612 "240.0.0.1 (reserved)",
21613 );
21614 assert_dns_denied(
21615 IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)),
21616 "224.0.0.1 (multicast)",
21617 );
21618 }
21619}
21620
21621#[cfg(test)]
21630mod dns_rebinding_pin_tests {
21631 use super::{issue_outbound_http_request, split_netloc, JavascriptHttpRequestOptions};
21632 use std::collections::BTreeMap;
21633 use std::io::{Read, Write};
21634 use std::net::{IpAddr, Ipv4Addr, TcpListener};
21635 use std::thread;
21636 use url::Url;
21637
21638 fn empty_headers() -> super::HttpHeaderCollection {
21639 super::parse_http_header_collection(&BTreeMap::new(), "test headers")
21640 .expect("empty header collection")
21641 }
21642
21643 fn options() -> JavascriptHttpRequestOptions {
21644 JavascriptHttpRequestOptions {
21645 method: Some(String::from("GET")),
21646 headers: BTreeMap::new(),
21647 body: None,
21648 reject_unauthorized: None,
21649 }
21650 }
21651
21652 #[test]
21653 fn split_netloc_handles_hostnames_and_bracketed_ipv6() {
21654 assert_eq!(
21655 split_netloc("attacker.example:80"),
21656 Some(("attacker.example", 80))
21657 );
21658 assert_eq!(split_netloc("[::1]:443"), Some(("::1", 443)));
21659 assert_eq!(split_netloc("10.0.0.1:8080"), Some(("10.0.0.1", 8080)));
21660 assert_eq!(split_netloc("no-port"), None);
21661 assert_eq!(split_netloc("host:notaport"), None);
21662 }
21663
21664 #[test]
21670 fn outbound_http_connect_is_pinned_to_vetted_ip() {
21671 let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind loopback server");
21672 let port = listener.local_addr().expect("local addr").port();
21673 let server = thread::spawn(move || {
21674 let (mut stream, _) = listener.accept().expect("accept");
21675 let mut buf = [0u8; 1024];
21676 let _ = stream.read(&mut buf);
21677 stream
21678 .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi")
21679 .expect("write response");
21680 let _ = stream.flush();
21681 });
21682
21683 let url = Url::parse(&format!("http://attacker.example:{port}/")).expect("url");
21684 let pinned = vec![IpAddr::V4(Ipv4Addr::LOCALHOST)];
21685 let result = issue_outbound_http_request(&url, &options(), &empty_headers(), &pinned)
21686 .expect("pinned request should reach the vetted loopback target");
21687 let payload = result.as_str().expect("string payload");
21688 assert!(
21689 payload.contains("\"status\":200"),
21690 "expected 200 from pinned target, got: {payload}"
21691 );
21692 server.join().expect("server thread");
21693 }
21694
21695 #[test]
21699 fn outbound_http_refuses_when_no_vetted_address() {
21700 let url = Url::parse("https://attacker.example/").expect("url");
21701 let error = issue_outbound_http_request(&url, &options(), &empty_headers(), &[])
21702 .expect_err("empty pinned set must be refused");
21703 let message = error.to_string();
21704 assert!(
21705 message.contains("EACCES") || message.contains("ERR_HTTP_REQUEST_FAILED"),
21706 "expected an egress refusal, got: {message}"
21707 );
21708 }
21709}