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, GuestRuntimeConfig, JavascriptExecutionEvent,
101 JavascriptExecutionLimits, JavascriptSyncRpcRequest, ModuleFsReader,
102 NodeSignalDispositionAction, NodeSignalHandlerRegistration, PythonExecutionEvent,
103 PythonExecutionLimits, PythonVfsRpcMethod, PythonVfsRpcRequest, PythonVfsRpcResponsePayload,
104 StartJavascriptExecutionRequest, StartPythonExecutionRequest, StartWasmExecutionRequest,
105 WasmExecutionEvent, WasmExecutionLimits, WasmPermissionTier as ExecutionWasmPermissionTier,
106};
107use secure_exec_kernel::dns::{
108 DnsLookupPolicy, DnsRecordResolution, DnsResolutionSource as KernelDnsResolutionSource,
109};
110use secure_exec_kernel::kernel::{KernelProcessHandle, SpawnOptions, VirtualProcessOptions};
111use secure_exec_kernel::permissions::NetworkOperation;
112use secure_exec_kernel::poll::{PollEvents, PollFd, PollTargetEntry, POLLERR, POLLHUP, POLLIN};
113use secure_exec_kernel::process_table::{ProcessStatus, WaitPidFlags, SIGKILL, SIGTERM};
114use secure_exec_kernel::pty::LineDisciplineConfig;
115use secure_exec_kernel::resource_accounting::ResourceLimits;
116use secure_exec_kernel::root_fs::RootFilesystemMode;
117use secure_exec_kernel::socket_table::{
118 InetSocketAddress, SocketDomain, SocketId, SocketShutdown as KernelSocketShutdown, SocketSpec,
119 SocketState, SocketType,
120};
121use serde::{Deserialize, Serialize};
122use serde_json::{json, Map, Value};
123use sha1::Sha1;
124use sha2::{digest::Digest, Sha256, Sha512};
125use socket2::{SockRef, TcpKeepalive};
126use std::collections::VecDeque;
127use std::collections::{BTreeMap, BTreeSet};
128use std::fmt;
129use std::fs;
130use std::io::{Cursor, Read, Write};
131use std::net::{
132 IpAddr, Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr, TcpListener, TcpStream, ToSocketAddrs,
133 UdpSocket,
134};
135use std::os::unix::fs::{MetadataExt, PermissionsExt};
136use std::os::unix::net::{SocketAddr as UnixSocketAddr, UnixListener, UnixStream};
137use std::path::{Path, PathBuf};
138use std::pin::Pin;
139use std::sync::atomic::{AtomicBool, Ordering};
140use std::sync::mpsc::{self, RecvTimeoutError, Sender};
141use std::sync::{Arc, Mutex, OnceLock, Weak};
142use std::thread;
143use std::time::{Duration, Instant};
144use tokio::io::{AsyncRead, AsyncWrite};
145use tokio::runtime::Builder as TokioRuntimeBuilder;
146use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
147use tokio_rustls::{TlsAcceptor, TlsConnector};
148use url::Url;
149
150const DEFAULT_KERNEL_STDIN_READ_MAX_BYTES: usize = 64 * 1024;
151const DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS: u64 = 100;
152const JAVASCRIPT_NET_TIMEOUT_SENTINEL: &str = "__secure_exec_net_timeout__";
153const PYTHON_PYODIDE_GUEST_ROOT: &str = "/__agentos_pyodide";
154const PYTHON_PYODIDE_CACHE_GUEST_ROOT: &str = "/__agentos_pyodide_cache";
155const TCP_SOCKET_POLL_TIMEOUT: Duration = Duration::from_millis(100);
156const TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5);
157const HTTP_LOOPBACK_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
158pub(crate) const MAX_PER_PROCESS_STATE_HANDLES: usize = 1024;
159const VM_FETCH_BUFFER_LIMIT_BYTES: usize = DEFAULT_MAX_FRAME_BYTES;
160const DEFAULT_SCRYPT_COST: u64 = 16_384;
161const DEFAULT_SCRYPT_BLOCK_SIZE: u32 = 8;
162const DEFAULT_SCRYPT_PARALLELIZATION: u32 = 1;
163const SQLITE_JS_SAFE_INTEGER_MAX: i64 = 9_007_199_254_740_991;
164const HTTP_LOOPBACK_REQUEST_TIMEOUT_MS_ENV: &str =
165 "SECURE_EXEC_TEST_HTTP_LOOPBACK_REQUEST_TIMEOUT_MS";
166
167trait Http2AsyncIo: AsyncRead + AsyncWrite + Unpin + Send {}
168
169impl<T> Http2AsyncIo for T where T: AsyncRead + AsyncWrite + Unpin + Send {}
170
171fn http_loopback_request_timeout() -> Duration {
172 static TIMEOUT: OnceLock<Duration> = OnceLock::new();
173 *TIMEOUT.get_or_init(|| {
174 std::env::var(HTTP_LOOPBACK_REQUEST_TIMEOUT_MS_ENV)
175 .ok()
176 .and_then(|value| value.parse::<u64>().ok())
177 .map(Duration::from_millis)
178 .unwrap_or(HTTP_LOOPBACK_REQUEST_TIMEOUT)
179 })
180}
181
182const DEFAULT_ALLOWED_NODE_BUILTINS: &[&str] = &[
183 "assert",
184 "buffer",
185 "console",
186 "child_process",
187 "crypto",
188 "dns",
189 "events",
190 "fs",
191 "http",
192 "http2",
193 "https",
194 "module",
195 "os",
196 "path",
197 "perf_hooks",
198 "querystring",
199 "sqlite",
200 "stream",
201 "string_decoder",
202 "timers",
203 "tls",
204 "tty",
205 "url",
206 "util",
207 "zlib",
208];
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211enum JavascriptCryptoDigestAlgorithm {
212 Md5,
213 Sha1,
214 Sha256,
215 Sha512,
216}
217
218#[derive(Debug, Default, Deserialize)]
219#[serde(default, rename_all = "camelCase")]
220struct JavascriptScryptOptions {
221 #[serde(alias = "N")]
222 cost: Option<u64>,
223 #[serde(alias = "r")]
224 block_size: Option<u32>,
225 #[serde(alias = "p")]
226 parallelization: Option<u32>,
227}
228
229#[derive(Debug, Deserialize)]
230#[serde(rename_all = "camelCase")]
231struct JavascriptHttpListenRequest {
232 server_id: u64,
233 #[serde(default)]
234 port: Option<u16>,
235 #[serde(default)]
236 hostname: Option<String>,
237}
238
239#[derive(Debug, Default, Deserialize)]
240#[serde(default, rename_all = "camelCase")]
241struct JavascriptHttpRequestOptions {
242 method: Option<String>,
243 headers: BTreeMap<String, Value>,
244 body: Option<String>,
245 reject_unauthorized: Option<bool>,
246}
247
248#[derive(Debug, Default, Deserialize)]
249#[serde(default, rename_all = "camelCase")]
250struct JavascriptHttp2ServerListenRequest {
251 server_id: u64,
252 secure: bool,
253 port: Option<u16>,
254 host: Option<String>,
255 backlog: Option<u32>,
256 timeout: Option<u64>,
257 settings: BTreeMap<String, Value>,
258 tls: Option<JavascriptTlsBridgeOptions>,
259}
260
261#[derive(Debug, Default, Deserialize)]
262#[serde(default, rename_all = "camelCase")]
263struct JavascriptHttp2SessionConnectRequest {
264 authority: Option<String>,
265 protocol: Option<String>,
266 host: Option<String>,
267 port: Option<u16>,
268 settings: BTreeMap<String, Value>,
269 tls: Option<JavascriptTlsBridgeOptions>,
270}
271
272#[derive(Debug, Default, Deserialize)]
273#[serde(default, rename_all = "camelCase")]
274struct JavascriptHttp2RequestOptions {
275 end_stream: bool,
276}
277
278#[derive(Debug, Default, Deserialize)]
279#[serde(default, rename_all = "camelCase")]
280struct JavascriptHttp2FileResponseOptions {
281 offset: Option<u64>,
282 length: Option<i64>,
283}
284
285#[derive(Debug, Clone)]
286struct HttpHeaderCollection {
287 normalized: BTreeMap<String, Vec<String>>,
288 raw_pairs: Vec<(String, String)>,
289}
290
291#[derive(Debug)]
292struct InsecureTlsVerifier {
293 supported_schemes: Vec<SignatureScheme>,
294}
295
296impl ServerCertVerifier for InsecureTlsVerifier {
297 fn verify_server_cert(
298 &self,
299 _end_entity: &CertificateDer<'_>,
300 _intermediates: &[CertificateDer<'_>],
301 _server_name: &ServerName<'_>,
302 _ocsp_response: &[u8],
303 _now: rustls::pki_types::UnixTime,
304 ) -> Result<ServerCertVerified, rustls::Error> {
305 Ok(ServerCertVerified::assertion())
306 }
307
308 fn verify_tls12_signature(
309 &self,
310 _message: &[u8],
311 _cert: &CertificateDer<'_>,
312 _dss: &DigitallySignedStruct,
313 ) -> Result<HandshakeSignatureValid, rustls::Error> {
314 Ok(HandshakeSignatureValid::assertion())
315 }
316
317 fn verify_tls13_signature(
318 &self,
319 _message: &[u8],
320 _cert: &CertificateDer<'_>,
321 _dss: &DigitallySignedStruct,
322 ) -> Result<HandshakeSignatureValid, rustls::Error> {
323 Ok(HandshakeSignatureValid::assertion())
324 }
325
326 fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
327 self.supported_schemes.clone()
328 }
329}
330
331impl ActiveProcess {
332 pub(crate) fn new(
333 kernel_pid: u32,
334 kernel_handle: KernelProcessHandle,
335 runtime: GuestRuntimeKind,
336 execution: ActiveExecution,
337 ) -> Self {
338 Self {
339 kernel_pid,
340 kernel_handle,
341 kernel_stdin_writer_fd: None,
342 runtime,
343 detached: false,
344 execution,
345 guest_cwd: String::from("/"),
346 env: BTreeMap::new(),
347 host_cwd: PathBuf::from("/"),
348 mapped_host_fds: BTreeMap::new(),
349 next_mapped_host_fd: MAPPED_HOST_FD_START,
350 pending_execution_events: VecDeque::new(),
351 pending_self_signal_exit: None,
352 child_processes: BTreeMap::new(),
353 next_child_process_id: 0,
354 http_servers: BTreeMap::new(),
355 pending_http_requests: BTreeMap::new(),
356 http2: Default::default(),
357 tcp_listeners: BTreeMap::new(),
358 next_tcp_listener_id: 0,
359 tcp_sockets: BTreeMap::new(),
360 next_tcp_socket_id: 0,
361 tcp_port_reservations: BTreeMap::new(),
362 next_tcp_port_reservation_id: 0,
363 unix_listeners: BTreeMap::new(),
364 next_unix_listener_id: 0,
365 unix_sockets: BTreeMap::new(),
366 next_unix_socket_id: 0,
367 udp_sockets: BTreeMap::new(),
368 next_udp_socket_id: 0,
369 cipher_sessions: BTreeMap::new(),
370 next_cipher_session_id: 0,
371 diffie_hellman_sessions: BTreeMap::new(),
372 next_diffie_hellman_session_id: 0,
373 sqlite_databases: BTreeMap::new(),
374 next_sqlite_database_id: 0,
375 sqlite_statements: BTreeMap::new(),
376 next_sqlite_statement_id: 0,
377 module_resolution_cache: secure_exec_execution::LocalModuleResolutionCache::default(),
378 }
379 }
380
381 pub(crate) fn queue_pending_execution_event(
382 &mut self,
383 event: ActiveExecutionEvent,
384 ) -> Result<(), SidecarError> {
385 if self.pending_execution_events.len() >= MAX_PROCESS_EVENT_QUEUE {
386 return Err(process_event_queue_overflow_error());
387 }
388 self.pending_execution_events.push_back(event);
389 Ok(())
390 }
391
392 pub(crate) fn with_host_cwd(mut self, host_cwd: PathBuf) -> Self {
393 self.host_cwd = host_cwd;
394 self
395 }
396
397 pub(crate) fn with_guest_cwd(mut self, guest_cwd: String) -> Self {
398 self.guest_cwd = guest_cwd;
399 self
400 }
401
402 pub(crate) fn with_env(mut self, env: BTreeMap<String, String>) -> Self {
403 self.env = env;
404 self
405 }
406
407 pub(crate) fn with_kernel_stdin_writer_fd(mut self, fd: u32) -> Self {
408 self.kernel_stdin_writer_fd = Some(fd);
409 self
410 }
411
412 pub(crate) fn with_detached(mut self, detached: bool) -> Self {
413 self.detached = detached;
414 self
415 }
416
417 pub(crate) fn allocate_mapped_host_fd(&mut self, fd: ActiveMappedHostFd) -> u32 {
418 let handle = self.next_mapped_host_fd;
419 self.next_mapped_host_fd = self
420 .next_mapped_host_fd
421 .checked_add(1)
422 .unwrap_or(MAPPED_HOST_FD_START);
423 self.mapped_host_fds.insert(handle, fd);
424 handle
425 }
426
427 pub(crate) fn mapped_host_fd(&self, fd: u32) -> Option<&ActiveMappedHostFd> {
428 self.mapped_host_fds.get(&fd)
429 }
430
431 pub(crate) fn mapped_host_fd_mut(&mut self, fd: u32) -> Option<&mut ActiveMappedHostFd> {
432 self.mapped_host_fds.get_mut(&fd)
433 }
434
435 pub(crate) fn close_mapped_host_fd(&mut self, fd: u32) -> bool {
436 self.mapped_host_fds.remove(&fd).is_some()
437 }
438
439 pub(crate) fn allocate_child_process_id(&mut self) -> String {
440 self.next_child_process_id += 1;
441 format!("child-{}", self.next_child_process_id)
442 }
443
444 fn allocate_tcp_listener_id(&mut self) -> String {
445 self.next_tcp_listener_id += 1;
446 format!("listener-{}", self.next_tcp_listener_id)
447 }
448
449 fn allocate_tcp_socket_id(&mut self) -> String {
450 self.next_tcp_socket_id += 1;
451 format!("socket-{}", self.next_tcp_socket_id)
452 }
453
454 fn allocate_tcp_port_reservation_id(&mut self) -> String {
455 self.next_tcp_port_reservation_id += 1;
456 format!("tcp-port-reservation-{}", self.next_tcp_port_reservation_id)
457 }
458
459 fn allocate_unix_listener_id(&mut self) -> String {
460 self.next_unix_listener_id += 1;
461 format!("unix-listener-{}", self.next_unix_listener_id)
462 }
463
464 fn allocate_unix_socket_id(&mut self) -> String {
465 self.next_unix_socket_id += 1;
466 format!("unix-socket-{}", self.next_unix_socket_id)
467 }
468
469 fn allocate_udp_socket_id(&mut self) -> String {
470 self.next_udp_socket_id += 1;
471 format!("udp-socket-{}", self.next_udp_socket_id)
472 }
473
474 pub(crate) fn network_resource_counts(&self) -> NetworkResourceCounts {
475 let mut counts = NetworkResourceCounts {
476 sockets: self.http_servers.len()
477 + self.tcp_listeners.len()
478 + self.tcp_sockets.len()
479 + self.unix_listeners.len()
480 + self.unix_sockets.len()
481 + self.udp_sockets.len(),
482 connections: self.tcp_sockets.len() + self.unix_sockets.len(),
483 };
484 if let Ok(http2) = self.http2.shared.lock() {
485 counts.sockets += http2.servers.len() + http2.sessions.len();
486 counts.connections += http2.sessions.len();
487 }
488
489 for child in self.child_processes.values() {
490 let child_counts = child.network_resource_counts();
491 counts.sockets += child_counts.sockets;
492 counts.connections += child_counts.connections;
493 }
494
495 counts
496 }
497
498 fn sidecar_only_network_resource_counts(&self) -> NetworkResourceCounts {
499 let mut counts = NetworkResourceCounts {
500 sockets: self.http_servers.len()
501 + self
502 .tcp_listeners
503 .values()
504 .filter(|listener| listener.kernel_socket_id.is_none())
505 .count()
506 + self
507 .tcp_sockets
508 .values()
509 .filter(|socket| socket.kernel_socket_id.is_none())
510 .count()
511 + self.unix_listeners.len()
512 + self.unix_sockets.len()
513 + self
514 .udp_sockets
515 .values()
516 .filter(|socket| socket.kernel_socket_id.is_none())
517 .count(),
518 connections: self
519 .tcp_sockets
520 .values()
521 .filter(|socket| socket.kernel_socket_id.is_none())
522 .count()
523 + self.unix_sockets.len(),
524 };
525 if let Ok(http2) = self.http2.shared.lock() {
526 counts.sockets += http2.servers.len() + http2.sessions.len();
527 counts.connections += http2.sessions.len();
528 }
529
530 for child in self.child_processes.values() {
531 let child_counts = child.sidecar_only_network_resource_counts();
532 counts.sockets += child_counts.sockets;
533 counts.connections += child_counts.connections;
534 }
535
536 counts
537 }
538}
539
540fn poll_tool_process_event(
541 execution: &ToolExecution,
542) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
543 let event = execution
544 .pending_events
545 .lock()
546 .unwrap_or_else(|poisoned| poisoned.into_inner())
547 .pop_front();
548 if event.is_some() {
549 return Ok(event);
550 }
551 if execution.events_overflowed.load(Ordering::Relaxed) {
552 return Err(process_event_queue_overflow_error());
553 }
554 Ok(None)
555}
556
557fn descendant_pending_execution_event_capacity(
558 root: &ActiveProcess,
559 child_path: &[&str],
560) -> Option<usize> {
561 let mut child = root;
562 for child_process_id in child_path {
563 child = child.child_processes.get(*child_process_id)?;
564 }
565 Some(MAX_PROCESS_EVENT_QUEUE.saturating_sub(child.pending_execution_events.len()))
566}
567
568fn poll_child_execution_after_exit(
569 child: &mut ActiveProcess,
570 wait: Duration,
571) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
572 match child.execution.poll_event_blocking(wait) {
573 Ok(event) => Ok(event),
574 Err(SidecarError::Execution(message))
575 if child.runtime == GuestRuntimeKind::WebAssembly
576 && message == WasmExecutionError::EventChannelClosed.to_string() =>
577 {
578 Ok(None)
579 }
580 Err(error) => Err(error),
581 }
582}
583
584fn closed_javascript_event_channel(message: &str) -> bool {
585 message == "guest JavaScript event channel closed unexpectedly"
586}
587
588fn closed_python_event_channel(message: &str) -> bool {
589 message == "guest Python event channel closed unexpectedly"
590}
591
592fn closed_wasm_event_channel(message: &str) -> bool {
593 message == WasmExecutionError::EventChannelClosed.to_string()
594}
595
596fn missing_vm_error(vm_id: &str) -> SidecarError {
597 SidecarError::InvalidState(format!("VM {vm_id} is no longer active"))
598}
599
600fn missing_process_error(vm_id: &str, process_id: &str) -> SidecarError {
601 SidecarError::InvalidState(format!(
602 "VM {vm_id} no longer has active process {process_id}"
603 ))
604}
605
606fn is_broken_pipe_error(error: &SidecarError) -> bool {
607 matches!(error, SidecarError::Execution(message) if message.contains("Broken pipe") || message.contains("os error 32") || message.contains("EPIPE"))
608}
609
610fn javascript_child_process_gone_error(process_id: &str, child_path: &[&str]) -> SidecarError {
611 let child_label = if child_path.is_empty() {
612 process_id.to_owned()
613 } else {
614 format!("{process_id}/{}", child_path.join("/"))
615 };
616 SidecarError::Execution(format!(
617 "ECHILD: child_process {child_label} is no longer available"
618 ))
619}
620
621fn is_javascript_child_process_gone_error(error: &SidecarError) -> bool {
622 matches!(
623 error,
624 SidecarError::Execution(message) if guest_errno_code(message) == Some("ECHILD")
625 )
626}
627
628fn loopback_tls_transport_registry(
629) -> &'static Mutex<BTreeMap<String, Weak<crate::state::LoopbackTlsTransportPair>>> {
630 static REGISTRY: OnceLock<
631 Mutex<BTreeMap<String, Weak<crate::state::LoopbackTlsTransportPair>>>,
632 > = OnceLock::new();
633 REGISTRY.get_or_init(|| Mutex::new(BTreeMap::new()))
634}
635
636fn loopback_tls_transport_key(
637 vm_id: &str,
638 socket_id: SocketId,
639 peer_socket_id: SocketId,
640) -> String {
641 let (lower, higher) = if socket_id <= peer_socket_id {
642 (socket_id, peer_socket_id)
643 } else {
644 (peer_socket_id, socket_id)
645 };
646 format!("{vm_id}:{lower}:{higher}")
647}
648
649fn loopback_tls_endpoint(
650 vm_id: &str,
651 socket_id: SocketId,
652 peer_socket_id: SocketId,
653) -> Result<crate::state::LoopbackTlsEndpoint, SidecarError> {
654 let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
655 let registry = loopback_tls_transport_registry();
656 let mut transports = registry.lock().map_err(|_| {
657 SidecarError::InvalidState(String::from(
658 "loopback TLS transport registry lock poisoned",
659 ))
660 })?;
661 transports.retain(|_, pair| pair.strong_count() > 0);
662 let pair = transports
663 .get(&key)
664 .and_then(Weak::upgrade)
665 .unwrap_or_else(|| {
666 let pair = Arc::new(crate::state::LoopbackTlsTransportPair {
667 state: Mutex::new(crate::state::LoopbackTlsTransportPairState::default()),
668 ready: std::sync::Condvar::new(),
669 });
670 transports.insert(key, Arc::downgrade(&pair));
671 pair
672 });
673 Ok(crate::state::LoopbackTlsEndpoint {
674 pair,
675 is_lower_socket: socket_id <= peer_socket_id,
676 })
677}
678
679impl crate::state::LoopbackTlsEndpoint {
680 fn shutdown_write(&self) -> Result<(), SidecarError> {
681 let mut state = self.pair.state.lock().map_err(|_| {
682 SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
683 })?;
684 if self.is_lower_socket {
685 state.lower_write_closed = true;
686 } else {
687 state.higher_write_closed = true;
688 }
689 self.pair.ready.notify_all();
690 Ok(())
691 }
692
693 fn close_endpoint(&self) -> Result<(), SidecarError> {
694 let mut state = self.pair.state.lock().map_err(|_| {
695 SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
696 })?;
697 if self.is_lower_socket {
698 state.lower_write_closed = true;
699 state.lower_closed = true;
700 } else {
701 state.higher_write_closed = true;
702 state.higher_closed = true;
703 }
704 self.pair.ready.notify_all();
705 Ok(())
706 }
707}
708
709fn parse_tls_client_hello_from_bytes(
710 buffer: &[u8],
711) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
712 if buffer.is_empty() {
713 return Ok(None);
714 }
715
716 let mut acceptor = rustls::server::Acceptor::default();
717 let mut cursor = Cursor::new(buffer);
718 acceptor.read_tls(&mut cursor).map_err(sidecar_net_error)?;
719 let Some(accepted) = acceptor.accept().map_err(|(error, _)| {
720 SidecarError::Execution(format!("failed to parse TLS client hello: {error}"))
721 })?
722 else {
723 return Ok(None);
724 };
725 let client_hello = accepted.client_hello();
726 let alpn_protocols = client_hello.alpn().map(|protocols| {
727 protocols
728 .filter_map(|protocol| String::from_utf8(protocol.to_vec()).ok())
729 .collect::<Vec<_>>()
730 });
731 Ok(Some(JavascriptTlsClientHello {
732 servername: client_hello.server_name().map(str::to_owned),
733 alpn_protocols,
734 }))
735}
736
737fn peek_loopback_tls_client_hello(
738 vm_id: &str,
739 socket_id: SocketId,
740 peer_socket_id: SocketId,
741) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
742 let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
743 let registry = loopback_tls_transport_registry();
744 let pair = registry
745 .lock()
746 .map_err(|_| {
747 SidecarError::InvalidState(String::from(
748 "loopback TLS transport registry lock poisoned",
749 ))
750 })?
751 .get(&key)
752 .and_then(Weak::upgrade);
753 let Some(pair) = pair else {
754 return Ok(None);
755 };
756 let is_lower_socket = socket_id <= peer_socket_id;
757 let state = pair.state.lock().map_err(|_| {
758 SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
759 })?;
760 let buffered = if is_lower_socket {
761 state.higher_to_lower.iter().copied().collect::<Vec<_>>()
762 } else {
763 state.lower_to_higher.iter().copied().collect::<Vec<_>>()
764 };
765 drop(state);
766 parse_tls_client_hello_from_bytes(&buffered)
767}
768
769fn wait_for_loopback_peer_socket_id(
770 kernel: &SidecarKernel,
771 socket_id: SocketId,
772) -> Option<SocketId> {
773 for _ in 0..50 {
774 if let Some(peer_socket_id) = kernel
775 .socket_get(socket_id)
776 .and_then(|record| record.peer_socket_id())
777 {
778 return Some(peer_socket_id);
779 }
780 std::thread::sleep(Duration::from_millis(10));
781 }
782 None
783}
784
785impl Drop for crate::state::LoopbackTlsEndpoint {
786 fn drop(&mut self) {
787 let _ = self.close_endpoint();
788 }
789}
790
791impl Read for crate::state::LoopbackTlsEndpoint {
792 fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
793 let mut state = self
794 .pair
795 .state
796 .lock()
797 .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
798
799 loop {
800 let (peer_write_closed, peer_closed) = if self.is_lower_socket {
801 (state.higher_write_closed, state.higher_closed)
802 } else {
803 (state.lower_write_closed, state.lower_closed)
804 };
805
806 let incoming = if self.is_lower_socket {
807 &mut state.higher_to_lower
808 } else {
809 &mut state.lower_to_higher
810 };
811
812 if !incoming.is_empty() {
813 let mut count = 0;
814 while count < buffer.len() {
815 let Some(byte) = incoming.pop_front() else {
816 break;
817 };
818 buffer[count] = byte;
819 count += 1;
820 }
821 return Ok(count);
822 }
823
824 if peer_write_closed || peer_closed {
825 return Ok(0);
826 }
827
828 let (next_state, wait_result) = self
829 .pair
830 .ready
831 .wait_timeout(state, TCP_SOCKET_POLL_TIMEOUT)
832 .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
833 state = next_state;
834 if wait_result.timed_out() {
835 return Err(std::io::Error::new(
836 std::io::ErrorKind::WouldBlock,
837 "loopback TLS transport read timed out",
838 ));
839 }
840 }
841 }
842}
843
844impl Write for crate::state::LoopbackTlsEndpoint {
845 fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
846 let mut state = self
847 .pair
848 .state
849 .lock()
850 .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
851
852 let peer_closed = if self.is_lower_socket {
853 state.higher_closed
854 } else {
855 state.lower_closed
856 };
857 let outgoing = if self.is_lower_socket {
858 &mut state.lower_to_higher
859 } else {
860 &mut state.higher_to_lower
861 };
862 if peer_closed {
863 return Err(std::io::Error::new(
864 std::io::ErrorKind::BrokenPipe,
865 "loopback TLS peer is closed",
866 ));
867 }
868
869 outgoing.extend(buffer.iter().copied());
870 self.pair.ready.notify_all();
871 Ok(buffer.len())
872 }
873
874 fn flush(&mut self) -> std::io::Result<()> {
875 Ok(())
876 }
877}
878
879struct ActiveTcpConnectRequest<'a, B> {
882 bridge: &'a SharedBridge<B>,
883 kernel: &'a mut SidecarKernel,
884 kernel_pid: u32,
885 vm_id: &'a str,
886 dns: &'a VmDnsConfig,
887 host: &'a str,
888 port: u16,
889 local_address: Option<&'a str>,
890 local_port: Option<u16>,
891 local_reservation: Option<(JavascriptSocketFamily, u16)>,
892 context: &'a JavascriptSocketPathContext,
893}
894
895struct ActiveUdpSendToRequest<'a, B> {
896 bridge: &'a SharedBridge<B>,
897 kernel: &'a mut SidecarKernel,
898 kernel_pid: u32,
899 vm_id: &'a str,
900 dns: &'a VmDnsConfig,
901 host: &'a str,
902 port: u16,
903 context: &'a JavascriptSocketPathContext,
904 contents: &'a [u8],
905}
906
907struct UdpRemoteAddrRequest<'a, B> {
908 bridge: &'a SharedBridge<B>,
909 kernel: &'a SidecarKernel,
910 vm_id: &'a str,
911 dns: &'a VmDnsConfig,
912 host: &'a str,
913 port: u16,
914 family: JavascriptUdpFamily,
915 context: &'a JavascriptSocketPathContext,
916}
917
918pub(crate) struct JavascriptSyncRpcServiceRequest<'a, B> {
919 pub(crate) bridge: &'a SharedBridge<B>,
920 pub(crate) vm_id: &'a str,
921 pub(crate) dns: &'a VmDnsConfig,
922 pub(crate) socket_paths: &'a JavascriptSocketPathContext,
923 pub(crate) kernel: &'a mut SidecarKernel,
924 pub(crate) process: &'a mut ActiveProcess,
925 pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
926 pub(crate) resource_limits: &'a ResourceLimits,
927 pub(crate) network_counts: NetworkResourceCounts,
928}
929
930pub(crate) struct JavascriptNetSyncRpcServiceRequest<'a, B> {
931 pub(crate) bridge: &'a SharedBridge<B>,
932 pub(crate) vm_id: &'a str,
933 pub(crate) dns: &'a VmDnsConfig,
934 pub(crate) socket_paths: &'a JavascriptSocketPathContext,
935 pub(crate) kernel: &'a mut SidecarKernel,
936 pub(crate) process: &'a mut ActiveProcess,
937 pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
938 pub(crate) resource_limits: &'a ResourceLimits,
939 pub(crate) network_counts: NetworkResourceCounts,
940}
941
942struct LoopbackHttpResponseWaitRequest<'a, B> {
943 bridge: &'a SharedBridge<B>,
944 vm_id: &'a str,
945 dns: &'a VmDnsConfig,
946 socket_paths: &'a JavascriptSocketPathContext,
947 kernel: &'a mut SidecarKernel,
948 process: &'a mut ActiveProcess,
949 resource_limits: &'a ResourceLimits,
950 request_key: (u64, u64),
951}
952
953pub(crate) struct LoopbackHttpDispatchRequest<'a, B> {
954 pub(crate) bridge: &'a SharedBridge<B>,
955 pub(crate) vm_id: &'a str,
956 pub(crate) dns: &'a VmDnsConfig,
957 pub(crate) socket_paths: &'a JavascriptSocketPathContext,
958 pub(crate) kernel: &'a mut SidecarKernel,
959 pub(crate) process: &'a mut ActiveProcess,
960 pub(crate) resource_limits: &'a ResourceLimits,
961 pub(crate) server_id: u64,
962 pub(crate) request_json: &'a str,
963}
964
965struct JavascriptDgramSyncRpcServiceRequest<'a, B> {
966 bridge: &'a SharedBridge<B>,
967 kernel: &'a mut SidecarKernel,
968 vm_id: &'a str,
969 dns: &'a VmDnsConfig,
970 socket_paths: &'a JavascriptSocketPathContext,
971 process: &'a mut ActiveProcess,
972 sync_request: &'a JavascriptSyncRpcRequest,
973 resource_limits: &'a ResourceLimits,
974 network_counts: NetworkResourceCounts,
975}
976
977struct JavascriptHttp2SyncRpcServiceRequest<'a, B> {
978 bridge: &'a SharedBridge<B>,
979 kernel: &'a mut SidecarKernel,
980 vm_id: &'a str,
981 dns: &'a VmDnsConfig,
982 socket_paths: &'a JavascriptSocketPathContext,
983 process: &'a mut ActiveProcess,
984 sync_request: &'a JavascriptSyncRpcRequest,
985 resource_limits: &'a ResourceLimits,
986 network_counts: NetworkResourceCounts,
987}
988
989impl ActiveTcpSocket {
990 fn connect<B>(request: ActiveTcpConnectRequest<'_, B>) -> Result<Self, SidecarError>
991 where
992 B: NativeSidecarBridge + Send + 'static,
993 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
994 {
995 let ActiveTcpConnectRequest {
996 bridge,
997 kernel,
998 kernel_pid,
999 vm_id,
1000 dns,
1001 host,
1002 port,
1003 local_address,
1004 local_port,
1005 local_reservation,
1006 context,
1007 } = request;
1008 let resolved = resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, context)?;
1009 if resolved.use_kernel_loopback {
1010 let family = JavascriptSocketFamily::from_ip(resolved.guest_remote_addr.ip());
1011 let requested_local_port = local_port.unwrap_or(0);
1012 let local_port = if requested_local_port != 0
1013 && local_reservation == Some((family, requested_local_port))
1014 {
1015 requested_local_port
1016 } else {
1017 allocate_guest_listen_port(
1018 requested_local_port,
1019 family,
1020 &context.used_tcp_guest_ports,
1021 context.listen_policy,
1022 )?
1023 };
1024 let local_ip = match (family, local_address) {
1025 (JavascriptSocketFamily::Ipv4, Some("0.0.0.0")) => {
1026 IpAddr::V4(Ipv4Addr::UNSPECIFIED)
1027 }
1028 (JavascriptSocketFamily::Ipv4, Some("127.0.0.1") | Some("localhost") | None) => {
1029 IpAddr::V4(Ipv4Addr::LOCALHOST)
1030 }
1031 (JavascriptSocketFamily::Ipv6, Some("::")) => IpAddr::V6(Ipv6Addr::UNSPECIFIED),
1032 (JavascriptSocketFamily::Ipv6, Some("::1") | Some("localhost") | None) => {
1033 IpAddr::V6(Ipv6Addr::LOCALHOST)
1034 }
1035 (JavascriptSocketFamily::Ipv4, Some(other)) => {
1036 return Err(SidecarError::Execution(format!(
1037 "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1038 )));
1039 }
1040 (JavascriptSocketFamily::Ipv6, Some(other)) => {
1041 return Err(SidecarError::Execution(format!(
1042 "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1043 )));
1044 }
1045 };
1046 let local_addr = SocketAddr::new(local_ip, local_port);
1047 let spec = match family {
1048 JavascriptSocketFamily::Ipv4 => SocketSpec::tcp(),
1049 JavascriptSocketFamily::Ipv6 => {
1050 SocketSpec::new(SocketDomain::Inet6, SocketType::Stream)
1051 }
1052 };
1053 let socket_id = kernel
1054 .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
1055 .map_err(kernel_error)?;
1056 kernel
1057 .socket_bind_inet(
1058 EXECUTION_DRIVER_NAME,
1059 kernel_pid,
1060 socket_id,
1061 InetSocketAddress::new(local_ip.to_string(), local_port),
1062 )
1063 .map_err(kernel_error)?;
1064 kernel
1065 .socket_connect_inet_loopback(
1066 EXECUTION_DRIVER_NAME,
1067 kernel_pid,
1068 socket_id,
1069 InetSocketAddress::new(
1070 resolved.guest_remote_addr.ip().to_string(),
1071 resolved.guest_remote_addr.port(),
1072 ),
1073 )
1074 .map_err(kernel_error)?;
1075 return Ok(Self::from_kernel(
1076 socket_id,
1077 None,
1078 local_addr,
1079 resolved.guest_remote_addr,
1080 ));
1081 }
1082
1083 let stream = TcpStream::connect_timeout(&resolved.actual_addr, Duration::from_secs(30))
1084 .map_err(sidecar_net_error)?;
1085 let guest_local_addr = stream.local_addr().map_err(sidecar_net_error)?;
1086 Self::from_stream(stream, None, guest_local_addr, resolved.guest_remote_addr)
1087 }
1088
1089 fn from_stream(
1090 stream: TcpStream,
1091 listener_id: Option<String>,
1092 guest_local_addr: SocketAddr,
1093 guest_remote_addr: SocketAddr,
1094 ) -> Result<Self, SidecarError> {
1095 let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
1096 read_stream
1097 .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
1098 .map_err(sidecar_net_error)?;
1099 let stream = Arc::new(Mutex::new(stream));
1100 let pending_read_stream = Arc::new(Mutex::new(Some(read_stream)));
1101 let (sender, events) = mpsc::channel();
1102 let tls_mode = Arc::new(AtomicBool::new(false));
1103 let tls_stream = Arc::new(Mutex::new(None));
1104 let tls_state = Arc::new(Mutex::new(None));
1105 let saw_local_shutdown = Arc::new(AtomicBool::new(false));
1106 let saw_remote_end = Arc::new(AtomicBool::new(false));
1107 let close_notified = Arc::new(AtomicBool::new(false));
1108
1109 Ok(Self {
1110 stream: Some(stream),
1111 pending_read_stream: Some(pending_read_stream),
1112 events: Some(events),
1113 event_sender: Some(sender),
1114 kernel_socket_id: None,
1115 no_delay: false,
1116 keep_alive: false,
1117 keep_alive_initial_delay_secs: None,
1118 guest_local_addr,
1119 guest_remote_addr,
1120 listener_id,
1121 tls_mode,
1122 tls_stream,
1123 tls_state,
1124 saw_local_shutdown,
1125 saw_remote_end,
1126 close_notified,
1127 })
1128 }
1129
1130 fn from_kernel(
1131 socket_id: SocketId,
1132 listener_id: Option<String>,
1133 guest_local_addr: SocketAddr,
1134 guest_remote_addr: SocketAddr,
1135 ) -> Self {
1136 let (sender, events) = mpsc::channel();
1137 Self {
1138 stream: None,
1139 pending_read_stream: None,
1140 events: Some(events),
1141 event_sender: Some(sender),
1142 kernel_socket_id: Some(socket_id),
1143 no_delay: false,
1144 keep_alive: false,
1145 keep_alive_initial_delay_secs: None,
1146 guest_local_addr,
1147 guest_remote_addr,
1148 listener_id,
1149 tls_mode: Arc::new(AtomicBool::new(false)),
1150 tls_stream: Arc::new(Mutex::new(None)),
1151 tls_state: Arc::new(Mutex::new(None)),
1152 saw_local_shutdown: Arc::new(AtomicBool::new(false)),
1153 saw_remote_end: Arc::new(AtomicBool::new(false)),
1154 close_notified: Arc::new(AtomicBool::new(false)),
1155 }
1156 }
1157
1158 fn poll(
1159 &mut self,
1160 kernel: &mut SidecarKernel,
1161 kernel_pid: u32,
1162 wait: Duration,
1163 ) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
1164 if self.tls_mode.load(Ordering::SeqCst) {
1165 self.ensure_tcp_reader()?;
1166 return match self
1167 .events
1168 .as_ref()
1169 .ok_or_else(|| {
1170 SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1171 })?
1172 .recv_timeout(wait)
1173 {
1174 Ok(event) => Ok(Some(event)),
1175 Err(RecvTimeoutError::Timeout) => Ok(None),
1176 Err(RecvTimeoutError::Disconnected) => Ok(None),
1177 };
1178 }
1179
1180 if let Some(socket_id) = self.kernel_socket_id {
1181 let result = kernel
1182 .poll_targets(
1183 EXECUTION_DRIVER_NAME,
1184 kernel_pid,
1185 vec![PollTargetEntry::socket(
1186 socket_id,
1187 POLLIN | POLLHUP | POLLERR,
1188 )],
1189 i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
1190 )
1191 .map_err(kernel_error)?;
1192 let revents = result
1193 .targets
1194 .first()
1195 .map(|entry| entry.revents)
1196 .unwrap_or_else(PollEvents::empty);
1197 if revents.is_empty() {
1198 return Ok(None);
1199 }
1200 if revents.intersects(POLLIN) {
1201 return match kernel.socket_read(
1202 EXECUTION_DRIVER_NAME,
1203 kernel_pid,
1204 socket_id,
1205 64 * 1024,
1206 ) {
1207 Ok(Some(bytes)) if !bytes.is_empty() => {
1208 Ok(Some(JavascriptTcpSocketEvent::Data(bytes)))
1209 }
1210 Ok(Some(_)) => Ok(Some(JavascriptTcpSocketEvent::Data(Vec::new()))),
1211 Ok(None) => Ok(Some(JavascriptTcpSocketEvent::End)),
1212 Err(error) if error.code() == "EAGAIN" => Ok(None),
1213 Err(error) => Ok(Some(JavascriptTcpSocketEvent::Error {
1214 code: Some(error.code().to_string()),
1215 message: error.to_string(),
1216 })),
1217 };
1218 }
1219 if revents.intersects(POLLHUP) {
1220 return Ok(Some(JavascriptTcpSocketEvent::End));
1221 }
1222 if revents.intersects(POLLERR) {
1223 return Ok(Some(JavascriptTcpSocketEvent::Error {
1224 code: Some(String::from("EPIPE")),
1225 message: String::from("kernel TCP socket reported POLLERR"),
1226 }));
1227 }
1228 return Ok(None);
1229 }
1230
1231 self.ensure_tcp_reader()?;
1232 match self
1233 .events
1234 .as_ref()
1235 .ok_or_else(|| {
1236 SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1237 })?
1238 .recv_timeout(wait)
1239 {
1240 Ok(event) => Ok(Some(event)),
1241 Err(RecvTimeoutError::Timeout) => Ok(None),
1242 Err(RecvTimeoutError::Disconnected) => Ok(None),
1243 }
1244 }
1245
1246 fn ensure_tcp_reader(&self) -> Result<(), SidecarError> {
1247 if self.kernel_socket_id.is_some() {
1248 return Ok(());
1249 }
1250 if self.tls_mode.load(Ordering::SeqCst) {
1251 return Ok(());
1252 }
1253 let read_stream = self
1254 .pending_read_stream
1255 .as_ref()
1256 .ok_or_else(|| {
1257 SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1258 })?
1259 .lock()
1260 .map_err(|_| {
1261 SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1262 })?
1263 .take();
1264 if let Some(read_stream) = read_stream {
1265 spawn_tcp_socket_reader(
1266 read_stream,
1267 self.event_sender
1268 .as_ref()
1269 .ok_or_else(|| {
1270 SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1271 })?
1272 .clone(),
1273 Arc::clone(&self.tls_mode),
1274 Arc::clone(&self.saw_local_shutdown),
1275 Arc::clone(&self.saw_remote_end),
1276 Arc::clone(&self.close_notified),
1277 );
1278 }
1279 Ok(())
1280 }
1281
1282 fn socket_info(&self) -> Value {
1283 json!({
1284 "localAddress": self.guest_local_addr.ip().to_string(),
1285 "localPort": self.guest_local_addr.port(),
1286 "localFamily": socket_addr_family(&self.guest_local_addr),
1287 "remoteAddress": self.guest_remote_addr.ip().to_string(),
1288 "remotePort": self.guest_remote_addr.port(),
1289 "remoteFamily": socket_addr_family(&self.guest_remote_addr),
1290 })
1291 }
1292
1293 fn set_no_delay(&mut self, enable: bool) -> Result<(), SidecarError> {
1294 self.no_delay = enable;
1295 if self.kernel_socket_id.is_some() {
1296 return Ok(());
1297 }
1298 let stream = self
1299 .stream
1300 .as_ref()
1301 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1302 .lock()
1303 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1304 stream.set_nodelay(enable).map_err(sidecar_net_error)
1305 }
1306
1307 fn set_keep_alive(
1308 &mut self,
1309 enable: bool,
1310 initial_delay_secs: Option<u64>,
1311 ) -> Result<(), SidecarError> {
1312 self.keep_alive = enable;
1313 self.keep_alive_initial_delay_secs = initial_delay_secs;
1314 if self.kernel_socket_id.is_some() {
1315 return Ok(());
1316 }
1317 let stream = self
1318 .stream
1319 .as_ref()
1320 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1321 .lock()
1322 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1323 let socket = SockRef::from(&*stream);
1324 socket.set_keepalive(enable).map_err(sidecar_net_error)?;
1325 if enable {
1326 if let Some(delay_secs) = initial_delay_secs.filter(|delay_secs| *delay_secs > 0) {
1327 socket
1328 .set_tcp_keepalive(
1329 &TcpKeepalive::new().with_time(Duration::from_secs(delay_secs)),
1330 )
1331 .map_err(sidecar_net_error)?;
1332 }
1333 }
1334 Ok(())
1335 }
1336
1337 fn upgrade_tls(
1338 &self,
1339 vm_id: &str,
1340 kernel: &SidecarKernel,
1341 options: JavascriptTlsBridgeOptions,
1342 ) -> Result<(), SidecarError> {
1343 if self.tls_mode.load(Ordering::SeqCst) {
1344 return Ok(());
1345 }
1346
1347 let client_hello = if options.is_server {
1348 self.peek_tls_client_hello(vm_id, kernel)?
1349 } else {
1350 None
1351 };
1352
1353 let tls_stream = if let Some(socket_id) = self.kernel_socket_id {
1354 let peer_socket_id = wait_for_loopback_peer_socket_id(kernel, socket_id)
1355 .ok_or_else(|| {
1356 SidecarError::Execution(format!(
1357 "ERR_NOT_IMPLEMENTED: kernel-backed loopback socket {socket_id} has no peer for TLS upgrade"
1358 ))
1359 })?;
1360 let endpoint = loopback_tls_endpoint(vm_id, socket_id, peer_socket_id)?;
1361 if options.is_server {
1362 ActiveTlsStream::LoopbackServer(build_server_loopback_tls_stream(
1363 endpoint, &options,
1364 )?)
1365 } else {
1366 ActiveTlsStream::LoopbackClient(build_client_loopback_tls_stream(
1367 endpoint, &options,
1368 )?)
1369 }
1370 } else {
1371 self.pending_read_stream
1372 .as_ref()
1373 .ok_or_else(|| {
1374 SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1375 })?
1376 .lock()
1377 .map_err(|_| {
1378 SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1379 })?
1380 .take();
1381 let stream = self
1382 .stream
1383 .as_ref()
1384 .ok_or_else(|| {
1385 SidecarError::InvalidState(String::from("TCP socket stream missing"))
1386 })?
1387 .lock()
1388 .map_err(|_| {
1389 SidecarError::InvalidState(String::from("TCP socket lock poisoned"))
1390 })?;
1391 let cloned = stream.try_clone().map_err(sidecar_net_error)?;
1392 drop(stream);
1393
1394 if options.is_server {
1395 ActiveTlsStream::Server(build_server_tls_stream(cloned, &options)?)
1396 } else {
1397 ActiveTlsStream::Client(build_client_tls_stream(cloned, &options)?)
1398 }
1399 };
1400
1401 let tls_state = ActiveTlsState {
1402 client_hello,
1403 local_certificates: tls_local_certificates(&options)?,
1404 session_reused: false,
1405 };
1406
1407 self.tls_mode.store(true, Ordering::SeqCst);
1408 {
1409 let mut state = self
1410 .tls_state
1411 .lock()
1412 .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?;
1413 *state = Some(tls_state);
1414 }
1415 {
1416 let mut stream = self.tls_stream.lock().map_err(|_| {
1417 SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
1418 })?;
1419 *stream = Some(tls_stream);
1420 }
1421
1422 spawn_tls_socket_reader(
1423 Arc::clone(&self.tls_stream),
1424 self.event_sender
1425 .as_ref()
1426 .ok_or_else(|| {
1427 SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1428 })?
1429 .clone(),
1430 Arc::clone(&self.saw_local_shutdown),
1431 Arc::clone(&self.saw_remote_end),
1432 Arc::clone(&self.close_notified),
1433 );
1434 Ok(())
1435 }
1436
1437 fn peek_tls_client_hello(
1438 &self,
1439 vm_id: &str,
1440 kernel: &SidecarKernel,
1441 ) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
1442 if let Some(socket_id) = self.kernel_socket_id {
1443 let Some(peer_socket_id) = kernel
1444 .socket_get(socket_id)
1445 .and_then(|record| record.peer_socket_id())
1446 else {
1447 return Ok(None);
1448 };
1449 return peek_loopback_tls_client_hello(vm_id, socket_id, peer_socket_id);
1450 }
1451
1452 let stream = self
1453 .stream
1454 .as_ref()
1455 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1456 .lock()
1457 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1458 let mut buffer = vec![0_u8; 16 * 1024];
1459 let bytes = match stream.peek(&mut buffer) {
1460 Ok(0) => return Ok(None),
1461 Ok(bytes) => bytes,
1462 Err(error)
1463 if matches!(
1464 error.kind(),
1465 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
1466 ) =>
1467 {
1468 return Ok(None);
1469 }
1470 Err(error) => return Err(sidecar_net_error(error)),
1471 };
1472 parse_tls_client_hello_from_bytes(&buffer[..bytes])
1473 }
1474
1475 fn tls_client_hello_json(
1476 &self,
1477 vm_id: &str,
1478 kernel: &SidecarKernel,
1479 ) -> Result<Value, SidecarError> {
1480 if let Some(client_hello) = self
1481 .tls_state
1482 .lock()
1483 .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
1484 .as_ref()
1485 .and_then(|state| state.client_hello.clone())
1486 {
1487 return javascript_net_json_string(
1488 serde_json::to_value(client_hello).map_err(|error| {
1489 SidecarError::InvalidState(format!(
1490 "failed to serialize TLS client hello: {error}"
1491 ))
1492 })?,
1493 "net.socket_get_tls_client_hello",
1494 );
1495 }
1496
1497 javascript_net_json_string(
1498 serde_json::to_value(
1499 self.peek_tls_client_hello(vm_id, kernel)?
1500 .unwrap_or_default(),
1501 )
1502 .map_err(|error| {
1503 SidecarError::InvalidState(format!("failed to serialize TLS client hello: {error}"))
1504 })?,
1505 "net.socket_get_tls_client_hello",
1506 )
1507 }
1508
1509 fn tls_query(&self, query: &str, detailed: bool) -> Result<Value, SidecarError> {
1510 let state = self
1511 .tls_state
1512 .lock()
1513 .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
1514 .clone();
1515 let mut tls_stream = self
1516 .tls_stream
1517 .lock()
1518 .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?;
1519 let Some(stream) = tls_stream.as_mut() else {
1520 return javascript_net_json_string(
1521 tls_bridge_undefined_value(),
1522 "net.socket_tls_query",
1523 );
1524 };
1525
1526 let payload = match query {
1527 "getSession" => tls_bridge_undefined_value(),
1528 "isSessionReused" => Value::Bool(
1529 state
1530 .as_ref()
1531 .is_some_and(|tls_state| tls_state.session_reused),
1532 ),
1533 "getPeerCertificate" => {
1534 let certificate = stream
1535 .peer_certificates()
1536 .and_then(|certificates| certificates.first())
1537 .map(|certificate| {
1538 tls_certificate_bridge_value(certificate.as_ref(), detailed)
1539 });
1540 certificate.unwrap_or_else(tls_bridge_undefined_value)
1541 }
1542 "getCertificate" => state
1543 .as_ref()
1544 .and_then(|tls_state| tls_state.local_certificates.first())
1545 .map(|certificate| tls_certificate_bridge_value(certificate, detailed))
1546 .unwrap_or_else(tls_bridge_undefined_value),
1547 "getProtocol" => stream
1548 .protocol_version()
1549 .map(tls_protocol_name)
1550 .map(Value::String)
1551 .unwrap_or(Value::Null),
1552 "getCipher" => stream
1553 .negotiated_cipher_suite()
1554 .map(tls_cipher_bridge_value)
1555 .unwrap_or_else(tls_bridge_undefined_value),
1556 other => {
1557 return Err(SidecarError::InvalidState(format!(
1558 "unsupported TLS query {other}"
1559 )));
1560 }
1561 };
1562 javascript_net_json_string(payload, "net.socket_tls_query")
1563 }
1564
1565 fn write_all(
1566 &self,
1567 kernel: &mut SidecarKernel,
1568 kernel_pid: u32,
1569 contents: &[u8],
1570 ) -> Result<usize, SidecarError> {
1571 if self.tls_mode.load(Ordering::SeqCst) {
1572 let mut tls_stream = self.tls_stream.lock().map_err(|_| {
1573 SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
1574 })?;
1575 let stream = tls_stream.as_mut().ok_or_else(|| {
1576 SidecarError::InvalidState(String::from("TLS stream missing for upgraded socket"))
1577 })?;
1578 stream.write_all(contents)?;
1579 return Ok(contents.len());
1580 }
1581 if let Some(socket_id) = self.kernel_socket_id {
1582 return kernel
1583 .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, contents)
1584 .map_err(kernel_error);
1585 }
1586
1587 let mut stream = self
1588 .stream
1589 .as_ref()
1590 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1591 .lock()
1592 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1593 stream.write_all(contents).map_err(sidecar_net_error)?;
1594 Ok(contents.len())
1595 }
1596
1597 fn shutdown_write(
1598 &self,
1599 kernel: &mut SidecarKernel,
1600 kernel_pid: u32,
1601 ) -> Result<(), SidecarError> {
1602 if self.tls_mode.load(Ordering::SeqCst) {
1603 if let Some(stream) = self
1604 .tls_stream
1605 .lock()
1606 .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
1607 .as_mut()
1608 {
1609 let _ = stream.send_close_notify();
1610 let _ = stream.shutdown_write();
1611 }
1612 if self.kernel_socket_id.is_some() {
1613 self.saw_local_shutdown.store(true, Ordering::SeqCst);
1614 return Ok(());
1615 }
1616 }
1617 if let Some(socket_id) = self.kernel_socket_id {
1618 return kernel
1619 .socket_shutdown(
1620 EXECUTION_DRIVER_NAME,
1621 kernel_pid,
1622 socket_id,
1623 KernelSocketShutdown::Write,
1624 )
1625 .map_err(kernel_error);
1626 }
1627 let stream = self
1628 .stream
1629 .as_ref()
1630 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1631 .lock()
1632 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1633 self.saw_local_shutdown.store(true, Ordering::SeqCst);
1634 match stream.shutdown(Shutdown::Write) {
1635 Ok(()) => {}
1636 Err(error) if error.kind() == std::io::ErrorKind::NotConnected => {}
1637 Err(error) => return Err(sidecar_net_error(error)),
1638 }
1639 if self.saw_remote_end.load(Ordering::SeqCst)
1640 && !self.close_notified.swap(true, Ordering::SeqCst)
1641 {
1642 let _ = self
1643 .event_sender
1644 .as_ref()
1645 .ok_or_else(|| {
1646 SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1647 })?
1648 .send(JavascriptTcpSocketEvent::Close { had_error: false });
1649 }
1650 Ok(())
1651 }
1652
1653 fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
1654 if self.tls_mode.load(Ordering::SeqCst) {
1655 if let Some(stream) = self
1656 .tls_stream
1657 .lock()
1658 .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
1659 .as_mut()
1660 {
1661 let _ = stream.send_close_notify();
1662 let _ = stream.close();
1663 }
1664 if self.kernel_socket_id.is_some() {
1665 return Ok(());
1666 }
1667 }
1668 if let Some(socket_id) = self.kernel_socket_id {
1669 return kernel
1670 .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
1671 .map_err(kernel_error);
1672 }
1673 let stream = self
1674 .stream
1675 .as_ref()
1676 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1677 .lock()
1678 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1679 stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
1680 }
1681}
1682
1683impl ActiveTlsStream {
1684 fn write_all(&mut self, contents: &[u8]) -> Result<(), SidecarError> {
1685 match self {
1686 Self::Client(stream) => {
1687 stream.write_all(contents).map_err(sidecar_net_error)?;
1688 stream.flush().map_err(sidecar_net_error)
1689 }
1690 Self::Server(stream) => {
1691 stream.write_all(contents).map_err(sidecar_net_error)?;
1692 stream.flush().map_err(sidecar_net_error)
1693 }
1694 Self::LoopbackClient(stream) => {
1695 stream.write_all(contents).map_err(sidecar_net_error)?;
1696 stream.flush().map_err(sidecar_net_error)
1697 }
1698 Self::LoopbackServer(stream) => {
1699 stream.write_all(contents).map_err(sidecar_net_error)?;
1700 stream.flush().map_err(sidecar_net_error)
1701 }
1702 }
1703 }
1704
1705 fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
1706 match self {
1707 Self::Client(stream) => stream.read(buffer),
1708 Self::Server(stream) => stream.read(buffer),
1709 Self::LoopbackClient(stream) => stream.read(buffer),
1710 Self::LoopbackServer(stream) => stream.read(buffer),
1711 }
1712 }
1713
1714 fn send_close_notify(&mut self) -> Result<(), SidecarError> {
1715 match self {
1716 Self::Client(stream) => {
1717 stream.conn.send_close_notify();
1718 let _ = stream.conn.complete_io(&mut stream.sock);
1719 }
1720 Self::Server(stream) => {
1721 stream.conn.send_close_notify();
1722 let _ = stream.conn.complete_io(&mut stream.sock);
1723 }
1724 Self::LoopbackClient(stream) => {
1725 stream.conn.send_close_notify();
1726 let _ = stream.conn.complete_io(&mut stream.sock);
1727 }
1728 Self::LoopbackServer(stream) => {
1729 stream.conn.send_close_notify();
1730 let _ = stream.conn.complete_io(&mut stream.sock);
1731 }
1732 }
1733 Ok(())
1734 }
1735
1736 fn shutdown_write(&mut self) -> Result<(), SidecarError> {
1737 match self {
1738 Self::Client(stream) => stream
1739 .sock
1740 .shutdown(Shutdown::Write)
1741 .map_err(sidecar_net_error),
1742 Self::Server(stream) => stream
1743 .sock
1744 .shutdown(Shutdown::Write)
1745 .map_err(sidecar_net_error),
1746 Self::LoopbackClient(stream) => stream.sock.shutdown_write(),
1747 Self::LoopbackServer(stream) => stream.sock.shutdown_write(),
1748 }
1749 }
1750
1751 fn close(&mut self) -> Result<(), SidecarError> {
1752 match self {
1753 Self::Client(stream) => stream
1754 .sock
1755 .shutdown(Shutdown::Both)
1756 .map_err(sidecar_net_error),
1757 Self::Server(stream) => stream
1758 .sock
1759 .shutdown(Shutdown::Both)
1760 .map_err(sidecar_net_error),
1761 Self::LoopbackClient(stream) => stream.sock.close_endpoint(),
1762 Self::LoopbackServer(stream) => stream.sock.close_endpoint(),
1763 }
1764 }
1765
1766 fn peer_certificates(&self) -> Option<&[CertificateDer<'static>]> {
1767 match self {
1768 Self::Client(stream) => stream.conn.peer_certificates(),
1769 Self::Server(stream) => stream.conn.peer_certificates(),
1770 Self::LoopbackClient(stream) => stream.conn.peer_certificates(),
1771 Self::LoopbackServer(stream) => stream.conn.peer_certificates(),
1772 }
1773 }
1774
1775 fn negotiated_cipher_suite(&self) -> Option<rustls::SupportedCipherSuite> {
1776 match self {
1777 Self::Client(stream) => stream.conn.negotiated_cipher_suite(),
1778 Self::Server(stream) => stream.conn.negotiated_cipher_suite(),
1779 Self::LoopbackClient(stream) => stream.conn.negotiated_cipher_suite(),
1780 Self::LoopbackServer(stream) => stream.conn.negotiated_cipher_suite(),
1781 }
1782 }
1783
1784 fn protocol_version(&self) -> Option<rustls::ProtocolVersion> {
1785 match self {
1786 Self::Client(stream) => stream.conn.protocol_version(),
1787 Self::Server(stream) => stream.conn.protocol_version(),
1788 Self::LoopbackClient(stream) => stream.conn.protocol_version(),
1789 Self::LoopbackServer(stream) => stream.conn.protocol_version(),
1790 }
1791 }
1792}
1793
1794impl ActiveUnixSocket {
1799 fn connect(host_path: &Path, guest_path: &str) -> Result<Self, SidecarError> {
1800 let stream = UnixStream::connect(host_path).map_err(sidecar_net_error)?;
1801 Self::from_stream(stream, None, None, Some(guest_path.to_owned()))
1802 }
1803
1804 fn from_stream(
1805 stream: UnixStream,
1806 listener_id: Option<String>,
1807 local_path: Option<String>,
1808 remote_path: Option<String>,
1809 ) -> Result<Self, SidecarError> {
1810 let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
1811 let stream = Arc::new(Mutex::new(stream));
1812 let (sender, events) = mpsc::channel();
1813 let saw_local_shutdown = Arc::new(AtomicBool::new(false));
1814 let saw_remote_end = Arc::new(AtomicBool::new(false));
1815 let close_notified = Arc::new(AtomicBool::new(false));
1816 spawn_unix_socket_reader(
1817 read_stream,
1818 sender.clone(),
1819 Arc::clone(&saw_local_shutdown),
1820 Arc::clone(&saw_remote_end),
1821 Arc::clone(&close_notified),
1822 );
1823
1824 Ok(Self {
1825 stream,
1826 events,
1827 event_sender: sender,
1828 listener_id,
1829 local_path,
1830 remote_path,
1831 saw_local_shutdown,
1832 saw_remote_end,
1833 close_notified,
1834 })
1835 }
1836
1837 fn poll(&mut self, wait: Duration) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
1838 match self.events.recv_timeout(wait) {
1839 Ok(event) => Ok(Some(event)),
1840 Err(RecvTimeoutError::Timeout) => Ok(None),
1841 Err(RecvTimeoutError::Disconnected) => Ok(None),
1842 }
1843 }
1844
1845 fn socket_info(&self) -> Value {
1846 json!({
1847 "localPath": self.local_path.clone(),
1848 "remotePath": self.remote_path.clone(),
1849 })
1850 }
1851
1852 fn write_all(&self, contents: &[u8]) -> Result<usize, SidecarError> {
1853 let mut stream = self
1854 .stream
1855 .lock()
1856 .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1857 stream.write_all(contents).map_err(sidecar_net_error)?;
1858 Ok(contents.len())
1859 }
1860
1861 fn shutdown_write(&self) -> Result<(), SidecarError> {
1862 let stream = self
1863 .stream
1864 .lock()
1865 .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1866 self.saw_local_shutdown.store(true, Ordering::SeqCst);
1867 stream
1868 .shutdown(Shutdown::Write)
1869 .map_err(sidecar_net_error)?;
1870 if self.saw_remote_end.load(Ordering::SeqCst)
1871 && !self.close_notified.swap(true, Ordering::SeqCst)
1872 {
1873 let _ = self
1874 .event_sender
1875 .send(JavascriptTcpSocketEvent::Close { had_error: false });
1876 }
1877 Ok(())
1878 }
1879
1880 fn close(&self) -> Result<(), SidecarError> {
1881 let stream = self
1882 .stream
1883 .lock()
1884 .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1885 stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
1886 }
1887}
1888
1889impl ActiveUnixListener {
1892 fn bind(
1893 host_path: &Path,
1894 guest_path: &str,
1895 backlog: Option<u32>,
1896 ) -> Result<Self, SidecarError> {
1897 if let Some(parent) = host_path.parent() {
1898 fs::create_dir_all(parent).map_err(sidecar_net_error)?;
1899 }
1900 let listener = UnixListener::bind(host_path).map_err(sidecar_net_error)?;
1901 listener.set_nonblocking(true).map_err(sidecar_net_error)?;
1902 Ok(Self {
1903 listener,
1904 path: guest_path.to_owned(),
1905 backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
1906 .expect("default backlog fits within usize"),
1907 active_connection_ids: BTreeSet::new(),
1908 })
1909 }
1910
1911 fn path(&self) -> &str {
1912 &self.path
1913 }
1914
1915 fn poll(
1916 &mut self,
1917 wait: Duration,
1918 ) -> Result<Option<JavascriptUnixListenerEvent>, SidecarError> {
1919 let deadline = Instant::now() + wait;
1920 loop {
1921 match self.listener.accept() {
1922 Ok((stream, remote_addr)) => {
1923 if self.active_connection_ids.len() >= self.backlog {
1924 let _ = stream.shutdown(Shutdown::Both);
1925 if wait.is_zero() || Instant::now() >= deadline {
1926 return Ok(None);
1927 }
1928 continue;
1929 }
1930
1931 let local_path = Some(self.path.clone());
1932 let remote_path = unix_socket_path(&remote_addr);
1933 return Ok(Some(JavascriptUnixListenerEvent::Connection(
1934 PendingUnixSocket {
1935 stream,
1936 local_path,
1937 remote_path,
1938 },
1939 )));
1940 }
1941 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
1942 if wait.is_zero() || Instant::now() >= deadline {
1943 return Ok(None);
1944 }
1945 thread::sleep(Duration::from_millis(10));
1946 }
1947 Err(error) => {
1948 return Ok(Some(JavascriptUnixListenerEvent::Error {
1949 code: io_error_code(&error),
1950 message: error.to_string(),
1951 }));
1952 }
1953 }
1954 }
1955 }
1956
1957 fn close(&self) -> Result<(), SidecarError> {
1958 Ok(())
1959 }
1960
1961 fn active_connection_count(&self) -> usize {
1962 self.active_connection_ids.len()
1963 }
1964
1965 fn register_connection(&mut self, socket_id: &str) {
1966 self.active_connection_ids.insert(socket_id.to_string());
1967 }
1968
1969 fn release_connection(&mut self, socket_id: &str) {
1970 self.active_connection_ids.remove(socket_id);
1971 }
1972}
1973
1974impl ActiveTcpListener {
1975 fn bind(
1976 bind_host: &str,
1977 guest_host: &str,
1978 guest_port: u16,
1979 backlog: Option<u32>,
1980 ) -> Result<Self, SidecarError> {
1981 let bind_addr = resolve_tcp_bind_addr(bind_host, 0)?;
1982 let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
1983 let listener = TcpListener::bind(bind_addr).map_err(sidecar_net_error)?;
1984 listener.set_nonblocking(true).map_err(sidecar_net_error)?;
1985 let local_addr = listener.local_addr().map_err(sidecar_net_error)?;
1986 Ok(Self {
1987 listener: Some(listener),
1988 kernel_socket_id: None,
1989 local_addr: Some(local_addr),
1990 guest_local_addr: guest_addr,
1991 backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
1992 .expect("default backlog fits within usize"),
1993 active_connection_ids: BTreeSet::new(),
1994 })
1995 }
1996
1997 fn bind_kernel(
1998 kernel: &mut SidecarKernel,
1999 kernel_pid: u32,
2000 guest_host: &str,
2001 guest_port: u16,
2002 backlog: Option<u32>,
2003 ) -> Result<Self, SidecarError> {
2004 let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
2005 let spec = match guest_addr {
2006 SocketAddr::V4(_) => SocketSpec::tcp(),
2007 SocketAddr::V6(_) => SocketSpec::new(SocketDomain::Inet6, SocketType::Stream),
2008 };
2009 let socket_id = kernel
2010 .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
2011 .map_err(kernel_error)?;
2012 kernel
2013 .socket_bind_inet(
2014 EXECUTION_DRIVER_NAME,
2015 kernel_pid,
2016 socket_id,
2017 InetSocketAddress::new(guest_addr.ip().to_string(), guest_addr.port()),
2018 )
2019 .map_err(kernel_error)?;
2020 kernel
2021 .socket_listen(
2022 EXECUTION_DRIVER_NAME,
2023 kernel_pid,
2024 socket_id,
2025 usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2026 .expect("default backlog fits within usize"),
2027 )
2028 .map_err(kernel_error)?;
2029 Ok(Self {
2030 listener: None,
2031 kernel_socket_id: Some(socket_id),
2032 local_addr: Some(guest_addr),
2033 guest_local_addr: guest_addr,
2034 backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2035 .expect("default backlog fits within usize"),
2036 active_connection_ids: BTreeSet::new(),
2037 })
2038 }
2039
2040 pub(crate) fn local_addr(&self) -> SocketAddr {
2041 self.local_addr.unwrap_or(self.guest_local_addr)
2042 }
2043
2044 fn guest_local_addr(&self) -> SocketAddr {
2045 self.guest_local_addr
2046 }
2047
2048 fn poll(
2049 &mut self,
2050 kernel: &mut SidecarKernel,
2051 kernel_pid: u32,
2052 wait: Duration,
2053 ) -> Result<Option<JavascriptTcpListenerEvent>, SidecarError> {
2054 if let Some(socket_id) = self.kernel_socket_id {
2055 let result = kernel
2056 .poll_targets(
2057 EXECUTION_DRIVER_NAME,
2058 kernel_pid,
2059 vec![PollTargetEntry::socket(socket_id, POLLIN)],
2060 i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
2061 )
2062 .map_err(kernel_error)?;
2063 let revents = result
2064 .targets
2065 .first()
2066 .map(|entry| entry.revents)
2067 .unwrap_or_else(PollEvents::empty);
2068 if revents.is_empty() {
2069 return Ok(None);
2070 }
2071 let accepted_socket_id =
2072 match kernel.socket_accept(EXECUTION_DRIVER_NAME, kernel_pid, socket_id) {
2073 Ok(accepted_socket_id) => accepted_socket_id,
2074 Err(error) if error.code() == "EAGAIN" => return Ok(None),
2075 Err(error) => {
2076 return Ok(Some(JavascriptTcpListenerEvent::Error {
2077 code: Some(error.code().to_string()),
2078 message: error.to_string(),
2079 }));
2080 }
2081 };
2082 let accepted = kernel.socket_get(accepted_socket_id).ok_or_else(|| {
2083 SidecarError::InvalidState(format!(
2084 "accepted kernel TCP socket {accepted_socket_id} is missing"
2085 ))
2086 })?;
2087 let local_addr = accepted.local_address().ok_or_else(|| {
2088 SidecarError::InvalidState(format!(
2089 "accepted kernel TCP socket {accepted_socket_id} missing local address"
2090 ))
2091 })?;
2092 let remote_addr = accepted.peer_address().ok_or_else(|| {
2093 SidecarError::InvalidState(format!(
2094 "accepted kernel TCP socket {accepted_socket_id} missing peer address"
2095 ))
2096 })?;
2097 return Ok(Some(JavascriptTcpListenerEvent::Connection(
2098 PendingTcpSocket {
2099 stream: None,
2100 kernel_socket_id: Some(accepted_socket_id),
2101 preallocated: true,
2102 guest_local_addr: resolve_tcp_bind_addr(local_addr.host(), local_addr.port())?,
2103 guest_remote_addr: resolve_tcp_bind_addr(
2104 remote_addr.host(),
2105 remote_addr.port(),
2106 )?,
2107 },
2108 )));
2109 }
2110
2111 let deadline = Instant::now() + wait;
2112 loop {
2113 match self
2114 .listener
2115 .as_ref()
2116 .ok_or_else(|| {
2117 SidecarError::InvalidState(String::from("TCP listener socket missing"))
2118 })?
2119 .accept()
2120 {
2121 Ok((stream, remote_addr)) => {
2122 if self.active_connection_ids.len() >= self.backlog {
2123 let _ = stream.shutdown(Shutdown::Both);
2124 if wait.is_zero() || Instant::now() >= deadline {
2125 return Ok(None);
2126 }
2127 continue;
2128 }
2129 return Ok(Some(JavascriptTcpListenerEvent::Connection(
2130 PendingTcpSocket {
2131 stream: Some(stream),
2132 kernel_socket_id: None,
2133 preallocated: false,
2134 guest_local_addr: self.guest_local_addr,
2135 guest_remote_addr: SocketAddr::new(
2136 remote_addr.ip(),
2137 remote_addr.port(),
2138 ),
2139 },
2140 )));
2141 }
2142 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2143 if wait.is_zero() || Instant::now() >= deadline {
2144 return Ok(None);
2145 }
2146 thread::sleep(Duration::from_millis(10));
2147 }
2148 Err(error) => {
2149 return Ok(Some(JavascriptTcpListenerEvent::Error {
2150 code: io_error_code(&error),
2151 message: error.to_string(),
2152 }));
2153 }
2154 }
2155 }
2156 }
2157
2158 fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
2159 if let Some(socket_id) = self.kernel_socket_id {
2160 kernel
2161 .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
2162 .map_err(kernel_error)?;
2163 }
2164 Ok(())
2165 }
2166
2167 fn active_connection_count(&self) -> usize {
2168 self.active_connection_ids.len()
2169 }
2170
2171 fn register_connection(&mut self, socket_id: &str) {
2172 self.active_connection_ids.insert(socket_id.to_string());
2173 }
2174
2175 fn release_connection(&mut self, socket_id: &str) {
2176 self.active_connection_ids.remove(socket_id);
2177 }
2178}
2179
2180impl ActiveUdpSocket {
2183 fn new(
2184 kernel: &mut SidecarKernel,
2185 kernel_pid: u32,
2186 family: JavascriptUdpFamily,
2187 ) -> Result<Self, SidecarError> {
2188 let spec = match family {
2189 JavascriptUdpFamily::Ipv4 => SocketSpec::udp(),
2190 JavascriptUdpFamily::Ipv6 => SocketSpec::new(SocketDomain::Inet6, SocketType::Datagram),
2191 };
2192 let socket_id = kernel
2193 .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
2194 .map_err(kernel_error)?;
2195 Ok(Self {
2196 family,
2197 socket: None,
2198 kernel_socket_id: Some(socket_id),
2199 guest_local_addr: None,
2200 recv_buffer_size: 0,
2201 send_buffer_size: 0,
2202 })
2203 }
2204
2205 fn local_addr(&self) -> Option<SocketAddr> {
2206 self.guest_local_addr
2207 }
2208
2209 fn socket(&self) -> Result<&UdpSocket, SidecarError> {
2210 self.socket
2211 .as_ref()
2212 .ok_or_else(|| SidecarError::Execution(String::from("EBADF: bad file descriptor")))
2213 }
2214
2215 fn bind(
2216 &mut self,
2217 kernel: &mut SidecarKernel,
2218 kernel_pid: u32,
2219 host: Option<&str>,
2220 port: u16,
2221 context: &JavascriptSocketPathContext,
2222 ) -> Result<SocketAddr, SidecarError> {
2223 if self.socket.is_some() || self.guest_local_addr.is_some() {
2224 return Err(SidecarError::Execution(String::from(
2225 "EINVAL: secure-exec dgram socket is already bound",
2226 )));
2227 }
2228
2229 let (bind_host, guest_host, guest_family) = normalize_udp_bind_host(host, self.family)?;
2230 let guest_port = allocate_guest_listen_port(
2231 port,
2232 guest_family,
2233 &context.used_udp_guest_ports,
2234 context.listen_policy,
2235 )?;
2236 let local_addr = resolve_udp_bind_addr(guest_host, guest_port, self.family)?;
2237 if let Some(socket_id) = self.kernel_socket_id {
2238 kernel
2239 .socket_bind_inet(
2240 EXECUTION_DRIVER_NAME,
2241 kernel_pid,
2242 socket_id,
2243 InetSocketAddress::new(local_addr.ip().to_string(), local_addr.port()),
2244 )
2245 .map_err(kernel_error)?;
2246 } else {
2247 let bind_addr = resolve_udp_bind_addr(bind_host, 0, self.family)?;
2248 let socket = UdpSocket::bind(bind_addr).map_err(sidecar_net_error)?;
2249 socket.set_nonblocking(true).map_err(sidecar_net_error)?;
2250 self.socket = Some(socket);
2251 }
2252 self.guest_local_addr = Some(local_addr);
2253 Ok(local_addr)
2254 }
2255
2256 fn ensure_bound_for_send(
2257 &mut self,
2258 kernel: &mut SidecarKernel,
2259 kernel_pid: u32,
2260 context: &JavascriptSocketPathContext,
2261 ) -> Result<SocketAddr, SidecarError> {
2262 if let Some(local_addr) = self.local_addr() {
2263 return Ok(local_addr);
2264 }
2265
2266 self.bind(kernel, kernel_pid, None, 0, context)
2267 }
2268
2269 fn send_to<B>(
2270 &mut self,
2271 request: ActiveUdpSendToRequest<'_, B>,
2272 ) -> Result<(usize, SocketAddr), SidecarError>
2273 where
2274 B: NativeSidecarBridge + Send + 'static,
2275 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
2276 {
2277 let ActiveUdpSendToRequest {
2278 bridge,
2279 kernel,
2280 kernel_pid,
2281 vm_id,
2282 dns,
2283 host,
2284 port,
2285 context,
2286 contents,
2287 } = request;
2288 let remote_addr = resolve_udp_addr(UdpRemoteAddrRequest {
2289 bridge,
2290 kernel,
2291 vm_id,
2292 dns,
2293 host,
2294 port,
2295 family: self.family,
2296 context,
2297 })?;
2298 let local_addr = self.ensure_bound_for_send(kernel, kernel_pid, context)?;
2299 let written = if let Some(socket_id) = self.kernel_socket_id {
2300 if is_loopback_ip(remote_addr.ip()) && remote_addr.port() == port {
2301 kernel
2302 .socket_send_to_inet_loopback(
2303 EXECUTION_DRIVER_NAME,
2304 kernel_pid,
2305 socket_id,
2306 InetSocketAddress::new(remote_addr.ip().to_string(), remote_addr.port()),
2307 contents,
2308 )
2309 .map_err(kernel_error)?
2310 } else {
2311 return Err(SidecarError::Execution(String::from(
2312 "ERR_NOT_IMPLEMENTED: external UDP datagrams are not yet supported by the kernel-backed V8 bridge",
2313 )));
2314 }
2315 } else {
2316 let socket = self.socket.as_ref().ok_or_else(|| {
2317 SidecarError::InvalidState(String::from("UDP socket is not initialized"))
2318 })?;
2319 socket
2320 .send_to(contents, remote_addr)
2321 .map_err(sidecar_net_error)?
2322 };
2323 Ok((written, local_addr))
2324 }
2325
2326 fn poll(
2327 &self,
2328 kernel: &mut SidecarKernel,
2329 kernel_pid: u32,
2330 wait: Duration,
2331 ) -> Result<Option<JavascriptUdpSocketEvent>, SidecarError> {
2332 if let Some(socket_id) = self.kernel_socket_id {
2333 let result = kernel
2334 .poll_targets(
2335 EXECUTION_DRIVER_NAME,
2336 kernel_pid,
2337 vec![PollTargetEntry::socket(socket_id, POLLIN)],
2338 i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
2339 )
2340 .map_err(kernel_error)?;
2341 let revents = result
2342 .targets
2343 .first()
2344 .map(|entry| entry.revents)
2345 .unwrap_or_else(PollEvents::empty);
2346 if revents.is_empty() {
2347 return Ok(None);
2348 }
2349 return match kernel.socket_recv_datagram(
2350 EXECUTION_DRIVER_NAME,
2351 kernel_pid,
2352 socket_id,
2353 64 * 1024,
2354 ) {
2355 Ok(Some(datagram)) => {
2356 let (source_address, payload) = datagram.into_parts();
2357 let remote_addr = source_address
2358 .map(|source| {
2359 resolve_udp_bind_addr(source.host(), source.port(), self.family)
2360 })
2361 .transpose()?
2362 .unwrap_or_else(|| match self.family {
2363 JavascriptUdpFamily::Ipv4 => {
2364 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)
2365 }
2366 JavascriptUdpFamily::Ipv6 => {
2367 SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0)
2368 }
2369 });
2370 Ok(Some(JavascriptUdpSocketEvent::Message {
2371 data: payload,
2372 remote_addr,
2373 }))
2374 }
2375 Ok(None) => Ok(None),
2376 Err(error) if error.code() == "EAGAIN" => Ok(None),
2377 Err(error) => Ok(Some(JavascriptUdpSocketEvent::Error {
2378 code: Some(error.code().to_string()),
2379 message: error.to_string(),
2380 })),
2381 };
2382 }
2383 let socket = self.socket()?;
2384 let deadline = Instant::now() + wait;
2385 let mut buffer = vec![0_u8; 64 * 1024];
2386
2387 loop {
2388 match socket.recv_from(&mut buffer) {
2389 Ok((bytes_read, remote_addr)) => {
2390 return Ok(Some(JavascriptUdpSocketEvent::Message {
2391 data: buffer[..bytes_read].to_vec(),
2392 remote_addr,
2393 }));
2394 }
2395 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2396 if wait.is_zero() || Instant::now() >= deadline {
2397 return Ok(None);
2398 }
2399 thread::sleep(Duration::from_millis(10));
2400 }
2401 Err(error) => {
2402 return Ok(Some(JavascriptUdpSocketEvent::Error {
2403 code: io_error_code(&error),
2404 message: error.to_string(),
2405 }));
2406 }
2407 }
2408 }
2409 }
2410
2411 fn close(&mut self, kernel: &mut SidecarKernel, kernel_pid: u32) {
2412 if let Some(socket_id) = self.kernel_socket_id {
2413 let _ = kernel.socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id);
2414 }
2415 self.socket.take();
2416 self.guest_local_addr = None;
2417 }
2418
2419 fn set_buffer_size(&mut self, which: &str, size: usize) -> Result<(), SidecarError> {
2420 match which {
2421 "recv" => self.recv_buffer_size = size,
2422 "send" => self.send_buffer_size = size,
2423 other => {
2424 return Err(SidecarError::InvalidState(format!(
2425 "unsupported UDP buffer size kind {other}"
2426 )));
2427 }
2428 }
2429 if self.kernel_socket_id.is_some() {
2430 return Ok(());
2431 }
2432 let socket = self.socket()?;
2433 let socket = SockRef::from(socket);
2434 match which {
2435 "recv" => socket.set_recv_buffer_size(size).map_err(sidecar_net_error),
2436 "send" => socket.set_send_buffer_size(size).map_err(sidecar_net_error),
2437 other => Err(SidecarError::InvalidState(format!(
2438 "unsupported UDP buffer size kind {other}"
2439 ))),
2440 }
2441 }
2442
2443 fn get_buffer_size(&self, which: &str) -> Result<usize, SidecarError> {
2444 if self.kernel_socket_id.is_some() {
2445 return Ok(match which {
2446 "recv" => self.recv_buffer_size,
2447 "send" => self.send_buffer_size,
2448 other => {
2449 return Err(SidecarError::InvalidState(format!(
2450 "unsupported UDP buffer size kind {other}"
2451 )));
2452 }
2453 });
2454 }
2455 let socket = self.socket()?;
2456 let socket = SockRef::from(socket);
2457 match which {
2458 "recv" => socket.recv_buffer_size().map_err(sidecar_net_error),
2459 "send" => socket.send_buffer_size().map_err(sidecar_net_error),
2460 other => Err(SidecarError::InvalidState(format!(
2461 "unsupported UDP buffer size kind {other}"
2462 ))),
2463 }
2464 }
2465}
2466
2467impl ActiveExecution {
2470 pub(crate) fn uses_shared_v8_runtime(&self) -> bool {
2471 match self {
2472 Self::Javascript(execution) => execution.uses_shared_v8_runtime(),
2473 Self::Python(execution) => execution.uses_shared_v8_runtime(),
2474 Self::Wasm(execution) => execution.uses_shared_v8_runtime(),
2475 Self::Tool(_) => false,
2476 }
2477 }
2478
2479 pub(crate) fn child_pid(&self) -> u32 {
2480 match self {
2481 Self::Javascript(execution) => execution.child_pid(),
2482 Self::Python(execution) => execution.child_pid(),
2483 Self::Wasm(execution) => execution.child_pid(),
2484 Self::Tool(_) => 0,
2485 }
2486 }
2487
2488 pub(crate) fn write_stdin(&mut self, chunk: &[u8]) -> Result<(), SidecarError> {
2489 match self {
2490 Self::Javascript(execution) => execution
2491 .write_stdin(chunk)
2492 .map_err(|error| SidecarError::Execution(error.to_string())),
2493 Self::Python(execution) => execution
2494 .write_stdin(chunk)
2495 .map_err(|error| SidecarError::Execution(error.to_string())),
2496 Self::Wasm(execution) => execution
2497 .write_stdin(chunk)
2498 .map_err(|error| SidecarError::Execution(error.to_string())),
2499 Self::Tool(_) => Ok(()),
2500 }
2501 }
2502
2503 pub(crate) fn close_stdin(&mut self) -> Result<(), SidecarError> {
2504 match self {
2505 Self::Javascript(execution) => execution
2506 .close_stdin()
2507 .map_err(|error| SidecarError::Execution(error.to_string())),
2508 Self::Python(execution) => execution
2509 .close_stdin()
2510 .map_err(|error| SidecarError::Execution(error.to_string())),
2511 Self::Wasm(execution) => execution
2512 .close_stdin()
2513 .map_err(|error| SidecarError::Execution(error.to_string())),
2514 Self::Tool(_) => Ok(()),
2515 }
2516 }
2517
2518 pub(crate) fn respond_python_vfs_rpc_success(
2519 &mut self,
2520 id: u64,
2521 payload: PythonVfsRpcResponsePayload,
2522 ) -> Result<(), SidecarError> {
2523 match self {
2524 Self::Python(execution) => execution
2525 .respond_vfs_rpc_success(id, payload)
2526 .map_err(|error| SidecarError::Execution(error.to_string())),
2527 _ => Err(SidecarError::InvalidState(String::from(
2528 "only Python executions can service Python VFS RPC responses",
2529 ))),
2530 }
2531 }
2532
2533 pub(crate) fn respond_python_vfs_rpc_error(
2534 &mut self,
2535 id: u64,
2536 code: impl Into<String>,
2537 message: impl Into<String>,
2538 ) -> Result<(), SidecarError> {
2539 match self {
2540 Self::Python(execution) => execution
2541 .respond_vfs_rpc_error(id, code, message)
2542 .map_err(|error| SidecarError::Execution(error.to_string())),
2543 _ => Err(SidecarError::InvalidState(String::from(
2544 "only Python executions can service Python VFS RPC responses",
2545 ))),
2546 }
2547 }
2548
2549 pub(crate) fn send_javascript_stream_event(
2550 &self,
2551 event_type: &str,
2552 payload: Value,
2553 ) -> Result<(), SidecarError> {
2554 match self {
2555 Self::Javascript(execution) => execution
2556 .send_stream_event(event_type, payload)
2557 .map_err(|error| SidecarError::Execution(error.to_string())),
2558 Self::Wasm(execution) => execution
2559 .send_stream_event(event_type, payload)
2560 .map_err(|error| SidecarError::Execution(error.to_string())),
2561 _ => Err(SidecarError::InvalidState(String::from(
2562 "only embedded V8 executions can receive JavaScript stream events",
2563 ))),
2564 }
2565 }
2566
2567 pub(crate) fn javascript_v8_session_handle(&self) -> Option<V8SessionHandle> {
2568 match self {
2569 Self::Javascript(execution) => Some(execution.v8_session_handle()),
2570 Self::Wasm(execution) => Some(execution.v8_session_handle()),
2571 _ => None,
2572 }
2573 }
2574
2575 pub(crate) fn terminate(&mut self) -> Result<(), SidecarError> {
2576 match self {
2577 Self::Javascript(execution) => execution
2578 .terminate()
2579 .map_err(|error| SidecarError::Execution(error.to_string())),
2580 Self::Python(execution) => execution
2581 .kill()
2582 .map_err(|error| SidecarError::Execution(error.to_string())),
2583 Self::Wasm(execution) => execution
2584 .terminate()
2585 .map_err(|error| SidecarError::Execution(error.to_string())),
2586 Self::Tool(_) => Ok(()),
2587 }
2588 }
2589
2590 pub(crate) fn respond_javascript_sync_rpc_success(
2591 &mut self,
2592 id: u64,
2593 result: Value,
2594 ) -> Result<(), SidecarError> {
2595 match self {
2596 Self::Javascript(execution) => execution
2597 .respond_sync_rpc_success(id, result)
2598 .map_err(|error| SidecarError::Execution(error.to_string())),
2599 Self::Python(execution) => execution
2600 .respond_javascript_sync_rpc_success(id, result)
2601 .map_err(|error| SidecarError::Execution(error.to_string())),
2602 Self::Wasm(execution) => execution
2603 .respond_sync_rpc_success(id, result)
2604 .map_err(|error| SidecarError::Execution(error.to_string())),
2605 _ => Err(SidecarError::InvalidState(String::from(
2606 "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
2607 ))),
2608 }
2609 }
2610
2611 pub(crate) fn respond_javascript_sync_rpc_error(
2612 &mut self,
2613 id: u64,
2614 code: impl Into<String>,
2615 message: impl Into<String>,
2616 ) -> Result<(), SidecarError> {
2617 match self {
2618 Self::Javascript(execution) => execution
2619 .respond_sync_rpc_error(id, code, message)
2620 .map_err(|error| SidecarError::Execution(error.to_string())),
2621 Self::Python(execution) => execution
2622 .respond_javascript_sync_rpc_error(id, code, message)
2623 .map_err(|error| SidecarError::Execution(error.to_string())),
2624 Self::Wasm(execution) => execution
2625 .respond_sync_rpc_error(id, code, message)
2626 .map_err(|error| SidecarError::Execution(error.to_string())),
2627 _ => Err(SidecarError::InvalidState(String::from(
2628 "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
2629 ))),
2630 }
2631 }
2632
2633 pub(crate) async fn poll_event(
2634 &mut self,
2635 timeout: Duration,
2636 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
2637 match self {
2638 Self::Javascript(execution) => execution
2639 .poll_event(timeout)
2640 .await
2641 .map(|event| {
2642 event.map(|event| match event {
2643 JavascriptExecutionEvent::Stdout(chunk) => {
2644 ActiveExecutionEvent::Stdout(chunk)
2645 }
2646 JavascriptExecutionEvent::Stderr(chunk) => {
2647 ActiveExecutionEvent::Stderr(chunk)
2648 }
2649 JavascriptExecutionEvent::SyncRpcRequest(request) => {
2650 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2651 }
2652 JavascriptExecutionEvent::SignalState {
2653 signal,
2654 registration,
2655 } => ActiveExecutionEvent::SignalState {
2656 signal,
2657 registration: map_node_signal_registration(registration),
2658 },
2659 JavascriptExecutionEvent::Exited(code) => {
2660 ActiveExecutionEvent::Exited(code)
2661 }
2662 })
2663 })
2664 .map_err(|error| SidecarError::Execution(error.to_string())),
2665 Self::Python(execution) => execution
2666 .poll_event(timeout)
2667 .await
2668 .map(|event| {
2669 event.map(|event| match event {
2670 PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2671 PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2672 PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
2673 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2674 }
2675 PythonExecutionEvent::VfsRpcRequest(request) => {
2676 ActiveExecutionEvent::PythonVfsRpcRequest(request)
2677 }
2678 PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2679 })
2680 })
2681 .map_err(|error| SidecarError::Execution(error.to_string())),
2682 Self::Wasm(execution) => execution
2683 .poll_event(timeout)
2684 .await
2685 .map(|event| {
2686 event.map(|event| match event {
2687 WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2688 WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2689 WasmExecutionEvent::SyncRpcRequest(request) => {
2690 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2691 }
2692 WasmExecutionEvent::SignalState {
2693 signal,
2694 registration,
2695 } => ActiveExecutionEvent::SignalState {
2696 signal,
2697 registration: map_wasm_signal_registration(registration),
2698 },
2699 WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2700 })
2701 })
2702 .map_err(|error| SidecarError::Execution(error.to_string())),
2703 Self::Tool(execution) => {
2704 let _ = timeout;
2705 poll_tool_process_event(execution)
2706 }
2707 }
2708 }
2709
2710 pub(crate) fn poll_event_blocking(
2711 &mut self,
2712 timeout: Duration,
2713 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
2714 match self {
2715 Self::Javascript(execution) => execution
2716 .poll_event_blocking(timeout)
2717 .map(|event| {
2718 event.map(|event| match event {
2719 JavascriptExecutionEvent::Stdout(chunk) => {
2720 ActiveExecutionEvent::Stdout(chunk)
2721 }
2722 JavascriptExecutionEvent::Stderr(chunk) => {
2723 ActiveExecutionEvent::Stderr(chunk)
2724 }
2725 JavascriptExecutionEvent::SyncRpcRequest(request) => {
2726 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2727 }
2728 JavascriptExecutionEvent::SignalState {
2729 signal,
2730 registration,
2731 } => ActiveExecutionEvent::SignalState {
2732 signal,
2733 registration: map_node_signal_registration(registration),
2734 },
2735 JavascriptExecutionEvent::Exited(code) => {
2736 ActiveExecutionEvent::Exited(code)
2737 }
2738 })
2739 })
2740 .map_err(|error| SidecarError::Execution(error.to_string())),
2741 Self::Python(execution) => execution
2742 .poll_event_blocking(timeout)
2743 .map(|event| {
2744 event.map(|event| match event {
2745 PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2746 PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2747 PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
2748 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2749 }
2750 PythonExecutionEvent::VfsRpcRequest(request) => {
2751 ActiveExecutionEvent::PythonVfsRpcRequest(request)
2752 }
2753 PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2754 })
2755 })
2756 .map_err(|error| SidecarError::Execution(error.to_string())),
2757 Self::Wasm(execution) => execution
2758 .poll_event_blocking(timeout)
2759 .map(|event| {
2760 event.map(|event| match event {
2761 WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2762 WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2763 WasmExecutionEvent::SyncRpcRequest(request) => {
2764 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2765 }
2766 WasmExecutionEvent::SignalState {
2767 signal,
2768 registration,
2769 } => ActiveExecutionEvent::SignalState {
2770 signal,
2771 registration: map_wasm_signal_registration(registration),
2772 },
2773 WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2774 })
2775 })
2776 .map_err(|error| SidecarError::Execution(error.to_string())),
2777 Self::Tool(execution) => {
2778 let _ = timeout;
2779 poll_tool_process_event(execution)
2780 }
2781 }
2782 }
2783}
2784
2785struct ToolProcessEventRequest {
2786 sidecar_requests: SharedSidecarRequestClient,
2787 connection_id: String,
2788 session_id: String,
2789 vm_id: String,
2790 tool_resolution: ToolCommandResolution,
2791 cancelled: Arc<AtomicBool>,
2792 pending_events: Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
2793 events_overflowed: Arc<AtomicBool>,
2794}
2795
2796pub(crate) fn send_tool_process_event(
2797 pending_events: &Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
2798 events_overflowed: &AtomicBool,
2799 event: ActiveExecutionEvent,
2800) -> bool {
2801 let mut pending_events = pending_events
2802 .lock()
2803 .unwrap_or_else(|poisoned| poisoned.into_inner());
2804 if pending_events.len() >= MAX_PROCESS_EVENT_QUEUE {
2805 events_overflowed.store(true, Ordering::Relaxed);
2806 return false;
2807 }
2808 pending_events.push_back(event);
2809 true
2810}
2811
2812fn spawn_tool_process_events(request: ToolProcessEventRequest) {
2813 let ToolProcessEventRequest {
2814 sidecar_requests,
2815 connection_id,
2816 session_id,
2817 vm_id,
2818 tool_resolution,
2819 cancelled,
2820 pending_events,
2821 events_overflowed,
2822 } = request;
2823 std::thread::spawn(move || match tool_resolution {
2824 ToolCommandResolution::Failure(message) => {
2825 if !send_tool_process_event(
2826 &pending_events,
2827 &events_overflowed,
2828 ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
2829 ) {
2830 return;
2831 }
2832 let _ = send_tool_process_event(
2833 &pending_events,
2834 &events_overflowed,
2835 ActiveExecutionEvent::Exited(1),
2836 );
2837 }
2838 ToolCommandResolution::Invoke { request, timeout } => {
2839 let response = sidecar_requests.invoke(
2840 OwnershipScope::vm(connection_id.clone(), session_id.clone(), vm_id.clone()),
2841 SidecarRequestPayload::HostCallback(request.clone()),
2842 timeout,
2843 );
2844 if cancelled.load(Ordering::Relaxed) {
2845 return;
2846 }
2847
2848 match response {
2849 Ok(crate::protocol::SidecarResponsePayload::HostCallbackResult(result)) => {
2850 if let Some(value) = result.result {
2851 let value: serde_json::Value = serde_json::from_str(&value)
2852 .unwrap_or(serde_json::Value::String(value));
2853 let stdout = serde_json::to_vec(&json!({
2854 "ok": true,
2855 "result": value,
2856 }))
2857 .unwrap_or_else(|error| {
2858 format_tool_failure_output(&format!(
2859 "failed to serialize tool result: {error}"
2860 ))
2861 });
2862 if !send_tool_process_event(
2863 &pending_events,
2864 &events_overflowed,
2865 ActiveExecutionEvent::Stdout(stdout),
2866 ) {
2867 return;
2868 }
2869 let _ = send_tool_process_event(
2870 &pending_events,
2871 &events_overflowed,
2872 ActiveExecutionEvent::Exited(0),
2873 );
2874 } else {
2875 let message = result
2876 .error
2877 .unwrap_or_else(|| String::from("tool invocation returned no result"));
2878 if !send_tool_process_event(
2879 &pending_events,
2880 &events_overflowed,
2881 ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
2882 ) {
2883 return;
2884 }
2885 let _ = send_tool_process_event(
2886 &pending_events,
2887 &events_overflowed,
2888 ActiveExecutionEvent::Exited(1),
2889 );
2890 }
2891 }
2892 Ok(_) => {
2893 if !send_tool_process_event(
2894 &pending_events,
2895 &events_overflowed,
2896 ActiveExecutionEvent::Stderr(format_tool_failure_output(
2897 "unexpected sidecar tool response",
2898 )),
2899 ) {
2900 return;
2901 }
2902 let _ = send_tool_process_event(
2903 &pending_events,
2904 &events_overflowed,
2905 ActiveExecutionEvent::Exited(1),
2906 );
2907 }
2908 Err(error) => {
2909 if !send_tool_process_event(
2910 &pending_events,
2911 &events_overflowed,
2912 ActiveExecutionEvent::Stderr(format_tool_failure_output(
2913 &error.to_string(),
2914 )),
2915 ) {
2916 return;
2917 }
2918 let _ = send_tool_process_event(
2919 &pending_events,
2920 &events_overflowed,
2921 ActiveExecutionEvent::Exited(1),
2922 );
2923 }
2924 }
2925 }
2926 });
2927}
2928
2929impl<B> NativeSidecar<B>
2930where
2931 B: NativeSidecarBridge + Send + 'static,
2932 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
2933{
2934 pub(crate) async fn execute(
2935 &mut self,
2936 request: &RequestFrame,
2937 payload: ExecuteRequest,
2938 ) -> Result<DispatchResult, SidecarError> {
2939 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
2940 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
2941
2942 let vm = self
2943 .vms
2944 .get_mut(&vm_id)
2945 .ok_or_else(|| missing_vm_error(&vm_id))?;
2946 if vm.active_processes.contains_key(&payload.process_id) {
2947 return Err(SidecarError::InvalidState(format!(
2948 "VM {vm_id} already has an active process with id {}",
2949 payload.process_id
2950 )));
2951 }
2952
2953 if let Some(command) = payload.command.as_deref() {
2954 if let Some(tool_resolution) =
2955 resolve_tool_command(vm, command, &payload.args, payload.cwd.as_deref())?
2956 {
2957 let guest_cwd = payload
2958 .cwd
2959 .as_deref()
2960 .map(normalize_path)
2961 .unwrap_or_else(|| vm.guest_cwd.clone());
2962 let kernel_handle = vm
2963 .kernel
2964 .create_virtual_process(
2965 EXECUTION_DRIVER_NAME,
2966 TOOL_DRIVER_NAME,
2967 command,
2968 std::iter::once(command.to_owned())
2969 .chain(payload.args.iter().cloned())
2970 .collect(),
2971 VirtualProcessOptions {
2972 env: vm.guest_env.clone(),
2973 cwd: Some(guest_cwd.clone()),
2974 ..VirtualProcessOptions::default()
2975 },
2976 )
2977 .map_err(kernel_error)?;
2978 let kernel_pid = kernel_handle.pid();
2979 let tool_execution = ToolExecution::default();
2980 let cancelled = tool_execution.cancelled.clone();
2981 let pending_events = tool_execution.pending_events.clone();
2982 let events_overflowed = tool_execution.events_overflowed.clone();
2983 vm.active_processes.insert(
2984 payload.process_id.clone(),
2985 ActiveProcess::new(
2986 kernel_pid,
2987 kernel_handle,
2988 GuestRuntimeKind::JavaScript,
2989 ActiveExecution::Tool(tool_execution),
2990 )
2991 .with_guest_cwd(guest_cwd.clone())
2992 .with_host_cwd(resolve_vm_guest_path_to_host(vm, &guest_cwd)),
2993 );
2994 self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
2995 spawn_tool_process_events(ToolProcessEventRequest {
2996 sidecar_requests: self.sidecar_requests.clone(),
2997 connection_id: connection_id.clone(),
2998 session_id: session_id.clone(),
2999 vm_id: vm_id.clone(),
3000 tool_resolution,
3001 cancelled,
3002 pending_events,
3003 events_overflowed,
3004 });
3005
3006 return Ok(DispatchResult {
3007 response: self.respond(
3008 request,
3009 ResponsePayload::ProcessStarted(ProcessStartedResponse {
3010 process_id: payload.process_id,
3011 pid: Some(kernel_pid),
3012 }),
3013 ),
3014 events: Vec::new(),
3015 });
3016 }
3017 }
3018
3019 let resolved = resolve_execute_request(vm, &payload)?;
3020 let mut env = resolved.env.clone();
3021 let sandbox_root = normalize_host_path(&vm.cwd);
3022 env.insert(
3023 String::from(EXECUTION_SANDBOX_ROOT_ENV),
3024 sandbox_root.to_string_lossy().into_owned(),
3025 );
3026 if resolved.runtime == GuestRuntimeKind::JavaScript {
3027 env.insert(
3028 String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
3029 String::from("1"),
3030 );
3031 } else if resolved.runtime == GuestRuntimeKind::WebAssembly {
3032 env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
3033 }
3034 let argv = std::iter::once(resolved.entrypoint.clone())
3035 .chain(resolved.execution_args.iter().cloned())
3036 .collect::<Vec<_>>();
3037 let kernel_handle = vm
3038 .kernel
3039 .spawn_process(
3040 &resolved.command,
3041 argv,
3042 SpawnOptions {
3043 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
3044 cwd: Some(resolved.guest_cwd.clone()),
3045 ..SpawnOptions::default()
3046 },
3047 )
3048 .map_err(kernel_error)?;
3049 let kernel_pid = kernel_handle.pid();
3050
3051 let (execution, process_env) = match resolved.runtime {
3052 GuestRuntimeKind::JavaScript => {
3053 let inline_code = load_javascript_entrypoint_source(
3054 vm,
3055 &resolved.host_cwd,
3056 &resolved.entrypoint,
3057 &env,
3058 );
3059 prepare_javascript_shadow(vm, &resolved)?;
3060
3061 let context =
3062 self.javascript_engine
3063 .create_context(CreateJavascriptContextRequest {
3064 vm_id: vm_id.clone(),
3065 bootstrap_module: None,
3066 compile_cache_root: Some(self.cache_root.join("node-compile-cache")),
3067 });
3068 let module_reader = build_module_reader(vm, &resolved)
3069 .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
3070 let execution = self
3071 .javascript_engine
3072 .start_execution_with_module_reader(
3073 StartJavascriptExecutionRequest {
3074 guest_runtime: guest_runtime_identity(vm, None, None),
3075 vm_id: vm_id.clone(),
3076 context_id: context.context_id,
3077 argv: std::iter::once(resolved.entrypoint.clone())
3078 .chain(resolved.execution_args.iter().cloned())
3079 .collect(),
3080 env: env.clone(),
3081 cwd: resolved.host_cwd.clone(),
3082 limits: javascript_execution_limits(vm),
3083 inline_code,
3084 },
3085 module_reader,
3086 )
3087 .map_err(javascript_error)?;
3088 (ActiveExecution::Javascript(execution), env.clone())
3089 }
3090 GuestRuntimeKind::Python => {
3091 let python_file_path = python_file_entrypoint(&resolved.entrypoint);
3092 let pyodide_dist_path = self
3093 .python_engine
3094 .bundled_pyodide_dist_path_for_vm(&vm_id)
3095 .map_err(python_error)?;
3096 let pyodide_cache_path = pyodide_dist_path
3097 .parent()
3098 .and_then(Path::parent)
3099 .unwrap_or(pyodide_dist_path.as_path())
3100 .join("pyodide-package-cache");
3101 add_runtime_guest_path_mapping(
3102 &mut env,
3103 PYTHON_PYODIDE_GUEST_ROOT,
3104 &pyodide_dist_path,
3105 );
3106 add_runtime_guest_path_mapping(
3107 &mut env,
3108 PYTHON_PYODIDE_CACHE_GUEST_ROOT,
3109 &pyodide_cache_path,
3110 );
3111 add_runtime_host_access_path(
3112 &mut env,
3113 "AGENTOS_EXTRA_FS_READ_PATHS",
3114 &pyodide_dist_path,
3115 true,
3116 );
3117 add_runtime_host_access_path(
3118 &mut env,
3119 "AGENTOS_EXTRA_FS_READ_PATHS",
3120 &pyodide_cache_path,
3121 true,
3122 );
3123 add_runtime_host_access_path(
3124 &mut env,
3125 "AGENTOS_EXTRA_FS_WRITE_PATHS",
3126 &pyodide_cache_path,
3127 false,
3128 );
3129 let context = self
3130 .python_engine
3131 .create_context(CreatePythonContextRequest {
3132 vm_id: vm_id.clone(),
3133 pyodide_dist_path,
3134 });
3135 let execution = self
3136 .python_engine
3137 .start_execution(StartPythonExecutionRequest {
3138 vm_id: vm_id.clone(),
3139 context_id: context.context_id,
3140 code: resolved.entrypoint.clone(),
3141 file_path: python_file_path,
3142 env: env.clone(),
3143 cwd: resolved.host_cwd.clone(),
3144 limits: python_execution_limits(vm),
3145 guest_runtime: guest_runtime_identity(vm, None, None),
3146 })
3147 .map_err(python_error)?;
3148 (ActiveExecution::Python(execution), env.clone())
3149 }
3150 GuestRuntimeKind::WebAssembly => {
3151 let wasm_limits = wasm_execution_limits(vm);
3152 let wasm_guest_runtime =
3153 guest_runtime_identity(vm, Some(u64::from(kernel_pid)), Some(0));
3154 let wasm_permission_tier = resolved.wasm_permission_tier.unwrap_or_else(|| {
3155 resolve_wasm_permission_tier(
3156 vm,
3157 Some(&resolved.command),
3158 None,
3159 &resolved.entrypoint,
3160 )
3161 });
3162 let context = self.wasm_engine.create_context(CreateWasmContextRequest {
3163 vm_id: vm_id.clone(),
3164 module_path: Some(resolved.entrypoint.clone()),
3165 });
3166 let execution = self
3167 .wasm_engine
3168 .start_execution(StartWasmExecutionRequest {
3169 vm_id: vm_id.clone(),
3170 context_id: context.context_id,
3171 argv: resolved.process_args.clone(),
3172 env: env.clone(),
3173 cwd: resolved.host_cwd.clone(),
3174 permission_tier: execution_wasm_permission_tier(wasm_permission_tier),
3175 limits: wasm_limits,
3176 guest_runtime: wasm_guest_runtime,
3177 })
3178 .map_err(wasm_error)?;
3179 (ActiveExecution::Wasm(Box::new(execution)), env)
3180 }
3181 };
3182 let child_pid = execution.child_pid();
3183 let kernel_stdin_writer_fd = install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?;
3184 vm.active_processes.insert(
3185 payload.process_id.clone(),
3186 ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
3187 .with_kernel_stdin_writer_fd(kernel_stdin_writer_fd)
3188 .with_guest_cwd(resolved.guest_cwd.clone())
3189 .with_env(process_env)
3190 .with_host_cwd(resolved.host_cwd.clone()),
3191 );
3192 self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
3193
3194 Ok(DispatchResult {
3195 response: self.respond(
3196 request,
3197 ResponsePayload::ProcessStarted(ProcessStartedResponse {
3198 process_id: payload.process_id,
3199 pid: Some(if child_pid == 0 {
3200 kernel_pid
3201 } else {
3202 child_pid
3203 }),
3204 }),
3205 ),
3206 events: Vec::new(),
3207 })
3208 }
3209
3210 pub(crate) async fn write_stdin(
3211 &mut self,
3212 request: &RequestFrame,
3213 payload: WriteStdinRequest,
3214 ) -> Result<DispatchResult, SidecarError> {
3215 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3216 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3217
3218 let vm = self
3219 .vms
3220 .get_mut(&vm_id)
3221 .ok_or_else(|| missing_vm_error(&vm_id))?;
3222 let process = vm
3223 .active_processes
3224 .get_mut(&payload.process_id)
3225 .ok_or_else(|| {
3226 SidecarError::InvalidState(format!(
3227 "VM {vm_id} has no active process {}",
3228 payload.process_id
3229 ))
3230 })?;
3231 process.execution.write_stdin(&payload.chunk)?;
3232 write_kernel_process_stdin(&mut vm.kernel, process, &payload.chunk)?;
3233
3234 Ok(DispatchResult {
3235 response: self.respond(
3236 request,
3237 ResponsePayload::StdinWritten(StdinWrittenResponse {
3238 process_id: payload.process_id,
3239 accepted_bytes: payload.chunk.len() as u64,
3240 }),
3241 ),
3242 events: Vec::new(),
3243 })
3244 }
3245
3246 pub(crate) async fn close_stdin(
3247 &mut self,
3248 request: &RequestFrame,
3249 payload: CloseStdinRequest,
3250 ) -> Result<DispatchResult, SidecarError> {
3251 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3252 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3253
3254 let vm = self
3255 .vms
3256 .get_mut(&vm_id)
3257 .ok_or_else(|| missing_vm_error(&vm_id))?;
3258 let process = vm
3259 .active_processes
3260 .get_mut(&payload.process_id)
3261 .ok_or_else(|| {
3262 SidecarError::InvalidState(format!(
3263 "VM {vm_id} has no active process {}",
3264 payload.process_id
3265 ))
3266 })?;
3267 process.execution.close_stdin()?;
3268 close_kernel_process_stdin(&mut vm.kernel, process)?;
3269
3270 Ok(DispatchResult {
3271 response: self.respond(
3272 request,
3273 ResponsePayload::StdinClosed(StdinClosedResponse {
3274 process_id: payload.process_id,
3275 }),
3276 ),
3277 events: Vec::new(),
3278 })
3279 }
3280
3281 pub(crate) async fn kill_process(
3282 &mut self,
3283 request: &RequestFrame,
3284 payload: KillProcessRequest,
3285 ) -> Result<DispatchResult, SidecarError> {
3286 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3287 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3288 self.kill_process_internal(&vm_id, &payload.process_id, &payload.signal)?;
3289
3290 Ok(DispatchResult {
3291 response: self.respond(
3292 request,
3293 ResponsePayload::ProcessKilled(ProcessKilledResponse {
3294 process_id: payload.process_id,
3295 }),
3296 ),
3297 events: Vec::new(),
3298 })
3299 }
3300
3301 pub(crate) async fn find_listener(
3302 &mut self,
3303 request: &RequestFrame,
3304 payload: FindListenerRequest,
3305 ) -> Result<DispatchResult, SidecarError> {
3306 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3307 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3308 require_vm_inspection_permission(
3309 &self.bridge,
3310 &vm_id,
3311 "network.inspect",
3312 "network",
3313 &socket_query_resource(SocketQueryKind::TcpListener, &payload),
3314 )?;
3315
3316 let listener =
3317 find_socket_state_entry(self.vms.get(&vm_id), SocketQueryKind::TcpListener, &payload)?;
3318
3319 Ok(DispatchResult {
3320 response: self.respond(
3321 request,
3322 ResponsePayload::ListenerSnapshot(ListenerSnapshotResponse { listener }),
3323 ),
3324 events: Vec::new(),
3325 })
3326 }
3327
3328 pub(crate) async fn get_process_snapshot(
3329 &mut self,
3330 request: &RequestFrame,
3331 _payload: GetProcessSnapshotRequest,
3332 ) -> Result<DispatchResult, SidecarError> {
3333 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3334 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3335 require_vm_inspection_permission(
3336 &self.bridge,
3337 &vm_id,
3338 "process.inspect",
3339 "process",
3340 "process://snapshot",
3341 )?;
3342
3343 let processes = self
3344 .vms
3345 .get_mut(&vm_id)
3346 .map(|vm| {
3347 prune_exited_process_snapshots(vm);
3348 snapshot_vm_processes(vm)
3349 })
3350 .unwrap_or_default();
3351
3352 Ok(DispatchResult {
3353 response: self.respond(
3354 request,
3355 ResponsePayload::ProcessSnapshot(ProcessSnapshotResponse { processes }),
3356 ),
3357 events: Vec::new(),
3358 })
3359 }
3360
3361 pub(crate) async fn find_bound_udp(
3362 &mut self,
3363 request: &RequestFrame,
3364 payload: FindBoundUdpRequest,
3365 ) -> Result<DispatchResult, SidecarError> {
3366 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3367 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3368
3369 let lookup_request = FindListenerRequest {
3370 host: payload.host,
3371 port: payload.port,
3372 path: None,
3373 };
3374 require_vm_inspection_permission(
3375 &self.bridge,
3376 &vm_id,
3377 "network.inspect",
3378 "network",
3379 &socket_query_resource(SocketQueryKind::UdpBound, &lookup_request),
3380 )?;
3381 let socket = find_socket_state_entry(
3382 self.vms.get(&vm_id),
3383 SocketQueryKind::UdpBound,
3384 &lookup_request,
3385 )?;
3386
3387 Ok(DispatchResult {
3388 response: self.respond(
3389 request,
3390 ResponsePayload::BoundUdpSnapshot(BoundUdpSnapshotResponse { socket }),
3391 ),
3392 events: Vec::new(),
3393 })
3394 }
3395
3396 pub(crate) async fn vm_fetch(
3397 &mut self,
3398 request: &RequestFrame,
3399 payload: VmFetchRequest,
3400 ) -> Result<DispatchResult, SidecarError> {
3401 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3402 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3403
3404 let vm = self
3405 .vms
3406 .get_mut(&vm_id)
3407 .ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
3408 let target_path = if payload.path.starts_with('/') {
3409 payload.path.clone()
3410 } else {
3411 format!("/{}", payload.path)
3412 };
3413 let request_url = Url::parse(&format!("http://127.0.0.1:{}{target_path}", payload.port))
3414 .map_err(|error| {
3415 SidecarError::InvalidState(format!(
3416 "invalid vm.fetch target {target_path:?}: {error}"
3417 ))
3418 })?;
3419 let header_values: BTreeMap<String, Value> = serde_json::from_str(&payload.headers_json)
3420 .map_err(|error| {
3421 SidecarError::InvalidState(format!(
3422 "vm.fetch headers_json must be valid JSON: {error}"
3423 ))
3424 })?;
3425 let options = JavascriptHttpRequestOptions {
3426 method: Some(payload.method),
3427 headers: header_values,
3428 body: payload.body,
3429 reject_unauthorized: None,
3430 };
3431 let headers = parse_http_header_collection(&options.headers, "vm.fetch headers")?;
3432 let target_process_id = find_kernel_http_listener_process(vm, payload.port);
3433 if let Some(target_process_id) = target_process_id {
3434 let max_fetch_response_bytes = vm.limits.http.max_fetch_response_bytes;
3435 let response_json = match dispatch_kernel_http_fetch(
3436 &self.bridge,
3437 &vm_id,
3438 vm,
3439 &target_process_id,
3440 payload.port,
3441 &target_path,
3442 &options,
3443 &headers,
3444 max_fetch_response_bytes,
3445 ) {
3446 Ok(response_json) => response_json,
3447 Err(error) => {
3448 if let Some(exit_code) = kernel_http_fetch_target_exit_code(&error) {
3449 let _ = vm;
3450 self.finish_active_process_exit(&vm_id, &target_process_id, exit_code)?;
3451 }
3452 return Err(error);
3453 }
3454 };
3455 let response = self.respond(
3456 request,
3457 ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
3458 );
3459 ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
3460
3461 return Ok(DispatchResult {
3462 response,
3463 events: Vec::new(),
3464 });
3465 }
3466
3467 let Some((target_process_id, server_id)) =
3468 vm.active_processes
3469 .iter()
3470 .find_map(|(process_id, process)| {
3471 process
3472 .http_servers
3473 .iter()
3474 .find(|(_, server)| server.guest_local_addr.port() == payload.port)
3475 .map(|(server_id, _)| (process_id.clone(), *server_id))
3476 })
3477 else {
3478 return Err(SidecarError::Execution(format!(
3479 "vm.fetch could not find a guest HTTP listener on port {}",
3480 payload.port
3481 )));
3482 };
3483 let socket_paths = build_javascript_socket_path_context(vm)?;
3484 let resource_limits = vm.kernel.resource_limits().clone();
3485 let process = vm
3486 .active_processes
3487 .get_mut(&target_process_id)
3488 .ok_or_else(|| {
3489 SidecarError::InvalidState(format!(
3490 "vm.fetch target process disappeared: {target_process_id}"
3491 ))
3492 })?;
3493 let request_json = serialize_http_loopback_request(&request_url, &options, &headers)?;
3494 let response_json = dispatch_loopback_http_request(LoopbackHttpDispatchRequest {
3495 bridge: &self.bridge,
3496 vm_id: &vm_id,
3497 dns: &vm.dns,
3498 socket_paths: &socket_paths,
3499 kernel: &mut vm.kernel,
3500 process,
3501 resource_limits: &resource_limits,
3502 server_id,
3503 request_json: &request_json,
3504 })?;
3505
3506 let response = self.respond(
3507 request,
3508 ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
3509 );
3510 ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
3511
3512 Ok(DispatchResult {
3513 response,
3514 events: Vec::new(),
3515 })
3516 }
3517
3518 pub(crate) async fn get_signal_state(
3519 &mut self,
3520 request: &RequestFrame,
3521 payload: GetSignalStateRequest,
3522 ) -> Result<DispatchResult, SidecarError> {
3523 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3524 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3525
3526 let handlers = self
3527 .vms
3528 .get(&vm_id)
3529 .and_then(|vm| vm.signal_states.get(&payload.process_id))
3530 .cloned()
3531 .unwrap_or_default();
3532
3533 Ok(DispatchResult {
3534 response: self.respond(
3535 request,
3536 ResponsePayload::SignalState(SignalStateResponse {
3537 process_id: payload.process_id,
3538 handlers: handlers.into_iter().collect(),
3539 }),
3540 ),
3541 events: Vec::new(),
3542 })
3543 }
3544
3545 pub(crate) async fn get_zombie_timer_count(
3546 &mut self,
3547 request: &RequestFrame,
3548 _payload: GetZombieTimerCountRequest,
3549 ) -> Result<DispatchResult, SidecarError> {
3550 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3551 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3552
3553 let count = self
3554 .vms
3555 .get(&vm_id)
3556 .map(|vm| vm.kernel.zombie_timer_count() as u64)
3557 .unwrap_or_default();
3558
3559 Ok(DispatchResult {
3560 response: self.respond(
3561 request,
3562 ResponsePayload::ZombieTimerCount(ZombieTimerCountResponse { count }),
3563 ),
3564 events: Vec::new(),
3565 })
3566 }
3567
3568 pub(crate) fn kill_process_internal(
3569 &mut self,
3570 vm_id: &str,
3571 process_id: &str,
3572 signal: &str,
3573 ) -> Result<(), SidecarError> {
3574 let signal_name = signal.to_owned();
3575 let signal = parse_signal(signal)?;
3576 let vm = self
3577 .vms
3578 .get_mut(vm_id)
3579 .ok_or_else(|| SidecarError::InvalidState(format!("unknown sidecar VM {vm_id}")))?;
3580 let process = vm.active_processes.get_mut(process_id).ok_or_else(|| {
3581 SidecarError::InvalidState(format!("VM {vm_id} has no active process {process_id}"))
3582 })?;
3583 let kernel_pid = process.kernel_pid;
3584
3585 enum KillBehavior {
3586 Tool,
3587 SharedV8StateOnly,
3588 SharedV8Continue,
3589 SharedV8Terminate,
3590 SharedV8DispatchOrTerminate,
3591 Noop,
3592 HostPid(u32),
3593 }
3594
3595 let behavior = match &process.execution {
3596 ActiveExecution::Tool(_) => KillBehavior::Tool,
3597 ActiveExecution::Javascript(execution)
3598 if execution.uses_shared_v8_runtime() && matches!(signal, 0 | libc::SIGSTOP) =>
3599 {
3600 KillBehavior::SharedV8StateOnly
3601 }
3602 ActiveExecution::Javascript(execution)
3603 if execution.uses_shared_v8_runtime() && signal == libc::SIGCONT =>
3604 {
3605 KillBehavior::SharedV8Continue
3606 }
3607 ActiveExecution::Wasm(execution)
3608 if execution.uses_shared_v8_runtime()
3609 && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
3610 {
3611 KillBehavior::SharedV8StateOnly
3612 }
3613 ActiveExecution::Python(execution)
3614 if execution.uses_shared_v8_runtime()
3615 && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
3616 {
3617 KillBehavior::SharedV8StateOnly
3618 }
3619 ActiveExecution::Javascript(execution)
3620 if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
3621 {
3622 KillBehavior::SharedV8Terminate
3623 }
3624 ActiveExecution::Wasm(execution)
3625 if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
3626 {
3627 KillBehavior::SharedV8Terminate
3628 }
3629 ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime() => {
3630 KillBehavior::SharedV8DispatchOrTerminate
3631 }
3632 ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime() => {
3633 KillBehavior::SharedV8Terminate
3634 }
3635 ActiveExecution::Python(execution) if execution.uses_shared_v8_runtime() => {
3636 KillBehavior::SharedV8Terminate
3637 }
3638 ActiveExecution::Javascript(execution) if execution.child_pid() == 0 => {
3639 KillBehavior::Noop
3640 }
3641 _ => KillBehavior::HostPid(process.execution.child_pid()),
3642 };
3643
3644 match behavior {
3645 KillBehavior::Tool => {
3646 let ActiveExecution::Tool(execution) = &process.execution else {
3647 unreachable!("kill behavior must match tool execution");
3648 };
3649 if signal != 0 {
3650 execution.cancelled.store(true, Ordering::Relaxed);
3651 process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
3652 128 + signal,
3653 ))?;
3654 }
3655 }
3656 KillBehavior::SharedV8StateOnly => {
3657 if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
3658 vm.kernel
3659 .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
3660 .map_err(kernel_error)?;
3661 }
3662 }
3663 KillBehavior::SharedV8Continue => {
3664 vm.kernel
3665 .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
3666 .map_err(kernel_error)?;
3667 if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
3668 process.execution.terminate()?;
3669 }
3670 }
3671 KillBehavior::SharedV8Terminate => {
3672 if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
3673 close_kernel_process_stdin(&mut vm.kernel, process)?;
3674 }
3675 process.execution.terminate()?;
3676 let needs_synthetic_exit = matches!(process.execution, ActiveExecution::Wasm(_))
3677 || (signal == SIGKILL
3678 && matches!(process.execution, ActiveExecution::Javascript(_)));
3679 if signal != 0 && needs_synthetic_exit {
3680 process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
3681 128 + signal,
3682 ))?;
3683 }
3684 }
3685 KillBehavior::SharedV8DispatchOrTerminate => {
3686 if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
3687 process.execution.terminate()?;
3688 }
3689 }
3690 KillBehavior::Noop => {}
3691 KillBehavior::HostPid(pid) => {
3692 if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
3693 close_kernel_process_stdin(&mut vm.kernel, process)?;
3694 }
3695 signal_runtime_process(pid, signal)?;
3696 }
3697 }
3698 emit_security_audit_event(
3699 &self.bridge,
3700 vm_id,
3701 "security.process.kill",
3702 audit_fields([
3703 (String::from("source"), String::from("control_plane")),
3704 (String::from("source_pid"), String::from("0")),
3705 (String::from("target_pid"), process.kernel_pid.to_string()),
3706 (String::from("process_id"), process_id.to_owned()),
3707 (String::from("signal"), signal_name),
3708 (
3709 String::from("host_pid"),
3710 process.execution.child_pid().to_string(),
3711 ),
3712 ]),
3713 );
3714 Ok(())
3715 }
3716
3717 pub async fn pump_process_events(
3718 &mut self,
3719 ownership: &OwnershipScope,
3720 ) -> Result<bool, SidecarError> {
3721 let mut emitted_any = false;
3722
3723 let mut queued_envelopes = Vec::new();
3724 {
3725 let pending_capacity = self.pending_process_event_capacity();
3726 let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
3727 SidecarError::InvalidState(String::from("process event receiver unavailable"))
3728 })?;
3729 loop {
3730 if queued_envelopes.len() >= pending_capacity {
3731 if receiver.is_empty() {
3732 break;
3733 }
3734 return Err(process_event_queue_overflow_error());
3735 }
3736 match receiver.try_recv() {
3737 Ok(envelope) => {
3738 queued_envelopes.push(envelope);
3739 emitted_any = true;
3740 }
3741 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
3742 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
3743 }
3744 }
3745 }
3746 for envelope in queued_envelopes {
3747 self.queue_pending_process_event(envelope)?;
3748 }
3749
3750 let vm_ids = self.vm_ids_for_scope(ownership)?;
3751 for vm_id in vm_ids {
3752 while let Some(vm) = self.vms.get(&vm_id) {
3753 let connection_id = vm.connection_id.clone();
3754 let session_id = vm.session_id.clone();
3755 let process_ids = self
3756 .vms
3757 .get(&vm_id)
3758 .map(|vm| vm.active_processes.keys().cloned().collect::<Vec<_>>())
3759 .unwrap_or_default();
3760 let mut emitted_this_pass = false;
3761
3762 for process_id in process_ids {
3763 if self
3764 .vms
3765 .get(&vm_id)
3766 .is_some_and(|vm| vm.detached_child_processes.contains(&process_id))
3767 {
3768 continue;
3769 }
3770 enum ProcessPollResult {
3771 Event(Box<Option<ActiveExecutionEvent>>),
3772 RecoverClosedChannel,
3773 }
3774 let poll_result = {
3775 let Some(vm) = self.vms.get_mut(&vm_id) else {
3776 continue;
3777 };
3778 let Some(process) = vm.active_processes.get_mut(&process_id) else {
3779 continue;
3780 };
3781 if let Some(event) = process.pending_execution_events.pop_front() {
3782 ProcessPollResult::Event(Box::new(Some(event)))
3783 } else {
3784 match process.execution.poll_event(Duration::ZERO).await {
3785 Ok(event) => ProcessPollResult::Event(Box::new(event)),
3786 Err(SidecarError::Execution(message))
3787 if (process.runtime == GuestRuntimeKind::JavaScript
3788 && closed_javascript_event_channel(&message))
3789 || (process.runtime == GuestRuntimeKind::Python
3790 && closed_python_event_channel(&message))
3791 || (process.runtime == GuestRuntimeKind::WebAssembly
3792 && closed_wasm_event_channel(&message)) =>
3793 {
3794 ProcessPollResult::RecoverClosedChannel
3795 }
3796 Err(other) => return Err(other),
3797 }
3798 }
3799 };
3800 let event = match poll_result {
3801 ProcessPollResult::Event(event) => *event,
3802 ProcessPollResult::RecoverClosedChannel => {
3803 self.recover_closed_root_runtime_process_event(&vm_id, &process_id)?
3804 }
3805 };
3806
3807 let Some(event) = event else {
3808 continue;
3809 };
3810
3811 if Self::internal_execution_event(&event) {
3812 self.handle_execution_event(&vm_id, &process_id, event)?;
3817 } else {
3818 self.queue_pending_process_event(ProcessEventEnvelope {
3819 connection_id: connection_id.clone(),
3820 session_id: session_id.clone(),
3821 vm_id: vm_id.clone(),
3822 process_id: process_id.clone(),
3823 event,
3824 })?;
3825 }
3826 emitted_any = true;
3827 emitted_this_pass = true;
3828 }
3829
3830 if !emitted_this_pass {
3831 break;
3832 }
3833 }
3834
3835 if self.pump_detached_child_process_events(&vm_id)? {
3836 emitted_any = true;
3837 }
3838 }
3839
3840 Ok(emitted_any)
3841 }
3842
3843 fn internal_execution_event(event: &ActiveExecutionEvent) -> bool {
3844 matches!(
3845 event,
3846 ActiveExecutionEvent::JavascriptSyncRpcRequest(_)
3847 | ActiveExecutionEvent::PythonVfsRpcRequest(_)
3848 | ActiveExecutionEvent::SignalState { .. }
3849 )
3850 }
3851
3852 fn recover_closed_root_runtime_process_event(
3853 &mut self,
3854 vm_id: &str,
3855 process_id: &str,
3856 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
3857 let Some(vm) = self.vms.get_mut(vm_id) else {
3858 return Ok(None);
3859 };
3860 let Some(process) = vm.active_processes.get(process_id) else {
3861 return Ok(None);
3862 };
3863 if process.execution.uses_shared_v8_runtime() {
3864 return Ok(None);
3865 }
3866 if process.runtime != GuestRuntimeKind::JavaScript
3867 && process.runtime != GuestRuntimeKind::Python
3868 && process.runtime != GuestRuntimeKind::WebAssembly
3869 {
3870 return Ok(None);
3871 }
3872 let runtime_child_pid = process.execution.child_pid();
3873 if runtime_child_pid == 0 {
3874 return Ok(None);
3875 }
3876 if let Some(status) = runtime_child_exit_status(runtime_child_pid)? {
3877 return Ok(Some(ActiveExecutionEvent::Exited(status)));
3878 }
3879 if runtime_child_is_alive(runtime_child_pid)? {
3880 return Ok(None);
3881 }
3882 Ok(Some(ActiveExecutionEvent::Exited(0)))
3883 }
3884
3885 fn active_process_by_path<'a>(
3886 process: &'a ActiveProcess,
3887 child_path: &[&str],
3888 ) -> Option<&'a ActiveProcess> {
3889 let mut current = process;
3890 for child_id in child_path {
3891 current = current.child_processes.get(*child_id)?;
3892 }
3893 Some(current)
3894 }
3895
3896 fn active_process_by_path_mut<'a>(
3897 process: &'a mut ActiveProcess,
3898 child_path: &[&str],
3899 ) -> Option<&'a mut ActiveProcess> {
3900 let mut current = process;
3901 for child_id in child_path {
3902 current = current.child_processes.get_mut(*child_id)?;
3903 }
3904 Some(current)
3905 }
3906
3907 fn active_process_by_owned_path_mut<'a>(
3908 process: &'a mut ActiveProcess,
3909 child_path: &[String],
3910 ) -> Option<&'a mut ActiveProcess> {
3911 let mut current = process;
3912 for child_id in child_path {
3913 current = current.child_processes.get_mut(child_id)?;
3914 }
3915 Some(current)
3916 }
3917
3918 fn active_process_path_by_kernel_pid(
3919 process: &ActiveProcess,
3920 kernel_pid: u32,
3921 ) -> Option<Vec<String>> {
3922 if process.kernel_pid == kernel_pid {
3923 return Some(Vec::new());
3924 }
3925
3926 for (child_id, child) in &process.child_processes {
3927 let Some(mut path) = Self::active_process_path_by_kernel_pid(child, kernel_pid) else {
3928 continue;
3929 };
3930 path.insert(0, child_id.clone());
3931 return Some(path);
3932 }
3933
3934 None
3935 }
3936
3937 fn descendant_parent_process<'a>(
3938 vm: &'a VmState,
3939 process_id: &str,
3940 child_path: &[&str],
3941 ) -> Option<&'a ActiveProcess> {
3942 let root = vm.active_processes.get(process_id)?;
3943 Self::active_process_by_path(root, child_path)
3944 }
3945
3946 fn descendant_parent_process_mut<'a>(
3947 vm: &'a mut VmState,
3948 process_id: &str,
3949 child_path: &[&str],
3950 ) -> Option<&'a mut ActiveProcess> {
3951 let root = vm.active_processes.get_mut(process_id)?;
3952 Self::active_process_by_path_mut(root, child_path)
3953 }
3954
3955 fn child_process_path_label(process_id: &str, child_path: &[&str]) -> String {
3956 if child_path.is_empty() {
3957 process_id.to_owned()
3958 } else {
3959 format!("{process_id}/{}", child_path.join("/"))
3960 }
3961 }
3962
3963 fn adopt_detached_child_processes(
3964 current_process_id: &str,
3965 process: &mut ActiveProcess,
3966 ) -> Vec<(String, ActiveProcess)> {
3967 let mut adopted = Vec::new();
3968 let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
3969 for child_id in child_ids {
3970 let child_process_id = format!("{current_process_id}/{child_id}");
3971 let Some(mut child) = process.child_processes.remove(&child_id) else {
3972 continue;
3973 };
3974 if child.detached {
3975 adopted.push((child_process_id, child));
3976 continue;
3977 }
3978
3979 adopted.extend(Self::adopt_detached_child_processes(
3980 &child_process_id,
3981 &mut child,
3982 ));
3983 process.child_processes.insert(child_id, child);
3984 }
3985 adopted
3986 }
3987
3988 fn child_process_signal_key<'a>(process_id: &'a str, child_path: &[&'a str]) -> &'a str {
3989 child_path.last().copied().unwrap_or(process_id)
3990 }
3991
3992 fn resolve_detached_child_process_path(
3993 vm: &VmState,
3994 detached_process_id: &str,
3995 ) -> Option<(String, Vec<String>)> {
3996 let root_process_id = vm
3997 .active_processes
3998 .keys()
3999 .filter(|candidate| {
4000 detached_process_id == candidate.as_str()
4001 || detached_process_id
4002 .strip_prefix(candidate.as_str())
4003 .is_some_and(|remainder| remainder.starts_with('/'))
4004 })
4005 .max_by_key(|candidate| candidate.len())?
4006 .clone();
4007
4008 let remainder = detached_process_id
4009 .strip_prefix(root_process_id.as_str())
4010 .unwrap_or_default();
4011 if remainder.is_empty() {
4012 return Some((root_process_id, Vec::new()));
4013 }
4014
4015 Some((
4016 root_process_id,
4017 remainder
4018 .trim_start_matches('/')
4019 .split('/')
4020 .map(str::to_owned)
4021 .collect(),
4022 ))
4023 }
4024
4025 fn pump_detached_child_process_events(&mut self, vm_id: &str) -> Result<bool, SidecarError> {
4026 let detached_process_ids = self
4027 .vms
4028 .get(vm_id)
4029 .map(|vm| {
4030 vm.detached_child_processes
4031 .iter()
4032 .cloned()
4033 .collect::<Vec<_>>()
4034 })
4035 .unwrap_or_default();
4036 let mut emitted_any = false;
4037 for detached_process_id in detached_process_ids {
4038 let Some((root_process_id, child_path)) = self
4039 .vms
4040 .get(vm_id)
4041 .and_then(|vm| Self::resolve_detached_child_process_path(vm, &detached_process_id))
4042 else {
4043 if let Some(vm) = self.vms.get_mut(vm_id) {
4044 vm.detached_child_processes.remove(&detached_process_id);
4045 }
4046 continue;
4047 };
4048 if child_path.is_empty() {
4049 loop {
4050 enum ProcessPollResult {
4051 Event(Box<Option<ActiveExecutionEvent>>),
4052 RecoverClosedChannel,
4053 }
4054 let poll_result = {
4055 let Some(vm) = self.vms.get_mut(vm_id) else {
4056 break;
4057 };
4058 let Some(process) = vm.active_processes.get_mut(&root_process_id) else {
4059 break;
4060 };
4061 if let Some(event) = process.pending_execution_events.pop_front() {
4062 ProcessPollResult::Event(Box::new(Some(event)))
4063 } else {
4064 match process.execution.poll_event_blocking(Duration::ZERO) {
4065 Ok(event) => ProcessPollResult::Event(Box::new(event)),
4066 Err(SidecarError::Execution(message))
4067 if (process.runtime == GuestRuntimeKind::JavaScript
4068 && closed_javascript_event_channel(&message))
4069 || (process.runtime == GuestRuntimeKind::Python
4070 && closed_python_event_channel(&message))
4071 || (process.runtime == GuestRuntimeKind::WebAssembly
4072 && closed_wasm_event_channel(&message)) =>
4073 {
4074 ProcessPollResult::RecoverClosedChannel
4075 }
4076 Err(error) => return Err(error),
4077 }
4078 }
4079 };
4080 let event = match poll_result {
4081 ProcessPollResult::Event(event) => *event,
4082 ProcessPollResult::RecoverClosedChannel => {
4083 self.recover_closed_root_runtime_process_event(vm_id, &root_process_id)?
4084 }
4085 };
4086 let Some(event) = event else {
4087 break;
4088 };
4089 let Some((connection_id, session_id)) = self
4090 .vms
4091 .get(vm_id)
4092 .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
4093 else {
4094 break;
4095 };
4096 match event {
4097 ActiveExecutionEvent::Stdout(chunk) => {
4098 self.queue_pending_process_event(ProcessEventEnvelope {
4099 connection_id,
4100 session_id,
4101 vm_id: vm_id.to_owned(),
4102 process_id: detached_process_id.clone(),
4103 event: ActiveExecutionEvent::Stdout(chunk),
4104 })?;
4105 emitted_any = true;
4106 }
4107 ActiveExecutionEvent::Stderr(chunk) => {
4108 self.queue_pending_process_event(ProcessEventEnvelope {
4109 connection_id,
4110 session_id,
4111 vm_id: vm_id.to_owned(),
4112 process_id: detached_process_id.clone(),
4113 event: ActiveExecutionEvent::Stderr(chunk),
4114 })?;
4115 emitted_any = true;
4116 }
4117 ActiveExecutionEvent::Exited(exit_code) => {
4118 if let Some(vm) = self.vms.get_mut(vm_id) {
4119 vm.detached_child_processes.remove(&detached_process_id);
4120 }
4121 self.queue_pending_process_event(ProcessEventEnvelope {
4122 connection_id,
4123 session_id,
4124 vm_id: vm_id.to_owned(),
4125 process_id: detached_process_id.clone(),
4126 event: ActiveExecutionEvent::Exited(exit_code),
4127 })?;
4128 emitted_any = true;
4129 break;
4130 }
4131 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
4132 self.handle_javascript_sync_rpc_request(
4133 vm_id,
4134 &root_process_id,
4135 request,
4136 )?;
4137 }
4138 ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
4139 self.handle_python_vfs_rpc_request(vm_id, &root_process_id, *request)?;
4140 }
4141 ActiveExecutionEvent::SignalState {
4142 signal,
4143 registration,
4144 } => {
4145 if let Some(vm) = self.vms.get_mut(vm_id) {
4146 vm.signal_states
4147 .entry(root_process_id.clone())
4148 .or_default()
4149 .insert(signal, registration);
4150 }
4151 }
4152 }
4153 }
4154 continue;
4155 }
4156
4157 let parent_path = child_path[..child_path.len() - 1]
4158 .iter()
4159 .map(String::as_str)
4160 .collect::<Vec<_>>();
4161 let child_process_id = child_path.last().expect("child path cannot be empty");
4162
4163 loop {
4164 let event = match self.poll_descendant_javascript_child_process(
4165 vm_id,
4166 &root_process_id,
4167 &parent_path,
4168 child_process_id,
4169 0,
4170 ) {
4171 Ok(event) => event,
4172 Err(SidecarError::InvalidState(message))
4173 if message.contains("unknown child process")
4174 || message.contains("unknown child process path") =>
4175 {
4176 if let Some(vm) = self.vms.get_mut(vm_id) {
4177 vm.detached_child_processes.remove(&detached_process_id);
4178 }
4179 break;
4180 }
4181 Err(error) if is_javascript_child_process_gone_error(&error) => {
4182 if let Some(vm) = self.vms.get_mut(vm_id) {
4183 vm.detached_child_processes.remove(&detached_process_id);
4184 }
4185 break;
4186 }
4187 Err(error) => return Err(error),
4188 };
4189
4190 let Some(event_type) = event.get("type").and_then(Value::as_str) else {
4191 break;
4192 };
4193 let Some((connection_id, session_id)) = self
4194 .vms
4195 .get(vm_id)
4196 .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
4197 else {
4198 break;
4199 };
4200
4201 let envelope = match event_type {
4202 "stdout" => Some(ProcessEventEnvelope {
4203 connection_id: connection_id.clone(),
4204 session_id: session_id.clone(),
4205 vm_id: vm_id.to_owned(),
4206 process_id: detached_process_id.clone(),
4207 event: ActiveExecutionEvent::Stdout(javascript_sync_rpc_bytes_arg(
4208 &[event.get("data").cloned().unwrap_or(Value::Null)],
4209 0,
4210 "detached child_process stdout",
4211 )?),
4212 }),
4213 "stderr" => Some(ProcessEventEnvelope {
4214 connection_id: connection_id.clone(),
4215 session_id: session_id.clone(),
4216 vm_id: vm_id.to_owned(),
4217 process_id: detached_process_id.clone(),
4218 event: ActiveExecutionEvent::Stderr(javascript_sync_rpc_bytes_arg(
4219 &[event.get("data").cloned().unwrap_or(Value::Null)],
4220 0,
4221 "detached child_process stderr",
4222 )?),
4223 }),
4224 "exit" => {
4225 if let Some(vm) = self.vms.get_mut(vm_id) {
4226 vm.detached_child_processes.remove(&detached_process_id);
4227 }
4228 Some(ProcessEventEnvelope {
4229 connection_id,
4230 session_id,
4231 vm_id: vm_id.to_owned(),
4232 process_id: detached_process_id.clone(),
4233 event: ActiveExecutionEvent::Exited(
4234 event
4235 .get("exitCode")
4236 .and_then(Value::as_i64)
4237 .map(|value| value as i32)
4238 .unwrap_or(1),
4239 ),
4240 })
4241 }
4242 _ => None,
4243 };
4244
4245 let Some(envelope) = envelope else {
4246 break;
4247 };
4248 self.queue_pending_process_event(envelope)?;
4249 emitted_any = true;
4250
4251 if event_type == "exit" {
4252 break;
4253 }
4254 }
4255 }
4256
4257 Ok(emitted_any)
4258 }
4259 pub(crate) fn drain_queued_descendant_javascript_child_process_events(
4260 &mut self,
4261 vm_id: &str,
4262 process_id: &str,
4263 child_path: &[&str],
4264 ) -> Result<(), SidecarError> {
4265 if child_path.is_empty() {
4266 return Ok(());
4267 }
4268 let target_process_id = Self::child_process_path_label(process_id, child_path);
4269 let mut child_capacity = self
4270 .vms
4271 .get(vm_id)
4272 .and_then(|vm| vm.active_processes.get(process_id))
4273 .and_then(|root| descendant_pending_execution_event_capacity(root, child_path));
4274
4275 let mut deferred = VecDeque::new();
4276 while let Some(envelope) = self.pending_process_events.pop_front() {
4277 if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
4278 if matches!(child_capacity, Some(0)) {
4279 self.pending_process_events.push_front(envelope);
4280 while let Some(deferred_envelope) = deferred.pop_back() {
4281 self.pending_process_events.push_front(deferred_envelope);
4282 }
4283 return Err(process_event_queue_overflow_error());
4284 }
4285 if let Some(vm) = self.vms.get_mut(vm_id) {
4286 if let Some(root) = vm.active_processes.get_mut(process_id) {
4287 if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
4288 child.queue_pending_execution_event(envelope.event)?;
4289 child_capacity = child_capacity.map(|capacity| capacity - 1);
4290 continue;
4291 }
4292 }
4293 }
4294 }
4295 deferred.push_back(envelope);
4296 }
4297 self.pending_process_events = deferred;
4298
4299 let mut queued = Vec::new();
4300 {
4301 let transfer_capacity = self
4302 .pending_process_event_capacity()
4303 .min(child_capacity.unwrap_or(usize::MAX));
4304 let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
4305 SidecarError::InvalidState(String::from("process event receiver unavailable"))
4306 })?;
4307 loop {
4308 if queued.len() >= transfer_capacity {
4309 if receiver.is_empty() {
4310 break;
4311 }
4312 return Err(process_event_queue_overflow_error());
4313 }
4314 match receiver.try_recv() {
4315 Ok(envelope) => queued.push(envelope),
4316 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
4317 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
4318 }
4319 }
4320 }
4321 for envelope in queued {
4322 if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
4323 if let Some(vm) = self.vms.get_mut(vm_id) {
4324 if let Some(root) = vm.active_processes.get_mut(process_id) {
4325 if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
4326 child.queue_pending_execution_event(envelope.event)?;
4327 continue;
4328 }
4329 }
4330 }
4331 }
4332 self.queue_pending_process_event(envelope)?;
4333 }
4334
4335 Ok(())
4336 }
4337
4338 pub(crate) fn handle_execution_event(
4339 &mut self,
4340 vm_id: &str,
4341 process_id: &str,
4342 event: ActiveExecutionEvent,
4343 ) -> Result<Option<EventFrame>, SidecarError> {
4344 let Some(vm) = self.vms.get(vm_id) else {
4345 log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
4346 return Ok(None);
4347 };
4348 if !vm.active_processes.contains_key(process_id) {
4349 log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
4350 return Ok(None);
4351 }
4352 let (connection_id, session_id) = { (vm.connection_id.clone(), vm.session_id.clone()) };
4353 let ownership = OwnershipScope::vm(&connection_id, &session_id, vm_id);
4354
4355 if self.capture_extension_process_output_event(vm_id, process_id, &event) {
4356 return Ok(None);
4357 }
4358
4359 match event {
4360 ActiveExecutionEvent::Stdout(chunk) => Ok(Some(EventFrame::new(
4361 ownership,
4362 EventPayload::ProcessOutput(ProcessOutputEvent {
4363 process_id: process_id.to_owned(),
4364 channel: StreamChannel::Stdout,
4365 chunk,
4366 }),
4367 ))),
4368 ActiveExecutionEvent::Stderr(chunk) => Ok(Some(EventFrame::new(
4369 ownership,
4370 EventPayload::ProcessOutput(ProcessOutputEvent {
4371 process_id: process_id.to_owned(),
4372 channel: StreamChannel::Stderr,
4373 chunk,
4374 }),
4375 ))),
4376 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
4377 self.handle_javascript_sync_rpc_request(vm_id, process_id, request)?;
4378 Ok(None)
4379 }
4380 ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
4381 self.handle_python_vfs_rpc_request(vm_id, process_id, *request)?;
4382 Ok(None)
4383 }
4384 ActiveExecutionEvent::SignalState {
4385 signal,
4386 registration,
4387 } => {
4388 let Some(vm) = self.vms.get_mut(vm_id) else {
4389 return Ok(None);
4390 };
4391 if !vm.active_processes.contains_key(process_id) {
4392 return Ok(None);
4393 }
4394 vm.signal_states
4395 .entry(process_id.to_owned())
4396 .or_default()
4397 .insert(signal, registration);
4398 Ok(None)
4399 }
4400 ActiveExecutionEvent::Exited(exit_code) => {
4401 let became_idle = self
4402 .finish_active_process_exit(vm_id, process_id, exit_code)?
4403 .unwrap_or(false);
4404
4405 if became_idle {
4406 self.bridge.emit_lifecycle(vm_id, LifecycleState::Ready)?;
4407 }
4408
4409 Ok(Some(EventFrame::new(
4410 ownership,
4411 EventPayload::ProcessExited(ProcessExitedEvent {
4412 process_id: process_id.to_owned(),
4413 exit_code,
4414 }),
4415 )))
4416 }
4417 }
4418 }
4419
4420 pub(crate) fn finish_active_process_exit(
4421 &mut self,
4422 vm_id: &str,
4423 process_id: &str,
4424 exit_code: i32,
4425 ) -> Result<Option<bool>, SidecarError> {
4426 let Some(vm) = self.vms.get_mut(vm_id) else {
4427 log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
4428 return Ok(None);
4429 };
4430 if !vm.active_processes.contains_key(process_id) {
4431 log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
4432 return Ok(None);
4433 }
4434
4435 prune_exited_process_snapshots(vm);
4436 let process_table = vm.kernel.list_processes();
4437 let Some(mut process) = vm.active_processes.remove(process_id) else {
4438 return Ok(None);
4439 };
4440 if let Some(info) = process_table.get(&process.kernel_pid) {
4441 vm.exited_process_snapshots
4442 .push_back(ExitedProcessSnapshot {
4443 captured_at: Instant::now(),
4444 process: build_process_snapshot_entry(
4445 process_id,
4446 &process,
4447 info,
4448 Some(exit_code),
4449 ),
4450 });
4451 }
4452 let detached_children = Self::adopt_detached_child_processes(process_id, &mut process);
4453 sync_process_host_writes_to_kernel(vm, &process)?;
4454 terminate_child_process_tree(&mut vm.kernel, &mut process);
4455 process.kernel_handle.finish(exit_code);
4456 let _ = vm.kernel.wait_and_reap(process.kernel_pid);
4457 vm.signal_states.remove(process_id);
4458 for (detached_process_id, detached_child) in detached_children {
4459 vm.detached_child_processes
4460 .insert(detached_process_id.clone());
4461 vm.active_processes
4462 .insert(detached_process_id, detached_child);
4463 }
4464 let became_idle = vm.active_processes.is_empty();
4465 self.prune_extension_process_resource(process_id);
4466
4467 Ok(Some(became_idle))
4468 }
4469
4470 pub(crate) fn drain_process_events_blocking_with_limit(
4471 &mut self,
4472 vm_id: &str,
4473 process_id: &str,
4474 max_events: usize,
4475 ) -> Result<Vec<ActiveExecutionEvent>, SidecarError> {
4476 let mut events = Vec::new();
4477 if max_events == 0 {
4478 return Ok(events);
4479 }
4480 let mut deadline = Instant::now() + Duration::from_millis(150);
4481
4482 loop {
4483 if events.len() >= max_events {
4484 break;
4485 }
4486 let event = {
4487 let Some(vm) = self.vms.get_mut(vm_id) else {
4488 break;
4489 };
4490 let Some(process) = vm.active_processes.get_mut(process_id) else {
4491 break;
4492 };
4493 if let Some(event) = process.pending_execution_events.pop_front() {
4494 Some(event)
4495 } else {
4496 match process.execution.poll_event_blocking(Duration::ZERO) {
4497 Ok(event) => event,
4498 Err(SidecarError::Execution(_)) => None,
4499 Err(other) => return Err(other),
4500 }
4501 }
4502 };
4503
4504 let Some(event) = event else {
4505 if Instant::now() >= deadline {
4506 break;
4507 }
4508 let blocking_wait = deadline.saturating_duration_since(Instant::now());
4509 if blocking_wait.is_zero() {
4510 break;
4511 }
4512 if events.len() >= max_events {
4513 break;
4514 }
4515 let delayed_event = {
4516 let Some(vm) = self.vms.get_mut(vm_id) else {
4517 break;
4518 };
4519 let Some(process) = vm.active_processes.get_mut(process_id) else {
4520 break;
4521 };
4522 if let Some(event) = process.pending_execution_events.pop_front() {
4523 Some(event)
4524 } else {
4525 match process.execution.poll_event_blocking(blocking_wait) {
4526 Ok(event) => event,
4527 Err(SidecarError::Execution(_)) => None,
4528 Err(other) => return Err(other),
4529 }
4530 }
4531 };
4532 let Some(event) = delayed_event else {
4533 break;
4534 };
4535 events.push(event);
4536 deadline = Instant::now() + Duration::from_millis(150);
4537 continue;
4538 };
4539 events.push(event);
4540 deadline = Instant::now() + Duration::from_millis(150);
4541 }
4542
4543 Ok(events)
4544 }
4545
4546 pub(crate) fn handle_python_vfs_rpc_request(
4547 &mut self,
4548 vm_id: &str,
4549 process_id: &str,
4550 request: PythonVfsRpcRequest,
4551 ) -> Result<(), SidecarError> {
4552 match request.method {
4553 PythonVfsRpcMethod::Read
4554 | PythonVfsRpcMethod::Write
4555 | PythonVfsRpcMethod::Stat
4556 | PythonVfsRpcMethod::ReadDir
4557 | PythonVfsRpcMethod::Mkdir => {
4558 filesystem_handle_python_vfs_rpc_request(self, vm_id, process_id, request)
4559 }
4560 PythonVfsRpcMethod::HttpRequest => {
4561 self.handle_python_http_rpc_request(vm_id, process_id, request)
4562 }
4563 PythonVfsRpcMethod::DnsLookup => {
4564 self.handle_python_dns_rpc_request(vm_id, process_id, request)
4565 }
4566 PythonVfsRpcMethod::SubprocessRun => {
4567 self.handle_python_subprocess_rpc_request(vm_id, process_id, request)
4568 }
4569 }
4570 }
4571
4572 fn handle_python_http_rpc_request(
4573 &mut self,
4574 vm_id: &str,
4575 process_id: &str,
4576 request: PythonVfsRpcRequest,
4577 ) -> Result<(), SidecarError> {
4578 let Some(vm) = self.vms.get(vm_id) else {
4579 return Ok(());
4580 };
4581 if !vm.active_processes.contains_key(process_id) {
4582 return Ok(());
4583 }
4584 let response = (|| {
4585 let url_text = request.url.as_deref().ok_or_else(|| {
4586 SidecarError::InvalidState(String::from("python httpRequest requires a url"))
4587 })?;
4588 let url = Url::parse(url_text)
4589 .map_err(|error| SidecarError::Execution(format!("ERR_INVALID_URL: {error}")))?;
4590 let host = url.host_str().ok_or_else(|| {
4591 SidecarError::Execution(String::from("ERR_INVALID_URL: missing host"))
4592 })?;
4593 let port = url.port_or_known_default().ok_or_else(|| {
4594 SidecarError::Execution(String::from("ERR_INVALID_URL: missing port"))
4595 })?;
4596 self.bridge.require_network_access(
4597 vm_id,
4598 NetworkOperation::Http,
4599 format_tcp_resource(host, port),
4600 )?;
4601 let pinned_addresses = if let Ok(literal_ip) = host.parse::<IpAddr>() {
4608 filter_dns_safe_ip_addrs(vec![literal_ip], host)?
4609 } else {
4610 filter_dns_safe_ip_addrs(
4611 resolve_dns_ip_addrs(
4612 &self.bridge,
4613 &vm.kernel,
4614 vm_id,
4615 &vm.dns,
4616 host,
4617 DnsLookupPolicy::SkipPermissions,
4618 )?,
4619 host,
4620 )?
4621 };
4622 let mut headers = BTreeMap::new();
4623 for (name, value) in &request.headers {
4624 headers.insert(name.clone(), Value::String(value.clone()));
4625 }
4626 let options = JavascriptHttpRequestOptions {
4627 method: Some(
4628 request
4629 .http_method
4630 .clone()
4631 .unwrap_or_else(|| String::from("GET")),
4632 ),
4633 headers,
4634 body: request.body_base64.as_deref().map(|body| {
4635 String::from_utf8(
4636 base64::engine::general_purpose::STANDARD
4637 .decode(body)
4638 .unwrap_or_default(),
4639 )
4640 .unwrap_or_default()
4641 }),
4642 reject_unauthorized: None,
4643 };
4644 let headers =
4645 parse_http_header_collection(&options.headers, "python httpRequest headers")?;
4646 let response =
4647 issue_outbound_http_request(&url, &options, &headers, &pinned_addresses)?;
4648 let payload_json = response.as_str().ok_or_else(|| {
4649 SidecarError::Execution(String::from(
4650 "python httpRequest returned a non-string response payload",
4651 ))
4652 })?;
4653 let payload: Value = serde_json::from_str(payload_json).map_err(|error| {
4654 SidecarError::Execution(format!(
4655 "python httpRequest response must be valid JSON: {error}"
4656 ))
4657 })?;
4658 let header_map = payload
4659 .get("headers")
4660 .and_then(Value::as_array)
4661 .map(|entries| {
4662 let mut normalized = BTreeMap::<String, Vec<String>>::new();
4663 for entry in entries {
4664 let Some(pair) = entry.as_array() else {
4665 continue;
4666 };
4667 let Some(name) = pair.first().and_then(Value::as_str) else {
4668 continue;
4669 };
4670 let Some(value) = pair.get(1).and_then(Value::as_str) else {
4671 continue;
4672 };
4673 normalized
4674 .entry(name.to_owned())
4675 .or_default()
4676 .push(value.to_owned());
4677 }
4678 normalized
4679 })
4680 .unwrap_or_default();
4681 Ok(PythonVfsRpcResponsePayload::Http {
4682 status: payload
4683 .get("status")
4684 .and_then(Value::as_u64)
4685 .map(|value| value as u16)
4686 .unwrap_or_default(),
4687 reason: payload
4688 .get("statusText")
4689 .and_then(Value::as_str)
4690 .unwrap_or_default()
4691 .to_owned(),
4692 url: payload
4693 .get("url")
4694 .and_then(Value::as_str)
4695 .unwrap_or(url_text)
4696 .to_owned(),
4697 headers: header_map,
4698 body_base64: payload
4699 .get("body")
4700 .and_then(Value::as_str)
4701 .unwrap_or_default()
4702 .to_owned(),
4703 })
4704 })();
4705
4706 self.respond_python_rpc(vm_id, process_id, request.id, response)
4707 }
4708
4709 fn handle_python_dns_rpc_request(
4710 &mut self,
4711 vm_id: &str,
4712 process_id: &str,
4713 request: PythonVfsRpcRequest,
4714 ) -> Result<(), SidecarError> {
4715 let Some(vm) = self.vms.get(vm_id) else {
4716 return Ok(());
4717 };
4718 if !vm.active_processes.contains_key(process_id) {
4719 return Ok(());
4720 }
4721 let response = (|| {
4722 let hostname = request.hostname.as_deref().ok_or_else(|| {
4723 SidecarError::InvalidState(String::from("python dnsLookup requires a hostname"))
4724 })?;
4725 let mut addresses = filter_dns_safe_ip_addrs(
4726 resolve_dns_ip_addrs(
4727 &self.bridge,
4728 &vm.kernel,
4729 vm_id,
4730 &vm.dns,
4731 hostname,
4732 DnsLookupPolicy::CheckPermissions,
4733 )?,
4734 hostname,
4735 )?;
4736 if let Some(family) = request.family {
4737 addresses.retain(|address| {
4738 matches!((family, address), (4, IpAddr::V4(_)) | (6, IpAddr::V6(_)))
4739 });
4740 }
4741 Ok(PythonVfsRpcResponsePayload::DnsLookup {
4742 addresses: addresses
4743 .into_iter()
4744 .map(|address| address.to_string())
4745 .collect(),
4746 })
4747 })();
4748
4749 self.respond_python_rpc(vm_id, process_id, request.id, response)
4750 }
4751
4752 fn handle_python_subprocess_rpc_request(
4753 &mut self,
4754 vm_id: &str,
4755 process_id: &str,
4756 request: PythonVfsRpcRequest,
4757 ) -> Result<(), SidecarError> {
4758 let command = request.command.clone().ok_or_else(|| {
4759 SidecarError::InvalidState(String::from("python subprocessRun requires a command"))
4760 })?;
4761 let (internal_bootstrap_env, cwd) = {
4762 let Some(vm) = self.vms.get(vm_id) else {
4763 return Ok(());
4764 };
4765 let Some(process) = vm.active_processes.get(process_id) else {
4766 return Ok(());
4767 };
4768 let virtual_home = guest_virtual_home(vm);
4769 let cwd = request.cwd.clone().or_else(|| {
4770 guest_runtime_path_for_host_path(
4771 &vm.guest_env,
4772 &virtual_home,
4773 &vm.host_cwd,
4774 &process.host_cwd.to_string_lossy(),
4775 )
4776 });
4777 (
4778 sanitize_javascript_child_process_internal_bootstrap_env(&vm.guest_env),
4779 cwd,
4780 )
4781 };
4782 let response = self
4783 .spawn_javascript_child_process_sync(
4784 vm_id,
4785 process_id,
4786 JavascriptChildProcessSpawnRequest {
4787 command,
4788 args: request.args.clone(),
4789 options: JavascriptChildProcessSpawnOptions {
4790 cwd,
4791 env: request.env.clone(),
4792 input: None,
4793 internal_bootstrap_env,
4794 shell: request.shell,
4795 detached: false,
4796 stdio: vec![
4797 String::from("pipe"),
4798 String::from("pipe"),
4799 String::from("pipe"),
4800 ],
4801 timeout: None,
4802 kill_signal: None,
4803 },
4804 },
4805 request.max_buffer,
4806 )
4807 .map(|payload| PythonVfsRpcResponsePayload::SubprocessRun {
4808 exit_code: payload
4809 .get("code")
4810 .and_then(Value::as_i64)
4811 .map(|value| value as i32)
4812 .unwrap_or(1),
4813 stdout: payload
4814 .get("stdout")
4815 .and_then(Value::as_str)
4816 .unwrap_or_default()
4817 .to_owned(),
4818 stderr: payload
4819 .get("stderr")
4820 .and_then(Value::as_str)
4821 .unwrap_or_default()
4822 .to_owned(),
4823 max_buffer_exceeded: payload
4824 .get("maxBufferExceeded")
4825 .and_then(Value::as_bool)
4826 .unwrap_or(false),
4827 });
4828
4829 self.respond_python_rpc(vm_id, process_id, request.id, response)
4830 }
4831
4832 fn respond_python_rpc(
4833 &mut self,
4834 vm_id: &str,
4835 process_id: &str,
4836 request_id: u64,
4837 response: Result<PythonVfsRpcResponsePayload, SidecarError>,
4838 ) -> Result<(), SidecarError> {
4839 let Some(vm) = self.vms.get_mut(vm_id) else {
4840 return Ok(());
4841 };
4842 let Some(process) = vm.active_processes.get_mut(process_id) else {
4843 return Ok(());
4844 };
4845 let result = match response {
4846 Ok(payload) => process
4847 .execution
4848 .respond_python_vfs_rpc_success(request_id, payload),
4849 Err(error) => process.execution.respond_python_vfs_rpc_error(
4850 request_id,
4851 "ERR_AGENTOS_PYTHON_VFS_RPC",
4852 error.to_string(),
4853 ),
4854 };
4855 match result {
4856 Ok(()) => Ok(()),
4857 Err(error) if is_broken_pipe_error(&error) => Ok(()),
4858 Err(error) => Err(error),
4859 }
4860 }
4861
4862 pub(crate) fn resolve_javascript_child_process_execution(
4863 &self,
4864 vm: &VmState,
4865 parent_env: &BTreeMap<String, String>,
4866 parent_guest_cwd: &str,
4867 parent_host_cwd: &Path,
4868 request: &JavascriptChildProcessSpawnRequest,
4869 ) -> Result<ResolvedChildProcessExecution, SidecarError> {
4870 let mut runtime_env = parent_env.clone();
4871 runtime_env.extend(request.options.internal_bootstrap_env.clone());
4872 let (guest_cwd, host_cwd_override) = request
4873 .options
4874 .cwd
4875 .as_deref()
4876 .map(|cwd| {
4877 let normalized_parent_host_cwd = normalize_host_path(parent_host_cwd);
4878 let requested_host_cwd = normalize_host_path(Path::new(cwd));
4879 if path_is_within_root(&requested_host_cwd, &normalized_parent_host_cwd) {
4880 let relative = requested_host_cwd
4881 .strip_prefix(&normalized_parent_host_cwd)
4882 .unwrap_or_else(|_| Path::new(""));
4883 let relative = relative.to_string_lossy().replace('\\', "/");
4884 let guest_cwd = if relative.is_empty() {
4885 parent_guest_cwd.to_owned()
4886 } else {
4887 normalize_path(&format!("{parent_guest_cwd}/{relative}"))
4888 };
4889 (guest_cwd, Some(requested_host_cwd))
4890 } else if Path::new(cwd).is_relative() {
4891 (
4892 normalize_path(&format!("{parent_guest_cwd}/{cwd}")),
4893 Some(normalize_host_path(&parent_host_cwd.join(cwd))),
4894 )
4895 } else {
4896 (normalize_path(cwd), None)
4897 }
4898 })
4899 .unwrap_or_else(|| (parent_guest_cwd.to_owned(), None));
4900 let inherited_host_cwd = (host_cwd_override.is_none() && guest_cwd == parent_guest_cwd)
4901 .then(|| normalize_host_path(parent_host_cwd));
4902 let host_cwd = host_cwd_override
4903 .or(inherited_host_cwd)
4904 .or_else(|| {
4905 host_runtime_path_for_guest_path_with_env(
4906 vm,
4907 &runtime_env,
4908 &guest_cwd,
4909 parent_host_cwd,
4910 )
4911 })
4912 .unwrap_or_else(|| {
4913 let candidate = PathBuf::from(&guest_cwd);
4914 if guest_cwd == parent_guest_cwd {
4915 normalize_host_path(parent_host_cwd)
4916 } else if candidate.is_absolute() {
4917 shadow_path_for_guest(vm, &guest_cwd)
4918 } else {
4919 vm.host_cwd.clone()
4920 }
4921 });
4922 let mut env = parent_env.clone();
4923 env.extend(request.options.env.clone());
4924 env.remove("AGENTOS_GUEST_ENTRYPOINT");
4927 env.remove("AGENTOS_NODE_EVAL");
4928
4929 let (command, process_args) = if request.options.shell {
4930 let tokens = tokenize_shell_free_command(&request.command);
4931 let requires_shell = command_requires_shell(&request.command)
4932 || tokens.first().is_some_and(|command| {
4933 is_posix_shell_builtin(command) || shell_first_token_requires_shell(command)
4934 });
4935 if requires_shell {
4936 if !vm.command_guest_paths.contains_key("sh") {
4937 return Err(SidecarError::InvalidState(format!(
4938 "shell-mode child_process command requires /bin/sh, which is not \
4939 installed in this VM (install a software package that provides sh, \
4940 for example @secure-exec/coreutils): {}",
4941 request.command
4942 )));
4943 }
4944 (
4945 String::from("sh"),
4946 vec![String::from("-c"), request.command.clone()],
4947 )
4948 } else {
4949 let Some((command, args)) = tokens.split_first() else {
4950 return Err(SidecarError::InvalidState(String::from(
4951 "child_process shell command must not be empty",
4952 )));
4953 };
4954 (command.clone(), args.to_vec())
4955 }
4956 } else {
4957 (request.command.clone(), request.args.clone())
4958 };
4959 let process_args = apply_shell_cwd_prefix(&command, process_args, &guest_cwd);
4960 if is_tool_command(vm, &command) {
4961 let command = normalized_tool_command_name(&command).unwrap_or(command);
4962 return Ok(ResolvedChildProcessExecution {
4963 command: command.clone(),
4964 process_args: std::iter::once(command.clone())
4965 .chain(process_args.iter().cloned())
4966 .collect(),
4967 runtime: GuestRuntimeKind::JavaScript,
4968 entrypoint: command,
4969 execution_args: process_args,
4970 env,
4971 guest_cwd,
4972 host_cwd,
4973 wasm_permission_tier: None,
4974 tool_command: true,
4975 });
4976 }
4977
4978 if is_path_like_specifier(&command)
4979 && matches!(
4980 Path::new(&command).extension().and_then(|ext| ext.to_str()),
4981 Some("js" | "mjs" | "cjs" | "ts" | "mts" | "cts")
4982 )
4983 {
4984 let guest_entrypoint = if command.starts_with('/') {
4985 normalize_path(&command)
4986 } else if command.starts_with("file:") {
4987 normalize_path(command.trim_start_matches("file:"))
4988 } else {
4989 normalize_path(&format!("{guest_cwd}/{command}"))
4990 };
4991 let host_entrypoint = if command.starts_with("./") || command.starts_with("../") {
4992 normalize_host_path(&host_cwd.join(&command))
4993 } else {
4994 host_runtime_path_for_guest_path_with_env(
4995 vm,
4996 &runtime_env,
4997 &guest_entrypoint,
4998 parent_host_cwd,
4999 )
5000 .unwrap_or_else(|| {
5001 let candidate = PathBuf::from(&guest_entrypoint);
5002 if candidate.is_absolute() {
5003 candidate
5004 } else {
5005 host_cwd.join(&guest_entrypoint)
5006 }
5007 })
5008 };
5009 env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
5010 let guest_entrypoint = env.get("AGENTOS_GUEST_ENTRYPOINT").cloned();
5011 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
5012
5013 return Ok(ResolvedChildProcessExecution {
5014 command: command.clone(),
5015 process_args: std::iter::once(command)
5016 .chain(process_args.iter().cloned())
5017 .collect(),
5018 runtime: GuestRuntimeKind::JavaScript,
5019 entrypoint: host_entrypoint.to_string_lossy().into_owned(),
5020 execution_args: process_args,
5021 env,
5022 guest_cwd,
5023 host_cwd,
5024 wasm_permission_tier: None,
5025 tool_command: false,
5026 });
5027 }
5028
5029 if is_node_runtime_command(&command) {
5030 if let Some(cli) = resolve_host_node_cli_entrypoint(&command) {
5031 env.insert(
5032 String::from("AGENTOS_NODE_EVAL"),
5033 build_host_node_cli_eval(&cli),
5034 );
5035 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5036 add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
5037 add_runtime_host_access_path(
5038 &mut env,
5039 "AGENTOS_EXTRA_FS_READ_PATHS",
5040 &cli.package_root,
5041 true,
5042 );
5043
5044 return Ok(ResolvedChildProcessExecution {
5045 command: command.clone(),
5046 process_args: std::iter::once(command.clone())
5047 .chain(process_args.iter().cloned())
5048 .collect(),
5049 runtime: GuestRuntimeKind::JavaScript,
5050 entrypoint: String::from("-e"),
5051 execution_args: std::iter::once(cli.guest_entrypoint.clone())
5052 .chain(process_args.iter().cloned())
5053 .collect(),
5054 env,
5055 guest_cwd,
5056 host_cwd,
5057 wasm_permission_tier: None,
5058 tool_command: false,
5059 });
5060 }
5061
5062 if process_args.is_empty() {
5063 env.insert(String::from("AGENTOS_NODE_EVAL"), String::new());
5064 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5065
5066 return Ok(ResolvedChildProcessExecution {
5067 command: command.clone(),
5068 process_args: vec![command.clone()],
5069 runtime: GuestRuntimeKind::JavaScript,
5070 entrypoint: String::from("-e"),
5071 execution_args: Vec::new(),
5072 env,
5073 guest_cwd,
5074 host_cwd,
5075 wasm_permission_tier: None,
5076 tool_command: false,
5077 });
5078 }
5079
5080 if let Some((entrypoint, execution_args)) =
5081 resolve_special_node_cli_invocation(&process_args, &mut env)
5082 {
5083 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5084
5085 return Ok(ResolvedChildProcessExecution {
5086 command: command.clone(),
5087 process_args: std::iter::once(command.clone())
5088 .chain(process_args.iter().cloned())
5089 .collect(),
5090 runtime: GuestRuntimeKind::JavaScript,
5091 entrypoint,
5092 execution_args,
5093 env,
5094 guest_cwd,
5095 host_cwd,
5096 wasm_permission_tier: None,
5097 tool_command: false,
5098 });
5099 }
5100
5101 let Some(entrypoint_specifier) = process_args.first() else {
5102 return Err(SidecarError::InvalidState(format!(
5103 "{command} child_process spawn requires an entrypoint"
5104 )));
5105 };
5106
5107 let (entrypoint, execution_args) = if is_path_like_specifier(entrypoint_specifier) {
5108 let guest_entrypoint = if entrypoint_specifier.starts_with('/') {
5109 normalize_path(entrypoint_specifier)
5110 } else if entrypoint_specifier.starts_with("file:") {
5111 normalize_path(entrypoint_specifier.trim_start_matches("file:"))
5112 } else {
5113 normalize_path(&format!("{guest_cwd}/{entrypoint_specifier}"))
5114 };
5115 let host_entrypoint = if entrypoint_specifier.starts_with("./")
5116 || entrypoint_specifier.starts_with("../")
5117 {
5118 normalize_host_path(&host_cwd.join(entrypoint_specifier))
5119 } else {
5120 host_runtime_path_for_guest_path_with_env(
5121 vm,
5122 &runtime_env,
5123 &guest_entrypoint,
5124 parent_host_cwd,
5125 )
5126 .unwrap_or_else(|| {
5127 let candidate = PathBuf::from(&guest_entrypoint);
5128 if candidate.is_absolute() {
5129 candidate
5130 } else {
5131 host_cwd.join(&guest_entrypoint)
5132 }
5133 })
5134 };
5135 env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
5136 (
5137 host_entrypoint.to_string_lossy().into_owned(),
5138 process_args.iter().skip(1).cloned().collect(),
5139 )
5140 } else {
5141 (
5142 entrypoint_specifier.clone(),
5143 process_args.iter().skip(1).cloned().collect(),
5144 )
5145 };
5146 let guest_entrypoint = env.get("AGENTOS_GUEST_ENTRYPOINT").cloned();
5147 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
5148
5149 return Ok(ResolvedChildProcessExecution {
5150 command: command.clone(),
5151 process_args: std::iter::once(command)
5152 .chain(process_args.iter().cloned())
5153 .collect(),
5154 runtime: GuestRuntimeKind::JavaScript,
5155 entrypoint,
5156 execution_args,
5157 env,
5158 guest_cwd,
5159 host_cwd,
5160 wasm_permission_tier: None,
5161 tool_command: false,
5162 });
5163 }
5164
5165 if command == PYTHON_COMMAND {
5166 return Err(SidecarError::InvalidState(String::from(
5167 "nested python child_process execution is not supported yet",
5168 )));
5169 }
5170
5171 let guest_entrypoint = resolve_guest_command_entrypoint(
5172 vm,
5173 &guest_cwd,
5174 &command,
5175 env.get("PATH").map(String::as_str),
5176 )
5177 .ok_or_else(|| SidecarError::InvalidState(format!("command not found: {command}")))?;
5178 let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
5179 let wasm_permission_tier = vm.command_permissions.get(&command).copied().or_else(|| {
5180 Path::new(&guest_entrypoint)
5181 .file_name()
5182 .and_then(|name| name.to_str())
5183 .and_then(|name| vm.command_permissions.get(name).copied())
5184 });
5185 if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
5186 resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
5187 {
5188 prepare_guest_runtime_env(
5189 vm,
5190 &mut env,
5191 &guest_cwd,
5192 &host_cwd,
5193 Some(javascript_guest_entrypoint),
5194 )?;
5195
5196 return Ok(ResolvedChildProcessExecution {
5197 command: command.clone(),
5198 process_args: std::iter::once(command)
5199 .chain(process_args.iter().cloned())
5200 .collect(),
5201 runtime: GuestRuntimeKind::JavaScript,
5202 entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
5203 execution_args: process_args,
5204 env,
5205 guest_cwd,
5206 host_cwd,
5207 wasm_permission_tier: None,
5208 tool_command: false,
5209 });
5210 }
5211 prepare_guest_runtime_env(
5212 vm,
5213 &mut env,
5214 &guest_cwd,
5215 &host_cwd,
5216 Some(guest_entrypoint.clone()),
5217 )?;
5218
5219 Ok(ResolvedChildProcessExecution {
5220 command: command.clone(),
5221 process_args: std::iter::once(command)
5222 .chain(process_args.iter().cloned())
5223 .collect(),
5224 runtime: GuestRuntimeKind::WebAssembly,
5225 entrypoint: host_entrypoint.to_string_lossy().into_owned(),
5226 execution_args: process_args,
5227 env,
5228 guest_cwd,
5229 host_cwd,
5230 wasm_permission_tier,
5231 tool_command: false,
5232 })
5233 }
5234
5235 pub(crate) fn spawn_javascript_child_process(
5236 &mut self,
5237 vm_id: &str,
5238 process_id: &str,
5239 request: JavascriptChildProcessSpawnRequest,
5240 ) -> Result<Value, SidecarError> {
5241 let resolved = {
5242 let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
5243 let parent = vm
5244 .active_processes
5245 .get(process_id)
5246 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5247 self.resolve_javascript_child_process_execution(
5248 vm,
5249 &parent.env,
5250 &parent.guest_cwd,
5251 &parent.host_cwd,
5252 &request,
5253 )?
5254 };
5255 let (parent_kernel_pid, child_process_id) = {
5256 let vm = self
5257 .vms
5258 .get_mut(vm_id)
5259 .ok_or_else(|| missing_vm_error(vm_id))?;
5260 let process = vm
5261 .active_processes
5262 .get_mut(process_id)
5263 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5264 (process.kernel_pid, process.allocate_child_process_id())
5265 };
5266 let sidecar_requests = self.sidecar_requests.clone();
5267 let vm = self
5268 .vms
5269 .get_mut(vm_id)
5270 .ok_or_else(|| missing_vm_error(vm_id))?;
5271 let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
5272 .tool_command
5273 {
5274 let tool_resolution = resolve_tool_command(
5275 vm,
5276 &resolved.command,
5277 &resolved.execution_args,
5278 Some(&resolved.guest_cwd),
5279 )?
5280 .ok_or_else(|| {
5281 SidecarError::InvalidState(format!(
5282 "tool command no longer resolves: {}",
5283 resolved.command
5284 ))
5285 })?;
5286 let kernel_handle = vm
5287 .kernel
5288 .create_virtual_process(
5289 EXECUTION_DRIVER_NAME,
5290 TOOL_DRIVER_NAME,
5291 &resolved.command,
5292 resolved.process_args.clone(),
5293 VirtualProcessOptions {
5294 parent_pid: Some(parent_kernel_pid),
5295 env: resolved.env.clone(),
5296 cwd: Some(resolved.guest_cwd.clone()),
5297 },
5298 )
5299 .map_err(kernel_error)?;
5300 let kernel_pid = kernel_handle.pid();
5301 let tool_execution = ToolExecution::default();
5302 let cancelled = tool_execution.cancelled.clone();
5303 let pending_events = tool_execution.pending_events.clone();
5304 let events_overflowed = tool_execution.events_overflowed.clone();
5305 spawn_tool_process_events(ToolProcessEventRequest {
5306 sidecar_requests: sidecar_requests.clone(),
5307 connection_id: vm.connection_id.clone(),
5308 session_id: vm.session_id.clone(),
5309 vm_id: vm_id.to_owned(),
5310 tool_resolution,
5311 cancelled,
5312 pending_events,
5313 events_overflowed,
5314 });
5315 (
5316 kernel_pid,
5317 kernel_handle,
5318 ActiveExecution::Tool(tool_execution),
5319 None,
5320 )
5321 } else {
5322 let kernel_command = match resolved.runtime {
5323 GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
5324 GuestRuntimeKind::WebAssembly => WASM_COMMAND,
5325 GuestRuntimeKind::Python => {
5326 unreachable!("python child_process execution is rejected")
5327 }
5328 };
5329 let kernel_handle = vm
5330 .kernel
5331 .spawn_process(
5332 kernel_command,
5333 resolved.process_args.clone(),
5334 SpawnOptions {
5335 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
5336 parent_pid: Some(parent_kernel_pid),
5337 env: resolved.env.clone(),
5338 cwd: Some(resolved.guest_cwd.clone()),
5339 },
5340 )
5341 .map_err(kernel_error)?;
5342 let kernel_pid = kernel_handle.pid();
5343 if request.options.detached {
5344 vm.kernel
5345 .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
5346 .map_err(kernel_error)?;
5347 }
5348 let mut execution_env = resolved.env.clone();
5349 execution_env.insert(
5350 String::from(EXECUTION_SANDBOX_ROOT_ENV),
5351 normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
5352 );
5353
5354 let execution = match resolved.runtime {
5355 GuestRuntimeKind::JavaScript => {
5356 execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
5357 &request.options.internal_bootstrap_env,
5358 ));
5359 execution_env.insert(
5360 String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
5361 String::from("1"),
5362 );
5363 let context =
5364 self.javascript_engine
5365 .create_context(CreateJavascriptContextRequest {
5366 vm_id: vm_id.to_owned(),
5367 bootstrap_module: None,
5368 compile_cache_root: Some(
5369 self.cache_root.join("node-compile-cache"),
5370 ),
5371 });
5372 let inline_code = load_javascript_entrypoint_source(
5373 vm,
5374 &resolved.host_cwd,
5375 &resolved.entrypoint,
5376 &execution_env,
5377 );
5378 prepare_javascript_shadow(vm, &resolved)?;
5379
5380 let module_reader = build_module_reader(vm, &resolved)
5381 .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
5382 let execution = self
5383 .javascript_engine
5384 .start_execution_with_module_reader(
5385 StartJavascriptExecutionRequest {
5386 guest_runtime: guest_runtime_identity(
5387 vm,
5388 Some(u64::from(kernel_pid)),
5389 Some(u64::from(parent_kernel_pid)),
5390 ),
5391 vm_id: vm_id.to_owned(),
5392 context_id: context.context_id,
5393 argv: std::iter::once(resolved.entrypoint.clone())
5394 .chain(resolved.execution_args.clone())
5395 .collect(),
5396 env: execution_env,
5397 cwd: resolved.host_cwd.clone(),
5398 limits: javascript_execution_limits(vm),
5399 inline_code,
5400 },
5401 module_reader,
5402 )
5403 .map_err(javascript_error)?;
5404 ActiveExecution::Javascript(execution)
5405 }
5406 GuestRuntimeKind::WebAssembly => {
5407 execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
5408 let wasm_limits = wasm_execution_limits(vm);
5409 let wasm_guest_runtime = guest_runtime_identity(
5410 vm,
5411 Some(u64::from(kernel_pid)),
5412 Some(u64::from(parent_kernel_pid)),
5413 );
5414 let context = self.wasm_engine.create_context(CreateWasmContextRequest {
5415 vm_id: vm_id.to_owned(),
5416 module_path: Some(resolved.entrypoint.clone()),
5417 });
5418 let execution = self
5419 .wasm_engine
5420 .start_execution(StartWasmExecutionRequest {
5421 vm_id: vm_id.to_owned(),
5422 context_id: context.context_id,
5423 argv: resolved.process_args.clone(),
5424 env: execution_env,
5425 cwd: resolved.host_cwd.clone(),
5426 permission_tier: execution_wasm_permission_tier(
5427 resolved
5428 .wasm_permission_tier
5429 .unwrap_or(WasmPermissionTier::Full),
5430 ),
5431 limits: wasm_limits,
5432 guest_runtime: wasm_guest_runtime,
5433 })
5434 .map_err(wasm_error)?;
5435 ActiveExecution::Wasm(Box::new(execution))
5436 }
5437 GuestRuntimeKind::Python => {
5438 unreachable!("python child_process execution is rejected")
5439 }
5440 };
5441 let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
5442 "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5443 "ignore" => {
5444 vm.kernel
5445 .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
5446 .map_err(kernel_error)?;
5447 None
5448 }
5449 "inherit" => None,
5450 _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5451 };
5452 (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
5453 };
5454
5455 let process = vm
5456 .active_processes
5457 .get_mut(process_id)
5458 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5459 process.child_processes.insert(
5460 child_process_id.clone(),
5461 ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
5462 .with_detached(request.options.detached)
5463 .with_guest_cwd(resolved.guest_cwd.clone())
5464 .with_env(resolved.env.clone())
5465 .with_host_cwd(resolved.host_cwd.clone()),
5466 );
5467 if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
5468 process
5469 .child_processes
5470 .get_mut(&child_process_id)
5471 .ok_or_else(|| {
5472 SidecarError::InvalidState(format!(
5473 "child process {child_process_id} disappeared during spawn"
5474 ))
5475 })?
5476 .kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
5477 }
5478 Ok(json!({
5479 "childId": child_process_id,
5480 "pid": kernel_pid,
5481 "command": resolved.command,
5482 "args": resolved.process_args,
5483 }))
5484 }
5485
5486 pub(crate) fn spawn_javascript_child_process_sync(
5487 &mut self,
5488 vm_id: &str,
5489 process_id: &str,
5490 request: JavascriptChildProcessSpawnRequest,
5491 max_buffer: Option<usize>,
5492 ) -> Result<Value, SidecarError> {
5493 let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
5494 let timeout_deadline = request
5495 .options
5496 .timeout
5497 .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
5498 let timeout_signal = request
5499 .options
5500 .kill_signal
5501 .clone()
5502 .unwrap_or_else(|| String::from("SIGTERM"));
5503 let spawned = self.spawn_javascript_child_process(vm_id, process_id, request)?;
5504 let child_process_id = spawned
5505 .get("childId")
5506 .and_then(Value::as_str)
5507 .ok_or_else(|| {
5508 SidecarError::InvalidState(String::from(
5509 "child_process.spawn_sync response is missing childId",
5510 ))
5511 })?
5512 .to_owned();
5513
5514 if let Some(input) = sync_input.as_deref() {
5515 self.write_javascript_child_process_stdin(vm_id, process_id, &child_process_id, input)?;
5516 }
5517 self.close_javascript_child_process_stdin(vm_id, process_id, &child_process_id)?;
5518
5519 let max_buffer = max_buffer.unwrap_or(1024 * 1024);
5520 let mut stdout = Vec::new();
5521 let mut stderr = Vec::new();
5522 let mut max_buffer_exceeded = false;
5523 let mut kill_sent = false;
5524 let mut timed_out = false;
5525
5526 let exit_code = loop {
5527 let wait_ms = if let Some(deadline) = timeout_deadline {
5528 let now = Instant::now();
5529 if now >= deadline {
5530 if !kill_sent {
5531 timed_out = true;
5532 self.kill_javascript_child_process(
5533 vm_id,
5534 process_id,
5535 &child_process_id,
5536 &timeout_signal,
5537 )?;
5538 kill_sent = true;
5539 }
5540 0
5541 } else {
5542 u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
5543 .unwrap_or(50)
5544 }
5545 } else {
5546 50
5547 };
5548 let event =
5549 self.poll_javascript_child_process(vm_id, process_id, &child_process_id, wait_ms)?;
5550 if event.is_null() {
5551 continue;
5552 }
5553
5554 match event.get("type").and_then(Value::as_str) {
5555 Some("stdout") => {
5556 let chunk = javascript_sync_rpc_bytes_arg(
5557 &[event.get("data").cloned().unwrap_or(Value::Null)],
5558 0,
5559 "child_process.spawn_sync stdout",
5560 )?;
5561 stdout.extend_from_slice(&chunk);
5562 if stdout.len() > max_buffer && !kill_sent {
5563 max_buffer_exceeded = true;
5564 self.kill_javascript_child_process(
5565 vm_id,
5566 process_id,
5567 &child_process_id,
5568 "SIGTERM",
5569 )?;
5570 kill_sent = true;
5571 }
5572 }
5573 Some("stderr") => {
5574 let chunk = javascript_sync_rpc_bytes_arg(
5575 &[event.get("data").cloned().unwrap_or(Value::Null)],
5576 0,
5577 "child_process.spawn_sync stderr",
5578 )?;
5579 stderr.extend_from_slice(&chunk);
5580 if stderr.len() > max_buffer && !kill_sent {
5581 max_buffer_exceeded = true;
5582 self.kill_javascript_child_process(
5583 vm_id,
5584 process_id,
5585 &child_process_id,
5586 "SIGTERM",
5587 )?;
5588 kill_sent = true;
5589 }
5590 }
5591 Some("exit") => {
5592 break event
5593 .get("exitCode")
5594 .and_then(Value::as_i64)
5595 .map(|value| value as i32)
5596 .unwrap_or(1);
5597 }
5598 _ => {}
5599 }
5600 };
5601
5602 Ok(json!({
5603 "stdout": String::from_utf8_lossy(&stdout),
5604 "stderr": String::from_utf8_lossy(&stderr),
5605 "code": exit_code,
5606 "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
5607 "timedOut": timed_out,
5608 "maxBufferExceeded": max_buffer_exceeded,
5609 }))
5610 }
5611
5612 fn spawn_descendant_javascript_child_process(
5613 &mut self,
5614 vm_id: &str,
5615 process_id: &str,
5616 current_process_path: &[&str],
5617 request: JavascriptChildProcessSpawnRequest,
5618 ) -> Result<Value, SidecarError> {
5619 let current_process_label =
5620 Self::child_process_path_label(process_id, current_process_path);
5621 let (resolved, parent_kernel_pid) = {
5622 let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
5623 let root = vm
5624 .active_processes
5625 .get(process_id)
5626 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5627 let parent =
5628 Self::active_process_by_path(root, current_process_path).ok_or_else(|| {
5629 SidecarError::InvalidState(format!(
5630 "unknown child process path {current_process_label} during nested spawn"
5631 ))
5632 })?;
5633 (
5634 self.resolve_javascript_child_process_execution(
5635 vm,
5636 &parent.env,
5637 &parent.guest_cwd,
5638 &parent.host_cwd,
5639 &request,
5640 )?,
5641 parent.kernel_pid,
5642 )
5643 };
5644
5645 let sidecar_requests = self.sidecar_requests.clone();
5646 let vm = self
5647 .vms
5648 .get_mut(vm_id)
5649 .ok_or_else(|| missing_vm_error(vm_id))?;
5650 let child_process_id = {
5651 let root = vm
5652 .active_processes
5653 .get_mut(process_id)
5654 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5655 let parent =
5656 Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
5657 SidecarError::InvalidState(format!(
5658 "unknown child process path {current_process_label} during nested spawn"
5659 ))
5660 })?;
5661 parent.allocate_child_process_id()
5662 };
5663 let mut child_path = current_process_path.to_vec();
5664 child_path.push(child_process_id.as_str());
5665 let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
5666 .tool_command
5667 {
5668 let tool_resolution = resolve_tool_command(
5669 vm,
5670 &resolved.command,
5671 &resolved.execution_args,
5672 Some(&resolved.guest_cwd),
5673 )?
5674 .ok_or_else(|| {
5675 SidecarError::InvalidState(format!(
5676 "tool command no longer resolves: {}",
5677 resolved.command
5678 ))
5679 })?;
5680 let kernel_handle = vm
5681 .kernel
5682 .create_virtual_process(
5683 EXECUTION_DRIVER_NAME,
5684 TOOL_DRIVER_NAME,
5685 &resolved.command,
5686 resolved.process_args.clone(),
5687 VirtualProcessOptions {
5688 parent_pid: Some(parent_kernel_pid),
5689 env: resolved.env.clone(),
5690 cwd: Some(resolved.guest_cwd.clone()),
5691 },
5692 )
5693 .map_err(kernel_error)?;
5694 let kernel_pid = kernel_handle.pid();
5695 let tool_execution = ToolExecution::default();
5696 let cancelled = tool_execution.cancelled.clone();
5697 let pending_events = tool_execution.pending_events.clone();
5698 let events_overflowed = tool_execution.events_overflowed.clone();
5699 spawn_tool_process_events(ToolProcessEventRequest {
5700 sidecar_requests: sidecar_requests.clone(),
5701 connection_id: vm.connection_id.clone(),
5702 session_id: vm.session_id.clone(),
5703 vm_id: vm_id.to_owned(),
5704 tool_resolution,
5705 cancelled,
5706 pending_events,
5707 events_overflowed,
5708 });
5709 (
5710 kernel_pid,
5711 kernel_handle,
5712 ActiveExecution::Tool(tool_execution),
5713 None,
5714 )
5715 } else {
5716 let kernel_command = match resolved.runtime {
5717 GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
5718 GuestRuntimeKind::WebAssembly => WASM_COMMAND,
5719 GuestRuntimeKind::Python => {
5720 unreachable!("python child_process execution is rejected")
5721 }
5722 };
5723 let kernel_handle = vm
5724 .kernel
5725 .spawn_process(
5726 kernel_command,
5727 resolved.process_args.clone(),
5728 SpawnOptions {
5729 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
5730 parent_pid: Some(parent_kernel_pid),
5731 env: resolved.env.clone(),
5732 cwd: Some(resolved.guest_cwd.clone()),
5733 },
5734 )
5735 .map_err(kernel_error)?;
5736 let kernel_pid = kernel_handle.pid();
5737 if request.options.detached {
5738 vm.kernel
5739 .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
5740 .map_err(kernel_error)?;
5741 }
5742 let mut execution_env = resolved.env.clone();
5743 execution_env.insert(
5744 String::from(EXECUTION_SANDBOX_ROOT_ENV),
5745 normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
5746 );
5747 let execution = match resolved.runtime {
5748 GuestRuntimeKind::JavaScript => {
5749 execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
5750 &request.options.internal_bootstrap_env,
5751 ));
5752 execution_env.insert(
5753 String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
5754 String::from("1"),
5755 );
5756 let context =
5757 self.javascript_engine
5758 .create_context(CreateJavascriptContextRequest {
5759 vm_id: vm_id.to_owned(),
5760 bootstrap_module: None,
5761 compile_cache_root: Some(
5762 self.cache_root.join("node-compile-cache"),
5763 ),
5764 });
5765 let inline_code = load_javascript_entrypoint_source(
5766 vm,
5767 &resolved.host_cwd,
5768 &resolved.entrypoint,
5769 &execution_env,
5770 );
5771 prepare_javascript_shadow(vm, &resolved)?;
5772
5773 let module_reader = build_module_reader(vm, &resolved)
5774 .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
5775 let execution = self
5776 .javascript_engine
5777 .start_execution_with_module_reader(
5778 StartJavascriptExecutionRequest {
5779 guest_runtime: guest_runtime_identity(
5780 vm,
5781 Some(u64::from(kernel_pid)),
5782 Some(u64::from(parent_kernel_pid)),
5783 ),
5784 vm_id: vm_id.to_owned(),
5785 context_id: context.context_id,
5786 argv: std::iter::once(resolved.entrypoint.clone())
5787 .chain(resolved.execution_args.clone())
5788 .collect(),
5789 env: execution_env,
5790 cwd: resolved.host_cwd.clone(),
5791 limits: javascript_execution_limits(vm),
5792 inline_code,
5793 },
5794 module_reader,
5795 )
5796 .map_err(javascript_error)?;
5797 ActiveExecution::Javascript(execution)
5798 }
5799 GuestRuntimeKind::WebAssembly => {
5800 execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
5801 let wasm_limits = wasm_execution_limits(vm);
5802 let wasm_guest_runtime = guest_runtime_identity(
5803 vm,
5804 Some(u64::from(kernel_pid)),
5805 Some(u64::from(parent_kernel_pid)),
5806 );
5807 let context = self.wasm_engine.create_context(CreateWasmContextRequest {
5808 vm_id: vm_id.to_owned(),
5809 module_path: Some(resolved.entrypoint.clone()),
5810 });
5811 let execution = self
5812 .wasm_engine
5813 .start_execution(StartWasmExecutionRequest {
5814 vm_id: vm_id.to_owned(),
5815 context_id: context.context_id,
5816 argv: resolved.process_args.clone(),
5817 env: execution_env,
5818 cwd: resolved.host_cwd.clone(),
5819 permission_tier: execution_wasm_permission_tier(
5820 resolved
5821 .wasm_permission_tier
5822 .unwrap_or(WasmPermissionTier::Full),
5823 ),
5824 limits: wasm_limits,
5825 guest_runtime: wasm_guest_runtime,
5826 })
5827 .map_err(wasm_error)?;
5828 ActiveExecution::Wasm(Box::new(execution))
5829 }
5830 GuestRuntimeKind::Python => {
5831 unreachable!("python child_process execution is rejected")
5832 }
5833 };
5834 let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
5835 "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5836 "ignore" => {
5837 vm.kernel
5838 .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
5839 .map_err(kernel_error)?;
5840 None
5841 }
5842 "inherit" => None,
5843 _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5844 };
5845 (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
5846 };
5847
5848 let root = vm
5849 .active_processes
5850 .get_mut(process_id)
5851 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5852 let parent =
5853 Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
5854 SidecarError::InvalidState(format!(
5855 "unknown child process path {current_process_label} during nested spawn"
5856 ))
5857 })?;
5858 parent.child_processes.insert(
5859 child_process_id.clone(),
5860 ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
5861 .with_detached(request.options.detached)
5862 .with_guest_cwd(resolved.guest_cwd.clone())
5863 .with_env(resolved.env.clone())
5864 .with_host_cwd(resolved.host_cwd.clone()),
5865 );
5866 if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
5867 parent
5868 .child_processes
5869 .get_mut(&child_process_id)
5870 .ok_or_else(|| {
5871 SidecarError::InvalidState(format!(
5872 "child process {child_process_id} disappeared during nested spawn"
5873 ))
5874 })?
5875 .kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
5876 }
5877 Ok(json!({
5878 "childId": child_process_id,
5879 "pid": kernel_pid,
5880 "command": resolved.command,
5881 "args": resolved.process_args,
5882 }))
5883 }
5884
5885 fn spawn_descendant_javascript_child_process_sync(
5886 &mut self,
5887 vm_id: &str,
5888 process_id: &str,
5889 current_process_path: &[&str],
5890 request: JavascriptChildProcessSpawnRequest,
5891 max_buffer: Option<usize>,
5892 ) -> Result<Value, SidecarError> {
5893 let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
5894 let timeout_deadline = request
5895 .options
5896 .timeout
5897 .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
5898 let timeout_signal = request
5899 .options
5900 .kill_signal
5901 .clone()
5902 .unwrap_or_else(|| String::from("SIGTERM"));
5903 let spawned = self.spawn_descendant_javascript_child_process(
5904 vm_id,
5905 process_id,
5906 current_process_path,
5907 request,
5908 )?;
5909 let child_process_id = spawned
5910 .get("childId")
5911 .and_then(Value::as_str)
5912 .ok_or_else(|| {
5913 SidecarError::InvalidState(String::from(
5914 "child_process.spawn_sync response is missing childId",
5915 ))
5916 })?
5917 .to_owned();
5918
5919 if let Some(input) = sync_input.as_deref() {
5920 self.write_descendant_javascript_child_process_stdin(
5921 vm_id,
5922 process_id,
5923 current_process_path,
5924 &child_process_id,
5925 input,
5926 )?;
5927 }
5928 self.close_descendant_javascript_child_process_stdin(
5929 vm_id,
5930 process_id,
5931 current_process_path,
5932 &child_process_id,
5933 )?;
5934
5935 let max_buffer = max_buffer.unwrap_or(1024 * 1024);
5936 let mut stdout = Vec::new();
5937 let mut stderr = Vec::new();
5938 let mut max_buffer_exceeded = false;
5939 let mut kill_sent = false;
5940 let mut timed_out = false;
5941
5942 let exit_code = loop {
5943 let wait_ms = if let Some(deadline) = timeout_deadline {
5944 let now = Instant::now();
5945 if now >= deadline {
5946 if !kill_sent {
5947 timed_out = true;
5948 self.kill_descendant_javascript_child_process(
5949 vm_id,
5950 process_id,
5951 current_process_path,
5952 &child_process_id,
5953 &timeout_signal,
5954 )?;
5955 kill_sent = true;
5956 }
5957 0
5958 } else {
5959 u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
5960 .unwrap_or(50)
5961 }
5962 } else {
5963 50
5964 };
5965 let event = self.poll_descendant_javascript_child_process(
5966 vm_id,
5967 process_id,
5968 current_process_path,
5969 &child_process_id,
5970 wait_ms,
5971 )?;
5972 if event.is_null() {
5973 continue;
5974 }
5975
5976 match event.get("type").and_then(Value::as_str) {
5977 Some("stdout") => {
5978 let chunk = javascript_sync_rpc_bytes_arg(
5979 &[event.get("data").cloned().unwrap_or(Value::Null)],
5980 0,
5981 "child_process.spawn_sync stdout",
5982 )?;
5983 stdout.extend_from_slice(&chunk);
5984 if stdout.len() > max_buffer && !kill_sent {
5985 max_buffer_exceeded = true;
5986 self.kill_descendant_javascript_child_process(
5987 vm_id,
5988 process_id,
5989 current_process_path,
5990 &child_process_id,
5991 "SIGTERM",
5992 )?;
5993 kill_sent = true;
5994 }
5995 }
5996 Some("stderr") => {
5997 let chunk = javascript_sync_rpc_bytes_arg(
5998 &[event.get("data").cloned().unwrap_or(Value::Null)],
5999 0,
6000 "child_process.spawn_sync stderr",
6001 )?;
6002 stderr.extend_from_slice(&chunk);
6003 if stderr.len() > max_buffer && !kill_sent {
6004 max_buffer_exceeded = true;
6005 self.kill_descendant_javascript_child_process(
6006 vm_id,
6007 process_id,
6008 current_process_path,
6009 &child_process_id,
6010 "SIGTERM",
6011 )?;
6012 kill_sent = true;
6013 }
6014 }
6015 Some("exit") => {
6016 break event
6017 .get("exitCode")
6018 .and_then(Value::as_i64)
6019 .map(|value| value as i32)
6020 .unwrap_or(1);
6021 }
6022 _ => {}
6023 }
6024 };
6025
6026 Ok(json!({
6027 "stdout": String::from_utf8_lossy(&stdout),
6028 "stderr": String::from_utf8_lossy(&stderr),
6029 "code": exit_code,
6030 "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
6031 "timedOut": timed_out,
6032 "maxBufferExceeded": max_buffer_exceeded,
6033 }))
6034 }
6035
6036 fn handle_descendant_javascript_child_process_rpc(
6037 &mut self,
6038 vm_id: &str,
6039 process_id: &str,
6040 current_process_path: &[&str],
6041 request: &JavascriptSyncRpcRequest,
6042 ) -> Result<Value, SidecarError> {
6043 match request.method.as_str() {
6044 "child_process.spawn" => {
6045 let Some(vm) = self.vms.get(vm_id) else {
6046 return Ok(Value::Null);
6047 };
6048 let (payload, _) = parse_javascript_child_process_spawn_request(vm, &request.args)?;
6049 self.spawn_descendant_javascript_child_process(
6050 vm_id,
6051 process_id,
6052 current_process_path,
6053 payload,
6054 )
6055 }
6056 "child_process.spawn_sync" => {
6057 let Some(vm) = self.vms.get(vm_id) else {
6058 return Ok(Value::Null);
6059 };
6060 let (payload, max_buffer) =
6061 parse_javascript_child_process_spawn_request(vm, &request.args)?;
6062 self.spawn_descendant_javascript_child_process_sync(
6063 vm_id,
6064 process_id,
6065 current_process_path,
6066 payload,
6067 max_buffer,
6068 )
6069 }
6070 "child_process.poll" => {
6071 let child_process_id =
6072 javascript_sync_rpc_arg_str(&request.args, 0, "child_process.poll child id")?;
6073 let wait_ms = javascript_sync_rpc_arg_u64_optional(
6074 &request.args,
6075 1,
6076 "child_process.poll wait ms",
6077 )?
6078 .unwrap_or_default();
6079 self.poll_descendant_javascript_child_process(
6080 vm_id,
6081 process_id,
6082 current_process_path,
6083 child_process_id,
6084 wait_ms,
6085 )
6086 }
6087 "child_process.write_stdin" => {
6088 let child_process_id = javascript_sync_rpc_arg_str(
6089 &request.args,
6090 0,
6091 "child_process.write_stdin child id",
6092 )?;
6093 let chunk = javascript_sync_rpc_bytes_arg(
6094 &request.args,
6095 1,
6096 "child_process.write_stdin chunk",
6097 )?;
6098 self.write_descendant_javascript_child_process_stdin(
6099 vm_id,
6100 process_id,
6101 current_process_path,
6102 child_process_id,
6103 &chunk,
6104 )?;
6105 Ok(Value::Null)
6106 }
6107 "child_process.close_stdin" => {
6108 let child_process_id = javascript_sync_rpc_arg_str(
6109 &request.args,
6110 0,
6111 "child_process.close_stdin child id",
6112 )?;
6113 self.close_descendant_javascript_child_process_stdin(
6114 vm_id,
6115 process_id,
6116 current_process_path,
6117 child_process_id,
6118 )?;
6119 Ok(Value::Null)
6120 }
6121 "child_process.kill" => {
6122 let child_process_id =
6123 javascript_sync_rpc_arg_str(&request.args, 0, "child_process.kill child id")?;
6124 let signal =
6125 javascript_sync_rpc_arg_str(&request.args, 1, "child_process.kill signal")?;
6126 self.kill_descendant_javascript_child_process(
6127 vm_id,
6128 process_id,
6129 current_process_path,
6130 child_process_id,
6131 signal,
6132 )?;
6133 Ok(Value::Null)
6134 }
6135 _ => Err(SidecarError::InvalidState(format!(
6136 "unsupported nested child process RPC method {}",
6137 request.method
6138 ))),
6139 }
6140 }
6141
6142 fn poll_descendant_javascript_child_process(
6143 &mut self,
6144 vm_id: &str,
6145 process_id: &str,
6146 current_process_path: &[&str],
6147 child_process_id: &str,
6148 wait_ms: u64,
6149 ) -> Result<Value, SidecarError> {
6150 let mut child_path = current_process_path.to_vec();
6151 child_path.push(child_process_id);
6152 let child_gone_error = || javascript_child_process_gone_error(process_id, &child_path);
6153 let deadline = Instant::now() + Duration::from_millis(wait_ms);
6154 let mut polled_once = false;
6155
6156 loop {
6157 self.drain_queued_descendant_javascript_child_process_events(
6158 vm_id,
6159 process_id,
6160 &child_path,
6161 )?;
6162 enum ChildPollResult {
6163 Event(Box<Option<ActiveExecutionEvent>>),
6164 RecoverRuntimeExit,
6165 Timeout,
6166 }
6167 let wait = if wait_ms == 0 {
6168 Duration::ZERO
6169 } else {
6170 deadline.saturating_duration_since(Instant::now())
6171 };
6172 let poll_result = {
6173 let Some(vm) = self.vms.get_mut(vm_id) else {
6174 return Ok(Value::Null);
6175 };
6176 let Some(parent) =
6177 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6178 else {
6179 return Err(child_gone_error());
6180 };
6181 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6182 return Err(child_gone_error());
6183 };
6184 if let Some(event) = child.pending_execution_events.pop_front() {
6185 ChildPollResult::Event(Box::new(Some(event)))
6186 } else if polled_once && wait.is_zero() {
6187 ChildPollResult::Timeout
6188 } else {
6189 polled_once = true;
6190 match child.execution.poll_event_blocking(wait) {
6191 Ok(Some(event)) => ChildPollResult::Event(Box::new(Some(event))),
6192 Ok(None) => ChildPollResult::RecoverRuntimeExit,
6193 Err(SidecarError::Execution(message))
6194 if (child.runtime == GuestRuntimeKind::JavaScript
6195 && closed_javascript_event_channel(&message))
6196 || (child.runtime == GuestRuntimeKind::Python
6197 && closed_python_event_channel(&message))
6198 || (child.runtime == GuestRuntimeKind::WebAssembly
6199 && closed_wasm_event_channel(&message)) =>
6200 {
6201 ChildPollResult::RecoverRuntimeExit
6202 }
6203 Err(error) => return Err(error),
6204 }
6205 }
6206 };
6207 let event = match poll_result {
6208 ChildPollResult::Event(event) => *event,
6209 ChildPollResult::Timeout => return Ok(Value::Null),
6210 ChildPollResult::RecoverRuntimeExit => self
6211 .recover_descendant_runtime_child_process_event(
6212 vm_id,
6213 process_id,
6214 current_process_path,
6215 child_process_id,
6216 wait.as_millis().try_into().unwrap_or(u64::MAX),
6217 )?,
6218 };
6219
6220 let Some(event) = event else {
6221 return Ok(Value::Null);
6222 };
6223
6224 match event {
6225 ActiveExecutionEvent::Stdout(chunk) => {
6226 return Ok(json!({
6227 "type": "stdout",
6228 "data": javascript_sync_rpc_bytes_value(&chunk),
6229 }));
6230 }
6231 ActiveExecutionEvent::Stderr(chunk) => {
6232 return Ok(json!({
6233 "type": "stderr",
6234 "data": javascript_sync_rpc_bytes_value(&chunk),
6235 }));
6236 }
6237 ActiveExecutionEvent::Exited(exit_code) => {
6238 let had_trailing_events = {
6239 let Some(vm) = self.vms.get_mut(vm_id) else {
6240 return Ok(Value::Null);
6241 };
6242 let Some(parent) = Self::descendant_parent_process_mut(
6243 vm,
6244 process_id,
6245 current_process_path,
6246 ) else {
6247 return Ok(Value::Null);
6248 };
6249 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6250 return Ok(Value::Null);
6251 };
6252 let deadline = Instant::now() + Duration::from_millis(150);
6253 loop {
6254 let wait = deadline.saturating_duration_since(Instant::now());
6255 let next = poll_child_execution_after_exit(child, wait)?;
6256 let Some(next) = next else {
6257 break;
6258 };
6259 if matches!(next, ActiveExecutionEvent::Exited(_)) {
6260 continue;
6261 }
6262 child.queue_pending_execution_event(next)?;
6263 if Instant::now() >= deadline {
6264 break;
6265 }
6266 }
6267 if !child.pending_execution_events.is_empty() {
6268 child.queue_pending_execution_event(ActiveExecutionEvent::Exited(
6269 exit_code,
6270 ))?;
6271 true
6272 } else {
6273 false
6274 }
6275 };
6276 if had_trailing_events {
6277 continue;
6278 }
6279
6280 let parent_signal_key =
6281 Self::child_process_signal_key(process_id, current_process_path);
6282 let Some(vm) = self.vms.get_mut(vm_id) else {
6283 return Ok(Value::Null);
6284 };
6285 let signal_name = {
6286 let Some(parent) = Self::descendant_parent_process_mut(
6287 vm,
6288 process_id,
6289 current_process_path,
6290 ) else {
6291 return Ok(Value::Null);
6292 };
6293 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6294 return Ok(Value::Null);
6295 };
6296 child.pending_self_signal_exit.take().and_then(|signal| {
6297 if exit_code == 128 + signal {
6298 canonical_signal_name(signal).map(str::to_owned)
6299 } else {
6300 None
6301 }
6302 })
6303 };
6304 let (parent_runtime_pid, parent_v8_signal_session, should_signal_parent) = {
6305 let Some(parent) =
6306 Self::descendant_parent_process(vm, process_id, current_process_path)
6307 else {
6308 return Ok(Value::Null);
6309 };
6310 (
6311 parent.execution.child_pid(),
6312 parent.execution.javascript_v8_session_handle().filter(|_| {
6313 matches!(
6314 &parent.execution,
6315 ActiveExecution::Javascript(execution)
6316 if execution.uses_shared_v8_runtime()
6317 )
6318 }),
6319 vm.signal_states
6320 .get(parent_signal_key)
6321 .and_then(|handlers| handlers.get(&(libc::SIGCHLD as u32)))
6322 .is_some_and(|registration| {
6323 registration.action != SignalDispositionAction::Default
6324 }),
6325 )
6326 };
6327 let Some(parent) =
6328 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6329 else {
6330 return Ok(Value::Null);
6331 };
6332 let Some(mut child) = parent.child_processes.remove(child_process_id) else {
6333 return Ok(Value::Null);
6334 };
6335 let child_process_label =
6336 Self::child_process_path_label(process_id, &child_path);
6337 let detached_children =
6338 Self::adopt_detached_child_processes(&child_process_label, &mut child);
6339 sync_process_host_writes_to_kernel(vm, &child)?;
6340 terminate_child_process_tree(&mut vm.kernel, &mut child);
6341 child.kernel_handle.finish(exit_code);
6342 let _ = vm.kernel.wait_and_reap(child.kernel_pid);
6343 vm.signal_states.remove(child_process_id);
6344 for (detached_process_id, detached_child) in detached_children {
6345 vm.detached_child_processes
6346 .insert(detached_process_id.clone());
6347 vm.active_processes
6348 .insert(detached_process_id, detached_child);
6349 }
6350 if should_signal_parent {
6351 if let Some(session) = parent_v8_signal_session {
6352 dispatch_v8_session_signal_async(session, libc::SIGCHLD);
6353 } else {
6354 signal_runtime_process(parent_runtime_pid, libc::SIGCHLD)?;
6355 }
6356 }
6357 let mut payload = Map::new();
6358 payload.insert(String::from("type"), Value::String(String::from("exit")));
6359 payload.insert(String::from("exitCode"), Value::from(exit_code));
6360 if let Some(signal_name) = signal_name {
6361 payload.insert(String::from("signal"), Value::String(signal_name));
6362 }
6363 return Ok(Value::Object(payload));
6364 }
6365 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
6366 let mut current_child_path = current_process_path.to_vec();
6367 current_child_path.push(child_process_id);
6368 let response = if request.method == "process.signal_state" {
6369 let (signal, registration) =
6370 parse_process_signal_state_request(&request.args)?;
6371 let Some(vm) = self.vms.get_mut(vm_id) else {
6372 return Ok(Value::Null);
6373 };
6374 let signal_key =
6375 Self::child_process_signal_key(process_id, ¤t_child_path)
6376 .to_owned();
6377 apply_process_signal_state_update(
6378 &mut vm.signal_states,
6379 &signal_key,
6380 signal,
6381 registration,
6382 );
6383 Ok(Value::Null)
6384 } else if request.method == "process.kill" {
6385 self.handle_descendant_process_kill_rpc(
6386 vm_id,
6387 process_id,
6388 current_process_path,
6389 child_process_id,
6390 &request,
6391 )
6392 } else if request.method.starts_with("child_process.") {
6393 self.handle_descendant_javascript_child_process_rpc(
6394 vm_id,
6395 process_id,
6396 ¤t_child_path,
6397 &request,
6398 )
6399 } else {
6400 let Some(vm) = self.vms.get_mut(vm_id) else {
6401 return Ok(Value::Null);
6402 };
6403 let resource_limits = vm.kernel.resource_limits().clone();
6404 let network_counts = vm_network_resource_counts(vm);
6405 let socket_paths = build_javascript_socket_path_context(vm)?;
6406 let Some(root) = vm.active_processes.get_mut(process_id) else {
6407 return Ok(Value::Null);
6408 };
6409 let Some(parent) =
6410 Self::active_process_by_path_mut(root, current_process_path)
6411 else {
6412 return Ok(Value::Null);
6413 };
6414 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6415 return Ok(Value::Null);
6416 };
6417 service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
6418 bridge: &self.bridge,
6419 vm_id,
6420 dns: &vm.dns,
6421 socket_paths: &socket_paths,
6422 kernel: &mut vm.kernel,
6423 process: child,
6424 sync_request: &request,
6425 resource_limits: &resource_limits,
6426 network_counts,
6427 })
6428 };
6429
6430 let Some(vm) = self.vms.get_mut(vm_id) else {
6431 return Ok(Value::Null);
6432 };
6433 let Some(parent) =
6434 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6435 else {
6436 return Ok(Value::Null);
6437 };
6438 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6439 return Ok(Value::Null);
6440 };
6441 let parent_signal_event = response.as_ref().ok().and_then(|result| {
6442 let target_path_label =
6443 Self::child_process_path_label(process_id, current_process_path);
6444 if request.method != "process.kill"
6445 || result.get("action").and_then(Value::as_str) != Some("user")
6446 || result.get("targetProcessPath").and_then(Value::as_str)
6447 != Some(target_path_label.as_str())
6448 {
6449 return None;
6450 }
6451 Some(json!({
6452 "type": "signal",
6453 "signal": result.get("signal").and_then(Value::as_str).unwrap_or_default(),
6454 "number": result.get("number").and_then(Value::as_i64).unwrap_or_default(),
6455 }))
6456 });
6457 match response {
6458 Ok(result) => child
6459 .execution
6460 .respond_javascript_sync_rpc_success(request.id, result)
6461 .or_else(ignore_stale_javascript_sync_rpc_response)?,
6462 Err(error) => child
6463 .execution
6464 .respond_javascript_sync_rpc_error(
6465 request.id,
6466 javascript_sync_rpc_error_code(&error),
6467 error.to_string(),
6468 )
6469 .or_else(ignore_stale_javascript_sync_rpc_response)?,
6470 }
6471 if let Some(event) = parent_signal_event {
6472 return Ok(event);
6473 }
6474 }
6475 ActiveExecutionEvent::PythonVfsRpcRequest(_) => {
6476 return Err(SidecarError::InvalidState(String::from(
6477 "nested Python child_process execution is not supported yet",
6478 )));
6479 }
6480 ActiveExecutionEvent::SignalState {
6481 signal,
6482 registration,
6483 } => {
6484 let Some(vm) = self.vms.get_mut(vm_id) else {
6485 return Ok(Value::Null);
6486 };
6487 let signal_key =
6488 Self::child_process_signal_key(process_id, &child_path).to_owned();
6489 apply_process_signal_state_update(
6490 &mut vm.signal_states,
6491 &signal_key,
6492 signal,
6493 registration.clone(),
6494 );
6495 return Ok(json!({
6496 "type": "signal_state",
6497 "signal": signal,
6498 "registration": registration,
6499 }));
6500 }
6501 }
6502 }
6503 }
6504
6505 fn recover_descendant_runtime_child_process_event(
6506 &mut self,
6507 vm_id: &str,
6508 process_id: &str,
6509 current_process_path: &[&str],
6510 child_process_id: &str,
6511 wait_ms: u64,
6512 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
6513 let (
6514 parent_kernel_pid,
6515 child_kernel_pid,
6516 child_runtime_pid,
6517 child_runtime,
6518 child_shared_runtime,
6519 ) = {
6520 let mut child_path = current_process_path.to_vec();
6521 child_path.push(child_process_id);
6522 let Some(vm) = self.vms.get_mut(vm_id) else {
6523 return Ok(None);
6524 };
6525 let Some(parent) =
6526 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6527 else {
6528 return Err(javascript_child_process_gone_error(process_id, &child_path));
6529 };
6530 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6531 return Err(javascript_child_process_gone_error(process_id, &child_path));
6532 };
6533 (
6534 parent.kernel_pid,
6535 child.kernel_pid,
6536 child.execution.child_pid(),
6537 child.runtime.clone(),
6538 child.execution.uses_shared_v8_runtime(),
6539 )
6540 };
6541 if child_runtime != GuestRuntimeKind::JavaScript
6542 && child_runtime != GuestRuntimeKind::Python
6543 && child_runtime != GuestRuntimeKind::WebAssembly
6544 {
6545 return Ok(None);
6546 }
6547 let wait_deadline = Instant::now() + Duration::from_millis(wait_ms.min(25));
6548 loop {
6549 let Some(vm) = self.vms.get_mut(vm_id) else {
6550 return Ok(None);
6551 };
6552 if let Some(process_info) = vm.kernel.list_processes().get(&child_kernel_pid) {
6553 if process_info.status == ProcessStatus::Exited {
6554 return Ok(Some(ActiveExecutionEvent::Exited(
6555 process_info.exit_code.unwrap_or(0),
6556 )));
6557 }
6558 }
6559 if let Some(wait_result) = vm
6560 .kernel
6561 .waitpid_with_options(
6562 EXECUTION_DRIVER_NAME,
6563 parent_kernel_pid,
6564 child_kernel_pid as i32,
6565 WaitPidFlags::WNOHANG,
6566 )
6567 .map_err(kernel_error)?
6568 {
6569 return Ok(Some(ActiveExecutionEvent::Exited(wait_result.status)));
6570 }
6571
6572 if !child_shared_runtime && child_runtime_pid != 0 {
6573 if let Some(status) = runtime_child_exit_status(child_runtime_pid)? {
6574 return Ok(Some(ActiveExecutionEvent::Exited(status)));
6575 }
6576 if !runtime_child_is_alive(child_runtime_pid)? {
6577 return Ok(Some(ActiveExecutionEvent::Exited(0)));
6578 }
6579 }
6580 if Instant::now() >= wait_deadline {
6581 return Ok(None);
6582 }
6583 std::thread::sleep(Duration::from_millis(5));
6584 }
6585 }
6586
6587 fn write_descendant_javascript_child_process_stdin(
6588 &mut self,
6589 vm_id: &str,
6590 process_id: &str,
6591 current_process_path: &[&str],
6592 child_process_id: &str,
6593 chunk: &[u8],
6594 ) -> Result<(), SidecarError> {
6595 let mut child_path = current_process_path.to_vec();
6596 child_path.push(child_process_id);
6597 let Some(vm) = self.vms.get_mut(vm_id) else {
6598 return Err(javascript_child_process_gone_error(process_id, &child_path));
6599 };
6600 let Some(root) = vm.active_processes.get_mut(process_id) else {
6601 return Err(javascript_child_process_gone_error(process_id, &child_path));
6602 };
6603 let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6604 return Err(javascript_child_process_gone_error(process_id, &child_path));
6605 };
6606 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6607 return Err(javascript_child_process_gone_error(process_id, &child_path));
6608 };
6609 if let Err(error) = child.execution.write_stdin(chunk) {
6610 if is_broken_pipe_error(&error) {
6611 return Ok(());
6612 }
6613 return Err(error);
6614 }
6615 write_kernel_process_stdin(&mut vm.kernel, child, chunk)
6616 }
6617
6618 fn close_descendant_javascript_child_process_stdin(
6619 &mut self,
6620 vm_id: &str,
6621 process_id: &str,
6622 current_process_path: &[&str],
6623 child_process_id: &str,
6624 ) -> Result<(), SidecarError> {
6625 let mut child_path = current_process_path.to_vec();
6626 child_path.push(child_process_id);
6627 let Some(vm) = self.vms.get_mut(vm_id) else {
6628 return Err(javascript_child_process_gone_error(process_id, &child_path));
6629 };
6630 let Some(root) = vm.active_processes.get_mut(process_id) else {
6631 return Err(javascript_child_process_gone_error(process_id, &child_path));
6632 };
6633 let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6634 return Err(javascript_child_process_gone_error(process_id, &child_path));
6635 };
6636 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6637 return Err(javascript_child_process_gone_error(process_id, &child_path));
6638 };
6639 child.execution.close_stdin()?;
6640 close_kernel_process_stdin(&mut vm.kernel, child)
6641 }
6642
6643 fn kill_descendant_javascript_child_process(
6644 &mut self,
6645 vm_id: &str,
6646 process_id: &str,
6647 current_process_path: &[&str],
6648 child_process_id: &str,
6649 signal: &str,
6650 ) -> Result<(), SidecarError> {
6651 let signal_name = signal.to_owned();
6652 let signal = parse_signal(signal)?;
6653 let Some(vm) = self.vms.get_mut(vm_id) else {
6654 return Ok(());
6655 };
6656 let Some(root) = vm.active_processes.get_mut(process_id) else {
6657 return Ok(());
6658 };
6659 let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6660 return Ok(());
6661 };
6662 let source_pid = parent.kernel_pid;
6663 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6664 return Ok(());
6665 };
6666 terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
6667 let child_process_label = if current_process_path.is_empty() {
6668 child_process_id.to_owned()
6669 } else {
6670 format!("{}/{}", current_process_path.join("/"), child_process_id)
6671 };
6672 emit_security_audit_event(
6673 &self.bridge,
6674 vm_id,
6675 "security.process.kill",
6676 audit_fields([
6677 (String::from("source"), String::from("guest_child_process")),
6678 (String::from("source_pid"), source_pid.to_string()),
6679 (String::from("target_pid"), child.kernel_pid.to_string()),
6680 (String::from("process_id"), process_id.to_owned()),
6681 (String::from("child_process_id"), child_process_label),
6682 (String::from("signal"), signal_name),
6683 ]),
6684 );
6685 Ok(())
6686 }
6687
6688 fn handle_descendant_process_kill_rpc(
6689 &mut self,
6690 vm_id: &str,
6691 process_id: &str,
6692 current_process_path: &[&str],
6693 child_process_id: &str,
6694 request: &JavascriptSyncRpcRequest,
6695 ) -> Result<Value, SidecarError> {
6696 let target_pid = javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
6697 let signal_name = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
6698 let signal = parse_signal(signal_name)?;
6699
6700 let mut source_path = current_process_path.to_vec();
6701 source_path.push(child_process_id);
6702
6703 if signal != 0 && target_pid < 0 {
6704 let pgid = target_pid.unsigned_abs();
6705 let caller_kernel_pid = {
6706 let Some(vm) = self.vms.get(vm_id) else {
6707 return Err(SidecarError::InvalidState(String::from(
6708 "ESRCH: unknown VM during process.kill",
6709 )));
6710 };
6711 let Some(root) = vm.active_processes.get(process_id) else {
6712 return Err(SidecarError::InvalidState(format!(
6713 "ESRCH: unknown process {process_id} during process.kill",
6714 )));
6715 };
6716 let Some(source) = Self::active_process_by_path(root, &source_path) else {
6717 return Err(SidecarError::InvalidState(format!(
6718 "ESRCH: unknown child process {child_process_id} during process.kill",
6719 )));
6720 };
6721 source.kernel_pid
6722 };
6723 let caller_is_member =
6724 self.signal_vm_process_group(vm_id, caller_kernel_pid, pgid, signal_name)?;
6725 if !caller_is_member {
6726 return Ok(Value::Null);
6727 }
6728 let Some(vm) = self.vms.get_mut(vm_id) else {
6729 return Ok(Value::Null);
6730 };
6731 let Some(root) = vm.active_processes.get_mut(process_id) else {
6732 return Ok(Value::Null);
6733 };
6734 let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
6735 return Ok(Value::Null);
6736 };
6737 source.pending_self_signal_exit = None;
6738 if !matches!(
6739 canonical_signal_name(signal),
6740 Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
6741 ) {
6742 source.pending_self_signal_exit = Some(signal);
6743 }
6744 return Ok(json!({
6745 "self": true,
6746 "action": "default",
6747 }));
6748 }
6749
6750 let Some(vm) = self.vms.get_mut(vm_id) else {
6751 return Err(SidecarError::InvalidState(String::from(
6752 "ESRCH: unknown VM during process.kill",
6753 )));
6754 };
6755
6756 if signal == 0 {
6757 vm.kernel
6758 .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
6759 .map_err(kernel_error)?;
6760 return Ok(Value::Null);
6761 }
6762
6763 let target_kernel_pid = u32::try_from(target_pid).map_err(|_| {
6764 SidecarError::InvalidState(format!("EINVAL: invalid process pid {target_pid}"))
6765 })?;
6766 let (source_pid, located_target_path) = {
6767 let Some(root) = vm.active_processes.get(process_id) else {
6768 return Err(SidecarError::InvalidState(format!(
6769 "ESRCH: unknown process {process_id} during process.kill",
6770 )));
6771 };
6772 let Some(source) = Self::active_process_by_path(root, &source_path) else {
6773 return Err(SidecarError::InvalidState(format!(
6774 "ESRCH: unknown child process {child_process_id} during process.kill",
6775 )));
6776 };
6777 vm.kernel
6778 .signal_process(EXECUTION_DRIVER_NAME, target_pid, 0)
6779 .map_err(kernel_error)?;
6780 (
6781 source.kernel_pid,
6782 Self::active_process_path_by_kernel_pid(root, target_kernel_pid),
6783 )
6784 };
6785 let Some(target_path) = located_target_path else {
6786 self.signal_vm_kernel_pid(vm_id, target_kernel_pid, signal_name)?;
6790 return Ok(Value::Null);
6791 };
6792 let Some(vm) = self.vms.get_mut(vm_id) else {
6793 return Err(SidecarError::InvalidState(String::from(
6794 "ESRCH: unknown VM during process.kill",
6795 )));
6796 };
6797
6798 if source_pid == target_kernel_pid {
6799 let Some(root) = vm.active_processes.get_mut(process_id) else {
6800 return Ok(Value::Null);
6801 };
6802 let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
6803 return Ok(Value::Null);
6804 };
6805 source.pending_self_signal_exit = None;
6806 if !matches!(
6807 canonical_signal_name(signal),
6808 Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
6809 ) {
6810 source.pending_self_signal_exit = Some(signal);
6811 }
6812 return Ok(json!({
6813 "self": true,
6814 "action": "default",
6815 }));
6816 }
6817
6818 let signal_key = target_path.last().map(String::as_str).unwrap_or(process_id);
6819 let registration = vm
6820 .signal_states
6821 .get(signal_key)
6822 .and_then(|handlers| handlers.get(&(signal as u32)))
6823 .cloned();
6824
6825 let action = match registration
6826 .as_ref()
6827 .map(|registration| ®istration.action)
6828 {
6829 Some(SignalDispositionAction::Ignore) => "ignore",
6830 Some(SignalDispositionAction::User) => {
6831 let Some(root) = vm.active_processes.get_mut(process_id) else {
6832 return Ok(Value::Null);
6833 };
6834 let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
6835 else {
6836 return Err(SidecarError::InvalidState(format!(
6837 "ESRCH: unknown process pid {target_pid}"
6838 )));
6839 };
6840 if let Some(session) = target.execution.javascript_v8_session_handle().filter(
6841 |_| matches!(&target.execution, ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime())
6842 || matches!(&target.execution, ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime()),
6843 ) {
6844 dispatch_v8_session_signal_async(session, signal);
6845 } else if !dispatch_v8_process_signal(target, signal)? {
6846 return Err(SidecarError::InvalidState(format!(
6847 "unsupported guest signal delivery for pid {target_pid}"
6848 )));
6849 }
6850 "user"
6851 }
6852 Some(SignalDispositionAction::Default) | None
6853 if matches!(
6854 canonical_signal_name(signal),
6855 Some("SIGWINCH" | "SIGCHLD" | "SIGURG")
6856 ) =>
6857 {
6858 "ignore"
6859 }
6860 Some(SignalDispositionAction::Default) | None => {
6861 let Some(root) = vm.active_processes.get_mut(process_id) else {
6862 return Ok(Value::Null);
6863 };
6864 let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
6865 else {
6866 return Err(SidecarError::InvalidState(format!(
6867 "ESRCH: unknown process pid {target_pid}"
6868 )));
6869 };
6870 apply_active_process_default_signal(&mut vm.kernel, target, signal)?;
6871 "default"
6872 }
6873 };
6874
6875 let target_path_label = Self::child_process_path_label(
6876 process_id,
6877 &target_path.iter().map(String::as_str).collect::<Vec<_>>(),
6878 );
6879 emit_security_audit_event(
6880 &self.bridge,
6881 vm_id,
6882 "security.process.kill",
6883 audit_fields([
6884 (String::from("source"), String::from("guest_process")),
6885 (String::from("source_pid"), source_pid.to_string()),
6886 (String::from("target_pid"), target_pid.to_string()),
6887 (String::from("process_id"), process_id.to_owned()),
6888 (
6889 String::from("target_process_path"),
6890 target_path_label.clone(),
6891 ),
6892 (String::from("signal"), signal_name.to_owned()),
6893 ]),
6894 );
6895
6896 Ok(json!({
6897 "self": false,
6898 "action": action,
6899 "signal": signal_name,
6900 "number": signal,
6901 "targetProcessPath": target_path_label,
6902 }))
6903 }
6904
6905 pub(crate) fn poll_javascript_child_process(
6906 &mut self,
6907 vm_id: &str,
6908 process_id: &str,
6909 child_process_id: &str,
6910 wait_ms: u64,
6911 ) -> Result<Value, SidecarError> {
6912 self.poll_descendant_javascript_child_process(
6913 vm_id,
6914 process_id,
6915 &[],
6916 child_process_id,
6917 wait_ms,
6918 )
6919 }
6920
6921 pub(crate) fn write_javascript_child_process_stdin(
6922 &mut self,
6923 vm_id: &str,
6924 process_id: &str,
6925 child_process_id: &str,
6926 chunk: &[u8],
6927 ) -> Result<(), SidecarError> {
6928 let Some(vm) = self.vms.get_mut(vm_id) else {
6929 return Err(javascript_child_process_gone_error(
6930 process_id,
6931 &[child_process_id],
6932 ));
6933 };
6934 let Some(child) = vm
6935 .active_processes
6936 .get_mut(process_id)
6937 .ok_or_else(|| missing_process_error(vm_id, process_id))?
6938 .child_processes
6939 .get_mut(child_process_id)
6940 else {
6941 return Err(javascript_child_process_gone_error(
6942 process_id,
6943 &[child_process_id],
6944 ));
6945 };
6946 if let Err(error) = child.execution.write_stdin(chunk) {
6947 if is_broken_pipe_error(&error) {
6948 return Ok(());
6949 }
6950 return Err(error);
6951 }
6952 write_kernel_process_stdin(&mut vm.kernel, child, chunk)
6953 }
6954
6955 pub(crate) fn close_javascript_child_process_stdin(
6956 &mut self,
6957 vm_id: &str,
6958 process_id: &str,
6959 child_process_id: &str,
6960 ) -> Result<(), SidecarError> {
6961 let Some(vm) = self.vms.get_mut(vm_id) else {
6962 return Err(javascript_child_process_gone_error(
6963 process_id,
6964 &[child_process_id],
6965 ));
6966 };
6967 let Some(child) = vm
6968 .active_processes
6969 .get_mut(process_id)
6970 .ok_or_else(|| missing_process_error(vm_id, process_id))?
6971 .child_processes
6972 .get_mut(child_process_id)
6973 else {
6974 return Err(javascript_child_process_gone_error(
6975 process_id,
6976 &[child_process_id],
6977 ));
6978 };
6979 child.execution.close_stdin()?;
6980 close_kernel_process_stdin(&mut vm.kernel, child)
6981 }
6982
6983 pub(crate) fn kill_javascript_child_process(
6984 &mut self,
6985 vm_id: &str,
6986 process_id: &str,
6987 child_process_id: &str,
6988 signal: &str,
6989 ) -> Result<(), SidecarError> {
6990 let signal_name = signal.to_owned();
6991 let signal = parse_signal(signal)?;
6992 let Some(vm) = self.vms.get_mut(vm_id) else {
6993 return Ok(());
6994 };
6995 let process = vm
6996 .active_processes
6997 .get_mut(process_id)
6998 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
6999 let source_pid = process.kernel_pid;
7000 let child = process
7001 .child_processes
7002 .get_mut(child_process_id)
7003 .ok_or_else(|| {
7004 SidecarError::InvalidState(format!(
7005 "unknown child process {child_process_id} during kill"
7006 ))
7007 })?;
7008 terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
7009 emit_security_audit_event(
7010 &self.bridge,
7011 vm_id,
7012 "security.process.kill",
7013 audit_fields([
7014 (String::from("source"), String::from("guest_child_process")),
7015 (String::from("source_pid"), source_pid.to_string()),
7016 (String::from("target_pid"), child.kernel_pid.to_string()),
7017 (String::from("process_id"), process_id.to_owned()),
7018 (
7019 String::from("child_process_id"),
7020 child_process_id.to_owned(),
7021 ),
7022 (String::from("signal"), signal_name),
7023 ]),
7024 );
7025 Ok(())
7026 }
7027
7028 pub(crate) fn signal_vm_kernel_pid(
7034 &mut self,
7035 vm_id: &str,
7036 target_kernel_pid: u32,
7037 signal_name: &str,
7038 ) -> Result<(), SidecarError> {
7039 let signal = parse_signal(signal_name)?;
7040 let located = {
7041 let Some(vm) = self.vms.get(vm_id) else {
7042 return Err(SidecarError::InvalidState(String::from(
7043 "ESRCH: unknown VM during process.kill",
7044 )));
7045 };
7046 let alive = vm
7047 .kernel
7048 .list_processes()
7049 .get(&target_kernel_pid)
7050 .is_some_and(|info| info.status != ProcessStatus::Exited);
7051 if !alive {
7052 return Err(SidecarError::InvalidState(format!(
7053 "ESRCH: no such process {target_kernel_pid}"
7054 )));
7055 }
7056 vm.active_processes.iter().find_map(|(process_id, root)| {
7057 Self::active_process_path_by_kernel_pid(root, target_kernel_pid)
7058 .map(|path| (process_id.clone(), path))
7059 })
7060 };
7061
7062 match located {
7063 Some((process_id, path)) if path.is_empty() => {
7064 self.kill_process_internal(vm_id, &process_id, signal_name)
7065 }
7066 Some((process_id, path)) => {
7067 let Some(vm) = self.vms.get_mut(vm_id) else {
7068 return Ok(());
7069 };
7070 let Some(root) = vm.active_processes.get_mut(&process_id) else {
7071 return Ok(());
7072 };
7073 let Some(target) = Self::active_process_by_owned_path_mut(root, &path) else {
7074 return Err(SidecarError::InvalidState(format!(
7075 "ESRCH: no such process {target_kernel_pid}"
7076 )));
7077 };
7078 terminate_tracked_child_process_for_signal(&mut vm.kernel, target, signal)?;
7079 emit_security_audit_event(
7080 &self.bridge,
7081 vm_id,
7082 "security.process.kill",
7083 audit_fields([
7084 (String::from("source"), String::from("guest_process")),
7085 (String::from("target_pid"), target_kernel_pid.to_string()),
7086 (String::from("process_id"), process_id),
7087 (String::from("signal"), signal_name.to_owned()),
7088 ]),
7089 );
7090 Ok(())
7091 }
7092 None => {
7093 let Some(vm) = self.vms.get_mut(vm_id) else {
7094 return Ok(());
7095 };
7096 let target_pid = i32::try_from(target_kernel_pid).map_err(|_| {
7097 SidecarError::InvalidState(format!(
7098 "EINVAL: invalid process pid {target_kernel_pid}"
7099 ))
7100 })?;
7101 vm.kernel
7102 .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
7103 .map_err(kernel_error)?;
7104 emit_security_audit_event(
7105 &self.bridge,
7106 vm_id,
7107 "security.process.kill",
7108 audit_fields([
7109 (String::from("source"), String::from("guest_process")),
7110 (String::from("target_pid"), target_kernel_pid.to_string()),
7111 (String::from("signal"), signal_name.to_owned()),
7112 ]),
7113 );
7114 Ok(())
7115 }
7116 }
7117 }
7118
7119 pub(crate) fn signal_vm_process_group(
7124 &mut self,
7125 vm_id: &str,
7126 caller_kernel_pid: u32,
7127 pgid: u32,
7128 signal_name: &str,
7129 ) -> Result<bool, SidecarError> {
7130 parse_signal(signal_name)?;
7131 let members = {
7132 let Some(vm) = self.vms.get(vm_id) else {
7133 return Err(SidecarError::InvalidState(String::from(
7134 "ESRCH: unknown VM during process.kill",
7135 )));
7136 };
7137 vm.kernel
7138 .list_processes()
7139 .into_iter()
7140 .filter(|(_, info)| info.pgid == pgid && info.status != ProcessStatus::Exited)
7141 .map(|(pid, _)| pid)
7142 .collect::<Vec<_>>()
7143 };
7144 if members.is_empty() {
7145 return Err(SidecarError::InvalidState(format!(
7146 "ESRCH: no such process group {pgid}"
7147 )));
7148 }
7149
7150 let mut caller_is_member = false;
7151 for member_pid in members {
7152 if member_pid == caller_kernel_pid {
7153 caller_is_member = true;
7154 continue;
7155 }
7156 match self.signal_vm_kernel_pid(vm_id, member_pid, signal_name) {
7157 Ok(()) => {}
7158 Err(error) if sidecar_error_is_esrch(&error) => {}
7161 Err(error) => return Err(error),
7162 }
7163 }
7164 Ok(caller_is_member)
7165 }
7166}
7167
7168fn terminate_tracked_child_process_for_signal(
7173 kernel: &mut SidecarKernel,
7174 child: &mut ActiveProcess,
7175 signal: i32,
7176) -> Result<(), SidecarError> {
7177 let should_terminate_shared_runtime = child.execution.uses_shared_v8_runtime()
7178 && signal != 0
7179 && !matches!(
7180 signal,
7181 libc::SIGHUP
7182 | libc::SIGINT
7183 | libc::SIGTERM
7184 | libc::SIGCHLD
7185 | libc::SIGWINCH
7186 | libc::SIGSTOP
7187 | libc::SIGCONT
7188 );
7189 if should_terminate_shared_runtime {
7190 child.execution.terminate()?;
7191 child.pending_self_signal_exit = Some(signal);
7192 child.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
7193 } else {
7194 kernel
7195 .kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, signal)
7196 .map_err(kernel_error)?;
7197 }
7198 Ok(())
7199}
7200
7201fn sidecar_error_is_esrch(error: &SidecarError) -> bool {
7202 error.to_string().contains("ESRCH")
7203}
7204
7205fn apply_active_process_default_signal(
7206 kernel: &mut SidecarKernel,
7207 process: &mut ActiveProcess,
7208 signal: i32,
7209) -> Result<(), SidecarError> {
7210 if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
7211 return kernel
7212 .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
7213 .map_err(kernel_error);
7214 }
7215
7216 if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
7217 close_kernel_process_stdin(kernel, process)?;
7218 }
7219
7220 if process.execution.uses_shared_v8_runtime() {
7221 process.execution.terminate()?;
7222 if signal != 0 && matches!(process.execution, ActiveExecution::Wasm(_)) {
7223 process.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
7224 }
7225 return Ok(());
7226 }
7227
7228 kernel
7229 .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
7230 .map_err(kernel_error)
7231}
7232
7233fn map_wasm_signal_registration(
7234 registration: secure_exec_execution::wasm::WasmSignalHandlerRegistration,
7235) -> SignalHandlerRegistration {
7236 SignalHandlerRegistration {
7237 action: match registration.action {
7238 secure_exec_execution::wasm::WasmSignalDispositionAction::Default => {
7239 crate::protocol::SignalDispositionAction::Default
7240 }
7241 secure_exec_execution::wasm::WasmSignalDispositionAction::Ignore => {
7242 crate::protocol::SignalDispositionAction::Ignore
7243 }
7244 secure_exec_execution::wasm::WasmSignalDispositionAction::User => {
7245 crate::protocol::SignalDispositionAction::User
7246 }
7247 },
7248 mask: registration.mask,
7249 flags: registration.flags,
7250 }
7251}
7252
7253fn parse_process_signal_state_request(
7254 args: &[Value],
7255) -> Result<(u32, SignalHandlerRegistration), SidecarError> {
7256 let signal = javascript_sync_rpc_arg_u32(args, 0, "process.signal_state signal")?;
7257 let action = javascript_sync_rpc_arg_str(args, 1, "process.signal_state action")?;
7258 let mask_json = javascript_sync_rpc_arg_str(args, 2, "process.signal_state mask")?;
7259 let flags = javascript_sync_rpc_arg_u32(args, 3, "process.signal_state flags")?;
7260 let mask: Vec<u32> = serde_json::from_str(mask_json).map_err(|error| {
7261 SidecarError::InvalidState(format!(
7262 "process.signal_state mask must be valid JSON: {error}"
7263 ))
7264 })?;
7265 let action = match action.trim().to_ascii_lowercase().as_str() {
7266 "default" => SignalDispositionAction::Default,
7267 "ignore" => SignalDispositionAction::Ignore,
7268 "user" => SignalDispositionAction::User,
7269 other => {
7270 return Err(SidecarError::InvalidState(format!(
7271 "unsupported process.signal_state action {other}"
7272 )));
7273 }
7274 };
7275
7276 Ok((
7277 signal,
7278 SignalHandlerRegistration {
7279 action,
7280 mask,
7281 flags,
7282 },
7283 ))
7284}
7285
7286fn apply_process_signal_state_update(
7287 signal_states: &mut BTreeMap<String, BTreeMap<u32, SignalHandlerRegistration>>,
7288 process_id: &str,
7289 signal: u32,
7290 registration: SignalHandlerRegistration,
7291) {
7292 if registration.action == SignalDispositionAction::Default
7293 && registration.mask.is_empty()
7294 && registration.flags == 0
7295 {
7296 let remove_process_entry = signal_states
7297 .get_mut(process_id)
7298 .map(|handlers| {
7299 handlers.remove(&signal);
7300 handlers.is_empty()
7301 })
7302 .unwrap_or(false);
7303 if remove_process_entry {
7304 signal_states.remove(process_id);
7305 }
7306 return;
7307 }
7308
7309 signal_states
7310 .entry(process_id.to_owned())
7311 .or_default()
7312 .insert(signal, registration);
7313}
7314
7315fn map_node_signal_registration(
7316 registration: NodeSignalHandlerRegistration,
7317) -> SignalHandlerRegistration {
7318 SignalHandlerRegistration {
7319 action: match registration.action {
7320 NodeSignalDispositionAction::Default => SignalDispositionAction::Default,
7321 NodeSignalDispositionAction::Ignore => SignalDispositionAction::Ignore,
7322 NodeSignalDispositionAction::User => SignalDispositionAction::User,
7323 },
7324 mask: registration.mask,
7325 flags: registration.flags,
7326 }
7327}
7328
7329fn javascript_child_process_sync_input_bytes(
7330 value: Option<&Value>,
7331) -> Result<Option<Vec<u8>>, SidecarError> {
7332 let Some(value) = value else {
7333 return Ok(None);
7334 };
7335
7336 match value {
7337 Value::Null => Ok(None),
7338 Value::String(text) => Ok(Some(text.as_bytes().to_vec())),
7339 other => javascript_sync_rpc_bytes_arg(
7340 std::slice::from_ref(other),
7341 0,
7342 "child_process.spawn_sync input",
7343 )
7344 .map(Some),
7345 }
7346}
7347
7348fn resolve_execute_request(
7353 vm: &VmState,
7354 payload: &ExecuteRequest,
7355) -> Result<ResolvedChildProcessExecution, SidecarError> {
7356 let payload_env: BTreeMap<String, String> = payload
7357 .env
7358 .iter()
7359 .map(|(k, v)| (k.clone(), v.clone()))
7360 .collect();
7361 if let Some(command) = payload.command.as_deref() {
7362 return resolve_command_execution(
7363 vm,
7364 command,
7365 &payload.args,
7366 &payload_env,
7367 payload.cwd.as_deref(),
7368 payload.wasm_permission_tier,
7369 );
7370 }
7371
7372 let runtime = payload.runtime.clone().ok_or_else(|| {
7373 SidecarError::InvalidState(String::from("execute requires either command or runtime"))
7374 })?;
7375 let entrypoint = payload.entrypoint.clone().ok_or_else(|| {
7376 SidecarError::InvalidState(String::from(
7377 "execute requires either command or entrypoint",
7378 ))
7379 })?;
7380 let (guest_cwd, host_cwd, allow_host_path_overrides) =
7381 resolve_execution_cwds(vm, payload.cwd.as_deref());
7382 let mut env = vm.guest_env.clone();
7383 env.extend(payload_env.clone());
7384
7385 let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint);
7386 if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7387 let requested_cwd = payload.cwd.as_deref().unwrap_or(guest_cwd.as_str());
7388 return Err(SidecarError::InvalidState(format!(
7389 "execution cwd {requested_cwd} is outside sandbox root {}",
7390 vm.host_cwd.to_string_lossy()
7391 )));
7392 }
7393 let host_entrypoint_override = allow_host_path_overrides
7394 .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint))
7395 .flatten();
7396
7397 let guest_entrypoint = host_entrypoint_override
7398 .as_ref()
7399 .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7400 .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, &entrypoint));
7401 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7402
7403 Ok(ResolvedChildProcessExecution {
7404 command: match runtime {
7405 GuestRuntimeKind::JavaScript => String::from(JAVASCRIPT_COMMAND),
7406 GuestRuntimeKind::Python => String::from(PYTHON_COMMAND),
7407 GuestRuntimeKind::WebAssembly => String::from(WASM_COMMAND),
7408 },
7409 process_args: std::iter::once(entrypoint.clone())
7410 .chain(payload.args.iter().cloned())
7411 .collect(),
7412 runtime,
7413 entrypoint: host_entrypoint_override
7414 .map(|(_, host_entrypoint)| host_entrypoint)
7415 .unwrap_or(entrypoint),
7416 execution_args: payload.args.clone(),
7417 env,
7418 guest_cwd,
7419 host_cwd,
7420 wasm_permission_tier: payload.wasm_permission_tier,
7421 tool_command: false,
7422 })
7423}
7424
7425fn resolve_command_execution(
7426 vm: &VmState,
7427 command: &str,
7428 args: &[String],
7429 extra_env: &BTreeMap<String, String>,
7430 cwd: Option<&str>,
7431 explicit_wasm_permission_tier: Option<WasmPermissionTier>,
7432) -> Result<ResolvedChildProcessExecution, SidecarError> {
7433 let (guest_cwd, host_cwd, allow_host_path_overrides) = resolve_execution_cwds(vm, cwd);
7434 let mut env = vm.guest_env.clone();
7435 env.extend(extra_env.clone());
7436 let args = apply_shell_cwd_prefix(command, args.to_vec(), &guest_cwd);
7437
7438 if is_tool_command(vm, command) {
7439 let command = normalized_tool_command_name(command).unwrap_or_else(|| command.to_owned());
7440 return Ok(ResolvedChildProcessExecution {
7441 command: command.clone(),
7442 process_args: std::iter::once(command.clone())
7443 .chain(args.iter().cloned())
7444 .collect(),
7445 runtime: GuestRuntimeKind::JavaScript,
7446 entrypoint: command,
7447 execution_args: args,
7448 env,
7449 guest_cwd,
7450 host_cwd,
7451 wasm_permission_tier: None,
7452 tool_command: true,
7453 });
7454 }
7455
7456 if is_node_runtime_command(command) {
7457 if let Some(cli) = resolve_host_node_cli_entrypoint(command) {
7458 env.insert(
7459 String::from("AGENTOS_NODE_EVAL"),
7460 build_host_node_cli_eval(&cli),
7461 );
7462 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7463 add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
7464 add_runtime_host_access_path(
7465 &mut env,
7466 "AGENTOS_EXTRA_FS_READ_PATHS",
7467 &cli.package_root,
7468 true,
7469 );
7470
7471 return Ok(ResolvedChildProcessExecution {
7472 command: String::from(JAVASCRIPT_COMMAND),
7473 process_args: std::iter::once(command.to_owned())
7474 .chain(args.iter().cloned())
7475 .collect(),
7476 runtime: GuestRuntimeKind::JavaScript,
7477 entrypoint: String::from("-e"),
7478 execution_args: std::iter::once(cli.guest_entrypoint.clone())
7479 .chain(args.iter().cloned())
7480 .collect(),
7481 env,
7482 guest_cwd,
7483 host_cwd,
7484 wasm_permission_tier: None,
7485 tool_command: false,
7486 });
7487 }
7488
7489 if args.is_empty() {
7490 env.insert(String::from("AGENTOS_NODE_EVAL"), String::new());
7491 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7492
7493 return Ok(ResolvedChildProcessExecution {
7494 command: String::from(JAVASCRIPT_COMMAND),
7495 process_args: vec![command.to_owned()],
7496 runtime: GuestRuntimeKind::JavaScript,
7497 entrypoint: String::from("-e"),
7498 execution_args: Vec::new(),
7499 env,
7500 guest_cwd,
7501 host_cwd,
7502 wasm_permission_tier: None,
7503 tool_command: false,
7504 });
7505 }
7506
7507 if let Some((entrypoint, execution_args)) =
7508 resolve_special_node_cli_invocation(&args, &mut env)
7509 {
7510 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7511
7512 return Ok(ResolvedChildProcessExecution {
7513 command: String::from(JAVASCRIPT_COMMAND),
7514 process_args: std::iter::once(command.to_owned())
7515 .chain(args.iter().cloned())
7516 .collect(),
7517 runtime: GuestRuntimeKind::JavaScript,
7518 entrypoint,
7519 execution_args,
7520 env,
7521 guest_cwd,
7522 host_cwd,
7523 wasm_permission_tier: None,
7524 tool_command: false,
7525 });
7526 }
7527
7528 let Some(entrypoint_specifier) = args.first() else {
7529 return Err(SidecarError::InvalidState(format!(
7530 "{command} execution requires an entrypoint"
7531 )));
7532 };
7533
7534 let (entrypoint, execution_args, guest_entrypoint) = {
7535 let requested_host_entrypoint =
7536 resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier);
7537 if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7538 let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
7539 return Err(SidecarError::InvalidState(format!(
7540 "execution cwd {requested_cwd} is outside sandbox root {}",
7541 vm.host_cwd.to_string_lossy()
7542 )));
7543 }
7544 let host_entrypoint_override = allow_host_path_overrides
7545 .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier))
7546 .flatten();
7547 let guest_entrypoint = host_entrypoint_override
7548 .as_ref()
7549 .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7550 .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, entrypoint_specifier));
7551 let entrypoint = host_entrypoint_override.map_or_else(
7552 || {
7553 guest_entrypoint.as_ref().map_or_else(
7554 || entrypoint_specifier.clone(),
7555 |guest_entrypoint| {
7556 resolve_vm_guest_path_to_host(vm, guest_entrypoint)
7557 .to_string_lossy()
7558 .into_owned()
7559 },
7560 )
7561 },
7562 |(_, host_entrypoint)| host_entrypoint,
7563 );
7564 (
7565 entrypoint,
7566 args.iter().skip(1).cloned().collect(),
7567 guest_entrypoint,
7568 )
7569 };
7570
7571 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7572
7573 return Ok(ResolvedChildProcessExecution {
7574 command: String::from(JAVASCRIPT_COMMAND),
7575 process_args: std::iter::once(command.to_owned())
7576 .chain(args.iter().cloned())
7577 .collect(),
7578 runtime: GuestRuntimeKind::JavaScript,
7579 entrypoint,
7580 execution_args,
7581 env,
7582 guest_cwd,
7583 host_cwd,
7584 wasm_permission_tier: None,
7585 tool_command: false,
7586 });
7587 }
7588
7589 if command.ends_with(".js") || command.ends_with(".mjs") || command.ends_with(".cjs") {
7590 let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, command);
7591 if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7592 let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
7593 return Err(SidecarError::InvalidState(format!(
7594 "execution cwd {requested_cwd} is outside sandbox root {}",
7595 vm.host_cwd.to_string_lossy()
7596 )));
7597 }
7598 let host_entrypoint_override = allow_host_path_overrides
7599 .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, command))
7600 .flatten();
7601 let guest_entrypoint = host_entrypoint_override
7602 .as_ref()
7603 .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7604 .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, command));
7605 let entrypoint = host_entrypoint_override.map_or_else(
7606 || {
7607 guest_entrypoint.as_ref().map_or_else(
7608 || command.to_owned(),
7609 |guest_entrypoint| {
7610 resolve_vm_guest_path_to_host(vm, guest_entrypoint)
7611 .to_string_lossy()
7612 .into_owned()
7613 },
7614 )
7615 },
7616 |(_, host_entrypoint)| host_entrypoint,
7617 );
7618 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7619
7620 return Ok(ResolvedChildProcessExecution {
7621 command: String::from(JAVASCRIPT_COMMAND),
7622 process_args: std::iter::once(command.to_owned())
7623 .chain(args.iter().cloned())
7624 .collect(),
7625 runtime: GuestRuntimeKind::JavaScript,
7626 entrypoint,
7627 execution_args: args.to_vec(),
7628 env,
7629 guest_cwd,
7630 host_cwd,
7631 wasm_permission_tier: None,
7632 tool_command: false,
7633 });
7634 }
7635
7636 let guest_entrypoint = resolve_guest_command_entrypoint(
7637 vm,
7638 &guest_cwd,
7639 command,
7640 env.get("PATH").map(String::as_str),
7641 )
7642 .ok_or_else(|| {
7643 SidecarError::InvalidState(format!(
7644 "command not found on native sidecar path: {command}"
7645 ))
7646 })?;
7647 let wasm_permission_tier = explicit_wasm_permission_tier
7648 .or_else(|| vm.command_permissions.get(command).copied())
7649 .or_else(|| {
7650 Path::new(&guest_entrypoint)
7651 .file_name()
7652 .and_then(|name| name.to_str())
7653 .and_then(|name| vm.command_permissions.get(name).copied())
7654 });
7655
7656 let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
7657 if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
7658 resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
7659 {
7660 prepare_guest_runtime_env(
7661 vm,
7662 &mut env,
7663 &guest_cwd,
7664 &host_cwd,
7665 Some(javascript_guest_entrypoint),
7666 )?;
7667
7668 return Ok(ResolvedChildProcessExecution {
7669 command: command.to_owned(),
7670 process_args: std::iter::once(command.to_owned())
7671 .chain(args.iter().cloned())
7672 .collect(),
7673 runtime: GuestRuntimeKind::JavaScript,
7674 entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
7675 execution_args: args.to_vec(),
7676 env,
7677 guest_cwd,
7678 host_cwd,
7679 wasm_permission_tier: None,
7680 tool_command: false,
7681 });
7682 }
7683 prepare_guest_runtime_env(
7684 vm,
7685 &mut env,
7686 &guest_cwd,
7687 &host_cwd,
7688 Some(guest_entrypoint.clone()),
7689 )?;
7690
7691 Ok(ResolvedChildProcessExecution {
7692 command: command.to_owned(),
7693 process_args: std::iter::once(command.to_owned())
7694 .chain(args.iter().cloned())
7695 .collect(),
7696 runtime: GuestRuntimeKind::WebAssembly,
7697 entrypoint: host_entrypoint.to_string_lossy().into_owned(),
7698 execution_args: args.to_vec(),
7699 env,
7700 guest_cwd,
7701 host_cwd,
7702 wasm_permission_tier,
7703 tool_command: false,
7704 })
7705}
7706
7707const MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH: usize = 4;
7708
7709fn resolve_javascript_command_entrypoint(
7710 vm: &VmState,
7711 guest_entrypoint: &str,
7712 host_entrypoint: &Path,
7713) -> Option<(String, PathBuf)> {
7714 resolve_javascript_command_entrypoint_inner(
7715 vm,
7716 guest_entrypoint,
7717 host_entrypoint,
7718 MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH,
7719 )
7720}
7721
7722fn resolve_javascript_command_entrypoint_inner(
7723 vm: &VmState,
7724 guest_entrypoint: &str,
7725 host_entrypoint: &Path,
7726 redirects_remaining: usize,
7727) -> Option<(String, PathBuf)> {
7728 if redirects_remaining > 0 {
7729 let symlink_target = fs::symlink_metadata(host_entrypoint)
7730 .ok()
7731 .filter(|metadata| metadata.file_type().is_symlink())
7732 .and_then(|_| fs::read_link(host_entrypoint).ok());
7733 if let Some(symlink_target) = symlink_target {
7734 let guest_parent = Path::new(guest_entrypoint)
7735 .parent()
7736 .and_then(|path| path.to_str())
7737 .unwrap_or("/");
7738 let symlink_guest_entrypoint = if symlink_target.is_absolute() {
7739 normalize_path(&symlink_target.to_string_lossy())
7740 } else {
7741 normalize_path(&format!(
7742 "{guest_parent}/{}",
7743 symlink_target.to_string_lossy().replace('\\', "/")
7744 ))
7745 };
7746 let symlink_host_entrypoint =
7747 resolve_vm_guest_path_to_host(vm, &symlink_guest_entrypoint);
7748 return resolve_javascript_command_entrypoint_inner(
7749 vm,
7750 &symlink_guest_entrypoint,
7751 &symlink_host_entrypoint,
7752 redirects_remaining - 1,
7753 );
7754 }
7755 }
7756
7757 let script = load_executable_script_preview(host_entrypoint)?;
7758 let interpreter = parse_script_interpreter_name(&script);
7759
7760 if interpreter.is_none() && is_probable_javascript_entrypoint(host_entrypoint, &script) {
7761 return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
7762 }
7763
7764 let interpreter = interpreter?;
7765 if interpreter == "node" {
7766 return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
7767 }
7768
7769 if redirects_remaining == 0 || !matches!(interpreter.as_str(), "sh" | "bash" | "dash") {
7770 return None;
7771 }
7772
7773 let shim_target = parse_node_shell_shim_target(&script)?;
7774 let guest_parent = Path::new(guest_entrypoint)
7775 .parent()
7776 .and_then(|path| path.to_str())
7777 .unwrap_or("/");
7778 let shim_guest_entrypoint = normalize_path(&format!("{guest_parent}/{shim_target}"));
7779 let shim_host_entrypoint = resolve_vm_guest_path_to_host(vm, &shim_guest_entrypoint);
7780 resolve_javascript_command_entrypoint_inner(
7781 vm,
7782 &shim_guest_entrypoint,
7783 &shim_host_entrypoint,
7784 redirects_remaining - 1,
7785 )
7786}
7787
7788fn load_executable_script_preview(path: &Path) -> Option<String> {
7789 let bytes = fs::read(path).ok()?;
7790 let preview_len = bytes.len().min(16 * 1024);
7791 Some(String::from_utf8_lossy(&bytes[..preview_len]).into_owned())
7792}
7793
7794fn parse_script_interpreter_name(script: &str) -> Option<String> {
7795 let shebang = script.lines().next()?.strip_prefix("#!")?.trim();
7796 let mut tokens = shebang.split_whitespace();
7797 let command = tokens.next()?;
7798 let command_name = Path::new(command).file_name()?.to_str()?;
7799 if command_name == "env" {
7800 for token in tokens {
7801 if token.starts_with('-') {
7802 continue;
7803 }
7804 return Path::new(token)
7805 .file_name()
7806 .and_then(|name| name.to_str())
7807 .map(ToOwned::to_owned);
7808 }
7809 return None;
7810 }
7811
7812 Some(command_name.to_owned())
7813}
7814
7815fn parse_node_shell_shim_target(script: &str) -> Option<String> {
7816 for line in script.lines() {
7817 let trimmed = line.trim();
7818 if !trimmed.starts_with("exec ") {
7819 continue;
7820 }
7821
7822 let mut remaining = trimmed;
7823 while let Some(start) = remaining.find("\"$basedir/") {
7824 let after_prefix = &remaining[start + "\"$basedir/".len()..];
7825 let end = after_prefix.find('"')?;
7826 let candidate = &after_prefix[..end];
7827 remaining = &after_prefix[end + 1..];
7828
7829 if candidate.is_empty() || candidate == "node" || candidate.ends_with("/node") {
7830 continue;
7831 }
7832
7833 return Some(candidate.to_owned());
7834 }
7835 }
7836
7837 None
7838}
7839
7840fn is_probable_javascript_entrypoint(path: &Path, script: &str) -> bool {
7841 let extension = path
7842 .extension()
7843 .and_then(|value| value.to_str())
7844 .unwrap_or_default();
7845 if matches!(extension, "js" | "cjs" | "mjs") {
7846 return true;
7847 }
7848
7849 if !path
7850 .components()
7851 .any(|component| component.as_os_str() == "node_modules")
7852 {
7853 return false;
7854 }
7855
7856 let preview = script.trim_start_matches('\u{feff}').trim_start();
7857 !preview.is_empty()
7858 && !preview.starts_with("#!")
7859 && (preview.starts_with("\"use strict\"")
7860 || preview.starts_with("'use strict'")
7861 || preview.starts_with("import ")
7862 || preview.starts_with("export ")
7863 || preview.starts_with("const ")
7864 || preview.starts_with("let ")
7865 || preview.starts_with("var ")
7866 || preview.starts_with("Object.defineProperty(exports")
7867 || preview.starts_with("module.exports")
7868 || preview.starts_with("require("))
7869}
7870
7871fn resolve_guest_execution_cwd(vm: &VmState, value: Option<&str>) -> String {
7872 value
7873 .map(normalize_path)
7874 .unwrap_or_else(|| vm.guest_cwd.clone())
7875}
7876
7877fn resolve_execution_cwds(vm: &VmState, value: Option<&str>) -> (String, PathBuf, bool) {
7878 if let Some(raw_cwd) = value {
7879 let normalized_vm_host_cwd = normalize_host_path(&vm.host_cwd);
7880 let requested_host_cwd = normalize_host_path(Path::new(raw_cwd));
7881 if path_is_within_root(&requested_host_cwd, &normalized_vm_host_cwd) {
7882 let relative = requested_host_cwd
7883 .strip_prefix(&normalized_vm_host_cwd)
7884 .unwrap_or_else(|_| Path::new(""));
7885 let relative = relative.to_string_lossy().replace('\\', "/");
7886 let guest_cwd = if relative.is_empty() {
7887 String::from("/")
7888 } else {
7889 normalize_path(&format!("/{relative}"))
7890 };
7891 return (guest_cwd, requested_host_cwd, true);
7892 }
7893 }
7894
7895 let guest_cwd = resolve_guest_execution_cwd(vm, value);
7896 let host_cwd = if value.is_none() {
7897 vm.host_cwd.clone()
7898 } else {
7899 resolve_vm_guest_path_to_host(vm, &guest_cwd)
7900 };
7901 (guest_cwd, host_cwd, value.is_none())
7902}
7903
7904fn resolve_vm_guest_path_to_host(vm: &VmState, guest_path: &str) -> PathBuf {
7905 host_mount_path_for_guest_path(vm, guest_path)
7906 .unwrap_or_else(|| shadow_path_for_guest(vm, guest_path))
7907}
7908
7909fn shadow_path_for_guest(vm: &VmState, guest_path: &str) -> PathBuf {
7910 let normalized = normalize_path(guest_path);
7911 let relative = normalized.trim_start_matches('/');
7912 if relative.is_empty() {
7913 return vm.cwd.clone();
7914 }
7915 vm.cwd.join(relative)
7916}
7917
7918fn apply_shell_cwd_prefix(command: &str, mut args: Vec<String>, guest_cwd: &str) -> Vec<String> {
7919 if guest_cwd == "/" || !is_shell_command(command) {
7920 return args;
7921 }
7922
7923 let Some(flag) = args.first() else {
7924 return args;
7925 };
7926 if !matches!(flag.as_str(), "-c" | "-lc") || args.len() < 2 {
7927 return args;
7928 }
7929
7930 let command_text = args[1].clone();
7931 let quoted_cwd = shell_single_quote(guest_cwd);
7932 args[1] = format!("cd {quoted_cwd} && {command_text}");
7933 args
7934}
7935
7936fn is_shell_command(command: &str) -> bool {
7937 Path::new(command)
7938 .file_name()
7939 .and_then(|name| name.to_str())
7940 .unwrap_or(command)
7941 .trim_end_matches(".exe")
7942 .eq("sh")
7943 || Path::new(command)
7944 .file_name()
7945 .and_then(|name| name.to_str())
7946 .unwrap_or(command)
7947 .trim_end_matches(".exe")
7948 .eq("bash")
7949}
7950
7951fn shell_single_quote(value: &str) -> String {
7952 if value.is_empty() {
7953 return String::from("''");
7954 }
7955 format!("'{}'", value.replace('\'', "'\"'\"'"))
7956}
7957
7958pub(crate) fn sync_active_process_host_writes_to_kernel(
7959 vm: &mut VmState,
7960) -> Result<(), SidecarError> {
7961 if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
7962 let shadow_root = vm.cwd.clone();
7963 sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
7964 }
7965
7966 let normalized_vm_root = normalize_host_path(&vm.cwd);
7967 let extra_roots = collect_active_process_host_sync_roots(vm, &normalized_vm_root);
7968 for (host_cwd, guest_cwd) in extra_roots {
7969 sync_host_directory_tree_to_kernel(vm, &host_cwd, &guest_cwd)?;
7970 }
7971
7972 Ok(())
7973}
7974
7975fn collect_active_process_host_sync_roots(
7976 vm: &VmState,
7977 normalized_vm_root: &Path,
7978) -> Vec<(PathBuf, String)> {
7979 let mut roots = Vec::new();
7980 let mut seen = BTreeSet::new();
7981
7982 for process in vm.active_processes.values() {
7983 collect_process_host_sync_roots(process, normalized_vm_root, &mut seen, &mut roots);
7984 }
7985
7986 roots
7987}
7988
7989fn collect_process_host_sync_roots(
7990 process: &ActiveProcess,
7991 normalized_vm_root: &Path,
7992 seen: &mut BTreeSet<(PathBuf, String)>,
7993 roots: &mut Vec<(PathBuf, String)>,
7994) {
7995 let normalized_host_cwd = normalize_host_path(&process.host_cwd);
7996 if !path_is_within_root(&normalized_host_cwd, normalized_vm_root) {
7997 let guest_cwd = normalize_path(&process.guest_cwd);
7998 if seen.insert((normalized_host_cwd.clone(), guest_cwd.clone())) {
7999 roots.push((normalized_host_cwd, guest_cwd));
8000 }
8001 }
8002
8003 for child in process.child_processes.values() {
8004 collect_process_host_sync_roots(child, normalized_vm_root, seen, roots);
8005 }
8006}
8007
8008fn sync_process_host_writes_to_kernel(
8009 vm: &mut VmState,
8010 process: &ActiveProcess,
8011) -> Result<(), SidecarError> {
8012 if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
8013 let shadow_root = vm.cwd.clone();
8014 sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
8015 }
8016
8017 if !path_is_within_root(
8018 &normalize_host_path(&process.host_cwd),
8019 &normalize_host_path(&vm.cwd),
8020 ) {
8021 sync_host_directory_tree_to_kernel(vm, &process.host_cwd, &process.guest_cwd)?;
8022 }
8023
8024 Ok(())
8025}
8026
8027fn sync_host_directory_tree_to_kernel(
8028 vm: &mut VmState,
8029 host_root: &Path,
8030 guest_root: &str,
8031) -> Result<(), SidecarError> {
8032 let normalized_host_root = normalize_host_path(host_root);
8033 let normalized_guest_root = normalize_path(guest_root);
8034 let mut synced_file_times = BTreeMap::new();
8035 sync_host_directory_tree_to_kernel_inner(
8036 vm,
8037 &normalized_host_root,
8038 &normalized_host_root,
8039 &normalized_guest_root,
8040 &mut synced_file_times,
8041 )
8042}
8043
8044fn sync_host_directory_tree_to_kernel_inner(
8045 vm: &mut VmState,
8046 host_root: &Path,
8047 current_host_dir: &Path,
8048 guest_root: &str,
8049 synced_file_times: &mut BTreeMap<(u64, u64), (u64, u64)>,
8050) -> Result<(), SidecarError> {
8051 let entries = match fs::read_dir(current_host_dir) {
8052 Ok(entries) => entries,
8053 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
8054 Err(error) => {
8055 return Err(SidecarError::Io(format!(
8056 "failed to read host shadow directory {}: {error}",
8057 current_host_dir.display()
8058 )));
8059 }
8060 };
8061
8062 for entry in entries {
8063 let entry = entry.map_err(|error| {
8064 SidecarError::Io(format!(
8065 "failed to read host shadow entry in {}: {error}",
8066 current_host_dir.display()
8067 ))
8068 })?;
8069 let host_path = entry.path();
8070 let file_type = entry.file_type().map_err(|error| {
8071 SidecarError::Io(format!(
8072 "failed to stat host shadow entry {}: {error}",
8073 host_path.display()
8074 ))
8075 })?;
8076 let relative_path = host_path
8077 .strip_prefix(host_root)
8078 .map_err(|error| {
8079 SidecarError::InvalidState(format!(
8080 "failed to relativize host shadow path {} against {}: {error}",
8081 host_path.display(),
8082 host_root.display()
8083 ))
8084 })?
8085 .to_string_lossy()
8086 .replace('\\', "/");
8087 let guest_path = if guest_root == "/" {
8088 normalize_path(&format!("/{relative_path}"))
8089 } else {
8090 normalize_path(&format!(
8091 "{}/{}",
8092 guest_root.trim_end_matches('/'),
8093 relative_path
8094 ))
8095 };
8096
8097 if should_skip_shadow_sync_path(vm, &guest_path) {
8098 continue;
8099 }
8100
8101 if file_type.is_dir() {
8102 let metadata = entry.metadata().map_err(|error| {
8103 SidecarError::Io(format!(
8104 "failed to read host shadow metadata {}: {error}",
8105 host_path.display()
8106 ))
8107 })?;
8108 if !is_shadow_bootstrap_dir(&guest_path)
8109 && !vm.kernel.exists(&guest_path).unwrap_or(false)
8110 {
8111 vm.kernel.mkdir(&guest_path, true).map_err(|error| {
8112 SidecarError::InvalidState(format!(
8113 "failed to sync host shadow directory {} to guest {}: {}",
8114 host_path.display(),
8115 guest_path,
8116 kernel_error(error)
8117 ))
8118 })?;
8119 vm.kernel
8120 .chmod(&guest_path, host_shadow_mode(&metadata))
8121 .map_err(|error| {
8122 SidecarError::InvalidState(format!(
8123 "failed to sync host shadow directory mode {} to guest {}: {}",
8124 host_path.display(),
8125 guest_path,
8126 kernel_error(error)
8127 ))
8128 })?;
8129 }
8130 sync_host_directory_tree_to_kernel_inner(
8131 vm,
8132 host_root,
8133 &host_path,
8134 guest_root,
8135 synced_file_times,
8136 )?;
8137 continue;
8138 }
8139
8140 if file_type.is_file() {
8141 let metadata = entry.metadata().map_err(|error| {
8142 SidecarError::Io(format!(
8143 "failed to read host shadow metadata {}: {error}",
8144 host_path.display()
8145 ))
8146 })?;
8147 let timestamp_key = (metadata.dev(), metadata.ino());
8148 let (atime_ms, mtime_ms) =
8149 *synced_file_times.entry(timestamp_key).or_insert_with(|| {
8150 (
8151 metadata_time_ms(metadata.atime(), metadata.atime_nsec()),
8152 metadata_time_ms(metadata.mtime(), metadata.mtime_nsec()),
8153 )
8154 });
8155 let desired_mode = host_shadow_mode(&metadata);
8156 let bytes = read_host_shadow_file(&host_path, desired_mode).map_err(|error| {
8157 SidecarError::Io(format!(
8158 "failed to read host shadow file {}: {error}",
8159 host_path.display()
8160 ))
8161 })?;
8162 vm.kernel.write_file(&guest_path, bytes).map_err(|error| {
8163 SidecarError::InvalidState(format!(
8164 "failed to sync host shadow file {} to guest {}: {}",
8165 host_path.display(),
8166 guest_path,
8167 kernel_error(error)
8168 ))
8169 })?;
8170 vm.kernel
8171 .chmod(&guest_path, desired_mode)
8172 .map_err(|error| {
8173 SidecarError::InvalidState(format!(
8174 "failed to sync host shadow file mode {} to guest {}: {}",
8175 host_path.display(),
8176 guest_path,
8177 kernel_error(error)
8178 ))
8179 })?;
8180 vm.kernel
8181 .utimes(&guest_path, atime_ms, mtime_ms)
8182 .map_err(|error| {
8183 SidecarError::InvalidState(format!(
8184 "failed to sync host shadow file times {} to guest {}: {}",
8185 host_path.display(),
8186 guest_path,
8187 kernel_error(error)
8188 ))
8189 })?;
8190 continue;
8191 }
8192
8193 if file_type.is_symlink() {
8194 let target = match fs::read_link(&host_path) {
8195 Ok(target) => target,
8196 Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
8197 Err(error) => {
8198 return Err(SidecarError::Io(format!(
8199 "failed to read host shadow symlink {}: {error}",
8200 host_path.display()
8201 )));
8202 }
8203 };
8204 replace_kernel_symlink(vm, &guest_path, &target.to_string_lossy())?;
8205 }
8206 }
8207
8208 Ok(())
8209}
8210
8211fn replace_kernel_symlink(
8212 vm: &mut VmState,
8213 guest_path: &str,
8214 target: &str,
8215) -> Result<(), SidecarError> {
8216 if vm.kernel.symlink(target, guest_path).is_ok() {
8217 return Ok(());
8218 }
8219
8220 if let Ok(existing_target) = vm.kernel.read_link(guest_path) {
8221 if existing_target == target {
8222 return Ok(());
8223 }
8224 }
8225
8226 let _ = vm.kernel.remove_file(guest_path);
8227 let _ = vm.kernel.remove_dir(guest_path);
8228 vm.kernel
8229 .symlink(target, guest_path)
8230 .map_err(kernel_error)?;
8231 Ok(())
8232}
8233
8234fn host_shadow_mode(metadata: &fs::Metadata) -> u32 {
8235 metadata.permissions().mode() & 0o7777
8236}
8237
8238fn read_host_shadow_file(host_path: &Path, mode: u32) -> std::io::Result<Vec<u8>> {
8244 match fs::read(host_path) {
8245 Ok(bytes) => Ok(bytes),
8246 Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
8247 fs::set_permissions(host_path, fs::Permissions::from_mode(mode | 0o400))?;
8248 let result = fs::read(host_path);
8249 fs::set_permissions(host_path, fs::Permissions::from_mode(mode))?;
8250 result
8251 }
8252 Err(error) => Err(error),
8253 }
8254}
8255
8256fn metadata_time_ms(seconds: i64, nanos: i64) -> u64 {
8257 let seconds = seconds.max(0) as u64;
8258 let nanos = nanos.max(0) as u64;
8259 seconds
8260 .saturating_mul(1_000)
8261 .saturating_add(nanos / 1_000_000)
8262}
8263
8264fn is_shadow_bootstrap_dir(path: &str) -> bool {
8265 matches!(
8266 path,
8267 "/dev"
8268 | "/proc"
8269 | "/tmp"
8270 | "/bin"
8271 | "/lib"
8272 | "/sbin"
8273 | "/boot"
8274 | "/etc"
8275 | "/root"
8276 | "/run"
8277 | "/srv"
8278 | "/sys"
8279 | "/opt"
8280 | "/mnt"
8281 | "/media"
8282 | "/home"
8283 | "/home/agentos"
8284 | "/usr"
8285 | "/usr/bin"
8286 | "/usr/games"
8287 | "/usr/include"
8288 | "/usr/lib"
8289 | "/usr/libexec"
8290 | "/usr/man"
8291 | "/usr/local"
8292 | "/usr/local/bin"
8293 | "/usr/sbin"
8294 | "/usr/share"
8295 | "/usr/share/man"
8296 | "/var"
8297 | "/var/cache"
8298 | "/var/empty"
8299 | "/var/lib"
8300 | "/var/lock"
8301 | "/var/log"
8302 | "/var/run"
8303 | "/var/spool"
8304 | "/var/tmp"
8305 | "/etc/agentos"
8306 | "/workspace"
8307 )
8308}
8309
8310#[cfg(test)]
8311mod shadow_sync_tests {
8312 use super::{is_protected_agentos_shadow_sync_path, is_shadow_bootstrap_dir};
8313
8314 #[test]
8315 fn shadow_bootstrap_sync_skips_virtual_home_tree() {
8316 assert!(is_shadow_bootstrap_dir("/home"));
8317 assert!(is_shadow_bootstrap_dir("/home/agentos"));
8318 }
8319
8320 #[test]
8321 fn protected_agentos_paths_are_not_shadow_synced() {
8322 assert!(is_protected_agentos_shadow_sync_path("/etc/agentos"));
8323 assert!(is_protected_agentos_shadow_sync_path(
8324 "/etc/agentos/instructions.md"
8325 ));
8326 assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos-copy"));
8327 assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos.md"));
8328 }
8329}
8330
8331fn is_kernel_owned_shadow_sync_path(path: &str) -> bool {
8332 matches!(path, "/dev" | "/proc" | "/sys")
8333 || path.starts_with("/dev/")
8334 || path.starts_with("/proc/")
8335 || path.starts_with("/sys/")
8336}
8337
8338pub(crate) fn is_protected_agentos_shadow_sync_path(path: &str) -> bool {
8339 path == "/etc/agentos" || path.starts_with("/etc/agentos/")
8340}
8341
8342fn should_skip_shadow_sync_path(vm: &VmState, guest_path: &str) -> bool {
8343 is_kernel_owned_shadow_sync_path(guest_path)
8344 || is_protected_agentos_shadow_sync_path(guest_path)
8345 || host_mount_path_for_guest_path_from_mounts(&vm.configuration.mounts, guest_path)
8346 .is_some()
8347}
8348
8349fn resolve_path_like_guest_specifier(cwd: &str, specifier: &str) -> String {
8350 if specifier.starts_with("file://") {
8351 normalize_path(specifier.trim_start_matches("file://"))
8352 } else if specifier.starts_with("file:") {
8353 normalize_path(specifier.trim_start_matches("file:"))
8354 } else if specifier.starts_with('/') {
8355 normalize_path(specifier)
8356 } else {
8357 normalize_path(&format!("{cwd}/{specifier}"))
8358 }
8359}
8360
8361fn guest_entrypoint_for_specifier(cwd: &str, specifier: &str) -> Option<String> {
8362 is_path_like_specifier(specifier).then(|| resolve_path_like_guest_specifier(cwd, specifier))
8363}
8364
8365fn is_node_runtime_command(command: &str) -> bool {
8366 matches!(command, "node" | "npm" | "npx")
8367 || Path::new(command)
8368 .file_name()
8369 .and_then(|name| name.to_str())
8370 .is_some_and(|name| matches!(name, "node" | "npm" | "npx"))
8371}
8372
8373fn resolve_special_node_cli_invocation(
8374 args: &[String],
8375 env: &mut BTreeMap<String, String>,
8376) -> Option<(String, Vec<String>)> {
8377 let first = args.first()?;
8378 match first.as_str() {
8379 "-e" | "--eval" => {
8380 env.insert(
8381 String::from("AGENTOS_NODE_EVAL"),
8382 args.get(1).cloned().unwrap_or_default(),
8383 );
8384 Some((first.clone(), args.iter().skip(2).cloned().collect()))
8385 }
8386 "-v" | "--version" => {
8387 env.insert(
8388 String::from("AGENTOS_NODE_EVAL"),
8389 String::from("console.log(process.version);"),
8390 );
8391 Some((String::from("-e"), args.to_vec()))
8392 }
8393 _ => None,
8394 }
8395}
8396
8397fn node_runtime_command_name(command: &str) -> Option<&str> {
8398 let name = Path::new(command)
8399 .file_name()
8400 .and_then(|name| name.to_str())?;
8401 matches!(name, "node" | "npm" | "npx").then_some(name)
8402}
8403
8404struct ResolvedHostNodeCliEntrypoint {
8405 command_name: String,
8406 guest_root: String,
8407 guest_entrypoint: String,
8408 package_root: PathBuf,
8409}
8410
8411fn resolve_host_node_cli_entrypoint(command: &str) -> Option<ResolvedHostNodeCliEntrypoint> {
8412 let command_name = node_runtime_command_name(command)?;
8413 if !matches!(command_name, "npm" | "npx") {
8414 return None;
8415 }
8416
8417 let path = std::env::var_os("PATH")?;
8418 for root in std::env::split_paths(&path) {
8419 let candidate = root.join(command_name);
8420 if !candidate.is_file() {
8421 continue;
8422 }
8423 let entrypoint = candidate.canonicalize().ok().unwrap_or(candidate);
8424 let package_root = entrypoint.parent()?.parent()?.to_path_buf();
8425 let guest_root = format!("/__secure_exec/node-runtime/{command_name}");
8426 let relative_entrypoint = entrypoint.strip_prefix(&package_root).ok()?;
8427 let guest_entrypoint = normalize_path(&format!(
8428 "{guest_root}/{}",
8429 relative_entrypoint.to_string_lossy().replace('\\', "/")
8430 ));
8431 return Some(ResolvedHostNodeCliEntrypoint {
8432 command_name: command_name.to_owned(),
8433 guest_root,
8434 guest_entrypoint,
8435 package_root,
8436 });
8437 }
8438
8439 None
8440}
8441
8442fn build_host_node_cli_eval(cli: &ResolvedHostNodeCliEntrypoint) -> String {
8443 let guest_npm_main = normalize_path(&format!("{}/lib/npm.js", cli.guest_root));
8444 let guest_npm_cli = normalize_path(&format!("{}/bin/npm-cli.js", cli.guest_root));
8445 let guest_package_json = normalize_path(&format!("{}/package.json", cli.guest_root));
8446 let guest_display_module = normalize_path(&format!("{}/lib/utils/display.js", cli.guest_root));
8447 let guest_log_file_module =
8448 normalize_path(&format!("{}/lib/utils/log-file.js", cli.guest_root));
8449 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); } } }";
8450 let display_stub = format!(
8451 "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 }};",
8452 display_module = serde_json::to_string(&guest_display_module)
8453 .unwrap_or_else(|_| format!("\"{guest_display_module}\"")),
8454 log_file_module = serde_json::to_string(&guest_log_file_module)
8455 .unwrap_or_else(|_| format!("\"{guest_log_file_module}\"")),
8456 );
8457 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)); }";
8458 match cli.command_name.as_str() {
8459 "npx" => format!(
8460 "{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); }});",
8461 debug_preamble = debug_preamble,
8462 display_stub = display_stub,
8463 registry_fetch_stub = registry_fetch_stub.replace(
8464 "__AGENTOS_NPM_MAIN__",
8465 &serde_json::to_string(&guest_npm_main)
8466 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8467 ),
8468 npm_main = serde_json::to_string(&guest_npm_main)
8469 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8470 npm_cli = serde_json::to_string(&guest_npm_cli)
8471 .unwrap_or_else(|_| format!("\"{guest_npm_cli}\"")),
8472 package_json = serde_json::to_string(&guest_package_json)
8473 .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
8474 ),
8475 _ => format!(
8476 "{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); }});",
8477 debug_preamble = debug_preamble,
8478 display_stub = display_stub,
8479 registry_fetch_stub = registry_fetch_stub.replace(
8480 "__AGENTOS_NPM_MAIN__",
8481 &serde_json::to_string(&guest_npm_main)
8482 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8483 ),
8484 npm_main = serde_json::to_string(&guest_npm_main)
8485 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8486 package_json = serde_json::to_string(&guest_package_json)
8487 .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
8488 ),
8489 }
8490}
8491
8492fn resolve_guest_command_entrypoint(
8493 vm: &VmState,
8494 guest_cwd: &str,
8495 command: &str,
8496 path_env: Option<&str>,
8497) -> Option<String> {
8498 if !is_path_like_specifier(command) {
8499 if let Some(entrypoint) = vm.command_guest_paths.get(command) {
8500 return Some(entrypoint.clone());
8501 }
8502
8503 for search_dir in guest_command_search_dirs(vm, guest_cwd, path_env) {
8504 let candidate = normalize_path(&format!("{search_dir}/{command}"));
8505 if let Some(entrypoint) = resolve_guest_command_path_candidate(vm, &candidate) {
8506 return Some(entrypoint);
8507 }
8508 }
8509
8510 return None;
8511 }
8512
8513 let normalized = resolve_path_like_guest_specifier(guest_cwd, command);
8514 resolve_guest_command_path_candidate(vm, &normalized).or_else(|| {
8515 let parent_dir = Path::new(&normalized).parent()?.to_str()?;
8519 if !guest_command_search_dirs(vm, guest_cwd, path_env)
8520 .iter()
8521 .any(|search_dir| normalize_path(search_dir) == normalize_path(parent_dir))
8522 {
8523 return None;
8524 }
8525
8526 let file_name = Path::new(&normalized).file_name()?.to_str()?;
8527 vm.command_guest_paths.get(file_name).cloned()
8528 })
8529}
8530
8531fn guest_command_search_dirs(vm: &VmState, guest_cwd: &str, path_env: Option<&str>) -> Vec<String> {
8532 let mut search_dirs = Vec::new();
8533 let mut seen = BTreeSet::new();
8534
8535 if let Some(path) = path_env.or_else(|| vm.guest_env.get("PATH").map(String::as_str)) {
8536 for segment in path.split(':') {
8537 let trimmed = segment.trim();
8538 if trimmed.is_empty() {
8539 continue;
8540 }
8541 let normalized = if trimmed.starts_with('/') {
8542 normalize_path(trimmed)
8543 } else {
8544 normalize_path(&format!("{guest_cwd}/{trimmed}"))
8545 };
8546 if seen.insert(normalized.clone()) {
8547 search_dirs.push(normalized);
8548 }
8549 }
8550 }
8551
8552 for fallback in ["/bin", "/usr/bin", "/usr/local/bin"] {
8553 let normalized = String::from(fallback);
8554 if seen.insert(normalized.clone()) {
8555 search_dirs.push(normalized);
8556 }
8557 }
8558
8559 search_dirs
8560}
8561
8562fn resolve_guest_command_path_candidate(vm: &VmState, candidate: &str) -> Option<String> {
8563 if candidate.starts_with("/bin/")
8564 || candidate.starts_with("/usr/bin/")
8565 || candidate.starts_with("/usr/local/bin/")
8566 || candidate.starts_with("/__secure_exec/commands/")
8567 {
8568 if let Some(file_name) = Path::new(candidate)
8569 .file_name()
8570 .and_then(|name| name.to_str())
8571 {
8572 if let Some(guest_entrypoint) = vm.command_guest_paths.get(file_name) {
8573 return Some(guest_entrypoint.clone());
8574 }
8575 }
8576 }
8577
8578 if vm
8579 .kernel
8580 .exists(candidate)
8581 .ok()
8582 .is_some_and(|exists| exists)
8583 {
8584 return Some(normalize_path(candidate));
8585 }
8586
8587 resolve_vm_guest_path_to_host(vm, candidate)
8588 .is_file()
8589 .then(|| normalize_path(candidate))
8590}
8591
8592fn resolve_host_entrypoint_within_vm_host_cwd(
8593 vm: &VmState,
8594 specifier: &str,
8595) -> Option<(String, String)> {
8596 let candidate = Path::new(specifier);
8597 if !candidate.is_absolute() {
8598 return None;
8599 }
8600
8601 let normalized_entrypoint = normalize_host_path(candidate);
8602 let normalized_host_cwd = normalize_host_path(&vm.host_cwd);
8603 if !path_is_within_root(&normalized_entrypoint, &normalized_host_cwd) {
8604 return None;
8605 }
8606
8607 let relative = normalized_entrypoint
8608 .strip_prefix(&normalized_host_cwd)
8609 .ok()?
8610 .to_string_lossy()
8611 .replace('\\', "/");
8612 let guest_entrypoint = if relative.is_empty() {
8613 String::from("/")
8614 } else {
8615 normalize_path(&format!("/{relative}"))
8616 };
8617 Some((
8618 guest_entrypoint,
8619 normalized_entrypoint.to_string_lossy().into_owned(),
8620 ))
8621}
8622
8623fn prepare_guest_runtime_env(
8624 vm: &VmState,
8625 env: &mut BTreeMap<String, String>,
8626 guest_cwd: &str,
8627 host_cwd: &Path,
8628 guest_entrypoint: Option<String>,
8629) -> Result<(), SidecarError> {
8630 let user = vm.kernel.user_profile();
8631 let path_mappings = runtime_guest_path_mappings(vm);
8632 let read_paths = expand_host_access_paths(
8633 std::iter::once(vm.cwd.clone())
8634 .chain(
8635 path_mappings
8636 .iter()
8637 .map(|mapping| PathBuf::from(&mapping.host_path)),
8638 )
8639 .chain(std::iter::once(host_cwd.to_path_buf()))
8640 .collect::<Vec<_>>()
8641 .as_slice(),
8642 );
8643 let write_paths = dedupe_host_paths(
8644 std::iter::once(vm.cwd.clone())
8645 .chain(std::iter::once(host_cwd.to_path_buf()))
8646 .chain(runtime_guest_writable_host_paths(vm))
8647 .collect::<Vec<_>>()
8648 .as_slice(),
8649 );
8650 let allowed_node_builtins = configured_allowed_node_builtins(vm);
8651 let loopback_exempt_ports = configured_loopback_exempt_ports(vm);
8652
8653 env.insert(
8654 String::from("AGENTOS_GUEST_PATH_MAPPINGS"),
8655 serde_json::to_string(&path_mappings).map_err(|error| {
8656 SidecarError::InvalidState(format!("failed to encode guest path mappings: {error}"))
8657 })?,
8658 );
8659 env.entry(String::from(EXECUTION_SANDBOX_ROOT_ENV))
8660 .or_insert_with(|| normalize_host_path(&vm.cwd).to_string_lossy().into_owned());
8661 env.insert(
8662 String::from("AGENTOS_EXTRA_FS_READ_PATHS"),
8663 serde_json::to_string(
8664 &read_paths
8665 .iter()
8666 .map(|path| path.to_string_lossy().into_owned())
8667 .collect::<Vec<_>>(),
8668 )
8669 .map_err(|error| {
8670 SidecarError::InvalidState(format!("failed to encode read paths: {error}"))
8671 })?,
8672 );
8673 env.insert(
8674 String::from("AGENTOS_EXTRA_FS_WRITE_PATHS"),
8675 serde_json::to_string(
8676 &write_paths
8677 .iter()
8678 .map(|path| path.to_string_lossy().into_owned())
8679 .collect::<Vec<_>>(),
8680 )
8681 .map_err(|error| {
8682 SidecarError::InvalidState(format!("failed to encode write paths: {error}"))
8683 })?,
8684 );
8685 env.insert(
8686 String::from("AGENTOS_ALLOWED_NODE_BUILTINS"),
8687 serde_json::to_string(&allowed_node_builtins).map_err(|error| {
8688 SidecarError::InvalidState(format!("failed to encode allowed builtins: {error}"))
8689 })?,
8690 );
8691 env.insert(
8694 String::from("AGENTOS_JS_PLATFORM"),
8695 js_runtime_platform_env(vm).to_owned(),
8696 );
8697 if let Some(resolution) = js_runtime_module_resolution_env(vm) {
8699 env.insert(
8700 String::from("AGENTOS_JS_MODULE_RESOLUTION"),
8701 resolution.to_owned(),
8702 );
8703 }
8704 if let Some(allowlist) = js_runtime_enforced_builtins(vm) {
8708 env.insert(
8709 String::from("AGENTOS_JS_BUILTIN_ALLOWLIST"),
8710 serde_json::to_string(&allowlist).map_err(|error| {
8711 SidecarError::InvalidState(format!(
8712 "failed to encode jsRuntime builtin allow-list: {error}"
8713 ))
8714 })?,
8715 );
8716 }
8717 env.entry(String::from("HOME"))
8724 .or_insert_with(|| user.homedir.clone());
8725 env.entry(String::from("USER"))
8726 .or_insert_with(|| user.username.clone());
8727 env.entry(String::from("LOGNAME"))
8728 .or_insert_with(|| user.username.clone());
8729 env.entry(String::from("SHELL"))
8730 .or_insert_with(|| user.shell.clone());
8731 env.entry(String::from("PATH")).or_insert_with(|| {
8732 vm.guest_env
8733 .get("PATH")
8734 .cloned()
8735 .unwrap_or_else(|| crate::vm::DEFAULT_GUEST_PATH_ENV.to_owned())
8736 });
8737 env.entry(String::from("TMPDIR"))
8738 .or_insert_with(|| String::from("/tmp"));
8739 env.insert(String::from("PWD"), guest_cwd.to_owned());
8740 if !loopback_exempt_ports.is_empty() {
8741 env.insert(
8742 String::from(LOOPBACK_EXEMPT_PORTS_ENV),
8743 serde_json::to_string(&loopback_exempt_ports).map_err(|error| {
8744 SidecarError::InvalidState(format!("failed to encode loopback exemptions: {error}"))
8745 })?,
8746 );
8747 }
8748 if let Some(guest_entrypoint) = guest_entrypoint {
8749 env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
8750 }
8751 Ok(())
8752}
8753
8754fn virtual_os_cpu_count(resource_limits: &ResourceLimits) -> usize {
8755 resource_limits.virtual_cpu_count.unwrap_or(1).max(1)
8756}
8757
8758fn virtual_os_totalmem_bytes(resource_limits: &ResourceLimits) -> u64 {
8759 resource_limits
8760 .max_wasm_memory_bytes
8761 .unwrap_or(1024 * 1024 * 1024)
8762}
8763
8764fn virtual_os_freemem_bytes(resource_limits: &ResourceLimits) -> u64 {
8765 resource_limits
8766 .max_wasm_memory_bytes
8767 .unwrap_or(512 * 1024 * 1024)
8768}
8769
8770fn javascript_execution_limits(vm: &VmState) -> JavascriptExecutionLimits {
8775 JavascriptExecutionLimits {
8776 v8_heap_limit_mb: vm.limits.js_runtime.v8_heap_limit_mb,
8777 sync_rpc_wait_timeout_ms: vm.limits.js_runtime.sync_rpc_wait_timeout_ms,
8778 }
8779}
8780
8781fn guest_runtime_identity(
8787 vm: &VmState,
8788 virtual_pid: Option<u64>,
8789 virtual_ppid: Option<u64>,
8790) -> GuestRuntimeConfig {
8791 let user = vm.kernel.user_profile();
8792 let resource_limits = vm.kernel.resource_limits();
8793 GuestRuntimeConfig {
8794 virtual_uid: Some(u64::from(user.uid)),
8795 virtual_gid: Some(u64::from(user.gid)),
8796 virtual_pid,
8797 virtual_ppid,
8798 virtual_exec_path: None,
8799 os_cpu_count: Some(virtual_os_cpu_count(resource_limits) as u64),
8800 os_totalmem: Some(virtual_os_totalmem_bytes(resource_limits)),
8801 os_freemem: Some(virtual_os_freemem_bytes(resource_limits)),
8802 os_homedir: Some(user.homedir.clone()),
8803 os_hostname: None,
8804 os_shell: Some(user.shell.clone()),
8805 os_user: Some(user.username.clone()),
8806 }
8807}
8808
8809fn guest_virtual_home(vm: &VmState) -> String {
8814 let homedir = vm.kernel.user_profile().homedir;
8815 if homedir.starts_with('/') {
8816 homedir
8817 } else {
8818 String::from("/root")
8819 }
8820}
8821
8822fn python_execution_limits(vm: &VmState) -> PythonExecutionLimits {
8824 PythonExecutionLimits {
8825 output_buffer_max_bytes: Some(vm.limits.python.output_buffer_max_bytes),
8826 execution_timeout_ms: Some(vm.limits.python.execution_timeout_ms),
8827 max_old_space_mb: Some(vm.limits.python.max_old_space_mb),
8828 vfs_rpc_timeout_ms: Some(vm.limits.python.vfs_rpc_timeout_ms),
8829 }
8830}
8831
8832fn wasm_execution_limits(vm: &VmState) -> WasmExecutionLimits {
8837 let resource_limits = vm.kernel.resource_limits();
8838 WasmExecutionLimits {
8839 max_fuel: resource_limits.max_wasm_fuel,
8840 max_memory_bytes: resource_limits.max_wasm_memory_bytes,
8841 max_stack_bytes: resource_limits
8842 .max_wasm_stack_bytes
8843 .map(|value| value as u64),
8844 }
8845}
8846
8847fn js_runtime_platform(vm: &VmState) -> vm_config::JsRuntimePlatform {
8850 vm.configuration
8851 .js_runtime
8852 .as_ref()
8853 .map(|cfg| cfg.platform)
8854 .unwrap_or(vm_config::JsRuntimePlatform::Node)
8855}
8856
8857fn js_runtime_platform_env(vm: &VmState) -> &'static str {
8860 match js_runtime_platform(vm) {
8861 vm_config::JsRuntimePlatform::Node => "node",
8862 vm_config::JsRuntimePlatform::Browser => "browser",
8863 vm_config::JsRuntimePlatform::Neutral => "neutral",
8864 vm_config::JsRuntimePlatform::Bare => "bare",
8865 }
8866}
8867
8868fn js_runtime_module_resolution_env(vm: &VmState) -> Option<&'static str> {
8871 let resolution = vm
8872 .configuration
8873 .js_runtime
8874 .as_ref()
8875 .map(|cfg| cfg.module_resolution)
8876 .unwrap_or(vm_config::JsModuleResolution::Node);
8877 match resolution {
8878 vm_config::JsModuleResolution::Node => None,
8879 vm_config::JsModuleResolution::Relative => Some("relative"),
8880 vm_config::JsModuleResolution::None => Some("none"),
8881 }
8882}
8883
8884fn js_runtime_enforced_builtins(vm: &VmState) -> Option<Vec<String>> {
8888 if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
8889 return Some(Vec::new());
8890 }
8891 vm.configuration
8892 .js_runtime
8893 .as_ref()
8894 .and_then(|cfg| cfg.allowed_builtins.clone())
8895}
8896
8897fn configured_allowed_node_builtins(vm: &VmState) -> Vec<String> {
8898 if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
8900 return Vec::new();
8901 }
8902 let configured = match vm
8905 .configuration
8906 .js_runtime
8907 .as_ref()
8908 .and_then(|cfg| cfg.allowed_builtins.as_ref())
8909 {
8910 Some(list) => list.clone(),
8911 None => DEFAULT_ALLOWED_NODE_BUILTINS
8912 .iter()
8913 .map(|value| (*value).to_owned())
8914 .collect::<Vec<_>>(),
8915 };
8916 dedupe_strings(&configured)
8917}
8918
8919fn configured_loopback_exempt_ports(vm: &VmState) -> Vec<String> {
8920 if !vm.configuration.loopback_exempt_ports.is_empty() {
8921 return vm
8922 .configuration
8923 .loopback_exempt_ports
8924 .iter()
8925 .map(ToString::to_string)
8926 .collect();
8927 }
8928
8929 vm.create_loopback_exempt_ports
8930 .iter()
8931 .map(ToString::to_string)
8932 .collect()
8933}
8934
8935fn mount_config_host_path(config: &str) -> Option<String> {
8937 serde_json::from_str::<Value>(config)
8938 .ok()?
8939 .get("hostPath")
8940 .and_then(Value::as_str)
8941 .map(str::to_owned)
8942}
8943
8944fn runtime_guest_writable_host_paths(vm: &VmState) -> Vec<PathBuf> {
8945 vm.configuration
8946 .mounts
8947 .iter()
8948 .filter(|mount| !mount.read_only)
8949 .filter_map(|mount| {
8950 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
8951 .then(|| mount_config_host_path(&mount.plugin.config))
8952 .flatten()
8953 .map(PathBuf::from)
8954 })
8955 .collect()
8956}
8957
8958fn runtime_guest_path_mappings(vm: &VmState) -> Vec<RuntimeGuestPathMapping> {
8959 let mut mappings = vm
8960 .configuration
8961 .mounts
8962 .iter()
8963 .filter_map(|mount| {
8964 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
8965 .then(|| {
8966 mount_config_host_path(&mount.plugin.config).map(|host_path| {
8967 RuntimeGuestPathMapping {
8968 guest_path: normalize_path(&mount.guest_path),
8969 host_path,
8970 read_only: mount.read_only,
8971 }
8972 })
8973 })
8974 .flatten()
8975 })
8976 .collect::<Vec<_>>();
8977 let mut command_root_mappings = vm
8978 .command_guest_paths
8979 .values()
8980 .filter_map(|guest_path| {
8981 Path::new(guest_path)
8982 .parent()
8983 .and_then(|parent| parent.to_str())
8984 .map(normalize_path)
8985 })
8986 .collect::<BTreeSet<_>>()
8987 .into_iter()
8988 .map(|guest_path| RuntimeGuestPathMapping {
8989 host_path: resolve_vm_guest_path_to_host(vm, &guest_path)
8990 .to_string_lossy()
8991 .into_owned(),
8992 guest_path,
8993 read_only: false,
8994 })
8995 .collect::<Vec<_>>();
8996 mappings.append(&mut command_root_mappings);
8997 let mut extra_node_modules_roots = mappings
8998 .iter()
8999 .filter(|mapping| mapping.guest_path.starts_with("/root/node_modules/"))
9000 .filter_map(|mapping| {
9001 host_node_modules_root(Path::new(&mapping.host_path)).map(|host_root| {
9002 RuntimeGuestPathMapping {
9003 guest_path: String::from("/root/node_modules"),
9004 host_path: host_root.to_string_lossy().into_owned(),
9005 read_only: mapping.read_only,
9006 }
9007 })
9008 })
9009 .collect::<Vec<_>>();
9010 mappings.append(&mut extra_node_modules_roots);
9011 mappings.push(RuntimeGuestPathMapping {
9012 guest_path: String::from("/"),
9013 host_path: vm.cwd.to_string_lossy().into_owned(),
9014 read_only: false,
9015 });
9016 mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.guest_path.len()));
9017 mappings.dedup_by(|left, right| {
9018 left.guest_path == right.guest_path && left.host_path == right.host_path
9019 });
9020 mappings
9021}
9022
9023fn build_module_reader(
9034 vm: &VmState,
9035 resolved: &ResolvedChildProcessExecution,
9036) -> Option<crate::plugins::host_dir::HostDirModuleReader> {
9037 let mut pairs: Vec<(String, PathBuf)> = vm
9038 .configuration
9039 .mounts
9040 .iter()
9041 .filter(|mount| mount.read_only)
9042 .filter(|mount| (mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
9043 .filter_map(|mount| {
9044 mount_config_host_path(&mount.plugin.config)
9045 .map(|host_path| (normalize_path(&mount.guest_path), PathBuf::from(host_path)))
9046 })
9047 .collect();
9048
9049 let guest_entrypoint = resolved
9050 .env
9051 .get("AGENTOS_GUEST_ENTRYPOINT")
9052 .map(|path| normalize_path(path));
9053 if let Some(guest_entrypoint) = guest_entrypoint.as_deref() {
9054 let entrypoint_in_read_only_mount = pairs.iter().any(|(guest_path, _)| {
9055 guest_entrypoint == guest_path
9056 || guest_entrypoint.starts_with(&format!("{guest_path}/"))
9057 });
9058 if !entrypoint_in_read_only_mount {
9059 return None;
9060 }
9061 }
9062
9063 let extra_roots: Vec<(String, PathBuf)> = pairs
9067 .iter()
9068 .filter(|(guest_path, _)| guest_path.starts_with("/root/node_modules/"))
9069 .filter_map(|(_, host_path)| {
9070 host_node_modules_root(host_path).map(|root| (String::from("/root/node_modules"), root))
9071 })
9072 .collect();
9073 pairs.extend(extra_roots);
9074
9075 crate::plugins::host_dir::HostDirModuleReader::from_mounts(pairs)
9076}
9077
9078fn host_node_modules_root(path: &Path) -> Option<PathBuf> {
9079 if let Some(root) = path
9080 .ancestors()
9081 .filter(|candidate| {
9082 candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
9083 })
9084 .last()
9085 .map(Path::to_path_buf)
9086 {
9087 return Some(root);
9088 }
9089
9090 fs::canonicalize(path)
9091 .ok()?
9092 .ancestors()
9093 .filter(|candidate| {
9094 candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
9095 })
9096 .last()
9097 .map(Path::to_path_buf)
9098}
9099
9100#[cfg(test)]
9101mod runtime_guest_path_mapping_tests {
9102 use super::{host_node_modules_root, javascript_sync_rpc_option_bool};
9103 use serde_json::json;
9104 use std::fs;
9105 use std::time::{SystemTime, UNIX_EPOCH};
9106
9107 #[test]
9108 fn host_node_modules_root_prefers_workspace_root_over_pnpm_package_node_modules() {
9109 let unique = SystemTime::now()
9110 .duration_since(UNIX_EPOCH)
9111 .expect("clock should be monotonic")
9112 .as_nanos();
9113 let temp = std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-{unique}"));
9114 let workspace_node_modules = temp.join("node_modules");
9115 let package_root = workspace_node_modules
9116 .join(".pnpm")
9117 .join("example@1.0.0")
9118 .join("node_modules")
9119 .join("@scope")
9120 .join("pkg");
9121 fs::create_dir_all(&package_root).expect("package root should be created");
9122
9123 let resolved =
9124 host_node_modules_root(&package_root).expect("node_modules root should resolve");
9125
9126 assert_eq!(resolved, workspace_node_modules);
9127
9128 fs::remove_dir_all(&temp).expect("temp tree should be removed");
9129 }
9130
9131 #[test]
9132 fn host_node_modules_root_preserves_symlinked_workspace_node_modules_path() {
9133 let unique = SystemTime::now()
9134 .duration_since(UNIX_EPOCH)
9135 .expect("clock should be monotonic")
9136 .as_nanos();
9137 let temp =
9138 std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-symlink-{unique}"));
9139 let workspace_node_modules = temp.join("node_modules");
9140 let package_link = workspace_node_modules.join("@scope").join("pkg");
9141 let real_package = temp.join("registry").join("agent").join("pkg");
9142 fs::create_dir_all(package_link.parent().expect("package parent should exist"))
9143 .expect("scoped parent should be created");
9144 fs::create_dir_all(&real_package).expect("real package root should be created");
9145 std::os::unix::fs::symlink(&real_package, &package_link)
9146 .expect("package symlink should be created");
9147
9148 let resolved =
9149 host_node_modules_root(&package_link).expect("node_modules root should resolve");
9150
9151 assert_eq!(resolved, workspace_node_modules);
9152
9153 fs::remove_dir_all(&temp).expect("temp tree should be removed");
9154 }
9155
9156 #[test]
9157 fn javascript_sync_rpc_option_bool_accepts_boolean_recursive_argument() {
9158 assert_eq!(
9159 javascript_sync_rpc_option_bool(&[json!("/workspace"), json!(true)], 1, "recursive"),
9160 Some(true)
9161 );
9162 assert_eq!(
9163 javascript_sync_rpc_option_bool(
9164 &[json!("/workspace"), json!({ "recursive": false })],
9165 1,
9166 "recursive"
9167 ),
9168 Some(false)
9169 );
9170 }
9171}
9172
9173#[cfg(test)]
9174mod kernel_poll_sync_rpc_tests {
9175 use super::{
9176 service_javascript_kernel_poll_sync_rpc, ActiveExecution, ActiveProcess,
9177 JavascriptSyncRpcRequest, KernelPollFdResponse, SidecarKernel, ToolExecution,
9178 EXECUTION_DRIVER_NAME, JAVASCRIPT_COMMAND,
9179 };
9180 use secure_exec_kernel::command_registry::CommandDriver;
9181 use secure_exec_kernel::kernel::{KernelVmConfig, SpawnOptions};
9182 use secure_exec_kernel::mount_table::MountTable;
9183 use secure_exec_kernel::permissions::Permissions;
9184 use secure_exec_kernel::poll::{POLLHUP, POLLIN};
9185 use secure_exec_kernel::vfs::MemoryFileSystem;
9186 use serde_json::{json, Value};
9187 #[test]
9188 fn javascript_kernel_poll_sync_rpc_reports_multiple_kernel_fds() {
9189 let mut config = KernelVmConfig::new("vm-js-kernel-poll");
9190 config.permissions = Permissions::allow_all();
9191 let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config);
9192 kernel
9193 .register_driver(CommandDriver::new(
9194 EXECUTION_DRIVER_NAME,
9195 [JAVASCRIPT_COMMAND],
9196 ))
9197 .expect("register execution driver");
9198
9199 let kernel_handle = kernel
9200 .spawn_process(
9201 JAVASCRIPT_COMMAND,
9202 Vec::new(),
9203 SpawnOptions {
9204 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
9205 ..SpawnOptions::default()
9206 },
9207 )
9208 .expect("spawn javascript kernel process");
9209 let pid = kernel_handle.pid();
9210
9211 let (stdin_read_fd, stdin_write_fd) = kernel
9212 .open_pipe(EXECUTION_DRIVER_NAME, pid)
9213 .expect("open kernel stdin pipe");
9214 kernel
9215 .fd_dup2(EXECUTION_DRIVER_NAME, pid, stdin_read_fd, 0)
9216 .expect("dup stdin pipe onto fd 0");
9217 kernel
9218 .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_read_fd)
9219 .expect("close original stdin read fd");
9220
9221 let process = ActiveProcess::new(
9222 pid,
9223 kernel_handle,
9224 super::GuestRuntimeKind::JavaScript,
9225 ActiveExecution::Tool(ToolExecution::default()),
9226 );
9227
9228 kernel
9229 .fd_write(EXECUTION_DRIVER_NAME, pid, stdin_write_fd, b"poll-ready")
9230 .expect("write kernel stdin payload");
9231 kernel
9232 .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_write_fd)
9233 .expect("close kernel stdin writer");
9234
9235 let response = service_javascript_kernel_poll_sync_rpc(
9236 &mut kernel,
9237 &process,
9238 &JavascriptSyncRpcRequest {
9239 id: 1,
9240 method: String::from("__kernel_poll"),
9241 args: vec![
9242 json!([
9243 { "fd": 0, "events": POLLIN.bits() },
9244 { "fd": 1, "events": POLLIN.bits() }
9245 ]),
9246 json!(250),
9247 ],
9248 },
9249 )
9250 .expect("poll kernel fds");
9251
9252 assert_eq!(response["readyCount"], Value::from(1));
9253 let fds: Vec<KernelPollFdResponse> =
9254 serde_json::from_value(response["fds"].clone()).expect("kernel poll fd response");
9255 assert_eq!(
9256 fds,
9257 vec![
9258 KernelPollFdResponse {
9259 fd: 0,
9260 events: POLLIN.bits(),
9261 revents: (POLLIN | POLLHUP).bits(),
9262 },
9263 KernelPollFdResponse {
9264 fd: 1,
9265 events: POLLIN.bits(),
9266 revents: 0,
9267 },
9268 ]
9269 );
9270
9271 process.kernel_handle.finish(0);
9272 kernel.waitpid(pid).expect("wait javascript kernel process");
9273 }
9274}
9275
9276fn dedupe_strings(values: &[String]) -> Vec<String> {
9277 let mut seen = BTreeSet::new();
9278 let mut deduped = Vec::new();
9279 for value in values {
9280 if seen.insert(value.clone()) {
9281 deduped.push(value.clone());
9282 }
9283 }
9284 deduped
9285}
9286
9287fn dedupe_host_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
9288 let mut seen = BTreeSet::new();
9289 let mut deduped = Vec::new();
9290 for path in paths {
9291 let normalized = normalize_host_path(path);
9292 let key = normalized.to_string_lossy().into_owned();
9293 if seen.insert(key) {
9294 deduped.push(normalized);
9295 }
9296 }
9297 deduped
9298}
9299
9300fn expand_host_access_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
9301 let mut expanded = Vec::new();
9302 let mut seen = BTreeSet::new();
9303
9304 let mut add_path = |candidate: PathBuf| {
9305 let normalized = normalize_host_path(&candidate);
9306 let key = normalized.to_string_lossy().into_owned();
9307 if seen.insert(key) {
9308 expanded.push(normalized);
9309 }
9310 };
9311
9312 for host_path in paths {
9313 add_path(host_path.clone());
9314 if let Ok(realpath) = fs::canonicalize(host_path) {
9315 add_path(realpath);
9316 }
9317
9318 if host_path.file_name().and_then(|name| name.to_str()) != Some("node_modules") {
9319 continue;
9320 }
9321
9322 let mut current = host_path.parent();
9323 while let Some(parent) = current {
9324 let candidate = parent.join("node_modules");
9325 if candidate.exists() {
9326 add_path(candidate.clone());
9327 if let Ok(realpath) = fs::canonicalize(&candidate) {
9328 add_path(realpath);
9329 }
9330 }
9331 current = parent.parent();
9332 }
9333 }
9334
9335 expanded
9336}
9337
9338fn prepare_javascript_shadow(
9339 vm: &mut VmState,
9340 resolved: &ResolvedChildProcessExecution,
9341) -> Result<(), SidecarError> {
9342 let guest_entrypoint = resolved
9343 .env
9344 .get("AGENTOS_GUEST_ENTRYPOINT")
9345 .cloned()
9346 .or_else(|| {
9354 resolve_host_entrypoint_within_vm_host_cwd(vm, &resolved.entrypoint)
9355 .map(|(guest_entrypoint, _)| guest_entrypoint)
9356 })
9357 .or_else(|| {
9358 resolved
9359 .entrypoint
9360 .starts_with('/')
9361 .then(|| normalize_path(&resolved.entrypoint))
9362 });
9363 let Some(guest_entrypoint) = guest_entrypoint else {
9364 return Ok(());
9365 };
9366 if host_mount_path_for_guest_path(vm, &guest_entrypoint).is_some() {
9367 return Ok(());
9368 }
9369 if vm.kernel.lstat(&guest_entrypoint).is_err() {
9370 let host_entrypoint = {
9371 let candidate = Path::new(&resolved.entrypoint);
9372 if candidate.is_absolute() {
9373 candidate.to_path_buf()
9374 } else {
9375 resolved.host_cwd.join(candidate)
9376 }
9377 };
9378 if host_entrypoint.exists() {
9379 materialize_host_path_to_shadow(vm, &guest_entrypoint, &host_entrypoint)?;
9380 return sync_shadow_entrypoint_into_kernel(vm, &guest_entrypoint);
9385 }
9386 }
9387 materialize_guest_path_to_shadow(vm, &guest_entrypoint)
9388}
9389
9390fn sync_shadow_entrypoint_into_kernel(
9395 vm: &mut VmState,
9396 guest_entrypoint: &str,
9397) -> Result<(), SidecarError> {
9398 if vm.kernel.exists(guest_entrypoint).unwrap_or(false) {
9399 return Ok(());
9400 }
9401 let shadow_path = shadow_path_for_guest(vm, guest_entrypoint);
9402 let bytes = match fs::read(&shadow_path) {
9403 Ok(bytes) => bytes,
9404 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
9405 Err(error) => {
9406 return Err(SidecarError::Io(format!(
9407 "failed to read staged shadow entrypoint {}: {error}",
9408 shadow_path.display()
9409 )));
9410 }
9411 };
9412 if let Some(parent) = guest_parent_path(guest_entrypoint) {
9413 if !vm.kernel.exists(&parent).unwrap_or(false) {
9414 vm.kernel.mkdir(&parent, true).map_err(kernel_error)?;
9415 }
9416 }
9417 vm.kernel
9418 .write_file(guest_entrypoint, bytes)
9419 .map_err(kernel_error)?;
9420 Ok(())
9421}
9422
9423fn guest_parent_path(guest_path: &str) -> Option<String> {
9424 let parent = Path::new(guest_path).parent()?;
9425 let parent = parent.to_string_lossy();
9426 if parent.is_empty() || parent == "/" {
9427 None
9428 } else {
9429 Some(parent.into_owned())
9430 }
9431}
9432
9433fn materialize_host_path_to_shadow(
9434 vm: &VmState,
9435 guest_path: &str,
9436 host_path: &Path,
9437) -> Result<(), SidecarError> {
9438 let shadow_path = shadow_path_for_guest(vm, guest_path);
9439 let metadata = fs::symlink_metadata(host_path)
9440 .map_err(|error| SidecarError::Io(format!("failed to stat host entrypoint: {error}")))?;
9441
9442 if metadata.file_type().is_symlink() {
9443 if let Some(parent) = shadow_path.parent() {
9444 fs::create_dir_all(parent).map_err(|error| {
9445 SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
9446 })?;
9447 }
9448 let _ = fs::remove_file(&shadow_path);
9449 let _ = fs::remove_dir_all(&shadow_path);
9450 let target = fs::read_link(host_path)
9451 .map_err(|error| SidecarError::Io(format!("failed to read host symlink: {error}")))?;
9452 std::os::unix::fs::symlink(&target, &shadow_path)
9453 .map_err(|error| SidecarError::Io(format!("failed to mirror host symlink: {error}")))?;
9454 return Ok(());
9455 }
9456
9457 if metadata.is_dir() {
9458 fs::create_dir_all(&shadow_path).map_err(|error| {
9459 SidecarError::Io(format!("failed to create shadow directory: {error}"))
9460 })?;
9461 fs::set_permissions(
9462 &shadow_path,
9463 fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
9464 )
9465 .map_err(|error| {
9466 SidecarError::Io(format!(
9467 "failed to set shadow directory mode on {}: {error}",
9468 shadow_path.display()
9469 ))
9470 })?;
9471 return Ok(());
9472 }
9473
9474 if let Some(parent) = shadow_path.parent() {
9475 fs::create_dir_all(parent).map_err(|error| {
9476 SidecarError::Io(format!("failed to create shadow parent: {error}"))
9477 })?;
9478 }
9479 let bytes = fs::read(host_path)
9480 .map_err(|error| SidecarError::Io(format!("failed to read host entrypoint: {error}")))?;
9481 fs::write(&shadow_path, bytes).map_err(|error| {
9482 SidecarError::Io(format!(
9483 "failed to mirror host file into shadow root: {error}"
9484 ))
9485 })?;
9486 fs::set_permissions(
9487 &shadow_path,
9488 fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
9489 )
9490 .map_err(|error| {
9491 SidecarError::Io(format!(
9492 "failed to set shadow file mode on {}: {error}",
9493 shadow_path.display()
9494 ))
9495 })?;
9496 Ok(())
9497}
9498
9499fn materialize_guest_path_to_shadow(
9500 vm: &mut VmState,
9501 guest_path: &str,
9502) -> Result<(), SidecarError> {
9503 let stat = vm.kernel.lstat(guest_path).map_err(kernel_error)?;
9504 let shadow_path = shadow_path_for_guest(vm, guest_path);
9505
9506 if stat.is_symbolic_link {
9507 if let Some(parent) = shadow_path.parent() {
9508 fs::create_dir_all(parent).map_err(|error| {
9509 SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
9510 })?;
9511 }
9512 let _ = fs::remove_file(&shadow_path);
9513 let _ = fs::remove_dir_all(&shadow_path);
9514 let target = vm.kernel.read_link(guest_path).map_err(kernel_error)?;
9515 std::os::unix::fs::symlink(&target, &shadow_path)
9516 .map_err(|error| SidecarError::Io(format!("failed to mirror symlink: {error}")))?;
9517 return Ok(());
9518 }
9519
9520 if stat.is_directory {
9521 fs::create_dir_all(&shadow_path).map_err(|error| {
9522 SidecarError::Io(format!("failed to create shadow directory: {error}"))
9523 })?;
9524 fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
9525 |error| {
9526 SidecarError::Io(format!(
9527 "failed to set shadow directory mode on {}: {error}",
9528 shadow_path.display()
9529 ))
9530 },
9531 )?;
9532 return Ok(());
9533 }
9534
9535 if let Some(parent) = shadow_path.parent() {
9536 fs::create_dir_all(parent).map_err(|error| {
9537 SidecarError::Io(format!("failed to create shadow parent: {error}"))
9538 })?;
9539 }
9540 let bytes = vm.kernel.read_file(guest_path).map_err(kernel_error)?;
9541 fs::write(&shadow_path, bytes).map_err(|error| {
9542 SidecarError::Io(format!(
9543 "failed to mirror guest file into shadow root: {error}"
9544 ))
9545 })?;
9546 fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
9547 |error| {
9548 SidecarError::Io(format!(
9549 "failed to set shadow file mode on {}: {error}",
9550 shadow_path.display()
9551 ))
9552 },
9553 )?;
9554 Ok(())
9555}
9556
9557fn load_javascript_entrypoint_source(
9558 vm: &mut VmState,
9559 host_cwd: &Path,
9560 entrypoint: &str,
9561 env: &BTreeMap<String, String>,
9562) -> Option<String> {
9563 let mut read_guest_file = |path: &str| {
9564 vm.kernel
9565 .read_file(path)
9566 .ok()
9567 .and_then(|bytes| String::from_utf8(bytes).ok())
9568 };
9569
9570 if let Some(source) = env
9571 .get("AGENTOS_GUEST_ENTRYPOINT")
9572 .filter(|path| path.starts_with('/'))
9573 .and_then(|path| read_guest_file(path))
9574 {
9575 return Some(source);
9576 }
9577
9578 if entrypoint.starts_with('/') {
9579 if let Some(source) = read_guest_file(entrypoint) {
9580 return Some(source);
9581 }
9582 }
9583
9584 let host_entrypoint = if Path::new(entrypoint).is_absolute() {
9585 PathBuf::from(entrypoint)
9586 } else {
9587 host_cwd.join(entrypoint)
9588 };
9589 let normalized_entrypoint = normalize_host_path(&host_entrypoint);
9590 let sandbox_root = normalize_host_path(&vm.cwd);
9591 let host_cwd = normalize_host_path(&vm.host_cwd);
9592 if !path_is_within_root(&normalized_entrypoint, &sandbox_root)
9593 && !path_is_within_root(&normalized_entrypoint, &host_cwd)
9594 {
9595 return None;
9596 }
9597
9598 fs::read_to_string(&normalized_entrypoint).ok()
9599}
9600
9601fn emit_dns_resolution_event<B>(
9602 bridge: &SharedBridge<B>,
9603 vm_id: &str,
9604 hostname: &str,
9605 source: KernelDnsResolutionSource,
9606 addresses: &[IpAddr],
9607 dns: &VmDnsConfig,
9608) where
9609 B: NativeSidecarBridge + Send + 'static,
9610 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9611{
9612 let _ = emit_structured_event(
9613 bridge,
9614 vm_id,
9615 "network.dns.resolved",
9616 audit_fields([
9617 ("hostname", hostname.to_owned()),
9618 ("source", source.as_str().to_owned()),
9619 (
9620 "addresses",
9621 addresses
9622 .iter()
9623 .map(ToString::to_string)
9624 .collect::<Vec<_>>()
9625 .join(","),
9626 ),
9627 ("address_count", addresses.len().to_string()),
9628 ("resolver_count", dns.name_servers.len().to_string()),
9629 (
9630 "resolvers",
9631 dns.name_servers
9632 .iter()
9633 .map(ToString::to_string)
9634 .collect::<Vec<_>>()
9635 .join(","),
9636 ),
9637 ]),
9638 );
9639}
9640
9641fn emit_dns_record_resolution_event<B>(
9642 bridge: &SharedBridge<B>,
9643 vm_id: &str,
9644 hostname: &str,
9645 resolution: &DnsRecordResolution,
9646 dns: &VmDnsConfig,
9647) where
9648 B: NativeSidecarBridge + Send + 'static,
9649 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9650{
9651 if let Some(addresses) = dns_resolution_ip_addrs(resolution.records()) {
9652 emit_dns_resolution_event(
9653 bridge,
9654 vm_id,
9655 hostname,
9656 resolution.source(),
9657 &addresses,
9658 dns,
9659 );
9660 return;
9661 }
9662
9663 let _ = emit_structured_event(
9664 bridge,
9665 vm_id,
9666 "network.dns.resolved",
9667 audit_fields([
9668 ("hostname", hostname.to_owned()),
9669 ("source", resolution.source().as_str().to_owned()),
9670 (
9671 "addresses",
9672 resolution
9673 .records()
9674 .iter()
9675 .map(summarize_dns_record)
9676 .collect::<Vec<_>>()
9677 .join(","),
9678 ),
9679 ("address_count", resolution.records().len().to_string()),
9680 ("resolver_count", dns.name_servers.len().to_string()),
9681 (
9682 "resolvers",
9683 dns.name_servers
9684 .iter()
9685 .map(ToString::to_string)
9686 .collect::<Vec<_>>()
9687 .join(","),
9688 ),
9689 ]),
9690 );
9691}
9692
9693fn emit_dns_resolution_failure_event<B>(
9694 bridge: &SharedBridge<B>,
9695 vm_id: &str,
9696 hostname: &str,
9697 dns: &VmDnsConfig,
9698 error: &SidecarError,
9699) where
9700 B: NativeSidecarBridge + Send + 'static,
9701 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9702{
9703 let _ = emit_structured_event(
9704 bridge,
9705 vm_id,
9706 "network.dns.resolve_failed",
9707 audit_fields([
9708 ("hostname", hostname.to_owned()),
9709 ("reason", error.to_string()),
9710 ("resolver_count", dns.name_servers.len().to_string()),
9711 (
9712 "resolvers",
9713 dns.name_servers
9714 .iter()
9715 .map(ToString::to_string)
9716 .collect::<Vec<_>>()
9717 .join(","),
9718 ),
9719 ]),
9720 );
9721}
9722
9723fn parse_dns_record_type(rrtype: &str) -> Result<RecordType, SidecarError> {
9724 match rrtype {
9725 "A" => Ok(RecordType::A),
9726 "AAAA" => Ok(RecordType::AAAA),
9727 "MX" => Ok(RecordType::MX),
9728 "TXT" => Ok(RecordType::TXT),
9729 "SRV" => Ok(RecordType::SRV),
9730 "CNAME" => Ok(RecordType::CNAME),
9731 "PTR" => Ok(RecordType::PTR),
9732 "NS" => Ok(RecordType::NS),
9733 "SOA" => Ok(RecordType::SOA),
9734 "NAPTR" => Ok(RecordType::NAPTR),
9735 "CAA" => Ok(RecordType::CAA),
9736 "ANY" => Ok(RecordType::ANY),
9737 other => Err(SidecarError::Execution(format!(
9738 "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
9739 ))),
9740 }
9741}
9742
9743fn dns_resolution_to_node_value(
9744 resolution: &DnsRecordResolution,
9745 requested_type: &str,
9746) -> Result<Value, SidecarError> {
9747 let safe_ips = dns_resolution_safe_ip_set(resolution.records(), resolution.hostname())?;
9748 match requested_type {
9749 "A" | "AAAA" => Ok(Value::Array(
9750 resolution
9751 .records()
9752 .iter()
9753 .filter_map(|record| dns_record_ip_string(record, &safe_ips))
9754 .map(Value::String)
9755 .collect(),
9756 )),
9757 "MX" => Ok(Value::Array(
9758 resolution
9759 .records()
9760 .iter()
9761 .filter_map(|record| match record.data() {
9762 RData::MX(mx) => Some(json!({
9763 "priority": mx.preference,
9764 "exchange": normalize_dns_name_for_node(&mx.exchange),
9765 "type": "MX",
9766 })),
9767 _ => None,
9768 })
9769 .collect(),
9770 )),
9771 "TXT" => Ok(Value::Array(
9772 resolution
9773 .records()
9774 .iter()
9775 .filter_map(|record| match record.data() {
9776 RData::TXT(txt) => Some(Value::Array(
9777 txt.txt_data
9778 .iter()
9779 .map(|entry| Value::String(String::from_utf8_lossy(entry).into_owned()))
9780 .collect(),
9781 )),
9782 _ => None,
9783 })
9784 .collect(),
9785 )),
9786 "SRV" => Ok(Value::Array(
9787 resolution
9788 .records()
9789 .iter()
9790 .filter_map(|record| match record.data() {
9791 RData::SRV(srv) => Some(json!({
9792 "priority": srv.priority,
9793 "weight": srv.weight,
9794 "port": srv.port,
9795 "name": normalize_dns_name_for_node(&srv.target),
9796 "type": "SRV",
9797 })),
9798 _ => None,
9799 })
9800 .collect(),
9801 )),
9802 "CNAME" => Ok(Value::Array(
9803 resolution
9804 .records()
9805 .iter()
9806 .filter_map(|record| match record.data() {
9807 RData::CNAME(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9808 _ => None,
9809 })
9810 .collect(),
9811 )),
9812 "PTR" => Ok(Value::Array(
9813 resolution
9814 .records()
9815 .iter()
9816 .filter_map(|record| match record.data() {
9817 RData::PTR(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9818 _ => None,
9819 })
9820 .collect(),
9821 )),
9822 "NS" => Ok(Value::Array(
9823 resolution
9824 .records()
9825 .iter()
9826 .filter_map(|record| match record.data() {
9827 RData::NS(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9828 _ => None,
9829 })
9830 .collect(),
9831 )),
9832 "SOA" => resolution
9833 .records()
9834 .iter()
9835 .find_map(|record| match record.data() {
9836 RData::SOA(soa) => Some(json!({
9837 "nsname": normalize_dns_name_for_node(&soa.mname),
9838 "hostmaster": normalize_dns_name_for_node(&soa.rname),
9839 "serial": soa.serial,
9840 "refresh": soa.refresh,
9841 "retry": soa.retry,
9842 "expire": soa.expire,
9843 "minttl": soa.minimum,
9844 })),
9845 _ => None,
9846 })
9847 .ok_or_else(|| {
9848 SidecarError::Execution(String::from("failed to resolve DNS SOA record"))
9849 }),
9850 "NAPTR" => Ok(Value::Array(
9851 resolution
9852 .records()
9853 .iter()
9854 .filter_map(|record| match record.data() {
9855 RData::NAPTR(naptr) => Some(json!({
9856 "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
9857 "service": String::from_utf8_lossy(&naptr.services).into_owned(),
9858 "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
9859 "replacement": normalize_dns_name_for_node(&naptr.replacement),
9860 "order": naptr.order,
9861 "preference": naptr.preference,
9862 })),
9863 _ => None,
9864 })
9865 .collect(),
9866 )),
9867 "CAA" => Ok(Value::Array(
9868 resolution
9869 .records()
9870 .iter()
9871 .filter_map(|record| match record.data() {
9872 RData::CAA(caa) => {
9873 let mut value = serde_json::Map::new();
9874 value.insert(
9875 "critical".to_owned(),
9876 Value::from(u8::from(caa.issuer_critical)),
9877 );
9878 value.insert("type".to_owned(), Value::String(String::from("CAA")));
9879 if caa.tag.eq_ignore_ascii_case("iodef") {
9880 value.insert(
9881 "iodef".to_owned(),
9882 Value::String(
9883 caa.value_as_iodef()
9884 .map(|url| url.to_string())
9885 .unwrap_or_else(|_| {
9886 String::from_utf8_lossy(&caa.value).into_owned()
9887 }),
9888 ),
9889 );
9890 } else if let Ok((issuer, _params)) = caa.value_as_issue() {
9891 let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
9892 "issuewild"
9893 } else {
9894 "issue"
9895 };
9896 value.insert(
9897 field.to_owned(),
9898 Value::String(
9899 issuer.as_ref().map(ToString::to_string).unwrap_or_else(|| {
9900 String::from_utf8_lossy(&caa.value).into_owned()
9901 }),
9902 ),
9903 );
9904 } else {
9905 value.insert(
9906 caa.tag.to_ascii_lowercase(),
9907 Value::String(String::from_utf8_lossy(&caa.value).into_owned()),
9908 );
9909 }
9910 Some(Value::Object(value))
9911 }
9912 _ => None,
9913 })
9914 .collect(),
9915 )),
9916 "ANY" => Ok(Value::Array(
9917 resolution
9918 .records()
9919 .iter()
9920 .filter_map(|record| dns_any_record_to_value(record, &safe_ips))
9921 .collect(),
9922 )),
9923 other => Err(SidecarError::Execution(format!(
9924 "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
9925 ))),
9926 }
9927}
9928
9929fn dns_resolution_safe_ip_set(
9930 records: &[Record],
9931 hostname: &str,
9932) -> Result<BTreeSet<IpAddr>, SidecarError> {
9933 let ips = records
9934 .iter()
9935 .filter_map(dns_record_ip_addr)
9936 .collect::<Vec<_>>();
9937 if ips.is_empty() {
9938 return Ok(BTreeSet::new());
9939 }
9940 Ok(filter_dns_safe_ip_addrs(ips, hostname)?
9941 .into_iter()
9942 .collect())
9943}
9944
9945fn dns_resolution_ip_addrs(records: &[Record]) -> Option<Vec<IpAddr>> {
9946 let ips = records
9947 .iter()
9948 .filter_map(dns_record_ip_addr)
9949 .collect::<Vec<_>>();
9950 if ips.is_empty() {
9951 return None;
9952 }
9953 Some(ips)
9954}
9955
9956fn dns_record_ip_addr(record: &Record) -> Option<IpAddr> {
9957 match record.data() {
9958 RData::A(address) => Some(IpAddr::V4(**address)),
9959 RData::AAAA(address) => Some(IpAddr::V6(**address)),
9960 _ => None,
9961 }
9962}
9963
9964fn dns_record_ip_string(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<String> {
9965 let ip = dns_record_ip_addr(record)?;
9966 safe_ips.contains(&ip).then(|| ip.to_string())
9967}
9968
9969fn dns_any_record_to_value(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<Value> {
9970 let value = match record.data() {
9971 RData::A(_) | RData::AAAA(_) => json!({
9972 "address": dns_record_ip_string(record, safe_ips)?,
9973 "ttl": record.ttl(),
9974 "type": record.record_type().to_string(),
9975 }),
9976 RData::MX(mx) => json!({
9977 "exchange": normalize_dns_name_for_node(&mx.exchange),
9978 "priority": mx.preference,
9979 "type": "MX",
9980 }),
9981 RData::TXT(txt) => json!({
9982 "entries": txt
9983 .txt_data
9984 .iter()
9985 .map(|entry| String::from_utf8_lossy(entry).into_owned())
9986 .collect::<Vec<_>>(),
9987 "type": "TXT",
9988 }),
9989 RData::SRV(srv) => json!({
9990 "name": normalize_dns_name_for_node(&srv.target),
9991 "port": srv.port,
9992 "priority": srv.priority,
9993 "weight": srv.weight,
9994 "type": "SRV",
9995 }),
9996 RData::CNAME(name) => json!({
9997 "value": normalize_dns_name_for_node(&name.0),
9998 "type": "CNAME",
9999 }),
10000 RData::PTR(name) => json!({
10001 "value": normalize_dns_name_for_node(&name.0),
10002 "type": "PTR",
10003 }),
10004 RData::NS(name) => json!({
10005 "value": normalize_dns_name_for_node(&name.0),
10006 "type": "NS",
10007 }),
10008 RData::SOA(soa) => json!({
10009 "nsname": normalize_dns_name_for_node(&soa.mname),
10010 "hostmaster": normalize_dns_name_for_node(&soa.rname),
10011 "serial": soa.serial,
10012 "refresh": soa.refresh,
10013 "retry": soa.retry,
10014 "expire": soa.expire,
10015 "minttl": soa.minimum,
10016 "type": "SOA",
10017 }),
10018 RData::NAPTR(naptr) => json!({
10019 "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
10020 "service": String::from_utf8_lossy(&naptr.services).into_owned(),
10021 "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
10022 "replacement": normalize_dns_name_for_node(&naptr.replacement),
10023 "order": naptr.order,
10024 "preference": naptr.preference,
10025 "type": "NAPTR",
10026 }),
10027 RData::CAA(caa) => {
10028 let mut value = serde_json::Map::new();
10029 value.insert(
10030 "critical".to_owned(),
10031 Value::from(u8::from(caa.issuer_critical)),
10032 );
10033 value.insert("type".to_owned(), Value::String(String::from("CAA")));
10034 if caa.tag.eq_ignore_ascii_case("iodef") {
10035 value.insert(
10036 "iodef".to_owned(),
10037 Value::String(
10038 caa.value_as_iodef()
10039 .map(|url| url.to_string())
10040 .unwrap_or_else(|_| String::from_utf8_lossy(&caa.value).into_owned()),
10041 ),
10042 );
10043 } else if let Ok((issuer, _params)) = caa.value_as_issue() {
10044 let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
10045 "issuewild"
10046 } else {
10047 "issue"
10048 };
10049 value.insert(
10050 field.to_owned(),
10051 Value::String(
10052 issuer
10053 .as_ref()
10054 .map(ToString::to_string)
10055 .unwrap_or_else(|| String::from_utf8_lossy(&caa.value).into_owned()),
10056 ),
10057 );
10058 }
10059 Value::Object(value)
10060 }
10061 _ => return None,
10062 };
10063 Some(value)
10064}
10065
10066fn normalize_dns_name_for_node(name: &impl ToString) -> String {
10067 name.to_string().trim_end_matches('.').to_owned()
10068}
10069
10070fn summarize_dns_record(record: &Record) -> String {
10071 match record.data() {
10072 RData::A(_) | RData::AAAA(_) => record.data().to_string(),
10073 _ => format!("{} {}", record.record_type(), record.data()),
10074 }
10075}
10076
10077fn find_socket_state_entry(
10085 vm: Option<&VmState>,
10086 kind: SocketQueryKind,
10087 request: &FindListenerRequest,
10088) -> Result<Option<SocketStateEntry>, SidecarError> {
10089 let vm = vm.ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
10090
10091 for (process_id, process) in &vm.active_processes {
10092 if let Some(path) = request.path.as_deref() {
10093 if matches!(kind, SocketQueryKind::TcpListener) {
10094 for listener in process.unix_listeners.values() {
10095 if listener.path() != path {
10096 continue;
10097 }
10098 return Ok(Some(SocketStateEntry {
10099 process_id: process_id.to_owned(),
10100 host: None,
10101 port: None,
10102 path: Some(path.to_owned()),
10103 }));
10104 }
10105 }
10106 }
10107
10108 if request.path.is_none() {
10109 if let Some(entry) =
10110 find_kernel_socket_state_entry(&vm.kernel, process_id, process, kind, request)?
10111 {
10112 return Ok(Some(entry));
10113 }
10114
10115 match kind {
10116 SocketQueryKind::TcpListener => {
10117 for server in process.http_servers.values() {
10118 let local_addr = server.guest_local_addr;
10119 let local_host = local_addr.ip().to_string();
10120 if !socket_host_matches(request.host.as_deref(), &local_host) {
10121 continue;
10122 }
10123 if let Some(port) = request.port {
10124 if local_addr.port() != port {
10125 continue;
10126 }
10127 }
10128 return Ok(Some(SocketStateEntry {
10129 process_id: process_id.to_owned(),
10130 host: Some(local_host),
10131 port: Some(local_addr.port()),
10132 path: None,
10133 }));
10134 }
10135
10136 for listener in process.tcp_listeners.values() {
10137 if listener.kernel_socket_id.is_some() {
10138 continue;
10139 }
10140 let local_addr = listener.guest_local_addr();
10141 let local_host = local_addr.ip().to_string();
10142 if !socket_host_matches(request.host.as_deref(), &local_host) {
10143 continue;
10144 }
10145 if let Some(port) = request.port {
10146 if local_addr.port() != port {
10147 continue;
10148 }
10149 }
10150 return Ok(Some(SocketStateEntry {
10151 process_id: process_id.to_owned(),
10152 host: Some(local_host),
10153 port: Some(local_addr.port()),
10154 path: None,
10155 }));
10156 }
10157 }
10158 SocketQueryKind::UdpBound => {
10159 for socket in process.udp_sockets.values() {
10160 if socket.kernel_socket_id.is_some() {
10161 continue;
10162 }
10163 let Some(local_addr) = socket.local_addr() else {
10164 continue;
10165 };
10166 let local_host = local_addr.ip().to_string();
10167 if !socket_host_matches(request.host.as_deref(), &local_host) {
10168 continue;
10169 }
10170 if let Some(port) = request.port {
10171 if local_addr.port() != port {
10172 continue;
10173 }
10174 }
10175 return Ok(Some(SocketStateEntry {
10176 process_id: process_id.to_owned(),
10177 host: Some(local_host),
10178 port: Some(local_addr.port()),
10179 path: None,
10180 }));
10181 }
10182 }
10183 }
10184 }
10185
10186 let child_pid = process.execution.child_pid();
10187 let inodes = socket_inodes_for_pid(child_pid)?;
10188 if inodes.is_empty() {
10189 continue;
10190 }
10191
10192 if let Some(path) = request.path.as_deref() {
10193 if let Some(listener) = find_unix_socket_for_pid(child_pid, &inodes, path, process_id)?
10194 {
10195 return Ok(Some(listener));
10196 }
10197 continue;
10198 }
10199
10200 let table_paths = match kind {
10201 SocketQueryKind::TcpListener => [
10202 format!("/proc/{child_pid}/net/tcp"),
10203 format!("/proc/{child_pid}/net/tcp6"),
10204 ],
10205 SocketQueryKind::UdpBound => [
10206 format!("/proc/{child_pid}/net/udp"),
10207 format!("/proc/{child_pid}/net/udp6"),
10208 ],
10209 };
10210 for table_path in table_paths {
10211 if let Some(entry) = find_inet_socket_for_pid(
10212 &table_path,
10213 &inodes,
10214 kind,
10215 request.host.as_deref(),
10216 request.port,
10217 process_id,
10218 )? {
10219 return Ok(Some(entry));
10220 }
10221 }
10222 }
10223
10224 Ok(None)
10225}
10226
10227fn require_vm_inspection_permission<B>(
10228 bridge: &SharedBridge<B>,
10229 vm_id: &str,
10230 capability: &str,
10231 domain: &str,
10232 resource: &str,
10233) -> Result<(), SidecarError>
10234where
10235 B: NativeSidecarBridge + Send + 'static,
10236 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
10237{
10238 let decision = bridge.static_permission_decision(vm_id, capability, domain, Some(resource));
10239 if decision.as_ref().is_some_and(|decision| decision.allow) {
10240 return Ok(());
10241 }
10242
10243 let reason = decision
10244 .and_then(|decision| decision.reason)
10245 .unwrap_or_else(|| format!("{capability} permission required"));
10246 Err(SidecarError::Execution(format!(
10247 "EACCES: permission denied, {resource}: {reason}"
10248 )))
10249}
10250
10251fn socket_query_resource(kind: SocketQueryKind, request: &FindListenerRequest) -> String {
10252 if let Some(path) = request.path.as_deref() {
10253 return format!("unix://{path}");
10254 }
10255
10256 let host = request.host.as_deref().unwrap_or("*");
10257 let port = request
10258 .port
10259 .map_or_else(|| String::from("*"), |port| port.to_string());
10260 match kind {
10261 SocketQueryKind::TcpListener => format!("tcp://{host}:{port}"),
10262 SocketQueryKind::UdpBound => format!("udp://{host}:{port}"),
10263 }
10264}
10265
10266fn snapshot_vm_processes(vm: &VmState) -> Vec<ProcessSnapshotEntry> {
10267 let process_table = vm.kernel.list_processes();
10268 snapshot_vm_processes_inner(vm, &process_table)
10269}
10270
10271fn snapshot_vm_processes_inner(
10272 vm: &VmState,
10273 process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
10274) -> Vec<ProcessSnapshotEntry> {
10275 let mut entries = Vec::new();
10276
10277 for (process_id, process) in &vm.active_processes {
10278 collect_process_snapshot_entries(process_id, process, process_table, &mut entries);
10279 }
10280
10281 for exited in &vm.exited_process_snapshots {
10282 entries.push(exited.process.clone());
10283 }
10284
10285 entries
10286}
10287
10288fn prune_exited_process_snapshots(vm: &mut VmState) {
10289 let cutoff = Instant::now() - EXITED_PROCESS_SNAPSHOT_RETENTION;
10290 while vm
10291 .exited_process_snapshots
10292 .front()
10293 .is_some_and(|snapshot| snapshot.captured_at < cutoff)
10294 {
10295 vm.exited_process_snapshots.pop_front();
10296 }
10297}
10298
10299fn build_process_snapshot_entry(
10300 process_id: &str,
10301 process: &ActiveProcess,
10302 info: &secure_exec_kernel::process_table::ProcessInfo,
10303 exit_code: Option<i32>,
10304) -> ProcessSnapshotEntry {
10305 ProcessSnapshotEntry {
10306 process_id: process_id.to_owned(),
10307 pid: info.pid,
10308 ppid: info.ppid,
10309 pgid: info.pgid,
10310 sid: info.sid,
10311 driver: info.driver.clone(),
10312 command: info.command.clone(),
10313 args: Vec::new(),
10314 cwd: process.guest_cwd.clone(),
10315 status: if exit_code.is_some() {
10316 ProcessSnapshotStatus::Exited
10317 } else {
10318 match info.status {
10319 ProcessStatus::Running => ProcessSnapshotStatus::Running,
10320 ProcessStatus::Stopped => ProcessSnapshotStatus::Stopped,
10321 ProcessStatus::Exited => ProcessSnapshotStatus::Exited,
10322 }
10323 },
10324 exit_code: exit_code.or(info.exit_code),
10325 }
10326}
10327
10328fn collect_process_snapshot_entries(
10329 process_id: &str,
10330 process: &ActiveProcess,
10331 process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
10332 entries: &mut Vec<ProcessSnapshotEntry>,
10333) {
10334 if let Some(info) = process_table.get(&process.kernel_pid) {
10335 entries.push(build_process_snapshot_entry(
10336 process_id, process, info, None,
10337 ));
10338 }
10339
10340 for (child_id, child) in &process.child_processes {
10341 let child_process_id = format!("{process_id}/{child_id}");
10342 collect_process_snapshot_entries(&child_process_id, child, process_table, entries);
10343 }
10344}
10345
10346fn find_kernel_socket_state_entry(
10347 kernel: &SidecarKernel,
10348 process_id: &str,
10349 process: &ActiveProcess,
10350 kind: SocketQueryKind,
10351 request: &FindListenerRequest,
10352) -> Result<Option<SocketStateEntry>, SidecarError> {
10353 let entry = match kind {
10354 SocketQueryKind::TcpListener => process
10355 .tcp_listeners
10356 .values()
10357 .filter_map(|listener| listener.kernel_socket_id)
10358 .find_map(|socket_id| {
10359 kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
10360 }),
10361 SocketQueryKind::UdpBound => process
10362 .udp_sockets
10363 .values()
10364 .filter_map(|socket| socket.kernel_socket_id)
10365 .find_map(|socket_id| {
10366 kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
10367 }),
10368 };
10369
10370 if entry.is_some() {
10371 return Ok(entry);
10372 }
10373
10374 for child in process.child_processes.values() {
10375 if let Some(entry) =
10376 find_kernel_socket_state_entry(kernel, process_id, child, kind, request)?
10377 {
10378 return Ok(Some(entry));
10379 }
10380 }
10381
10382 Ok(None)
10383}
10384
10385fn kernel_socket_state_entry(
10386 kernel: &SidecarKernel,
10387 process_id: &str,
10388 socket_id: SocketId,
10389 kind: SocketQueryKind,
10390 request: &FindListenerRequest,
10391) -> Option<SocketStateEntry> {
10392 let record = kernel.socket_get(socket_id)?;
10393 let local_address = record.local_address()?;
10394 match kind {
10395 SocketQueryKind::TcpListener if record.state() == SocketState::Listening => {}
10396 SocketQueryKind::TcpListener => return None,
10397 SocketQueryKind::UdpBound => {}
10398 }
10399
10400 if !socket_host_matches(request.host.as_deref(), local_address.host()) {
10401 return None;
10402 }
10403 if request
10404 .port
10405 .is_some_and(|port| local_address.port() != port)
10406 {
10407 return None;
10408 }
10409
10410 Some(SocketStateEntry {
10411 process_id: process_id.to_owned(),
10412 host: Some(local_address.host().to_owned()),
10413 port: Some(local_address.port()),
10414 path: None,
10415 })
10416}
10417
10418fn socket_inodes_for_pid(pid: u32) -> Result<BTreeSet<u64>, SidecarError> {
10419 let fd_dir = PathBuf::from(format!("/proc/{pid}/fd"));
10420 let entries = match fs::read_dir(&fd_dir) {
10421 Ok(entries) => entries,
10422 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(BTreeSet::new()),
10423 Err(error) => {
10424 return Err(SidecarError::Io(format!(
10425 "failed to read socket descriptors for process {pid}: {error}"
10426 )));
10427 }
10428 };
10429
10430 let mut inodes = BTreeSet::new();
10431 for entry in entries {
10432 let entry = entry.map_err(|error| {
10433 SidecarError::Io(format!(
10434 "failed to inspect fd entry for process {pid}: {error}"
10435 ))
10436 })?;
10437 let target = match fs::read_link(entry.path()) {
10438 Ok(target) => target,
10439 Err(_) => continue,
10440 };
10441 if let Some(inode) = parse_socket_inode(&target) {
10442 inodes.insert(inode);
10443 }
10444 }
10445
10446 Ok(inodes)
10447}
10448
10449fn parse_socket_inode(target: &Path) -> Option<u64> {
10450 let value = target.to_string_lossy();
10451 let trimmed = value.strip_prefix("socket:[")?.strip_suffix(']')?;
10452 trimmed.parse().ok()
10453}
10454
10455fn unix_socket_path(addr: &UnixSocketAddr) -> Option<String> {
10456 addr.as_pathname()
10457 .map(|path| path.to_string_lossy().into_owned())
10458}
10459
10460fn find_unix_socket_for_pid(
10461 pid: u32,
10462 inodes: &BTreeSet<u64>,
10463 path: &str,
10464 process_id: &str,
10465) -> Result<Option<SocketStateEntry>, SidecarError> {
10466 let table_path = format!("/proc/{pid}/net/unix");
10467 let contents = match fs::read_to_string(&table_path) {
10468 Ok(contents) => contents,
10469 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
10470 Err(error) => {
10471 return Err(SidecarError::Io(format!(
10472 "failed to inspect unix sockets for process {pid}: {error}"
10473 )));
10474 }
10475 };
10476
10477 for line in contents.lines().skip(1) {
10478 let columns = line.split_whitespace().collect::<Vec<_>>();
10479 if columns.len() < 8 {
10480 continue;
10481 }
10482 let Ok(inode) = columns[6].parse::<u64>() else {
10483 continue;
10484 };
10485 if !inodes.contains(&inode) || columns[7] != path {
10486 continue;
10487 }
10488 return Ok(Some(SocketStateEntry {
10489 process_id: process_id.to_owned(),
10490 host: None,
10491 port: None,
10492 path: Some(path.to_owned()),
10493 }));
10494 }
10495
10496 Ok(None)
10497}
10498
10499fn find_inet_socket_for_pid(
10500 table_path: &str,
10501 inodes: &BTreeSet<u64>,
10502 kind: SocketQueryKind,
10503 requested_host: Option<&str>,
10504 requested_port: Option<u16>,
10505 process_id: &str,
10506) -> Result<Option<SocketStateEntry>, SidecarError> {
10507 for entry in parse_proc_net_entries(table_path)? {
10508 if !inodes.contains(&entry.inode) {
10509 continue;
10510 }
10511 if matches!(kind, SocketQueryKind::TcpListener) && entry.state != "0A" {
10512 continue;
10513 }
10514 if !socket_host_matches(requested_host, &entry.local_host) {
10515 continue;
10516 }
10517 if let Some(port) = requested_port {
10518 if entry.local_port != port {
10519 continue;
10520 }
10521 }
10522 return Ok(Some(SocketStateEntry {
10523 process_id: process_id.to_owned(),
10524 host: Some(entry.local_host),
10525 port: Some(entry.local_port),
10526 path: None,
10527 }));
10528 }
10529
10530 Ok(None)
10531}
10532
10533fn is_unspecified_socket_host(host: &str) -> bool {
10534 host == "0.0.0.0" || host == "::"
10535}
10536
10537fn is_loopback_socket_host(host: &str) -> bool {
10538 host == "127.0.0.1" || host == "::1" || host.eq_ignore_ascii_case("localhost")
10539}
10540
10541pub(crate) fn vm_network_resource_counts(vm: &VmState) -> NetworkResourceCounts {
10542 let snapshot = vm.kernel.resource_snapshot();
10543 let mut counts = NetworkResourceCounts {
10544 sockets: snapshot.sockets,
10545 connections: snapshot.socket_connections,
10546 };
10547 for process in vm.active_processes.values() {
10548 let process_counts = process.sidecar_only_network_resource_counts();
10549 counts.sockets += process_counts.sockets;
10550 counts.connections += process_counts.connections;
10551 }
10552 counts
10553}
10554
10555fn collect_javascript_socket_port_state(
10556 kernel: &SidecarKernel,
10557 process_id: &str,
10558 process: &ActiveProcess,
10559 tcp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10560 http_loopback_targets: &mut BTreeMap<
10561 (JavascriptSocketFamily, u16),
10562 JavascriptHttpLoopbackTarget,
10563 >,
10564 udp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10565 udp_host_to_guest: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10566 used_tcp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10567 used_udp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10568) {
10569 for (family, port) in process.tcp_port_reservations.values() {
10570 used_tcp_ports.entry(*family).or_default().insert(*port);
10571 }
10572
10573 let mut record_tcp_listener = |guest_addr: SocketAddr, host_port: u16| {
10574 let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10575 used_tcp_ports
10576 .entry(family)
10577 .or_default()
10578 .insert(guest_addr.port());
10579 tcp_guest_to_host.insert((family, guest_addr.port()), host_port);
10582 };
10583
10584 for listener in process.tcp_listeners.values() {
10585 let local_addr = listener
10586 .kernel_socket_id
10587 .and_then(|socket_id| kernel.socket_get(socket_id))
10588 .and_then(|record| record.local_address().cloned())
10589 .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
10590 .unwrap_or_else(|| listener.guest_local_addr());
10591 record_tcp_listener(local_addr, local_addr.port());
10592 }
10593
10594 for (server_id, server) in &process.http_servers {
10595 let host_port = match server.listener.local_addr() {
10596 Ok(addr) => addr.port(),
10597 Err(_) => continue,
10598 };
10599 record_tcp_listener(server.guest_local_addr, host_port);
10600 let family = JavascriptSocketFamily::from_ip(server.guest_local_addr.ip());
10601 http_loopback_targets.insert(
10602 (family, server.guest_local_addr.port()),
10603 JavascriptHttpLoopbackTarget {
10604 process_id: process_id.to_owned(),
10605 server_id: *server_id,
10606 },
10607 );
10608 }
10609
10610 if let Ok(http2) = process.http2.shared.lock() {
10611 for server in http2.servers.values() {
10612 record_tcp_listener(server.guest_local_addr, server.actual_local_addr.port());
10613 }
10614 }
10615
10616 for socket in process.tcp_sockets.values() {
10617 let guest_addr = socket
10618 .kernel_socket_id
10619 .and_then(|socket_id| kernel.socket_get(socket_id))
10620 .and_then(|record| record.local_address().cloned())
10621 .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
10622 .unwrap_or(socket.guest_local_addr);
10623 let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10624 used_tcp_ports
10625 .entry(family)
10626 .or_default()
10627 .insert(guest_addr.port());
10628 }
10629
10630 for socket in process.udp_sockets.values() {
10631 let guest_addr = socket
10632 .kernel_socket_id
10633 .and_then(|socket_id| kernel.socket_get(socket_id))
10634 .and_then(|record| record.local_address().cloned())
10635 .and_then(|address| {
10636 resolve_udp_bind_addr(address.host(), address.port(), socket.family).ok()
10637 })
10638 .or_else(|| socket.local_addr());
10639 let Some(guest_addr) = guest_addr else {
10640 continue;
10641 };
10642 let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10643 used_udp_ports
10644 .entry(family)
10645 .or_default()
10646 .insert(guest_addr.port());
10647 if let Some(host_addr) = socket
10648 .socket
10649 .as_ref()
10650 .and_then(|socket| socket.local_addr().ok())
10651 {
10652 if is_loopback_ip(guest_addr.ip()) {
10653 udp_guest_to_host.insert((family, guest_addr.port()), host_addr.port());
10654 udp_host_to_guest.insert((family, host_addr.port()), guest_addr.port());
10655 }
10656 } else if socket.kernel_socket_id.is_some() && is_loopback_ip(guest_addr.ip()) {
10657 udp_guest_to_host.insert((family, guest_addr.port()), guest_addr.port());
10658 udp_host_to_guest.insert((family, guest_addr.port()), guest_addr.port());
10659 }
10660 }
10661
10662 for (child_process_id, child) in &process.child_processes {
10663 let child_id = format!("{process_id}/{child_process_id}");
10664 collect_javascript_socket_port_state(
10665 kernel,
10666 &child_id,
10667 child,
10668 tcp_guest_to_host,
10669 http_loopback_targets,
10670 udp_guest_to_host,
10671 udp_host_to_guest,
10672 used_tcp_ports,
10673 used_udp_ports,
10674 );
10675 }
10676}
10677
10678pub(crate) fn build_javascript_socket_path_context(
10679 vm: &VmState,
10680) -> Result<JavascriptSocketPathContext, SidecarError> {
10681 let mut loopback_exempt_ports = vm.create_loopback_exempt_ports.clone();
10682 loopback_exempt_ports.extend(vm.configuration.loopback_exempt_ports.iter().copied());
10683 let mut tcp_loopback_guest_to_host_ports = BTreeMap::new();
10684 let mut http_loopback_targets = BTreeMap::new();
10685 let mut udp_loopback_guest_to_host_ports = BTreeMap::new();
10686 let mut udp_loopback_host_to_guest_ports = BTreeMap::new();
10687 let mut used_tcp_guest_ports = BTreeMap::new();
10688 let mut used_udp_guest_ports = BTreeMap::new();
10689 for (process_id, process) in &vm.active_processes {
10690 collect_javascript_socket_port_state(
10691 &vm.kernel,
10692 process_id,
10693 process,
10694 &mut tcp_loopback_guest_to_host_ports,
10695 &mut http_loopback_targets,
10696 &mut udp_loopback_guest_to_host_ports,
10697 &mut udp_loopback_host_to_guest_ports,
10698 &mut used_tcp_guest_ports,
10699 &mut used_udp_guest_ports,
10700 );
10701 }
10702 Ok(JavascriptSocketPathContext {
10703 sandbox_root: vm.cwd.clone(),
10704 mounts: vm.configuration.mounts.clone(),
10705 listen_policy: vm.listen_policy,
10706 loopback_exempt_ports,
10707 tcp_loopback_guest_to_host_ports,
10708 http_loopback_targets,
10709 udp_loopback_guest_to_host_ports,
10710 udp_loopback_host_to_guest_ports,
10711 used_tcp_guest_ports,
10712 used_udp_guest_ports,
10713 })
10714}
10715
10716fn check_network_resource_limit(
10717 limit: Option<usize>,
10718 current: usize,
10719 additional: usize,
10720 label: &str,
10721) -> Result<(), SidecarError> {
10722 if let Some(limit) = limit {
10723 if current.saturating_add(additional) > limit {
10724 return Err(SidecarError::Execution(format!(
10725 "EAGAIN: maximum {label} count reached"
10726 )));
10727 }
10728 }
10729 Ok(())
10730}
10731
10732fn normalize_tcp_listen_host(
10733 host: Option<&str>,
10734) -> Result<(JavascriptSocketFamily, &'static str, &'static str), SidecarError> {
10735 match host.unwrap_or("127.0.0.1") {
10736 "127.0.0.1" | "localhost" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "127.0.0.1")),
10737 "::1" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::1")),
10738 "0.0.0.0" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "0.0.0.0")),
10739 "::" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::")),
10740 other => Err(SidecarError::Execution(format!(
10741 "EACCES: TCP listeners must bind to loopback or unspecified addresses, got {other}"
10742 ))),
10743 }
10744}
10745
10746fn normalize_udp_bind_host(
10747 host: Option<&str>,
10748 family: JavascriptUdpFamily,
10749) -> Result<(&'static str, &'static str, JavascriptSocketFamily), SidecarError> {
10750 match (family, host) {
10751 (JavascriptUdpFamily::Ipv4, None) | (JavascriptUdpFamily::Ipv4, Some("0.0.0.0")) => {
10752 Ok(("127.0.0.1", "0.0.0.0", JavascriptSocketFamily::Ipv4))
10753 }
10754 (JavascriptUdpFamily::Ipv4, Some("127.0.0.1"))
10755 | (JavascriptUdpFamily::Ipv4, Some("localhost")) => {
10756 Ok(("127.0.0.1", "127.0.0.1", JavascriptSocketFamily::Ipv4))
10757 }
10758 (JavascriptUdpFamily::Ipv6, None) | (JavascriptUdpFamily::Ipv6, Some("::")) => {
10759 Ok(("::1", "::", JavascriptSocketFamily::Ipv6))
10760 }
10761 (JavascriptUdpFamily::Ipv6, Some("::1"))
10762 | (JavascriptUdpFamily::Ipv6, Some("localhost")) => {
10763 Ok(("::1", "::1", JavascriptSocketFamily::Ipv6))
10764 }
10765 (JavascriptUdpFamily::Ipv4, Some(other)) => Err(SidecarError::Execution(format!(
10766 "EACCES: udp4 sockets must bind to 127.0.0.1 or 0.0.0.0, got {other}"
10767 ))),
10768 (JavascriptUdpFamily::Ipv6, Some(other)) => Err(SidecarError::Execution(format!(
10769 "EACCES: udp6 sockets must bind to ::1 or ::, got {other}"
10770 ))),
10771 }
10772}
10773
10774fn allocate_guest_listen_port(
10775 requested_port: u16,
10776 family: JavascriptSocketFamily,
10777 used_ports: &BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10778 policy: VmListenPolicy,
10779) -> Result<u16, SidecarError> {
10780 let is_allowed = |port: u16| {
10781 port >= policy.port_min
10782 && port <= policy.port_max
10783 && (policy.allow_privileged || port >= 1024)
10784 };
10785 let used = used_ports.get(&family);
10786
10787 if requested_port != 0 {
10788 if !is_allowed(requested_port) {
10789 let reason = if requested_port < 1024 && !policy.allow_privileged {
10790 format!(
10791 "EACCES: privileged listen port {requested_port} requires {}=true",
10792 VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY
10793 )
10794 } else {
10795 format!(
10796 "EACCES: listen port {requested_port} is outside the allowed range {}-{}",
10797 policy.port_min, policy.port_max
10798 )
10799 };
10800 return Err(SidecarError::Execution(reason));
10801 }
10802 if used.is_some_and(|ports| ports.contains(&requested_port)) {
10803 return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
10804 libc::EADDRINUSE,
10805 )));
10806 }
10807 return Ok(requested_port);
10808 }
10809
10810 let allocation_start = policy
10811 .port_min
10812 .max(if policy.allow_privileged { 1 } else { 1024 });
10813 for candidate in allocation_start..=policy.port_max {
10814 if used.is_some_and(|ports| ports.contains(&candidate)) {
10815 continue;
10816 }
10817 return Ok(candidate);
10818 }
10819
10820 Err(sidecar_net_error(std::io::Error::from_raw_os_error(
10821 libc::EADDRINUSE,
10822 )))
10823}
10824
10825fn socket_host_matches(requested: Option<&str>, actual: &str) -> bool {
10826 match requested {
10827 None => true,
10828 Some(requested) if requested == actual => true,
10829 Some(requested)
10830 if is_unspecified_socket_host(requested) && is_unspecified_socket_host(actual) =>
10831 {
10832 true
10833 }
10834 Some(requested) if is_unspecified_socket_host(requested) => is_loopback_socket_host(actual),
10835 Some(requested) if requested.eq_ignore_ascii_case("localhost") => {
10836 is_loopback_socket_host(actual)
10837 }
10838 _ => false,
10839 }
10840}
10841
10842fn parse_proc_net_entries(table_path: &str) -> Result<Vec<ProcNetEntry>, SidecarError> {
10843 let contents = match fs::read_to_string(table_path) {
10844 Ok(contents) => contents,
10845 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
10846 Err(error) => {
10847 return Err(SidecarError::Io(format!(
10848 "failed to inspect socket table {table_path}: {error}"
10849 )));
10850 }
10851 };
10852
10853 let mut entries = Vec::new();
10854 for line in contents.lines().skip(1) {
10855 let columns = line.split_whitespace().collect::<Vec<_>>();
10856 if columns.len() < 10 {
10857 continue;
10858 }
10859 let Some((host, port)) = parse_proc_ip_port(columns[1]) else {
10860 continue;
10861 };
10862 let Ok(inode) = columns[9].parse::<u64>() else {
10863 continue;
10864 };
10865 entries.push(ProcNetEntry {
10866 local_host: host,
10867 local_port: port,
10868 state: columns[3].to_owned(),
10869 inode,
10870 });
10871 }
10872
10873 Ok(entries)
10874}
10875
10876fn parse_proc_ip_port(value: &str) -> Option<(String, u16)> {
10877 let (raw_ip, raw_port) = value.split_once(':')?;
10878 let port = u16::from_str_radix(raw_port, 16).ok()?;
10879 let host = match raw_ip.len() {
10880 8 => {
10881 let raw = u32::from_str_radix(raw_ip, 16).ok()?;
10882 Ipv4Addr::from(raw.to_le_bytes()).to_string()
10883 }
10884 32 => {
10885 let mut bytes = [0_u8; 16];
10886 for (index, chunk) in raw_ip.as_bytes().chunks(8).enumerate() {
10887 let word = u32::from_str_radix(std::str::from_utf8(chunk).ok()?, 16).ok()?;
10888 bytes[index * 4..(index + 1) * 4].copy_from_slice(&word.to_le_bytes());
10889 }
10890 Ipv6Addr::from(bytes).to_string()
10891 }
10892 _ => return None,
10893 };
10894 Some((host, port))
10895}
10896
10897fn python_file_entrypoint(entrypoint: &str) -> Option<PathBuf> {
10898 let path = Path::new(entrypoint);
10899 (path.extension().and_then(|extension| extension.to_str()) == Some("py"))
10900 .then(|| path.to_path_buf())
10901}
10902
10903fn add_runtime_guest_path_mapping(
10904 env: &mut BTreeMap<String, String>,
10905 guest_path: &str,
10906 host_path: &Path,
10907) {
10908 let mut mappings = env
10909 .get("AGENTOS_GUEST_PATH_MAPPINGS")
10910 .and_then(|value| serde_json::from_str::<Vec<Value>>(value).ok())
10911 .unwrap_or_default();
10912 mappings.retain(|mapping| {
10913 mapping
10914 .get("guestPath")
10915 .and_then(Value::as_str)
10916 .map(|existing| normalize_path(existing) != normalize_path(guest_path))
10917 .unwrap_or(true)
10918 });
10919 mappings.push(json!({
10920 "guestPath": normalize_path(guest_path),
10921 "hostPath": host_path.display().to_string(),
10922 }));
10923 if let Ok(serialized) = serde_json::to_string(&mappings) {
10924 env.insert(String::from("AGENTOS_GUEST_PATH_MAPPINGS"), serialized);
10925 }
10926}
10927
10928fn add_runtime_host_access_path(
10929 env: &mut BTreeMap<String, String>,
10930 key: &str,
10931 host_path: &Path,
10932 expand: bool,
10933) {
10934 let existing = env
10935 .get(key)
10936 .and_then(|value| serde_json::from_str::<Vec<String>>(value).ok())
10937 .unwrap_or_default()
10938 .into_iter()
10939 .map(PathBuf::from)
10940 .collect::<Vec<_>>();
10941 let mut paths = existing;
10942 paths.push(host_path.to_path_buf());
10943 let normalized = if expand {
10944 expand_host_access_paths(&paths)
10945 } else {
10946 dedupe_host_paths(&paths)
10947 };
10948 let serialized = normalized
10949 .iter()
10950 .map(|path| path.to_string_lossy().into_owned())
10951 .collect::<Vec<_>>();
10952 if let Ok(serialized) = serde_json::to_string(&serialized) {
10953 env.insert(key.to_owned(), serialized);
10954 }
10955}
10956
10957fn is_path_like_specifier(specifier: &str) -> bool {
10960 specifier.starts_with('/')
10961 || specifier.starts_with("./")
10962 || specifier.starts_with("../")
10963 || specifier.starts_with("file:")
10964}
10965
10966fn execution_wasm_permission_tier(tier: WasmPermissionTier) -> ExecutionWasmPermissionTier {
10967 match tier {
10968 WasmPermissionTier::Full => ExecutionWasmPermissionTier::Full,
10969 WasmPermissionTier::ReadWrite => ExecutionWasmPermissionTier::ReadWrite,
10970 WasmPermissionTier::ReadOnly => ExecutionWasmPermissionTier::ReadOnly,
10971 WasmPermissionTier::Isolated => ExecutionWasmPermissionTier::Isolated,
10972 }
10973}
10974
10975fn resolve_wasm_permission_tier(
10976 vm: &VmState,
10977 command_name: Option<&str>,
10978 explicit_tier: Option<WasmPermissionTier>,
10979 entrypoint: &str,
10980) -> WasmPermissionTier {
10981 explicit_tier
10982 .or_else(|| command_name.and_then(|command| vm.command_permissions.get(command).copied()))
10983 .or_else(|| {
10984 Path::new(entrypoint)
10985 .file_name()
10986 .and_then(|name| name.to_str())
10987 .and_then(|command| vm.command_permissions.get(command).copied())
10988 })
10989 .unwrap_or(WasmPermissionTier::Full)
10990}
10991
10992fn tokenize_shell_free_command(command: &str) -> Vec<String> {
10993 command
10994 .split_whitespace()
10995 .filter(|segment| !segment.is_empty())
10996 .map(str::to_owned)
10997 .collect()
10998}
10999
11000fn is_posix_shell_builtin(command: &str) -> bool {
11001 matches!(
11002 command,
11003 "." | ":"
11004 | "break"
11005 | "cd"
11006 | "continue"
11007 | "eval"
11008 | "exec"
11009 | "exit"
11010 | "export"
11011 | "readonly"
11012 | "return"
11013 | "set"
11014 | "shift"
11015 | "times"
11016 | "trap"
11017 | "umask"
11018 | "unset"
11019 )
11020}
11021
11022fn shell_first_token_requires_shell(token: &str) -> bool {
11028 token.contains('=') || is_shell_reserved_word(token)
11029}
11030
11031fn is_shell_reserved_word(token: &str) -> bool {
11032 matches!(
11033 token,
11034 "if" | "then"
11035 | "elif"
11036 | "else"
11037 | "fi"
11038 | "for"
11039 | "in"
11040 | "do"
11041 | "done"
11042 | "while"
11043 | "until"
11044 | "case"
11045 | "esac"
11046 | "{"
11047 | "}"
11048 | "!"
11049 )
11050}
11051
11052fn command_requires_shell(command: &str) -> bool {
11053 command.chars().any(|ch| {
11054 matches!(
11055 ch,
11056 '|' | '&'
11057 | ';'
11058 | '<'
11059 | '>'
11060 | '('
11061 | ')'
11062 | '$'
11063 | '`'
11064 | '*'
11065 | '?'
11066 | '['
11067 | ']'
11068 | '{'
11069 | '}'
11070 | '~'
11071 | '\''
11072 | '"'
11073 | '\\'
11074 | '\n'
11075 )
11076 })
11077}
11078
11079fn host_mount_path_for_guest_path(vm: &VmState, guest_path: &str) -> Option<PathBuf> {
11080 let normalized = normalize_path(guest_path);
11081
11082 let mut mounts = vm
11083 .configuration
11084 .mounts
11085 .iter()
11086 .filter_map(|mount| {
11087 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11088 .then(|| {
11089 mount_config_host_path(&mount.plugin.config)
11090 .map(|host_path| (mount.guest_path.as_str(), host_path))
11091 })
11092 .flatten()
11093 })
11094 .collect::<Vec<_>>();
11095 mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
11096
11097 for (guest_root, host_root) in mounts {
11098 if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
11099 continue;
11100 }
11101
11102 let suffix = normalized
11103 .strip_prefix(guest_root)
11104 .unwrap_or_default()
11105 .trim_start_matches('/');
11106 let mut path = PathBuf::from(host_root);
11107 if !suffix.is_empty() {
11108 path.push(suffix);
11109 }
11110 return Some(path);
11111 }
11112
11113 None
11114}
11115
11116fn host_runtime_path_for_guest_path_with_env(
11117 vm: &VmState,
11118 runtime_env: &BTreeMap<String, String>,
11119 guest_path: &str,
11120 default_host_cwd: &Path,
11121) -> Option<PathBuf> {
11122 if let Some(path) = host_mount_path_for_guest_path(vm, guest_path) {
11123 return Some(path);
11124 }
11125 if let Some(path) = host_path_from_runtime_guest_mappings(runtime_env, guest_path) {
11126 return Some(path);
11127 }
11128
11129 let normalized = normalize_path(guest_path);
11130 let virtual_home = guest_virtual_home(vm);
11131
11132 if normalized == virtual_home || normalized.starts_with(&format!("{virtual_home}/")) {
11133 let suffix = normalized
11134 .strip_prefix(&virtual_home)
11135 .unwrap_or_default()
11136 .trim_start_matches('/');
11137 let mut host_path = default_host_cwd.to_path_buf();
11138 if !suffix.is_empty() {
11139 host_path.push(suffix);
11140 }
11141 return Some(host_path);
11142 }
11143
11144 None
11145}
11146
11147#[derive(Deserialize, Serialize)]
11148struct RuntimeGuestPathMapping {
11149 #[serde(rename = "guestPath")]
11150 guest_path: String,
11151 #[serde(rename = "hostPath")]
11152 host_path: String,
11153 #[serde(rename = "readOnly", default)]
11154 read_only: bool,
11155}
11156
11157pub(crate) fn host_path_from_runtime_guest_mappings(
11158 runtime_env: &BTreeMap<String, String>,
11159 guest_path: &str,
11160) -> Option<PathBuf> {
11161 let mappings = runtime_env
11162 .get("AGENTOS_GUEST_PATH_MAPPINGS")
11163 .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
11164 let normalized = normalize_path(guest_path);
11165
11166 let mut sorted_mappings = mappings
11167 .into_iter()
11168 .filter_map(|mapping| {
11169 (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
11170 normalize_path(&mapping.guest_path),
11171 PathBuf::from(mapping.host_path),
11172 ))
11173 })
11174 .collect::<Vec<_>>();
11175 sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.0.len()));
11176
11177 for (guest_root, mut host_root) in sorted_mappings {
11178 if guest_root != "/"
11179 && normalized != guest_root
11180 && !normalized.starts_with(&format!("{guest_root}/"))
11181 {
11182 continue;
11183 }
11184 if guest_root == "/" && !normalized.starts_with('/') {
11185 continue;
11186 }
11187
11188 if host_root.is_relative() {
11189 host_root = std::env::current_dir().ok()?.join(host_root);
11190 }
11191
11192 let suffix = if guest_root == "/" {
11193 normalized.trim_start_matches('/')
11194 } else {
11195 normalized
11196 .strip_prefix(&guest_root)
11197 .unwrap_or_default()
11198 .trim_start_matches('/')
11199 };
11200 if !suffix.is_empty() {
11201 host_root.push(suffix);
11202 }
11203 return Some(host_root);
11204 }
11205
11206 None
11207}
11208
11209fn guest_runtime_path_for_host_path(
11210 runtime_env: &BTreeMap<String, String>,
11211 virtual_home: &str,
11212 cwd: &Path,
11213 host_path: &str,
11214) -> Option<String> {
11215 let resolved = if host_path.starts_with("file://") {
11216 PathBuf::from(host_path.trim_start_matches("file://"))
11217 } else if host_path.starts_with("file:") {
11218 PathBuf::from(host_path.trim_start_matches("file:"))
11219 } else {
11220 let candidate = PathBuf::from(host_path);
11221 if candidate.is_absolute() {
11222 candidate
11223 } else if host_path.starts_with("./") || host_path.starts_with("../") {
11224 cwd.join(candidate)
11225 } else {
11226 return None;
11227 }
11228 };
11229 let normalized = normalize_host_path(&resolved);
11230
11231 if let Some(path) = guest_path_from_runtime_host_mappings(runtime_env, &normalized) {
11232 return Some(path);
11233 }
11234
11235 let normalized_cwd = normalize_host_path(cwd);
11236 if !path_is_within_root(&normalized, &normalized_cwd) {
11237 return None;
11238 }
11239
11240 let virtual_home = if virtual_home.starts_with('/') {
11241 virtual_home.to_string()
11242 } else {
11243 String::from("/root")
11244 };
11245 let suffix = normalized
11246 .strip_prefix(&normalized_cwd)
11247 .ok()?
11248 .to_string_lossy()
11249 .replace('\\', "/")
11250 .trim_start_matches('/')
11251 .to_owned();
11252
11253 Some(if suffix.is_empty() {
11254 virtual_home
11255 } else {
11256 normalize_path(&format!("{virtual_home}/{suffix}"))
11257 })
11258}
11259
11260fn guest_path_from_runtime_host_mappings(
11261 runtime_env: &BTreeMap<String, String>,
11262 host_path: &Path,
11263) -> Option<String> {
11264 let mappings = runtime_env
11265 .get("AGENTOS_GUEST_PATH_MAPPINGS")
11266 .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
11267 let normalized = normalize_host_path(host_path);
11268
11269 let mut sorted_mappings = mappings
11270 .into_iter()
11271 .filter_map(|mapping| {
11272 (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
11273 normalize_path(&mapping.guest_path),
11274 normalize_host_path(Path::new(&mapping.host_path)),
11275 ))
11276 })
11277 .collect::<Vec<_>>();
11278 sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.1.as_os_str().len()));
11279
11280 for (guest_root, host_root) in sorted_mappings {
11281 if !path_is_within_root(&normalized, &host_root) {
11282 continue;
11283 }
11284 let suffix = normalized
11285 .strip_prefix(&host_root)
11286 .ok()?
11287 .to_string_lossy()
11288 .replace('\\', "/")
11289 .trim_start_matches('/')
11290 .to_owned();
11291
11292 return Some(if suffix.is_empty() {
11293 guest_root
11294 } else if guest_root == "/" {
11295 normalize_path(&format!("/{suffix}"))
11296 } else {
11297 normalize_path(&format!("{guest_root}/{suffix}"))
11298 });
11299 }
11300
11301 None
11302}
11303
11304fn host_mount_path_for_guest_path_from_mounts(
11305 mounts: &[crate::protocol::MountDescriptor],
11306 guest_path: &str,
11307) -> Option<PathBuf> {
11308 let normalized = normalize_path(guest_path);
11309
11310 let mut host_mounts = mounts
11311 .iter()
11312 .filter_map(|mount| {
11313 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11314 .then(|| {
11315 mount_config_host_path(&mount.plugin.config)
11316 .map(|host_path| (mount.guest_path.as_str(), host_path))
11317 })
11318 .flatten()
11319 })
11320 .collect::<Vec<_>>();
11321 host_mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
11322
11323 for (guest_root, host_root) in host_mounts {
11324 if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
11325 continue;
11326 }
11327
11328 let suffix = normalized
11329 .strip_prefix(guest_root)
11330 .unwrap_or_default()
11331 .trim_start_matches('/');
11332 let mut path = PathBuf::from(host_root);
11333 if !suffix.is_empty() {
11334 path.push(suffix);
11335 }
11336 return Some(path);
11337 }
11338
11339 None
11340}
11341
11342#[cfg(test)]
11343mod host_mount_path_for_guest_path_from_mounts_tests {
11344 use super::host_mount_path_for_guest_path_from_mounts;
11345 use crate::protocol::{MountDescriptor, MountPluginDescriptor};
11346 use serde_json::json;
11347 use std::path::PathBuf;
11348
11349 #[test]
11350 fn resolves_module_access_mount_paths() {
11351 let mounts = vec![MountDescriptor {
11352 guest_path: String::from("/root/node_modules"),
11353 read_only: true,
11354 plugin: MountPluginDescriptor {
11355 id: String::from("module_access"),
11356 config: json!({
11357 "hostPath": "/tmp/workspace/node_modules",
11358 })
11359 .to_string(),
11360 },
11361 }];
11362
11363 let resolved =
11364 host_mount_path_for_guest_path_from_mounts(&mounts, "/root/node_modules/pkg/index.js")
11365 .expect("module_access mount should resolve");
11366
11367 assert_eq!(
11368 resolved,
11369 PathBuf::from("/tmp/workspace/node_modules/pkg/index.js")
11370 );
11371 }
11372}
11373
11374fn resolve_guest_socket_host_path(
11375 context: &JavascriptSocketPathContext,
11376 guest_path: &str,
11377) -> PathBuf {
11378 if let Some(path) = host_mount_path_for_guest_path_from_mounts(&context.mounts, guest_path) {
11379 return path;
11380 }
11381
11382 let normalized = normalize_path(guest_path);
11383 let mut host_path = context.sandbox_root.clone();
11384 let suffix = normalized.trim_start_matches('/');
11385 if !suffix.is_empty() {
11386 host_path.push(suffix);
11387 }
11388 host_path
11389}
11390
11391fn ensure_kernel_parent_directories(
11392 kernel: &mut SidecarKernel,
11393 path: &str,
11394) -> Result<(), SidecarError> {
11395 let parent = dirname(path);
11396 if parent != "/" && !kernel.exists(&parent).map_err(kernel_error)? {
11397 kernel.mkdir(&parent, true).map_err(kernel_error)?;
11398 }
11399 Ok(())
11400}
11401
11402pub(crate) fn sanitize_javascript_child_process_internal_bootstrap_env(
11406 env: &BTreeMap<String, String>,
11407) -> BTreeMap<String, String> {
11408 const ALLOWED_KEYS: &[&str] = &[
11409 "AGENTOS_ALLOWED_NODE_BUILTINS",
11410 "AGENTOS_GUEST_PATH_MAPPINGS",
11411 "AGENTOS_LOOPBACK_EXEMPT_PORTS",
11412 "AGENTOS_VIRTUAL_PROCESS_EXEC_PATH",
11413 "AGENTOS_VIRTUAL_PROCESS_UID",
11414 "AGENTOS_VIRTUAL_PROCESS_GID",
11415 "AGENTOS_VIRTUAL_PROCESS_VERSION",
11416 ];
11417
11418 env.iter()
11419 .filter(|(key, _)| {
11420 ALLOWED_KEYS.contains(&key.as_str()) || key.starts_with("AGENTOS_VIRTUAL_OS_")
11421 })
11422 .map(|(key, value)| (key.clone(), value.clone()))
11423 .collect()
11424}
11425
11426fn resolve_tcp_bind_addr(host: &str, port: u16) -> Result<SocketAddr, SidecarError> {
11431 (host, port)
11432 .to_socket_addrs()
11433 .map_err(sidecar_net_error)?
11434 .next()
11435 .ok_or_else(|| {
11436 SidecarError::Execution(format!("failed to resolve TCP bind address {host}:{port}"))
11437 })
11438}
11439
11440pub(crate) fn format_dns_resource(hostname: &str) -> String {
11441 format!("dns://{hostname}")
11442}
11443
11444pub(crate) fn format_tcp_resource(host: &str, port: u16) -> String {
11445 format!("tcp://{host}:{port}")
11446}
11447
11448fn is_loopback_ip(ip: IpAddr) -> bool {
11449 match ip {
11450 IpAddr::V4(ip) => ip.is_loopback(),
11451 IpAddr::V6(ip) => {
11452 ip.is_loopback()
11453 || ip
11454 .to_ipv4_mapped()
11455 .is_some_and(|mapped| mapped.is_loopback())
11456 }
11457 }
11458}
11459
11460fn loopback_cidr(ip: IpAddr) -> &'static str {
11461 match ip {
11462 IpAddr::V4(ip) if ip.is_loopback() => "127.0.0.0/8",
11463 IpAddr::V6(ip)
11464 if ip
11465 .to_ipv4_mapped()
11466 .is_some_and(|mapped| mapped.is_loopback()) =>
11467 {
11468 "127.0.0.0/8"
11469 }
11470 IpAddr::V6(_) => "::1/128",
11471 IpAddr::V4(_) => "127.0.0.0/8",
11472 }
11473}
11474
11475fn ipv4_compatible_embedded(ip: Ipv6Addr) -> Option<Ipv4Addr> {
11481 let segments = ip.segments();
11482 if segments[0..6].iter().any(|&s| s != 0) {
11483 return None;
11484 }
11485 let embedded = (u32::from(segments[6]) << 16) | u32::from(segments[7]);
11486 if embedded == 0 || embedded == 1 {
11489 return None;
11490 }
11491 Some(Ipv4Addr::from(embedded))
11492}
11493
11494fn restricted_non_loopback_ip_range(ip: IpAddr) -> Option<(&'static str, &'static str)> {
11495 match ip {
11496 IpAddr::V4(ip) => {
11497 if ip.is_unspecified() {
11498 return Some(("0.0.0.0/32", "unspecified"));
11501 }
11502 let [first, second, ..] = ip.octets();
11503 match (first, second) {
11504 (10, _) => Some(("10.0.0.0/8", "private")),
11505 (100, 64..=127) => Some(("100.64.0.0/10", "carrier-grade-nat")),
11506 (172, 16..=31) => Some(("172.16.0.0/12", "private")),
11507 (192, 168) => Some(("192.168.0.0/16", "private")),
11508 (169, 254) => Some(("169.254.0.0/16", "link-local")),
11509 (224..=239, _) => Some(("224.0.0.0/4", "multicast")),
11514 (240..=255, _) => Some(("240.0.0.0/4", "reserved")),
11515 _ => None,
11516 }
11517 }
11518 IpAddr::V6(ip) => {
11519 if let Some(mapped) = ip.to_ipv4_mapped() {
11520 return restricted_non_loopback_ip_range(IpAddr::V4(mapped));
11521 }
11522 if let Some(compat) = ipv4_compatible_embedded(ip) {
11529 return restricted_non_loopback_ip_range(IpAddr::V4(compat));
11530 }
11531
11532 if ip.is_unspecified() {
11533 return Some(("::/128", "unspecified"));
11536 }
11537
11538 let segments = ip.segments();
11539 if (segments[0] & 0xfe00) == 0xfc00 {
11540 return Some(("fc00::/7", "unique-local"));
11541 }
11542 if (segments[0] & 0xffc0) == 0xfe80 {
11543 return Some(("fe80::/10", "link-local"));
11544 }
11545 None
11546 }
11547 }
11548}
11549
11550fn blocked_dns_resolution_error(
11551 resource: &str,
11552 ip: IpAddr,
11553 cidr: &str,
11554 label: &str,
11555) -> SidecarError {
11556 SidecarError::Execution(format!(
11557 "EACCES: blocked outbound network access to {resource}: {ip} is within restricted {label} range {cidr}"
11558 ))
11559}
11560
11561fn blocked_loopback_connect_error(resource: &str, ip: IpAddr, port: u16) -> SidecarError {
11562 SidecarError::Execution(format!(
11563 "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}",
11564 loopback_cidr(ip)
11565 ))
11566}
11567
11568fn filter_dns_safe_ip_addrs(
11569 addresses: Vec<IpAddr>,
11570 hostname: &str,
11571) -> Result<Vec<IpAddr>, SidecarError> {
11572 let resource = format_dns_resource(hostname);
11573 let mut allowed = Vec::new();
11574 let mut blocked = None;
11575
11576 for ip in addresses {
11577 if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
11578 blocked.get_or_insert((ip, cidr, label));
11579 continue;
11580 }
11581 allowed.push(ip);
11582 }
11583
11584 if allowed.is_empty() {
11585 let (ip, cidr, label) = blocked.expect("blocked DNS results should capture a reason");
11586 return Err(blocked_dns_resolution_error(&resource, ip, cidr, label));
11587 }
11588
11589 Ok(allowed)
11590}
11591
11592fn loopback_connect_allowed(context: &JavascriptSocketPathContext, port: u16) -> bool {
11593 context.loopback_port_allowed(port)
11594}
11595
11596fn filter_tcp_connect_ip_addrs(
11597 addresses: Vec<IpAddr>,
11598 host: &str,
11599 port: u16,
11600 context: &JavascriptSocketPathContext,
11601) -> Result<Vec<IpAddr>, SidecarError> {
11602 let resource = format_tcp_resource(host, port);
11603 let mut allowed = Vec::new();
11604 let mut blocked = None;
11605
11606 for ip in addresses {
11607 if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
11608 blocked.get_or_insert_with(|| blocked_dns_resolution_error(&resource, ip, cidr, label));
11609 continue;
11610 }
11611 if is_loopback_ip(ip) && !loopback_connect_allowed(context, port) {
11612 blocked.get_or_insert_with(|| blocked_loopback_connect_error(&resource, ip, port));
11613 continue;
11614 }
11615 allowed.push(ip);
11616 }
11617
11618 if allowed.is_empty() {
11619 return Err(blocked.expect("blocked TCP connect results should capture a reason"));
11620 }
11621
11622 Ok(allowed)
11623}
11624
11625fn resolve_tcp_connect_addr<B>(
11626 bridge: &SharedBridge<B>,
11627 kernel: &SidecarKernel,
11628 vm_id: &str,
11629 dns: &VmDnsConfig,
11630 host: &str,
11631 port: u16,
11632 context: &JavascriptSocketPathContext,
11633) -> Result<ResolvedTcpConnectAddr, SidecarError>
11634where
11635 B: NativeSidecarBridge + Send + 'static,
11636 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11637{
11638 let allowed = filter_tcp_connect_ip_addrs(
11639 resolve_dns_ip_addrs(
11640 bridge,
11641 kernel,
11642 vm_id,
11643 dns,
11644 host,
11645 DnsLookupPolicy::SkipPermissions,
11646 )?,
11647 host,
11648 port,
11649 context,
11650 )?;
11651 let ip = allowed
11652 .iter()
11653 .copied()
11654 .find(|candidate| {
11655 let family = JavascriptSocketFamily::from_ip(*candidate);
11656 context.translate_tcp_loopback_port(family, port).is_some()
11657 })
11658 .or_else(|| allowed.iter().copied().find(IpAddr::is_ipv4))
11661 .or_else(|| allowed.first().copied())
11662 .ok_or_else(|| {
11663 SidecarError::Execution(format!("failed to resolve TCP address {host}:{port}"))
11664 })?;
11665 let family = JavascriptSocketFamily::from_ip(ip);
11666 let translated_loopback_port = context.translate_tcp_loopback_port(family, port);
11667 let use_kernel_loopback = is_loopback_ip(ip) && translated_loopback_port == Some(port);
11668 let actual_port = if is_loopback_ip(ip) {
11669 translated_loopback_port.unwrap_or(port)
11670 } else {
11671 port
11672 };
11673 Ok(ResolvedTcpConnectAddr {
11674 actual_addr: SocketAddr::new(ip, actual_port),
11675 guest_remote_addr: SocketAddr::new(ip, port),
11676 use_kernel_loopback,
11677 })
11678}
11679
11680fn resolve_dns_ip_addrs<B>(
11681 bridge: &SharedBridge<B>,
11682 kernel: &SidecarKernel,
11683 vm_id: &str,
11684 dns: &VmDnsConfig,
11685 hostname: &str,
11686 policy: DnsLookupPolicy,
11687) -> Result<Vec<IpAddr>, SidecarError>
11688where
11689 B: NativeSidecarBridge + Send + 'static,
11690 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11691{
11692 let resolution = match kernel.resolve_dns(hostname, policy) {
11693 Ok(resolution) => resolution,
11694 Err(error) => {
11695 let sidecar_error = kernel_error(error.clone());
11696 if error.code() != "EACCES" {
11697 emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
11698 }
11699 return Err(sidecar_error);
11700 }
11701 };
11702 emit_dns_resolution_event(
11703 bridge,
11704 vm_id,
11705 hostname,
11706 resolution.source(),
11707 resolution.addresses(),
11708 dns,
11709 );
11710 Ok(resolution.addresses().to_vec())
11711}
11712
11713fn resolve_dns_records<B>(
11714 bridge: &SharedBridge<B>,
11715 kernel: &SidecarKernel,
11716 vm_id: &str,
11717 dns: &VmDnsConfig,
11718 hostname: &str,
11719 record_type: RecordType,
11720 policy: DnsLookupPolicy,
11721) -> Result<DnsRecordResolution, SidecarError>
11722where
11723 B: NativeSidecarBridge + Send + 'static,
11724 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11725{
11726 let resolution = match kernel.resolve_dns_records(hostname, record_type, policy) {
11727 Ok(resolution) => resolution,
11728 Err(error) => {
11729 let sidecar_error = kernel_error(error.clone());
11730 if error.code() != "EACCES" {
11731 emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
11732 }
11733 return Err(sidecar_error);
11734 }
11735 };
11736 emit_dns_record_resolution_event(bridge, vm_id, hostname, &resolution, dns);
11737 Ok(resolution)
11738}
11739
11740fn filter_dns_ip_addrs(
11741 addresses: Vec<IpAddr>,
11742 family: Option<u8>,
11743) -> Result<Vec<IpAddr>, SidecarError> {
11744 let filtered: Vec<_> = match family.unwrap_or(0) {
11745 0 => addresses,
11746 4 => addresses
11747 .into_iter()
11748 .filter(|ip| matches!(ip, IpAddr::V4(_)))
11749 .collect(),
11750 6 => addresses
11751 .into_iter()
11752 .filter(|ip| matches!(ip, IpAddr::V6(_)))
11753 .collect(),
11754 other => {
11755 return Err(SidecarError::InvalidState(format!(
11756 "unsupported dns family {other}"
11757 )));
11758 }
11759 };
11760
11761 if filtered.is_empty() {
11762 return Err(SidecarError::Execution(String::from(
11763 "failed to resolve DNS address for requested family",
11764 )));
11765 }
11766
11767 Ok(filtered)
11768}
11769
11770fn resolve_udp_bind_addr(
11771 host: &str,
11772 port: u16,
11773 family: JavascriptUdpFamily,
11774) -> Result<SocketAddr, SidecarError> {
11775 (host, port)
11776 .to_socket_addrs()
11777 .map_err(sidecar_net_error)?
11778 .find(|addr| family.matches_addr(addr))
11779 .ok_or_else(|| {
11780 SidecarError::Execution(format!(
11781 "failed to resolve {} UDP bind address {host}:{port}",
11782 family.socket_type()
11783 ))
11784 })
11785}
11786
11787fn resolve_udp_addr<B>(request: UdpRemoteAddrRequest<'_, B>) -> Result<SocketAddr, SidecarError>
11788where
11789 B: NativeSidecarBridge + Send + 'static,
11790 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11791{
11792 let UdpRemoteAddrRequest {
11793 bridge,
11794 kernel,
11795 vm_id,
11796 dns,
11797 host,
11798 port,
11799 family,
11800 context,
11801 } = request;
11802 resolve_dns_ip_addrs(
11803 bridge,
11804 kernel,
11805 vm_id,
11806 dns,
11807 host,
11808 DnsLookupPolicy::SkipPermissions,
11809 )?
11810 .into_iter()
11811 .map(|ip| {
11812 let family_key = JavascriptSocketFamily::from_ip(ip);
11813 let actual_port = if is_loopback_ip(ip) {
11814 context
11815 .translate_udp_loopback_port(family_key, port)
11816 .unwrap_or(port)
11817 } else {
11818 port
11819 };
11820 SocketAddr::new(ip, actual_port)
11821 })
11822 .find(|addr| family.matches_addr(addr))
11823 .ok_or_else(|| {
11824 SidecarError::Execution(format!(
11825 "failed to resolve {} UDP address {host}:{port}",
11826 family.socket_type()
11827 ))
11828 })
11829}
11830
11831fn socket_addr_family(addr: &SocketAddr) -> &'static str {
11832 match addr {
11833 SocketAddr::V4(_) => "IPv4",
11834 SocketAddr::V6(_) => "IPv6",
11835 }
11836}
11837
11838fn javascript_net_timeout_value() -> Value {
11839 Value::String(String::from(JAVASCRIPT_NET_TIMEOUT_SENTINEL))
11840}
11841
11842fn javascript_net_json_string(value: Value, label: &str) -> Result<Value, SidecarError> {
11843 serde_json::to_string(&value)
11844 .map(Value::String)
11845 .map_err(|error| {
11846 SidecarError::InvalidState(format!("failed to serialize {label} payload: {error}"))
11847 })
11848}
11849
11850fn javascript_net_read_value(
11851 event: Option<JavascriptTcpSocketEvent>,
11852) -> Result<Value, SidecarError> {
11853 match event {
11854 Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(Value::String(
11855 base64::engine::general_purpose::STANDARD.encode(chunk),
11856 )),
11857 Some(JavascriptTcpSocketEvent::End | JavascriptTcpSocketEvent::Close { .. }) => {
11858 Ok(Value::Null)
11859 }
11860 Some(JavascriptTcpSocketEvent::Error { code, message }) => {
11861 let detail = code.unwrap_or_else(|| String::from("socket read"));
11862 Err(SidecarError::Execution(format!("{detail}: {message}")))
11863 }
11864 None => Ok(javascript_net_timeout_value()),
11865 }
11866}
11867
11868fn io_error_code(error: &std::io::Error) -> Option<String> {
11869 match error.raw_os_error() {
11870 Some(libc::EADDRINUSE) => Some(String::from("EADDRINUSE")),
11871 Some(libc::EADDRNOTAVAIL) => Some(String::from("EADDRNOTAVAIL")),
11872 Some(libc::ECONNREFUSED) => Some(String::from("ECONNREFUSED")),
11873 Some(libc::ECONNRESET) => Some(String::from("ECONNRESET")),
11874 Some(libc::EINVAL) => Some(String::from("EINVAL")),
11875 Some(libc::EPIPE) => Some(String::from("EPIPE")),
11876 Some(libc::ETIMEDOUT) => Some(String::from("ETIMEDOUT")),
11877 Some(libc::EHOSTUNREACH) => Some(String::from("EHOSTUNREACH")),
11878 Some(libc::ENETUNREACH) => Some(String::from("ENETUNREACH")),
11879 _ => None,
11880 }
11881}
11882
11883fn sidecar_net_error(error: std::io::Error) -> SidecarError {
11884 let message = match io_error_code(&error) {
11885 Some(code) => format!("{code}: {error}"),
11886 None => error.to_string(),
11887 };
11888 SidecarError::Execution(message)
11889}
11890
11891fn tls_provider() -> Arc<rustls::crypto::CryptoProvider> {
11892 Arc::new(aws_lc_rs::default_provider())
11893}
11894
11895fn tls_local_certificates(
11896 options: &JavascriptTlsBridgeOptions,
11897) -> Result<Vec<Vec<u8>>, SidecarError> {
11898 let Some(certificates) = options.cert.as_ref() else {
11899 return Ok(Vec::new());
11900 };
11901 tls_material_entries(certificates)
11902}
11903
11904fn tls_material_entries(material: &JavascriptTlsMaterial) -> Result<Vec<Vec<u8>>, SidecarError> {
11905 match material {
11906 JavascriptTlsMaterial::Single(entry) => tls_data_value(entry).map(|value| vec![value]),
11907 JavascriptTlsMaterial::Many(entries) => entries.iter().map(tls_data_value).collect(),
11908 }
11909}
11910
11911fn tls_data_value(value: &JavascriptTlsDataValue) -> Result<Vec<u8>, SidecarError> {
11912 match value {
11913 JavascriptTlsDataValue::Buffer { data } => base64::engine::general_purpose::STANDARD
11914 .decode(data)
11915 .map_err(|error| {
11916 SidecarError::InvalidState(format!("TLS material contains invalid base64: {error}"))
11917 }),
11918 JavascriptTlsDataValue::String { data } => Ok(data.as_bytes().to_vec()),
11919 }
11920}
11921
11922fn tls_certificates_from_material(
11923 material: &JavascriptTlsMaterial,
11924) -> Result<Vec<CertificateDer<'static>>, SidecarError> {
11925 let mut certificates = Vec::new();
11926 for entry in tls_material_entries(material)? {
11927 let mut reader = std::io::BufReader::new(Cursor::new(entry.clone()));
11928 let parsed = rustls_pemfile::certs(&mut reader)
11929 .collect::<Result<Vec<_>, _>>()
11930 .map_err(sidecar_net_error)?;
11931 if parsed.is_empty() {
11932 certificates.push(CertificateDer::from(entry));
11933 } else {
11934 certificates.extend(parsed);
11935 }
11936 }
11937 if certificates.is_empty() {
11938 return Err(SidecarError::InvalidState(String::from(
11939 "TLS certificate material did not contain any certificates",
11940 )));
11941 }
11942 Ok(certificates)
11943}
11944
11945fn tls_private_key_from_material(
11946 material: &JavascriptTlsMaterial,
11947) -> Result<PrivateKeyDer<'static>, SidecarError> {
11948 for entry in tls_material_entries(material)? {
11949 let mut reader = std::io::BufReader::new(Cursor::new(entry));
11950 if let Some(key) = rustls_pemfile::private_key(&mut reader).map_err(sidecar_net_error)? {
11951 return Ok(key);
11952 }
11953 }
11954 Err(SidecarError::InvalidState(String::from(
11955 "TLS private key material did not contain a supported key",
11956 )))
11957}
11958
11959fn tls_root_store(options: &JavascriptTlsBridgeOptions) -> Result<RootCertStore, SidecarError> {
11960 let mut roots = RootCertStore::empty();
11961 if let Some(ca) = options.ca.as_ref() {
11962 for certificate in tls_certificates_from_material(ca)? {
11963 roots.add(certificate).map_err(|error| {
11964 SidecarError::InvalidState(format!("failed to add TLS CA certificate: {error}"))
11965 })?;
11966 }
11967 return Ok(roots);
11968 }
11969
11970 for certificate in rustls_native_certs::load_native_certs().certs {
11971 roots.add(certificate).map_err(|error| {
11972 SidecarError::InvalidState(format!(
11973 "failed to add native TLS certificate to root store: {error}"
11974 ))
11975 })?;
11976 }
11977 Ok(roots)
11978}
11979
11980fn build_client_tls_stream(
11981 stream: TcpStream,
11982 options: &JavascriptTlsBridgeOptions,
11983) -> Result<rustls::StreamOwned<ClientConnection, TcpStream>, SidecarError> {
11984 let config = build_client_tls_config(options)?;
11985 let server_name = options
11986 .servername
11987 .clone()
11988 .unwrap_or_else(|| String::from("localhost"));
11989 let server_name = ServerName::try_from(server_name)
11990 .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
11991 stream
11992 .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
11993 .map_err(sidecar_net_error)?;
11994 stream
11995 .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
11996 .map_err(sidecar_net_error)?;
11997 let mut tls_stream = rustls::StreamOwned::new(
11998 ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
11999 SidecarError::Execution(format!("failed to start TLS client: {error}"))
12000 })?,
12001 stream,
12002 );
12003 while tls_stream.conn.is_handshaking() {
12004 tls_stream
12005 .conn
12006 .complete_io(&mut tls_stream.sock)
12007 .map_err(sidecar_net_error)?;
12008 }
12009 tls_stream
12010 .sock
12011 .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
12012 .map_err(sidecar_net_error)?;
12013 tls_stream
12014 .sock
12015 .set_write_timeout(None)
12016 .map_err(sidecar_net_error)?;
12017 Ok(tls_stream)
12018}
12019
12020fn build_client_loopback_tls_stream(
12021 transport: crate::state::LoopbackTlsEndpoint,
12022 options: &JavascriptTlsBridgeOptions,
12023) -> Result<rustls::StreamOwned<ClientConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
12024{
12025 let config = build_client_tls_config(options)?;
12026 let server_name = options
12027 .servername
12028 .clone()
12029 .unwrap_or_else(|| String::from("localhost"));
12030 let server_name = ServerName::try_from(server_name)
12031 .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
12032 let mut tls_stream = rustls::StreamOwned::new(
12033 ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
12034 SidecarError::Execution(format!("failed to start TLS client: {error}"))
12035 })?,
12036 transport,
12037 );
12038 match tls_stream.conn.complete_io(&mut tls_stream.sock) {
12039 Ok(_) => {}
12040 Err(error)
12041 if matches!(
12042 error.kind(),
12043 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12044 ) => {}
12045 Err(error) => return Err(sidecar_net_error(error)),
12046 }
12047 Ok(tls_stream)
12048}
12049
12050fn build_client_tls_config(
12051 options: &JavascriptTlsBridgeOptions,
12052) -> Result<ClientConfig, SidecarError> {
12053 let provider = tls_provider();
12054 let builder = ClientConfig::builder_with_provider(provider.clone())
12055 .with_safe_default_protocol_versions()
12056 .map_err(|error| {
12057 SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
12058 })?;
12059
12060 let mut config = if options.reject_unauthorized == Some(false) {
12061 let verifier = Arc::new(InsecureTlsVerifier {
12062 supported_schemes: provider
12063 .signature_verification_algorithms
12064 .supported_schemes(),
12065 });
12066 builder
12067 .dangerous()
12068 .with_custom_certificate_verifier(verifier)
12069 .with_no_client_auth()
12070 } else {
12071 builder
12072 .with_root_certificates(tls_root_store(options)?)
12073 .with_no_client_auth()
12074 };
12075
12076 if let Some(protocols) = options.alpn_protocols.as_ref() {
12077 config.alpn_protocols = protocols
12078 .iter()
12079 .map(|protocol| protocol.as_bytes().to_vec())
12080 .collect();
12081 }
12082 Ok(config)
12083}
12084
12085fn build_server_tls_stream(
12086 stream: TcpStream,
12087 options: &JavascriptTlsBridgeOptions,
12088) -> Result<rustls::StreamOwned<ServerConnection, TcpStream>, SidecarError> {
12089 let config = build_server_tls_config(options)?;
12090 stream
12091 .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12092 .map_err(sidecar_net_error)?;
12093 stream
12094 .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12095 .map_err(sidecar_net_error)?;
12096 let mut tls_stream = rustls::StreamOwned::new(
12097 ServerConnection::new(Arc::new(config)).map_err(|error| {
12098 SidecarError::Execution(format!("failed to start TLS server: {error}"))
12099 })?,
12100 stream,
12101 );
12102 while tls_stream.conn.is_handshaking() {
12103 tls_stream
12104 .conn
12105 .complete_io(&mut tls_stream.sock)
12106 .map_err(sidecar_net_error)?;
12107 }
12108 tls_stream
12109 .sock
12110 .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
12111 .map_err(sidecar_net_error)?;
12112 tls_stream
12113 .sock
12114 .set_write_timeout(None)
12115 .map_err(sidecar_net_error)?;
12116 Ok(tls_stream)
12117}
12118
12119fn build_server_loopback_tls_stream(
12120 transport: crate::state::LoopbackTlsEndpoint,
12121 options: &JavascriptTlsBridgeOptions,
12122) -> Result<rustls::StreamOwned<ServerConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
12123{
12124 let config = build_server_tls_config(options)?;
12125 Ok(rustls::StreamOwned::new(
12126 ServerConnection::new(Arc::new(config)).map_err(|error| {
12127 SidecarError::Execution(format!("failed to start TLS server: {error}"))
12128 })?,
12129 transport,
12130 ))
12131}
12132
12133fn build_server_tls_config(
12134 options: &JavascriptTlsBridgeOptions,
12135) -> Result<ServerConfig, SidecarError> {
12136 let certificates = tls_certificates_from_material(options.cert.as_ref().ok_or_else(|| {
12137 SidecarError::InvalidState(String::from("TLS server upgrade requires a certificate"))
12138 })?)?;
12139 let key = tls_private_key_from_material(options.key.as_ref().ok_or_else(|| {
12140 SidecarError::InvalidState(String::from("TLS server upgrade requires a private key"))
12141 })?)?;
12142
12143 let mut config = ServerConfig::builder_with_provider(tls_provider())
12144 .with_safe_default_protocol_versions()
12145 .map_err(|error| {
12146 SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
12147 })?
12148 .with_no_client_auth()
12149 .with_single_cert(certificates, key)
12150 .map_err(|error| {
12151 SidecarError::InvalidState(format!("invalid TLS server config: {error}"))
12152 })?;
12153
12154 if let Some(protocols) = options.alpn_protocols.as_ref() {
12155 config.alpn_protocols = protocols
12156 .iter()
12157 .map(|protocol| protocol.as_bytes().to_vec())
12158 .collect();
12159 }
12160 Ok(config)
12161}
12162
12163fn tls_protocol_name(version: rustls::ProtocolVersion) -> String {
12164 match version {
12165 rustls::ProtocolVersion::TLSv1_2 => String::from("TLSv1.2"),
12166 rustls::ProtocolVersion::TLSv1_3 => String::from("TLSv1.3"),
12167 other => other
12168 .as_str()
12169 .map(str::to_owned)
12170 .unwrap_or_else(|| format!("{other:?}")),
12171 }
12172}
12173
12174fn tls_cipher_bridge_value(suite: rustls::SupportedCipherSuite) -> Value {
12175 tls_bridge_object(vec![
12176 (
12177 "name",
12178 suite
12179 .suite()
12180 .as_str()
12181 .map(|value| Value::String(value.to_owned()))
12182 .unwrap_or(Value::Null),
12183 ),
12184 (
12185 "standardName",
12186 suite
12187 .suite()
12188 .as_str()
12189 .map(|value| Value::String(value.to_owned()))
12190 .unwrap_or(Value::Null),
12191 ),
12192 (
12193 "version",
12194 Value::String(if suite.tls13().is_some() {
12195 String::from("TLSv1.3")
12196 } else {
12197 String::from("TLSv1.2")
12198 }),
12199 ),
12200 ])
12201}
12202
12203fn tls_certificate_bridge_value(certificate: &[u8], detailed: bool) -> Value {
12204 let mut fields = vec![("raw", tls_bridge_buffer_value(certificate))];
12205 if detailed {
12206 fields.push(("issuerCertificate", tls_bridge_undefined_value()));
12207 }
12208 tls_bridge_object(fields)
12209}
12210
12211fn tls_bridge_buffer_value(bytes: &[u8]) -> Value {
12212 json!({
12213 "type": "buffer",
12214 "data": base64::engine::general_purpose::STANDARD.encode(bytes),
12215 })
12216}
12217
12218fn tls_bridge_object(entries: Vec<(&str, Value)>) -> Value {
12219 let value = entries
12220 .into_iter()
12221 .map(|(key, value)| (key.to_owned(), value))
12222 .collect::<serde_json::Map<String, Value>>();
12223 json!({
12224 "type": "object",
12225 "id": 1,
12226 "value": value,
12227 })
12228}
12229
12230fn tls_bridge_undefined_value() -> Value {
12231 json!({
12232 "type": "undefined",
12233 })
12234}
12235
12236fn spawn_tcp_socket_reader(
12237 stream: TcpStream,
12238 sender: Sender<JavascriptTcpSocketEvent>,
12239 tls_mode: Arc<AtomicBool>,
12240 saw_local_shutdown: Arc<AtomicBool>,
12241 saw_remote_end: Arc<AtomicBool>,
12242 close_notified: Arc<AtomicBool>,
12243) {
12244 thread::spawn(move || {
12245 let mut stream = stream;
12246 let mut buffer = vec![0_u8; 64 * 1024];
12247 loop {
12248 if tls_mode.load(Ordering::SeqCst) {
12249 break;
12250 }
12251 match stream.read(&mut buffer) {
12252 Ok(0) => {
12253 saw_remote_end.store(true, Ordering::SeqCst);
12254 let _ = sender.send(JavascriptTcpSocketEvent::End);
12255 if saw_local_shutdown.load(Ordering::SeqCst)
12256 && !close_notified.swap(true, Ordering::SeqCst)
12257 {
12258 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12259 }
12260 break;
12261 }
12262 Ok(bytes_read) => {
12263 if sender
12264 .send(JavascriptTcpSocketEvent::Data(
12265 buffer[..bytes_read].to_vec(),
12266 ))
12267 .is_err()
12268 {
12269 break;
12270 }
12271 }
12272 Err(error)
12273 if matches!(
12274 error.kind(),
12275 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12276 ) =>
12277 {
12278 continue;
12279 }
12280 Err(error) => {
12281 let code = io_error_code(&error);
12282 let _ = sender.send(JavascriptTcpSocketEvent::Error {
12283 code,
12284 message: error.to_string(),
12285 });
12286 if !close_notified.swap(true, Ordering::SeqCst) {
12287 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12288 }
12289 break;
12290 }
12291 }
12292 }
12293 });
12294}
12295
12296fn spawn_tls_socket_reader(
12297 tls_stream: Arc<Mutex<Option<ActiveTlsStream>>>,
12298 sender: Sender<JavascriptTcpSocketEvent>,
12299 saw_local_shutdown: Arc<AtomicBool>,
12300 saw_remote_end: Arc<AtomicBool>,
12301 close_notified: Arc<AtomicBool>,
12302) {
12303 thread::spawn(move || {
12304 let mut buffer = vec![0_u8; 64 * 1024];
12305 loop {
12306 let read_result = {
12307 let mut guard = match tls_stream.lock() {
12308 Ok(guard) => guard,
12309 Err(_) => return,
12310 };
12311 let Some(stream) = guard.as_mut() else {
12312 return;
12313 };
12314 stream.read(&mut buffer)
12315 };
12316
12317 match read_result {
12318 Ok(0) => {
12319 saw_remote_end.store(true, Ordering::SeqCst);
12320 let _ = sender.send(JavascriptTcpSocketEvent::End);
12321 if saw_local_shutdown.load(Ordering::SeqCst)
12322 && !close_notified.swap(true, Ordering::SeqCst)
12323 {
12324 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12325 }
12326 break;
12327 }
12328 Ok(bytes_read) => {
12329 if sender
12330 .send(JavascriptTcpSocketEvent::Data(
12331 buffer[..bytes_read].to_vec(),
12332 ))
12333 .is_err()
12334 {
12335 break;
12336 }
12337 }
12338 Err(error)
12339 if matches!(
12340 error.kind(),
12341 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12342 ) =>
12343 {
12344 std::thread::sleep(Duration::from_millis(1));
12347 continue;
12348 }
12349 Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
12350 saw_remote_end.store(true, Ordering::SeqCst);
12351 let _ = sender.send(JavascriptTcpSocketEvent::End);
12352 if saw_local_shutdown.load(Ordering::SeqCst)
12353 && !close_notified.swap(true, Ordering::SeqCst)
12354 {
12355 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12356 }
12357 break;
12358 }
12359 Err(error) => {
12360 let code = io_error_code(&error);
12361 let _ = sender.send(JavascriptTcpSocketEvent::Error {
12362 code,
12363 message: error.to_string(),
12364 });
12365 if !close_notified.swap(true, Ordering::SeqCst) {
12366 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12367 }
12368 break;
12369 }
12370 }
12371 }
12372 });
12373}
12374
12375fn spawn_unix_socket_reader(
12376 stream: UnixStream,
12377 sender: Sender<JavascriptTcpSocketEvent>,
12378 saw_local_shutdown: Arc<AtomicBool>,
12379 saw_remote_end: Arc<AtomicBool>,
12380 close_notified: Arc<AtomicBool>,
12381) {
12382 thread::spawn(move || {
12383 let mut stream = stream;
12384 let mut buffer = vec![0_u8; 64 * 1024];
12385 loop {
12386 match stream.read(&mut buffer) {
12387 Ok(0) => {
12388 saw_remote_end.store(true, Ordering::SeqCst);
12389 let _ = sender.send(JavascriptTcpSocketEvent::End);
12390 if saw_local_shutdown.load(Ordering::SeqCst)
12391 && !close_notified.swap(true, Ordering::SeqCst)
12392 {
12393 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12394 }
12395 break;
12396 }
12397 Ok(bytes_read) => {
12398 if sender
12399 .send(JavascriptTcpSocketEvent::Data(
12400 buffer[..bytes_read].to_vec(),
12401 ))
12402 .is_err()
12403 {
12404 break;
12405 }
12406 }
12407 Err(error) => {
12408 let code = io_error_code(&error);
12409 let _ = sender.send(JavascriptTcpSocketEvent::Error {
12410 code,
12411 message: error.to_string(),
12412 });
12413 if !close_notified.swap(true, Ordering::SeqCst) {
12414 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12415 }
12416 break;
12417 }
12418 }
12419 }
12420 });
12421}
12422
12423fn terminate_child_process_tree(kernel: &mut SidecarKernel, process: &mut ActiveProcess) {
12424 let sqlite_database_ids = process.sqlite_databases.keys().copied().collect::<Vec<_>>();
12425 for database_id in sqlite_database_ids {
12426 let _ = close_sqlite_database(kernel, process, database_id);
12427 }
12428 process.sqlite_statements.clear();
12429 process.http_servers.clear();
12430 process.pending_http_requests.clear();
12431 if let Ok(mut http2) = process.http2.shared.lock() {
12432 let sessions = http2.sessions.values().cloned().collect::<Vec<_>>();
12433 http2.server_events.clear();
12434 http2.session_events.clear();
12435 http2.streams.clear();
12436 http2.servers.clear();
12437 http2.sessions.clear();
12438 drop(http2);
12439 for session in sessions {
12440 let (respond_to, _rx) = mpsc::channel();
12441 let _ = session.command_tx.send(Http2SessionCommand::Close {
12442 abrupt: true,
12443 respond_to,
12444 });
12445 }
12446 }
12447
12448 let listener_ids = process.tcp_listeners.keys().cloned().collect::<Vec<_>>();
12449 for listener_id in listener_ids {
12450 if let Some(listener) = process.tcp_listeners.remove(&listener_id) {
12451 let _ = listener.close(kernel, process.kernel_pid);
12452 }
12453 }
12454
12455 let sockets = process.tcp_sockets.keys().cloned().collect::<Vec<_>>();
12456 for socket_id in sockets {
12457 if let Some(socket) = process.tcp_sockets.remove(&socket_id) {
12458 let _ = socket.close(kernel, process.kernel_pid);
12459 }
12460 }
12461
12462 let unix_listener_ids = process.unix_listeners.keys().cloned().collect::<Vec<_>>();
12463 for listener_id in unix_listener_ids {
12464 if let Some(listener) = process.unix_listeners.remove(&listener_id) {
12465 let _ = listener.close();
12466 }
12467 }
12468
12469 let unix_sockets = process.unix_sockets.keys().cloned().collect::<Vec<_>>();
12470 for socket_id in unix_sockets {
12471 if let Some(socket) = process.unix_sockets.remove(&socket_id) {
12472 let _ = socket.close();
12473 }
12474 }
12475
12476 let udp_socket_ids = process.udp_sockets.keys().cloned().collect::<Vec<_>>();
12477 for socket_id in udp_socket_ids {
12478 if let Some(mut socket) = process.udp_sockets.remove(&socket_id) {
12479 socket.close(kernel, process.kernel_pid);
12480 }
12481 }
12482
12483 let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
12484 for child_id in child_ids {
12485 let Some(mut child) = process.child_processes.remove(&child_id) else {
12486 continue;
12487 };
12488 terminate_child_process_tree(kernel, &mut child);
12489 let _ = kernel.kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, SIGTERM);
12490 let _ = signal_runtime_process(child.execution.child_pid(), SIGTERM);
12491 child.kernel_handle.finish(0);
12492 let _ = kernel.wait_and_reap(child.kernel_pid);
12493 }
12494}
12495
12496fn service_javascript_sqlite_sync_rpc(
12497 kernel: &mut SidecarKernel,
12498 process: &mut ActiveProcess,
12499 request: &JavascriptSyncRpcRequest,
12500) -> Result<Value, SidecarError> {
12501 match request.method.as_str() {
12502 "sqlite.constants" => Ok(json!({})),
12503 "sqlite.open" => sqlite_open_database(kernel, process, request),
12504 "sqlite.close" => {
12505 let database_id =
12506 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.close database id")?;
12507 close_sqlite_database(kernel, process, database_id)?;
12508 Ok(Value::Null)
12509 }
12510 "sqlite.exec" => sqlite_exec_database(kernel, process, request),
12511 "sqlite.query" => sqlite_query_database(process, request),
12512 "sqlite.prepare" => sqlite_prepare_statement(process, request),
12513 "sqlite.location" => {
12514 let database_id =
12515 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.location database id")?;
12516 let database = sqlite_database(process, database_id)?;
12517 Ok(database
12518 .vm_path
12519 .as_ref()
12520 .map(|path| Value::String(path.clone()))
12521 .unwrap_or(Value::Null))
12522 }
12523 "sqlite.checkpoint" => {
12524 let database_id =
12525 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.checkpoint database id")?;
12526 let kernel_pid = process.kernel_pid;
12527 let database = sqlite_database_mut(process, database_id)?;
12528 sqlite_sync_database(kernel, kernel_pid, database)?;
12529 Ok(Value::Null)
12530 }
12531 "sqlite.statement.run" => sqlite_run_statement(kernel, process, request),
12532 "sqlite.statement.get" => sqlite_get_statement(process, request),
12533 "sqlite.statement.all" | "sqlite.statement.iterate" => {
12534 sqlite_all_statement(process, request)
12535 }
12536 "sqlite.statement.columns" => sqlite_statement_columns(process, request),
12537 "sqlite.statement.setReturnArrays" => {
12538 let statement_id = javascript_sync_rpc_arg_u64(
12539 &request.args,
12540 0,
12541 "sqlite.statement.setReturnArrays statement id",
12542 )?;
12543 let enabled = javascript_sync_rpc_arg_bool(
12544 &request.args,
12545 1,
12546 "sqlite.statement.setReturnArrays enabled",
12547 )?;
12548 sqlite_statement_mut(process, statement_id)?.return_arrays = enabled;
12549 Ok(Value::Null)
12550 }
12551 "sqlite.statement.setReadBigInts" => {
12552 let statement_id = javascript_sync_rpc_arg_u64(
12553 &request.args,
12554 0,
12555 "sqlite.statement.setReadBigInts statement id",
12556 )?;
12557 let enabled = javascript_sync_rpc_arg_bool(
12558 &request.args,
12559 1,
12560 "sqlite.statement.setReadBigInts enabled",
12561 )?;
12562 sqlite_statement_mut(process, statement_id)?.read_bigints = enabled;
12563 Ok(Value::Null)
12564 }
12565 "sqlite.statement.setAllowBareNamedParameters" => {
12566 let statement_id = javascript_sync_rpc_arg_u64(
12567 &request.args,
12568 0,
12569 "sqlite.statement.setAllowBareNamedParameters statement id",
12570 )?;
12571 let enabled = javascript_sync_rpc_arg_bool(
12572 &request.args,
12573 1,
12574 "sqlite.statement.setAllowBareNamedParameters enabled",
12575 )?;
12576 sqlite_statement_mut(process, statement_id)?.allow_bare_named_parameters = enabled;
12577 Ok(Value::Null)
12578 }
12579 "sqlite.statement.setAllowUnknownNamedParameters" => {
12580 let statement_id = javascript_sync_rpc_arg_u64(
12581 &request.args,
12582 0,
12583 "sqlite.statement.setAllowUnknownNamedParameters statement id",
12584 )?;
12585 let enabled = javascript_sync_rpc_arg_bool(
12586 &request.args,
12587 1,
12588 "sqlite.statement.setAllowUnknownNamedParameters enabled",
12589 )?;
12590 sqlite_statement_mut(process, statement_id)?.allow_unknown_named_parameters = enabled;
12591 Ok(Value::Null)
12592 }
12593 "sqlite.statement.finalize" => {
12594 let statement_id = javascript_sync_rpc_arg_u64(
12595 &request.args,
12596 0,
12597 "sqlite.statement.finalize statement id",
12598 )?;
12599 process
12600 .sqlite_statements
12601 .remove(&statement_id)
12602 .ok_or_else(|| {
12603 SidecarError::InvalidState(format!(
12604 "sqlite statement handle not found: {statement_id}"
12605 ))
12606 })?;
12607 Ok(Value::Null)
12608 }
12609 other => Err(SidecarError::InvalidState(format!(
12610 "unsupported JavaScript sqlite sync RPC method {other}"
12611 ))),
12612 }
12613}
12614
12615fn sqlite_open_database(
12616 kernel: &mut SidecarKernel,
12617 process: &mut ActiveProcess,
12618 request: &JavascriptSyncRpcRequest,
12619) -> Result<Value, SidecarError> {
12620 ensure_per_process_state_handle_capacity(process.sqlite_databases.len(), "sqlite database")?;
12621 let path = request.args.first().and_then(Value::as_str);
12622 let vm_path = path.filter(|value| !value.is_empty() && *value != ":memory:");
12623 let options = request.args.get(1);
12624 let read_only = sqlite_option_bool(options, "readOnly").unwrap_or(false);
12625 let create = sqlite_option_bool(options, "create").unwrap_or(!read_only);
12626 let timeout_ms = sqlite_option_u64(options, "timeout");
12627
12628 process.next_sqlite_database_id += 1;
12629 let database_id = process.next_sqlite_database_id;
12630
12631 let host_path = if vm_path.is_some() {
12632 Some(
12633 std::env::temp_dir()
12634 .join(format!(
12635 "secure-exec-sidecar-sqlite-{}-{database_id}",
12636 process.kernel_pid
12637 ))
12638 .join("database.sqlite"),
12639 )
12640 } else {
12641 None
12642 };
12643
12644 if let Some(host_path) = host_path.as_ref() {
12645 if let Some(parent) = host_path.parent() {
12646 fs::create_dir_all(parent).map_err(|error| {
12647 SidecarError::Io(format!(
12648 "failed to prepare sqlite temp directory {}: {error}",
12649 parent.display()
12650 ))
12651 })?;
12652 }
12653 }
12654
12655 if let (Some(vm_path), Some(host_path)) = (vm_path, host_path.as_ref()) {
12656 if kernel
12657 .exists_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
12658 .map_err(kernel_error)?
12659 {
12660 let contents = kernel
12661 .read_file_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
12662 .map_err(kernel_error)?;
12663 fs::write(host_path, contents).map_err(|error| {
12664 SidecarError::Io(format!(
12665 "failed to materialize sqlite database {}: {error}",
12666 host_path.display()
12667 ))
12668 })?;
12669 } else if read_only && !create {
12670 return Err(SidecarError::InvalidState(format!(
12671 "sqlite database does not exist: {vm_path}"
12672 )));
12673 }
12674 }
12675
12676 let target = host_path
12677 .as_ref()
12678 .map(|path| path.to_string_lossy().into_owned())
12679 .unwrap_or_else(|| String::from(":memory:"));
12680 let mut flags = if read_only {
12681 SqliteOpenFlags::SQLITE_OPEN_READ_ONLY
12682 } else {
12683 SqliteOpenFlags::SQLITE_OPEN_READ_WRITE
12684 };
12685 if create && !read_only {
12686 flags |= SqliteOpenFlags::SQLITE_OPEN_CREATE;
12687 }
12688
12689 let connection = SqliteConnection::open_with_flags(&target, flags).map_err(|error| {
12690 SidecarError::InvalidState(format!(
12691 "sqlite database open failed for {}: {error}",
12692 vm_path.unwrap_or(":memory:")
12693 ))
12694 })?;
12695 if let Some(timeout_ms) = timeout_ms {
12696 connection
12697 .busy_timeout(Duration::from_millis(timeout_ms))
12698 .map_err(sqlite_error)?;
12699 }
12700 if host_path.is_some() && !read_only {
12701 let _ = connection.pragma_update(None, "journal_mode", "WAL");
12702 }
12703
12704 process.sqlite_databases.insert(
12705 database_id,
12706 ActiveSqliteDatabase {
12707 connection,
12708 host_path,
12709 vm_path: vm_path.map(String::from),
12710 dirty: false,
12711 transaction_depth: 0,
12712 read_only,
12713 },
12714 );
12715
12716 Ok(json!(database_id))
12717}
12718
12719fn sqlite_exec_database(
12720 kernel: &mut SidecarKernel,
12721 process: &mut ActiveProcess,
12722 request: &JavascriptSyncRpcRequest,
12723) -> Result<Value, SidecarError> {
12724 let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.exec database id")?;
12725 let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.exec sql")?;
12726 let kernel_pid = process.kernel_pid;
12727 let database = sqlite_database_mut(process, database_id)?;
12728 let before = database.connection.total_changes();
12729 database
12730 .connection
12731 .execute_batch(sql)
12732 .map_err(sqlite_error)?;
12733 mark_sqlite_mutation(database, sql);
12734 sqlite_sync_database(kernel, kernel_pid, database)?;
12735 Ok(json!(database
12736 .connection
12737 .total_changes()
12738 .saturating_sub(before)))
12739}
12740
12741fn sqlite_query_database(
12742 process: &mut ActiveProcess,
12743 request: &JavascriptSyncRpcRequest,
12744) -> Result<Value, SidecarError> {
12745 let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.query database id")?;
12746 let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.query sql")?;
12747 let params = request.args.get(2);
12748 let options = request.args.get(3);
12749 let return_arrays = sqlite_option_bool(options, "returnArrays").unwrap_or(false);
12750 let read_bigints = sqlite_option_bool(options, "readBigInts").unwrap_or(false);
12751 let database = sqlite_database_mut(process, database_id)?;
12752 sqlite_query_rows(
12753 &mut database.connection,
12754 sql,
12755 params,
12756 return_arrays,
12757 read_bigints,
12758 true,
12759 false,
12760 )
12761}
12762
12763fn sqlite_prepare_statement(
12764 process: &mut ActiveProcess,
12765 request: &JavascriptSyncRpcRequest,
12766) -> Result<Value, SidecarError> {
12767 ensure_per_process_state_handle_capacity(process.sqlite_statements.len(), "sqlite statement")?;
12768 let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.prepare database id")?;
12769 let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.prepare sql")?;
12770 let _ = sqlite_database(process, database_id)?;
12771 process.next_sqlite_statement_id += 1;
12772 let statement_id = process.next_sqlite_statement_id;
12773 process.sqlite_statements.insert(
12774 statement_id,
12775 ActiveSqliteStatement {
12776 database_id,
12777 sql: sql.to_owned(),
12778 return_arrays: false,
12779 read_bigints: false,
12780 allow_bare_named_parameters: false,
12781 allow_unknown_named_parameters: false,
12782 },
12783 );
12784 Ok(json!(statement_id))
12785}
12786
12787fn sqlite_run_statement(
12788 kernel: &mut SidecarKernel,
12789 process: &mut ActiveProcess,
12790 request: &JavascriptSyncRpcRequest,
12791) -> Result<Value, SidecarError> {
12792 let statement_id =
12793 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.run statement id")?;
12794 let params = request.args.get(1);
12795 let statement_state = sqlite_statement(process, statement_id)?.clone();
12796 let kernel_pid = process.kernel_pid;
12797 let database = sqlite_database_mut(process, statement_state.database_id)?;
12798 let before = database.connection.total_changes();
12799 {
12800 let mut statement = database
12801 .connection
12802 .prepare(&statement_state.sql)
12803 .map_err(sqlite_error)?;
12804 bind_sqlite_parameters(
12805 &mut statement,
12806 params,
12807 statement_state.allow_bare_named_parameters,
12808 statement_state.allow_unknown_named_parameters,
12809 )?;
12810 statement.raw_execute().map_err(sqlite_error)?;
12811 }
12812 let changes = database.connection.total_changes().saturating_sub(before);
12813 let last_insert_rowid = database.connection.last_insert_rowid();
12814 mark_sqlite_mutation(database, &statement_state.sql);
12815 sqlite_sync_database(kernel, kernel_pid, database)?;
12816 let result = json!({
12817 "changes": changes,
12818 "lastInsertRowid": encode_sqlite_integer(last_insert_rowid, true),
12819 });
12820 Ok(result)
12821}
12822
12823fn sqlite_get_statement(
12824 process: &mut ActiveProcess,
12825 request: &JavascriptSyncRpcRequest,
12826) -> Result<Value, SidecarError> {
12827 let statement_id =
12828 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.get statement id")?;
12829 let params = request.args.get(1);
12830 let statement_state = sqlite_statement(process, statement_id)?.clone();
12831 let database = sqlite_database_mut(process, statement_state.database_id)?;
12832 let rows = sqlite_query_rows(
12833 &mut database.connection,
12834 &statement_state.sql,
12835 params,
12836 statement_state.return_arrays,
12837 statement_state.read_bigints,
12838 statement_state.allow_bare_named_parameters,
12839 statement_state.allow_unknown_named_parameters,
12840 )?;
12841 Ok(rows
12842 .as_array()
12843 .and_then(|rows| rows.first().cloned())
12844 .unwrap_or(Value::Null))
12845}
12846
12847fn sqlite_all_statement(
12848 process: &mut ActiveProcess,
12849 request: &JavascriptSyncRpcRequest,
12850) -> Result<Value, SidecarError> {
12851 let statement_id =
12852 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.all statement id")?;
12853 let params = request.args.get(1);
12854 let statement_state = sqlite_statement(process, statement_id)?.clone();
12855 let database = sqlite_database_mut(process, statement_state.database_id)?;
12856 sqlite_query_rows(
12857 &mut database.connection,
12858 &statement_state.sql,
12859 params,
12860 statement_state.return_arrays,
12861 statement_state.read_bigints,
12862 statement_state.allow_bare_named_parameters,
12863 statement_state.allow_unknown_named_parameters,
12864 )
12865}
12866
12867fn sqlite_statement_columns(
12868 process: &mut ActiveProcess,
12869 request: &JavascriptSyncRpcRequest,
12870) -> Result<Value, SidecarError> {
12871 let statement_id =
12872 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.columns statement id")?;
12873 let statement_state = sqlite_statement(process, statement_id)?.clone();
12874 let database = sqlite_database_mut(process, statement_state.database_id)?;
12875 let statement = database
12876 .connection
12877 .prepare(&statement_state.sql)
12878 .map_err(sqlite_error)?;
12879 Ok(Value::Array(
12880 statement
12881 .column_names()
12882 .iter()
12883 .map(|name| json!({ "name": name }))
12884 .collect(),
12885 ))
12886}
12887
12888fn sqlite_query_rows(
12889 connection: &mut SqliteConnection,
12890 sql: &str,
12891 params: Option<&Value>,
12892 return_arrays: bool,
12893 read_bigints: bool,
12894 allow_bare_named_parameters: bool,
12895 allow_unknown_named_parameters: bool,
12896) -> Result<Value, SidecarError> {
12897 let mut statement = connection.prepare(sql).map_err(sqlite_error)?;
12898 let column_names = statement
12899 .column_names()
12900 .iter()
12901 .map(|name| (*name).to_owned())
12902 .collect::<Vec<_>>();
12903 let column_count = statement.column_count();
12904 bind_sqlite_parameters(
12905 &mut statement,
12906 params,
12907 allow_bare_named_parameters,
12908 allow_unknown_named_parameters,
12909 )?;
12910 let mut rows = statement.raw_query();
12911 let mut encoded_rows = Vec::new();
12912 while let Some(row) = rows.next().map_err(sqlite_error)? {
12913 encoded_rows.push(encode_sqlite_row(
12914 row,
12915 &column_names,
12916 column_count,
12917 return_arrays,
12918 read_bigints,
12919 )?);
12920 }
12921 Ok(Value::Array(encoded_rows))
12922}
12923
12924fn encode_sqlite_row(
12925 row: &rusqlite::Row<'_>,
12926 column_names: &[String],
12927 column_count: usize,
12928 return_arrays: bool,
12929 read_bigints: bool,
12930) -> Result<Value, SidecarError> {
12931 if return_arrays {
12932 let mut values = Vec::with_capacity(column_count);
12933 for index in 0..column_count {
12934 values.push(encode_sqlite_value_ref(
12935 row.get_ref(index).map_err(sqlite_error)?,
12936 read_bigints,
12937 )?);
12938 }
12939 return Ok(Value::Array(values));
12940 }
12941
12942 let mut object = Map::with_capacity(column_count);
12943 for (index, name) in column_names.iter().enumerate() {
12944 object.insert(
12945 name.clone(),
12946 encode_sqlite_value_ref(row.get_ref(index).map_err(sqlite_error)?, read_bigints)?,
12947 );
12948 }
12949 Ok(Value::Object(object))
12950}
12951
12952fn encode_sqlite_value_ref(
12953 value: SqliteValueRef<'_>,
12954 read_bigints: bool,
12955) -> Result<Value, SidecarError> {
12956 Ok(match value {
12957 SqliteValueRef::Null => Value::Null,
12958 SqliteValueRef::Integer(number) => encode_sqlite_integer(number, read_bigints),
12959 SqliteValueRef::Real(number) => json!(number),
12960 SqliteValueRef::Text(text) => Value::String(String::from_utf8_lossy(text).into_owned()),
12961 SqliteValueRef::Blob(bytes) => json!({
12962 "__agentosSqliteType": "uint8array",
12963 "value": base64::engine::general_purpose::STANDARD.encode(bytes),
12964 }),
12965 })
12966}
12967
12968fn encode_sqlite_integer(number: i64, read_bigints: bool) -> Value {
12969 if read_bigints || number.abs() > SQLITE_JS_SAFE_INTEGER_MAX {
12970 json!({
12971 "__agentosSqliteType": "bigint",
12972 "value": number.to_string(),
12973 })
12974 } else {
12975 json!(number)
12976 }
12977}
12978
12979fn bind_sqlite_parameters(
12980 statement: &mut SqliteStatement<'_>,
12981 params: Option<&Value>,
12982 allow_bare_named_parameters: bool,
12983 allow_unknown_named_parameters: bool,
12984) -> Result<(), SidecarError> {
12985 let Some(params) = params else {
12986 return Ok(());
12987 };
12988 match params {
12989 Value::Null => Ok(()),
12990 Value::Array(values) => {
12991 for (index, value) in values.iter().enumerate() {
12992 statement
12993 .raw_bind_parameter(index + 1, decode_sqlite_parameter(value)?)
12994 .map_err(sqlite_error)?;
12995 }
12996 Ok(())
12997 }
12998 Value::Object(map)
12999 if map
13000 .get("__agentosSqliteType")
13001 .and_then(Value::as_str)
13002 .is_none() =>
13003 {
13004 for (key, value) in map {
13005 let index =
13006 resolve_sqlite_parameter_index(statement, key, allow_bare_named_parameters)?;
13007 let Some(index) = index else {
13008 if allow_unknown_named_parameters {
13009 continue;
13010 }
13011 return Err(SidecarError::InvalidState(format!(
13012 "sqlite named parameter not found: {key}"
13013 )));
13014 };
13015 statement
13016 .raw_bind_parameter(index, decode_sqlite_parameter(value)?)
13017 .map_err(sqlite_error)?;
13018 }
13019 Ok(())
13020 }
13021 other => statement
13022 .raw_bind_parameter(1, decode_sqlite_parameter(other)?)
13023 .map_err(sqlite_error),
13024 }
13025}
13026
13027fn resolve_sqlite_parameter_index(
13028 statement: &mut SqliteStatement<'_>,
13029 key: &str,
13030 allow_bare_named_parameters: bool,
13031) -> Result<Option<usize>, SidecarError> {
13032 let mut candidates = vec![key.to_owned()];
13033 if allow_bare_named_parameters
13034 && !key.starts_with(':')
13035 && !key.starts_with('@')
13036 && !key.starts_with('$')
13037 {
13038 candidates.push(format!(":{key}"));
13039 candidates.push(format!("@{key}"));
13040 candidates.push(format!("${key}"));
13041 }
13042 for candidate in candidates {
13043 if let Some(index) = statement
13044 .parameter_index(&candidate)
13045 .map_err(sqlite_error)?
13046 {
13047 return Ok(Some(index));
13048 }
13049 }
13050 Ok(None)
13051}
13052
13053fn decode_sqlite_parameter(value: &Value) -> Result<rusqlite::types::Value, SidecarError> {
13054 Ok(match value {
13055 Value::Null => rusqlite::types::Value::Null,
13056 Value::Bool(value) => rusqlite::types::Value::Integer(i64::from(*value)),
13057 Value::Number(value) => match (value.as_i64(), value.as_f64()) {
13058 (Some(integer), _) => rusqlite::types::Value::Integer(integer),
13059 (_, Some(real)) => rusqlite::types::Value::Real(real),
13060 _ => {
13061 return Err(SidecarError::InvalidState(String::from(
13062 "sqlite parameter number is not representable",
13063 )));
13064 }
13065 },
13066 Value::String(value) => rusqlite::types::Value::Text(value.clone()),
13067 Value::Array(_) => {
13068 return Err(SidecarError::InvalidState(String::from(
13069 "sqlite parameters do not support nested arrays",
13070 )));
13071 }
13072 Value::Object(map) => match map.get("__agentosSqliteType").and_then(Value::as_str) {
13073 Some("bigint") => rusqlite::types::Value::Integer(
13074 map.get("value")
13075 .and_then(Value::as_str)
13076 .ok_or_else(|| {
13077 SidecarError::InvalidState(String::from(
13078 "sqlite bigint parameter missing string value",
13079 ))
13080 })?
13081 .parse::<i64>()
13082 .map_err(|error| {
13083 SidecarError::InvalidState(format!(
13084 "sqlite bigint parameter is not a signed 64-bit integer: {error}"
13085 ))
13086 })?,
13087 ),
13088 Some("uint8array") => rusqlite::types::Value::Blob(
13089 base64::engine::general_purpose::STANDARD
13090 .decode(map.get("value").and_then(Value::as_str).ok_or_else(|| {
13091 SidecarError::InvalidState(String::from(
13092 "sqlite blob parameter missing base64 value",
13093 ))
13094 })?)
13095 .map_err(|error| {
13096 SidecarError::InvalidState(format!(
13097 "sqlite blob parameter contains invalid base64: {error}"
13098 ))
13099 })?,
13100 ),
13101 Some(other) => {
13102 return Err(SidecarError::InvalidState(format!(
13103 "unsupported sqlite tagged parameter type {other}"
13104 )));
13105 }
13106 None => {
13107 return Err(SidecarError::InvalidState(String::from(
13108 "sqlite named parameter objects must be passed as the top-level params object",
13109 )));
13110 }
13111 },
13112 })
13113}
13114
13115fn close_sqlite_database(
13116 kernel: &mut SidecarKernel,
13117 process: &mut ActiveProcess,
13118 database_id: u64,
13119) -> Result<(), SidecarError> {
13120 let mut database = process
13121 .sqlite_databases
13122 .remove(&database_id)
13123 .ok_or_else(|| {
13124 SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13125 })?;
13126 process
13127 .sqlite_statements
13128 .retain(|_, statement| statement.database_id != database_id);
13129 sqlite_sync_database(kernel, process.kernel_pid, &mut database)?;
13130 let host_path = database.host_path.clone();
13131 drop(database);
13132 cleanup_sqlite_host_artifacts(host_path.as_deref())?;
13133 Ok(())
13134}
13135
13136fn ensure_per_process_state_handle_capacity(len: usize, label: &str) -> Result<(), SidecarError> {
13137 if len >= MAX_PER_PROCESS_STATE_HANDLES {
13138 return Err(SidecarError::InvalidState(format!(
13139 "{label} handle limit exceeded: limit is {MAX_PER_PROCESS_STATE_HANDLES}"
13140 )));
13141 }
13142 Ok(())
13143}
13144
13145fn sqlite_sync_database(
13146 kernel: &mut SidecarKernel,
13147 kernel_pid: u32,
13148 database: &mut ActiveSqliteDatabase,
13149) -> Result<(), SidecarError> {
13150 if !database.dirty
13151 || database.transaction_depth > 0
13152 || database.read_only
13153 || database.host_path.is_none()
13154 || database.vm_path.is_none()
13155 {
13156 return Ok(());
13157 }
13158
13159 let _ = database
13160 .connection
13161 .execute_batch("PRAGMA wal_checkpoint(TRUNCATE)");
13162 let host_path = database.host_path.as_ref().expect("sqlite host path");
13163 if !host_path.exists() {
13164 return Ok(());
13165 }
13166 ensure_vm_parent_dir(
13167 kernel,
13168 kernel_pid,
13169 database.vm_path.as_deref().expect("sqlite vm path"),
13170 )?;
13171 let contents = fs::read(host_path).map_err(|error| {
13172 SidecarError::Io(format!(
13173 "failed to read sqlite temp database {}: {error}",
13174 host_path.display()
13175 ))
13176 })?;
13177 kernel
13178 .write_file_for_process(
13179 EXECUTION_DRIVER_NAME,
13180 kernel_pid,
13181 database.vm_path.as_deref().expect("sqlite vm path"),
13182 contents,
13183 None,
13184 )
13185 .map_err(kernel_error)?;
13186 database.dirty = false;
13187 Ok(())
13188}
13189
13190fn cleanup_sqlite_host_artifacts(host_path: Option<&Path>) -> Result<(), SidecarError> {
13191 let Some(host_path) = host_path else {
13192 return Ok(());
13193 };
13194 let parent = host_path.parent().map(PathBuf::from);
13195 for suffix in ["", "-wal", "-shm"] {
13196 let path = PathBuf::from(format!("{}{}", host_path.display(), suffix));
13197 if path.exists() {
13198 fs::remove_file(&path).map_err(|error| {
13199 SidecarError::Io(format!(
13200 "failed to remove sqlite temp artifact {}: {error}",
13201 path.display()
13202 ))
13203 })?;
13204 }
13205 }
13206 if let Some(parent) = parent {
13207 let _ = fs::remove_dir_all(parent);
13208 }
13209 Ok(())
13210}
13211
13212fn ensure_vm_parent_dir(
13213 kernel: &mut SidecarKernel,
13214 kernel_pid: u32,
13215 path: &str,
13216) -> Result<(), SidecarError> {
13217 let parent = dirname(path);
13218 if parent == "/" || parent == "." {
13219 return Ok(());
13220 }
13221 let mut current = String::new();
13222 for segment in parent.split('/').filter(|segment| !segment.is_empty()) {
13223 current.push('/');
13224 current.push_str(segment);
13225 if !kernel
13226 .exists_for_process(EXECUTION_DRIVER_NAME, kernel_pid, ¤t)
13227 .map_err(kernel_error)?
13228 {
13229 kernel
13230 .mkdir_for_process(EXECUTION_DRIVER_NAME, kernel_pid, ¤t, false, None)
13231 .map_err(kernel_error)?;
13232 }
13233 }
13234 Ok(())
13235}
13236
13237fn sqlite_database(
13238 process: &ActiveProcess,
13239 database_id: u64,
13240) -> Result<&ActiveSqliteDatabase, SidecarError> {
13241 process.sqlite_databases.get(&database_id).ok_or_else(|| {
13242 SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13243 })
13244}
13245
13246fn sqlite_database_mut(
13247 process: &mut ActiveProcess,
13248 database_id: u64,
13249) -> Result<&mut ActiveSqliteDatabase, SidecarError> {
13250 process
13251 .sqlite_databases
13252 .get_mut(&database_id)
13253 .ok_or_else(|| {
13254 SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13255 })
13256}
13257
13258fn sqlite_statement(
13259 process: &ActiveProcess,
13260 statement_id: u64,
13261) -> Result<&ActiveSqliteStatement, SidecarError> {
13262 process.sqlite_statements.get(&statement_id).ok_or_else(|| {
13263 SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
13264 })
13265}
13266
13267fn sqlite_statement_mut(
13268 process: &mut ActiveProcess,
13269 statement_id: u64,
13270) -> Result<&mut ActiveSqliteStatement, SidecarError> {
13271 process
13272 .sqlite_statements
13273 .get_mut(&statement_id)
13274 .ok_or_else(|| {
13275 SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
13276 })
13277}
13278
13279fn mark_sqlite_mutation(database: &mut ActiveSqliteDatabase, sql: &str) {
13280 let normalized = sql.trim_start().to_ascii_lowercase();
13281 if normalized.starts_with("begin") || normalized.starts_with("savepoint") {
13282 database.dirty = true;
13283 database.transaction_depth += 1;
13284 return;
13285 }
13286 if normalized.starts_with("commit") || normalized.starts_with("release savepoint") {
13287 database.dirty = true;
13288 database.transaction_depth = database.transaction_depth.saturating_sub(1);
13289 return;
13290 }
13291 if normalized.starts_with("rollback") && !normalized.starts_with("rollback to") {
13292 database.dirty = true;
13293 database.transaction_depth = database.transaction_depth.saturating_sub(1);
13294 return;
13295 }
13296 if normalized.starts_with("insert")
13297 || normalized.starts_with("update")
13298 || normalized.starts_with("delete")
13299 || normalized.starts_with("replace")
13300 || normalized.starts_with("create")
13301 || normalized.starts_with("alter")
13302 || normalized.starts_with("drop")
13303 || normalized.starts_with("vacuum")
13304 || normalized.starts_with("reindex")
13305 || normalized.starts_with("analyze")
13306 || normalized.starts_with("attach")
13307 || normalized.starts_with("detach")
13308 || normalized.starts_with("pragma")
13309 {
13310 database.dirty = true;
13311 }
13312}
13313
13314fn sqlite_option_bool(options: Option<&Value>, key: &str) -> Option<bool> {
13315 options
13316 .and_then(|value| value.get(key))
13317 .and_then(Value::as_bool)
13318}
13319
13320fn sqlite_option_u64(options: Option<&Value>, key: &str) -> Option<u64> {
13321 options
13322 .and_then(|value| value.get(key))
13323 .and_then(Value::as_u64)
13324}
13325
13326fn sqlite_error(error: rusqlite::Error) -> SidecarError {
13327 SidecarError::InvalidState(format!("sqlite error: {error}"))
13328}
13329
13330pub(crate) fn javascript_sync_rpc_arg_str<'a>(
13331 args: &'a [Value],
13332 index: usize,
13333 label: &str,
13334) -> Result<&'a str, SidecarError> {
13335 args.get(index)
13336 .and_then(Value::as_str)
13337 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a string argument")))
13338}
13339
13340pub(crate) fn javascript_sync_rpc_arg_bool(
13341 args: &[Value],
13342 index: usize,
13343 label: &str,
13344) -> Result<bool, SidecarError> {
13345 args.get(index)
13346 .and_then(Value::as_bool)
13347 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a boolean argument")))
13348}
13349
13350pub(crate) fn javascript_sync_rpc_encoding(args: &[Value]) -> Option<String> {
13351 args.get(1).and_then(|value| {
13352 value.as_str().map(str::to_owned).or_else(|| {
13353 value
13354 .get("encoding")
13355 .and_then(Value::as_str)
13356 .map(str::to_owned)
13357 })
13358 })
13359}
13360
13361pub(crate) fn javascript_sync_rpc_option_bool(
13362 args: &[Value],
13363 index: usize,
13364 key: &str,
13365) -> Option<bool> {
13366 let value = args.get(index)?;
13367 if key == "recursive" {
13368 if let Some(boolean) = value.as_bool() {
13369 return Some(boolean);
13370 }
13371 }
13372 value.get(key).and_then(Value::as_bool)
13373}
13374
13375pub(crate) fn javascript_sync_rpc_option_u32(
13376 args: &[Value],
13377 index: usize,
13378 key: &str,
13379) -> Result<Option<u32>, SidecarError> {
13380 let Some(value) = args.get(index).and_then(|value| {
13381 if value.is_object() {
13382 value.get(key)
13383 } else if key == "mode" && value.is_number() {
13384 Some(value)
13385 } else {
13386 None
13387 }
13388 }) else {
13389 return Ok(None);
13390 };
13391 if value.is_null() {
13392 return Ok(None);
13393 }
13394
13395 let numeric = value
13396 .as_u64()
13397 .or_else(|| {
13398 value
13399 .as_f64()
13400 .filter(|number| number.is_finite() && *number >= 0.0)
13401 .map(|number| number as u64)
13402 })
13403 .ok_or_else(|| SidecarError::InvalidState(format!("{key} must be numeric")))?;
13404
13405 u32::try_from(numeric)
13406 .map(Some)
13407 .map_err(|_| SidecarError::InvalidState(format!("{key} must fit within u32")))
13408}
13409
13410pub(crate) fn javascript_sync_rpc_arg_u32(
13411 args: &[Value],
13412 index: usize,
13413 label: &str,
13414) -> Result<u32, SidecarError> {
13415 let value = javascript_sync_rpc_arg_u64(args, index, label)?;
13416 u32::try_from(value)
13417 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
13418}
13419
13420pub(crate) fn javascript_sync_rpc_arg_i32(
13421 args: &[Value],
13422 index: usize,
13423 label: &str,
13424) -> Result<i32, SidecarError> {
13425 let Some(value) = args.get(index) else {
13426 return Err(SidecarError::InvalidState(format!("{label} is required")));
13427 };
13428
13429 let numeric = value
13430 .as_i64()
13431 .or_else(|| {
13432 value
13433 .as_f64()
13434 .filter(|number| number.is_finite())
13435 .map(|number| number as i64)
13436 })
13437 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))?;
13438
13439 i32::try_from(numeric)
13440 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within i32")))
13441}
13442
13443pub(crate) fn javascript_sync_rpc_arg_u32_optional(
13444 args: &[Value],
13445 index: usize,
13446 label: &str,
13447) -> Result<Option<u32>, SidecarError> {
13448 javascript_sync_rpc_arg_u64_optional(args, index, label)?
13449 .map(|value| {
13450 u32::try_from(value)
13451 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
13452 })
13453 .transpose()
13454}
13455
13456pub(crate) fn javascript_sync_rpc_arg_u64(
13457 args: &[Value],
13458 index: usize,
13459 label: &str,
13460) -> Result<u64, SidecarError> {
13461 let Some(value) = args.get(index) else {
13462 return Err(SidecarError::InvalidState(format!("{label} is required")));
13463 };
13464
13465 value
13466 .as_u64()
13467 .or_else(|| {
13468 value
13469 .as_f64()
13470 .filter(|number| number.is_finite() && *number >= 0.0)
13471 .map(|number| number as u64)
13472 })
13473 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))
13474}
13475
13476pub(crate) fn javascript_sync_rpc_arg_u64_optional(
13477 args: &[Value],
13478 index: usize,
13479 label: &str,
13480) -> Result<Option<u64>, SidecarError> {
13481 let Some(value) = args.get(index) else {
13482 return Ok(None);
13483 };
13484 if value.is_null() {
13485 return Ok(None);
13486 }
13487 javascript_sync_rpc_arg_u64(args, index, label).map(Some)
13488}
13489
13490pub(crate) fn javascript_sync_rpc_bytes_arg(
13491 args: &[Value],
13492 index: usize,
13493 label: &str,
13494) -> Result<Vec<u8>, SidecarError> {
13495 let Some(value) = args.get(index) else {
13496 return Err(SidecarError::InvalidState(format!("{label} is required")));
13497 };
13498
13499 if let Some(text) = value.as_str() {
13500 return Ok(text.as_bytes().to_vec());
13501 }
13502
13503 let Some(base64_value) = value
13504 .get("__agentOSType")
13505 .and_then(Value::as_str)
13506 .filter(|kind| *kind == "bytes")
13507 .and_then(|_| value.get("base64"))
13508 .and_then(Value::as_str)
13509 else {
13510 return Err(SidecarError::InvalidState(format!(
13511 "{label} must be a string or encoded bytes payload"
13512 )));
13513 };
13514
13515 base64::engine::general_purpose::STANDARD
13516 .decode(base64_value)
13517 .map_err(|error| {
13518 SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
13519 })
13520}
13521
13522pub(crate) fn javascript_sync_rpc_bytes_value(bytes: &[u8]) -> Value {
13523 json!({
13524 "__agentOSType": "bytes",
13525 "base64": base64::engine::general_purpose::STANDARD.encode(bytes),
13526 })
13527}
13528
13529#[derive(Debug, Deserialize)]
13530struct KernelPollFdRequest {
13531 fd: u32,
13532 events: u16,
13533}
13534
13535#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
13536struct KernelPollFdResponse {
13537 fd: u32,
13538 events: u16,
13539 revents: u16,
13540}
13541
13542fn javascript_sync_rpc_base64_arg(
13543 args: &[Value],
13544 index: usize,
13545 label: &str,
13546) -> Result<Vec<u8>, SidecarError> {
13547 let value = javascript_sync_rpc_arg_str(args, index, label)?;
13548 base64::engine::general_purpose::STANDARD
13549 .decode(value)
13550 .map_err(|error| {
13551 SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
13552 })
13553}
13554
13555pub(crate) fn service_javascript_sync_rpc<B>(
13556 request: JavascriptSyncRpcServiceRequest<'_, B>,
13557) -> Result<Value, SidecarError>
13558where
13559 B: NativeSidecarBridge + Send + 'static,
13560 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
13561{
13562 let JavascriptSyncRpcServiceRequest {
13563 bridge,
13564 vm_id,
13565 dns,
13566 socket_paths,
13567 kernel,
13568 process,
13569 sync_request: request,
13570 resource_limits,
13571 network_counts,
13572 } = request;
13573 match request.method.as_str() {
13574 "_resolveModule"
13577 | "_resolveModuleSync"
13578 | "__resolve_module"
13579 | "_batchResolveModules"
13580 | "__batch_resolve_modules"
13581 | "_loadFile"
13582 | "_loadFileSync"
13583 | "__load_file"
13584 | "_moduleFormat"
13585 | "__module_format" => service_javascript_module_sync_rpc(kernel, process, request),
13586 "_loadPolyfill" | "__load_polyfill" => {
13588 service_javascript_internal_bridge_sync_rpc(process, request)
13589 }
13590 "__kernel_stdin_read" => match &process.execution {
13591 ActiveExecution::Javascript(execution) => execution
13592 .read_kernel_stdin_sync_rpc(request)
13593 .map_err(|error| SidecarError::Execution(error.to_string())),
13594 ActiveExecution::Python(_) | ActiveExecution::Wasm(_) | ActiveExecution::Tool(_) => {
13595 service_javascript_kernel_stdin_sync_rpc(kernel, process, request)
13596 }
13597 },
13598 "__kernel_stdio_write" => {
13599 service_javascript_kernel_stdio_write_sync_rpc(kernel, process, request)
13600 }
13601 "__kernel_poll" => service_javascript_kernel_poll_sync_rpc(kernel, process, request),
13602 "__pty_set_raw_mode" => {
13603 service_javascript_pty_set_raw_mode_sync_rpc(kernel, process, request)
13604 }
13605 "crypto.hashDigest"
13606 | "crypto.hmacDigest"
13607 | "crypto.pbkdf2"
13608 | "crypto.scrypt"
13609 | "crypto.cipheriv"
13610 | "crypto.decipheriv"
13611 | "crypto.cipherivCreate"
13612 | "crypto.cipherivUpdate"
13613 | "crypto.cipherivFinal"
13614 | "crypto.sign"
13615 | "crypto.verify"
13616 | "crypto.asymmetricOp"
13617 | "crypto.createKeyObject"
13618 | "crypto.generateKeyPairSync"
13619 | "crypto.generateKeySync"
13620 | "crypto.generatePrimeSync"
13621 | "crypto.diffieHellman"
13622 | "crypto.diffieHellmanGroup"
13623 | "crypto.diffieHellmanSessionCreate"
13624 | "crypto.diffieHellmanSessionCall"
13625 | "crypto.diffieHellmanSessionDestroy"
13626 | "crypto.subtle" => service_javascript_crypto_sync_rpc(process, request),
13627 "dns.lookup" | "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
13628 service_javascript_dns_sync_rpc(bridge, kernel, vm_id, dns, request)
13629 }
13630 "net.http_listen" | "net.http_close" | "net.http_wait" | "net.http_respond" => {
13631 service_javascript_net_sync_rpc(JavascriptNetSyncRpcServiceRequest {
13632 bridge,
13633 vm_id,
13634 dns,
13635 socket_paths,
13636 kernel,
13637 process,
13638 sync_request: request,
13639 resource_limits,
13640 network_counts,
13641 })
13642 }
13643 "net.http2_server_listen"
13644 | "net.http2_server_poll"
13645 | "net.http2_server_close"
13646 | "net.http2_server_respond"
13647 | "net.http2_server_wait"
13648 | "net.http2_session_connect"
13649 | "net.http2_session_request"
13650 | "net.http2_session_settings"
13651 | "net.http2_session_set_local_window_size"
13652 | "net.http2_session_goaway"
13653 | "net.http2_session_close"
13654 | "net.http2_session_destroy"
13655 | "net.http2_session_poll"
13656 | "net.http2_session_wait"
13657 | "net.http2_stream_respond"
13658 | "net.http2_stream_push_stream"
13659 | "net.http2_stream_write"
13660 | "net.http2_stream_end"
13661 | "net.http2_stream_close"
13662 | "net.http2_stream_pause"
13663 | "net.http2_stream_resume"
13664 | "net.http2_stream_respond_with_file" => {
13665 service_javascript_http2_sync_rpc(JavascriptHttp2SyncRpcServiceRequest {
13666 bridge,
13667 kernel,
13668 vm_id,
13669 dns,
13670 socket_paths,
13671 process,
13672 sync_request: request,
13673 resource_limits,
13674 network_counts,
13675 })
13676 }
13677 "net.connect"
13678 | "net.reserve_tcp_port"
13679 | "net.release_tcp_port"
13680 | "net.listen"
13681 | "net.poll"
13682 | "net.socket_wait_connect"
13683 | "net.socket_read"
13684 | "net.socket_set_no_delay"
13685 | "net.socket_set_keep_alive"
13686 | "net.socket_upgrade_tls"
13687 | "net.socket_get_tls_client_hello"
13688 | "net.socket_tls_query"
13689 | "net.server_poll"
13690 | "net.server_accept"
13691 | "net.server_connections"
13692 | "net.upgrade_socket_write"
13693 | "net.upgrade_socket_end"
13694 | "net.upgrade_socket_destroy"
13695 | "net.write"
13696 | "net.shutdown"
13697 | "net.destroy"
13698 | "net.server_close"
13699 | "tls.get_ciphers" => {
13700 service_javascript_net_sync_rpc(JavascriptNetSyncRpcServiceRequest {
13701 bridge,
13702 vm_id,
13703 dns,
13704 socket_paths,
13705 kernel,
13706 process,
13707 sync_request: request,
13708 resource_limits,
13709 network_counts,
13710 })
13711 }
13712 "dgram.createSocket"
13713 | "dgram.bind"
13714 | "dgram.send"
13715 | "dgram.poll"
13716 | "dgram.close"
13717 | "dgram.address"
13718 | "dgram.setBufferSize"
13719 | "dgram.getBufferSize" => {
13720 service_javascript_dgram_sync_rpc(JavascriptDgramSyncRpcServiceRequest {
13721 bridge,
13722 kernel,
13723 vm_id,
13724 dns,
13725 socket_paths,
13726 process,
13727 sync_request: request,
13728 resource_limits,
13729 network_counts,
13730 })
13731 }
13732 "sqlite.constants"
13733 | "sqlite.open"
13734 | "sqlite.close"
13735 | "sqlite.exec"
13736 | "sqlite.query"
13737 | "sqlite.prepare"
13738 | "sqlite.location"
13739 | "sqlite.checkpoint"
13740 | "sqlite.statement.run"
13741 | "sqlite.statement.get"
13742 | "sqlite.statement.all"
13743 | "sqlite.statement.iterate"
13744 | "sqlite.statement.columns"
13745 | "sqlite.statement.setReturnArrays"
13746 | "sqlite.statement.setReadBigInts"
13747 | "sqlite.statement.setAllowBareNamedParameters"
13748 | "sqlite.statement.setAllowUnknownNamedParameters"
13749 | "sqlite.statement.finalize" => {
13750 service_javascript_sqlite_sync_rpc(kernel, process, request)
13751 }
13752 "process.kill" => {
13753 let target_pid =
13754 javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
13755 let signal = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
13756 let parsed_signal = parse_signal(signal)?;
13757 if parsed_signal == 0 {
13758 kernel
13759 .signal_process(EXECUTION_DRIVER_NAME, target_pid, parsed_signal)
13760 .map_err(kernel_error)?;
13761 return Ok(Value::Null);
13762 }
13763 let process_pid = i32::try_from(process.kernel_pid)
13764 .map_err(|_| SidecarError::InvalidState("process pid exceeds i32".into()))?;
13765 if target_pid != process_pid {
13766 return Err(SidecarError::InvalidState(format!(
13767 "unknown process pid {target_pid}"
13768 )));
13769 }
13770 process.pending_self_signal_exit = None;
13771 if parsed_signal != 0
13772 && !matches!(
13773 canonical_signal_name(parsed_signal),
13774 Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
13775 )
13776 {
13777 process.pending_self_signal_exit = Some(parsed_signal);
13778 }
13779 Ok(json!({
13780 "self": true,
13781 "action": "default",
13782 }))
13783 }
13784 "process.umask" => {
13785 let new_mask = javascript_sync_rpc_arg_u32_optional(&request.args, 0, "process umask")?;
13786 kernel
13787 .umask(EXECUTION_DRIVER_NAME, process.kernel_pid, new_mask)
13788 .map(|mask| json!(mask))
13789 .map_err(kernel_error)
13790 }
13791 "fs.chmodSync" | "fs.promises.chmod" => {
13792 let response =
13793 service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request)?;
13794 mirror_process_chmod_to_host(process, request)?;
13795 Ok(response)
13796 }
13797 _ => service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request),
13798 }
13799}
13800
13801fn service_javascript_internal_bridge_sync_rpc(
13802 process: &ActiveProcess,
13803 request: &JavascriptSyncRpcRequest,
13804) -> Result<Value, SidecarError> {
13805 let method = match request.method.as_str() {
13809 "_loadPolyfill" | "__load_polyfill" => "_loadPolyfill",
13810 other => {
13811 return Err(SidecarError::InvalidState(format!(
13812 "unsupported JavaScript internal bridge method {other}"
13813 )));
13814 }
13815 };
13816
13817 handle_internal_bridge_call_from_host_context(
13818 &process.host_cwd,
13819 &process.guest_cwd,
13820 &process.env,
13821 method,
13822 &request.args,
13823 )
13824 .ok_or_else(|| {
13825 SidecarError::InvalidState(format!(
13826 "JavaScript internal bridge method {method} returned no value"
13827 ))
13828 })
13829}
13830
13831fn mirror_process_chmod_to_host(
13832 process: &ActiveProcess,
13833 request: &JavascriptSyncRpcRequest,
13834) -> Result<(), SidecarError> {
13835 let guest_path = javascript_sync_rpc_arg_str(&request.args, 0, "filesystem chmod path")?;
13836 let mode = javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem chmod mode")? & 0o7777;
13837 let Some(host_path) = resolve_process_guest_path_to_host(process, guest_path) else {
13838 return Ok(());
13839 };
13840 if !host_path.exists() {
13841 return Ok(());
13842 }
13843 fs::set_permissions(&host_path, fs::Permissions::from_mode(mode)).map_err(|error| {
13844 SidecarError::Io(format!(
13845 "failed to mirror chmod to host path {}: {error}",
13846 host_path.display()
13847 ))
13848 })
13849}
13850
13851fn resolve_process_guest_path_to_host(
13852 process: &ActiveProcess,
13853 guest_path: &str,
13854) -> Option<PathBuf> {
13855 let normalized_guest_path = if guest_path.starts_with('/') {
13856 normalize_path(guest_path)
13857 } else {
13858 normalize_path(&format!(
13859 "{}/{}",
13860 process.guest_cwd.trim_end_matches('/'),
13861 guest_path
13862 ))
13863 };
13864 if let Some(host_path) =
13865 host_path_from_runtime_guest_mappings(&process.env, &normalized_guest_path)
13866 {
13867 return Some(host_path);
13868 }
13869 let normalized_guest_cwd = normalize_path(&process.guest_cwd);
13870 let mut host_root = normalize_host_path(&process.host_cwd);
13871 for _ in normalized_guest_cwd
13872 .trim_start_matches('/')
13873 .split('/')
13874 .filter(|segment| !segment.is_empty())
13875 {
13876 host_root = host_root.parent()?.to_path_buf();
13877 }
13878 if normalized_guest_path == "/" {
13879 Some(host_root)
13880 } else {
13881 Some(host_root.join(normalized_guest_path.trim_start_matches('/')))
13882 }
13883}
13884
13885pub(crate) fn service_javascript_crypto_sync_rpc(
13886 process: &mut ActiveProcess,
13887 request: &JavascriptSyncRpcRequest,
13888) -> Result<Value, SidecarError> {
13889 match request.method.as_str() {
13890 "crypto.hashDigest" => {
13891 let algorithm = javascript_crypto_digest_algorithm(
13892 &request.args,
13893 0,
13894 "crypto.hashDigest algorithm",
13895 )?;
13896 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hashDigest data")?;
13897 Ok(Value::String(
13898 base64::engine::general_purpose::STANDARD.encode(algorithm.digest(&data)),
13899 ))
13900 }
13901 "crypto.hmacDigest" => {
13902 let algorithm = javascript_crypto_digest_algorithm(
13903 &request.args,
13904 0,
13905 "crypto.hmacDigest algorithm",
13906 )?;
13907 let key = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hmacDigest key")?;
13908 let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.hmacDigest data")?;
13909 Ok(Value::String(
13910 base64::engine::general_purpose::STANDARD.encode(algorithm.hmac(&key, &data)?),
13911 ))
13912 }
13913 "crypto.pbkdf2" => {
13914 let password =
13915 javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.pbkdf2 password")?;
13916 let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.pbkdf2 salt")?;
13917 let iterations =
13918 javascript_sync_rpc_arg_u32(&request.args, 2, "crypto.pbkdf2 iterations")?;
13919 if iterations == 0 {
13920 return Err(SidecarError::InvalidState(String::from(
13921 "crypto.pbkdf2 iterations must be greater than zero",
13922 )));
13923 }
13924 let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
13925 &request.args,
13926 3,
13927 "crypto.pbkdf2 key length",
13928 )?)
13929 .map_err(|_| {
13930 SidecarError::InvalidState(String::from(
13931 "crypto.pbkdf2 key length must fit within usize",
13932 ))
13933 })?;
13934 let algorithm =
13935 javascript_crypto_digest_algorithm(&request.args, 4, "crypto.pbkdf2 digest")?;
13936 let mut output = vec![0u8; key_len];
13937 algorithm.pbkdf2(&password, &salt, iterations, &mut output);
13938 Ok(Value::String(
13939 base64::engine::general_purpose::STANDARD.encode(output),
13940 ))
13941 }
13942 "crypto.scrypt" => {
13943 let password =
13944 javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.scrypt password")?;
13945 let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.scrypt salt")?;
13946 let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
13947 &request.args,
13948 2,
13949 "crypto.scrypt key length",
13950 )?)
13951 .map_err(|_| {
13952 SidecarError::InvalidState(String::from(
13953 "crypto.scrypt key length must fit within usize",
13954 ))
13955 })?;
13956 let options_json =
13957 javascript_sync_rpc_arg_str(&request.args, 3, "crypto.scrypt options")?;
13958 let options: JavascriptScryptOptions =
13959 serde_json::from_str(options_json).map_err(|error| {
13960 SidecarError::InvalidState(format!(
13961 "crypto.scrypt options must be valid JSON: {error}"
13962 ))
13963 })?;
13964 let cost = options.cost.unwrap_or(DEFAULT_SCRYPT_COST);
13965 if cost == 0 || !cost.is_power_of_two() {
13966 return Err(SidecarError::InvalidState(String::from(
13967 "crypto.scrypt cost must be a positive power of two",
13968 )));
13969 }
13970 let log_n = u8::try_from(cost.ilog2()).map_err(|_| {
13971 SidecarError::InvalidState(String::from(
13972 "crypto.scrypt cost exceeds supported parameter range",
13973 ))
13974 })?;
13975 let params = ScryptParams::new(
13976 log_n,
13977 options.block_size.unwrap_or(DEFAULT_SCRYPT_BLOCK_SIZE),
13978 options
13979 .parallelization
13980 .unwrap_or(DEFAULT_SCRYPT_PARALLELIZATION),
13981 key_len,
13982 )
13983 .map_err(|error| {
13984 SidecarError::InvalidState(format!("crypto.scrypt options are invalid: {error}"))
13985 })?;
13986 let mut output = vec![0u8; key_len];
13987 scrypt(&password, &salt, ¶ms, &mut output).map_err(|error| {
13988 SidecarError::Execution(format!("crypto.scrypt failed: {error}"))
13989 })?;
13990 Ok(Value::String(
13991 base64::engine::general_purpose::STANDARD.encode(output),
13992 ))
13993 }
13994 "crypto.cipheriv" => service_javascript_crypto_cipheriv_sync_rpc(request),
13995 "crypto.decipheriv" => service_javascript_crypto_decipheriv_sync_rpc(request),
13996 "crypto.cipherivCreate" => {
13997 service_javascript_crypto_cipheriv_create_sync_rpc(process, request)
13998 }
13999 "crypto.cipherivUpdate" => {
14000 service_javascript_crypto_cipheriv_update_sync_rpc(process, request)
14001 }
14002 "crypto.cipherivFinal" => {
14003 service_javascript_crypto_cipheriv_final_sync_rpc(process, request)
14004 }
14005 "crypto.sign" => service_javascript_crypto_sign_sync_rpc(request),
14006 "crypto.verify" => service_javascript_crypto_verify_sync_rpc(request),
14007 "crypto.asymmetricOp" => service_javascript_crypto_asymmetric_op_sync_rpc(request),
14008 "crypto.createKeyObject" => service_javascript_crypto_create_key_object_sync_rpc(request),
14009 "crypto.generateKeyPairSync" => {
14010 service_javascript_crypto_generate_key_pair_sync_rpc(request)
14011 }
14012 "crypto.generateKeySync" => service_javascript_crypto_generate_key_sync_rpc(request),
14013 "crypto.generatePrimeSync" => service_javascript_crypto_generate_prime_sync_rpc(request),
14014 "crypto.diffieHellman" => service_javascript_crypto_diffie_hellman_sync_rpc(request),
14015 "crypto.diffieHellmanGroup" => {
14016 service_javascript_crypto_diffie_hellman_group_sync_rpc(request)
14017 }
14018 "crypto.diffieHellmanSessionCreate" => {
14019 service_javascript_crypto_diffie_hellman_session_create_sync_rpc(process, request)
14020 }
14021 "crypto.diffieHellmanSessionCall" => {
14022 service_javascript_crypto_diffie_hellman_session_call_sync_rpc(process, request)
14023 }
14024 "crypto.diffieHellmanSessionDestroy" => {
14025 service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(process, request)
14026 }
14027 "crypto.subtle" => service_javascript_crypto_subtle_sync_rpc(request),
14028 _ => Err(SidecarError::InvalidState(format!(
14029 "unsupported JavaScript crypto sync RPC method {}",
14030 request.method
14031 ))),
14032 }
14033}
14034
14035fn javascript_crypto_digest_algorithm(
14036 args: &[Value],
14037 index: usize,
14038 label: &str,
14039) -> Result<JavascriptCryptoDigestAlgorithm, SidecarError> {
14040 JavascriptCryptoDigestAlgorithm::parse(javascript_sync_rpc_arg_str(args, index, label)?)
14041}
14042
14043impl JavascriptCryptoDigestAlgorithm {
14044 fn parse(value: &str) -> Result<Self, SidecarError> {
14045 match value.trim().to_ascii_lowercase().replace('-', "").as_str() {
14046 "md5" => Ok(Self::Md5),
14047 "sha1" => Ok(Self::Sha1),
14048 "sha256" => Ok(Self::Sha256),
14049 "sha512" => Ok(Self::Sha512),
14050 _ => Err(SidecarError::InvalidState(format!(
14051 "unsupported crypto digest algorithm {value}"
14052 ))),
14053 }
14054 }
14055
14056 fn digest(self, data: &[u8]) -> Vec<u8> {
14057 match self {
14058 Self::Md5 => Md5::digest(data).to_vec(),
14059 Self::Sha1 => Sha1::digest(data).to_vec(),
14060 Self::Sha256 => Sha256::digest(data).to_vec(),
14061 Self::Sha512 => Sha512::digest(data).to_vec(),
14062 }
14063 }
14064
14065 fn hmac(self, key: &[u8], data: &[u8]) -> Result<Vec<u8>, SidecarError> {
14066 match self {
14067 Self::Md5 => {
14068 let mut mac = Hmac::<Md5>::new_from_slice(key).map_err(|error| {
14069 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14070 })?;
14071 mac.update(data);
14072 Ok(mac.finalize().into_bytes().to_vec())
14073 }
14074 Self::Sha1 => {
14075 let mut mac = Hmac::<Sha1>::new_from_slice(key).map_err(|error| {
14076 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14077 })?;
14078 mac.update(data);
14079 Ok(mac.finalize().into_bytes().to_vec())
14080 }
14081 Self::Sha256 => {
14082 let mut mac = Hmac::<Sha256>::new_from_slice(key).map_err(|error| {
14083 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14084 })?;
14085 mac.update(data);
14086 Ok(mac.finalize().into_bytes().to_vec())
14087 }
14088 Self::Sha512 => {
14089 let mut mac = Hmac::<Sha512>::new_from_slice(key).map_err(|error| {
14090 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14091 })?;
14092 mac.update(data);
14093 Ok(mac.finalize().into_bytes().to_vec())
14094 }
14095 }
14096 }
14097
14098 fn pbkdf2(self, password: &[u8], salt: &[u8], iterations: u32, output: &mut [u8]) {
14099 match self {
14100 Self::Md5 => pbkdf2_hmac::<Md5>(password, salt, iterations, output),
14101 Self::Sha1 => pbkdf2_hmac::<Sha1>(password, salt, iterations, output),
14102 Self::Sha256 => pbkdf2_hmac::<Sha256>(password, salt, iterations, output),
14103 Self::Sha512 => pbkdf2_hmac::<Sha512>(password, salt, iterations, output),
14104 }
14105 }
14106}
14107
14108#[derive(Debug, Clone)]
14109enum JavascriptCryptoKeyMaterial {
14110 Private(PKey<Private>),
14111 Public(PKey<Public>),
14112 Secret(Vec<u8>),
14113}
14114
14115#[derive(Debug, Clone, Deserialize, Serialize)]
14116struct JavascriptSerializedSandboxKeyObject {
14117 #[serde(rename = "type")]
14118 kind: String,
14119 #[serde(skip_serializing_if = "Option::is_none")]
14120 pem: Option<String>,
14121 #[serde(skip_serializing_if = "Option::is_none")]
14122 raw: Option<String>,
14123 #[serde(skip_serializing_if = "Option::is_none", rename = "asymmetricKeyType")]
14124 asymmetric_key_type: Option<String>,
14125 #[serde(
14126 skip_serializing_if = "Option::is_none",
14127 rename = "asymmetricKeyDetails"
14128 )]
14129 asymmetric_key_details: Option<Map<String, Value>>,
14130 #[serde(skip_serializing_if = "Option::is_none")]
14131 jwk: Option<Value>,
14132}
14133
14134#[derive(Debug, Clone)]
14135struct JavascriptDirectKeyInput {
14136 key: JavascriptCryptoKeyMaterial,
14137 padding: Option<Padding>,
14138}
14139
14140fn service_javascript_crypto_cipheriv_sync_rpc(
14141 request: &JavascriptSyncRpcRequest,
14142) -> Result<Value, SidecarError> {
14143 service_javascript_crypto_cipheriv_inner(request, false)
14144}
14145
14146fn service_javascript_crypto_decipheriv_sync_rpc(
14147 request: &JavascriptSyncRpcRequest,
14148) -> Result<Value, SidecarError> {
14149 service_javascript_crypto_cipheriv_inner(request, true)
14150}
14151
14152fn service_javascript_crypto_cipheriv_create_sync_rpc(
14153 process: &mut ActiveProcess,
14154 request: &JavascriptSyncRpcRequest,
14155) -> Result<Value, SidecarError> {
14156 ensure_per_process_state_handle_capacity(process.cipher_sessions.len(), "cipher session")?;
14157 let mode = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.cipherivCreate mode")?;
14158 let decrypt = mode == "decipher";
14159 let algorithm =
14160 javascript_sync_rpc_arg_str(&request.args, 1, "crypto.cipherivCreate algorithm")?;
14161 let key = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.cipherivCreate key")?;
14162 let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 3, "crypto.cipherivCreate iv")?;
14163 let options =
14164 javascript_sync_rpc_json_arg_optional(&request.args, 4, "crypto.cipherivCreate options")?;
14165 let auth_tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options.as_ref())?;
14166 let context = javascript_crypto_build_cipher_context(
14167 algorithm,
14168 &key,
14169 iv.as_deref(),
14170 decrypt,
14171 options.as_ref(),
14172 )?;
14173 process.next_cipher_session_id += 1;
14174 let session_id = process.next_cipher_session_id;
14175 process.cipher_sessions.insert(
14176 session_id,
14177 ActiveCipherSession {
14178 algorithm: algorithm.to_string(),
14179 auth_tag_len,
14180 context,
14181 },
14182 );
14183 Ok(json!(session_id))
14184}
14185
14186fn service_javascript_crypto_cipheriv_update_sync_rpc(
14187 process: &mut ActiveProcess,
14188 request: &JavascriptSyncRpcRequest,
14189) -> Result<Value, SidecarError> {
14190 let session_id =
14191 javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivUpdate session id")?;
14192 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.cipherivUpdate data")?;
14193 let session = process
14194 .cipher_sessions
14195 .get_mut(&session_id)
14196 .ok_or_else(|| {
14197 SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
14198 })?;
14199 let result = javascript_crypto_cipher_update(&mut session.context, &data)?;
14200 Ok(Value::String(
14201 base64::engine::general_purpose::STANDARD.encode(result),
14202 ))
14203}
14204
14205fn service_javascript_crypto_cipheriv_final_sync_rpc(
14206 process: &mut ActiveProcess,
14207 request: &JavascriptSyncRpcRequest,
14208) -> Result<Value, SidecarError> {
14209 let session_id =
14210 javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivFinal session id")?;
14211 let mut session = process.cipher_sessions.remove(&session_id).ok_or_else(|| {
14212 SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
14213 })?;
14214 let data = javascript_crypto_cipher_finalize(&mut session.context)?;
14215 let mut response = Map::new();
14216 response.insert(
14217 String::from("data"),
14218 Value::String(base64::engine::general_purpose::STANDARD.encode(data)),
14219 );
14220 if javascript_crypto_is_aead(&session.algorithm) {
14221 let mut auth_tag = vec![0_u8; session.auth_tag_len];
14222 session
14223 .context
14224 .get_tag(&mut auth_tag)
14225 .map_err(javascript_crypto_openssl_error)?;
14226 response.insert(
14227 String::from("authTag"),
14228 Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
14229 );
14230 }
14231 Ok(Value::String(serde_json::to_string(&response).map_err(
14232 |error| SidecarError::InvalidState(format!("serialize cipher final response: {error}")),
14233 )?))
14234}
14235
14236fn service_javascript_crypto_sign_sync_rpc(
14237 request: &JavascriptSyncRpcRequest,
14238) -> Result<Value, SidecarError> {
14239 let algorithm = request.args.first().and_then(Value::as_str);
14240 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.sign data")?;
14241 let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.sign key")?;
14242 let key_input =
14243 javascript_crypto_parse_direct_key_input(key_json, Some("private"), "crypto.sign key")?;
14244 let private_key = javascript_crypto_expect_private_key(key_input.key, "crypto.sign key")?;
14245 let mut signer = javascript_crypto_new_signer(algorithm, &private_key)?;
14246 if let Some(padding) = key_input.padding {
14247 signer
14248 .set_rsa_padding(padding)
14249 .map_err(javascript_crypto_openssl_error)?;
14250 }
14251 signer
14252 .update(&data)
14253 .map_err(javascript_crypto_openssl_error)?;
14254 Ok(Value::String(
14255 base64::engine::general_purpose::STANDARD.encode(
14256 signer
14257 .sign_to_vec()
14258 .map_err(javascript_crypto_openssl_error)?,
14259 ),
14260 ))
14261}
14262
14263fn service_javascript_crypto_verify_sync_rpc(
14264 request: &JavascriptSyncRpcRequest,
14265) -> Result<Value, SidecarError> {
14266 let algorithm = request.args.first().and_then(Value::as_str);
14267 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.verify data")?;
14268 let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.verify key")?;
14269 let signature = javascript_sync_rpc_base64_arg(&request.args, 3, "crypto.verify signature")?;
14270 let key_input =
14271 javascript_crypto_parse_direct_key_input(key_json, Some("public"), "crypto.verify key")?;
14272 let public_key = javascript_crypto_expect_public_key(key_input.key, "crypto.verify key")?;
14273 let mut verifier = javascript_crypto_new_verifier(algorithm, &public_key)?;
14274 if let Some(padding) = key_input.padding {
14275 verifier
14276 .set_rsa_padding(padding)
14277 .map_err(javascript_crypto_openssl_error)?;
14278 }
14279 verifier
14280 .update(&data)
14281 .map_err(javascript_crypto_openssl_error)?;
14282 Ok(json!(verifier
14283 .verify(&signature)
14284 .map_err(javascript_crypto_openssl_error)?))
14285}
14286
14287fn service_javascript_crypto_asymmetric_op_sync_rpc(
14288 request: &JavascriptSyncRpcRequest,
14289) -> Result<Value, SidecarError> {
14290 let operation = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.asymmetricOp operation")?;
14291 let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.asymmetricOp key")?;
14292 let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.asymmetricOp data")?;
14293 let expect_kind = match operation {
14294 "publicEncrypt" | "publicDecrypt" => Some("public"),
14295 "privateEncrypt" | "privateDecrypt" => Some("private"),
14296 other => {
14297 return Err(SidecarError::InvalidState(format!(
14298 "Unsupported asymmetric crypto operation: {other}"
14299 )));
14300 }
14301 };
14302 let key_input =
14303 javascript_crypto_parse_direct_key_input(key_json, expect_kind, "crypto.asymmetricOp key")?;
14304 let padding = key_input.padding.unwrap_or(Padding::PKCS1);
14305 let mut output = vec![0_u8; javascript_crypto_rsa_output_size(&key_input.key)?];
14306 let written = match (operation, key_input.key) {
14307 ("publicEncrypt", JavascriptCryptoKeyMaterial::Public(key))
14308 | ("publicDecrypt", JavascriptCryptoKeyMaterial::Public(key)) => {
14309 let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
14310 if operation == "publicEncrypt" {
14311 rsa.public_encrypt(&data, &mut output, padding)
14312 .map_err(javascript_crypto_openssl_error)?
14313 } else {
14314 rsa.public_decrypt(&data, &mut output, padding)
14315 .map_err(javascript_crypto_openssl_error)?
14316 }
14317 }
14318 ("privateEncrypt", JavascriptCryptoKeyMaterial::Private(key))
14319 | ("privateDecrypt", JavascriptCryptoKeyMaterial::Private(key)) => {
14320 let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
14321 if operation == "privateEncrypt" {
14322 rsa.private_encrypt(&data, &mut output, padding)
14323 .map_err(javascript_crypto_openssl_error)?
14324 } else {
14325 rsa.private_decrypt(&data, &mut output, padding)
14326 .map_err(javascript_crypto_openssl_error)?
14327 }
14328 }
14329 _ => {
14330 return Err(SidecarError::InvalidState(format!(
14331 "{operation} requires an RSA {} key",
14332 expect_kind.unwrap_or("asymmetric")
14333 )));
14334 }
14335 };
14336 output.truncate(written);
14337 Ok(Value::String(
14338 base64::engine::general_purpose::STANDARD.encode(output),
14339 ))
14340}
14341
14342fn service_javascript_crypto_create_key_object_sync_rpc(
14343 request: &JavascriptSyncRpcRequest,
14344) -> Result<Value, SidecarError> {
14345 let operation =
14346 javascript_sync_rpc_arg_str(&request.args, 0, "crypto.createKeyObject operation")?;
14347 let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.createKeyObject key")?;
14348 let expected = match operation {
14349 "createPrivateKey" => Some("private"),
14350 "createPublicKey" => Some("public"),
14351 other => {
14352 return Err(SidecarError::InvalidState(format!(
14353 "Unsupported key creation operation: {other}"
14354 )));
14355 }
14356 };
14357 let key_input =
14358 javascript_crypto_parse_direct_key_input(key_json, expected, "crypto.createKeyObject key")?;
14359 Ok(Value::String(
14360 serde_json::to_string(&javascript_crypto_serialize_sandbox_key_object(
14361 &key_input.key,
14362 )?)
14363 .map_err(|error| {
14364 SidecarError::InvalidState(format!("serialize crypto key object: {error}"))
14365 })?,
14366 ))
14367}
14368
14369fn service_javascript_crypto_generate_key_pair_sync_rpc(
14370 request: &JavascriptSyncRpcRequest,
14371) -> Result<Value, SidecarError> {
14372 let key_type =
14373 javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeyPairSync type")?;
14374 let options = javascript_crypto_parse_serialized_options_arg(
14375 &request.args,
14376 1,
14377 "crypto.generateKeyPairSync options",
14378 )?
14379 .unwrap_or(Value::Object(Map::new()));
14380 let public_encoding = options.get("publicKeyEncoding").cloned();
14381 let private_encoding = options.get("privateKeyEncoding").cloned();
14382
14383 let private_key = match key_type {
14384 "rsa" => {
14385 let bits = options
14386 .get("modulusLength")
14387 .and_then(Value::as_u64)
14388 .unwrap_or(2048) as u32;
14389 let exponent = options
14390 .get("publicExponent")
14391 .map(|value| javascript_crypto_u32_from_bridge_value(value, "rsa publicExponent"))
14392 .transpose()?
14393 .unwrap_or(65_537);
14394 let exponent = BigNum::from_u32(exponent).map_err(javascript_crypto_openssl_error)?;
14395 let rsa =
14396 Rsa::generate_with_e(bits, &exponent).map_err(javascript_crypto_openssl_error)?;
14397 PKey::from_rsa(rsa).map_err(javascript_crypto_openssl_error)?
14398 }
14399 "ec" => {
14400 let named_curve = options
14401 .get("namedCurve")
14402 .and_then(Value::as_str)
14403 .ok_or_else(|| {
14404 SidecarError::InvalidState(String::from(
14405 "crypto.generateKeyPairSync ec requires namedCurve",
14406 ))
14407 })?;
14408 let group = EcGroup::from_curve_name(javascript_crypto_curve_nid(named_curve)?)
14409 .map_err(javascript_crypto_openssl_error)?;
14410 let key = EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?;
14411 PKey::from_ec_key(key).map_err(javascript_crypto_openssl_error)?
14412 }
14413 "ed25519" => PKey::generate_ed25519().map_err(javascript_crypto_openssl_error)?,
14414 "x25519" => PKey::generate_x25519().map_err(javascript_crypto_openssl_error)?,
14415 other => {
14416 return Err(SidecarError::InvalidState(format!(
14417 "unsupported crypto key pair type {other}"
14418 )));
14419 }
14420 };
14421 let public_key = PKey::public_key_from_pem(
14422 &private_key
14423 .public_key_to_pem()
14424 .map_err(javascript_crypto_openssl_error)?,
14425 )
14426 .map_err(javascript_crypto_openssl_error)?;
14427 let response = if public_encoding.is_some() || private_encoding.is_some() {
14428 json!({
14429 "publicKey": javascript_crypto_serialize_encoded_key_value_public(&public_key, public_encoding.as_ref())?,
14430 "privateKey": javascript_crypto_serialize_encoded_key_value_private(&private_key, private_encoding.as_ref())?,
14431 })
14432 } else {
14433 json!({
14434 "publicKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(public_key))?,
14435 "privateKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(private_key))?,
14436 })
14437 };
14438 Ok(Value::String(serde_json::to_string(&response).map_err(
14439 |error| SidecarError::InvalidState(format!("serialize generated key pair: {error}")),
14440 )?))
14441}
14442
14443fn service_javascript_crypto_generate_key_sync_rpc(
14444 request: &JavascriptSyncRpcRequest,
14445) -> Result<Value, SidecarError> {
14446 let key_type = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeySync type")?;
14447 let options = javascript_crypto_parse_serialized_options_arg(
14448 &request.args,
14449 1,
14450 "crypto.generateKeySync options",
14451 )?
14452 .unwrap_or(Value::Object(Map::new()));
14453 let bit_length = options
14454 .get("length")
14455 .and_then(Value::as_u64)
14456 .ok_or_else(|| {
14457 SidecarError::InvalidState(String::from(
14458 "crypto.generateKeySync options.length is required",
14459 ))
14460 })? as usize;
14461 let mut raw = vec![0_u8; bit_length.div_ceil(8)];
14462 rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
14463 let serialized = match key_type {
14464 "hmac" => javascript_crypto_serialize_sandbox_key_object(
14465 &JavascriptCryptoKeyMaterial::Secret(raw),
14466 )?,
14467 "aes" => javascript_crypto_serialize_sandbox_key_object(
14468 &JavascriptCryptoKeyMaterial::Secret(raw),
14469 )?,
14470 other => {
14471 return Err(SidecarError::InvalidState(format!(
14472 "unsupported crypto.generateKeySync type {other}"
14473 )));
14474 }
14475 };
14476 Ok(Value::String(serde_json::to_string(&serialized).map_err(
14477 |error| SidecarError::InvalidState(format!("serialize generated key: {error}")),
14478 )?))
14479}
14480
14481fn service_javascript_crypto_generate_prime_sync_rpc(
14482 request: &JavascriptSyncRpcRequest,
14483) -> Result<Value, SidecarError> {
14484 let bits =
14485 javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.generatePrimeSync size")? as i32;
14486 let options = javascript_crypto_parse_serialized_options_arg(
14487 &request.args,
14488 1,
14489 "crypto.generatePrimeSync options",
14490 )?
14491 .unwrap_or(Value::Object(Map::new()));
14492 let safe = options
14493 .get("safe")
14494 .and_then(Value::as_bool)
14495 .unwrap_or(false);
14496 let add = options
14497 .get("add")
14498 .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime add"))
14499 .transpose()?;
14500 let rem = options
14501 .get("rem")
14502 .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime rem"))
14503 .transpose()?;
14504 let mut prime = BigNum::new().map_err(javascript_crypto_openssl_error)?;
14505 prime
14506 .generate_prime(bits, safe, add.as_deref(), rem.as_deref())
14507 .map_err(javascript_crypto_openssl_error)?;
14508 let payload = if options
14509 .get("bigint")
14510 .and_then(Value::as_bool)
14511 .unwrap_or(false)
14512 {
14513 json!({
14514 "__type": "bigint",
14515 "value": prime.to_dec_str().map_err(javascript_crypto_openssl_error)?.to_string(),
14516 })
14517 } else {
14518 json!({
14519 "__type": "buffer",
14520 "value": base64::engine::general_purpose::STANDARD.encode(prime.to_vec()),
14521 })
14522 };
14523 Ok(Value::String(serde_json::to_string(&payload).map_err(
14524 |error| SidecarError::InvalidState(format!("serialize generated prime: {error}")),
14525 )?))
14526}
14527
14528fn service_javascript_crypto_diffie_hellman_sync_rpc(
14529 request: &JavascriptSyncRpcRequest,
14530) -> Result<Value, SidecarError> {
14531 let options = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellman options")?;
14532 let parsed: Value = serde_json::from_str(options).map_err(|error| {
14533 SidecarError::InvalidState(format!(
14534 "crypto.diffieHellman options must be valid JSON: {error}"
14535 ))
14536 })?;
14537 let private_key = javascript_crypto_parse_key_material_value(
14538 parsed.get("privateKey").ok_or_else(|| {
14539 SidecarError::InvalidState(String::from("crypto.diffieHellman missing privateKey"))
14540 })?,
14541 Some("private"),
14542 "crypto.diffieHellman privateKey",
14543 )?;
14544 let public_key = javascript_crypto_parse_key_material_value(
14545 parsed.get("publicKey").ok_or_else(|| {
14546 SidecarError::InvalidState(String::from("crypto.diffieHellman missing publicKey"))
14547 })?,
14548 Some("public"),
14549 "crypto.diffieHellman publicKey",
14550 )?;
14551 let private_key =
14552 javascript_crypto_expect_private_key(private_key, "crypto.diffieHellman privateKey")?;
14553 let public_key =
14554 javascript_crypto_expect_public_key(public_key, "crypto.diffieHellman publicKey")?;
14555 let mut deriver = Deriver::new(&private_key).map_err(javascript_crypto_openssl_error)?;
14556 deriver
14557 .set_peer(&public_key)
14558 .map_err(javascript_crypto_openssl_error)?;
14559 let secret = deriver
14560 .derive_to_vec()
14561 .map_err(javascript_crypto_openssl_error)?;
14562 Ok(Value::String(
14563 serde_json::to_string(&json!({
14564 "__type": "buffer",
14565 "value": base64::engine::general_purpose::STANDARD.encode(secret),
14566 }))
14567 .map_err(|error| {
14568 SidecarError::InvalidState(format!("serialize derived secret: {error}"))
14569 })?,
14570 ))
14571}
14572
14573fn service_javascript_crypto_diffie_hellman_group_sync_rpc(
14574 request: &JavascriptSyncRpcRequest,
14575) -> Result<Value, SidecarError> {
14576 let name = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellmanGroup name")?;
14577 let params = javascript_crypto_named_dh_group(name)?;
14578 let response = json!({
14579 "prime": {
14580 "__type": "buffer",
14581 "value": base64::engine::general_purpose::STANDARD.encode(params.prime_p().to_vec()),
14582 },
14583 "generator": {
14584 "__type": "buffer",
14585 "value": base64::engine::general_purpose::STANDARD.encode(params.generator().to_vec()),
14586 },
14587 });
14588 Ok(Value::String(serde_json::to_string(&response).map_err(
14589 |error| {
14590 SidecarError::InvalidState(format!("serialize diffieHellmanGroup response: {error}"))
14591 },
14592 )?))
14593}
14594
14595fn service_javascript_crypto_diffie_hellman_session_create_sync_rpc(
14596 process: &mut ActiveProcess,
14597 request: &JavascriptSyncRpcRequest,
14598) -> Result<Value, SidecarError> {
14599 ensure_per_process_state_handle_capacity(
14600 process.diffie_hellman_sessions.len(),
14601 "diffie-hellman session",
14602 )?;
14603 let raw = javascript_sync_rpc_arg_str(
14604 &request.args,
14605 0,
14606 "crypto.diffieHellmanSessionCreate request",
14607 )?;
14608 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14609 SidecarError::InvalidState(format!(
14610 "crypto.diffieHellmanSessionCreate request must be valid JSON: {error}"
14611 ))
14612 })?;
14613 let session = match parsed.get("type").and_then(Value::as_str) {
14614 Some("group") => {
14615 let name = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
14616 SidecarError::InvalidState(String::from(
14617 "crypto.diffieHellmanSessionCreate group requires name",
14618 ))
14619 })?;
14620 ActiveDiffieHellmanSession::Dh(ActiveDhSession {
14621 params: javascript_crypto_named_dh_group(name)?,
14622 key_pair: None,
14623 })
14624 }
14625 Some("dh") => {
14626 let args = parsed
14627 .get("args")
14628 .and_then(Value::as_array)
14629 .ok_or_else(|| {
14630 SidecarError::InvalidState(String::from(
14631 "crypto.diffieHellmanSessionCreate dh requires args",
14632 ))
14633 })?;
14634 let params = javascript_crypto_build_dh_params(args)?;
14635 ActiveDiffieHellmanSession::Dh(ActiveDhSession {
14636 params,
14637 key_pair: None,
14638 })
14639 }
14640 Some("ecdh") => {
14641 let curve = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
14642 SidecarError::InvalidState(String::from(
14643 "crypto.diffieHellmanSessionCreate ecdh requires name",
14644 ))
14645 })?;
14646 ActiveDiffieHellmanSession::Ecdh(ActiveEcdhSession {
14647 curve: curve.to_string(),
14648 key_pair: None,
14649 })
14650 }
14651 other => {
14652 return Err(SidecarError::InvalidState(format!(
14653 "Unsupported Diffie-Hellman session type: {}",
14654 other.unwrap_or("<missing>")
14655 )));
14656 }
14657 };
14658 process.next_diffie_hellman_session_id += 1;
14659 let session_id = process.next_diffie_hellman_session_id;
14660 process.diffie_hellman_sessions.insert(session_id, session);
14661 Ok(json!(session_id))
14662}
14663
14664fn service_javascript_crypto_diffie_hellman_session_call_sync_rpc(
14665 process: &mut ActiveProcess,
14666 request: &JavascriptSyncRpcRequest,
14667) -> Result<Value, SidecarError> {
14668 let session_id = javascript_sync_rpc_arg_u64(
14669 &request.args,
14670 0,
14671 "crypto.diffieHellmanSessionCall session id",
14672 )?;
14673 let raw =
14674 javascript_sync_rpc_arg_str(&request.args, 1, "crypto.diffieHellmanSessionCall request")?;
14675 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14676 SidecarError::InvalidState(format!(
14677 "crypto.diffieHellmanSessionCall request must be valid JSON: {error}"
14678 ))
14679 })?;
14680 let method = parsed
14681 .get("method")
14682 .and_then(Value::as_str)
14683 .ok_or_else(|| {
14684 SidecarError::InvalidState(String::from(
14685 "crypto.diffieHellmanSessionCall request missing method",
14686 ))
14687 })?;
14688 let args = parsed
14689 .get("args")
14690 .and_then(Value::as_array)
14691 .cloned()
14692 .unwrap_or_default();
14693 let session = process
14694 .diffie_hellman_sessions
14695 .get_mut(&session_id)
14696 .ok_or_else(|| {
14697 SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
14698 })?;
14699 let (result, has_result) = match session {
14700 ActiveDiffieHellmanSession::Dh(session) => {
14701 javascript_crypto_call_dh_session(session, method, &args)?
14702 }
14703 ActiveDiffieHellmanSession::Ecdh(session) => {
14704 javascript_crypto_call_ecdh_session(session, method, &args)?
14705 }
14706 };
14707 Ok(Value::String(
14708 serde_json::to_string(&json!({
14709 "result": result,
14710 "hasResult": has_result,
14711 }))
14712 .map_err(|error| {
14713 SidecarError::InvalidState(format!("serialize diffie session result: {error}"))
14714 })?,
14715 ))
14716}
14717
14718fn service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(
14719 process: &mut ActiveProcess,
14720 request: &JavascriptSyncRpcRequest,
14721) -> Result<Value, SidecarError> {
14722 let session_id = javascript_sync_rpc_arg_u64(
14723 &request.args,
14724 0,
14725 "crypto.diffieHellmanSessionDestroy session id",
14726 )?;
14727 process
14728 .diffie_hellman_sessions
14729 .remove(&session_id)
14730 .ok_or_else(|| {
14731 SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
14732 })?;
14733 Ok(Value::Null)
14734}
14735
14736fn service_javascript_crypto_subtle_sync_rpc(
14737 request: &JavascriptSyncRpcRequest,
14738) -> Result<Value, SidecarError> {
14739 let raw = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.subtle request")?;
14740 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14741 SidecarError::InvalidState(format!("crypto.subtle request must be valid JSON: {error}"))
14742 })?;
14743 let op = parsed.get("op").and_then(Value::as_str).ok_or_else(|| {
14744 SidecarError::InvalidState(String::from("crypto.subtle request missing op"))
14745 })?;
14746 match op {
14747 "digest" => {
14748 let algorithm = parsed
14749 .get("algorithm")
14750 .and_then(Value::as_str)
14751 .ok_or_else(|| {
14752 SidecarError::InvalidState(String::from(
14753 "crypto.subtle.digest missing algorithm",
14754 ))
14755 })?;
14756 let data = parsed.get("data").and_then(Value::as_str).ok_or_else(|| {
14757 SidecarError::InvalidState(String::from("crypto.subtle.digest missing data"))
14758 })?;
14759 let bytes = base64::engine::general_purpose::STANDARD
14760 .decode(data)
14761 .map_err(|error| {
14762 SidecarError::InvalidState(format!("crypto.subtle.digest data base64: {error}"))
14763 })?;
14764 let digest = JavascriptCryptoDigestAlgorithm::parse(algorithm)?.digest(&bytes);
14765 Ok(Value::String(
14766 serde_json::to_string(&json!({
14767 "data": base64::engine::general_purpose::STANDARD.encode(digest),
14768 }))
14769 .map_err(|error| {
14770 SidecarError::InvalidState(format!("serialize crypto.subtle digest: {error}"))
14771 })?,
14772 ))
14773 }
14774 "generateKey" => {
14775 let algorithm = parsed.get("algorithm").ok_or_else(|| {
14776 SidecarError::InvalidState(String::from(
14777 "crypto.subtle.generateKey missing algorithm",
14778 ))
14779 })?;
14780 let name =
14781 javascript_crypto_subtle_algorithm_name(algorithm, "crypto.subtle.generateKey")?;
14782 if !matches!(name, "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW") {
14783 return Err(SidecarError::InvalidState(format!(
14784 "Unsupported key algorithm: {name}"
14785 )));
14786 }
14787 let length_bits = algorithm
14788 .get("length")
14789 .and_then(Value::as_u64)
14790 .ok_or_else(|| {
14791 SidecarError::InvalidState(String::from(
14792 "crypto.subtle.generateKey AES algorithm requires length",
14793 ))
14794 })?;
14795 if length_bits % 8 != 0 {
14796 return Err(SidecarError::InvalidState(String::from(
14797 "crypto.subtle.generateKey length must be byte-aligned",
14798 )));
14799 }
14800 let length_bytes = usize::try_from(length_bits / 8).map_err(|_| {
14801 SidecarError::InvalidState(String::from(
14802 "crypto.subtle.generateKey length is too large",
14803 ))
14804 })?;
14805 let mut raw = vec![0_u8; length_bytes];
14806 rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
14807 let key = javascript_crypto_serialize_subtle_secret_key(
14808 &raw,
14809 javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
14810 parsed
14811 .get("extractable")
14812 .and_then(Value::as_bool)
14813 .unwrap_or(false),
14814 parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
14815 )?;
14816 Ok(Value::String(
14817 serde_json::to_string(&json!({ "key": key })).map_err(|error| {
14818 SidecarError::InvalidState(format!(
14819 "serialize crypto.subtle generated key: {error}"
14820 ))
14821 })?,
14822 ))
14823 }
14824 "importKey" => {
14825 let format = parsed
14826 .get("format")
14827 .and_then(Value::as_str)
14828 .ok_or_else(|| {
14829 SidecarError::InvalidState(String::from(
14830 "crypto.subtle.importKey missing format",
14831 ))
14832 })?;
14833 if format != "raw" {
14834 return Err(SidecarError::InvalidState(format!(
14835 "Unsupported import format: {format}"
14836 )));
14837 }
14838 let key_data = parsed
14839 .get("keyData")
14840 .and_then(Value::as_str)
14841 .ok_or_else(|| {
14842 SidecarError::InvalidState(String::from(
14843 "crypto.subtle.importKey missing keyData",
14844 ))
14845 })?;
14846 let raw = base64::engine::general_purpose::STANDARD
14847 .decode(key_data)
14848 .map_err(|error| {
14849 SidecarError::InvalidState(format!(
14850 "crypto.subtle.importKey keyData base64: {error}"
14851 ))
14852 })?;
14853 let algorithm = parsed.get("algorithm").ok_or_else(|| {
14854 SidecarError::InvalidState(String::from(
14855 "crypto.subtle.importKey missing algorithm",
14856 ))
14857 })?;
14858 let key = javascript_crypto_serialize_subtle_secret_key(
14859 &raw,
14860 javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
14861 parsed
14862 .get("extractable")
14863 .and_then(Value::as_bool)
14864 .unwrap_or(false),
14865 parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
14866 )?;
14867 Ok(Value::String(
14868 serde_json::to_string(&json!({ "key": key })).map_err(|error| {
14869 SidecarError::InvalidState(format!(
14870 "serialize crypto.subtle imported key: {error}"
14871 ))
14872 })?,
14873 ))
14874 }
14875 "exportKey" => {
14876 let format = parsed
14877 .get("format")
14878 .and_then(Value::as_str)
14879 .ok_or_else(|| {
14880 SidecarError::InvalidState(String::from(
14881 "crypto.subtle.exportKey missing format",
14882 ))
14883 })?;
14884 if format != "raw" {
14885 return Err(SidecarError::InvalidState(format!(
14886 "Unsupported export format: {format}"
14887 )));
14888 }
14889 let raw = javascript_crypto_subtle_key_raw(
14890 parsed.get("key").ok_or_else(|| {
14891 SidecarError::InvalidState(String::from("crypto.subtle.exportKey missing key"))
14892 })?,
14893 "crypto.subtle.exportKey key",
14894 )?;
14895 Ok(Value::String(
14896 serde_json::to_string(&json!({
14897 "data": base64::engine::general_purpose::STANDARD.encode(raw),
14898 }))
14899 .map_err(|error| {
14900 SidecarError::InvalidState(format!("serialize crypto.subtle export: {error}"))
14901 })?,
14902 ))
14903 }
14904 "encrypt" | "decrypt" => service_javascript_crypto_subtle_aes_crypt_sync_rpc(op, &parsed),
14905 _ => Err(SidecarError::InvalidState(format!(
14906 "Unsupported subtle operation: {op}"
14907 ))),
14908 }
14909}
14910
14911fn javascript_crypto_subtle_algorithm_name<'a>(
14912 algorithm: &'a Value,
14913 label: &str,
14914) -> Result<&'a str, SidecarError> {
14915 if let Some(name) = algorithm.as_str() {
14916 return Ok(name);
14917 }
14918 algorithm
14919 .get("name")
14920 .and_then(Value::as_str)
14921 .ok_or_else(|| SidecarError::InvalidState(format!("{label} algorithm missing name")))
14922}
14923
14924fn javascript_crypto_normalize_subtle_secret_algorithm(
14925 algorithm: Value,
14926 raw: &[u8],
14927) -> Result<Value, SidecarError> {
14928 let mut object = match algorithm {
14929 Value::String(name) => {
14930 let mut object = Map::new();
14931 object.insert(String::from("name"), Value::String(name));
14932 object
14933 }
14934 Value::Object(object) => object,
14935 _ => {
14936 return Err(SidecarError::InvalidState(String::from(
14937 "crypto.subtle secret algorithm must be a string or object",
14938 )));
14939 }
14940 };
14941 let name = object
14942 .get("name")
14943 .and_then(Value::as_str)
14944 .ok_or_else(|| {
14945 SidecarError::InvalidState(String::from("crypto.subtle secret algorithm missing name"))
14946 })?
14947 .to_string();
14948 if matches!(name.as_str(), "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW")
14949 && !object.contains_key("length")
14950 {
14951 object.insert(String::from("length"), json!(raw.len() * 8));
14952 }
14953 Ok(Value::Object(object))
14954}
14955
14956fn javascript_crypto_serialize_subtle_secret_key(
14957 raw: &[u8],
14958 algorithm: Value,
14959 extractable: bool,
14960 usages: Value,
14961) -> Result<Value, SidecarError> {
14962 let raw_base64 = base64::engine::general_purpose::STANDARD.encode(raw);
14963 let source_key_object_data = javascript_crypto_serialize_sandbox_key_object(
14964 &JavascriptCryptoKeyMaterial::Secret(raw.to_vec()),
14965 )?;
14966 Ok(json!({
14967 "type": "secret",
14968 "algorithm": algorithm,
14969 "extractable": extractable,
14970 "usages": usages,
14971 "_raw": raw_base64,
14972 "_sourceKeyObjectData": source_key_object_data,
14973 }))
14974}
14975
14976fn javascript_crypto_subtle_key_raw(key: &Value, label: &str) -> Result<Vec<u8>, SidecarError> {
14977 let raw = key.get("_raw").and_then(Value::as_str).ok_or_else(|| {
14978 SidecarError::InvalidState(format!("{label} must be a raw secret CryptoKey"))
14979 })?;
14980 base64::engine::general_purpose::STANDARD
14981 .decode(raw)
14982 .map_err(|error| SidecarError::InvalidState(format!("{label} raw base64: {error}")))
14983}
14984
14985fn service_javascript_crypto_subtle_aes_crypt_sync_rpc(
14986 op: &str,
14987 parsed: &Value,
14988) -> Result<Value, SidecarError> {
14989 let algorithm = parsed.get("algorithm").ok_or_else(|| {
14990 SidecarError::InvalidState(format!("crypto.subtle.{op} missing algorithm"))
14991 })?;
14992 let name = javascript_crypto_subtle_algorithm_name(algorithm, &format!("crypto.subtle.{op}"))?;
14993 if name != "AES-GCM" {
14994 return Err(SidecarError::InvalidState(format!(
14995 "Unsupported subtle AES operation algorithm: {name}"
14996 )));
14997 }
14998 let key = javascript_crypto_subtle_key_raw(
14999 parsed
15000 .get("key")
15001 .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing key")))?,
15002 &format!("crypto.subtle.{op} key"),
15003 )?;
15004 let iv = algorithm.get("iv").and_then(Value::as_str).ok_or_else(|| {
15005 SidecarError::InvalidState(format!("crypto.subtle.{op} AES-GCM missing iv"))
15006 })?;
15007 let iv = base64::engine::general_purpose::STANDARD
15008 .decode(iv)
15009 .map_err(|error| {
15010 SidecarError::InvalidState(format!("crypto.subtle.{op} iv base64: {error}"))
15011 })?;
15012 let data = parsed
15013 .get("data")
15014 .and_then(Value::as_str)
15015 .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing data")))?;
15016 let mut data = base64::engine::general_purpose::STANDARD
15017 .decode(data)
15018 .map_err(|error| {
15019 SidecarError::InvalidState(format!("crypto.subtle.{op} data base64: {error}"))
15020 })?;
15021 let tag_len = javascript_crypto_subtle_aes_gcm_tag_len(algorithm)?;
15022 let mut options = Map::new();
15023 options.insert(String::from("authTagLength"), json!(tag_len));
15024 if let Some(additional_data) = algorithm.get("additionalData").and_then(Value::as_str) {
15025 options.insert(
15026 String::from("aad"),
15027 Value::String(additional_data.to_string()),
15028 );
15029 }
15030 let decrypt = op == "decrypt";
15031 if decrypt {
15032 if data.len() < tag_len {
15033 return Err(SidecarError::InvalidState(String::from(
15034 "crypto.subtle.decrypt AES-GCM data shorter than auth tag",
15035 )));
15036 }
15037 let auth_tag = data.split_off(data.len() - tag_len);
15038 options.insert(
15039 String::from("authTag"),
15040 Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
15041 );
15042 }
15043 let cipher_name = format!("aes-{}-gcm", key.len() * 8);
15044 let mut context = javascript_crypto_build_cipher_context(
15045 &cipher_name,
15046 &key,
15047 Some(&iv),
15048 decrypt,
15049 Some(&Value::Object(options)),
15050 )?;
15051 let mut output = javascript_crypto_cipher_update(&mut context, &data)?;
15052 output.extend(javascript_crypto_cipher_finalize(&mut context)?);
15053 if !decrypt {
15054 let mut auth_tag = vec![0_u8; tag_len];
15055 context
15056 .get_tag(&mut auth_tag)
15057 .map_err(javascript_crypto_openssl_error)?;
15058 output.extend(auth_tag);
15059 }
15060 Ok(Value::String(
15061 serde_json::to_string(&json!({
15062 "data": base64::engine::general_purpose::STANDARD.encode(output),
15063 }))
15064 .map_err(|error| {
15065 SidecarError::InvalidState(format!("serialize crypto.subtle {op}: {error}"))
15066 })?,
15067 ))
15068}
15069
15070fn javascript_crypto_subtle_aes_gcm_tag_len(algorithm: &Value) -> Result<usize, SidecarError> {
15071 let tag_bits = algorithm
15072 .get("tagLength")
15073 .and_then(Value::as_u64)
15074 .unwrap_or(128);
15075 if !tag_bits.is_multiple_of(8) {
15076 return Err(SidecarError::InvalidState(String::from(
15077 "crypto.subtle AES-GCM tagLength must be byte-aligned",
15078 )));
15079 }
15080 usize::try_from(tag_bits / 8).map_err(|_| {
15081 SidecarError::InvalidState(String::from("crypto.subtle AES-GCM tagLength too large"))
15082 })
15083}
15084
15085fn service_javascript_crypto_cipheriv_inner(
15086 request: &JavascriptSyncRpcRequest,
15087 decrypt: bool,
15088) -> Result<Value, SidecarError> {
15089 let label = if decrypt {
15090 "crypto.decipheriv"
15091 } else {
15092 "crypto.cipheriv"
15093 };
15094 let algorithm = javascript_sync_rpc_arg_str(&request.args, 0, &format!("{label} algorithm"))?;
15095 let key = javascript_sync_rpc_base64_arg(&request.args, 1, &format!("{label} key"))?;
15096 let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 2, &format!("{label} iv"))?;
15097 let data = javascript_sync_rpc_base64_arg(&request.args, 3, &format!("{label} data"))?;
15098 let options =
15099 javascript_sync_rpc_json_arg_optional(&request.args, 4, &format!("{label} options"))?;
15100 let auth_tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options.as_ref())?;
15101 let mut context = javascript_crypto_build_cipher_context(
15102 algorithm,
15103 &key,
15104 iv.as_deref(),
15105 decrypt,
15106 options.as_ref(),
15107 )?;
15108 let payload = javascript_crypto_cipher_update(&mut context, &data)?;
15109 let final_bytes = javascript_crypto_cipher_finalize(&mut context)?;
15110 if decrypt {
15111 let mut output = payload;
15112 output.extend(final_bytes);
15113 return Ok(Value::String(
15114 base64::engine::general_purpose::STANDARD.encode(output),
15115 ));
15116 }
15117
15118 let mut response = Map::new();
15119 let mut encrypted = payload;
15120 encrypted.extend(final_bytes);
15121 response.insert(
15122 String::from("data"),
15123 Value::String(base64::engine::general_purpose::STANDARD.encode(encrypted)),
15124 );
15125 if javascript_crypto_is_aead(algorithm) {
15126 let mut auth_tag = vec![0_u8; auth_tag_len];
15127 context
15128 .get_tag(&mut auth_tag)
15129 .map_err(javascript_crypto_openssl_error)?;
15130 response.insert(
15131 String::from("authTag"),
15132 Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
15133 );
15134 }
15135 Ok(Value::String(serde_json::to_string(&response).map_err(
15136 |error| SidecarError::InvalidState(format!("serialize {label} response: {error}")),
15137 )?))
15138}
15139
15140fn javascript_sync_rpc_base64_arg_optional(
15141 args: &[Value],
15142 index: usize,
15143 label: &str,
15144) -> Result<Option<Vec<u8>>, SidecarError> {
15145 if args.get(index).is_none() || args[index].is_null() {
15146 return Ok(None);
15147 }
15148 javascript_sync_rpc_base64_arg(args, index, label).map(Some)
15149}
15150
15151fn javascript_sync_rpc_json_arg_optional(
15152 args: &[Value],
15153 index: usize,
15154 label: &str,
15155) -> Result<Option<Value>, SidecarError> {
15156 if args.get(index).is_none() || args[index].is_null() {
15157 return Ok(None);
15158 }
15159 let raw = javascript_sync_rpc_arg_str(args, index, label)?;
15160 serde_json::from_str(raw)
15161 .map(Some)
15162 .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
15163}
15164
15165fn javascript_crypto_parse_direct_key_input(
15166 raw: &str,
15167 expected: Option<&str>,
15168 label: &str,
15169) -> Result<JavascriptDirectKeyInput, SidecarError> {
15170 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
15171 SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
15172 })?;
15173 let padding = match parsed.as_object().and_then(|value| value.get("padding")) {
15174 Some(value) => javascript_crypto_padding_from_value(value)?,
15175 None => None,
15176 };
15177 Ok(JavascriptDirectKeyInput {
15178 key: javascript_crypto_parse_key_material_value(&parsed, expected, label)?,
15179 padding,
15180 })
15181}
15182
15183fn javascript_crypto_parse_key_material_value(
15184 value: &Value,
15185 expected: Option<&str>,
15186 label: &str,
15187) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15188 if let Some(object) = value.as_object() {
15189 if object.get("__type").and_then(Value::as_str) == Some("keyObject") {
15190 let serialized = object.get("value").ok_or_else(|| {
15191 SidecarError::InvalidState(format!("{label} keyObject is missing a value"))
15192 })?;
15193 return javascript_crypto_parse_serialized_key_object(serialized, expected, label);
15194 }
15195 if object.contains_key("type") && (object.contains_key("pem") || object.contains_key("raw"))
15196 {
15197 return javascript_crypto_parse_serialized_key_object(value, expected, label);
15198 }
15199 if let Some(source) = object.get("key") {
15200 return javascript_crypto_parse_key_source(
15201 source,
15202 object.get("format").and_then(Value::as_str),
15203 object.get("type").and_then(Value::as_str),
15204 expected,
15205 label,
15206 );
15207 }
15208 }
15209 javascript_crypto_parse_key_source(value, None, None, expected, label)
15210}
15211
15212fn javascript_crypto_parse_key_source(
15213 source: &Value,
15214 format: Option<&str>,
15215 kind: Option<&str>,
15216 expected: Option<&str>,
15217 label: &str,
15218) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15219 match source {
15220 Value::String(pem) => javascript_crypto_parse_key_from_pem(pem.as_bytes(), expected, label),
15221 Value::Object(object) if object.get("__type").and_then(Value::as_str) == Some("buffer") => {
15222 let data = javascript_crypto_decode_bridge_buffer(source, label)?;
15223 javascript_crypto_parse_key_from_bytes(&data, format, kind, expected, label)
15224 }
15225 Value::Object(_) => {
15226 if format == Some("jwk") {
15227 return Err(SidecarError::InvalidState(format!(
15228 "{label} jwk inputs are not supported yet"
15229 )));
15230 }
15231 Err(SidecarError::InvalidState(format!(
15232 "{label} has an unsupported key shape"
15233 )))
15234 }
15235 _ => Err(SidecarError::InvalidState(format!(
15236 "{label} has an unsupported key value"
15237 ))),
15238 }
15239}
15240
15241fn javascript_crypto_parse_key_from_pem(
15242 pem: &[u8],
15243 expected: Option<&str>,
15244 label: &str,
15245) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15246 match expected {
15247 Some("private") => PKey::private_key_from_pem(pem)
15248 .map(JavascriptCryptoKeyMaterial::Private)
15249 .map_err(|error| {
15250 SidecarError::InvalidState(format!("{label} private key is invalid: {error}"))
15251 }),
15252 Some("public") => PKey::public_key_from_pem(pem)
15253 .map(JavascriptCryptoKeyMaterial::Public)
15254 .map_err(|error| {
15255 SidecarError::InvalidState(format!("{label} public key is invalid: {error}"))
15256 }),
15257 _ => PKey::private_key_from_pem(pem)
15258 .map(JavascriptCryptoKeyMaterial::Private)
15259 .or_else(|_| PKey::public_key_from_pem(pem).map(JavascriptCryptoKeyMaterial::Public))
15260 .map_err(|error| {
15261 SidecarError::InvalidState(format!("{label} PEM key is invalid: {error}"))
15262 }),
15263 }
15264}
15265
15266fn javascript_crypto_parse_key_from_bytes(
15267 der: &[u8],
15268 format: Option<&str>,
15269 kind: Option<&str>,
15270 expected: Option<&str>,
15271 label: &str,
15272) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15273 match (format.unwrap_or("der"), kind.or(expected)) {
15274 ("der", Some("pkcs8")) | ("der", Some("private")) => PKey::private_key_from_der(der)
15275 .map(JavascriptCryptoKeyMaterial::Private)
15276 .map_err(|error| {
15277 SidecarError::InvalidState(format!("{label} private key DER is invalid: {error}"))
15278 }),
15279 ("der", Some("spki")) | ("der", Some("public")) => PKey::public_key_from_der(der)
15280 .map(JavascriptCryptoKeyMaterial::Public)
15281 .map_err(|error| {
15282 SidecarError::InvalidState(format!("{label} public key DER is invalid: {error}"))
15283 }),
15284 _ => Err(SidecarError::InvalidState(format!(
15285 "{label} unsupported key bytes format"
15286 ))),
15287 }
15288}
15289
15290fn javascript_crypto_parse_serialized_key_object(
15291 value: &Value,
15292 expected: Option<&str>,
15293 label: &str,
15294) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15295 let serialized: JavascriptSerializedSandboxKeyObject = serde_json::from_value(value.clone())
15296 .map_err(|error| {
15297 SidecarError::InvalidState(format!("{label} keyObject is invalid: {error}"))
15298 })?;
15299 match serialized.kind.as_str() {
15300 "secret" => {
15301 if expected == Some("public") || expected == Some("private") {
15302 return Err(SidecarError::InvalidState(format!(
15303 "{label} expected an asymmetric key"
15304 )));
15305 }
15306 Ok(JavascriptCryptoKeyMaterial::Secret(
15307 base64::engine::general_purpose::STANDARD
15308 .decode(serialized.raw.unwrap_or_default())
15309 .map_err(|error| {
15310 SidecarError::InvalidState(format!(
15311 "{label} secret key contains invalid base64: {error}"
15312 ))
15313 })?,
15314 ))
15315 }
15316 "private" => {
15317 let pem = serialized.pem.ok_or_else(|| {
15318 SidecarError::InvalidState(format!("{label} private keyObject is missing pem"))
15319 })?;
15320 javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("private"), label)
15321 }
15322 "public" => {
15323 let pem = serialized.pem.ok_or_else(|| {
15324 SidecarError::InvalidState(format!("{label} public keyObject is missing pem"))
15325 })?;
15326 javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("public"), label)
15327 }
15328 other => Err(SidecarError::InvalidState(format!(
15329 "{label} has unsupported keyObject type {other}"
15330 ))),
15331 }
15332}
15333
15334fn javascript_crypto_expect_private_key(
15335 key: JavascriptCryptoKeyMaterial,
15336 label: &str,
15337) -> Result<PKey<Private>, SidecarError> {
15338 match key {
15339 JavascriptCryptoKeyMaterial::Private(key) => Ok(key),
15340 _ => Err(SidecarError::InvalidState(format!(
15341 "{label} requires a private key"
15342 ))),
15343 }
15344}
15345
15346fn javascript_crypto_expect_public_key(
15347 key: JavascriptCryptoKeyMaterial,
15348 label: &str,
15349) -> Result<PKey<Public>, SidecarError> {
15350 match key {
15351 JavascriptCryptoKeyMaterial::Public(key) => Ok(key),
15352 JavascriptCryptoKeyMaterial::Private(key) => {
15353 let pem = key
15354 .public_key_to_pem()
15355 .map_err(javascript_crypto_openssl_error)?;
15356 PKey::public_key_from_pem(&pem).map_err(javascript_crypto_openssl_error)
15357 }
15358 _ => Err(SidecarError::InvalidState(format!(
15359 "{label} requires a public key"
15360 ))),
15361 }
15362}
15363
15364fn javascript_crypto_new_signer<'a>(
15365 algorithm: Option<&'a str>,
15366 key: &'a PKey<Private>,
15367) -> Result<Signer<'a>, SidecarError> {
15368 if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
15369 return Signer::new_without_digest(key).map_err(javascript_crypto_openssl_error);
15370 }
15371 Signer::new(
15372 javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
15373 SidecarError::InvalidState(String::from("crypto.sign requires a digest algorithm"))
15374 })?)?,
15375 key,
15376 )
15377 .map_err(javascript_crypto_openssl_error)
15378}
15379
15380fn javascript_crypto_new_verifier<'a>(
15381 algorithm: Option<&'a str>,
15382 key: &'a PKey<Public>,
15383) -> Result<Verifier<'a>, SidecarError> {
15384 if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
15385 return Verifier::new_without_digest(key).map_err(javascript_crypto_openssl_error);
15386 }
15387 Verifier::new(
15388 javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
15389 SidecarError::InvalidState(String::from("crypto.verify requires a digest algorithm"))
15390 })?)?,
15391 key,
15392 )
15393 .map_err(javascript_crypto_openssl_error)
15394}
15395
15396fn javascript_crypto_message_digest_from_name(name: &str) -> Result<MessageDigest, SidecarError> {
15397 match name.trim().to_ascii_lowercase().replace('-', "").as_str() {
15398 "md5" => Ok(MessageDigest::md5()),
15399 "sha1" => Ok(MessageDigest::sha1()),
15400 "sha256" => Ok(MessageDigest::sha256()),
15401 "sha384" => Ok(MessageDigest::sha384()),
15402 "sha512" => Ok(MessageDigest::sha512()),
15403 other => Err(SidecarError::InvalidState(format!(
15404 "unsupported crypto digest algorithm {other}"
15405 ))),
15406 }
15407}
15408
15409fn javascript_crypto_padding_from_value(value: &Value) -> Result<Option<Padding>, SidecarError> {
15410 let Some(number) = value.as_i64() else {
15411 return Ok(None);
15412 };
15413 let padding = match number {
15414 1 => Padding::PKCS1,
15415 3 => Padding::NONE,
15416 4 => Padding::PKCS1_OAEP,
15417 6 => Padding::PKCS1_PSS,
15418 other => {
15419 return Err(SidecarError::InvalidState(format!(
15420 "unsupported RSA padding constant {other}"
15421 )));
15422 }
15423 };
15424 Ok(Some(padding))
15425}
15426
15427fn javascript_crypto_decode_bridge_buffer(
15428 value: &Value,
15429 label: &str,
15430) -> Result<Vec<u8>, SidecarError> {
15431 let base64_value = value
15432 .as_object()
15433 .filter(|object| object.get("__type").and_then(Value::as_str) == Some("buffer"))
15434 .and_then(|object| object.get("value"))
15435 .and_then(Value::as_str)
15436 .ok_or_else(|| {
15437 SidecarError::InvalidState(format!("{label} must be a serialized bridge buffer"))
15438 })?;
15439 base64::engine::general_purpose::STANDARD
15440 .decode(base64_value)
15441 .map_err(|error| {
15442 SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
15443 })
15444}
15445
15446fn javascript_crypto_serialize_sandbox_key_object(
15447 key: &JavascriptCryptoKeyMaterial,
15448) -> Result<Value, SidecarError> {
15449 let serialized = match key {
15450 JavascriptCryptoKeyMaterial::Private(key) => JavascriptSerializedSandboxKeyObject {
15451 kind: String::from("private"),
15452 pem: Some(
15453 String::from_utf8(
15454 key.private_key_to_pem_pkcs8()
15455 .map_err(javascript_crypto_openssl_error)?,
15456 )
15457 .map_err(|error| {
15458 SidecarError::InvalidState(format!("private key PEM is not utf8: {error}"))
15459 })?,
15460 ),
15461 raw: None,
15462 asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
15463 asymmetric_key_details: None,
15464 jwk: None,
15465 },
15466 JavascriptCryptoKeyMaterial::Public(key) => JavascriptSerializedSandboxKeyObject {
15467 kind: String::from("public"),
15468 pem: Some(
15469 String::from_utf8(
15470 key.public_key_to_pem()
15471 .map_err(javascript_crypto_openssl_error)?,
15472 )
15473 .map_err(|error| {
15474 SidecarError::InvalidState(format!("public key PEM is not utf8: {error}"))
15475 })?,
15476 ),
15477 raw: None,
15478 asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
15479 asymmetric_key_details: None,
15480 jwk: None,
15481 },
15482 JavascriptCryptoKeyMaterial::Secret(raw) => JavascriptSerializedSandboxKeyObject {
15483 kind: String::from("secret"),
15484 pem: None,
15485 raw: Some(base64::engine::general_purpose::STANDARD.encode(raw)),
15486 asymmetric_key_type: None,
15487 asymmetric_key_details: None,
15488 jwk: None,
15489 },
15490 };
15491 serde_json::to_value(serialized)
15492 .map_err(|error| SidecarError::InvalidState(format!("serialize key object: {error}")))
15493}
15494
15495fn javascript_crypto_pkey_type_name(id: PKeyId) -> Option<String> {
15496 match id {
15497 PKeyId::RSA => Some(String::from("rsa")),
15498 PKeyId::EC => Some(String::from("ec")),
15499 PKeyId::ED25519 => Some(String::from("ed25519")),
15500 PKeyId::ED448 => Some(String::from("ed448")),
15501 PKeyId::X25519 => Some(String::from("x25519")),
15502 PKeyId::X448 => Some(String::from("x448")),
15503 PKeyId::DH => Some(String::from("dh")),
15504 _ => None,
15505 }
15506}
15507
15508fn javascript_crypto_rsa_output_size(
15509 key: &JavascriptCryptoKeyMaterial,
15510) -> Result<usize, SidecarError> {
15511 match key {
15512 JavascriptCryptoKeyMaterial::Private(key) => key
15513 .rsa()
15514 .map(|rsa| rsa.size() as usize)
15515 .map_err(javascript_crypto_openssl_error),
15516 JavascriptCryptoKeyMaterial::Public(key) => key
15517 .rsa()
15518 .map(|rsa| rsa.size() as usize)
15519 .map_err(javascript_crypto_openssl_error),
15520 JavascriptCryptoKeyMaterial::Secret(_) => Err(SidecarError::InvalidState(String::from(
15521 "RSA operations require an asymmetric key",
15522 ))),
15523 }
15524}
15525
15526fn javascript_crypto_parse_serialized_options_arg(
15527 args: &[Value],
15528 index: usize,
15529 label: &str,
15530) -> Result<Option<Value>, SidecarError> {
15531 let Some(raw) = args.get(index).and_then(Value::as_str) else {
15532 return Ok(None);
15533 };
15534 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
15535 SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
15536 })?;
15537 if parsed.get("hasOptions").and_then(Value::as_bool) == Some(true) {
15538 Ok(parsed.get("options").cloned())
15539 } else {
15540 Ok(None)
15541 }
15542}
15543
15544fn javascript_crypto_u32_from_bridge_value(
15545 value: &Value,
15546 label: &str,
15547) -> Result<u32, SidecarError> {
15548 if let Some(number) = value.as_u64() {
15549 return u32::try_from(number)
15550 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")));
15551 }
15552 let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
15553 if bytes.len() > 4 {
15554 return Err(SidecarError::InvalidState(format!(
15555 "{label} buffer is too large for u32"
15556 )));
15557 }
15558 Ok(bytes
15559 .into_iter()
15560 .fold(0_u32, |acc, byte| (acc << 8) | u32::from(byte)))
15561}
15562
15563fn javascript_crypto_bignum_from_bridge_value(
15564 value: &Value,
15565 label: &str,
15566) -> Result<BigNum, SidecarError> {
15567 if let Some(object) = value.as_object() {
15568 if object.get("__type").and_then(Value::as_str) == Some("bigint") {
15569 let decimal = object.get("value").and_then(Value::as_str).ok_or_else(|| {
15570 SidecarError::InvalidState(format!("{label} bigint is missing a value"))
15571 })?;
15572 return BigNum::from_dec_str(decimal).map_err(javascript_crypto_openssl_error);
15573 }
15574 }
15575 let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
15576 BigNum::from_slice(&bytes).map_err(javascript_crypto_openssl_error)
15577}
15578
15579fn javascript_crypto_curve_nid(name: &str) -> Result<Nid, SidecarError> {
15580 match name {
15581 "prime256v1" | "P-256" => Ok(Nid::X9_62_PRIME256V1),
15582 "secp384r1" | "P-384" => Ok(Nid::SECP384R1),
15583 "secp521r1" | "P-521" => Ok(Nid::SECP521R1),
15584 "secp256k1" => Ok(Nid::SECP256K1),
15585 other => Err(SidecarError::InvalidState(format!(
15586 "unsupported EC curve {other}"
15587 ))),
15588 }
15589}
15590
15591fn javascript_crypto_named_dh_group(name: &str) -> Result<Dh<Params>, SidecarError> {
15592 match name {
15593 "modp2" => Dh::get_1024_160().map_err(javascript_crypto_openssl_error),
15594 "modp14" | "modp15" | "modp16" | "modp17" | "modp18" => {
15595 Dh::get_2048_256().map_err(javascript_crypto_openssl_error)
15596 }
15597 other => Err(SidecarError::InvalidState(format!(
15598 "unsupported Diffie-Hellman group {other}"
15599 ))),
15600 }
15601}
15602
15603fn javascript_crypto_clone_dh_params(params: &Dh<Params>) -> Result<Dh<Params>, SidecarError> {
15604 Dh::from_pqg(
15605 params
15606 .prime_p()
15607 .to_owned()
15608 .map_err(javascript_crypto_openssl_error)?,
15609 params
15610 .prime_q()
15611 .map(|value| value.to_owned().map_err(javascript_crypto_openssl_error))
15612 .transpose()?,
15613 params
15614 .generator()
15615 .to_owned()
15616 .map_err(javascript_crypto_openssl_error)?,
15617 )
15618 .map_err(javascript_crypto_openssl_error)
15619}
15620
15621fn javascript_crypto_build_dh_params(args: &[Value]) -> Result<Dh<Params>, SidecarError> {
15622 let Some(first) = args.first() else {
15623 return Err(SidecarError::InvalidState(String::from(
15624 "Diffie-Hellman session args are required",
15625 )));
15626 };
15627 if let Some(bits) = first.as_u64() {
15628 let generator = args
15629 .get(1)
15630 .map(|value| javascript_crypto_u32_from_bridge_value(value, "Diffie-Hellman generator"))
15631 .transpose()?
15632 .unwrap_or(2);
15633 return Dh::generate_params(bits as u32, generator)
15634 .map_err(javascript_crypto_openssl_error);
15635 }
15636 let prime = javascript_crypto_bignum_from_bridge_value(first, "Diffie-Hellman prime")?;
15637 let generator = args
15638 .get(1)
15639 .map(|value| javascript_crypto_bignum_from_bridge_value(value, "Diffie-Hellman generator"))
15640 .transpose()?
15641 .unwrap_or(BigNum::from_u32(2).map_err(javascript_crypto_openssl_error)?);
15642 Dh::from_pqg(prime, None, generator).map_err(javascript_crypto_openssl_error)
15643}
15644
15645fn javascript_crypto_call_dh_session(
15646 session: &mut ActiveDhSession,
15647 method: &str,
15648 args: &[Value],
15649) -> Result<(Value, bool), SidecarError> {
15650 match method {
15651 "verifyError" => Ok((Value::Null, false)),
15652 "generateKeys" => {
15653 if session.key_pair.is_none() {
15654 session.key_pair = Some(
15655 javascript_crypto_clone_dh_params(&session.params)?
15656 .generate_key()
15657 .map_err(javascript_crypto_openssl_error)?,
15658 );
15659 }
15660 let public = session
15661 .key_pair
15662 .as_ref()
15663 .expect("dh key pair")
15664 .public_key()
15665 .to_vec();
15666 Ok((javascript_crypto_bridge_buffer_value(&public), true))
15667 }
15668 "computeSecret" => {
15669 if session.key_pair.is_none() {
15670 session.key_pair = Some(
15671 javascript_crypto_clone_dh_params(&session.params)?
15672 .generate_key()
15673 .map_err(javascript_crypto_openssl_error)?,
15674 );
15675 }
15676 let peer = javascript_crypto_bignum_from_bridge_value(
15677 args.first().ok_or_else(|| {
15678 SidecarError::InvalidState(String::from(
15679 "computeSecret requires peer public key",
15680 ))
15681 })?,
15682 "Diffie-Hellman peer public key",
15683 )?;
15684 let secret = session
15685 .key_pair
15686 .as_ref()
15687 .expect("dh key pair")
15688 .compute_key(&peer)
15689 .map_err(javascript_crypto_openssl_error)?;
15690 Ok((javascript_crypto_bridge_buffer_value(&secret), true))
15691 }
15692 "getPrime" => Ok((
15693 javascript_crypto_bridge_buffer_value(&session.params.prime_p().to_vec()),
15694 true,
15695 )),
15696 "getGenerator" => Ok((
15697 javascript_crypto_bridge_buffer_value(&session.params.generator().to_vec()),
15698 true,
15699 )),
15700 "getPublicKey" => {
15701 if session.key_pair.is_none() {
15702 session.key_pair = Some(
15703 javascript_crypto_clone_dh_params(&session.params)?
15704 .generate_key()
15705 .map_err(javascript_crypto_openssl_error)?,
15706 );
15707 }
15708 Ok((
15709 javascript_crypto_bridge_buffer_value(
15710 &session
15711 .key_pair
15712 .as_ref()
15713 .expect("dh key pair")
15714 .public_key()
15715 .to_vec(),
15716 ),
15717 true,
15718 ))
15719 }
15720 "getPrivateKey" => {
15721 if session.key_pair.is_none() {
15722 session.key_pair = Some(
15723 javascript_crypto_clone_dh_params(&session.params)?
15724 .generate_key()
15725 .map_err(javascript_crypto_openssl_error)?,
15726 );
15727 }
15728 Ok((
15729 javascript_crypto_bridge_buffer_value(
15730 &session
15731 .key_pair
15732 .as_ref()
15733 .expect("dh key pair")
15734 .private_key()
15735 .to_vec(),
15736 ),
15737 true,
15738 ))
15739 }
15740 other => Err(SidecarError::InvalidState(format!(
15741 "Unsupported Diffie-Hellman method: {other}"
15742 ))),
15743 }
15744}
15745
15746fn javascript_crypto_call_ecdh_session(
15747 session: &mut ActiveEcdhSession,
15748 method: &str,
15749 args: &[Value],
15750) -> Result<(Value, bool), SidecarError> {
15751 let nid = javascript_crypto_curve_nid(&session.curve)?;
15752 let group = EcGroup::from_curve_name(nid).map_err(javascript_crypto_openssl_error)?;
15753 match method {
15754 "verifyError" => Ok((Value::Null, false)),
15755 "generateKeys" => {
15756 if session.key_pair.is_none() {
15757 session.key_pair =
15758 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15759 }
15760 let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15761 let bytes = session
15762 .key_pair
15763 .as_ref()
15764 .expect("ecdh key pair")
15765 .public_key()
15766 .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
15767 .map_err(javascript_crypto_openssl_error)?;
15768 Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
15769 }
15770 "computeSecret" => {
15771 if session.key_pair.is_none() {
15772 session.key_pair =
15773 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15774 }
15775 let peer_bytes = javascript_crypto_decode_bridge_buffer(
15776 args.first().ok_or_else(|| {
15777 SidecarError::InvalidState(String::from(
15778 "computeSecret requires peer public key",
15779 ))
15780 })?,
15781 "ECDH peer public key",
15782 )?;
15783 let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15784 let peer_point = EcPoint::from_bytes(&group, &peer_bytes, &mut ctx)
15785 .map_err(javascript_crypto_openssl_error)?;
15786 let peer_key = EcKey::from_public_key(&group, &peer_point)
15787 .map_err(javascript_crypto_openssl_error)?;
15788 let private =
15789 PKey::from_ec_key(session.key_pair.as_ref().expect("ecdh key pair").to_owned())
15790 .map_err(javascript_crypto_openssl_error)?;
15791 let peer = PKey::from_ec_key(peer_key).map_err(javascript_crypto_openssl_error)?;
15792 let mut deriver = Deriver::new(&private).map_err(javascript_crypto_openssl_error)?;
15793 deriver
15794 .set_peer(&peer)
15795 .map_err(javascript_crypto_openssl_error)?;
15796 let secret = deriver
15797 .derive_to_vec()
15798 .map_err(javascript_crypto_openssl_error)?;
15799 Ok((javascript_crypto_bridge_buffer_value(&secret), true))
15800 }
15801 "getPublicKey" => {
15802 if session.key_pair.is_none() {
15803 session.key_pair =
15804 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15805 }
15806 let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15807 let bytes = session
15808 .key_pair
15809 .as_ref()
15810 .expect("ecdh key pair")
15811 .public_key()
15812 .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
15813 .map_err(javascript_crypto_openssl_error)?;
15814 Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
15815 }
15816 "getPrivateKey" => {
15817 if session.key_pair.is_none() {
15818 session.key_pair =
15819 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15820 }
15821 Ok((
15822 javascript_crypto_bridge_buffer_value(
15823 &session
15824 .key_pair
15825 .as_ref()
15826 .expect("ecdh key pair")
15827 .private_key()
15828 .to_vec(),
15829 ),
15830 true,
15831 ))
15832 }
15833 other => Err(SidecarError::InvalidState(format!(
15834 "Unsupported Diffie-Hellman method: {other}"
15835 ))),
15836 }
15837}
15838
15839fn javascript_crypto_serialize_encoded_key_value_public(
15840 key: &PKey<Public>,
15841 encoding: Option<&Value>,
15842) -> Result<Value, SidecarError> {
15843 if let Some(encoding) = encoding {
15844 let format = encoding
15845 .get("format")
15846 .and_then(Value::as_str)
15847 .unwrap_or("pem");
15848 return Ok(match format {
15849 "der" => json!({
15850 "kind": "buffer",
15851 "value": base64::engine::general_purpose::STANDARD
15852 .encode(key.public_key_to_der().map_err(javascript_crypto_openssl_error)?),
15853 }),
15854 _ => json!({
15855 "kind": "string",
15856 "value": String::from_utf8(
15857 key.public_key_to_pem().map_err(javascript_crypto_openssl_error)?,
15858 )
15859 .map_err(|error| SidecarError::InvalidState(format!("public key PEM utf8: {error}")))?,
15860 }),
15861 });
15862 }
15863 javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(
15864 key.to_owned(),
15865 ))
15866}
15867
15868fn javascript_crypto_serialize_encoded_key_value_private(
15869 key: &PKey<Private>,
15870 encoding: Option<&Value>,
15871) -> Result<Value, SidecarError> {
15872 if let Some(encoding) = encoding {
15873 let format = encoding
15874 .get("format")
15875 .and_then(Value::as_str)
15876 .unwrap_or("pem");
15877 return Ok(match format {
15878 "der" => json!({
15879 "kind": "buffer",
15880 "value": base64::engine::general_purpose::STANDARD
15881 .encode(key.private_key_to_der().map_err(javascript_crypto_openssl_error)?),
15882 }),
15883 _ => json!({
15884 "kind": "string",
15885 "value": String::from_utf8(
15886 key.private_key_to_pem_pkcs8().map_err(javascript_crypto_openssl_error)?,
15887 )
15888 .map_err(|error| SidecarError::InvalidState(format!("private key PEM utf8: {error}")))?,
15889 }),
15890 });
15891 }
15892 javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(
15893 key.to_owned(),
15894 ))
15895}
15896
15897fn javascript_crypto_bridge_buffer_value(bytes: &[u8]) -> Value {
15898 json!({
15899 "__type": "buffer",
15900 "value": base64::engine::general_purpose::STANDARD.encode(bytes),
15901 })
15902}
15903
15904fn javascript_crypto_build_cipher_context(
15905 algorithm: &str,
15906 key: &[u8],
15907 iv: Option<&[u8]>,
15908 decrypt: bool,
15909 options: Option<&Value>,
15910) -> Result<Crypter, SidecarError> {
15911 let cipher = javascript_crypto_cipher_from_name(algorithm)?;
15912 let mode = if decrypt {
15913 Mode::Decrypt
15914 } else {
15915 Mode::Encrypt
15916 };
15917 let mut context =
15918 Crypter::new(cipher, mode, key, iv).map_err(javascript_crypto_openssl_error)?;
15919 if let Some(auto_padding) = options
15920 .and_then(|value| value.get("autoPadding"))
15921 .and_then(Value::as_bool)
15922 {
15923 context.pad(auto_padding);
15924 }
15925 if javascript_crypto_is_aead(algorithm) {
15926 if let Some(aad) = options
15927 .and_then(|value| value.get("aad"))
15928 .and_then(Value::as_str)
15929 {
15930 context
15931 .aad_update(
15932 &base64::engine::general_purpose::STANDARD
15933 .decode(aad)
15934 .map_err(|error| {
15935 SidecarError::InvalidState(format!(
15936 "cipher aad contains invalid base64: {error}"
15937 ))
15938 })?,
15939 )
15940 .map_err(javascript_crypto_openssl_error)?;
15941 }
15942 if decrypt {
15943 if let Some(auth_tag) = options
15944 .and_then(|value| value.get("authTag"))
15945 .and_then(Value::as_str)
15946 {
15947 let decoded = base64::engine::general_purpose::STANDARD
15948 .decode(auth_tag)
15949 .map_err(|error| {
15950 SidecarError::InvalidState(format!(
15951 "cipher authTag contains invalid base64: {error}"
15952 ))
15953 })?;
15954 context
15955 .set_tag(&decoded)
15956 .map_err(javascript_crypto_openssl_error)?;
15957 }
15958 }
15959 }
15960 Ok(context)
15961}
15962
15963fn javascript_crypto_requested_aead_tag_len(
15964 algorithm: &str,
15965 options: Option<&Value>,
15966) -> Result<usize, SidecarError> {
15967 if !javascript_crypto_is_aead(algorithm) {
15968 return Ok(0);
15969 }
15970 let requested = options
15971 .and_then(|value| value.get("authTagLength"))
15972 .and_then(Value::as_u64)
15973 .unwrap_or(javascript_crypto_aead_tag_len(algorithm) as u64);
15974 usize::try_from(requested).map_err(|_| {
15975 SidecarError::InvalidState(String::from("cipher authTagLength must fit within usize"))
15976 })
15977}
15978
15979fn javascript_crypto_cipher_update(
15980 context: &mut Crypter,
15981 data: &[u8],
15982) -> Result<Vec<u8>, SidecarError> {
15983 let mut output = vec![0_u8; data.len() + 32];
15984 let written = context
15985 .update(data, &mut output)
15986 .map_err(javascript_crypto_openssl_error)?;
15987 output.truncate(written);
15988 Ok(output)
15989}
15990
15991fn javascript_crypto_cipher_finalize(context: &mut Crypter) -> Result<Vec<u8>, SidecarError> {
15992 let mut output = vec![0_u8; 32];
15993 let written = context
15994 .finalize(&mut output)
15995 .map_err(javascript_crypto_openssl_error)?;
15996 output.truncate(written);
15997 Ok(output)
15998}
15999
16000fn javascript_crypto_cipher_from_name(name: &str) -> Result<Cipher, SidecarError> {
16001 match name.to_ascii_lowercase().as_str() {
16002 "aes-128-cbc" => Ok(Cipher::aes_128_cbc()),
16003 "aes-192-cbc" => Ok(Cipher::aes_192_cbc()),
16004 "aes-256-cbc" => Ok(Cipher::aes_256_cbc()),
16005 "aes-128-ctr" => Ok(Cipher::aes_128_ctr()),
16006 "aes-192-ctr" => Ok(Cipher::aes_192_ctr()),
16007 "aes-256-ctr" => Ok(Cipher::aes_256_ctr()),
16008 "aes-128-gcm" => Ok(Cipher::aes_128_gcm()),
16009 "aes-192-gcm" => Ok(Cipher::aes_192_gcm()),
16010 "aes-256-gcm" => Ok(Cipher::aes_256_gcm()),
16011 other => Err(SidecarError::InvalidState(format!(
16012 "unsupported crypto cipher algorithm {other}"
16013 ))),
16014 }
16015}
16016
16017fn javascript_crypto_is_aead(algorithm: &str) -> bool {
16018 algorithm.to_ascii_lowercase().ends_with("-gcm")
16019}
16020
16021fn javascript_crypto_aead_tag_len(_algorithm: &str) -> usize {
16022 16
16023}
16024
16025fn javascript_crypto_openssl_error(error: openssl::error::ErrorStack) -> SidecarError {
16026 SidecarError::Execution(format!("crypto operation failed: {error}"))
16027}
16028
16029fn service_javascript_kernel_stdin_sync_rpc(
16030 kernel: &mut SidecarKernel,
16031 process: &mut ActiveProcess,
16032 request: &JavascriptSyncRpcRequest,
16033) -> Result<Value, SidecarError> {
16034 let max_bytes =
16035 javascript_sync_rpc_arg_u64_optional(&request.args, 0, "__kernel_stdin_read max bytes")?
16036 .map(|value| value.clamp(1, DEFAULT_KERNEL_STDIN_READ_MAX_BYTES as u64) as usize)
16037 .unwrap_or(DEFAULT_KERNEL_STDIN_READ_MAX_BYTES);
16038 let timeout_ms =
16039 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_stdin_read timeout ms")?
16040 .unwrap_or(DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS);
16041
16042 match kernel
16043 .fd_read_with_timeout_result(
16044 EXECUTION_DRIVER_NAME,
16045 process.kernel_pid,
16046 0,
16047 max_bytes,
16048 Some(Duration::from_millis(timeout_ms)),
16049 )
16050 .map_err(kernel_error)
16051 {
16052 Ok(Some(chunk)) if !chunk.is_empty() => Ok(json!({
16053 "dataBase64": base64::engine::general_purpose::STANDARD.encode(chunk),
16054 })),
16055 Ok(Some(_)) => Ok(Value::Null),
16056 Ok(None) => Ok(json!({
16057 "done": true,
16058 })),
16059 Err(SidecarError::Kernel(error)) if error.starts_with("EAGAIN:") => Ok(Value::Null),
16060 Err(error) => Err(error),
16061 }
16062}
16063
16064fn service_javascript_pty_set_raw_mode_sync_rpc(
16065 kernel: &mut SidecarKernel,
16066 process: &mut ActiveProcess,
16067 request: &JavascriptSyncRpcRequest,
16068) -> Result<Value, SidecarError> {
16069 let enabled = javascript_sync_rpc_arg_bool(&request.args, 0, "__pty_set_raw_mode enabled")?;
16070 kernel
16071 .pty_set_discipline(
16072 EXECUTION_DRIVER_NAME,
16073 process.kernel_pid,
16074 0,
16075 LineDisciplineConfig {
16076 canonical: Some(!enabled),
16077 echo: Some(!enabled),
16078 isig: Some(!enabled),
16079 },
16080 )
16081 .map_err(kernel_error)?;
16082 Ok(Value::Null)
16083}
16084
16085fn service_javascript_kernel_stdio_write_sync_rpc(
16086 kernel: &mut SidecarKernel,
16087 process: &mut ActiveProcess,
16088 request: &JavascriptSyncRpcRequest,
16089) -> Result<Value, SidecarError> {
16090 let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_stdio_write fd")?;
16091 let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "__kernel_stdio_write chunk")?;
16092
16093 let written = match fd {
16094 1 => kernel
16095 .write_process_stdout(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
16096 .map_err(kernel_error)?,
16097 2 => kernel
16098 .write_process_stderr(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
16099 .map_err(kernel_error)?,
16100 other => {
16101 return Err(SidecarError::InvalidState(format!(
16102 "__kernel_stdio_write only supports fd 1/2, got {other}"
16103 )));
16104 }
16105 };
16106
16107 let event = if fd == 1 {
16108 ActiveExecutionEvent::Stdout(chunk)
16109 } else {
16110 ActiveExecutionEvent::Stderr(chunk)
16111 };
16112 process.queue_pending_execution_event(event)?;
16113
16114 Ok(json!(written))
16115}
16116
16117fn service_javascript_kernel_poll_sync_rpc(
16118 kernel: &mut SidecarKernel,
16119 process: &ActiveProcess,
16120 request: &JavascriptSyncRpcRequest,
16121) -> Result<Value, SidecarError> {
16122 let fd_requests: Vec<KernelPollFdRequest> = serde_json::from_value(
16123 request
16124 .args
16125 .first()
16126 .cloned()
16127 .unwrap_or_else(|| Value::Array(Vec::new())),
16128 )
16129 .map_err(|error| {
16130 SidecarError::InvalidState(format!(
16131 "__kernel_poll fd list must be a JSON array of {{ fd, events }} objects: {error}"
16132 ))
16133 })?;
16134 let timeout_ms =
16135 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_poll timeout ms")?
16136 .unwrap_or_default();
16137 let timeout_ms = i32::try_from(timeout_ms).map_err(|_| {
16138 SidecarError::InvalidState(String::from("__kernel_poll timeout ms must fit within i32"))
16139 })?;
16140
16141 let poll_fds = fd_requests
16142 .iter()
16143 .map(|entry| PollFd {
16144 fd: entry.fd,
16145 events: PollEvents::from_bits(entry.events),
16146 revents: PollEvents::empty(),
16147 })
16148 .collect::<Vec<_>>();
16149 let result = kernel
16150 .poll_fds(
16151 EXECUTION_DRIVER_NAME,
16152 process.kernel_pid,
16153 poll_fds,
16154 timeout_ms,
16155 )
16156 .map_err(kernel_error)?;
16157
16158 Ok(json!({
16159 "readyCount": result.ready_count,
16160 "fds": result
16161 .fds
16162 .into_iter()
16163 .map(|entry| KernelPollFdResponse {
16164 fd: entry.fd,
16165 events: entry.events.bits(),
16166 revents: entry.revents.bits(),
16167 })
16168 .collect::<Vec<_>>(),
16169 }))
16170}
16171
16172fn install_kernel_stdin_pipe(kernel: &mut SidecarKernel, pid: u32) -> Result<u32, SidecarError> {
16173 let (read_fd, write_fd) = kernel
16174 .open_pipe(EXECUTION_DRIVER_NAME, pid)
16175 .map_err(kernel_error)?;
16176 kernel
16177 .fd_dup2(EXECUTION_DRIVER_NAME, pid, read_fd, 0)
16178 .map_err(kernel_error)?;
16179 kernel
16180 .fd_close(EXECUTION_DRIVER_NAME, pid, read_fd)
16181 .map_err(kernel_error)?;
16182 Ok(write_fd)
16183}
16184
16185fn javascript_child_process_stdin_mode(request: &JavascriptChildProcessSpawnRequest) -> &str {
16186 request
16187 .options
16188 .stdio
16189 .first()
16190 .map(String::as_str)
16191 .unwrap_or("pipe")
16192}
16193
16194pub(crate) fn write_kernel_process_stdin(
16195 kernel: &mut SidecarKernel,
16196 process: &mut ActiveProcess,
16197 chunk: &[u8],
16198) -> Result<(), SidecarError> {
16199 if process.runtime == GuestRuntimeKind::JavaScript {
16200 return Ok(());
16201 }
16202 let Some(writer_fd) = process.kernel_stdin_writer_fd else {
16203 return Ok(());
16204 };
16205 kernel
16206 .fd_write(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd, chunk)
16207 .map(|_| ())
16208 .map_err(kernel_error)
16209}
16210
16211pub(crate) fn close_kernel_process_stdin(
16212 kernel: &mut SidecarKernel,
16213 process: &mut ActiveProcess,
16214) -> Result<(), SidecarError> {
16215 let Some(writer_fd) = process.kernel_stdin_writer_fd.take() else {
16216 return Ok(());
16217 };
16218 kernel
16219 .fd_close(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd)
16220 .map_err(kernel_error)
16221}
16222
16223fn parse_http_header_collection(
16224 headers: &BTreeMap<String, Value>,
16225 label: &str,
16226) -> Result<HttpHeaderCollection, SidecarError> {
16227 let mut normalized = BTreeMap::<String, Vec<String>>::new();
16228 let mut raw_pairs = Vec::new();
16229
16230 for (raw_name, value) in headers {
16231 let normalized_name = raw_name.to_ascii_lowercase();
16232 let values = match value {
16233 Value::String(text) => vec![text.clone()],
16234 Value::Array(values) => values
16235 .iter()
16236 .map(|entry| {
16237 entry.as_str().map(str::to_owned).ok_or_else(|| {
16238 SidecarError::InvalidState(format!(
16239 "{label} header {raw_name} must contain only strings"
16240 ))
16241 })
16242 })
16243 .collect::<Result<Vec<_>, _>>()?,
16244 other => {
16245 return Err(SidecarError::InvalidState(format!(
16246 "{label} header {raw_name} must be a string or string array, received {other}"
16247 )));
16248 }
16249 };
16250 raw_pairs.extend(
16251 values
16252 .iter()
16253 .cloned()
16254 .map(|entry| (raw_name.clone(), entry)),
16255 );
16256 normalized
16257 .entry(normalized_name)
16258 .or_default()
16259 .extend(values);
16260 }
16261
16262 Ok(HttpHeaderCollection {
16263 normalized,
16264 raw_pairs,
16265 })
16266}
16267
16268fn http_headers_json(headers: &HttpHeaderCollection) -> Value {
16269 let map = headers
16270 .normalized
16271 .iter()
16272 .map(|(name, values)| {
16273 let value = if values.len() == 1 {
16274 Value::String(values[0].clone())
16275 } else {
16276 Value::Array(values.iter().cloned().map(Value::String).collect())
16277 };
16278 (name.clone(), value)
16279 })
16280 .collect::<Map<String, Value>>();
16281 Value::Object(map)
16282}
16283
16284fn http_raw_headers_json(headers: &HttpHeaderCollection) -> Value {
16285 Value::Array(
16286 headers
16287 .raw_pairs
16288 .iter()
16289 .flat_map(|(name, value)| [Value::String(name.clone()), Value::String(value.clone())])
16290 .collect(),
16291 )
16292}
16293
16294fn is_loopback_request_host(host: &str) -> bool {
16295 let bare = host
16296 .strip_prefix('[')
16297 .and_then(|value| value.strip_suffix(']'))
16298 .unwrap_or(host);
16299 matches!(bare, "localhost" | "127.0.0.1" | "::1")
16300}
16301
16302fn serialize_http_loopback_request(
16303 url: &Url,
16304 options: &JavascriptHttpRequestOptions,
16305 headers: &HttpHeaderCollection,
16306) -> Result<String, SidecarError> {
16307 let body_base64 = options
16308 .body
16309 .as_ref()
16310 .map(|body| base64::engine::general_purpose::STANDARD.encode(body.as_bytes()));
16311 serde_json::to_string(&json!({
16312 "method": options.method.clone().unwrap_or_else(|| String::from("GET")),
16313 "url": http_request_target(url),
16314 "headers": http_headers_json(headers),
16315 "rawHeaders": http_raw_headers_json(headers),
16316 "bodyBase64": body_base64,
16317 }))
16318 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
16319}
16320
16321fn http_request_target(url: &Url) -> String {
16322 let path = if url.path().is_empty() {
16323 "/"
16324 } else {
16325 url.path()
16326 };
16327 format!(
16328 "{path}{}",
16329 url.query()
16330 .map(|query| format!("?{query}"))
16331 .unwrap_or_default()
16332 )
16333}
16334
16335fn find_kernel_http_listener_process(vm: &VmState, port: u16) -> Option<String> {
16336 vm.active_processes
16337 .iter()
16338 .find_map(|(process_id, process)| {
16339 process.tcp_listeners.values().find_map(|listener| {
16340 let socket_id = listener.kernel_socket_id?;
16341 let record = vm.kernel.socket_get(socket_id)?;
16342 let local_addr = record
16343 .local_address()
16344 .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
16345 .unwrap_or_else(|| listener.guest_local_addr());
16346 if local_addr.port() == port && is_vm_local_http_listener_addr(local_addr.ip()) {
16347 Some(process_id.to_owned())
16348 } else {
16349 None
16350 }
16351 })
16352 })
16353}
16354
16355fn is_vm_local_http_listener_addr(ip: IpAddr) -> bool {
16356 ip.is_loopback() || ip.is_unspecified()
16357}
16358
16359fn serialize_kernel_http_fetch_request(
16360 port: u16,
16361 path: &str,
16362 options: &JavascriptHttpRequestOptions,
16363 headers: &HttpHeaderCollection,
16364) -> Vec<u8> {
16365 let method = options.method.as_deref().unwrap_or("GET");
16366 let mut lines = vec![format!("{method} {path} HTTP/1.1")];
16367 let mut has_host = false;
16368 let mut has_connection = false;
16369 let mut has_content_length = false;
16370 for (name, values) in &headers.normalized {
16371 match name.as_str() {
16372 "host" => has_host = true,
16373 "connection" => has_connection = true,
16374 "content-length" => has_content_length = true,
16375 _ => {}
16376 }
16377 lines.push(format!("{name}: {}", values.join(", ")));
16378 }
16379 if !has_host {
16380 lines.push(format!("Host: 127.0.0.1:{port}"));
16381 }
16382 if !has_connection {
16383 lines.push(String::from("Connection: close"));
16384 }
16385 let body = options.body.as_deref().unwrap_or("").as_bytes();
16386 if !has_content_length && !body.is_empty() {
16387 lines.push(format!("Content-Length: {}", body.len()));
16388 }
16389 lines.push(String::new());
16390 lines.push(String::new());
16391
16392 let mut request = lines.join("\r\n").into_bytes();
16393 request.extend_from_slice(body);
16394 request
16395}
16396
16397fn parse_kernel_http_fetch_response(
16398 buffer: &[u8],
16399 peer_closed: bool,
16400 url: &str,
16401) -> Result<Option<String>, SidecarError> {
16402 let Some(header_end) = find_http_header_end(buffer) else {
16403 return Ok(None);
16404 };
16405 let header_bytes = &buffer[..header_end];
16406 let head = String::from_utf8_lossy(header_bytes);
16407 let mut lines = head.split("\r\n");
16408 let status_line = lines.next().unwrap_or_default();
16409 let mut status_parts = status_line.splitn(3, ' ');
16410 let version = status_parts.next().unwrap_or_default();
16411 if !version.starts_with("HTTP/") {
16412 return Err(SidecarError::Execution(format!(
16413 "invalid vm.fetch HTTP response status line: {status_line}"
16414 )));
16415 }
16416 let status = status_parts
16417 .next()
16418 .ok_or_else(|| {
16419 SidecarError::Execution(format!(
16420 "invalid vm.fetch HTTP response status line: {status_line}"
16421 ))
16422 })?
16423 .parse::<u16>()
16424 .map_err(|error| {
16425 SidecarError::Execution(format!(
16426 "invalid vm.fetch HTTP response status code in {status_line:?}: {error}"
16427 ))
16428 })?;
16429 let status_text = status_parts.next().unwrap_or_default();
16430 let mut headers = Vec::new();
16431 let mut raw_headers = Vec::new();
16432 let mut content_length = None;
16433 let mut transfer_encoding_values = Vec::new();
16434 for line in lines {
16435 if line.is_empty() {
16436 continue;
16437 }
16438 let Some((name, value)) = line.split_once(':') else {
16439 return Err(SidecarError::Execution(format!(
16440 "invalid vm.fetch HTTP response header line: {line}"
16441 )));
16442 };
16443 let value = value.trim().to_owned();
16444 let normalized = name.to_ascii_lowercase();
16445 if normalized == "content-length" {
16446 content_length = Some(value.parse::<usize>().map_err(|error| {
16447 SidecarError::Execution(format!(
16448 "invalid vm.fetch Content-Length header {value:?}: {error}"
16449 ))
16450 })?);
16451 } else if normalized == "transfer-encoding" {
16452 transfer_encoding_values.push(value.clone());
16453 }
16454 headers.push(json!([normalized, value.clone()]));
16455 raw_headers.push(Value::String(name.to_owned()));
16456 raw_headers.push(Value::String(value));
16457 }
16458
16459 let body_start = header_end + 4;
16460 let transfer_encoding = transfer_encoding_tokens(&transfer_encoding_values);
16461 let is_chunked = transfer_encoding.iter().any(|token| token == "chunked");
16462 let body = if is_chunked {
16463 if content_length.is_some() {
16464 return Err(SidecarError::Execution(String::from(
16465 "vm.fetch HTTP response cannot include both Transfer-Encoding: chunked and Content-Length",
16466 )));
16467 }
16468 if transfer_encoding.len() != 1 {
16469 return Err(SidecarError::Execution(format!(
16470 "unsupported vm.fetch Transfer-Encoding: {}",
16471 transfer_encoding.join(", ")
16472 )));
16473 }
16474 let Some(decoded) = decode_kernel_http_chunked_body(&buffer[body_start..])? else {
16475 return Ok(None);
16476 };
16477 decoded
16478 } else if !transfer_encoding.is_empty() {
16479 return Err(SidecarError::Execution(format!(
16480 "unsupported vm.fetch Transfer-Encoding: {}",
16481 transfer_encoding.join(", ")
16482 )));
16483 } else if let Some(content_length) = content_length {
16484 let body_end = body_start.saturating_add(content_length);
16485 if buffer.len() < body_end {
16486 return Ok(None);
16487 }
16488 buffer[body_start..body_end].to_vec()
16489 } else if peer_closed {
16490 buffer[body_start..].to_vec()
16491 } else {
16492 return Ok(None);
16493 };
16494
16495 serde_json::to_string(&json!({
16496 "status": status,
16497 "statusText": status_text,
16498 "headers": headers,
16499 "rawHeaders": raw_headers,
16500 "body": base64::engine::general_purpose::STANDARD.encode(&body),
16501 "bodyEncoding": "base64",
16502 "url": url,
16503 }))
16504 .map(Some)
16505 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
16506}
16507
16508fn find_http_header_end(buffer: &[u8]) -> Option<usize> {
16509 buffer.windows(4).position(|window| window == b"\r\n\r\n")
16510}
16511
16512fn find_crlf(buffer: &[u8], start: usize) -> Option<usize> {
16513 buffer
16514 .get(start..)?
16515 .windows(2)
16516 .position(|window| window == b"\r\n")
16517 .map(|offset| start + offset)
16518}
16519
16520fn transfer_encoding_tokens(values: &[String]) -> Vec<String> {
16521 values
16522 .iter()
16523 .flat_map(|value| value.split(','))
16524 .map(|token| token.trim().to_ascii_lowercase())
16525 .filter(|token| !token.is_empty())
16526 .collect()
16527}
16528
16529fn decode_kernel_http_chunked_body(buffer: &[u8]) -> Result<Option<Vec<u8>>, SidecarError> {
16530 let mut offset = 0;
16531 let mut body = Vec::new();
16532 loop {
16533 let Some(line_end) = find_crlf(buffer, offset) else {
16534 return Ok(None);
16535 };
16536 let size_line = std::str::from_utf8(&buffer[offset..line_end]).map_err(|error| {
16537 SidecarError::Execution(format!(
16538 "invalid vm.fetch chunk size line encoding: {error}"
16539 ))
16540 })?;
16541 let size_part = size_line.split(';').next().unwrap_or_default();
16542 if size_part.is_empty() || !size_part.bytes().all(|byte| byte.is_ascii_hexdigit()) {
16543 return Err(SidecarError::Execution(format!(
16544 "invalid vm.fetch chunk size line: {size_line:?}"
16545 )));
16546 }
16547 let chunk_size = usize::from_str_radix(size_part, 16).map_err(|error| {
16548 SidecarError::Execution(format!(
16549 "invalid vm.fetch chunk size {size_part:?}: {error}"
16550 ))
16551 })?;
16552 let chunk_start = line_end + 2;
16553 let chunk_end = chunk_start
16554 .checked_add(chunk_size)
16555 .ok_or_else(|| SidecarError::Execution(String::from("vm.fetch chunk size overflow")))?;
16556 if chunk_size > 0 {
16557 let chunk_terminator_end = chunk_end.checked_add(2).ok_or_else(|| {
16558 SidecarError::Execution(String::from("vm.fetch chunk terminator overflow"))
16559 })?;
16560 if chunk_terminator_end > buffer.len() {
16561 return Ok(None);
16562 }
16563 if buffer.get(chunk_end..chunk_terminator_end) != Some(b"\r\n") {
16564 return Err(SidecarError::Execution(String::from(
16565 "invalid vm.fetch chunk terminator",
16566 )));
16567 }
16568 body.extend_from_slice(&buffer[chunk_start..chunk_end]);
16569 offset = chunk_terminator_end;
16570 continue;
16571 }
16572
16573 if buffer.get(chunk_start..chunk_start + 2) == Some(b"\r\n") {
16574 return Ok(Some(body));
16575 }
16576 let Some(trailer_end) = find_http_header_end(&buffer[chunk_start..]) else {
16577 return Ok(None);
16578 };
16579 let trailer_bytes = &buffer[chunk_start..chunk_start + trailer_end];
16580 let trailers = String::from_utf8_lossy(trailer_bytes);
16581 for line in trailers.split("\r\n") {
16582 if line.is_empty() {
16583 continue;
16584 }
16585 if line.starts_with(' ') || line.starts_with('\t') || !line.contains(':') {
16586 return Err(SidecarError::Execution(format!(
16587 "invalid vm.fetch chunk trailer line: {line}"
16588 )));
16589 }
16590 }
16591 return Ok(Some(body));
16592 }
16593}
16594
16595fn kernel_http_fetch_target_exit_code(error: &SidecarError) -> Option<i32> {
16596 let SidecarError::Execution(message) = error else {
16597 return None;
16598 };
16599 message
16600 .strip_prefix("vm.fetch target exited before responding (exit code ")?
16601 .strip_suffix(')')?
16602 .parse()
16603 .ok()
16604}
16605
16606fn service_host_fetch_target_event<B>(
16607 bridge: &SharedBridge<B>,
16608 vm_id: &str,
16609 dns: &VmDnsConfig,
16610 socket_paths: &JavascriptSocketPathContext,
16611 kernel: &mut SidecarKernel,
16612 process: &mut ActiveProcess,
16613 resource_limits: &ResourceLimits,
16614 wait: Duration,
16615) -> Result<bool, SidecarError>
16616where
16617 B: NativeSidecarBridge + Send + 'static,
16618 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16619{
16620 let Some(event) = process
16621 .execution
16622 .poll_event_blocking(wait)
16623 .map_err(|error| SidecarError::Execution(error.to_string()))?
16624 else {
16625 return Ok(false);
16626 };
16627
16628 match event {
16629 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
16630 let network_counts = process.network_resource_counts();
16631 let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
16632 bridge,
16633 vm_id,
16634 dns,
16635 socket_paths,
16636 kernel,
16637 process,
16638 sync_request: &request,
16639 resource_limits,
16640 network_counts,
16641 });
16642 match response {
16643 Ok(result) => process
16644 .execution
16645 .respond_javascript_sync_rpc_success(request.id, result)
16646 .or_else(ignore_stale_javascript_sync_rpc_response)?,
16647 Err(error) => process
16648 .execution
16649 .respond_javascript_sync_rpc_error(
16650 request.id,
16651 javascript_sync_rpc_error_code(&error),
16652 error.to_string(),
16653 )
16654 .or_else(ignore_stale_javascript_sync_rpc_response)?,
16655 }
16656 }
16657 ActiveExecutionEvent::Exited(code) => {
16658 return Err(SidecarError::Execution(format!(
16659 "vm.fetch target exited before responding (exit code {code})"
16660 )));
16661 }
16662 other => {
16663 process.queue_pending_execution_event(other)?;
16664 }
16665 }
16666 Ok(true)
16667}
16668
16669fn drain_host_fetch_target_events<B>(
16670 bridge: &SharedBridge<B>,
16671 vm_id: &str,
16672 vm: &mut VmState,
16673 target_process_id: &str,
16674 socket_paths: &JavascriptSocketPathContext,
16675 resource_limits: &ResourceLimits,
16676) -> Result<(), SidecarError>
16677where
16678 B: NativeSidecarBridge + Send + 'static,
16679 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16680{
16681 for _ in 0..32 {
16682 let dns = vm.dns.clone();
16683 let Some(process) = vm.active_processes.get_mut(target_process_id) else {
16684 break;
16685 };
16686 let serviced = service_host_fetch_target_event(
16687 bridge,
16688 vm_id,
16689 &dns,
16690 socket_paths,
16691 &mut vm.kernel,
16692 process,
16693 resource_limits,
16694 Duration::from_millis(1),
16695 )?;
16696 if !serviced {
16697 break;
16698 }
16699 }
16700 Ok(())
16701}
16702
16703fn dispatch_kernel_http_fetch<B>(
16704 bridge: &SharedBridge<B>,
16705 vm_id: &str,
16706 vm: &mut VmState,
16707 target_process_id: &str,
16708 port: u16,
16709 path: &str,
16710 options: &JavascriptHttpRequestOptions,
16711 headers: &HttpHeaderCollection,
16712 max_fetch_response_bytes: usize,
16713) -> Result<String, SidecarError>
16714where
16715 B: NativeSidecarBridge + Send + 'static,
16716 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16717{
16718 let socket_paths = build_javascript_socket_path_context(vm)?;
16719 let family = JavascriptSocketFamily::Ipv4;
16720 let local_port = allocate_guest_listen_port(
16721 0,
16722 family,
16723 &socket_paths.used_tcp_guest_ports,
16724 socket_paths.listen_policy,
16725 )?;
16726 let resource_limits = vm.kernel.resource_limits().clone();
16727 let network_counts = vm_network_resource_counts(vm);
16728 check_network_resource_limit(
16729 resource_limits.max_sockets,
16730 network_counts.sockets,
16731 2,
16732 "socket",
16733 )?;
16734 check_network_resource_limit(
16735 resource_limits.max_connections,
16736 network_counts.connections,
16737 2,
16738 "connection",
16739 )?;
16740
16741 let kernel_pid = vm
16742 .active_processes
16743 .get(target_process_id)
16744 .ok_or_else(|| {
16745 SidecarError::InvalidState(format!(
16746 "vm.fetch target process disappeared: {target_process_id}"
16747 ))
16748 })?
16749 .kernel_pid;
16750 let socket_id = vm
16751 .kernel
16752 .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, SocketSpec::tcp())
16753 .map_err(kernel_error)?;
16754
16755 let result = dispatch_kernel_http_fetch_with_socket(
16756 bridge,
16757 vm_id,
16758 vm,
16759 target_process_id,
16760 kernel_pid,
16761 socket_id,
16762 local_port,
16763 port,
16764 path,
16765 options,
16766 headers,
16767 &socket_paths,
16768 &resource_limits,
16769 max_fetch_response_bytes,
16770 );
16771 let close_result = vm
16772 .kernel
16773 .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
16774 .map_err(kernel_error);
16775 let cleanup_result = if result.is_err() {
16776 drain_host_fetch_target_events(
16777 bridge,
16778 vm_id,
16779 vm,
16780 target_process_id,
16781 &socket_paths,
16782 &resource_limits,
16783 )
16784 } else {
16785 Ok(())
16786 };
16787 match (result, close_result) {
16788 (Ok(response), Ok(())) => cleanup_result.map(|()| response),
16789 (Err(error), _) => Err(error),
16790 (Ok(_), Err(error)) => Err(error),
16791 }
16792}
16793
16794#[allow(clippy::too_many_arguments)]
16795fn dispatch_kernel_http_fetch_with_socket<B>(
16796 bridge: &SharedBridge<B>,
16797 vm_id: &str,
16798 vm: &mut VmState,
16799 target_process_id: &str,
16800 kernel_pid: u32,
16801 socket_id: SocketId,
16802 local_port: u16,
16803 port: u16,
16804 path: &str,
16805 options: &JavascriptHttpRequestOptions,
16806 headers: &HttpHeaderCollection,
16807 socket_paths: &JavascriptSocketPathContext,
16808 resource_limits: &ResourceLimits,
16809 max_fetch_response_bytes: usize,
16810) -> Result<String, SidecarError>
16811where
16812 B: NativeSidecarBridge + Send + 'static,
16813 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16814{
16815 vm.kernel
16816 .socket_bind_inet(
16817 EXECUTION_DRIVER_NAME,
16818 kernel_pid,
16819 socket_id,
16820 InetSocketAddress::new("127.0.0.1", local_port),
16821 )
16822 .map_err(kernel_error)?;
16823 vm.kernel
16824 .socket_connect_inet_loopback(
16825 EXECUTION_DRIVER_NAME,
16826 kernel_pid,
16827 socket_id,
16828 InetSocketAddress::new("127.0.0.1", port),
16829 )
16830 .map_err(kernel_error)?;
16831
16832 let request_bytes = serialize_kernel_http_fetch_request(port, path, options, headers);
16833 vm.kernel
16834 .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, &request_bytes)
16835 .map_err(kernel_error)?;
16836
16837 let mut response_buffer = Vec::new();
16838 let mut peer_closed = false;
16839 let url = format!("http://127.0.0.1:{port}{path}");
16840 let deadline = Instant::now() + http_loopback_request_timeout();
16841 loop {
16842 if let Some(response) =
16843 parse_kernel_http_fetch_response(&response_buffer, peer_closed, &url)?
16844 {
16845 ensure_vm_fetch_response_within_limit(&response, "vm.fetch", max_fetch_response_bytes)?;
16846 return Ok(response);
16847 }
16848 if Instant::now() >= deadline {
16849 let preview = String::from_utf8_lossy(&response_buffer);
16850 return Err(SidecarError::Execution(format!(
16851 "vm.fetch timed out waiting for kernel TCP HTTP response ({} buffered bytes: {:?})",
16852 response_buffer.len(),
16853 preview.chars().take(200).collect::<String>()
16854 )));
16855 }
16856
16857 {
16858 let dns = vm.dns.clone();
16859 let process = vm
16860 .active_processes
16861 .get_mut(target_process_id)
16862 .ok_or_else(|| {
16863 SidecarError::InvalidState(format!(
16864 "vm.fetch target process disappeared: {target_process_id}"
16865 ))
16866 })?;
16867 service_host_fetch_target_event(
16868 bridge,
16869 vm_id,
16870 &dns,
16871 socket_paths,
16872 &mut vm.kernel,
16873 process,
16874 resource_limits,
16875 Duration::from_millis(5),
16876 )?;
16877 }
16878
16879 let poll = vm
16880 .kernel
16881 .poll_targets(
16882 EXECUTION_DRIVER_NAME,
16883 kernel_pid,
16884 vec![PollTargetEntry::socket(
16885 socket_id,
16886 POLLIN | POLLHUP | POLLERR,
16887 )],
16888 5,
16889 )
16890 .map_err(kernel_error)?;
16891 let revents = poll
16892 .targets
16893 .first()
16894 .map(|entry| entry.revents)
16895 .unwrap_or_else(PollEvents::empty);
16896 if revents.intersects(POLLERR) {
16897 return Err(SidecarError::Execution(String::from(
16898 "vm.fetch kernel TCP socket reported POLLERR",
16899 )));
16900 }
16901 if revents.intersects(POLLIN) {
16902 match vm
16903 .kernel
16904 .socket_read(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, 64 * 1024)
16905 {
16906 Ok(Some(bytes)) if !bytes.is_empty() => {
16907 response_buffer.extend(bytes);
16908 ensure_vm_fetch_raw_response_buffer_within_limit(
16909 response_buffer.len(),
16910 "vm.fetch",
16911 )?;
16912 }
16913 Ok(Some(_)) => {}
16914 Ok(None) => peer_closed = true,
16915 Err(error) if error.code() == "EAGAIN" => {}
16916 Err(error) => return Err(kernel_error(error)),
16917 }
16918 }
16919 if revents.intersects(POLLHUP) {
16920 peer_closed = true;
16921 }
16922 }
16923}
16924
16925fn outbound_http_response_json(url: &Url, response: ureq::Response) -> Result<Value, SidecarError> {
16926 let status = response.status();
16927 let status_text = response.status_text().to_owned();
16928 let mut header_pairs = Vec::new();
16929 let mut raw_headers = Vec::new();
16930 for raw_name in response.headers_names() {
16931 for value in response.all(&raw_name) {
16932 header_pairs.push(json!([raw_name.to_ascii_lowercase(), value]));
16933 raw_headers.push(Value::String(raw_name.clone()));
16934 raw_headers.push(Value::String(value.to_owned()));
16935 }
16936 }
16937 let mut reader = response.into_reader();
16938 let mut body = Vec::new();
16939 reader.read_to_end(&mut body).map_err(|error| {
16940 SidecarError::Execution(format!("failed to read HTTP response: {error}"))
16941 })?;
16942 serde_json::to_string(&json!({
16943 "status": status,
16944 "statusText": status_text,
16945 "headers": header_pairs,
16946 "rawHeaders": raw_headers,
16947 "body": base64::engine::general_purpose::STANDARD.encode(body),
16948 "bodyEncoding": "base64",
16949 "url": url.as_str(),
16950 }))
16951 .map(Value::String)
16952 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
16953}
16954
16955fn split_netloc(netloc: &str) -> Option<(&str, u16)> {
16959 let (host, port) = netloc.rsplit_once(':')?;
16960 let port: u16 = port.parse().ok()?;
16961 let host = host
16962 .strip_prefix('[')
16963 .and_then(|rest| rest.strip_suffix(']'))
16964 .unwrap_or(host);
16965 Some((host, port))
16966}
16967
16968fn issue_outbound_http_request(
16969 url: &Url,
16970 options: &JavascriptHttpRequestOptions,
16971 headers: &HttpHeaderCollection,
16972 pinned_addresses: &[IpAddr],
16973) -> Result<Value, SidecarError> {
16974 let method = options.method.as_deref().unwrap_or("GET");
16975 let pinned_host = url.host_str().map(str::to_owned);
16984 let pinned: Vec<IpAddr> = pinned_addresses.to_vec();
16985 let resolver = move |netloc: &str| -> std::io::Result<Vec<SocketAddr>> {
16986 let (host, port) = split_netloc(netloc).ok_or_else(|| {
16987 std::io::Error::new(
16988 std::io::ErrorKind::InvalidInput,
16989 format!("invalid network location: {netloc}"),
16990 )
16991 })?;
16992 let expected_host = pinned_host.as_deref();
16993 if expected_host != Some(host) {
16994 return Err(std::io::Error::new(
16995 std::io::ErrorKind::PermissionDenied,
16996 format!(
16997 "EACCES: outbound HTTP resolver pinned to {expected_host:?}, refusing {host}"
16998 ),
16999 ));
17000 }
17001 if pinned.is_empty() {
17002 return Err(std::io::Error::new(
17003 std::io::ErrorKind::PermissionDenied,
17004 "EACCES: no egress-vetted address available for outbound HTTP request",
17005 ));
17006 }
17007 Ok(pinned.iter().map(|ip| SocketAddr::new(*ip, port)).collect())
17008 };
17009 let mut agent_builder = ureq::AgentBuilder::new()
17010 .resolver(resolver)
17011 .timeout_connect(Duration::from_secs(5))
17012 .timeout_read(Duration::from_secs(15))
17013 .timeout_write(Duration::from_secs(15));
17014 if url.scheme() == "https" {
17015 let tls_options = JavascriptTlsBridgeOptions {
17016 is_server: false,
17017 servername: url.host_str().map(str::to_owned),
17018 alpn_protocols: Some(vec![String::from("http/1.1")]),
17019 reject_unauthorized: options.reject_unauthorized,
17020 ..JavascriptTlsBridgeOptions::default()
17021 };
17022 agent_builder = agent_builder.tls_config(Arc::new(build_client_tls_config(&tls_options)?));
17023 }
17024 let agent = agent_builder.build();
17025 let mut request = agent.request_url(method, url);
17026 for (name, values) in &headers.normalized {
17027 if name == "host" {
17028 continue;
17029 }
17030 let header_value = values.join(", ");
17031 request = request.set(name, &header_value);
17032 }
17033 let response = match options.body.as_deref() {
17034 Some(body) => request.send_string(body),
17035 None => request.call(),
17036 };
17037
17038 match response {
17039 Ok(response) => outbound_http_response_json(url, response),
17040 Err(ureq::Error::Status(_, response)) => outbound_http_response_json(url, response),
17041 Err(ureq::Error::Transport(error)) => Err(SidecarError::Execution(format!(
17042 "ERR_HTTP_REQUEST_FAILED: {error}"
17043 ))),
17044 }
17045}
17046
17047fn wait_for_loopback_http_response<B>(
17048 request: LoopbackHttpResponseWaitRequest<'_, B>,
17049) -> Result<String, SidecarError>
17050where
17051 B: NativeSidecarBridge + Send + 'static,
17052 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17053{
17054 let LoopbackHttpResponseWaitRequest {
17055 bridge,
17056 vm_id,
17057 dns,
17058 socket_paths,
17059 kernel,
17060 process,
17061 resource_limits,
17062 request_key,
17063 } = request;
17064 let deadline = Instant::now() + http_loopback_request_timeout();
17065 loop {
17066 if let Some(response) = process
17067 .pending_http_requests
17068 .get(&request_key)
17069 .and_then(|response| response.clone())
17070 {
17071 process.pending_http_requests.remove(&request_key);
17072 return Ok(response);
17073 }
17074
17075 if Instant::now() >= deadline {
17076 process.pending_http_requests.remove(&request_key);
17077 return Err(SidecarError::Execution(String::from(
17078 "HTTP loopback request timed out waiting for net.http_respond",
17079 )));
17080 }
17081
17082 let Some(event) = process
17083 .execution
17084 .poll_event_blocking(Duration::from_millis(10))
17085 .map_err(|error| SidecarError::Execution(error.to_string()))?
17086 else {
17087 continue;
17088 };
17089
17090 match event {
17091 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
17092 let network_counts = process.network_resource_counts();
17093 let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
17094 bridge,
17095 vm_id,
17096 dns,
17097 socket_paths,
17098 kernel,
17099 process,
17100 sync_request: &request,
17101 resource_limits,
17102 network_counts,
17103 });
17104 match response {
17105 Ok(result) => process
17106 .execution
17107 .respond_javascript_sync_rpc_success(request.id, result)
17108 .or_else(ignore_stale_javascript_sync_rpc_response)?,
17109 Err(error) => process
17110 .execution
17111 .respond_javascript_sync_rpc_error(
17112 request.id,
17113 javascript_sync_rpc_error_code(&error),
17114 error.to_string(),
17115 )
17116 .or_else(ignore_stale_javascript_sync_rpc_response)?,
17117 }
17118 }
17119 ActiveExecutionEvent::Exited(code) => {
17120 process.pending_http_requests.remove(&request_key);
17121 return Err(SidecarError::Execution(format!(
17122 "HTTP loopback server exited before responding (exit code {code})"
17123 )));
17124 }
17125 ActiveExecutionEvent::Stdout(_)
17126 | ActiveExecutionEvent::Stderr(_)
17127 | ActiveExecutionEvent::PythonVfsRpcRequest(_)
17128 | ActiveExecutionEvent::SignalState { .. } => {}
17129 }
17130 }
17131}
17132
17133pub(crate) fn dispatch_loopback_http_request<B>(
17134 request: LoopbackHttpDispatchRequest<'_, B>,
17135) -> Result<String, SidecarError>
17136where
17137 B: NativeSidecarBridge + Send + 'static,
17138 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17139{
17140 let LoopbackHttpDispatchRequest {
17141 bridge,
17142 vm_id,
17143 dns,
17144 socket_paths,
17145 kernel,
17146 process,
17147 resource_limits,
17148 server_id,
17149 request_json,
17150 } = request;
17151 let request_id = {
17152 let server = process.http_servers.get_mut(&server_id).ok_or_else(|| {
17153 SidecarError::InvalidState(format!("HTTP target server disappeared: {server_id}"))
17154 })?;
17155 server.next_request_id += 1;
17156 server.next_request_id
17157 };
17158 process
17159 .pending_http_requests
17160 .insert((server_id, request_id), None);
17161 process.execution.send_javascript_stream_event(
17162 "http_request",
17163 json!({
17164 "serverId": server_id,
17165 "requestId": request_id,
17166 "request": request_json,
17167 }),
17168 )?;
17169 wait_for_loopback_http_response(LoopbackHttpResponseWaitRequest {
17170 bridge,
17171 vm_id,
17172 dns,
17173 socket_paths,
17174 kernel,
17175 process,
17176 resource_limits,
17177 request_key: (server_id, request_id),
17178 })
17179}
17180
17181fn ensure_vm_fetch_response_within_limit(
17182 response_json: &str,
17183 operation: &str,
17184 limit: usize,
17185) -> Result<(), SidecarError> {
17186 let size = response_json.len();
17187 if size > limit {
17188 return Err(SidecarError::Execution(format!(
17189 "{operation} payload is {size} bytes, limit is {limit}"
17190 )));
17191 }
17192 Ok(())
17193}
17194
17195fn ensure_vm_fetch_raw_response_buffer_within_limit(
17196 size: usize,
17197 operation: &str,
17198) -> Result<(), SidecarError> {
17199 if size > VM_FETCH_BUFFER_LIMIT_BYTES {
17200 return Err(SidecarError::Execution(format!(
17201 "{operation} raw response buffer is {size} bytes, limit is {VM_FETCH_BUFFER_LIMIT_BYTES}"
17202 )));
17203 }
17204 Ok(())
17205}
17206
17207pub(crate) fn ensure_vm_fetch_response_frame_within_limit(
17208 response: &ResponseFrame,
17209 max_frame_bytes: usize,
17210) -> Result<(), SidecarError> {
17211 let max_frame_bytes = max_frame_bytes.min(VM_FETCH_BUFFER_LIMIT_BYTES);
17212 let frame = crate::protocol::to_generated_protocol_frame(
17213 &crate::protocol::ProtocolFrame::Response(response.clone()),
17214 )
17215 .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))?;
17216 let WireProtocolFrame::ResponseFrame(_) = &frame else {
17217 return Err(SidecarError::FrameTooLarge(String::from(
17218 "vm fetch response converted to non-response wire frame",
17219 )));
17220 };
17221 WireFrameCodec::new(max_frame_bytes)
17222 .encode(&frame)
17223 .map(|_| ())
17224 .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))
17225}
17226
17227fn service_javascript_dns_sync_rpc<B>(
17228 bridge: &SharedBridge<B>,
17229 kernel: &SidecarKernel,
17230 vm_id: &str,
17231 dns: &VmDnsConfig,
17232 request: &JavascriptSyncRpcRequest,
17233) -> Result<Value, SidecarError>
17234where
17235 B: NativeSidecarBridge + Send + 'static,
17236 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17237{
17238 match request.method.as_str() {
17239 "dns.lookup" => {
17240 let payload = request
17241 .args
17242 .first()
17243 .cloned()
17244 .ok_or_else(|| {
17245 SidecarError::InvalidState(String::from(
17246 "dns.lookup requires a request payload",
17247 ))
17248 })
17249 .and_then(|value| {
17250 serde_json::from_value::<JavascriptDnsLookupRequest>(value).map_err(|error| {
17251 SidecarError::InvalidState(format!("invalid dns.lookup payload: {error}"))
17252 })
17253 })?;
17254 let addresses = filter_dns_ip_addrs(
17255 resolve_dns_ip_addrs(
17256 bridge,
17257 kernel,
17258 vm_id,
17259 dns,
17260 &payload.hostname,
17261 DnsLookupPolicy::CheckPermissions,
17262 )?,
17263 payload.family,
17264 )?;
17265 let addresses = filter_dns_safe_ip_addrs(addresses, &payload.hostname)?;
17266 Ok(Value::Array(
17267 addresses
17268 .into_iter()
17269 .map(|ip| {
17270 json!({
17271 "address": ip.to_string(),
17272 "family": if ip.is_ipv6() { 6 } else { 4 },
17273 })
17274 })
17275 .collect(),
17276 ))
17277 }
17278 "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
17279 let payload = request
17280 .args
17281 .first()
17282 .cloned()
17283 .ok_or_else(|| {
17284 SidecarError::InvalidState(String::from(
17285 "dns.resolve requires a request payload",
17286 ))
17287 })
17288 .and_then(|value| {
17289 serde_json::from_value::<JavascriptDnsResolveRequest>(value).map_err(|error| {
17290 SidecarError::InvalidState(format!("invalid dns.resolve payload: {error}"))
17291 })
17292 })?;
17293 let requested_type = match request.method.as_str() {
17294 "dns.resolve4" => String::from("A"),
17295 "dns.resolve6" => String::from("AAAA"),
17296 _ => payload
17297 .rrtype
17298 .as_deref()
17299 .unwrap_or("A")
17300 .to_ascii_uppercase(),
17301 };
17302 let record_type = parse_dns_record_type(&requested_type)?;
17303 let resolution = resolve_dns_records(
17304 bridge,
17305 kernel,
17306 vm_id,
17307 dns,
17308 &payload.hostname,
17309 record_type,
17310 DnsLookupPolicy::CheckPermissions,
17311 )?;
17312 dns_resolution_to_node_value(&resolution, &requested_type)
17313 }
17314 other => Err(SidecarError::InvalidState(format!(
17315 "unsupported JavaScript dns sync RPC method {other}"
17316 ))),
17317 }
17318}
17319
17320fn service_javascript_dgram_sync_rpc<B>(
17321 request: JavascriptDgramSyncRpcServiceRequest<'_, B>,
17322) -> Result<Value, SidecarError>
17323where
17324 B: NativeSidecarBridge + Send + 'static,
17325 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17326{
17327 let JavascriptDgramSyncRpcServiceRequest {
17328 bridge,
17329 kernel,
17330 vm_id,
17331 dns,
17332 socket_paths,
17333 process,
17334 sync_request: request,
17335 resource_limits,
17336 network_counts,
17337 } = request;
17338 match request.method.as_str() {
17339 "dgram.createSocket" => {
17340 check_network_resource_limit(
17341 resource_limits.max_sockets,
17342 network_counts.sockets,
17343 1,
17344 "socket",
17345 )?;
17346 let payload = request
17347 .args
17348 .first()
17349 .cloned()
17350 .ok_or_else(|| {
17351 SidecarError::InvalidState(String::from(
17352 "dgram.createSocket requires a request payload",
17353 ))
17354 })
17355 .and_then(|value| {
17356 serde_json::from_value::<JavascriptDgramCreateSocketRequest>(value).map_err(
17357 |error| {
17358 SidecarError::InvalidState(format!(
17359 "invalid dgram.createSocket payload: {error}"
17360 ))
17361 },
17362 )
17363 })?;
17364 let family = JavascriptUdpFamily::from_socket_type(&payload.socket_type)?;
17365 let socket_id = process.allocate_udp_socket_id();
17366 process.udp_sockets.insert(
17367 socket_id.clone(),
17368 ActiveUdpSocket::new(kernel, process.kernel_pid, family)?,
17369 );
17370 Ok(json!({
17371 "socketId": socket_id,
17372 "type": family.socket_type(),
17373 }))
17374 }
17375 "dgram.bind" => {
17376 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.bind socket id")?;
17377 let payload = request
17378 .args
17379 .get(1)
17380 .cloned()
17381 .ok_or_else(|| {
17382 SidecarError::InvalidState(String::from(
17383 "dgram.bind requires a request payload",
17384 ))
17385 })
17386 .and_then(|value| {
17387 serde_json::from_value::<JavascriptDgramBindRequest>(value).map_err(|error| {
17388 SidecarError::InvalidState(format!("invalid dgram.bind payload: {error}"))
17389 })
17390 })?;
17391 let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17392 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17393 })?;
17394 let local_addr = socket.bind(
17395 kernel,
17396 process.kernel_pid,
17397 payload.address.as_deref(),
17398 payload.port,
17399 socket_paths,
17400 )?;
17401 Ok(json!({
17402 "localAddress": local_addr.ip().to_string(),
17403 "localPort": local_addr.port(),
17404 "family": socket_addr_family(&local_addr),
17405 }))
17406 }
17407 "dgram.send" => {
17408 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.send socket id")?;
17409 let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "dgram.send payload")?;
17410 let payload = request
17411 .args
17412 .get(2)
17413 .cloned()
17414 .ok_or_else(|| {
17415 SidecarError::InvalidState(String::from(
17416 "dgram.send requires a request payload",
17417 ))
17418 })
17419 .and_then(|value| {
17420 serde_json::from_value::<JavascriptDgramSendRequest>(value).map_err(|error| {
17421 SidecarError::InvalidState(format!("invalid dgram.send payload: {error}"))
17422 })
17423 })?;
17424 let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17425 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17426 })?;
17427 let (written, local_addr) = socket.send_to(ActiveUdpSendToRequest {
17428 bridge,
17429 kernel,
17430 kernel_pid: process.kernel_pid,
17431 vm_id,
17432 dns,
17433 host: payload.address.as_deref().unwrap_or("localhost"),
17434 port: payload.port,
17435 context: socket_paths,
17436 contents: &chunk,
17437 })?;
17438 Ok(json!({
17439 "bytes": written,
17440 "localAddress": local_addr.ip().to_string(),
17441 "localPort": local_addr.port(),
17442 "family": socket_addr_family(&local_addr),
17443 }))
17444 }
17445 "dgram.poll" => {
17446 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.poll socket id")?;
17447 let wait_ms =
17448 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "dgram.poll wait ms")?
17449 .unwrap_or_default();
17450 let event = {
17451 let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17452 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17453 })?;
17454 socket.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?
17455 };
17456
17457 match event {
17458 Some(JavascriptUdpSocketEvent::Message { data, remote_addr }) => {
17459 let family = JavascriptSocketFamily::from_ip(remote_addr.ip());
17460 let guest_remote_port = if is_loopback_ip(remote_addr.ip()) {
17461 socket_paths
17462 .guest_udp_port_for_host_port(family, remote_addr.port())
17463 .unwrap_or(remote_addr.port())
17464 } else {
17465 remote_addr.port()
17466 };
17467 Ok(json!({
17468 "type": "message",
17469 "data": javascript_sync_rpc_bytes_value(&data),
17470 "remoteAddress": remote_addr.ip().to_string(),
17471 "remotePort": guest_remote_port,
17472 "remoteFamily": socket_addr_family(&remote_addr),
17473 }))
17474 }
17475 Some(JavascriptUdpSocketEvent::Error { code, message }) => Ok(json!({
17476 "type": "error",
17477 "code": code,
17478 "message": message,
17479 })),
17480 None => Ok(Value::Null),
17481 }
17482 }
17483 "dgram.close" => {
17484 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.close socket id")?;
17485 let mut socket = process.udp_sockets.remove(socket_id).ok_or_else(|| {
17486 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17487 })?;
17488 socket.close(kernel, process.kernel_pid);
17489 Ok(Value::Null)
17490 }
17491 "dgram.address" => {
17492 let socket_id =
17493 javascript_sync_rpc_arg_str(&request.args, 0, "dgram.address socket id")?;
17494 let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17495 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17496 })?;
17497 let local_addr = socket.local_addr().ok_or_else(|| {
17498 SidecarError::Execution(String::from("EBADF: bad file descriptor"))
17499 })?;
17500 javascript_net_json_string(
17501 json!({
17502 "address": local_addr.ip().to_string(),
17503 "port": local_addr.port(),
17504 "family": socket_addr_family(&local_addr),
17505 }),
17506 "dgram.address",
17507 )
17508 }
17509 "dgram.setBufferSize" => {
17510 let socket_id =
17511 javascript_sync_rpc_arg_str(&request.args, 0, "dgram.setBufferSize socket id")?;
17512 let which =
17513 javascript_sync_rpc_arg_str(&request.args, 1, "dgram.setBufferSize buffer kind")?;
17514 let size = javascript_sync_rpc_arg_u64(&request.args, 2, "dgram.setBufferSize size")?;
17515 let size = usize::try_from(size).map_err(|_| {
17516 SidecarError::InvalidState(String::from(
17517 "dgram.setBufferSize size must fit within usize",
17518 ))
17519 })?;
17520 let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17521 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17522 })?;
17523 socket.set_buffer_size(which, size)?;
17524 Ok(Value::Null)
17525 }
17526 "dgram.getBufferSize" => {
17527 let socket_id =
17528 javascript_sync_rpc_arg_str(&request.args, 0, "dgram.getBufferSize socket id")?;
17529 let which =
17530 javascript_sync_rpc_arg_str(&request.args, 1, "dgram.getBufferSize buffer kind")?;
17531 let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17532 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17533 })?;
17534 let size = socket.get_buffer_size(which)?;
17535 Ok(json!(size))
17536 }
17537 other => Err(SidecarError::InvalidState(format!(
17538 "unsupported JavaScript dgram sync RPC method {other}"
17539 ))),
17540 }
17541}
17542
17543#[derive(Debug)]
17544struct ClientHttp2StreamState {
17545 send_stream: Option<h2::SendStream<Bytes>>,
17546}
17547
17548#[derive(Debug)]
17549struct ServerHttp2StreamState {
17550 send_response: Option<ServerHttp2Responder>,
17551 send_stream: Option<h2::SendStream<Bytes>>,
17552}
17553
17554#[derive(Debug)]
17555enum ServerHttp2Responder {
17556 Regular(server::SendResponse<Bytes>),
17557 Pushed(server::SendPushedResponse<Bytes>),
17558}
17559
17560const HTTP2_DEFAULT_WINDOW_SIZE: u32 = 65_535;
17561const HTTP2_POLL_DELAY: Duration = Duration::from_millis(10);
17562
17563fn http2_runtime_snapshot() -> Http2RuntimeSnapshot {
17564 Http2RuntimeSnapshot {
17565 effective_local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17566 local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17567 remote_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17568 next_stream_id: 1,
17569 outbound_queue_size: 1,
17570 deflate_dynamic_table_size: 0,
17571 inflate_dynamic_table_size: 0,
17572 }
17573}
17574
17575fn http2_snapshot_json(snapshot: &Http2SessionSnapshot) -> Result<String, SidecarError> {
17576 serde_json::to_string(snapshot)
17577 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17578}
17579
17580fn http2_event_value(event: &Http2BridgeEvent) -> Result<Value, SidecarError> {
17581 serde_json::to_string(event)
17582 .map(Value::String)
17583 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17584}
17585
17586fn push_http2_server_event(
17587 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17588 server_id: u64,
17589 event: Http2BridgeEvent,
17590) {
17591 if let Ok(mut state) = shared.lock() {
17592 state
17593 .server_events
17594 .entry(server_id)
17595 .or_default()
17596 .push_back(event);
17597 }
17598}
17599
17600fn push_http2_session_event(
17601 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17602 session_id: u64,
17603 event: Http2BridgeEvent,
17604) {
17605 if let Ok(mut state) = shared.lock() {
17606 state
17607 .session_events
17608 .entry(session_id)
17609 .or_default()
17610 .push_back(event);
17611 }
17612}
17613
17614fn pop_http2_event(
17615 queue: &mut BTreeMap<u64, VecDeque<Http2BridgeEvent>>,
17616 id: u64,
17617) -> Option<Http2BridgeEvent> {
17618 queue.get_mut(&id).and_then(VecDeque::pop_front)
17619}
17620
17621fn wait_for_http2_event(
17622 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17623 id: u64,
17624 is_server: bool,
17625 wait_ms: u64,
17626) -> Option<Http2BridgeEvent> {
17627 let deadline = Instant::now() + Duration::from_millis(wait_ms);
17628 loop {
17629 if let Ok(mut state) = shared.lock() {
17630 let queue = if is_server {
17631 &mut state.server_events
17632 } else {
17633 &mut state.session_events
17634 };
17635 if let Some(event) = pop_http2_event(queue, id) {
17636 return Some(event);
17637 }
17638 }
17639 if wait_ms == 0 || Instant::now() >= deadline {
17640 return None;
17641 }
17642 thread::sleep(HTTP2_POLL_DELAY);
17643 }
17644}
17645
17646fn next_http2_session_id(shared: &mut crate::state::Http2SharedState) -> u64 {
17647 shared.next_session_id += 1;
17648 shared.next_session_id
17649}
17650
17651fn next_http2_stream_id(shared: &mut crate::state::Http2SharedState) -> u64 {
17652 shared.next_stream_id += 1;
17653 shared.next_stream_id
17654}
17655
17656fn http2_reason(code: Option<u32>) -> Reason {
17657 code.unwrap_or(Reason::NO_ERROR.into()).into()
17658}
17659
17660fn http2_error_payload(message: impl Into<String>) -> String {
17661 serde_json::to_string(&json!({
17662 "name": "Error",
17663 "code": "ERR_HTTP2_ERROR",
17664 "message": message.into(),
17665 }))
17666 .unwrap_or_else(|_| {
17667 String::from(
17668 "{\"name\":\"Error\",\"code\":\"ERR_HTTP2_ERROR\",\"message\":\"HTTP/2 bridge error\"}",
17669 )
17670 })
17671}
17672
17673fn http2_socket_snapshot(local_addr: SocketAddr, remote_addr: SocketAddr) -> Http2SocketSnapshot {
17674 Http2SocketSnapshot {
17675 encrypted: false,
17676 allow_half_open: false,
17677 local_address: Some(local_addr.ip().to_string()),
17678 local_port: Some(local_addr.port()),
17679 local_family: Some(socket_addr_family(&local_addr).to_string()),
17680 remote_address: Some(remote_addr.ip().to_string()),
17681 remote_port: Some(remote_addr.port()),
17682 remote_family: Some(socket_addr_family(&remote_addr).to_string()),
17683 servername: None,
17684 alpn_protocol: Some(String::from("h2c")),
17685 }
17686}
17687
17688fn http2_wait_result(kind: &str, id: u64) -> Value {
17689 json!({
17690 "kind": kind,
17691 "id": id,
17692 })
17693}
17694
17695fn is_http2_terminal_event(event: &Http2BridgeEvent, is_server: bool, id: u64) -> bool {
17696 if is_server {
17697 event.kind == "serverClose" && event.id == id
17698 } else {
17699 event.kind == "sessionClose" && event.id == id
17700 }
17701}
17702
17703fn dispatch_http2_wait_loop(
17704 process: &ActiveProcess,
17705 id: u64,
17706 is_server: bool,
17707) -> Result<Value, SidecarError> {
17708 loop {
17709 if let Some(event) = wait_for_http2_event(&process.http2.shared, id, is_server, 50) {
17710 let payload = serde_json::to_value(&event).map_err(|error| {
17711 SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}"))
17712 })?;
17713 process
17714 .execution
17715 .send_javascript_stream_event("http2", payload.clone())?;
17716 if is_http2_terminal_event(&event, is_server, id) {
17717 return Ok(payload);
17718 }
17719 continue;
17720 }
17721
17722 let exists = process
17723 .http2
17724 .shared
17725 .lock()
17726 .map(|state| {
17727 if is_server {
17728 state.servers.contains_key(&id)
17729 } else {
17730 state.sessions.contains_key(&id)
17731 }
17732 })
17733 .unwrap_or(false);
17734 if !exists {
17735 return Ok(if is_server {
17736 http2_wait_result("serverClose", id)
17737 } else {
17738 http2_wait_result("sessionClose", id)
17739 });
17740 }
17741 }
17742}
17743
17744fn dispatch_http_wait_loop(process: &ActiveProcess, server_id: u64) -> Result<Value, SidecarError> {
17745 loop {
17746 if !process.http_servers.contains_key(&server_id) {
17747 return Ok(json!({
17748 "kind": "serverClose",
17749 "id": server_id,
17750 }));
17751 }
17752 thread::sleep(Duration::from_millis(25));
17753 }
17754}
17755
17756fn http2_settings_from_value(settings: &BTreeMap<String, Value>) -> BTreeMap<String, Value> {
17757 settings.clone()
17758}
17759
17760fn parse_http2_headers_json(
17761 headers_json: &str,
17762 label: &str,
17763) -> Result<BTreeMap<String, Value>, SidecarError> {
17764 serde_json::from_str::<BTreeMap<String, Value>>(headers_json)
17765 .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
17766}
17767
17768fn apply_http2_header_values(
17769 header_map: &mut HeaderMap,
17770 name: &str,
17771 value: &Value,
17772) -> Result<(), SidecarError> {
17773 let header_name = HeaderName::from_bytes(name.as_bytes()).map_err(|error| {
17774 SidecarError::InvalidState(format!("invalid HTTP/2 header name {name:?}: {error}"))
17775 })?;
17776 match value {
17777 Value::Array(values) => {
17778 for value in values {
17779 apply_http2_header_values(header_map, name, value)?;
17780 }
17781 }
17782 Value::String(text) => {
17783 let value = HeaderValue::from_str(text).map_err(|error| {
17784 SidecarError::InvalidState(format!(
17785 "invalid HTTP/2 header value for {name}: {error}"
17786 ))
17787 })?;
17788 header_map.append(header_name.clone(), value);
17789 }
17790 Value::Number(number) => {
17791 let value = HeaderValue::from_str(&number.to_string()).map_err(|error| {
17792 SidecarError::InvalidState(format!(
17793 "invalid HTTP/2 numeric header value for {name}: {error}"
17794 ))
17795 })?;
17796 header_map.append(header_name.clone(), value);
17797 }
17798 Value::Bool(boolean) => {
17799 let value = HeaderValue::from_str(if *boolean { "true" } else { "false" }).map_err(
17800 |error| {
17801 SidecarError::InvalidState(format!(
17802 "invalid HTTP/2 boolean header value for {name}: {error}"
17803 ))
17804 },
17805 )?;
17806 header_map.append(header_name.clone(), value);
17807 }
17808 Value::Null => {}
17809 Value::Object(_) => {
17810 return Err(SidecarError::InvalidState(format!(
17811 "unsupported HTTP/2 header object value for {name}"
17812 )));
17813 }
17814 }
17815 Ok(())
17816}
17817
17818fn build_http2_request(headers_json: &str) -> Result<Request<()>, SidecarError> {
17819 let headers = parse_http2_headers_json(headers_json, "HTTP/2 request headers")?;
17820 let method = headers
17821 .get(":method")
17822 .and_then(Value::as_str)
17823 .unwrap_or("GET");
17824 let path = headers.get(":path").and_then(Value::as_str).unwrap_or("/");
17825 let mut builder = Request::builder()
17826 .method(Method::from_bytes(method.as_bytes()).map_err(|error| {
17827 SidecarError::InvalidState(format!("invalid HTTP/2 method {method:?}: {error}"))
17828 })?)
17829 .uri(path.parse::<Uri>().map_err(|error| {
17830 SidecarError::InvalidState(format!("invalid HTTP/2 path {path:?}: {error}"))
17831 })?);
17832 {
17833 let header_map = builder.headers_mut().expect("request header map");
17834 for (name, value) in &headers {
17835 if name.starts_with(':') {
17836 continue;
17837 }
17838 apply_http2_header_values(header_map, name, value)?;
17839 }
17840 }
17841 builder
17842 .body(())
17843 .map_err(|error| SidecarError::InvalidState(format!("invalid HTTP/2 request: {error}")))
17844}
17845
17846fn build_http2_response(headers_json: &str) -> Result<Response<()>, SidecarError> {
17847 let headers = parse_http2_headers_json(headers_json, "HTTP/2 response headers")?;
17848 let status = headers
17849 .get(":status")
17850 .and_then(Value::as_u64)
17851 .or_else(|| {
17852 headers
17853 .get(":status")
17854 .and_then(Value::as_str)
17855 .and_then(|value| value.parse::<u16>().ok().map(u64::from))
17856 })
17857 .unwrap_or(200);
17858 let mut builder = Response::builder().status(status as u16);
17859 {
17860 let header_map = builder.headers_mut().expect("response header map");
17861 for (name, value) in &headers {
17862 if name.starts_with(':') {
17863 continue;
17864 }
17865 apply_http2_header_values(header_map, name, value)?;
17866 }
17867 }
17868 builder.body(()).map_err(|error| {
17869 SidecarError::InvalidState(format!("invalid HTTP/2 response headers: {error}"))
17870 })
17871}
17872
17873fn serialize_http2_headers_map(
17874 pseudo: BTreeMap<String, Value>,
17875 headers: &HeaderMap,
17876) -> Result<String, SidecarError> {
17877 let mut serialized = pseudo;
17878 for (name, value) in headers {
17879 let name = name.as_str().to_string();
17880 let value = Value::String(
17881 value
17882 .to_str()
17883 .map_err(|error| {
17884 SidecarError::Execution(format!("invalid HTTP/2 header value: {error}"))
17885 })?
17886 .to_owned(),
17887 );
17888 match serialized.get_mut(&name) {
17889 Some(Value::Array(values)) => values.push(value),
17890 Some(existing) => {
17891 let first = existing.clone();
17892 *existing = Value::Array(vec![first, value]);
17893 }
17894 None => {
17895 serialized.insert(name, value);
17896 }
17897 }
17898 }
17899 serde_json::to_string(&serialized)
17900 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17901}
17902
17903fn serialize_http2_request_headers(
17904 request: &Request<h2::RecvStream>,
17905) -> Result<String, SidecarError> {
17906 let mut pseudo = BTreeMap::new();
17907 pseudo.insert(
17908 String::from(":method"),
17909 Value::String(request.method().as_str().to_string()),
17910 );
17911 pseudo.insert(
17912 String::from(":path"),
17913 Value::String(
17914 request
17915 .uri()
17916 .path_and_query()
17917 .map(|value| value.as_str().to_string())
17918 .unwrap_or_else(|| String::from("/")),
17919 ),
17920 );
17921 serialize_http2_headers_map(pseudo, request.headers())
17922}
17923
17924fn serialize_http2_response_headers(
17925 response: &Response<h2::RecvStream>,
17926) -> Result<String, SidecarError> {
17927 let mut pseudo = BTreeMap::new();
17928 pseudo.insert(
17929 String::from(":status"),
17930 Value::Number(serde_json::Number::from(response.status().as_u16())),
17931 );
17932 serialize_http2_headers_map(pseudo, response.headers())
17933}
17934
17935fn remove_http2_session_resources(
17936 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17937 session_id: u64,
17938) {
17939 if let Ok(mut state) = shared.lock() {
17940 state.sessions.remove(&session_id);
17941 state.session_events.remove(&session_id);
17942 let stream_ids = state
17943 .streams
17944 .iter()
17945 .filter_map(|(stream_id, stream)| {
17946 (stream.session_id == session_id).then_some(*stream_id)
17947 })
17948 .collect::<Vec<_>>();
17949 for stream_id in stream_ids {
17950 state.streams.remove(&stream_id);
17951 }
17952 }
17953}
17954
17955fn spawn_http2_client_session(
17956 shared: Arc<Mutex<crate::state::Http2SharedState>>,
17957 session_id: u64,
17958 remote_addr: SocketAddr,
17959 tls: Option<JavascriptTlsBridgeOptions>,
17960 snapshot: Arc<Mutex<Http2SessionSnapshot>>,
17961 mut command_rx: UnboundedReceiver<Http2SessionCommand>,
17962) {
17963 thread::spawn(move || {
17964 let runtime = match TokioRuntimeBuilder::new_current_thread()
17965 .enable_all()
17966 .build()
17967 {
17968 Ok(runtime) => runtime,
17969 Err(error) => {
17970 push_http2_session_event(
17971 &shared,
17972 session_id,
17973 Http2BridgeEvent {
17974 kind: String::from("sessionError"),
17975 id: session_id,
17976 data: Some(http2_error_payload(error.to_string())),
17977 ..Http2BridgeEvent::default()
17978 },
17979 );
17980 remove_http2_session_resources(&shared, session_id);
17981 return;
17982 }
17983 };
17984
17985 runtime.block_on(async move {
17986 let stream = match tokio::net::TcpStream::connect(remote_addr).await {
17987 Ok(stream) => stream,
17988 Err(error) => {
17989 push_http2_session_event(
17990 &shared,
17991 session_id,
17992 Http2BridgeEvent {
17993 kind: String::from("sessionError"),
17994 id: session_id,
17995 data: Some(http2_error_payload(error.to_string())),
17996 ..Http2BridgeEvent::default()
17997 },
17998 );
17999 remove_http2_session_resources(&shared, session_id);
18000 return;
18001 }
18002 };
18003
18004 let local_addr = match stream.local_addr() {
18005 Ok(addr) => addr,
18006 Err(error) => {
18007 push_http2_session_event(
18008 &shared,
18009 session_id,
18010 Http2BridgeEvent {
18011 kind: String::from("sessionError"),
18012 id: session_id,
18013 data: Some(http2_error_payload(error.to_string())),
18014 ..Http2BridgeEvent::default()
18015 },
18016 );
18017 remove_http2_session_resources(&shared, session_id);
18018 return;
18019 }
18020 };
18021
18022 {
18023 let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
18024 snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
18025 if let Some(options) = tls.as_ref() {
18026 snapshot_guard.encrypted = true;
18027 snapshot_guard.alpn_protocol = Some(String::from("h2"));
18028 snapshot_guard.socket.encrypted = true;
18029 snapshot_guard.socket.servername = options.servername.clone();
18030 snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
18031 }
18032 snapshot_guard.state = http2_runtime_snapshot();
18033 }
18034 if let Ok(snapshot_json) =
18035 http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
18036 {
18037 push_http2_session_event(
18038 &shared,
18039 session_id,
18040 Http2BridgeEvent {
18041 kind: String::from("sessionConnect"),
18042 id: session_id,
18043 data: Some(snapshot_json),
18044 ..Http2BridgeEvent::default()
18045 },
18046 );
18047 }
18048
18049 let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
18050 let server_name = match ServerName::try_from(
18051 options
18052 .servername
18053 .clone()
18054 .unwrap_or_else(|| String::from("localhost")),
18055 ) {
18056 Ok(server_name) => server_name,
18057 Err(_) => {
18058 push_http2_session_event(
18059 &shared,
18060 session_id,
18061 Http2BridgeEvent {
18062 kind: String::from("sessionError"),
18063 id: session_id,
18064 data: Some(http2_error_payload("invalid TLS servername")),
18065 ..Http2BridgeEvent::default()
18066 },
18067 );
18068 remove_http2_session_resources(&shared, session_id);
18069 return;
18070 }
18071 };
18072 let connector = match build_client_tls_config(options) {
18073 Ok(config) => TlsConnector::from(Arc::new(config)),
18074 Err(error) => {
18075 push_http2_session_event(
18076 &shared,
18077 session_id,
18078 Http2BridgeEvent {
18079 kind: String::from("sessionError"),
18080 id: session_id,
18081 data: Some(http2_error_payload(error.to_string())),
18082 ..Http2BridgeEvent::default()
18083 },
18084 );
18085 remove_http2_session_resources(&shared, session_id);
18086 return;
18087 }
18088 };
18089 match connector.connect(server_name, stream).await {
18090 Ok(tls_stream) => Box::pin(tls_stream),
18091 Err(error) => {
18092 push_http2_session_event(
18093 &shared,
18094 session_id,
18095 Http2BridgeEvent {
18096 kind: String::from("sessionError"),
18097 id: session_id,
18098 data: Some(http2_error_payload(error.to_string())),
18099 ..Http2BridgeEvent::default()
18100 },
18101 );
18102 remove_http2_session_resources(&shared, session_id);
18103 return;
18104 }
18105 }
18106 } else {
18107 Box::pin(stream)
18108 };
18109
18110 let (mut sender, connection) = match client::handshake(io).await {
18111 Ok(parts) => parts,
18112 Err(error) => {
18113 push_http2_session_event(
18114 &shared,
18115 session_id,
18116 Http2BridgeEvent {
18117 kind: String::from("sessionError"),
18118 id: session_id,
18119 data: Some(http2_error_payload(error.to_string())),
18120 ..Http2BridgeEvent::default()
18121 },
18122 );
18123 remove_http2_session_resources(&shared, session_id);
18124 return;
18125 }
18126 };
18127
18128 let (status_tx, mut status_rx) = unbounded_channel::<Result<(), String>>();
18129 tokio::spawn(async move {
18130 let _ = status_tx.send(connection.await.map_err(|error| error.to_string()));
18131 });
18132
18133 let streams: Arc<Mutex<BTreeMap<u64, ClientHttp2StreamState>>> =
18134 Arc::new(Mutex::new(BTreeMap::new()));
18135
18136 loop {
18137 tokio::select! {
18138 Some(result) = status_rx.recv() => {
18139 if let Err(message) = result {
18140 push_http2_session_event(
18141 &shared,
18142 session_id,
18143 Http2BridgeEvent {
18144 kind: String::from("sessionError"),
18145 id: session_id,
18146 data: Some(http2_error_payload(message)),
18147 ..Http2BridgeEvent::default()
18148 },
18149 );
18150 }
18151 push_http2_session_event(
18152 &shared,
18153 session_id,
18154 Http2BridgeEvent {
18155 kind: String::from("sessionClose"),
18156 id: session_id,
18157 ..Http2BridgeEvent::default()
18158 },
18159 );
18160 remove_http2_session_resources(&shared, session_id);
18161 break;
18162 }
18163 Some(command) = command_rx.recv() => {
18164 match command {
18165 Http2SessionCommand::Request { headers_json, options_json, respond_to } => {
18166 let request = match build_http2_request(&headers_json) {
18167 Ok(request) => request,
18168 Err(error) => {
18169 let _ = respond_to.send(Err(error.to_string()));
18170 continue;
18171 }
18172 };
18173 let options: JavascriptHttp2RequestOptions =
18174 serde_json::from_str(&options_json).unwrap_or_default();
18175 let stream_id = {
18176 let mut state = shared.lock().expect("http2 shared state");
18177 let stream_id = next_http2_stream_id(&mut state);
18178 state.streams.insert(
18179 stream_id,
18180 ActiveHttp2Stream {
18181 session_id,
18182 paused: Arc::new(AtomicBool::new(false)),
18183 },
18184 );
18185 stream_id
18186 };
18187 match sender.send_request(request, options.end_stream) {
18188 Ok((response_future, send_stream)) => {
18189 if !options.end_stream {
18190 streams
18191 .lock()
18192 .expect("http2 client streams")
18193 .insert(stream_id, ClientHttp2StreamState { send_stream: Some(send_stream) });
18194 }
18195 let shared_clone = Arc::clone(&shared);
18196 let snapshot_clone = Arc::clone(&snapshot);
18197 tokio::spawn(async move {
18198 match response_future.await {
18199 Ok(response) => {
18200 if let Ok(headers_json) = serialize_http2_response_headers(&response) {
18201 push_http2_session_event(
18202 &shared_clone,
18203 session_id,
18204 Http2BridgeEvent {
18205 kind: String::from("clientResponseHeaders"),
18206 id: stream_id,
18207 data: Some(headers_json),
18208 ..Http2BridgeEvent::default()
18209 },
18210 );
18211 }
18212 let mut body = response.into_body();
18213 while let Some(chunk) = body.data().await {
18214 match chunk {
18215 Ok(bytes) => {
18216 let paused = {
18217 let state = shared_clone.lock().expect("http2 shared state");
18218 state.streams.get(&stream_id).map(|stream| Arc::clone(&stream.paused))
18219 };
18220 if let Some(paused) = paused {
18221 while paused.load(Ordering::SeqCst) {
18222 tokio::time::sleep(HTTP2_POLL_DELAY).await;
18223 }
18224 }
18225 let _ = body.flow_control().release_capacity(bytes.len());
18226 push_http2_session_event(
18227 &shared_clone,
18228 session_id,
18229 Http2BridgeEvent {
18230 kind: String::from("clientData"),
18231 id: stream_id,
18232 data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
18233 ..Http2BridgeEvent::default()
18234 },
18235 );
18236 }
18237 Err(error) => {
18238 push_http2_session_event(
18239 &shared_clone,
18240 session_id,
18241 Http2BridgeEvent {
18242 kind: String::from("clientError"),
18243 id: stream_id,
18244 data: Some(http2_error_payload(error.to_string())),
18245 ..Http2BridgeEvent::default()
18246 },
18247 );
18248 break;
18249 }
18250 }
18251 }
18252 {
18253 let mut snapshot = snapshot_clone.lock().expect("http2 snapshot lock");
18254 snapshot.state.next_stream_id =
18255 snapshot.state.next_stream_id.saturating_add(2);
18256 }
18257 push_http2_session_event(
18258 &shared_clone,
18259 session_id,
18260 Http2BridgeEvent {
18261 kind: String::from("clientEnd"),
18262 id: stream_id,
18263 ..Http2BridgeEvent::default()
18264 },
18265 );
18266 push_http2_session_event(
18267 &shared_clone,
18268 session_id,
18269 Http2BridgeEvent {
18270 kind: String::from("clientClose"),
18271 id: stream_id,
18272 extra_number: Some(0),
18273 ..Http2BridgeEvent::default()
18274 },
18275 );
18276 if let Ok(mut state) = shared_clone.lock() {
18277 state.streams.remove(&stream_id);
18278 }
18279 }
18280 Err(error) => {
18281 push_http2_session_event(
18282 &shared_clone,
18283 session_id,
18284 Http2BridgeEvent {
18285 kind: String::from("clientError"),
18286 id: stream_id,
18287 data: Some(http2_error_payload(error.to_string())),
18288 ..Http2BridgeEvent::default()
18289 },
18290 );
18291 push_http2_session_event(
18292 &shared_clone,
18293 session_id,
18294 Http2BridgeEvent {
18295 kind: String::from("clientClose"),
18296 id: stream_id,
18297 extra_number: Some(u32::from(Reason::INTERNAL_ERROR) as u64),
18298 ..Http2BridgeEvent::default()
18299 },
18300 );
18301 if let Ok(mut state) = shared_clone.lock() {
18302 state.streams.remove(&stream_id);
18303 }
18304 }
18305 }
18306 });
18307 let _ = respond_to.send(Ok(json!(stream_id)));
18308 }
18309 Err(error) => {
18310 if let Ok(mut state) = shared.lock() {
18311 state.streams.remove(&stream_id);
18312 }
18313 let _ = respond_to.send(Err(error.to_string()));
18314 }
18315 }
18316 }
18317 Http2SessionCommand::Settings { settings_json, respond_to } => {
18318 let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
18319 .unwrap_or_default();
18320 {
18321 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18322 snapshot.local_settings = http2_settings_from_value(&settings);
18323 }
18324 if let Ok(headers_json) = serde_json::to_string(&settings) {
18325 push_http2_session_event(
18326 &shared,
18327 session_id,
18328 Http2BridgeEvent {
18329 kind: String::from("sessionLocalSettings"),
18330 id: session_id,
18331 data: Some(headers_json.clone()),
18332 ..Http2BridgeEvent::default()
18333 },
18334 );
18335 push_http2_session_event(
18336 &shared,
18337 session_id,
18338 Http2BridgeEvent {
18339 kind: String::from("sessionSettingsAck"),
18340 id: session_id,
18341 ..Http2BridgeEvent::default()
18342 },
18343 );
18344 }
18345 let _ = respond_to.send(Ok(Value::Null));
18346 }
18347 Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
18348 {
18349 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18350 snapshot.state.local_window_size = size;
18351 snapshot.state.effective_local_window_size = size;
18352 }
18353 let value = snapshot
18354 .lock()
18355 .ok()
18356 .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
18357 .map(Value::String)
18358 .unwrap_or(Value::Null);
18359 let _ = respond_to.send(Ok(value));
18360 }
18361 Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
18362 push_http2_session_event(
18363 &shared,
18364 session_id,
18365 Http2BridgeEvent {
18366 kind: String::from("sessionGoaway"),
18367 id: session_id,
18368 data: opaque_data.map(|value| {
18369 base64::engine::general_purpose::STANDARD.encode(value)
18370 }),
18371 extra_number: Some(error_code as u64),
18372 flags: Some(last_stream_id as u64),
18373 ..Http2BridgeEvent::default()
18374 },
18375 );
18376 let _ = respond_to.send(Ok(Value::Null));
18377 }
18378 Http2SessionCommand::Close { respond_to, .. } => {
18379 let _ = respond_to.send(Ok(Value::Null));
18380 push_http2_session_event(
18381 &shared,
18382 session_id,
18383 Http2BridgeEvent {
18384 kind: String::from("sessionClose"),
18385 id: session_id,
18386 ..Http2BridgeEvent::default()
18387 },
18388 );
18389 remove_http2_session_resources(&shared, session_id);
18390 break;
18391 }
18392 Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
18393 let result = streams
18394 .lock()
18395 .expect("http2 client streams")
18396 .get_mut(&stream_id)
18397 .and_then(|stream| stream.send_stream.as_mut())
18398 .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 client stream {stream_id}")))
18399 .and_then(|stream| stream.send_data(Bytes::from(chunk), end_stream).map_err(|error| SidecarError::Execution(error.to_string())));
18400 match result {
18401 Ok(()) => {
18402 if end_stream {
18403 streams.lock().expect("http2 client streams").remove(&stream_id);
18404 }
18405 let _ = respond_to.send(Ok(Value::Bool(true)));
18406 }
18407 Err(error) => {
18408 let _ = respond_to.send(Err(error.to_string()));
18409 }
18410 }
18411 }
18412 Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
18413 let mut streams = streams.lock().expect("http2 client streams");
18414 let Some(mut state) = streams.remove(&stream_id) else {
18415 let _ = respond_to.send(Err(format!("unknown HTTP/2 client stream {stream_id}")));
18416 continue;
18417 };
18418 if let Some(stream) = state.send_stream.as_mut() {
18419 stream.send_reset(http2_reason(error_code));
18420 }
18421 if let Ok(mut state) = shared.lock() {
18422 state.streams.remove(&stream_id);
18423 }
18424 push_http2_session_event(
18425 &shared,
18426 session_id,
18427 Http2BridgeEvent {
18428 kind: String::from("clientClose"),
18429 id: stream_id,
18430 extra_number: Some(u32::from(http2_reason(error_code)) as u64),
18431 ..Http2BridgeEvent::default()
18432 },
18433 );
18434 let _ = respond_to.send(Ok(Value::Null));
18435 }
18436 Http2SessionCommand::StreamRespond { respond_to, .. }
18437 | Http2SessionCommand::StreamPush { respond_to, .. }
18438 | Http2SessionCommand::StreamRespondWithFile { respond_to, .. } => {
18439 let _ = respond_to.send(Err(String::from("HTTP/2 client streams cannot send server responses")));
18440 }
18441 }
18442 }
18443 else => break,
18444 }
18445 }
18446 });
18447 });
18448}
18449
18450fn spawn_http2_server_session(
18451 shared: Arc<Mutex<crate::state::Http2SharedState>>,
18452 server_id: u64,
18453 session_id: u64,
18454 stream: TcpStream,
18455 tls: Option<JavascriptTlsBridgeOptions>,
18456 snapshot: Arc<Mutex<Http2SessionSnapshot>>,
18457 mut command_rx: UnboundedReceiver<Http2SessionCommand>,
18458) {
18459 thread::spawn(move || {
18460 let runtime = match TokioRuntimeBuilder::new_current_thread()
18461 .enable_all()
18462 .build()
18463 {
18464 Ok(runtime) => runtime,
18465 Err(error) => {
18466 push_http2_server_event(
18467 &shared,
18468 server_id,
18469 Http2BridgeEvent {
18470 kind: String::from("serverStreamError"),
18471 id: session_id,
18472 data: Some(http2_error_payload(error.to_string())),
18473 ..Http2BridgeEvent::default()
18474 },
18475 );
18476 remove_http2_session_resources(&shared, session_id);
18477 return;
18478 }
18479 };
18480
18481 runtime.block_on(async move {
18482 if let Err(error) = stream.set_nonblocking(true) {
18483 push_http2_server_event(
18484 &shared,
18485 server_id,
18486 Http2BridgeEvent {
18487 kind: String::from("serverStreamError"),
18488 id: session_id,
18489 data: Some(http2_error_payload(error.to_string())),
18490 ..Http2BridgeEvent::default()
18491 },
18492 );
18493 remove_http2_session_resources(&shared, session_id);
18494 return;
18495 }
18496 let stream = match tokio::net::TcpStream::from_std(stream) {
18497 Ok(stream) => stream,
18498 Err(error) => {
18499 push_http2_server_event(
18500 &shared,
18501 server_id,
18502 Http2BridgeEvent {
18503 kind: String::from("serverStreamError"),
18504 id: session_id,
18505 data: Some(http2_error_payload(error.to_string())),
18506 ..Http2BridgeEvent::default()
18507 },
18508 );
18509 remove_http2_session_resources(&shared, session_id);
18510 return;
18511 }
18512 };
18513 let local_addr = match stream.local_addr() {
18514 Ok(addr) => addr,
18515 Err(error) => {
18516 push_http2_server_event(
18517 &shared,
18518 server_id,
18519 Http2BridgeEvent {
18520 kind: String::from("serverStreamError"),
18521 id: session_id,
18522 data: Some(http2_error_payload(error.to_string())),
18523 ..Http2BridgeEvent::default()
18524 },
18525 );
18526 remove_http2_session_resources(&shared, session_id);
18527 return;
18528 }
18529 };
18530 let remote_addr = match stream.peer_addr() {
18531 Ok(addr) => addr,
18532 Err(error) => {
18533 push_http2_server_event(
18534 &shared,
18535 server_id,
18536 Http2BridgeEvent {
18537 kind: String::from("serverStreamError"),
18538 id: session_id,
18539 data: Some(http2_error_payload(error.to_string())),
18540 ..Http2BridgeEvent::default()
18541 },
18542 );
18543 remove_http2_session_resources(&shared, session_id);
18544 return;
18545 }
18546 };
18547 {
18548 let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
18549 snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
18550 if tls.is_some() {
18551 snapshot_guard.encrypted = true;
18552 snapshot_guard.alpn_protocol = Some(String::from("h2"));
18553 snapshot_guard.socket.encrypted = true;
18554 snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
18555 }
18556 snapshot_guard.state = http2_runtime_snapshot();
18557 }
18558 if let Ok(snapshot_json) =
18559 http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
18560 {
18561 push_http2_server_event(
18562 &shared,
18563 server_id,
18564 Http2BridgeEvent {
18565 kind: String::from(if tls.is_some() {
18566 "serverSecureConnection"
18567 } else {
18568 "serverConnection"
18569 }),
18570 id: server_id,
18571 data: Some(serde_json::to_string(&http2_socket_snapshot(local_addr, remote_addr)).unwrap_or_default()),
18572 ..Http2BridgeEvent::default()
18573 },
18574 );
18575 push_http2_server_event(
18576 &shared,
18577 server_id,
18578 Http2BridgeEvent {
18579 kind: String::from("serverSession"),
18580 id: server_id,
18581 data: Some(snapshot_json),
18582 extra_number: Some(session_id),
18583 ..Http2BridgeEvent::default()
18584 },
18585 );
18586 }
18587
18588 let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
18589 let acceptor = match build_server_tls_config(options) {
18590 Ok(config) => TlsAcceptor::from(Arc::new(config)),
18591 Err(error) => {
18592 push_http2_server_event(
18593 &shared,
18594 server_id,
18595 Http2BridgeEvent {
18596 kind: String::from("serverStreamError"),
18597 id: session_id,
18598 data: Some(http2_error_payload(error.to_string())),
18599 ..Http2BridgeEvent::default()
18600 },
18601 );
18602 remove_http2_session_resources(&shared, session_id);
18603 return;
18604 }
18605 };
18606 match acceptor.accept(stream).await {
18607 Ok(tls_stream) => Box::pin(tls_stream),
18608 Err(error) => {
18609 push_http2_server_event(
18610 &shared,
18611 server_id,
18612 Http2BridgeEvent {
18613 kind: String::from("serverStreamError"),
18614 id: session_id,
18615 data: Some(http2_error_payload(error.to_string())),
18616 ..Http2BridgeEvent::default()
18617 },
18618 );
18619 remove_http2_session_resources(&shared, session_id);
18620 return;
18621 }
18622 }
18623 } else {
18624 Box::pin(stream)
18625 };
18626
18627 let mut connection = match server::handshake(io).await {
18628 Ok(connection) => connection,
18629 Err(error) => {
18630 push_http2_server_event(
18631 &shared,
18632 server_id,
18633 Http2BridgeEvent {
18634 kind: String::from("serverStreamError"),
18635 id: session_id,
18636 data: Some(http2_error_payload(error.to_string())),
18637 ..Http2BridgeEvent::default()
18638 },
18639 );
18640 remove_http2_session_resources(&shared, session_id);
18641 return;
18642 }
18643 };
18644
18645 let streams: Arc<Mutex<BTreeMap<u64, ServerHttp2StreamState>>> =
18646 Arc::new(Mutex::new(BTreeMap::new()));
18647
18648 loop {
18649 tokio::select! {
18650 incoming = connection.accept() => {
18651 match incoming {
18652 Some(Ok((request, respond))) => {
18653 let headers_json = match serialize_http2_request_headers(&request) {
18654 Ok(headers) => headers,
18655 Err(error) => {
18656 push_http2_server_event(
18657 &shared,
18658 server_id,
18659 Http2BridgeEvent {
18660 kind: String::from("serverStreamError"),
18661 id: server_id,
18662 data: Some(http2_error_payload(error.to_string())),
18663 ..Http2BridgeEvent::default()
18664 },
18665 );
18666 continue;
18667 }
18668 };
18669 let stream_id = {
18670 let mut state = shared.lock().expect("http2 shared state");
18671 let stream_id = next_http2_stream_id(&mut state);
18672 state.streams.insert(
18673 stream_id,
18674 ActiveHttp2Stream {
18675 session_id,
18676 paused: Arc::new(AtomicBool::new(false)),
18677 },
18678 );
18679 stream_id
18680 };
18681 streams.lock().expect("http2 server streams").insert(
18682 stream_id,
18683 ServerHttp2StreamState {
18684 send_response: Some(ServerHttp2Responder::Regular(respond)),
18685 send_stream: None,
18686 },
18687 );
18688 let snapshot_json = snapshot
18689 .lock()
18690 .ok()
18691 .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok());
18692 push_http2_server_event(
18693 &shared,
18694 server_id,
18695 Http2BridgeEvent {
18696 kind: String::from("serverStream"),
18697 id: server_id,
18698 data: Some(stream_id.to_string()),
18699 extra: snapshot_json,
18700 extra_number: Some(session_id),
18701 extra_headers: Some(headers_json),
18702 flags: Some(0),
18703 },
18704 );
18705 let shared_clone = Arc::clone(&shared);
18706 tokio::spawn(async move {
18707 let mut body = request.into_body();
18708 while let Some(chunk) = body.data().await {
18709 match chunk {
18710 Ok(bytes) => {
18711 let paused = {
18712 let state = shared_clone.lock().expect("http2 shared state");
18713 state.streams.get(&stream_id).map(|stream| Arc::clone(&stream.paused))
18714 };
18715 if let Some(paused) = paused {
18716 while paused.load(Ordering::SeqCst) {
18717 tokio::time::sleep(HTTP2_POLL_DELAY).await;
18718 }
18719 }
18720 let _ = body.flow_control().release_capacity(bytes.len());
18721 push_http2_server_event(
18722 &shared_clone,
18723 server_id,
18724 Http2BridgeEvent {
18725 kind: String::from("serverStreamData"),
18726 id: stream_id,
18727 data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
18728 ..Http2BridgeEvent::default()
18729 },
18730 );
18731 }
18732 Err(error) => {
18733 push_http2_server_event(
18734 &shared_clone,
18735 server_id,
18736 Http2BridgeEvent {
18737 kind: String::from("serverStreamError"),
18738 id: stream_id,
18739 data: Some(http2_error_payload(error.to_string())),
18740 ..Http2BridgeEvent::default()
18741 },
18742 );
18743 break;
18744 }
18745 }
18746 }
18747 push_http2_server_event(
18748 &shared_clone,
18749 server_id,
18750 Http2BridgeEvent {
18751 kind: String::from("serverStreamEnd"),
18752 id: stream_id,
18753 ..Http2BridgeEvent::default()
18754 },
18755 );
18756 });
18757 }
18758 Some(Err(error)) => {
18759 push_http2_server_event(
18760 &shared,
18761 server_id,
18762 Http2BridgeEvent {
18763 kind: String::from("serverStreamError"),
18764 id: server_id,
18765 data: Some(http2_error_payload(error.to_string())),
18766 ..Http2BridgeEvent::default()
18767 },
18768 );
18769 break;
18770 }
18771 None => {
18772 push_http2_server_event(
18773 &shared,
18774 server_id,
18775 Http2BridgeEvent {
18776 kind: String::from("sessionClose"),
18777 id: session_id,
18778 ..Http2BridgeEvent::default()
18779 },
18780 );
18781 remove_http2_session_resources(&shared, session_id);
18782 break;
18783 }
18784 }
18785 }
18786 Some(command) = command_rx.recv() => {
18787 match command {
18788 Http2SessionCommand::Settings { settings_json, respond_to } => {
18789 let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
18790 .unwrap_or_default();
18791 if let Some(initial_window_size) = settings
18792 .get("initialWindowSize")
18793 .and_then(Value::as_u64)
18794 {
18795 let _ = connection.set_initial_window_size(initial_window_size as u32);
18796 }
18797 {
18798 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18799 snapshot.local_settings = http2_settings_from_value(&settings);
18800 }
18801 if let Ok(headers_json) = serde_json::to_string(&settings) {
18802 push_http2_session_event(
18803 &shared,
18804 session_id,
18805 Http2BridgeEvent {
18806 kind: String::from("sessionLocalSettings"),
18807 id: session_id,
18808 data: Some(headers_json),
18809 ..Http2BridgeEvent::default()
18810 },
18811 );
18812 }
18813 let _ = respond_to.send(Ok(Value::Null));
18814 }
18815 Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
18816 connection.set_target_window_size(size);
18817 {
18818 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18819 snapshot.state.local_window_size = size;
18820 snapshot.state.effective_local_window_size = size;
18821 }
18822 let value = snapshot
18823 .lock()
18824 .ok()
18825 .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
18826 .map(Value::String)
18827 .unwrap_or(Value::Null);
18828 let _ = respond_to.send(Ok(value));
18829 }
18830 Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
18831 connection.abrupt_shutdown(http2_reason(Some(error_code)));
18832 push_http2_session_event(
18833 &shared,
18834 session_id,
18835 Http2BridgeEvent {
18836 kind: String::from("sessionGoaway"),
18837 id: session_id,
18838 data: opaque_data.map(|value| {
18839 base64::engine::general_purpose::STANDARD.encode(value)
18840 }),
18841 extra_number: Some(error_code as u64),
18842 flags: Some(last_stream_id as u64),
18843 ..Http2BridgeEvent::default()
18844 },
18845 );
18846 let _ = respond_to.send(Ok(Value::Null));
18847 }
18848 Http2SessionCommand::Close { abrupt, respond_to } => {
18849 if abrupt {
18850 connection.abrupt_shutdown(Reason::NO_ERROR);
18851 } else {
18852 connection.graceful_shutdown();
18853 }
18854 let _ = respond_to.send(Ok(Value::Null));
18855 push_http2_session_event(
18856 &shared,
18857 session_id,
18858 Http2BridgeEvent {
18859 kind: String::from("sessionClose"),
18860 id: session_id,
18861 ..Http2BridgeEvent::default()
18862 },
18863 );
18864 remove_http2_session_resources(&shared, session_id);
18865 break;
18866 }
18867 Http2SessionCommand::StreamRespond { stream_id, headers_json, respond_to } => {
18868 let response = match build_http2_response(&headers_json) {
18869 Ok(response) => response,
18870 Err(error) => {
18871 let _ = respond_to.send(Err(error.to_string()));
18872 continue;
18873 }
18874 };
18875 let mut streams = streams.lock().expect("http2 server streams");
18876 let Some(state) = streams.get_mut(&stream_id) else {
18877 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18878 continue;
18879 };
18880 let Some(send_response) = state.send_response.as_mut() else {
18881 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
18882 continue;
18883 };
18884 match match send_response {
18885 ServerHttp2Responder::Regular(send_response) => {
18886 send_response.send_response(response, false)
18887 }
18888 ServerHttp2Responder::Pushed(send_response) => {
18889 send_response.send_response(response, false)
18890 }
18891 } {
18892 Ok(send_stream) => {
18893 state.send_stream = Some(send_stream);
18894 state.send_response = None;
18895 let _ = respond_to.send(Ok(Value::Null));
18896 }
18897 Err(error) => {
18898 let _ = respond_to.send(Err(error.to_string()));
18899 }
18900 }
18901 }
18902 Http2SessionCommand::StreamPush { stream_id, headers_json, respond_to } => {
18903 let request = match build_http2_request(&headers_json) {
18904 Ok(request) => request,
18905 Err(error) => {
18906 let _ = respond_to.send(Err(error.to_string()));
18907 continue;
18908 }
18909 };
18910 let mut streams_guard = streams.lock().expect("http2 server streams");
18911 let Some(state) = streams_guard.get_mut(&stream_id) else {
18912 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18913 continue;
18914 };
18915 let Some(send_response) = state.send_response.as_mut() else {
18916 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} cannot push after responding")));
18917 continue;
18918 };
18919 let ServerHttp2Responder::Regular(send_response) = send_response else {
18920 let _ = respond_to.send(Err(format!("HTTP/2 pushed stream {stream_id} cannot create nested push promises")));
18921 continue;
18922 };
18923 match send_response.push_request(request) {
18924 Ok(pushed) => {
18925 let pushed_stream_id = {
18926 let mut state = shared.lock().expect("http2 shared state");
18927 let pushed_stream_id = next_http2_stream_id(&mut state);
18928 state.streams.insert(
18929 pushed_stream_id,
18930 ActiveHttp2Stream {
18931 session_id,
18932 paused: Arc::new(AtomicBool::new(false)),
18933 },
18934 );
18935 pushed_stream_id
18936 };
18937 streams_guard.insert(
18938 pushed_stream_id,
18939 ServerHttp2StreamState {
18940 send_response: Some(ServerHttp2Responder::Pushed(pushed)),
18941 send_stream: None,
18942 },
18943 );
18944 let _ = respond_to.send(Ok(json!({
18945 "streamId": pushed_stream_id,
18946 "headers": headers_json,
18947 }).to_string().into()));
18948 }
18949 Err(error) => {
18950 let _ = respond_to.send(Err(error.to_string()));
18951 }
18952 }
18953 }
18954 Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
18955 let mut streams = streams.lock().expect("http2 server streams");
18956 let Some(state) = streams.get_mut(&stream_id) else {
18957 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18958 continue;
18959 };
18960 let Some(send_stream) = state.send_stream.as_mut() else {
18961 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} has not sent response headers")));
18962 continue;
18963 };
18964 match send_stream.send_data(Bytes::from(chunk), end_stream) {
18965 Ok(()) => {
18966 if end_stream {
18967 streams.remove(&stream_id);
18968 if let Ok(mut state) = shared.lock() {
18969 state.streams.remove(&stream_id);
18970 }
18971 push_http2_server_event(
18972 &shared,
18973 server_id,
18974 Http2BridgeEvent {
18975 kind: String::from("serverStreamClose"),
18976 id: stream_id,
18977 extra_number: Some(0),
18978 ..Http2BridgeEvent::default()
18979 },
18980 );
18981 }
18982 let _ = respond_to.send(Ok(Value::Bool(true)));
18983 }
18984 Err(error) => {
18985 let _ = respond_to.send(Err(error.to_string()));
18986 }
18987 }
18988 }
18989 Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
18990 let mut streams_guard = streams.lock().expect("http2 server streams");
18991 let Some(mut state) = streams_guard.remove(&stream_id) else {
18992 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18993 continue;
18994 };
18995 let reason = http2_reason(error_code);
18996 if let Some(send_stream) = state.send_stream.as_mut() {
18997 send_stream.send_reset(reason);
18998 }
18999 if let Some(send_response) = state.send_response.as_mut() {
19000 match send_response {
19001 ServerHttp2Responder::Regular(send_response) => {
19002 send_response.send_reset(reason)
19003 }
19004 ServerHttp2Responder::Pushed(send_response) => {
19005 send_response.send_reset(reason)
19006 }
19007 }
19008 }
19009 if let Ok(mut shared_guard) = shared.lock() {
19010 shared_guard.streams.remove(&stream_id);
19011 }
19012 push_http2_server_event(
19013 &shared,
19014 server_id,
19015 Http2BridgeEvent {
19016 kind: String::from("serverStreamClose"),
19017 id: stream_id,
19018 extra_number: Some(u32::from(reason) as u64),
19019 ..Http2BridgeEvent::default()
19020 },
19021 );
19022 let _ = respond_to.send(Ok(Value::Null));
19023 }
19024 Http2SessionCommand::StreamRespondWithFile { stream_id, body, headers_json, options_json, respond_to } => {
19025 let options: JavascriptHttp2FileResponseOptions =
19026 serde_json::from_str(&options_json).unwrap_or_default();
19027 let response = match build_http2_response(&headers_json) {
19028 Ok(response) => response,
19029 Err(error) => {
19030 let _ = respond_to.send(Err(error.to_string()));
19031 continue;
19032 }
19033 };
19034 let offset = usize::try_from(options.offset.unwrap_or_default()).unwrap_or(0);
19035 let body = if offset >= body.len() {
19036 Vec::new()
19037 } else {
19038 let body = &body[offset..];
19039 match options.length {
19040 Some(length) if length >= 0 => {
19041 body[..body.len().min(length as usize)].to_vec()
19042 }
19043 _ => body.to_vec(),
19044 }
19045 };
19046 let mut streams_guard = streams.lock().expect("http2 server streams");
19047 let Some(state) = streams_guard.get_mut(&stream_id) else {
19048 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19049 continue;
19050 };
19051 let Some(send_response) = state.send_response.as_mut() else {
19052 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
19053 continue;
19054 };
19055 match match send_response {
19056 ServerHttp2Responder::Regular(send_response) => {
19057 send_response.send_response(response, body.is_empty())
19058 }
19059 ServerHttp2Responder::Pushed(send_response) => {
19060 send_response.send_response(response, body.is_empty())
19061 }
19062 } {
19063 Ok(mut send_stream) => {
19064 state.send_response = None;
19065 if body.is_empty() {
19066 streams_guard.remove(&stream_id);
19067 if let Ok(mut shared_guard) = shared.lock() {
19068 shared_guard.streams.remove(&stream_id);
19069 }
19070 } else {
19071 if let Err(error) = send_stream.send_data(Bytes::from(body), true) {
19072 let _ = respond_to.send(Err(error.to_string()));
19073 continue;
19074 }
19075 streams_guard.remove(&stream_id);
19076 if let Ok(mut shared_guard) = shared.lock() {
19077 shared_guard.streams.remove(&stream_id);
19078 }
19079 }
19080 push_http2_server_event(
19081 &shared,
19082 server_id,
19083 Http2BridgeEvent {
19084 kind: String::from("serverStreamClose"),
19085 id: stream_id,
19086 extra_number: Some(0),
19087 ..Http2BridgeEvent::default()
19088 },
19089 );
19090 let _ = respond_to.send(Ok(Value::Null));
19091 }
19092 Err(error) => {
19093 let _ = respond_to.send(Err(error.to_string()));
19094 }
19095 }
19096 }
19097 Http2SessionCommand::Request { respond_to, .. } => {
19098 let _ = respond_to.send(Err(String::from("HTTP/2 server sessions cannot initiate client requests")));
19099 }
19100 }
19101 }
19102 else => break,
19103 }
19104 }
19105 });
19106 });
19107}
19108
19109fn spawn_http2_server_accept_loop(
19110 shared: Arc<Mutex<crate::state::Http2SharedState>>,
19111 server_id: u64,
19112 listener: TcpListener,
19113) {
19114 thread::spawn(move || {
19115 let listener = listener;
19116 loop {
19117 let closed = shared
19118 .lock()
19119 .ok()
19120 .and_then(|state| {
19121 state
19122 .servers
19123 .get(&server_id)
19124 .map(|server| server.closed.load(Ordering::SeqCst))
19125 })
19126 .unwrap_or(true);
19127 if closed {
19128 break;
19129 }
19130 match listener.accept() {
19131 Ok((stream, _)) => {
19132 let (command_tx, command_rx) = unbounded_channel();
19133 let (guest_local_addr, secure, tls) = {
19134 let state = shared.lock().expect("http2 shared state");
19135 let server = state.servers.get(&server_id).expect("http2 server state");
19136 (server.guest_local_addr, server.secure, server.tls.clone())
19137 };
19138 let (local_addr, remote_addr) = match (stream.local_addr(), stream.peer_addr())
19139 {
19140 (Ok(local_addr), Ok(remote_addr)) => (local_addr, remote_addr),
19141 _ => continue,
19142 };
19143 let session_snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
19144 encrypted: secure,
19145 alpn_protocol: Some(if secure {
19146 String::from("h2")
19147 } else {
19148 String::from("h2c")
19149 }),
19150 local_settings: BTreeMap::new(),
19151 remote_settings: BTreeMap::new(),
19152 state: http2_runtime_snapshot(),
19153 socket: Http2SocketSnapshot {
19154 local_address: Some(guest_local_addr.ip().to_string()),
19155 local_port: Some(guest_local_addr.port()),
19156 local_family: Some(socket_addr_family(&guest_local_addr).to_string()),
19157 remote_address: Some(remote_addr.ip().to_string()),
19158 remote_port: Some(remote_addr.port()),
19159 remote_family: Some(socket_addr_family(&remote_addr).to_string()),
19160 ..http2_socket_snapshot(local_addr, remote_addr)
19161 },
19162 ..Http2SessionSnapshot::default()
19163 }));
19164 let session_id = {
19165 let mut state = shared.lock().expect("http2 shared state");
19166 let session_id = next_http2_session_id(&mut state);
19167 state
19168 .sessions
19169 .insert(session_id, ActiveHttp2Session { command_tx });
19170 session_id
19171 };
19172 spawn_http2_server_session(
19173 Arc::clone(&shared),
19174 server_id,
19175 session_id,
19176 stream,
19177 tls,
19178 session_snapshot,
19179 command_rx,
19180 );
19181 }
19182 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
19183 thread::sleep(HTTP2_POLL_DELAY);
19184 }
19185 Err(error) => {
19186 push_http2_server_event(
19187 &shared,
19188 server_id,
19189 Http2BridgeEvent {
19190 kind: String::from("serverStreamError"),
19191 id: server_id,
19192 data: Some(http2_error_payload(error.to_string())),
19193 ..Http2BridgeEvent::default()
19194 },
19195 );
19196 thread::sleep(HTTP2_POLL_DELAY);
19197 }
19198 }
19199 }
19200 });
19201}
19202
19203fn send_http2_command(
19204 session: &ActiveHttp2Session,
19205 command: impl FnOnce(Sender<Result<Value, String>>) -> Http2SessionCommand,
19206) -> Result<Value, SidecarError> {
19207 let (respond_to, response_rx) = mpsc::channel();
19208 session.command_tx.send(command(respond_to)).map_err(|_| {
19209 SidecarError::InvalidState(String::from("HTTP/2 session command channel closed"))
19210 })?;
19211 response_rx
19212 .recv_timeout(Duration::from_secs(30))
19213 .map_err(|_| {
19214 SidecarError::Execution(String::from("timed out waiting for HTTP/2 session command"))
19215 })?
19216 .map_err(SidecarError::Execution)
19217}
19218
19219fn parse_http2_server_listen_payload(
19220 request: &JavascriptSyncRpcRequest,
19221) -> Result<JavascriptHttp2ServerListenRequest, SidecarError> {
19222 let payload_json =
19223 javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_server_listen payload")?;
19224 serde_json::from_str(payload_json).map_err(|error| {
19225 SidecarError::InvalidState(format!(
19226 "net.http2_server_listen payload must be valid JSON: {error}"
19227 ))
19228 })
19229}
19230
19231fn parse_http2_connect_payload(
19232 request: &JavascriptSyncRpcRequest,
19233) -> Result<JavascriptHttp2SessionConnectRequest, SidecarError> {
19234 let payload_json =
19235 javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_session_connect payload")?;
19236 serde_json::from_str(payload_json).map_err(|error| {
19237 SidecarError::InvalidState(format!(
19238 "net.http2_session_connect payload must be valid JSON: {error}"
19239 ))
19240 })
19241}
19242
19243fn http2_session_for_id(
19244 process: &ActiveProcess,
19245 session_id: u64,
19246) -> Result<ActiveHttp2Session, SidecarError> {
19247 let shared = process
19248 .http2
19249 .shared
19250 .lock()
19251 .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
19252 shared
19253 .sessions
19254 .get(&session_id)
19255 .cloned()
19256 .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 session {session_id}")))
19257}
19258
19259fn http2_stream_for_id(
19260 process: &ActiveProcess,
19261 stream_id: u64,
19262) -> Result<ActiveHttp2Stream, SidecarError> {
19263 let shared = process
19264 .http2
19265 .shared
19266 .lock()
19267 .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
19268 shared
19269 .streams
19270 .get(&stream_id)
19271 .cloned()
19272 .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 stream {stream_id}")))
19273}
19274
19275fn service_javascript_http2_sync_rpc<B>(
19276 request: JavascriptHttp2SyncRpcServiceRequest<'_, B>,
19277) -> Result<Value, SidecarError>
19278where
19279 B: NativeSidecarBridge + Send + 'static,
19280 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19281{
19282 let JavascriptHttp2SyncRpcServiceRequest {
19283 bridge,
19284 kernel,
19285 vm_id,
19286 dns,
19287 socket_paths,
19288 process,
19289 sync_request: request,
19290 resource_limits,
19291 network_counts,
19292 } = request;
19293 match request.method.as_str() {
19294 "net.http2_server_listen" => {
19295 check_network_resource_limit(
19296 resource_limits.max_sockets,
19297 network_counts.sockets,
19298 1,
19299 "socket",
19300 )?;
19301 let payload = parse_http2_server_listen_payload(request)?;
19302 let (family, bind_host, guest_host) =
19303 normalize_tcp_listen_host(payload.host.as_deref())?;
19304 let requested_port = payload.port.unwrap_or(0);
19305 bridge.require_network_access(
19306 vm_id,
19307 NetworkOperation::Listen,
19308 format_tcp_resource(bind_host, requested_port),
19309 )?;
19310 let port = allocate_guest_listen_port(
19311 requested_port,
19312 family,
19313 &socket_paths.used_tcp_guest_ports,
19314 socket_paths.listen_policy,
19315 )?;
19316 let mut listener =
19317 ActiveTcpListener::bind(bind_host, guest_host, port, payload.backlog)?;
19318 let guest_local_addr = listener.guest_local_addr();
19319 let closed = Arc::new(AtomicBool::new(false));
19320 {
19321 let mut state = process.http2.shared.lock().map_err(|_| {
19322 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19323 })?;
19324 state.servers.insert(
19325 payload.server_id,
19326 ActiveHttp2Server {
19327 actual_local_addr: listener.local_addr(),
19328 guest_local_addr,
19329 secure: payload.secure,
19330 tls: payload.tls.clone().map(|mut tls| {
19331 tls.is_server = payload.secure;
19332 if payload.secure && tls.alpn_protocols.is_none() {
19333 tls.alpn_protocols = Some(vec![String::from("h2")]);
19334 }
19335 tls
19336 }),
19337 closed: Arc::clone(&closed),
19338 },
19339 );
19340 state.server_events.entry(payload.server_id).or_default();
19341 }
19342 spawn_http2_server_accept_loop(
19343 Arc::clone(&process.http2.shared),
19344 payload.server_id,
19345 listener.listener.take().ok_or_else(|| {
19346 SidecarError::InvalidState(String::from(
19347 "HTTP/2 listener missing host TCP socket",
19348 ))
19349 })?,
19350 );
19351 javascript_net_json_string(
19352 json!({
19353 "address": {
19354 "address": guest_local_addr.ip().to_string(),
19355 "family": socket_addr_family(&guest_local_addr),
19356 "port": guest_local_addr.port(),
19357 }
19358 }),
19359 "net.http2_server_listen",
19360 )
19361 }
19362 "net.http2_server_poll" => {
19363 let server_id =
19364 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_poll server id")?;
19365 let wait_ms = javascript_sync_rpc_arg_u64_optional(
19366 &request.args,
19367 1,
19368 "net.http2_server_poll wait ms",
19369 )?
19370 .unwrap_or_default();
19371 match wait_for_http2_event(&process.http2.shared, server_id, true, wait_ms) {
19372 Some(event) => http2_event_value(&event),
19373 None => Ok(Value::Null),
19374 }
19375 }
19376 "net.http2_server_wait" => {
19377 let server_id =
19378 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_wait server id")?;
19379 dispatch_http2_wait_loop(process, server_id, true)
19380 }
19381 "net.http2_server_close" => {
19382 let server_id =
19383 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_close server id")?;
19384 let server = {
19385 let mut state = process.http2.shared.lock().map_err(|_| {
19386 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19387 })?;
19388 state.servers.remove(&server_id)
19389 }
19390 .ok_or_else(|| {
19391 SidecarError::InvalidState(format!("unknown HTTP/2 server {server_id}"))
19392 })?;
19393 server.closed.store(true, Ordering::SeqCst);
19394 push_http2_server_event(
19395 &process.http2.shared,
19396 server_id,
19397 Http2BridgeEvent {
19398 kind: String::from("serverClose"),
19399 id: server_id,
19400 ..Http2BridgeEvent::default()
19401 },
19402 );
19403 Ok(Value::Null)
19404 }
19405 "net.http2_server_respond" => {
19406 let server_id = javascript_sync_rpc_arg_u64(
19407 &request.args,
19408 0,
19409 "net.http2_server_respond server id",
19410 )?;
19411 let request_id = javascript_sync_rpc_arg_u64(
19412 &request.args,
19413 1,
19414 "net.http2_server_respond request id",
19415 )?;
19416 let response_json =
19417 javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_server_respond payload")?;
19418 ensure_vm_fetch_response_within_limit(
19419 response_json,
19420 "net.http2_server_respond",
19421 VM_FETCH_BUFFER_LIMIT_BYTES,
19422 )?;
19423 serde_json::from_str::<Value>(response_json).map_err(|error| {
19424 SidecarError::Execution(format!(
19425 "net.http2_server_respond payload must be valid JSON: {error}"
19426 ))
19427 })?;
19428 let Some(pending) = process
19429 .pending_http_requests
19430 .get_mut(&(server_id, request_id))
19431 else {
19432 return Err(SidecarError::InvalidState(format!(
19433 "unknown pending HTTP/2 request {request_id} for server {server_id}"
19434 )));
19435 };
19436 *pending = Some(response_json.to_owned());
19437 Ok(Value::Bool(true))
19438 }
19439 "net.http2_session_connect" => {
19440 check_network_resource_limit(
19441 resource_limits.max_sockets,
19442 network_counts.sockets,
19443 1,
19444 "socket",
19445 )?;
19446 check_network_resource_limit(
19447 resource_limits.max_connections,
19448 network_counts.connections,
19449 1,
19450 "connection",
19451 )?;
19452 let payload = parse_http2_connect_payload(request)?;
19453 let authority = payload.authority.clone().unwrap_or_else(|| {
19454 format!(
19455 "{}://{}:{}",
19456 payload.protocol.as_deref().unwrap_or("http"),
19457 payload.host.as_deref().unwrap_or("localhost"),
19458 payload.port.unwrap_or(80)
19459 )
19460 });
19461 let url = Url::parse(&authority).map_err(|error| {
19462 SidecarError::InvalidState(format!(
19463 "invalid HTTP/2 authority {authority:?}: {error}"
19464 ))
19465 })?;
19466 let secure = url.scheme() == "https" || payload.protocol.as_deref() == Some("https:");
19467 let host = payload
19468 .host
19469 .as_deref()
19470 .or_else(|| url.host_str())
19471 .unwrap_or("localhost");
19472 let port = payload.port.or_else(|| url.port()).unwrap_or(80);
19473 bridge.require_network_access(
19474 vm_id,
19475 NetworkOperation::Http,
19476 format_tcp_resource(host, port),
19477 )?;
19478 let resolved = {
19479 let shared = process.http2.shared.lock().map_err(|_| {
19480 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19481 })?;
19482 shared
19483 .servers
19484 .values()
19485 .find(|server| {
19486 is_loopback_request_host(host) && server.guest_local_addr.port() == port
19487 })
19488 .map(|server| ResolvedTcpConnectAddr {
19489 actual_addr: server.actual_local_addr,
19490 guest_remote_addr: server.guest_local_addr,
19491 use_kernel_loopback: false,
19492 })
19493 };
19494 let resolved = match resolved {
19495 Some(resolved) => resolved,
19496 None => {
19497 resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, socket_paths)?
19498 }
19499 };
19500 let (command_tx, command_rx) = unbounded_channel();
19501 let snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
19502 encrypted: secure,
19503 alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
19504 local_settings: http2_settings_from_value(&payload.settings),
19505 remote_settings: BTreeMap::new(),
19506 state: http2_runtime_snapshot(),
19507 socket: Http2SocketSnapshot {
19508 encrypted: secure,
19509 remote_address: Some(resolved.guest_remote_addr.ip().to_string()),
19510 remote_port: Some(resolved.guest_remote_addr.port()),
19511 remote_family: Some(
19512 socket_addr_family(&resolved.guest_remote_addr).to_string(),
19513 ),
19514 servername: if secure {
19515 payload
19516 .tls
19517 .as_ref()
19518 .and_then(|tls| tls.servername.clone())
19519 .or_else(|| Some(host.to_string()))
19520 } else {
19521 None
19522 },
19523 alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
19524 ..Http2SocketSnapshot::default()
19525 },
19526 ..Http2SessionSnapshot::default()
19527 }));
19528 let session_id = {
19529 let mut state = process.http2.shared.lock().map_err(|_| {
19530 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19531 })?;
19532 let session_id = next_http2_session_id(&mut state);
19533 state
19534 .sessions
19535 .insert(session_id, ActiveHttp2Session { command_tx });
19536 state.session_events.entry(session_id).or_default();
19537 session_id
19538 };
19539 spawn_http2_client_session(
19540 Arc::clone(&process.http2.shared),
19541 session_id,
19542 resolved.actual_addr,
19543 if secure {
19544 Some(payload.tls.unwrap_or(JavascriptTlsBridgeOptions {
19545 is_server: false,
19546 servername: Some(host.to_string()),
19547 alpn_protocols: Some(vec![String::from("h2")]),
19548 ..JavascriptTlsBridgeOptions::default()
19549 }))
19550 } else {
19551 None
19552 },
19553 Arc::clone(&snapshot),
19554 command_rx,
19555 );
19556 let snapshot_json =
19557 http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())?;
19558 javascript_net_json_string(
19559 json!({
19560 "sessionId": session_id,
19561 "state": snapshot_json,
19562 }),
19563 "net.http2_session_connect",
19564 )
19565 }
19566 "net.http2_session_request" => {
19567 let session_id = javascript_sync_rpc_arg_u64(
19568 &request.args,
19569 0,
19570 "net.http2_session_request session id",
19571 )?;
19572 let headers_json =
19573 javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_session_request headers")?;
19574 let options_json =
19575 javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_session_request options")?;
19576 let session = http2_session_for_id(process, session_id)?;
19577 send_http2_command(&session, |respond_to| Http2SessionCommand::Request {
19578 headers_json: headers_json.to_owned(),
19579 options_json: options_json.to_owned(),
19580 respond_to,
19581 })
19582 }
19583 "net.http2_session_settings" => {
19584 let session_id = javascript_sync_rpc_arg_u64(
19585 &request.args,
19586 0,
19587 "net.http2_session_settings session id",
19588 )?;
19589 let settings_json = javascript_sync_rpc_arg_str(
19590 &request.args,
19591 1,
19592 "net.http2_session_settings settings",
19593 )?;
19594 let session = http2_session_for_id(process, session_id)?;
19595 send_http2_command(&session, |respond_to| Http2SessionCommand::Settings {
19596 settings_json: settings_json.to_owned(),
19597 respond_to,
19598 })
19599 }
19600 "net.http2_session_set_local_window_size" => {
19601 let session_id = javascript_sync_rpc_arg_u64(
19602 &request.args,
19603 0,
19604 "net.http2_session_set_local_window_size session id",
19605 )?;
19606 let window_size = javascript_sync_rpc_arg_u64(
19607 &request.args,
19608 1,
19609 "net.http2_session_set_local_window_size window size",
19610 )?;
19611 let session = http2_session_for_id(process, session_id)?;
19612 send_http2_command(&session, |respond_to| {
19613 Http2SessionCommand::SetLocalWindowSize {
19614 size: window_size as u32,
19615 respond_to,
19616 }
19617 })
19618 }
19619 "net.http2_session_goaway" => {
19620 let session_id = javascript_sync_rpc_arg_u64(
19621 &request.args,
19622 0,
19623 "net.http2_session_goaway session id",
19624 )?;
19625 let error_code = javascript_sync_rpc_arg_u64(
19626 &request.args,
19627 1,
19628 "net.http2_session_goaway error code",
19629 )?;
19630 let last_stream_id = javascript_sync_rpc_arg_u64(
19631 &request.args,
19632 2,
19633 "net.http2_session_goaway last stream id",
19634 )?;
19635 let opaque_data = request
19636 .args
19637 .get(3)
19638 .and_then(Value::as_str)
19639 .map(|value| {
19640 base64::engine::general_purpose::STANDARD
19641 .decode(value)
19642 .map_err(|error| {
19643 SidecarError::InvalidState(format!("invalid GOAWAY payload: {error}"))
19644 })
19645 })
19646 .transpose()?;
19647 let session = http2_session_for_id(process, session_id)?;
19648 send_http2_command(&session, |respond_to| Http2SessionCommand::Goaway {
19649 error_code: error_code as u32,
19650 last_stream_id: last_stream_id as u32,
19651 opaque_data,
19652 respond_to,
19653 })
19654 }
19655 "net.http2_session_close" | "net.http2_session_destroy" => {
19656 let session_id = javascript_sync_rpc_arg_u64(
19657 &request.args,
19658 0,
19659 "net.http2_session_close session id",
19660 )?;
19661 let session = http2_session_for_id(process, session_id)?;
19662 send_http2_command(&session, |respond_to| Http2SessionCommand::Close {
19663 abrupt: request.method == "net.http2_session_destroy",
19664 respond_to,
19665 })
19666 }
19667 "net.http2_session_poll" => {
19668 let session_id =
19669 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_poll session id")?;
19670 let wait_ms = javascript_sync_rpc_arg_u64_optional(
19671 &request.args,
19672 1,
19673 "net.http2_session_poll wait ms",
19674 )?
19675 .unwrap_or_default();
19676 match wait_for_http2_event(&process.http2.shared, session_id, false, wait_ms) {
19677 Some(event) => http2_event_value(&event),
19678 None => Ok(Value::Null),
19679 }
19680 }
19681 "net.http2_session_wait" => {
19682 let session_id =
19683 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_wait session id")?;
19684 dispatch_http2_wait_loop(process, session_id, false)
19685 }
19686 "net.http2_stream_respond" => {
19687 let stream_id = javascript_sync_rpc_arg_u64(
19688 &request.args,
19689 0,
19690 "net.http2_stream_respond stream id",
19691 )?;
19692 let headers_json =
19693 javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_stream_respond headers")?;
19694 let stream = http2_stream_for_id(process, stream_id)?;
19695 let session = http2_session_for_id(process, stream.session_id)?;
19696 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamRespond {
19697 stream_id,
19698 headers_json: headers_json.to_owned(),
19699 respond_to,
19700 })
19701 }
19702 "net.http2_stream_push_stream" => {
19703 let stream_id = javascript_sync_rpc_arg_u64(
19704 &request.args,
19705 0,
19706 "net.http2_stream_push_stream stream id",
19707 )?;
19708 let headers_json = javascript_sync_rpc_arg_str(
19709 &request.args,
19710 1,
19711 "net.http2_stream_push_stream headers",
19712 )?;
19713 let _options_json = javascript_sync_rpc_arg_str(
19714 &request.args,
19715 2,
19716 "net.http2_stream_push_stream options",
19717 )?;
19718 let stream = http2_stream_for_id(process, stream_id)?;
19719 let session = http2_session_for_id(process, stream.session_id)?;
19720 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamPush {
19721 stream_id,
19722 headers_json: headers_json.to_owned(),
19723 respond_to,
19724 })
19725 }
19726 "net.http2_stream_write" => {
19727 let stream_id =
19728 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_write stream id")?;
19729 let chunk =
19730 javascript_sync_rpc_base64_arg(&request.args, 1, "net.http2_stream_write data")?;
19731 let stream = http2_stream_for_id(process, stream_id)?;
19732 let session = http2_session_for_id(process, stream.session_id)?;
19733 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
19734 stream_id,
19735 chunk,
19736 end_stream: false,
19737 respond_to,
19738 })
19739 }
19740 "net.http2_stream_end" => {
19741 let stream_id =
19742 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_end stream id")?;
19743 let chunk = request
19744 .args
19745 .get(1)
19746 .and_then(Value::as_str)
19747 .map(|value| {
19748 base64::engine::general_purpose::STANDARD
19749 .decode(value)
19750 .map_err(|error| {
19751 SidecarError::InvalidState(format!(
19752 "invalid HTTP/2 stream payload: {error}"
19753 ))
19754 })
19755 })
19756 .transpose()?
19757 .unwrap_or_default();
19758 let stream = http2_stream_for_id(process, stream_id)?;
19759 let session = http2_session_for_id(process, stream.session_id)?;
19760 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
19761 stream_id,
19762 chunk,
19763 end_stream: true,
19764 respond_to,
19765 })
19766 }
19767 "net.http2_stream_close" => {
19768 let stream_id =
19769 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_close stream id")?;
19770 let code = javascript_sync_rpc_arg_u64_optional(
19771 &request.args,
19772 1,
19773 "net.http2_stream_close error code",
19774 )?
19775 .map(|value| value as u32);
19776 let stream = http2_stream_for_id(process, stream_id)?;
19777 let session = http2_session_for_id(process, stream.session_id)?;
19778 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamClose {
19779 stream_id,
19780 error_code: code,
19781 respond_to,
19782 })
19783 }
19784 "net.http2_stream_pause" => {
19785 let stream_id =
19786 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_pause stream id")?;
19787 let stream = http2_stream_for_id(process, stream_id)?;
19788 stream.paused.store(true, Ordering::SeqCst);
19789 Ok(Value::Null)
19790 }
19791 "net.http2_stream_resume" => {
19792 let stream_id =
19793 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_resume stream id")?;
19794 let stream = http2_stream_for_id(process, stream_id)?;
19795 stream.paused.store(false, Ordering::SeqCst);
19796 Ok(Value::Null)
19797 }
19798 "net.http2_stream_respond_with_file" => {
19799 let stream_id = javascript_sync_rpc_arg_u64(
19800 &request.args,
19801 0,
19802 "net.http2_stream_respond_with_file stream id",
19803 )?;
19804 let path = javascript_sync_rpc_arg_str(
19805 &request.args,
19806 1,
19807 "net.http2_stream_respond_with_file path",
19808 )?;
19809 let headers_json = javascript_sync_rpc_arg_str(
19810 &request.args,
19811 2,
19812 "net.http2_stream_respond_with_file headers",
19813 )?;
19814 let options_json = javascript_sync_rpc_arg_str(
19815 &request.args,
19816 3,
19817 "net.http2_stream_respond_with_file options",
19818 )?;
19819 let stream = http2_stream_for_id(process, stream_id)?;
19820 let session = http2_session_for_id(process, stream.session_id)?;
19821 let guest_path = resolve_http2_file_response_guest_path(process, path);
19822 let body = kernel.read_file(&guest_path).map_err(kernel_error)?;
19823 send_http2_command(&session, |respond_to| {
19824 Http2SessionCommand::StreamRespondWithFile {
19825 stream_id,
19826 body,
19827 headers_json: headers_json.to_owned(),
19828 options_json: options_json.to_owned(),
19829 respond_to,
19830 }
19831 })
19832 }
19833 other => Err(SidecarError::InvalidState(format!(
19834 "unsupported JavaScript HTTP/2 sync RPC method {other}"
19835 ))),
19836 }
19837}
19838
19839const JAVASCRIPT_NET_POLL_MAX_WAIT: Duration = Duration::from_millis(50);
19840const EXITED_PROCESS_SNAPSHOT_RETENTION: Duration = Duration::from_secs(2);
19841
19842fn resolve_http2_file_response_guest_path(process: &ActiveProcess, path: &str) -> String {
19843 if Path::new(path).is_absolute() {
19844 normalize_path(path)
19845 } else {
19846 normalize_path(&format!("{}/{}", process.guest_cwd, path))
19847 }
19848}
19849
19850pub(crate) fn clamp_javascript_net_poll_wait(wait_ms: u64) -> Duration {
19851 if wait_ms == 0 {
19854 Duration::ZERO
19855 } else {
19856 Duration::from_millis(wait_ms).min(JAVASCRIPT_NET_POLL_MAX_WAIT)
19857 }
19858}
19859
19860pub(crate) fn service_javascript_net_sync_rpc<B>(
19861 request: JavascriptNetSyncRpcServiceRequest<'_, B>,
19862) -> Result<Value, SidecarError>
19863where
19864 B: NativeSidecarBridge + Send + 'static,
19865 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19866{
19867 let JavascriptNetSyncRpcServiceRequest {
19868 bridge,
19869 vm_id,
19870 dns,
19871 socket_paths,
19872 kernel,
19873 process,
19874 sync_request: request,
19875 resource_limits,
19876 network_counts,
19877 } = request;
19878 match request.method.as_str() {
19879 "net.http_listen" => {
19880 check_network_resource_limit(
19881 resource_limits.max_sockets,
19882 network_counts.sockets,
19883 1,
19884 "socket",
19885 )?;
19886 let payload_json =
19887 javascript_sync_rpc_arg_str(&request.args, 0, "net.http_listen payload")?;
19888 let payload: JavascriptHttpListenRequest =
19889 serde_json::from_str(payload_json).map_err(|error| {
19890 SidecarError::InvalidState(format!(
19891 "net.http_listen payload must be valid JSON: {error}"
19892 ))
19893 })?;
19894 let (family, bind_host, guest_host) =
19895 normalize_tcp_listen_host(payload.hostname.as_deref())?;
19896 let requested_port = payload.port.unwrap_or(0);
19897 bridge.require_network_access(
19898 vm_id,
19899 NetworkOperation::Listen,
19900 format_tcp_resource(bind_host, requested_port),
19901 )?;
19902 let port = allocate_guest_listen_port(
19903 requested_port,
19904 family,
19905 &socket_paths.used_tcp_guest_ports,
19906 socket_paths.listen_policy,
19907 )?;
19908 let mut listener = ActiveTcpListener::bind(
19909 bind_host,
19910 guest_host,
19911 port,
19912 Some(DEFAULT_JAVASCRIPT_NET_BACKLOG),
19913 )?;
19914 let guest_local_addr = listener.guest_local_addr();
19915 process.http_servers.insert(
19916 payload.server_id,
19917 ActiveHttpServer {
19918 listener: listener.listener.take().ok_or_else(|| {
19919 SidecarError::InvalidState(String::from(
19920 "HTTP listener missing host TCP socket",
19921 ))
19922 })?,
19923 guest_local_addr,
19924 next_request_id: 0,
19925 },
19926 );
19927 serde_json::to_string(&json!({
19928 "address": {
19929 "address": guest_local_addr.ip().to_string(),
19930 "family": socket_addr_family(&guest_local_addr),
19931 "port": guest_local_addr.port(),
19932 }
19933 }))
19934 .map(Value::String)
19935 .map_err(|error| {
19936 SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}"))
19937 })
19938 }
19939 "net.http_close" => {
19940 let server_id =
19941 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_close server id")?;
19942 let server = process.http_servers.remove(&server_id).ok_or_else(|| {
19943 SidecarError::InvalidState(format!("unknown HTTP server {server_id}"))
19944 })?;
19945 drop(server.listener);
19946 process
19947 .pending_http_requests
19948 .retain(|(pending_server_id, _), _| *pending_server_id != server_id);
19949 Ok(Value::Null)
19950 }
19951 "net.http_wait" => {
19952 let server_id =
19953 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_wait server id")?;
19954 dispatch_http_wait_loop(process, server_id)
19955 }
19956 "net.http_respond" => {
19957 let server_id =
19958 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_respond server id")?;
19959 let request_id =
19960 javascript_sync_rpc_arg_u64(&request.args, 1, "net.http_respond request id")?;
19961 let response_json =
19962 javascript_sync_rpc_arg_str(&request.args, 2, "net.http_respond payload")?;
19963 ensure_vm_fetch_response_within_limit(
19964 response_json,
19965 "net.http_respond",
19966 VM_FETCH_BUFFER_LIMIT_BYTES,
19967 )?;
19968 serde_json::from_str::<Value>(response_json).map_err(|error| {
19969 SidecarError::Execution(format!(
19970 "net.http_respond payload must be valid JSON: {error}"
19971 ))
19972 })?;
19973 let Some(pending) = process
19974 .pending_http_requests
19975 .get_mut(&(server_id, request_id))
19976 else {
19977 return Err(SidecarError::InvalidState(format!(
19978 "unknown pending HTTP request {request_id} for server {server_id}"
19979 )));
19980 };
19981 *pending = Some(response_json.to_owned());
19982 Ok(Value::Null)
19983 }
19984 "net.reserve_tcp_port" => {
19985 let payload = request
19986 .args
19987 .first()
19988 .cloned()
19989 .ok_or_else(|| {
19990 SidecarError::InvalidState(String::from(
19991 "net.reserve_tcp_port requires a request payload",
19992 ))
19993 })
19994 .and_then(|value| {
19995 serde_json::from_value::<JavascriptNetReserveTcpPortRequest>(value).map_err(
19996 |error| {
19997 SidecarError::InvalidState(format!(
19998 "invalid net.reserve_tcp_port payload: {error}"
19999 ))
20000 },
20001 )
20002 })?;
20003 let (family, _bind_host, guest_host) =
20004 normalize_tcp_listen_host(payload.host.as_deref())?;
20005 let requested_port = payload.port.unwrap_or(0);
20006 let port = allocate_guest_listen_port(
20007 requested_port,
20008 family,
20009 &socket_paths.used_tcp_guest_ports,
20010 socket_paths.listen_policy,
20011 )?;
20012 let reservation_id = process.allocate_tcp_port_reservation_id();
20013 process
20014 .tcp_port_reservations
20015 .insert(reservation_id.clone(), (family, port));
20016 Ok(json!({
20017 "reservationId": reservation_id,
20018 "localAddress": guest_host,
20019 "localPort": port,
20020 "family": match family {
20021 JavascriptSocketFamily::Ipv4 => "IPv4",
20022 JavascriptSocketFamily::Ipv6 => "IPv6",
20023 },
20024 }))
20025 }
20026 "net.release_tcp_port" => {
20027 let reservation_id =
20028 javascript_sync_rpc_arg_str(&request.args, 0, "net.release_tcp_port reservation")?;
20029 process.tcp_port_reservations.remove(reservation_id);
20030 Ok(Value::Null)
20031 }
20032 "net.connect" => {
20033 check_network_resource_limit(
20034 resource_limits.max_sockets,
20035 network_counts.sockets,
20036 1,
20037 "socket",
20038 )?;
20039 check_network_resource_limit(
20040 resource_limits.max_connections,
20041 network_counts.connections,
20042 1,
20043 "connection",
20044 )?;
20045 let payload = request
20046 .args
20047 .first()
20048 .cloned()
20049 .ok_or_else(|| {
20050 SidecarError::InvalidState(String::from(
20051 "net.connect requires a request payload",
20052 ))
20053 })
20054 .and_then(|value| {
20055 serde_json::from_value::<JavascriptNetConnectRequest>(value).map_err(|error| {
20056 SidecarError::InvalidState(format!("invalid net.connect payload: {error}"))
20057 })
20058 })?;
20059 if let Some(path) = payload.path.as_deref() {
20060 let guest_path = normalize_path(path);
20061 let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
20062 let socket = ActiveUnixSocket::connect(&host_path, &guest_path)?;
20063 let socket_id = process.allocate_unix_socket_id();
20064 process.unix_sockets.insert(socket_id.clone(), socket);
20065 Ok(json!({
20066 "socketId": socket_id,
20067 "remotePath": guest_path,
20068 }))
20069 } else {
20070 let port = payload.port.ok_or_else(|| {
20071 SidecarError::InvalidState(String::from(
20072 "net.connect requires either a path or port",
20073 ))
20074 })?;
20075 let host = payload.host.as_deref().unwrap_or("localhost");
20076 let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
20077 process
20078 .tcp_port_reservations
20079 .remove(id)
20080 .map(|reservation| (id.to_owned(), reservation))
20081 });
20082 bridge.require_network_access(
20083 vm_id,
20084 NetworkOperation::Http,
20085 format_tcp_resource(host, port),
20086 )?;
20087 if is_loopback_socket_host(host) {
20088 let families = [JavascriptSocketFamily::Ipv4, JavascriptSocketFamily::Ipv6];
20089 if let Some((family, target)) = families.iter().find_map(|family| {
20090 socket_paths
20091 .http_loopback_target(*family, port)
20092 .map(|target| (*family, target))
20093 }) {
20094 if let Some((reservation_id, reservation)) = local_reservation {
20095 process
20096 .tcp_port_reservations
20097 .insert(reservation_id, reservation);
20098 }
20099 let remote_address = match family {
20100 JavascriptSocketFamily::Ipv4 => "127.0.0.1",
20101 JavascriptSocketFamily::Ipv6 => "::1",
20102 };
20103 return Ok(json!({
20104 "loopbackHttpTarget": {
20105 "processId": target.process_id.clone(),
20106 "serverId": target.server_id,
20107 "host": remote_address,
20108 "port": port,
20109 },
20110 "localAddress": match family {
20111 JavascriptSocketFamily::Ipv4 => "127.0.0.1",
20112 JavascriptSocketFamily::Ipv6 => "::1",
20113 },
20114 "localPort": payload.local_port.unwrap_or(0),
20115 "remoteAddress": remote_address,
20116 "remotePort": port,
20117 "remoteFamily": match family {
20118 JavascriptSocketFamily::Ipv4 => "IPv4",
20119 JavascriptSocketFamily::Ipv6 => "IPv6",
20120 },
20121 }));
20122 }
20123 }
20124 let connect_result = ActiveTcpSocket::connect(ActiveTcpConnectRequest {
20125 bridge,
20126 kernel,
20127 kernel_pid: process.kernel_pid,
20128 vm_id,
20129 dns,
20130 host,
20131 port,
20132 local_address: payload.local_address.as_deref(),
20133 local_port: payload.local_port,
20134 local_reservation: local_reservation
20135 .as_ref()
20136 .map(|(_, reservation)| *reservation),
20137 context: socket_paths,
20138 });
20139 if let Err(error) = connect_result {
20140 if let Some((reservation_id, reservation)) = local_reservation {
20141 process
20142 .tcp_port_reservations
20143 .insert(reservation_id, reservation);
20144 }
20145 return Err(error);
20146 }
20147 let socket = connect_result?;
20148 let socket_id = process.allocate_tcp_socket_id();
20149 let local_addr = socket.guest_local_addr;
20150 let remote_addr = socket.guest_remote_addr;
20151 process.tcp_sockets.insert(socket_id.clone(), socket);
20152 Ok(json!({
20153 "socketId": socket_id,
20154 "localAddress": local_addr.ip().to_string(),
20155 "localPort": local_addr.port(),
20156 "remoteAddress": remote_addr.ip().to_string(),
20157 "remotePort": remote_addr.port(),
20158 "remoteFamily": socket_addr_family(&remote_addr),
20159 }))
20160 }
20161 }
20162 "net.listen" => {
20163 check_network_resource_limit(
20164 resource_limits.max_sockets,
20165 network_counts.sockets,
20166 1,
20167 "socket",
20168 )?;
20169 let payload = request
20170 .args
20171 .first()
20172 .cloned()
20173 .ok_or_else(|| {
20174 SidecarError::InvalidState(String::from(
20175 "net.listen requires a request payload",
20176 ))
20177 })
20178 .and_then(|value| match value {
20179 Value::String(json) => {
20180 serde_json::from_str::<JavascriptNetListenRequest>(&json).map_err(|error| {
20181 SidecarError::InvalidState(format!(
20182 "invalid net.listen payload: {error}"
20183 ))
20184 })
20185 }
20186 other => serde_json::from_value::<JavascriptNetListenRequest>(other).map_err(
20187 |error| {
20188 SidecarError::InvalidState(format!(
20189 "invalid net.listen payload: {error}"
20190 ))
20191 },
20192 ),
20193 })?;
20194 if let Some(path) = payload.path.as_deref() {
20195 let guest_path = normalize_path(path);
20196 if kernel.exists(&guest_path).map_err(kernel_error)? {
20197 return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
20198 libc::EADDRINUSE,
20199 )));
20200 }
20201
20202 let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
20203 let on_host_mount =
20204 host_mount_path_for_guest_path_from_mounts(&socket_paths.mounts, &guest_path)
20205 .is_some();
20206 let listener = ActiveUnixListener::bind(&host_path, &guest_path, payload.backlog)?;
20207 if !on_host_mount {
20208 ensure_kernel_parent_directories(kernel, &guest_path)?;
20209 kernel
20210 .write_file(&guest_path, Vec::new())
20211 .map_err(kernel_error)?;
20212 }
20213 let listener_id = process.allocate_unix_listener_id();
20214 process.unix_listeners.insert(listener_id.clone(), listener);
20215 Ok(json!({
20216 "serverId": listener_id,
20217 "path": guest_path,
20218 }))
20219 } else {
20220 let (family, bind_host, guest_host) =
20221 normalize_tcp_listen_host(payload.host.as_deref())?;
20222 let requested_port = payload.port.unwrap_or(0);
20223 bridge.require_network_access(
20224 vm_id,
20225 NetworkOperation::Listen,
20226 format_tcp_resource(bind_host, requested_port),
20227 )?;
20228 let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
20229 process
20230 .tcp_port_reservations
20231 .remove(id)
20232 .map(|reservation| (id.to_owned(), reservation))
20233 });
20234 let port = if requested_port != 0
20235 && local_reservation
20236 .as_ref()
20237 .map(|(_, reservation)| *reservation)
20238 == Some((family, requested_port))
20239 {
20240 requested_port
20241 } else {
20242 allocate_guest_listen_port(
20243 requested_port,
20244 family,
20245 &socket_paths.used_tcp_guest_ports,
20246 socket_paths.listen_policy,
20247 )?
20248 };
20249 let listener_result = ActiveTcpListener::bind_kernel(
20250 kernel,
20251 process.kernel_pid,
20252 guest_host,
20253 port,
20254 payload.backlog,
20255 );
20256 if let Err(error) = listener_result {
20257 if let Some((reservation_id, reservation)) = local_reservation {
20258 process
20259 .tcp_port_reservations
20260 .insert(reservation_id, reservation);
20261 }
20262 return Err(error);
20263 }
20264 let listener = listener_result?;
20265 let listener_id = process.allocate_tcp_listener_id();
20266 let local_addr = listener.guest_local_addr();
20267 process.tcp_listeners.insert(listener_id.clone(), listener);
20268 Ok(json!({
20269 "serverId": listener_id,
20270 "localAddress": local_addr.ip().to_string(),
20271 "localPort": local_addr.port(),
20272 "family": socket_addr_family(&local_addr),
20273 }))
20274 }
20275 }
20276 "net.poll" => {
20277 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.poll socket id")?;
20278 let wait_ms =
20279 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.poll wait ms")?
20280 .unwrap_or_default();
20281 let wait = clamp_javascript_net_poll_wait(wait_ms);
20282 let event = if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20283 socket.poll(kernel, process.kernel_pid, wait)?
20284 } else if let Some(socket) = process.unix_sockets.get_mut(socket_id) {
20285 socket.poll(wait)?
20286 } else {
20287 return Err(SidecarError::InvalidState(format!(
20288 "unknown net socket {socket_id}"
20289 )));
20290 };
20291
20292 match event {
20293 Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(json!({
20294 "type": "data",
20295 "data": javascript_sync_rpc_bytes_value(&chunk),
20296 })),
20297 Some(JavascriptTcpSocketEvent::End) => Ok(json!({
20298 "type": "end",
20299 })),
20300 Some(JavascriptTcpSocketEvent::Error { code, message }) => Ok(json!({
20301 "type": "error",
20302 "code": code,
20303 "message": message,
20304 })),
20305 Some(JavascriptTcpSocketEvent::Close { had_error }) => {
20306 if let Some(socket) = process.tcp_sockets.remove(socket_id) {
20307 if let Some(listener_id) = socket.listener_id.as_deref() {
20308 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20309 listener.release_connection(socket_id);
20310 }
20311 }
20312 } else if let Some(socket) = process.unix_sockets.remove(socket_id) {
20313 if let Some(listener_id) = socket.listener_id.as_deref() {
20314 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20315 listener.release_connection(socket_id);
20316 }
20317 }
20318 }
20319 Ok(json!({
20320 "type": "close",
20321 "hadError": had_error,
20322 }))
20323 }
20324 None => Ok(Value::Null),
20325 }
20326 }
20327 "net.socket_wait_connect" => {
20328 let socket_id =
20329 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_wait_connect socket id")?;
20330 if let Some(socket) = process.tcp_sockets.get(socket_id) {
20331 javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
20332 } else {
20333 let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20334 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20335 })?;
20336 javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
20337 }
20338 }
20339 "net.socket_read" => {
20340 let socket_id =
20341 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_read socket id")?;
20342 if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20343 javascript_net_read_value(socket.poll(
20344 kernel,
20345 process.kernel_pid,
20346 Duration::ZERO,
20347 )?)
20348 } else {
20349 let socket = process.unix_sockets.get_mut(socket_id).ok_or_else(|| {
20350 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20351 })?;
20352 javascript_net_read_value(socket.poll(Duration::ZERO)?)
20353 }
20354 }
20355 "net.socket_set_no_delay" => {
20356 let socket_id =
20357 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_set_no_delay socket id")?;
20358 let enable =
20359 javascript_sync_rpc_arg_bool(&request.args, 1, "net.socket_set_no_delay enabled")?;
20360 if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20361 socket.set_no_delay(enable)?;
20362 } else if !process.unix_sockets.contains_key(socket_id) {
20363 return Err(SidecarError::InvalidState(format!(
20364 "unknown net socket {socket_id}"
20365 )));
20366 }
20367 Ok(Value::Null)
20368 }
20369 "net.socket_set_keep_alive" => {
20370 let socket_id = javascript_sync_rpc_arg_str(
20371 &request.args,
20372 0,
20373 "net.socket_set_keep_alive socket id",
20374 )?;
20375 let enable = javascript_sync_rpc_arg_bool(
20376 &request.args,
20377 1,
20378 "net.socket_set_keep_alive enabled",
20379 )?;
20380 let initial_delay_secs = javascript_sync_rpc_arg_u64_optional(
20381 &request.args,
20382 2,
20383 "net.socket_set_keep_alive initial delay seconds",
20384 )?;
20385 if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20386 socket.set_keep_alive(enable, initial_delay_secs)?;
20387 } else if !process.unix_sockets.contains_key(socket_id) {
20388 return Err(SidecarError::InvalidState(format!(
20389 "unknown net socket {socket_id}"
20390 )));
20391 }
20392 Ok(Value::Null)
20393 }
20394 "net.socket_upgrade_tls" => {
20395 let socket_id =
20396 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_upgrade_tls socket id")?;
20397 let options_json =
20398 javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_upgrade_tls options")?;
20399 let options: JavascriptTlsBridgeOptions =
20400 serde_json::from_str(options_json).map_err(|error| {
20401 SidecarError::InvalidState(format!(
20402 "net.socket_upgrade_tls options must be valid JSON: {error}"
20403 ))
20404 })?;
20405 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20406 SidecarError::InvalidState(format!(
20407 "unknown TCP socket {socket_id} for TLS upgrade"
20408 ))
20409 })?;
20410 socket.upgrade_tls(vm_id, kernel, options)?;
20411 Ok(Value::Null)
20412 }
20413 "net.socket_get_tls_client_hello" => {
20414 let socket_id = javascript_sync_rpc_arg_str(
20415 &request.args,
20416 0,
20417 "net.socket_get_tls_client_hello socket id",
20418 )?;
20419 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20420 SidecarError::InvalidState(format!(
20421 "unknown TCP socket {socket_id} for TLS client hello query"
20422 ))
20423 })?;
20424 socket.tls_client_hello_json(vm_id, kernel)
20425 }
20426 "net.socket_tls_query" => {
20427 let socket_id =
20428 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_tls_query socket id")?;
20429 let query =
20430 javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_tls_query query")?;
20431 let detailed = request
20432 .args
20433 .get(2)
20434 .and_then(Value::as_bool)
20435 .unwrap_or(false);
20436 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20437 SidecarError::InvalidState(format!("unknown TCP socket {socket_id} for TLS query"))
20438 })?;
20439 socket.tls_query(query, detailed)
20440 }
20441 "net.server_poll" => {
20442 let listener_id =
20443 javascript_sync_rpc_arg_str(&request.args, 0, "net.server_poll listener id")?;
20444 let wait_ms =
20445 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.server_poll wait ms")?
20446 .unwrap_or_default();
20447 let tcp_event = if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20448 Some(listener.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?)
20449 } else {
20450 None
20451 };
20452
20453 if let Some(event) = tcp_event {
20454 return match event {
20455 Some(JavascriptTcpListenerEvent::Connection(pending)) => {
20456 let PendingTcpSocket {
20457 stream,
20458 kernel_socket_id,
20459 preallocated,
20460 guest_local_addr,
20461 guest_remote_addr,
20462 } = pending;
20463 if !preallocated {
20464 if let Err(error) = check_network_resource_limit(
20465 resource_limits.max_sockets,
20466 network_counts.sockets,
20467 1,
20468 "socket",
20469 )
20470 .and_then(|()| {
20471 check_network_resource_limit(
20472 resource_limits.max_connections,
20473 network_counts.connections,
20474 1,
20475 "connection",
20476 )
20477 }) {
20478 if let Some(stream) = stream {
20479 let _ = stream.shutdown(Shutdown::Both);
20480 }
20481 return Ok(json!({
20482 "type": "error",
20483 "code": "EAGAIN",
20484 "message": error.to_string(),
20485 }));
20486 }
20487 }
20488 let socket = if let Some(stream) = stream {
20489 ActiveTcpSocket::from_stream(
20490 stream,
20491 Some(listener_id.to_string()),
20492 guest_local_addr,
20493 guest_remote_addr,
20494 )?
20495 } else {
20496 ActiveTcpSocket::from_kernel(
20497 kernel_socket_id.ok_or_else(|| {
20498 SidecarError::InvalidState(String::from(
20499 "kernel TCP accept missing socket id",
20500 ))
20501 })?,
20502 Some(listener_id.to_string()),
20503 guest_local_addr,
20504 guest_remote_addr,
20505 )
20506 };
20507 let socket_id = process.allocate_tcp_socket_id();
20508 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20509 listener.register_connection(&socket_id);
20510 }
20511 process.tcp_sockets.insert(socket_id.clone(), socket);
20512 Ok(json!({
20513 "type": "connection",
20514 "socketId": socket_id,
20515 "localAddress": guest_local_addr.ip().to_string(),
20516 "localPort": guest_local_addr.port(),
20517 "remoteAddress": guest_remote_addr.ip().to_string(),
20518 "remotePort": guest_remote_addr.port(),
20519 "remoteFamily": socket_addr_family(&guest_remote_addr),
20520 }))
20521 }
20522 Some(JavascriptTcpListenerEvent::Error { code, message }) => Ok(json!({
20523 "type": "error",
20524 "code": code,
20525 "message": message,
20526 })),
20527 None => Ok(Value::Null),
20528 };
20529 }
20530
20531 let event = {
20532 let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
20533 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20534 })?;
20535 listener.poll(Duration::from_millis(wait_ms))?
20536 };
20537
20538 match event {
20539 Some(JavascriptUnixListenerEvent::Connection(pending)) => {
20540 if let Err(error) = check_network_resource_limit(
20541 resource_limits.max_sockets,
20542 network_counts.sockets,
20543 1,
20544 "socket",
20545 )
20546 .and_then(|()| {
20547 check_network_resource_limit(
20548 resource_limits.max_connections,
20549 network_counts.connections,
20550 1,
20551 "connection",
20552 )
20553 }) {
20554 let _ = pending.stream.shutdown(Shutdown::Both);
20555 return Ok(json!({
20556 "type": "error",
20557 "code": "EAGAIN",
20558 "message": error.to_string(),
20559 }));
20560 }
20561 let socket = ActiveUnixSocket::from_stream(
20562 pending.stream,
20563 Some(listener_id.to_string()),
20564 pending.local_path.clone(),
20565 pending.remote_path.clone(),
20566 )?;
20567 let socket_id = process.allocate_unix_socket_id();
20568 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20569 listener.register_connection(&socket_id);
20570 }
20571 process.unix_sockets.insert(socket_id.clone(), socket);
20572 Ok(json!({
20573 "type": "connection",
20574 "socketId": socket_id,
20575 "localPath": pending.local_path,
20576 "remotePath": pending.remote_path,
20577 }))
20578 }
20579 Some(JavascriptUnixListenerEvent::Error { code, message }) => Ok(json!({
20580 "type": "error",
20581 "code": code,
20582 "message": message,
20583 })),
20584 None => Ok(Value::Null),
20585 }
20586 }
20587 "net.server_accept" => {
20588 let listener_id =
20589 javascript_sync_rpc_arg_str(&request.args, 0, "net.server_accept listener id")?;
20590 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20591 return match listener.poll(kernel, process.kernel_pid, Duration::ZERO)? {
20592 Some(JavascriptTcpListenerEvent::Connection(pending)) => {
20593 let PendingTcpSocket {
20594 stream,
20595 kernel_socket_id,
20596 preallocated,
20597 guest_local_addr,
20598 guest_remote_addr,
20599 } = pending;
20600 if !preallocated {
20601 check_network_resource_limit(
20602 resource_limits.max_sockets,
20603 network_counts.sockets,
20604 1,
20605 "socket",
20606 )?;
20607 check_network_resource_limit(
20608 resource_limits.max_connections,
20609 network_counts.connections,
20610 1,
20611 "connection",
20612 )?;
20613 }
20614 let info = json!({
20615 "localAddress": guest_local_addr.ip().to_string(),
20616 "localPort": guest_local_addr.port(),
20617 "localFamily": socket_addr_family(&guest_local_addr),
20618 "remoteAddress": guest_remote_addr.ip().to_string(),
20619 "remotePort": guest_remote_addr.port(),
20620 "remoteFamily": socket_addr_family(&guest_remote_addr),
20621 });
20622 let socket = if let Some(stream) = stream {
20623 ActiveTcpSocket::from_stream(
20624 stream,
20625 Some(listener_id.to_string()),
20626 guest_local_addr,
20627 guest_remote_addr,
20628 )?
20629 } else {
20630 ActiveTcpSocket::from_kernel(
20631 kernel_socket_id.ok_or_else(|| {
20632 SidecarError::InvalidState(String::from(
20633 "kernel TCP accept missing socket id",
20634 ))
20635 })?,
20636 Some(listener_id.to_string()),
20637 guest_local_addr,
20638 guest_remote_addr,
20639 )
20640 };
20641 let socket_id = process.allocate_tcp_socket_id();
20642 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20643 listener.register_connection(&socket_id);
20644 }
20645 process.tcp_sockets.insert(socket_id.clone(), socket);
20646 javascript_net_json_string(
20647 json!({
20648 "socketId": socket_id,
20649 "info": info,
20650 }),
20651 "net.server_accept",
20652 )
20653 }
20654 Some(JavascriptTcpListenerEvent::Error { code, message }) => {
20655 let detail = code.unwrap_or_else(|| String::from("server accept"));
20656 Err(SidecarError::Execution(format!("{detail}: {message}")))
20657 }
20658 None => Ok(javascript_net_timeout_value()),
20659 };
20660 }
20661
20662 let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
20663 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20664 })?;
20665 match listener.poll(Duration::ZERO)? {
20666 Some(JavascriptUnixListenerEvent::Connection(pending)) => {
20667 check_network_resource_limit(
20668 resource_limits.max_sockets,
20669 network_counts.sockets,
20670 1,
20671 "socket",
20672 )?;
20673 check_network_resource_limit(
20674 resource_limits.max_connections,
20675 network_counts.connections,
20676 1,
20677 "connection",
20678 )?;
20679 let info = json!({
20680 "localPath": pending.local_path.clone(),
20681 "remotePath": pending.remote_path.clone(),
20682 });
20683 let socket = ActiveUnixSocket::from_stream(
20684 pending.stream,
20685 Some(listener_id.to_string()),
20686 pending.local_path,
20687 pending.remote_path,
20688 )?;
20689 let socket_id = process.allocate_unix_socket_id();
20690 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20691 listener.register_connection(&socket_id);
20692 }
20693 process.unix_sockets.insert(socket_id.clone(), socket);
20694 javascript_net_json_string(
20695 json!({
20696 "socketId": socket_id,
20697 "info": info,
20698 }),
20699 "net.server_accept",
20700 )
20701 }
20702 Some(JavascriptUnixListenerEvent::Error { code, message }) => {
20703 let detail = code.unwrap_or_else(|| String::from("server accept"));
20704 Err(SidecarError::Execution(format!("{detail}: {message}")))
20705 }
20706 None => Ok(javascript_net_timeout_value()),
20707 }
20708 }
20709 "net.server_connections" => {
20710 let listener_id = javascript_sync_rpc_arg_str(
20711 &request.args,
20712 0,
20713 "net.server_connections listener id",
20714 )?;
20715 if let Some(listener) = process.tcp_listeners.get(listener_id) {
20716 Ok(json!(listener.active_connection_count()))
20717 } else {
20718 let listener = process.unix_listeners.get(listener_id).ok_or_else(|| {
20719 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20720 })?;
20721 Ok(json!(listener.active_connection_count()))
20722 }
20723 }
20724 "net.upgrade_socket_write" => {
20725 let socket_id = javascript_sync_rpc_arg_str(
20726 &request.args,
20727 0,
20728 "net.upgrade_socket_write socket id",
20729 )?;
20730 let chunk =
20731 javascript_sync_rpc_base64_arg(&request.args, 1, "net.upgrade_socket_write chunk")?;
20732 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20733 SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20734 })?;
20735 socket
20736 .write_all(kernel, process.kernel_pid, &chunk)
20737 .map(|written| json!(written))
20738 }
20739 "net.upgrade_socket_end" => {
20740 let socket_id =
20741 javascript_sync_rpc_arg_str(&request.args, 0, "net.upgrade_socket_end socket id")?;
20742 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20743 SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20744 })?;
20745 socket.shutdown_write(kernel, process.kernel_pid)?;
20746 Ok(Value::Null)
20747 }
20748 "net.upgrade_socket_destroy" => {
20749 let socket_id = javascript_sync_rpc_arg_str(
20750 &request.args,
20751 0,
20752 "net.upgrade_socket_destroy socket id",
20753 )?;
20754 let socket = process.tcp_sockets.remove(socket_id).ok_or_else(|| {
20755 SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20756 })?;
20757 if let Some(listener_id) = socket.listener_id.as_deref() {
20758 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20759 listener.release_connection(socket_id);
20760 }
20761 }
20762 let _ = socket.close(kernel, process.kernel_pid);
20763 Ok(Value::Null)
20764 }
20765 "net.write" => {
20766 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.write socket id")?;
20767 let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "net.write chunk")?;
20768 if let Some(socket) = process.tcp_sockets.get(socket_id) {
20769 socket
20770 .write_all(kernel, process.kernel_pid, &chunk)
20771 .map(|written| json!(written))
20772 } else {
20773 let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20774 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20775 })?;
20776 socket.write_all(&chunk).map(|written| json!(written))
20777 }
20778 }
20779 "net.shutdown" => {
20780 let socket_id =
20781 javascript_sync_rpc_arg_str(&request.args, 0, "net.shutdown socket id")?;
20782 if let Some(socket) = process.tcp_sockets.get(socket_id) {
20783 socket.shutdown_write(kernel, process.kernel_pid)?;
20784 } else {
20785 let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20786 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20787 })?;
20788 socket.shutdown_write()?;
20789 }
20790 Ok(Value::Null)
20791 }
20792 "net.destroy" => {
20793 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.destroy socket id")?;
20794 if let Some(socket) = process.tcp_sockets.remove(socket_id) {
20795 if let Some(listener_id) = socket.listener_id.as_deref() {
20796 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20797 listener.release_connection(socket_id);
20798 }
20799 }
20800 let _ = socket.close(kernel, process.kernel_pid);
20801 Ok(Value::Null)
20802 } else {
20803 let socket = process.unix_sockets.remove(socket_id).ok_or_else(|| {
20804 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20805 })?;
20806 if let Some(listener_id) = socket.listener_id.as_deref() {
20807 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20808 listener.release_connection(socket_id);
20809 }
20810 }
20811 let _ = socket.close();
20812 Ok(Value::Null)
20813 }
20814 }
20815 "net.server_close" => {
20816 let listener_id =
20817 javascript_sync_rpc_arg_str(&request.args, 0, "net.server_close listener id")?;
20818 if let Some(listener) = process.tcp_listeners.remove(listener_id) {
20819 listener.close(kernel, process.kernel_pid)?;
20820 Ok(Value::Null)
20821 } else {
20822 let listener = process.unix_listeners.remove(listener_id).ok_or_else(|| {
20823 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20824 })?;
20825 listener.close()?;
20826 Ok(Value::Null)
20827 }
20828 }
20829 "tls.get_ciphers" => javascript_net_json_string(
20830 Value::Array(
20831 tls_provider()
20832 .cipher_suites
20833 .iter()
20834 .filter_map(|suite| {
20835 suite
20836 .suite()
20837 .as_str()
20838 .map(|value| Value::String(value.to_owned()))
20839 })
20840 .collect(),
20841 ),
20842 "tls.get_ciphers",
20843 ),
20844 _ => Err(SidecarError::InvalidState(format!(
20845 "unsupported JavaScript net sync RPC method {}",
20846 request.method
20847 ))),
20848 }
20849}
20850
20851fn signal_name_for_stream_event(signal: i32) -> Option<&'static str> {
20852 match signal {
20853 libc::SIGHUP => Some("SIGHUP"),
20854 libc::SIGINT => Some("SIGINT"),
20855 libc::SIGUSR1 => Some("SIGUSR1"),
20856 libc::SIGALRM => Some("SIGALRM"),
20857 libc::SIGCONT => Some("SIGCONT"),
20858 libc::SIGTERM => Some("SIGTERM"),
20859 libc::SIGCHLD => Some("SIGCHLD"),
20860 libc::SIGWINCH => Some("SIGWINCH"),
20861 _ => None,
20862 }
20863}
20864
20865pub(crate) fn canonical_signal_name(signal: i32) -> Option<&'static str> {
20866 match signal {
20867 1 => Some("SIGHUP"),
20868 2 => Some("SIGINT"),
20869 3 => Some("SIGQUIT"),
20870 4 => Some("SIGILL"),
20871 5 => Some("SIGTRAP"),
20872 6 => Some("SIGABRT"),
20873 7 => Some("SIGBUS"),
20874 8 => Some("SIGFPE"),
20875 9 => Some("SIGKILL"),
20876 10 => Some("SIGUSR1"),
20877 11 => Some("SIGSEGV"),
20878 12 => Some("SIGUSR2"),
20879 13 => Some("SIGPIPE"),
20880 14 => Some("SIGALRM"),
20881 15 => Some("SIGTERM"),
20882 17 => Some("SIGCHLD"),
20883 18 => Some("SIGCONT"),
20884 19 => Some("SIGSTOP"),
20885 20 => Some("SIGTSTP"),
20886 21 => Some("SIGTTIN"),
20887 22 => Some("SIGTTOU"),
20888 23 => Some("SIGURG"),
20889 24 => Some("SIGXCPU"),
20890 25 => Some("SIGXFSZ"),
20891 26 => Some("SIGVTALRM"),
20892 27 => Some("SIGPROF"),
20893 28 => Some("SIGWINCH"),
20894 29 => Some("SIGIO"),
20895 30 => Some("SIGPWR"),
20896 31 => Some("SIGSYS"),
20897 _ => None,
20898 }
20899}
20900
20901fn dispatch_v8_process_signal(process: &ActiveProcess, signal: i32) -> Result<bool, SidecarError> {
20902 let Some(signal_name) = signal_name_for_stream_event(signal) else {
20903 return Ok(false);
20904 };
20905 process.execution.send_javascript_stream_event(
20906 "signal",
20907 json!({
20908 "signal": signal_name,
20909 "number": signal,
20910 "action": "default",
20911 }),
20912 )?;
20913 Ok(true)
20914}
20915
20916fn dispatch_v8_session_signal_async(session: V8SessionHandle, signal: i32) {
20917 let Some(signal_name) = signal_name_for_stream_event(signal).map(str::to_owned) else {
20918 return;
20919 };
20920 thread::spawn(move || {
20921 thread::sleep(Duration::from_millis(1));
20922 let payload = v8_runtime::json_to_cbor_payload(&json!({
20923 "signal": signal_name,
20924 "number": signal,
20925 "action": "default",
20926 }))
20927 .unwrap_or_default();
20928 let _ = session.send_stream_event("signal", payload);
20929 });
20930}
20931
20932pub(crate) fn parse_signal(signal: &str) -> Result<i32, SidecarError> {
20933 let trimmed = signal.trim();
20934 if trimmed.is_empty() {
20935 return Err(SidecarError::InvalidState(String::from(
20936 "kill_process requires a non-empty signal",
20937 )));
20938 }
20939
20940 if let Ok(value) = trimmed.parse::<i32>() {
20941 return match value {
20942 0..=31 => Ok(value),
20943 _ => Err(SidecarError::InvalidState(format!(
20944 "unsupported kill_process signal {signal}"
20945 ))),
20946 };
20947 }
20948
20949 let upper = trimmed.to_ascii_uppercase();
20950 let normalized = upper.strip_prefix("SIG").unwrap_or(&upper);
20951
20952 signal_number_from_name(normalized).ok_or_else(|| {
20953 SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
20954 })
20955}
20956
20957fn signal_number_from_name(signal: &str) -> Option<i32> {
20958 match signal {
20959 "0" => Some(0),
20960 "HUP" => Some(1),
20961 "INT" => Some(2),
20962 "QUIT" => Some(3),
20963 "ILL" => Some(4),
20964 "TRAP" => Some(5),
20965 "ABRT" | "IOT" => Some(6),
20966 "BUS" => Some(7),
20967 "FPE" => Some(8),
20968 "KILL" => Some(9),
20969 "USR1" => Some(10),
20970 "SEGV" => Some(11),
20971 "USR2" => Some(12),
20972 "PIPE" => Some(13),
20973 "ALRM" => Some(14),
20974 "TERM" => Some(15),
20975 "STKFLT" => Some(16),
20976 "CHLD" => Some(17),
20977 "CONT" => Some(18),
20978 "STOP" => Some(19),
20979 "TSTP" => Some(20),
20980 "TTIN" => Some(21),
20981 "TTOU" => Some(22),
20982 "URG" => Some(23),
20983 "XCPU" => Some(24),
20984 "XFSZ" => Some(25),
20985 "VTALRM" => Some(26),
20986 "PROF" => Some(27),
20987 "WINCH" => Some(28),
20988 "IO" | "POLL" => Some(29),
20989 "PWR" => Some(30),
20990 "SYS" => Some(31),
20991 _ => None,
20992 }
20993}
20994
20995pub(crate) fn runtime_child_is_alive(child_pid: u32) -> Result<bool, SidecarError> {
20996 Ok(runtime_child_exit_status(child_pid)?.is_none())
20997}
20998
20999#[cfg(not(target_os = "macos"))]
21000fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
21001 if child_pid == 0 {
21002 return Ok(Some(0));
21003 }
21004
21005 let wait_flags = WaitPidFlag::WNOHANG
21006 | WaitPidFlag::WNOWAIT
21007 | WaitPidFlag::WEXITED
21008 | WaitPidFlag::WUNTRACED
21009 | WaitPidFlag::WCONTINUED;
21010 match wait_on_child(WaitId::Pid(Pid::from_raw(child_pid as i32)), wait_flags) {
21011 Ok(WaitStatus::StillAlive)
21012 | Ok(WaitStatus::Stopped(_, _))
21013 | Ok(WaitStatus::Continued(_)) => Ok(None),
21014 Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
21015 Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
21016 #[cfg(any(target_os = "linux", target_os = "android"))]
21017 Ok(WaitStatus::PtraceEvent(_, _, _) | WaitStatus::PtraceSyscall(_)) => Ok(None),
21018 Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
21019 Err(error) => Err(SidecarError::Execution(format!(
21020 "failed to inspect guest runtime process {child_pid}: {error}"
21021 ))),
21022 }
21023}
21024
21025#[cfg(target_os = "macos")]
21031fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
21032 if child_pid == 0 {
21033 return Ok(Some(0));
21034 }
21035
21036 match waitpid(Pid::from_raw(child_pid as i32), Some(WaitPidFlag::WNOHANG)) {
21037 Ok(WaitStatus::StillAlive)
21038 | Ok(WaitStatus::Stopped(_, _))
21039 | Ok(WaitStatus::Continued(_)) => Ok(None),
21040 Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
21041 Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
21042 Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
21043 Err(error) => Err(SidecarError::Execution(format!(
21044 "failed to inspect guest runtime process {child_pid}: {error}"
21045 ))),
21046 }
21047}
21048
21049pub(crate) fn signal_runtime_process(child_pid: u32, signal: i32) -> Result<(), SidecarError> {
21050 if child_pid == 0 {
21051 return Ok(());
21052 }
21053
21054 if !runtime_child_is_alive(child_pid)? {
21055 return Ok(());
21056 }
21057
21058 if signal == 0 {
21059 return Ok(());
21060 }
21061
21062 let parsed = Signal::try_from(signal).map_err(|_| {
21063 SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
21064 })?;
21065 let result = send_signal(Pid::from_raw(child_pid as i32), Some(parsed));
21066
21067 match result {
21068 Ok(()) => Ok(()),
21069 Err(nix::errno::Errno::ESRCH) => Ok(()),
21070 Err(error) => Err(SidecarError::Execution(format!(
21071 "failed to signal guest runtime process {child_pid}: {error}"
21072 ))),
21073 }
21074}
21075
21076pub(crate) fn error_code(error: &SidecarError) -> &'static str {
21077 match error {
21078 SidecarError::InvalidState(_) => "invalid_state",
21079 SidecarError::ProtocolVersionMismatch(_) => "protocol_version_mismatch",
21080 SidecarError::BridgeVersionMismatch(_) => "bridge_version_mismatch",
21081 SidecarError::Conflict(_) => "conflict",
21082 SidecarError::Unauthorized(_) => "unauthorized",
21083 SidecarError::Unsupported(_) => "unsupported",
21084 SidecarError::FrameTooLarge(_) => "frame_too_large",
21085 SidecarError::Kernel(_) => "kernel_error",
21086 SidecarError::Plugin(_) => "plugin_error",
21087 SidecarError::Execution(_) => "execution_error",
21088 SidecarError::Bridge(_) => "bridge_error",
21089 SidecarError::Io(_) => "io_error",
21090 }
21091}
21092
21093fn guest_errno_code(message: &str) -> Option<&str> {
21094 const TRUSTED_PREFIXES: &[&str] = &[
21095 "ERR_AGENTOS_NODE_SYNC_RPC",
21096 "ERR_AGENTOS_PYTHON_VFS_RPC",
21097 "ERR_AGENTOS_BRIDGE",
21098 ];
21099
21100 let mut segments = message.split(':').map(str::trim);
21101 let first = segments.next()?;
21102 if is_guest_errno_segment(first) {
21103 return Some(first);
21104 }
21105
21106 if TRUSTED_PREFIXES.contains(&first) {
21107 let second = segments.next()?;
21108 if is_guest_errno_segment(second) {
21109 return Some(second);
21110 }
21111 }
21112
21113 None
21114}
21115
21116fn is_guest_errno_segment(segment: &str) -> bool {
21117 segment.len() >= 2
21118 && segment.starts_with('E')
21119 && !segment.starts_with("ERR_")
21120 && segment[1..]
21121 .bytes()
21122 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
21123}
21124
21125pub(crate) fn javascript_sync_rpc_error_code(error: &SidecarError) -> String {
21126 let message = error.to_string();
21127 if let Some(code) = guest_errno_code(&message) {
21128 return code.to_owned();
21129 }
21130 if message.starts_with("ERR_NATIVE_BINARY_NOT_SUPPORTED:") {
21131 return String::from("ERR_NATIVE_BINARY_NOT_SUPPORTED");
21132 }
21133
21134 let lower = message.to_ascii_lowercase();
21135 if lower.contains("no such file or directory")
21136 || lower.contains("entry not found")
21137 || lower.contains("not found")
21138 {
21139 return String::from("ENOENT");
21140 }
21141 if lower.contains("permission denied") {
21142 return String::from("EACCES");
21143 }
21144 if lower.contains("already exists")
21145 || lower.contains("already registered")
21146 || lower.contains("file exists")
21147 {
21148 return String::from("EEXIST");
21149 }
21150 if lower.contains("invalid argument") {
21151 return String::from("EINVAL");
21152 }
21153
21154 String::from("ERR_AGENTOS_NODE_SYNC_RPC")
21155}
21156
21157pub(crate) fn ignore_stale_javascript_sync_rpc_response(
21158 error: SidecarError,
21159) -> Result<(), SidecarError> {
21160 match error {
21161 SidecarError::Execution(message)
21162 if message.ends_with("is no longer pending")
21163 && message.starts_with("sync RPC request ") =>
21164 {
21165 Ok(())
21166 }
21167 SidecarError::Execution(message) => {
21168 let lower = message.to_ascii_lowercase();
21169 if lower.contains("sync rpc response")
21170 && (lower.contains("broken pipe") || lower.contains("channel closed unexpectedly"))
21171 {
21172 Ok(())
21173 } else {
21174 Err(SidecarError::Execution(message))
21175 }
21176 }
21177 other => Err(other),
21178 }
21179}
21180
21181#[cfg(test)]
21182mod error_code_tests {
21183 use super::{guest_errno_code, javascript_sync_rpc_error_code, SidecarError};
21184
21185 #[test]
21186 fn guest_errno_code_rejects_guest_controlled_errno_segments() {
21187 assert_eq!(guest_errno_code("user said 'EACCES: denied'"), None);
21188 assert_eq!(
21189 guest_errno_code("prefix: user said 'EPERM': more text"),
21190 None
21191 );
21192 assert_eq!(guest_errno_code("ERR_AGENTOS_FAKE: EACCES: denied"), None);
21193 }
21194
21195 #[test]
21196 fn guest_errno_code_accepts_trusted_secure_exec_prefixes() {
21197 assert_eq!(
21198 guest_errno_code("ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo"),
21199 Some("EACCES")
21200 );
21201 assert_eq!(
21202 guest_errno_code("ERR_AGENTOS_PYTHON_VFS_RPC: ENOENT: missing file"),
21203 Some("ENOENT")
21204 );
21205 assert_eq!(guest_errno_code("EEXIST: already exists"), Some("EEXIST"));
21206 }
21207
21208 #[test]
21209 fn javascript_sync_rpc_error_code_ignores_spoofed_errnos() {
21210 let error = SidecarError::Execution(String::from("user said 'EACCES: denied'"));
21211 assert_eq!(
21212 javascript_sync_rpc_error_code(&error),
21213 "ERR_AGENTOS_NODE_SYNC_RPC"
21214 );
21215 }
21216
21217 #[test]
21218 fn javascript_sync_rpc_error_code_preserves_real_sidecar_errnos() {
21219 let error = SidecarError::Execution(String::from(
21220 "ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo",
21221 ));
21222 assert_eq!(javascript_sync_rpc_error_code(&error), "EACCES");
21223 }
21224
21225 #[test]
21226 fn javascript_sync_rpc_error_code_maps_file_exists_messages() {
21227 let error = SidecarError::Io(String::from(
21228 "failed to create mapped guest directory /.next/server: File exists (os error 17)",
21229 ));
21230 assert_eq!(javascript_sync_rpc_error_code(&error), "EEXIST");
21231 }
21232
21233 #[test]
21234 fn javascript_sync_rpc_error_code_preserves_native_binary_rejections() {
21235 let error = SidecarError::Execution(String::from(
21236 "ERR_NATIVE_BINARY_NOT_SUPPORTED: refused to execute native ELF guest binary at /tmp/fake-rg inside the VM",
21237 ));
21238 assert_eq!(
21239 javascript_sync_rpc_error_code(&error),
21240 "ERR_NATIVE_BINARY_NOT_SUPPORTED"
21241 );
21242 }
21243}
21244#[cfg(test)]
21245mod ssrf_egress_classifier_tests {
21246 use super::{
21256 filter_dns_safe_ip_addrs, is_loopback_ip, restricted_non_loopback_ip_range, SidecarError,
21257 };
21258 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
21259
21260 fn assert_restricted(ip: IpAddr, expected_label: &str) {
21261 let classification = restricted_non_loopback_ip_range(ip);
21262 assert!(
21263 classification.is_some(),
21264 "{ip} must be classified as a restricted egress target"
21265 );
21266 let (_cidr, label) = classification.unwrap();
21267 assert_eq!(
21268 label, expected_label,
21269 "{ip} should be labelled {expected_label}, got {label}"
21270 );
21271 }
21272
21273 fn assert_dns_denied(ip: IpAddr, label: &str) {
21274 match filter_dns_safe_ip_addrs(vec![ip], "attacker.example") {
21275 Err(SidecarError::Execution(message)) => assert!(
21276 message.starts_with("EACCES:"),
21277 "{label}: egress filter must deny with EACCES, got: {message}"
21278 ),
21279 other => panic!("{label}: expected EACCES denial, got {other:?}"),
21280 }
21281 }
21282
21283 #[test]
21285 fn classifier_denies_unspecified_and_cgnat_targets() {
21286 assert_restricted(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "unspecified");
21288 assert_restricted(IpAddr::V6(Ipv6Addr::UNSPECIFIED), "unspecified");
21290
21291 assert_restricted(
21293 IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
21294 "carrier-grade-nat",
21295 );
21296 assert_restricted(
21297 IpAddr::V4(Ipv4Addr::new(100, 127, 255, 254)),
21298 "carrier-grade-nat",
21299 );
21300
21301 assert!(
21303 restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 63, 255, 255)))
21304 .is_none(),
21305 "100.63.255.255 is outside CGNAT and must remain allowed"
21306 );
21307 assert!(
21308 restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 128, 0, 0))).is_none(),
21309 "100.128.0.0 is outside CGNAT and must remain allowed"
21310 );
21311
21312 assert_dns_denied(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "0.0.0.0 (unspecified)");
21314 assert_dns_denied(IpAddr::V6(Ipv6Addr::UNSPECIFIED), ":: (unspecified)");
21315 assert_dns_denied(
21316 IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
21317 "100.64.0.1 (CGNAT)",
21318 );
21319 }
21320
21321 #[test]
21323 fn classifier_denies_ipv6_spelled_metadata_addresses() {
21324 let mapped = "::ffff:169.254.169.254".parse::<Ipv6Addr>().unwrap();
21327 assert_restricted(IpAddr::V6(mapped), "link-local");
21328
21329 let compat = "::169.254.169.254".parse::<Ipv6Addr>().unwrap();
21330 assert_restricted(IpAddr::V6(compat), "link-local");
21331
21332 assert_restricted(
21334 IpAddr::V6("::10.0.0.1".parse::<Ipv6Addr>().unwrap()),
21335 "private",
21336 );
21337 assert_restricted(
21338 IpAddr::V6("::100.64.0.1".parse::<Ipv6Addr>().unwrap()),
21339 "carrier-grade-nat",
21340 );
21341
21342 assert_eq!(
21346 restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::UNSPECIFIED)),
21347 Some(("::/128", "unspecified")),
21348 ":: must classify as unspecified, not via the IPv4-compat path"
21349 );
21350 assert!(
21351 restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::LOCALHOST)).is_none()
21352 || is_loopback_ip(IpAddr::V6(Ipv6Addr::LOCALHOST)),
21353 "::1 must not be classified as a restricted IPv4-compatible target"
21354 );
21355 assert!(
21356 restricted_non_loopback_ip_range(IpAddr::V6("::8.8.8.8".parse::<Ipv6Addr>().unwrap()))
21357 .is_none(),
21358 "::8.8.8.8 (public IPv4-compatible) must remain allowed"
21359 );
21360
21361 assert_dns_denied(
21363 IpAddr::V6("::169.254.169.254".parse::<Ipv6Addr>().unwrap()),
21364 "::169.254.169.254 (IPv4-compat metadata)",
21365 );
21366 }
21367
21368 #[test]
21370 fn classifier_denies_reserved_and_multicast_targets() {
21371 assert_restricted(IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)), "multicast");
21375 assert_restricted(IpAddr::V4(Ipv4Addr::new(239, 255, 255, 255)), "multicast");
21376 assert_restricted(IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)), "reserved");
21377 assert_restricted(IpAddr::V4(Ipv4Addr::BROADCAST), "reserved");
21379
21380 assert_restricted(
21382 IpAddr::V6("::224.0.0.1".parse::<Ipv6Addr>().unwrap()),
21383 "multicast",
21384 );
21385 assert_restricted(
21386 IpAddr::V6("::240.0.0.1".parse::<Ipv6Addr>().unwrap()),
21387 "reserved",
21388 );
21389
21390 assert!(
21392 restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(223, 255, 255, 255)))
21393 .is_none(),
21394 "223.255.255.255 is outside 224/4 and must remain allowed"
21395 );
21396
21397 assert_dns_denied(
21399 IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)),
21400 "240.0.0.1 (reserved)",
21401 );
21402 assert_dns_denied(
21403 IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)),
21404 "224.0.0.1 (multicast)",
21405 );
21406 }
21407}
21408
21409#[cfg(test)]
21418mod dns_rebinding_pin_tests {
21419 use super::{issue_outbound_http_request, split_netloc, JavascriptHttpRequestOptions};
21420 use std::collections::BTreeMap;
21421 use std::io::{Read, Write};
21422 use std::net::{IpAddr, Ipv4Addr, TcpListener};
21423 use std::thread;
21424 use url::Url;
21425
21426 fn empty_headers() -> super::HttpHeaderCollection {
21427 super::parse_http_header_collection(&BTreeMap::new(), "test headers")
21428 .expect("empty header collection")
21429 }
21430
21431 fn options() -> JavascriptHttpRequestOptions {
21432 JavascriptHttpRequestOptions {
21433 method: Some(String::from("GET")),
21434 headers: BTreeMap::new(),
21435 body: None,
21436 reject_unauthorized: None,
21437 }
21438 }
21439
21440 #[test]
21441 fn split_netloc_handles_hostnames_and_bracketed_ipv6() {
21442 assert_eq!(
21443 split_netloc("attacker.example:80"),
21444 Some(("attacker.example", 80))
21445 );
21446 assert_eq!(split_netloc("[::1]:443"), Some(("::1", 443)));
21447 assert_eq!(split_netloc("10.0.0.1:8080"), Some(("10.0.0.1", 8080)));
21448 assert_eq!(split_netloc("no-port"), None);
21449 assert_eq!(split_netloc("host:notaport"), None);
21450 }
21451
21452 #[test]
21458 fn outbound_http_connect_is_pinned_to_vetted_ip() {
21459 let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind loopback server");
21460 let port = listener.local_addr().expect("local addr").port();
21461 let server = thread::spawn(move || {
21462 let (mut stream, _) = listener.accept().expect("accept");
21463 let mut buf = [0u8; 1024];
21464 let _ = stream.read(&mut buf);
21465 stream
21466 .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi")
21467 .expect("write response");
21468 let _ = stream.flush();
21469 });
21470
21471 let url = Url::parse(&format!("http://attacker.example:{port}/")).expect("url");
21472 let pinned = vec![IpAddr::V4(Ipv4Addr::LOCALHOST)];
21473 let result = issue_outbound_http_request(&url, &options(), &empty_headers(), &pinned)
21474 .expect("pinned request should reach the vetted loopback target");
21475 let payload = result.as_str().expect("string payload");
21476 assert!(
21477 payload.contains("\"status\":200"),
21478 "expected 200 from pinned target, got: {payload}"
21479 );
21480 server.join().expect("server thread");
21481 }
21482
21483 #[test]
21487 fn outbound_http_refuses_when_no_vetted_address() {
21488 let url = Url::parse("https://attacker.example/").expect("url");
21489 let error = issue_outbound_http_request(&url, &options(), &empty_headers(), &[])
21490 .expect_err("empty pinned set must be refused");
21491 let message = error.to_string();
21492 assert!(
21493 message.contains("EACCES") || message.contains("ERR_HTTP_REQUEST_FAILED"),
21494 "expected an egress refusal, got: {message}"
21495 );
21496 }
21497}