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, JavascriptSocketFamily, JavascriptSocketPathContext,
38 JavascriptTcpListenerEvent, JavascriptTcpSocketEvent, JavascriptTlsBridgeOptions,
39 JavascriptTlsClientHello, JavascriptTlsDataValue, JavascriptTlsMaterial, JavascriptUdpFamily,
40 JavascriptUdpSocketEvent, JavascriptUnixListenerEvent, NetworkResourceCounts, PendingTcpSocket,
41 PendingUnixSocket, ProcNetEntry, ProcessEventEnvelope, ResolvedChildProcessExecution,
42 ResolvedTcpConnectAddr, SharedBridge, SharedSidecarRequestClient, SidecarKernel,
43 SocketQueryKind, ToolExecution, VmDnsConfig, VmListenPolicy, VmState,
44 DEFAULT_JAVASCRIPT_NET_BACKLOG, EXECUTION_DRIVER_NAME, EXECUTION_SANDBOX_ROOT_ENV,
45 JAVASCRIPT_COMMAND, LOOPBACK_EXEMPT_PORTS_ENV, MAPPED_HOST_FD_START, PYTHON_COMMAND,
46 TOOL_DRIVER_NAME, VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY, WASM_COMMAND,
47 WASM_STDIO_SYNC_RPC_ENV,
48};
49use crate::tools::{
50 format_tool_failure_output, is_tool_command, normalized_tool_command_name,
51 resolve_tool_command, ToolCommandResolution,
52};
53use crate::wire::{ProtocolFrame as WireProtocolFrame, WireFrameCodec, DEFAULT_MAX_FRAME_BYTES};
54use crate::{DispatchResult, NativeSidecar, NativeSidecarBridge, SidecarError};
55
56use base64::Engine;
57use bytes::Bytes;
58use h2::{client, server, Reason};
59use hickory_resolver::proto::rr::{RData, Record, RecordType};
60use hmac::{Hmac, Mac};
61use http::{HeaderMap, HeaderName, HeaderValue, Method, Request, Response, Uri};
62use md5::Md5;
63use nix::libc;
64use nix::sys::signal::{kill as send_signal, Signal};
65use nix::sys::wait::{waitid as wait_on_child, Id as WaitId, WaitPidFlag, WaitStatus};
66use nix::unistd::Pid;
67use openssl::bn::{BigNum, BigNumContext};
68use openssl::derive::Deriver;
69use openssl::dh::Dh;
70use openssl::ec::{EcGroup, EcKey, EcPoint, PointConversionForm};
71use openssl::hash::MessageDigest;
72use openssl::nid::Nid;
73use openssl::pkey::{Id as PKeyId, PKey, Params, Private, Public};
74use openssl::rand::rand_bytes;
75use openssl::rsa::{Padding, Rsa};
76use openssl::sign::{Signer, Verifier};
77use openssl::symm::{Cipher, Crypter, Mode};
78use pbkdf2::pbkdf2_hmac;
79use rusqlite::types::ValueRef as SqliteValueRef;
80use rusqlite::{
81 Connection as SqliteConnection, OpenFlags as SqliteOpenFlags, Statement as SqliteStatement,
82};
83use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
84use rustls::crypto::aws_lc_rs;
85use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
86use rustls::{
87 ClientConfig, ClientConnection, DigitallySignedStruct, RootCertStore, ServerConfig,
88 ServerConnection, SignatureScheme,
89};
90use scrypt::{scrypt, Params as ScryptParams};
91use secure_exec_bridge::LifecycleState;
92use secure_exec_execution::wasm::{
93 WasmExecutionError, WASM_MAX_FUEL_ENV, WASM_MAX_MEMORY_BYTES_ENV, WASM_MAX_STACK_BYTES_ENV,
94};
95use secure_exec_execution::{
96 javascript::handle_internal_bridge_call_from_host_context, v8_host::V8SessionHandle,
97 v8_runtime, CreateJavascriptContextRequest, CreatePythonContextRequest,
98 CreateWasmContextRequest, JavascriptExecutionEvent, JavascriptSyncRpcRequest, ModuleFsReader,
99 NodeSignalDispositionAction, NodeSignalHandlerRegistration, PythonExecutionEvent,
100 PythonVfsRpcMethod, PythonVfsRpcRequest, PythonVfsRpcResponsePayload,
101 StartJavascriptExecutionRequest, StartPythonExecutionRequest, StartWasmExecutionRequest,
102 WasmExecutionEvent, WasmPermissionTier as ExecutionWasmPermissionTier,
103};
104use secure_exec_kernel::dns::{
105 DnsLookupPolicy, DnsRecordResolution, DnsResolutionSource as KernelDnsResolutionSource,
106};
107use secure_exec_kernel::kernel::{KernelProcessHandle, SpawnOptions, VirtualProcessOptions};
108use secure_exec_kernel::permissions::NetworkOperation;
109use secure_exec_kernel::poll::{PollEvents, PollFd, PollTargetEntry, POLLERR, POLLHUP, POLLIN};
110use secure_exec_kernel::process_table::{ProcessStatus, WaitPidFlags, SIGKILL, SIGTERM};
111use secure_exec_kernel::pty::LineDisciplineConfig;
112use secure_exec_kernel::resource_accounting::ResourceLimits;
113use secure_exec_kernel::root_fs::RootFilesystemMode;
114use secure_exec_kernel::socket_table::{
115 InetSocketAddress, SocketDomain, SocketId, SocketShutdown as KernelSocketShutdown, SocketSpec,
116 SocketState, SocketType,
117};
118use serde::{Deserialize, Serialize};
119use serde_json::{json, Map, Value};
120use sha1::Sha1;
121use sha2::{digest::Digest, Sha256, Sha512};
122use socket2::{SockRef, TcpKeepalive};
123use std::collections::VecDeque;
124use std::collections::{BTreeMap, BTreeSet};
125use std::fmt;
126use std::fs;
127use std::io::{Cursor, Read, Write};
128use std::net::{
129 IpAddr, Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr, TcpListener, TcpStream, ToSocketAddrs,
130 UdpSocket,
131};
132use std::os::unix::fs::{MetadataExt, PermissionsExt};
133use std::os::unix::net::{SocketAddr as UnixSocketAddr, UnixListener, UnixStream};
134use std::path::{Path, PathBuf};
135use std::pin::Pin;
136use std::sync::atomic::{AtomicBool, Ordering};
137use std::sync::mpsc::{self, RecvTimeoutError, Sender};
138use std::sync::{Arc, Mutex, OnceLock, Weak};
139use std::thread;
140use std::time::{Duration, Instant};
141use tokio::io::{AsyncRead, AsyncWrite};
142use tokio::runtime::Builder as TokioRuntimeBuilder;
143use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
144use tokio_rustls::{TlsAcceptor, TlsConnector};
145use url::Url;
146
147const DEFAULT_KERNEL_STDIN_READ_MAX_BYTES: usize = 64 * 1024;
148const DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS: u64 = 100;
149const JAVASCRIPT_NET_TIMEOUT_SENTINEL: &str = "__secure_exec_net_timeout__";
150const PYTHON_PYODIDE_GUEST_ROOT: &str = "/__agent_os_pyodide";
151const PYTHON_PYODIDE_CACHE_GUEST_ROOT: &str = "/__agent_os_pyodide_cache";
152const TCP_SOCKET_POLL_TIMEOUT: Duration = Duration::from_millis(100);
153const TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5);
154const HTTP_LOOPBACK_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
155pub(crate) const MAX_PER_PROCESS_STATE_HANDLES: usize = 1024;
156const VM_FETCH_BUFFER_LIMIT_BYTES: usize = DEFAULT_MAX_FRAME_BYTES;
157const DEFAULT_SCRYPT_COST: u64 = 16_384;
158const DEFAULT_SCRYPT_BLOCK_SIZE: u32 = 8;
159const DEFAULT_SCRYPT_PARALLELIZATION: u32 = 1;
160const SQLITE_JS_SAFE_INTEGER_MAX: i64 = 9_007_199_254_740_991;
161
162trait Http2AsyncIo: AsyncRead + AsyncWrite + Unpin + Send {}
163
164impl<T> Http2AsyncIo for T where T: AsyncRead + AsyncWrite + Unpin + Send {}
165
166const DEFAULT_ALLOWED_NODE_BUILTINS: &[&str] = &[
167 "assert",
168 "buffer",
169 "console",
170 "child_process",
171 "crypto",
172 "dns",
173 "events",
174 "fs",
175 "http",
176 "http2",
177 "https",
178 "module",
179 "os",
180 "path",
181 "perf_hooks",
182 "querystring",
183 "sqlite",
184 "stream",
185 "string_decoder",
186 "timers",
187 "tls",
188 "tty",
189 "url",
190 "util",
191 "zlib",
192];
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195enum JavascriptCryptoDigestAlgorithm {
196 Md5,
197 Sha1,
198 Sha256,
199 Sha512,
200}
201
202#[derive(Debug, Default, Deserialize)]
203#[serde(default, rename_all = "camelCase")]
204struct JavascriptScryptOptions {
205 #[serde(alias = "N")]
206 cost: Option<u64>,
207 #[serde(alias = "r")]
208 block_size: Option<u32>,
209 #[serde(alias = "p")]
210 parallelization: Option<u32>,
211}
212
213#[derive(Debug, Deserialize)]
214#[serde(rename_all = "camelCase")]
215struct JavascriptHttpListenRequest {
216 server_id: u64,
217 #[serde(default)]
218 port: Option<u16>,
219 #[serde(default)]
220 hostname: Option<String>,
221}
222
223#[derive(Debug, Default, Deserialize)]
224#[serde(default, rename_all = "camelCase")]
225struct JavascriptHttpRequestOptions {
226 method: Option<String>,
227 headers: BTreeMap<String, Value>,
228 body: Option<String>,
229 reject_unauthorized: Option<bool>,
230}
231
232#[derive(Debug, Default, Deserialize)]
233#[serde(default, rename_all = "camelCase")]
234struct JavascriptHttp2ServerListenRequest {
235 server_id: u64,
236 secure: bool,
237 port: Option<u16>,
238 host: Option<String>,
239 backlog: Option<u32>,
240 timeout: Option<u64>,
241 settings: BTreeMap<String, Value>,
242 tls: Option<JavascriptTlsBridgeOptions>,
243}
244
245#[derive(Debug, Default, Deserialize)]
246#[serde(default, rename_all = "camelCase")]
247struct JavascriptHttp2SessionConnectRequest {
248 authority: Option<String>,
249 protocol: Option<String>,
250 host: Option<String>,
251 port: Option<u16>,
252 settings: BTreeMap<String, Value>,
253 tls: Option<JavascriptTlsBridgeOptions>,
254}
255
256#[derive(Debug, Default, Deserialize)]
257#[serde(default, rename_all = "camelCase")]
258struct JavascriptHttp2RequestOptions {
259 end_stream: bool,
260}
261
262#[derive(Debug, Default, Deserialize)]
263#[serde(default, rename_all = "camelCase")]
264struct JavascriptHttp2FileResponseOptions {
265 offset: Option<u64>,
266 length: Option<i64>,
267}
268
269#[derive(Debug, Clone)]
270struct HttpHeaderCollection {
271 normalized: BTreeMap<String, Vec<String>>,
272 raw_pairs: Vec<(String, String)>,
273}
274
275#[derive(Debug)]
276struct InsecureTlsVerifier {
277 supported_schemes: Vec<SignatureScheme>,
278}
279
280impl ServerCertVerifier for InsecureTlsVerifier {
281 fn verify_server_cert(
282 &self,
283 _end_entity: &CertificateDer<'_>,
284 _intermediates: &[CertificateDer<'_>],
285 _server_name: &ServerName<'_>,
286 _ocsp_response: &[u8],
287 _now: rustls::pki_types::UnixTime,
288 ) -> Result<ServerCertVerified, rustls::Error> {
289 Ok(ServerCertVerified::assertion())
290 }
291
292 fn verify_tls12_signature(
293 &self,
294 _message: &[u8],
295 _cert: &CertificateDer<'_>,
296 _dss: &DigitallySignedStruct,
297 ) -> Result<HandshakeSignatureValid, rustls::Error> {
298 Ok(HandshakeSignatureValid::assertion())
299 }
300
301 fn verify_tls13_signature(
302 &self,
303 _message: &[u8],
304 _cert: &CertificateDer<'_>,
305 _dss: &DigitallySignedStruct,
306 ) -> Result<HandshakeSignatureValid, rustls::Error> {
307 Ok(HandshakeSignatureValid::assertion())
308 }
309
310 fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
311 self.supported_schemes.clone()
312 }
313}
314
315impl ActiveProcess {
316 pub(crate) fn new(
317 kernel_pid: u32,
318 kernel_handle: KernelProcessHandle,
319 runtime: GuestRuntimeKind,
320 execution: ActiveExecution,
321 ) -> Self {
322 Self {
323 kernel_pid,
324 kernel_handle,
325 kernel_stdin_writer_fd: None,
326 runtime,
327 detached: false,
328 execution,
329 guest_cwd: String::from("/"),
330 env: BTreeMap::new(),
331 host_cwd: PathBuf::from("/"),
332 mapped_host_fds: BTreeMap::new(),
333 next_mapped_host_fd: MAPPED_HOST_FD_START,
334 pending_execution_events: VecDeque::new(),
335 pending_self_signal_exit: None,
336 child_processes: BTreeMap::new(),
337 next_child_process_id: 0,
338 http_servers: BTreeMap::new(),
339 pending_http_requests: BTreeMap::new(),
340 http2: Default::default(),
341 tcp_listeners: BTreeMap::new(),
342 next_tcp_listener_id: 0,
343 tcp_sockets: BTreeMap::new(),
344 next_tcp_socket_id: 0,
345 tcp_port_reservations: BTreeMap::new(),
346 next_tcp_port_reservation_id: 0,
347 unix_listeners: BTreeMap::new(),
348 next_unix_listener_id: 0,
349 unix_sockets: BTreeMap::new(),
350 next_unix_socket_id: 0,
351 udp_sockets: BTreeMap::new(),
352 next_udp_socket_id: 0,
353 cipher_sessions: BTreeMap::new(),
354 next_cipher_session_id: 0,
355 diffie_hellman_sessions: BTreeMap::new(),
356 next_diffie_hellman_session_id: 0,
357 sqlite_databases: BTreeMap::new(),
358 next_sqlite_database_id: 0,
359 sqlite_statements: BTreeMap::new(),
360 next_sqlite_statement_id: 0,
361 module_resolution_cache: secure_exec_execution::LocalModuleResolutionCache::default(),
362 }
363 }
364
365 pub(crate) fn queue_pending_execution_event(
366 &mut self,
367 event: ActiveExecutionEvent,
368 ) -> Result<(), SidecarError> {
369 if self.pending_execution_events.len() >= MAX_PROCESS_EVENT_QUEUE {
370 return Err(process_event_queue_overflow_error());
371 }
372 self.pending_execution_events.push_back(event);
373 Ok(())
374 }
375
376 pub(crate) fn with_host_cwd(mut self, host_cwd: PathBuf) -> Self {
377 self.host_cwd = host_cwd;
378 self
379 }
380
381 pub(crate) fn with_guest_cwd(mut self, guest_cwd: String) -> Self {
382 self.guest_cwd = guest_cwd;
383 self
384 }
385
386 pub(crate) fn with_env(mut self, env: BTreeMap<String, String>) -> Self {
387 self.env = env;
388 self
389 }
390
391 pub(crate) fn with_kernel_stdin_writer_fd(mut self, fd: u32) -> Self {
392 self.kernel_stdin_writer_fd = Some(fd);
393 self
394 }
395
396 pub(crate) fn with_detached(mut self, detached: bool) -> Self {
397 self.detached = detached;
398 self
399 }
400
401 pub(crate) fn allocate_mapped_host_fd(&mut self, fd: ActiveMappedHostFd) -> u32 {
402 let handle = self.next_mapped_host_fd;
403 self.next_mapped_host_fd = self
404 .next_mapped_host_fd
405 .checked_add(1)
406 .unwrap_or(MAPPED_HOST_FD_START);
407 self.mapped_host_fds.insert(handle, fd);
408 handle
409 }
410
411 pub(crate) fn mapped_host_fd(&self, fd: u32) -> Option<&ActiveMappedHostFd> {
412 self.mapped_host_fds.get(&fd)
413 }
414
415 pub(crate) fn mapped_host_fd_mut(&mut self, fd: u32) -> Option<&mut ActiveMappedHostFd> {
416 self.mapped_host_fds.get_mut(&fd)
417 }
418
419 pub(crate) fn close_mapped_host_fd(&mut self, fd: u32) -> bool {
420 self.mapped_host_fds.remove(&fd).is_some()
421 }
422
423 pub(crate) fn allocate_child_process_id(&mut self) -> String {
424 self.next_child_process_id += 1;
425 format!("child-{}", self.next_child_process_id)
426 }
427
428 fn allocate_tcp_listener_id(&mut self) -> String {
429 self.next_tcp_listener_id += 1;
430 format!("listener-{}", self.next_tcp_listener_id)
431 }
432
433 fn allocate_tcp_socket_id(&mut self) -> String {
434 self.next_tcp_socket_id += 1;
435 format!("socket-{}", self.next_tcp_socket_id)
436 }
437
438 fn allocate_tcp_port_reservation_id(&mut self) -> String {
439 self.next_tcp_port_reservation_id += 1;
440 format!("tcp-port-reservation-{}", self.next_tcp_port_reservation_id)
441 }
442
443 fn allocate_unix_listener_id(&mut self) -> String {
444 self.next_unix_listener_id += 1;
445 format!("unix-listener-{}", self.next_unix_listener_id)
446 }
447
448 fn allocate_unix_socket_id(&mut self) -> String {
449 self.next_unix_socket_id += 1;
450 format!("unix-socket-{}", self.next_unix_socket_id)
451 }
452
453 fn allocate_udp_socket_id(&mut self) -> String {
454 self.next_udp_socket_id += 1;
455 format!("udp-socket-{}", self.next_udp_socket_id)
456 }
457
458 pub(crate) fn network_resource_counts(&self) -> NetworkResourceCounts {
459 let mut counts = NetworkResourceCounts {
460 sockets: self.http_servers.len()
461 + self.tcp_listeners.len()
462 + self.tcp_sockets.len()
463 + self.unix_listeners.len()
464 + self.unix_sockets.len()
465 + self.udp_sockets.len(),
466 connections: self.tcp_sockets.len() + self.unix_sockets.len(),
467 };
468 if let Ok(http2) = self.http2.shared.lock() {
469 counts.sockets += http2.servers.len() + http2.sessions.len();
470 counts.connections += http2.sessions.len();
471 }
472
473 for child in self.child_processes.values() {
474 let child_counts = child.network_resource_counts();
475 counts.sockets += child_counts.sockets;
476 counts.connections += child_counts.connections;
477 }
478
479 counts
480 }
481
482 fn sidecar_only_network_resource_counts(&self) -> NetworkResourceCounts {
483 let mut counts = NetworkResourceCounts {
484 sockets: self.http_servers.len()
485 + self
486 .tcp_listeners
487 .values()
488 .filter(|listener| listener.kernel_socket_id.is_none())
489 .count()
490 + self
491 .tcp_sockets
492 .values()
493 .filter(|socket| socket.kernel_socket_id.is_none())
494 .count()
495 + self.unix_listeners.len()
496 + self.unix_sockets.len()
497 + self
498 .udp_sockets
499 .values()
500 .filter(|socket| socket.kernel_socket_id.is_none())
501 .count(),
502 connections: self
503 .tcp_sockets
504 .values()
505 .filter(|socket| socket.kernel_socket_id.is_none())
506 .count()
507 + self.unix_sockets.len(),
508 };
509 if let Ok(http2) = self.http2.shared.lock() {
510 counts.sockets += http2.servers.len() + http2.sessions.len();
511 counts.connections += http2.sessions.len();
512 }
513
514 for child in self.child_processes.values() {
515 let child_counts = child.sidecar_only_network_resource_counts();
516 counts.sockets += child_counts.sockets;
517 counts.connections += child_counts.connections;
518 }
519
520 counts
521 }
522}
523
524fn poll_tool_process_event(
525 execution: &ToolExecution,
526) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
527 let event = execution
528 .pending_events
529 .lock()
530 .unwrap_or_else(|poisoned| poisoned.into_inner())
531 .pop_front();
532 if event.is_some() {
533 return Ok(event);
534 }
535 if execution.events_overflowed.load(Ordering::Relaxed) {
536 return Err(process_event_queue_overflow_error());
537 }
538 Ok(None)
539}
540
541fn descendant_pending_execution_event_capacity(
542 root: &ActiveProcess,
543 child_path: &[&str],
544) -> Option<usize> {
545 let mut child = root;
546 for child_process_id in child_path {
547 child = child.child_processes.get(*child_process_id)?;
548 }
549 Some(MAX_PROCESS_EVENT_QUEUE.saturating_sub(child.pending_execution_events.len()))
550}
551
552fn poll_child_execution_after_exit(
553 child: &mut ActiveProcess,
554 wait: Duration,
555) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
556 match child.execution.poll_event_blocking(wait) {
557 Ok(event) => Ok(event),
558 Err(SidecarError::Execution(message))
559 if child.runtime == GuestRuntimeKind::WebAssembly
560 && message == WasmExecutionError::EventChannelClosed.to_string() =>
561 {
562 Ok(None)
563 }
564 Err(error) => Err(error),
565 }
566}
567
568fn closed_javascript_event_channel(message: &str) -> bool {
569 message == "guest JavaScript event channel closed unexpectedly"
570}
571
572fn closed_python_event_channel(message: &str) -> bool {
573 message == "guest Python event channel closed unexpectedly"
574}
575
576fn closed_wasm_event_channel(message: &str) -> bool {
577 message == WasmExecutionError::EventChannelClosed.to_string()
578}
579
580fn missing_vm_error(vm_id: &str) -> SidecarError {
581 SidecarError::InvalidState(format!("VM {vm_id} is no longer active"))
582}
583
584fn missing_process_error(vm_id: &str, process_id: &str) -> SidecarError {
585 SidecarError::InvalidState(format!(
586 "VM {vm_id} no longer has active process {process_id}"
587 ))
588}
589
590fn is_broken_pipe_error(error: &SidecarError) -> bool {
591 matches!(error, SidecarError::Execution(message) if message.contains("Broken pipe") || message.contains("os error 32") || message.contains("EPIPE"))
592}
593
594fn javascript_child_process_gone_error(process_id: &str, child_path: &[&str]) -> SidecarError {
595 let child_label = if child_path.is_empty() {
596 process_id.to_owned()
597 } else {
598 format!("{process_id}/{}", child_path.join("/"))
599 };
600 SidecarError::Execution(format!(
601 "ECHILD: child_process {child_label} is no longer available"
602 ))
603}
604
605fn is_javascript_child_process_gone_error(error: &SidecarError) -> bool {
606 matches!(
607 error,
608 SidecarError::Execution(message) if guest_errno_code(message) == Some("ECHILD")
609 )
610}
611
612fn loopback_tls_transport_registry(
613) -> &'static Mutex<BTreeMap<String, Weak<crate::state::LoopbackTlsTransportPair>>> {
614 static REGISTRY: OnceLock<
615 Mutex<BTreeMap<String, Weak<crate::state::LoopbackTlsTransportPair>>>,
616 > = OnceLock::new();
617 REGISTRY.get_or_init(|| Mutex::new(BTreeMap::new()))
618}
619
620fn loopback_tls_transport_key(
621 vm_id: &str,
622 socket_id: SocketId,
623 peer_socket_id: SocketId,
624) -> String {
625 let (lower, higher) = if socket_id <= peer_socket_id {
626 (socket_id, peer_socket_id)
627 } else {
628 (peer_socket_id, socket_id)
629 };
630 format!("{vm_id}:{lower}:{higher}")
631}
632
633fn loopback_tls_endpoint(
634 vm_id: &str,
635 socket_id: SocketId,
636 peer_socket_id: SocketId,
637) -> Result<crate::state::LoopbackTlsEndpoint, SidecarError> {
638 let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
639 let registry = loopback_tls_transport_registry();
640 let mut transports = registry.lock().map_err(|_| {
641 SidecarError::InvalidState(String::from(
642 "loopback TLS transport registry lock poisoned",
643 ))
644 })?;
645 transports.retain(|_, pair| pair.strong_count() > 0);
646 let pair = transports
647 .get(&key)
648 .and_then(Weak::upgrade)
649 .unwrap_or_else(|| {
650 let pair = Arc::new(crate::state::LoopbackTlsTransportPair {
651 state: Mutex::new(crate::state::LoopbackTlsTransportPairState::default()),
652 ready: std::sync::Condvar::new(),
653 });
654 transports.insert(key, Arc::downgrade(&pair));
655 pair
656 });
657 Ok(crate::state::LoopbackTlsEndpoint {
658 pair,
659 is_lower_socket: socket_id <= peer_socket_id,
660 })
661}
662
663impl crate::state::LoopbackTlsEndpoint {
664 fn shutdown_write(&self) -> Result<(), SidecarError> {
665 let mut state = self.pair.state.lock().map_err(|_| {
666 SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
667 })?;
668 if self.is_lower_socket {
669 state.lower_write_closed = true;
670 } else {
671 state.higher_write_closed = true;
672 }
673 self.pair.ready.notify_all();
674 Ok(())
675 }
676
677 fn close_endpoint(&self) -> Result<(), SidecarError> {
678 let mut state = self.pair.state.lock().map_err(|_| {
679 SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
680 })?;
681 if self.is_lower_socket {
682 state.lower_write_closed = true;
683 state.lower_closed = true;
684 } else {
685 state.higher_write_closed = true;
686 state.higher_closed = true;
687 }
688 self.pair.ready.notify_all();
689 Ok(())
690 }
691}
692
693fn parse_tls_client_hello_from_bytes(
694 buffer: &[u8],
695) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
696 if buffer.is_empty() {
697 return Ok(None);
698 }
699
700 let mut acceptor = rustls::server::Acceptor::default();
701 let mut cursor = Cursor::new(buffer);
702 acceptor.read_tls(&mut cursor).map_err(sidecar_net_error)?;
703 let Some(accepted) = acceptor.accept().map_err(|(error, _)| {
704 SidecarError::Execution(format!("failed to parse TLS client hello: {error}"))
705 })?
706 else {
707 return Ok(None);
708 };
709 let client_hello = accepted.client_hello();
710 let alpn_protocols = client_hello.alpn().map(|protocols| {
711 protocols
712 .filter_map(|protocol| String::from_utf8(protocol.to_vec()).ok())
713 .collect::<Vec<_>>()
714 });
715 Ok(Some(JavascriptTlsClientHello {
716 servername: client_hello.server_name().map(str::to_owned),
717 alpn_protocols,
718 }))
719}
720
721fn peek_loopback_tls_client_hello(
722 vm_id: &str,
723 socket_id: SocketId,
724 peer_socket_id: SocketId,
725) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
726 let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
727 let registry = loopback_tls_transport_registry();
728 let pair = registry
729 .lock()
730 .map_err(|_| {
731 SidecarError::InvalidState(String::from(
732 "loopback TLS transport registry lock poisoned",
733 ))
734 })?
735 .get(&key)
736 .and_then(Weak::upgrade);
737 let Some(pair) = pair else {
738 return Ok(None);
739 };
740 let is_lower_socket = socket_id <= peer_socket_id;
741 let state = pair.state.lock().map_err(|_| {
742 SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
743 })?;
744 let buffered = if is_lower_socket {
745 state.higher_to_lower.iter().copied().collect::<Vec<_>>()
746 } else {
747 state.lower_to_higher.iter().copied().collect::<Vec<_>>()
748 };
749 drop(state);
750 parse_tls_client_hello_from_bytes(&buffered)
751}
752
753fn wait_for_loopback_peer_socket_id(
754 kernel: &SidecarKernel,
755 socket_id: SocketId,
756) -> Option<SocketId> {
757 for _ in 0..50 {
758 if let Some(peer_socket_id) = kernel
759 .socket_get(socket_id)
760 .and_then(|record| record.peer_socket_id())
761 {
762 return Some(peer_socket_id);
763 }
764 std::thread::sleep(Duration::from_millis(10));
765 }
766 None
767}
768
769impl Drop for crate::state::LoopbackTlsEndpoint {
770 fn drop(&mut self) {
771 let _ = self.close_endpoint();
772 }
773}
774
775impl Read for crate::state::LoopbackTlsEndpoint {
776 fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
777 let mut state = self
778 .pair
779 .state
780 .lock()
781 .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
782
783 loop {
784 let (peer_write_closed, peer_closed) = if self.is_lower_socket {
785 (state.higher_write_closed, state.higher_closed)
786 } else {
787 (state.lower_write_closed, state.lower_closed)
788 };
789
790 let incoming = if self.is_lower_socket {
791 &mut state.higher_to_lower
792 } else {
793 &mut state.lower_to_higher
794 };
795
796 if !incoming.is_empty() {
797 let mut count = 0;
798 while count < buffer.len() {
799 let Some(byte) = incoming.pop_front() else {
800 break;
801 };
802 buffer[count] = byte;
803 count += 1;
804 }
805 return Ok(count);
806 }
807
808 if peer_write_closed || peer_closed {
809 return Ok(0);
810 }
811
812 let (next_state, wait_result) = self
813 .pair
814 .ready
815 .wait_timeout(state, TCP_SOCKET_POLL_TIMEOUT)
816 .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
817 state = next_state;
818 if wait_result.timed_out() {
819 return Err(std::io::Error::new(
820 std::io::ErrorKind::WouldBlock,
821 "loopback TLS transport read timed out",
822 ));
823 }
824 }
825 }
826}
827
828impl Write for crate::state::LoopbackTlsEndpoint {
829 fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
830 let mut state = self
831 .pair
832 .state
833 .lock()
834 .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
835
836 let peer_closed = if self.is_lower_socket {
837 state.higher_closed
838 } else {
839 state.lower_closed
840 };
841 let outgoing = if self.is_lower_socket {
842 &mut state.lower_to_higher
843 } else {
844 &mut state.higher_to_lower
845 };
846 if peer_closed {
847 return Err(std::io::Error::new(
848 std::io::ErrorKind::BrokenPipe,
849 "loopback TLS peer is closed",
850 ));
851 }
852
853 outgoing.extend(buffer.iter().copied());
854 self.pair.ready.notify_all();
855 Ok(buffer.len())
856 }
857
858 fn flush(&mut self) -> std::io::Result<()> {
859 Ok(())
860 }
861}
862
863struct ActiveTcpConnectRequest<'a, B> {
866 bridge: &'a SharedBridge<B>,
867 kernel: &'a mut SidecarKernel,
868 kernel_pid: u32,
869 vm_id: &'a str,
870 dns: &'a VmDnsConfig,
871 host: &'a str,
872 port: u16,
873 local_address: Option<&'a str>,
874 local_port: Option<u16>,
875 local_reservation: Option<(JavascriptSocketFamily, u16)>,
876 context: &'a JavascriptSocketPathContext,
877}
878
879struct ActiveUdpSendToRequest<'a, B> {
880 bridge: &'a SharedBridge<B>,
881 kernel: &'a mut SidecarKernel,
882 kernel_pid: u32,
883 vm_id: &'a str,
884 dns: &'a VmDnsConfig,
885 host: &'a str,
886 port: u16,
887 context: &'a JavascriptSocketPathContext,
888 contents: &'a [u8],
889}
890
891struct UdpRemoteAddrRequest<'a, B> {
892 bridge: &'a SharedBridge<B>,
893 kernel: &'a SidecarKernel,
894 vm_id: &'a str,
895 dns: &'a VmDnsConfig,
896 host: &'a str,
897 port: u16,
898 family: JavascriptUdpFamily,
899 context: &'a JavascriptSocketPathContext,
900}
901
902pub(crate) struct JavascriptSyncRpcServiceRequest<'a, B> {
903 pub(crate) bridge: &'a SharedBridge<B>,
904 pub(crate) vm_id: &'a str,
905 pub(crate) dns: &'a VmDnsConfig,
906 pub(crate) socket_paths: &'a JavascriptSocketPathContext,
907 pub(crate) kernel: &'a mut SidecarKernel,
908 pub(crate) process: &'a mut ActiveProcess,
909 pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
910 pub(crate) resource_limits: &'a ResourceLimits,
911 pub(crate) network_counts: NetworkResourceCounts,
912}
913
914pub(crate) struct JavascriptNetSyncRpcServiceRequest<'a, B> {
915 pub(crate) bridge: &'a SharedBridge<B>,
916 pub(crate) vm_id: &'a str,
917 pub(crate) dns: &'a VmDnsConfig,
918 pub(crate) socket_paths: &'a JavascriptSocketPathContext,
919 pub(crate) kernel: &'a mut SidecarKernel,
920 pub(crate) process: &'a mut ActiveProcess,
921 pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
922 pub(crate) resource_limits: &'a ResourceLimits,
923 pub(crate) network_counts: NetworkResourceCounts,
924}
925
926struct LoopbackHttpResponseWaitRequest<'a, B> {
927 bridge: &'a SharedBridge<B>,
928 vm_id: &'a str,
929 dns: &'a VmDnsConfig,
930 socket_paths: &'a JavascriptSocketPathContext,
931 kernel: &'a mut SidecarKernel,
932 process: &'a mut ActiveProcess,
933 resource_limits: &'a ResourceLimits,
934 request_key: (u64, u64),
935}
936
937struct JavascriptDgramSyncRpcServiceRequest<'a, B> {
938 bridge: &'a SharedBridge<B>,
939 kernel: &'a mut SidecarKernel,
940 vm_id: &'a str,
941 dns: &'a VmDnsConfig,
942 socket_paths: &'a JavascriptSocketPathContext,
943 process: &'a mut ActiveProcess,
944 sync_request: &'a JavascriptSyncRpcRequest,
945 resource_limits: &'a ResourceLimits,
946 network_counts: NetworkResourceCounts,
947}
948
949struct JavascriptHttp2SyncRpcServiceRequest<'a, B> {
950 bridge: &'a SharedBridge<B>,
951 kernel: &'a mut SidecarKernel,
952 vm_id: &'a str,
953 dns: &'a VmDnsConfig,
954 socket_paths: &'a JavascriptSocketPathContext,
955 process: &'a mut ActiveProcess,
956 sync_request: &'a JavascriptSyncRpcRequest,
957 resource_limits: &'a ResourceLimits,
958 network_counts: NetworkResourceCounts,
959}
960
961impl ActiveTcpSocket {
962 fn connect<B>(request: ActiveTcpConnectRequest<'_, B>) -> Result<Self, SidecarError>
963 where
964 B: NativeSidecarBridge + Send + 'static,
965 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
966 {
967 let ActiveTcpConnectRequest {
968 bridge,
969 kernel,
970 kernel_pid,
971 vm_id,
972 dns,
973 host,
974 port,
975 local_address,
976 local_port,
977 local_reservation,
978 context,
979 } = request;
980 let resolved = resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, context)?;
981 if resolved.use_kernel_loopback {
982 let family = JavascriptSocketFamily::from_ip(resolved.guest_remote_addr.ip());
983 let requested_local_port = local_port.unwrap_or(0);
984 let local_port = if requested_local_port != 0
985 && local_reservation == Some((family, requested_local_port))
986 {
987 requested_local_port
988 } else {
989 allocate_guest_listen_port(
990 requested_local_port,
991 family,
992 &context.used_tcp_guest_ports,
993 context.listen_policy,
994 )?
995 };
996 let local_ip = match (family, local_address) {
997 (JavascriptSocketFamily::Ipv4, Some("0.0.0.0")) => {
998 IpAddr::V4(Ipv4Addr::UNSPECIFIED)
999 }
1000 (JavascriptSocketFamily::Ipv4, Some("127.0.0.1") | Some("localhost") | None) => {
1001 IpAddr::V4(Ipv4Addr::LOCALHOST)
1002 }
1003 (JavascriptSocketFamily::Ipv6, Some("::")) => IpAddr::V6(Ipv6Addr::UNSPECIFIED),
1004 (JavascriptSocketFamily::Ipv6, Some("::1") | Some("localhost") | None) => {
1005 IpAddr::V6(Ipv6Addr::LOCALHOST)
1006 }
1007 (JavascriptSocketFamily::Ipv4, Some(other)) => {
1008 return Err(SidecarError::Execution(format!(
1009 "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1010 )));
1011 }
1012 (JavascriptSocketFamily::Ipv6, Some(other)) => {
1013 return Err(SidecarError::Execution(format!(
1014 "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1015 )));
1016 }
1017 };
1018 let local_addr = SocketAddr::new(local_ip, local_port);
1019 let spec = match family {
1020 JavascriptSocketFamily::Ipv4 => SocketSpec::tcp(),
1021 JavascriptSocketFamily::Ipv6 => {
1022 SocketSpec::new(SocketDomain::Inet6, SocketType::Stream)
1023 }
1024 };
1025 let socket_id = kernel
1026 .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
1027 .map_err(kernel_error)?;
1028 kernel
1029 .socket_bind_inet(
1030 EXECUTION_DRIVER_NAME,
1031 kernel_pid,
1032 socket_id,
1033 InetSocketAddress::new(local_ip.to_string(), local_port),
1034 )
1035 .map_err(kernel_error)?;
1036 kernel
1037 .socket_connect_inet_loopback(
1038 EXECUTION_DRIVER_NAME,
1039 kernel_pid,
1040 socket_id,
1041 InetSocketAddress::new(
1042 resolved.guest_remote_addr.ip().to_string(),
1043 resolved.guest_remote_addr.port(),
1044 ),
1045 )
1046 .map_err(kernel_error)?;
1047 return Ok(Self::from_kernel(
1048 socket_id,
1049 None,
1050 local_addr,
1051 resolved.guest_remote_addr,
1052 ));
1053 }
1054
1055 let stream = TcpStream::connect_timeout(&resolved.actual_addr, Duration::from_secs(30))
1056 .map_err(sidecar_net_error)?;
1057 let guest_local_addr = stream.local_addr().map_err(sidecar_net_error)?;
1058 Self::from_stream(stream, None, guest_local_addr, resolved.guest_remote_addr)
1059 }
1060
1061 fn from_stream(
1062 stream: TcpStream,
1063 listener_id: Option<String>,
1064 guest_local_addr: SocketAddr,
1065 guest_remote_addr: SocketAddr,
1066 ) -> Result<Self, SidecarError> {
1067 let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
1068 read_stream
1069 .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
1070 .map_err(sidecar_net_error)?;
1071 let stream = Arc::new(Mutex::new(stream));
1072 let pending_read_stream = Arc::new(Mutex::new(Some(read_stream)));
1073 let (sender, events) = mpsc::channel();
1074 let tls_mode = Arc::new(AtomicBool::new(false));
1075 let tls_stream = Arc::new(Mutex::new(None));
1076 let tls_state = Arc::new(Mutex::new(None));
1077 let saw_local_shutdown = Arc::new(AtomicBool::new(false));
1078 let saw_remote_end = Arc::new(AtomicBool::new(false));
1079 let close_notified = Arc::new(AtomicBool::new(false));
1080
1081 Ok(Self {
1082 stream: Some(stream),
1083 pending_read_stream: Some(pending_read_stream),
1084 events: Some(events),
1085 event_sender: Some(sender),
1086 kernel_socket_id: None,
1087 no_delay: false,
1088 keep_alive: false,
1089 keep_alive_initial_delay_secs: None,
1090 guest_local_addr,
1091 guest_remote_addr,
1092 listener_id,
1093 tls_mode,
1094 tls_stream,
1095 tls_state,
1096 saw_local_shutdown,
1097 saw_remote_end,
1098 close_notified,
1099 })
1100 }
1101
1102 fn from_kernel(
1103 socket_id: SocketId,
1104 listener_id: Option<String>,
1105 guest_local_addr: SocketAddr,
1106 guest_remote_addr: SocketAddr,
1107 ) -> Self {
1108 let (sender, events) = mpsc::channel();
1109 Self {
1110 stream: None,
1111 pending_read_stream: None,
1112 events: Some(events),
1113 event_sender: Some(sender),
1114 kernel_socket_id: Some(socket_id),
1115 no_delay: false,
1116 keep_alive: false,
1117 keep_alive_initial_delay_secs: None,
1118 guest_local_addr,
1119 guest_remote_addr,
1120 listener_id,
1121 tls_mode: Arc::new(AtomicBool::new(false)),
1122 tls_stream: Arc::new(Mutex::new(None)),
1123 tls_state: Arc::new(Mutex::new(None)),
1124 saw_local_shutdown: Arc::new(AtomicBool::new(false)),
1125 saw_remote_end: Arc::new(AtomicBool::new(false)),
1126 close_notified: Arc::new(AtomicBool::new(false)),
1127 }
1128 }
1129
1130 fn poll(
1131 &mut self,
1132 kernel: &mut SidecarKernel,
1133 kernel_pid: u32,
1134 wait: Duration,
1135 ) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
1136 if self.tls_mode.load(Ordering::SeqCst) {
1137 self.ensure_tcp_reader()?;
1138 return match self
1139 .events
1140 .as_ref()
1141 .ok_or_else(|| {
1142 SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1143 })?
1144 .recv_timeout(wait)
1145 {
1146 Ok(event) => Ok(Some(event)),
1147 Err(RecvTimeoutError::Timeout) => Ok(None),
1148 Err(RecvTimeoutError::Disconnected) => Ok(None),
1149 };
1150 }
1151
1152 if let Some(socket_id) = self.kernel_socket_id {
1153 let result = kernel
1154 .poll_targets(
1155 EXECUTION_DRIVER_NAME,
1156 kernel_pid,
1157 vec![PollTargetEntry::socket(
1158 socket_id,
1159 POLLIN | POLLHUP | POLLERR,
1160 )],
1161 i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
1162 )
1163 .map_err(kernel_error)?;
1164 let revents = result
1165 .targets
1166 .first()
1167 .map(|entry| entry.revents)
1168 .unwrap_or_else(PollEvents::empty);
1169 if revents.is_empty() {
1170 return Ok(None);
1171 }
1172 if revents.intersects(POLLIN) {
1173 return match kernel.socket_read(
1174 EXECUTION_DRIVER_NAME,
1175 kernel_pid,
1176 socket_id,
1177 64 * 1024,
1178 ) {
1179 Ok(Some(bytes)) if !bytes.is_empty() => {
1180 Ok(Some(JavascriptTcpSocketEvent::Data(bytes)))
1181 }
1182 Ok(Some(_)) => Ok(Some(JavascriptTcpSocketEvent::Data(Vec::new()))),
1183 Ok(None) => Ok(Some(JavascriptTcpSocketEvent::End)),
1184 Err(error) if error.code() == "EAGAIN" => Ok(None),
1185 Err(error) => Ok(Some(JavascriptTcpSocketEvent::Error {
1186 code: Some(error.code().to_string()),
1187 message: error.to_string(),
1188 })),
1189 };
1190 }
1191 if revents.intersects(POLLHUP) {
1192 return Ok(Some(JavascriptTcpSocketEvent::End));
1193 }
1194 if revents.intersects(POLLERR) {
1195 return Ok(Some(JavascriptTcpSocketEvent::Error {
1196 code: Some(String::from("EPIPE")),
1197 message: String::from("kernel TCP socket reported POLLERR"),
1198 }));
1199 }
1200 return Ok(None);
1201 }
1202
1203 self.ensure_tcp_reader()?;
1204 match self
1205 .events
1206 .as_ref()
1207 .ok_or_else(|| {
1208 SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1209 })?
1210 .recv_timeout(wait)
1211 {
1212 Ok(event) => Ok(Some(event)),
1213 Err(RecvTimeoutError::Timeout) => Ok(None),
1214 Err(RecvTimeoutError::Disconnected) => Ok(None),
1215 }
1216 }
1217
1218 fn ensure_tcp_reader(&self) -> Result<(), SidecarError> {
1219 if self.kernel_socket_id.is_some() {
1220 return Ok(());
1221 }
1222 if self.tls_mode.load(Ordering::SeqCst) {
1223 return Ok(());
1224 }
1225 let read_stream = self
1226 .pending_read_stream
1227 .as_ref()
1228 .ok_or_else(|| {
1229 SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1230 })?
1231 .lock()
1232 .map_err(|_| {
1233 SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1234 })?
1235 .take();
1236 if let Some(read_stream) = read_stream {
1237 spawn_tcp_socket_reader(
1238 read_stream,
1239 self.event_sender
1240 .as_ref()
1241 .ok_or_else(|| {
1242 SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1243 })?
1244 .clone(),
1245 Arc::clone(&self.tls_mode),
1246 Arc::clone(&self.saw_local_shutdown),
1247 Arc::clone(&self.saw_remote_end),
1248 Arc::clone(&self.close_notified),
1249 );
1250 }
1251 Ok(())
1252 }
1253
1254 fn socket_info(&self) -> Value {
1255 json!({
1256 "localAddress": self.guest_local_addr.ip().to_string(),
1257 "localPort": self.guest_local_addr.port(),
1258 "localFamily": socket_addr_family(&self.guest_local_addr),
1259 "remoteAddress": self.guest_remote_addr.ip().to_string(),
1260 "remotePort": self.guest_remote_addr.port(),
1261 "remoteFamily": socket_addr_family(&self.guest_remote_addr),
1262 })
1263 }
1264
1265 fn set_no_delay(&mut self, enable: bool) -> Result<(), SidecarError> {
1266 self.no_delay = enable;
1267 if self.kernel_socket_id.is_some() {
1268 return Ok(());
1269 }
1270 let stream = self
1271 .stream
1272 .as_ref()
1273 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1274 .lock()
1275 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1276 stream.set_nodelay(enable).map_err(sidecar_net_error)
1277 }
1278
1279 fn set_keep_alive(
1280 &mut self,
1281 enable: bool,
1282 initial_delay_secs: Option<u64>,
1283 ) -> Result<(), SidecarError> {
1284 self.keep_alive = enable;
1285 self.keep_alive_initial_delay_secs = initial_delay_secs;
1286 if self.kernel_socket_id.is_some() {
1287 return Ok(());
1288 }
1289 let stream = self
1290 .stream
1291 .as_ref()
1292 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1293 .lock()
1294 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1295 let socket = SockRef::from(&*stream);
1296 socket.set_keepalive(enable).map_err(sidecar_net_error)?;
1297 if enable {
1298 if let Some(delay_secs) = initial_delay_secs.filter(|delay_secs| *delay_secs > 0) {
1299 socket
1300 .set_tcp_keepalive(
1301 &TcpKeepalive::new().with_time(Duration::from_secs(delay_secs)),
1302 )
1303 .map_err(sidecar_net_error)?;
1304 }
1305 }
1306 Ok(())
1307 }
1308
1309 fn upgrade_tls(
1310 &self,
1311 vm_id: &str,
1312 kernel: &SidecarKernel,
1313 options: JavascriptTlsBridgeOptions,
1314 ) -> Result<(), SidecarError> {
1315 if self.tls_mode.load(Ordering::SeqCst) {
1316 return Ok(());
1317 }
1318
1319 let client_hello = if options.is_server {
1320 self.peek_tls_client_hello(vm_id, kernel)?
1321 } else {
1322 None
1323 };
1324
1325 let tls_stream = if let Some(socket_id) = self.kernel_socket_id {
1326 let peer_socket_id = wait_for_loopback_peer_socket_id(kernel, socket_id)
1327 .ok_or_else(|| {
1328 SidecarError::Execution(format!(
1329 "ERR_NOT_IMPLEMENTED: kernel-backed loopback socket {socket_id} has no peer for TLS upgrade"
1330 ))
1331 })?;
1332 let endpoint = loopback_tls_endpoint(vm_id, socket_id, peer_socket_id)?;
1333 if options.is_server {
1334 ActiveTlsStream::LoopbackServer(build_server_loopback_tls_stream(
1335 endpoint, &options,
1336 )?)
1337 } else {
1338 ActiveTlsStream::LoopbackClient(build_client_loopback_tls_stream(
1339 endpoint, &options,
1340 )?)
1341 }
1342 } else {
1343 self.pending_read_stream
1344 .as_ref()
1345 .ok_or_else(|| {
1346 SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1347 })?
1348 .lock()
1349 .map_err(|_| {
1350 SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1351 })?
1352 .take();
1353 let stream = self
1354 .stream
1355 .as_ref()
1356 .ok_or_else(|| {
1357 SidecarError::InvalidState(String::from("TCP socket stream missing"))
1358 })?
1359 .lock()
1360 .map_err(|_| {
1361 SidecarError::InvalidState(String::from("TCP socket lock poisoned"))
1362 })?;
1363 let cloned = stream.try_clone().map_err(sidecar_net_error)?;
1364 drop(stream);
1365
1366 if options.is_server {
1367 ActiveTlsStream::Server(build_server_tls_stream(cloned, &options)?)
1368 } else {
1369 ActiveTlsStream::Client(build_client_tls_stream(cloned, &options)?)
1370 }
1371 };
1372
1373 let tls_state = ActiveTlsState {
1374 client_hello,
1375 local_certificates: tls_local_certificates(&options)?,
1376 session_reused: false,
1377 };
1378
1379 self.tls_mode.store(true, Ordering::SeqCst);
1380 {
1381 let mut state = self
1382 .tls_state
1383 .lock()
1384 .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?;
1385 *state = Some(tls_state);
1386 }
1387 {
1388 let mut stream = self.tls_stream.lock().map_err(|_| {
1389 SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
1390 })?;
1391 *stream = Some(tls_stream);
1392 }
1393
1394 spawn_tls_socket_reader(
1395 Arc::clone(&self.tls_stream),
1396 self.event_sender
1397 .as_ref()
1398 .ok_or_else(|| {
1399 SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1400 })?
1401 .clone(),
1402 Arc::clone(&self.saw_local_shutdown),
1403 Arc::clone(&self.saw_remote_end),
1404 Arc::clone(&self.close_notified),
1405 );
1406 Ok(())
1407 }
1408
1409 fn peek_tls_client_hello(
1410 &self,
1411 vm_id: &str,
1412 kernel: &SidecarKernel,
1413 ) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
1414 if let Some(socket_id) = self.kernel_socket_id {
1415 let Some(peer_socket_id) = kernel
1416 .socket_get(socket_id)
1417 .and_then(|record| record.peer_socket_id())
1418 else {
1419 return Ok(None);
1420 };
1421 return peek_loopback_tls_client_hello(vm_id, socket_id, peer_socket_id);
1422 }
1423
1424 let stream = self
1425 .stream
1426 .as_ref()
1427 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1428 .lock()
1429 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1430 let mut buffer = vec![0_u8; 16 * 1024];
1431 let bytes = match stream.peek(&mut buffer) {
1432 Ok(0) => return Ok(None),
1433 Ok(bytes) => bytes,
1434 Err(error)
1435 if matches!(
1436 error.kind(),
1437 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
1438 ) =>
1439 {
1440 return Ok(None);
1441 }
1442 Err(error) => return Err(sidecar_net_error(error)),
1443 };
1444 parse_tls_client_hello_from_bytes(&buffer[..bytes])
1445 }
1446
1447 fn tls_client_hello_json(
1448 &self,
1449 vm_id: &str,
1450 kernel: &SidecarKernel,
1451 ) -> Result<Value, SidecarError> {
1452 if let Some(client_hello) = self
1453 .tls_state
1454 .lock()
1455 .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
1456 .as_ref()
1457 .and_then(|state| state.client_hello.clone())
1458 {
1459 return javascript_net_json_string(
1460 serde_json::to_value(client_hello).map_err(|error| {
1461 SidecarError::InvalidState(format!(
1462 "failed to serialize TLS client hello: {error}"
1463 ))
1464 })?,
1465 "net.socket_get_tls_client_hello",
1466 );
1467 }
1468
1469 javascript_net_json_string(
1470 serde_json::to_value(
1471 self.peek_tls_client_hello(vm_id, kernel)?
1472 .unwrap_or_default(),
1473 )
1474 .map_err(|error| {
1475 SidecarError::InvalidState(format!("failed to serialize TLS client hello: {error}"))
1476 })?,
1477 "net.socket_get_tls_client_hello",
1478 )
1479 }
1480
1481 fn tls_query(&self, query: &str, detailed: bool) -> Result<Value, SidecarError> {
1482 let state = self
1483 .tls_state
1484 .lock()
1485 .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
1486 .clone();
1487 let mut tls_stream = self
1488 .tls_stream
1489 .lock()
1490 .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?;
1491 let Some(stream) = tls_stream.as_mut() else {
1492 return javascript_net_json_string(
1493 tls_bridge_undefined_value(),
1494 "net.socket_tls_query",
1495 );
1496 };
1497
1498 let payload = match query {
1499 "getSession" => tls_bridge_undefined_value(),
1500 "isSessionReused" => Value::Bool(
1501 state
1502 .as_ref()
1503 .is_some_and(|tls_state| tls_state.session_reused),
1504 ),
1505 "getPeerCertificate" => {
1506 let certificate = stream
1507 .peer_certificates()
1508 .and_then(|certificates| certificates.first())
1509 .map(|certificate| {
1510 tls_certificate_bridge_value(certificate.as_ref(), detailed)
1511 });
1512 certificate.unwrap_or_else(tls_bridge_undefined_value)
1513 }
1514 "getCertificate" => state
1515 .as_ref()
1516 .and_then(|tls_state| tls_state.local_certificates.first())
1517 .map(|certificate| tls_certificate_bridge_value(certificate, detailed))
1518 .unwrap_or_else(tls_bridge_undefined_value),
1519 "getProtocol" => stream
1520 .protocol_version()
1521 .map(tls_protocol_name)
1522 .map(Value::String)
1523 .unwrap_or(Value::Null),
1524 "getCipher" => stream
1525 .negotiated_cipher_suite()
1526 .map(tls_cipher_bridge_value)
1527 .unwrap_or_else(tls_bridge_undefined_value),
1528 other => {
1529 return Err(SidecarError::InvalidState(format!(
1530 "unsupported TLS query {other}"
1531 )));
1532 }
1533 };
1534 javascript_net_json_string(payload, "net.socket_tls_query")
1535 }
1536
1537 fn write_all(
1538 &self,
1539 kernel: &mut SidecarKernel,
1540 kernel_pid: u32,
1541 contents: &[u8],
1542 ) -> Result<usize, SidecarError> {
1543 if self.tls_mode.load(Ordering::SeqCst) {
1544 let mut tls_stream = self.tls_stream.lock().map_err(|_| {
1545 SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
1546 })?;
1547 let stream = tls_stream.as_mut().ok_or_else(|| {
1548 SidecarError::InvalidState(String::from("TLS stream missing for upgraded socket"))
1549 })?;
1550 stream.write_all(contents)?;
1551 return Ok(contents.len());
1552 }
1553 if let Some(socket_id) = self.kernel_socket_id {
1554 return kernel
1555 .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, contents)
1556 .map_err(kernel_error);
1557 }
1558
1559 let mut stream = self
1560 .stream
1561 .as_ref()
1562 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1563 .lock()
1564 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1565 stream.write_all(contents).map_err(sidecar_net_error)?;
1566 Ok(contents.len())
1567 }
1568
1569 fn shutdown_write(
1570 &self,
1571 kernel: &mut SidecarKernel,
1572 kernel_pid: u32,
1573 ) -> Result<(), SidecarError> {
1574 if self.tls_mode.load(Ordering::SeqCst) {
1575 if let Some(stream) = self
1576 .tls_stream
1577 .lock()
1578 .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
1579 .as_mut()
1580 {
1581 let _ = stream.send_close_notify();
1582 let _ = stream.shutdown_write();
1583 }
1584 if self.kernel_socket_id.is_some() {
1585 self.saw_local_shutdown.store(true, Ordering::SeqCst);
1586 return Ok(());
1587 }
1588 }
1589 if let Some(socket_id) = self.kernel_socket_id {
1590 return kernel
1591 .socket_shutdown(
1592 EXECUTION_DRIVER_NAME,
1593 kernel_pid,
1594 socket_id,
1595 KernelSocketShutdown::Write,
1596 )
1597 .map_err(kernel_error);
1598 }
1599 let stream = self
1600 .stream
1601 .as_ref()
1602 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1603 .lock()
1604 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1605 self.saw_local_shutdown.store(true, Ordering::SeqCst);
1606 match stream.shutdown(Shutdown::Write) {
1607 Ok(()) => {}
1608 Err(error) if error.kind() == std::io::ErrorKind::NotConnected => {}
1609 Err(error) => return Err(sidecar_net_error(error)),
1610 }
1611 if self.saw_remote_end.load(Ordering::SeqCst)
1612 && !self.close_notified.swap(true, Ordering::SeqCst)
1613 {
1614 let _ = self
1615 .event_sender
1616 .as_ref()
1617 .ok_or_else(|| {
1618 SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1619 })?
1620 .send(JavascriptTcpSocketEvent::Close { had_error: false });
1621 }
1622 Ok(())
1623 }
1624
1625 fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
1626 if self.tls_mode.load(Ordering::SeqCst) {
1627 if let Some(stream) = self
1628 .tls_stream
1629 .lock()
1630 .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
1631 .as_mut()
1632 {
1633 let _ = stream.send_close_notify();
1634 let _ = stream.close();
1635 }
1636 if self.kernel_socket_id.is_some() {
1637 return Ok(());
1638 }
1639 }
1640 if let Some(socket_id) = self.kernel_socket_id {
1641 return kernel
1642 .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
1643 .map_err(kernel_error);
1644 }
1645 let stream = self
1646 .stream
1647 .as_ref()
1648 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1649 .lock()
1650 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1651 stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
1652 }
1653}
1654
1655impl ActiveTlsStream {
1656 fn write_all(&mut self, contents: &[u8]) -> Result<(), SidecarError> {
1657 match self {
1658 Self::Client(stream) => {
1659 stream.write_all(contents).map_err(sidecar_net_error)?;
1660 stream.flush().map_err(sidecar_net_error)
1661 }
1662 Self::Server(stream) => {
1663 stream.write_all(contents).map_err(sidecar_net_error)?;
1664 stream.flush().map_err(sidecar_net_error)
1665 }
1666 Self::LoopbackClient(stream) => {
1667 stream.write_all(contents).map_err(sidecar_net_error)?;
1668 stream.flush().map_err(sidecar_net_error)
1669 }
1670 Self::LoopbackServer(stream) => {
1671 stream.write_all(contents).map_err(sidecar_net_error)?;
1672 stream.flush().map_err(sidecar_net_error)
1673 }
1674 }
1675 }
1676
1677 fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
1678 match self {
1679 Self::Client(stream) => stream.read(buffer),
1680 Self::Server(stream) => stream.read(buffer),
1681 Self::LoopbackClient(stream) => stream.read(buffer),
1682 Self::LoopbackServer(stream) => stream.read(buffer),
1683 }
1684 }
1685
1686 fn send_close_notify(&mut self) -> Result<(), SidecarError> {
1687 match self {
1688 Self::Client(stream) => {
1689 stream.conn.send_close_notify();
1690 let _ = stream.conn.complete_io(&mut stream.sock);
1691 }
1692 Self::Server(stream) => {
1693 stream.conn.send_close_notify();
1694 let _ = stream.conn.complete_io(&mut stream.sock);
1695 }
1696 Self::LoopbackClient(stream) => {
1697 stream.conn.send_close_notify();
1698 let _ = stream.conn.complete_io(&mut stream.sock);
1699 }
1700 Self::LoopbackServer(stream) => {
1701 stream.conn.send_close_notify();
1702 let _ = stream.conn.complete_io(&mut stream.sock);
1703 }
1704 }
1705 Ok(())
1706 }
1707
1708 fn shutdown_write(&mut self) -> Result<(), SidecarError> {
1709 match self {
1710 Self::Client(stream) => stream
1711 .sock
1712 .shutdown(Shutdown::Write)
1713 .map_err(sidecar_net_error),
1714 Self::Server(stream) => stream
1715 .sock
1716 .shutdown(Shutdown::Write)
1717 .map_err(sidecar_net_error),
1718 Self::LoopbackClient(stream) => stream.sock.shutdown_write(),
1719 Self::LoopbackServer(stream) => stream.sock.shutdown_write(),
1720 }
1721 }
1722
1723 fn close(&mut self) -> Result<(), SidecarError> {
1724 match self {
1725 Self::Client(stream) => stream
1726 .sock
1727 .shutdown(Shutdown::Both)
1728 .map_err(sidecar_net_error),
1729 Self::Server(stream) => stream
1730 .sock
1731 .shutdown(Shutdown::Both)
1732 .map_err(sidecar_net_error),
1733 Self::LoopbackClient(stream) => stream.sock.close_endpoint(),
1734 Self::LoopbackServer(stream) => stream.sock.close_endpoint(),
1735 }
1736 }
1737
1738 fn peer_certificates(&self) -> Option<&[CertificateDer<'static>]> {
1739 match self {
1740 Self::Client(stream) => stream.conn.peer_certificates(),
1741 Self::Server(stream) => stream.conn.peer_certificates(),
1742 Self::LoopbackClient(stream) => stream.conn.peer_certificates(),
1743 Self::LoopbackServer(stream) => stream.conn.peer_certificates(),
1744 }
1745 }
1746
1747 fn negotiated_cipher_suite(&self) -> Option<rustls::SupportedCipherSuite> {
1748 match self {
1749 Self::Client(stream) => stream.conn.negotiated_cipher_suite(),
1750 Self::Server(stream) => stream.conn.negotiated_cipher_suite(),
1751 Self::LoopbackClient(stream) => stream.conn.negotiated_cipher_suite(),
1752 Self::LoopbackServer(stream) => stream.conn.negotiated_cipher_suite(),
1753 }
1754 }
1755
1756 fn protocol_version(&self) -> Option<rustls::ProtocolVersion> {
1757 match self {
1758 Self::Client(stream) => stream.conn.protocol_version(),
1759 Self::Server(stream) => stream.conn.protocol_version(),
1760 Self::LoopbackClient(stream) => stream.conn.protocol_version(),
1761 Self::LoopbackServer(stream) => stream.conn.protocol_version(),
1762 }
1763 }
1764}
1765
1766impl ActiveUnixSocket {
1771 fn connect(host_path: &Path, guest_path: &str) -> Result<Self, SidecarError> {
1772 let stream = UnixStream::connect(host_path).map_err(sidecar_net_error)?;
1773 Self::from_stream(stream, None, None, Some(guest_path.to_owned()))
1774 }
1775
1776 fn from_stream(
1777 stream: UnixStream,
1778 listener_id: Option<String>,
1779 local_path: Option<String>,
1780 remote_path: Option<String>,
1781 ) -> Result<Self, SidecarError> {
1782 let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
1783 let stream = Arc::new(Mutex::new(stream));
1784 let (sender, events) = mpsc::channel();
1785 let saw_local_shutdown = Arc::new(AtomicBool::new(false));
1786 let saw_remote_end = Arc::new(AtomicBool::new(false));
1787 let close_notified = Arc::new(AtomicBool::new(false));
1788 spawn_unix_socket_reader(
1789 read_stream,
1790 sender.clone(),
1791 Arc::clone(&saw_local_shutdown),
1792 Arc::clone(&saw_remote_end),
1793 Arc::clone(&close_notified),
1794 );
1795
1796 Ok(Self {
1797 stream,
1798 events,
1799 event_sender: sender,
1800 listener_id,
1801 local_path,
1802 remote_path,
1803 saw_local_shutdown,
1804 saw_remote_end,
1805 close_notified,
1806 })
1807 }
1808
1809 fn poll(&mut self, wait: Duration) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
1810 match self.events.recv_timeout(wait) {
1811 Ok(event) => Ok(Some(event)),
1812 Err(RecvTimeoutError::Timeout) => Ok(None),
1813 Err(RecvTimeoutError::Disconnected) => Ok(None),
1814 }
1815 }
1816
1817 fn socket_info(&self) -> Value {
1818 json!({
1819 "localPath": self.local_path.clone(),
1820 "remotePath": self.remote_path.clone(),
1821 })
1822 }
1823
1824 fn write_all(&self, contents: &[u8]) -> Result<usize, SidecarError> {
1825 let mut stream = self
1826 .stream
1827 .lock()
1828 .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1829 stream.write_all(contents).map_err(sidecar_net_error)?;
1830 Ok(contents.len())
1831 }
1832
1833 fn shutdown_write(&self) -> Result<(), SidecarError> {
1834 let stream = self
1835 .stream
1836 .lock()
1837 .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1838 self.saw_local_shutdown.store(true, Ordering::SeqCst);
1839 stream
1840 .shutdown(Shutdown::Write)
1841 .map_err(sidecar_net_error)?;
1842 if self.saw_remote_end.load(Ordering::SeqCst)
1843 && !self.close_notified.swap(true, Ordering::SeqCst)
1844 {
1845 let _ = self
1846 .event_sender
1847 .send(JavascriptTcpSocketEvent::Close { had_error: false });
1848 }
1849 Ok(())
1850 }
1851
1852 fn close(&self) -> Result<(), SidecarError> {
1853 let stream = self
1854 .stream
1855 .lock()
1856 .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1857 stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
1858 }
1859}
1860
1861impl ActiveUnixListener {
1864 fn bind(
1865 host_path: &Path,
1866 guest_path: &str,
1867 backlog: Option<u32>,
1868 ) -> Result<Self, SidecarError> {
1869 if let Some(parent) = host_path.parent() {
1870 fs::create_dir_all(parent).map_err(sidecar_net_error)?;
1871 }
1872 let listener = UnixListener::bind(host_path).map_err(sidecar_net_error)?;
1873 listener.set_nonblocking(true).map_err(sidecar_net_error)?;
1874 Ok(Self {
1875 listener,
1876 path: guest_path.to_owned(),
1877 backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
1878 .expect("default backlog fits within usize"),
1879 active_connection_ids: BTreeSet::new(),
1880 })
1881 }
1882
1883 fn path(&self) -> &str {
1884 &self.path
1885 }
1886
1887 fn poll(
1888 &mut self,
1889 wait: Duration,
1890 ) -> Result<Option<JavascriptUnixListenerEvent>, SidecarError> {
1891 let deadline = Instant::now() + wait;
1892 loop {
1893 match self.listener.accept() {
1894 Ok((stream, remote_addr)) => {
1895 if self.active_connection_ids.len() >= self.backlog {
1896 let _ = stream.shutdown(Shutdown::Both);
1897 if wait.is_zero() || Instant::now() >= deadline {
1898 return Ok(None);
1899 }
1900 continue;
1901 }
1902
1903 let local_path = Some(self.path.clone());
1904 let remote_path = unix_socket_path(&remote_addr);
1905 return Ok(Some(JavascriptUnixListenerEvent::Connection(
1906 PendingUnixSocket {
1907 stream,
1908 local_path,
1909 remote_path,
1910 },
1911 )));
1912 }
1913 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
1914 if wait.is_zero() || Instant::now() >= deadline {
1915 return Ok(None);
1916 }
1917 thread::sleep(Duration::from_millis(10));
1918 }
1919 Err(error) => {
1920 return Ok(Some(JavascriptUnixListenerEvent::Error {
1921 code: io_error_code(&error),
1922 message: error.to_string(),
1923 }));
1924 }
1925 }
1926 }
1927 }
1928
1929 fn close(&self) -> Result<(), SidecarError> {
1930 Ok(())
1931 }
1932
1933 fn active_connection_count(&self) -> usize {
1934 self.active_connection_ids.len()
1935 }
1936
1937 fn register_connection(&mut self, socket_id: &str) {
1938 self.active_connection_ids.insert(socket_id.to_string());
1939 }
1940
1941 fn release_connection(&mut self, socket_id: &str) {
1942 self.active_connection_ids.remove(socket_id);
1943 }
1944}
1945
1946impl ActiveTcpListener {
1947 fn bind(
1948 bind_host: &str,
1949 guest_host: &str,
1950 guest_port: u16,
1951 backlog: Option<u32>,
1952 ) -> Result<Self, SidecarError> {
1953 let bind_addr = resolve_tcp_bind_addr(bind_host, 0)?;
1954 let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
1955 let listener = TcpListener::bind(bind_addr).map_err(sidecar_net_error)?;
1956 listener.set_nonblocking(true).map_err(sidecar_net_error)?;
1957 let local_addr = listener.local_addr().map_err(sidecar_net_error)?;
1958 Ok(Self {
1959 listener: Some(listener),
1960 kernel_socket_id: None,
1961 local_addr: Some(local_addr),
1962 guest_local_addr: guest_addr,
1963 backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
1964 .expect("default backlog fits within usize"),
1965 active_connection_ids: BTreeSet::new(),
1966 })
1967 }
1968
1969 fn bind_kernel(
1970 kernel: &mut SidecarKernel,
1971 kernel_pid: u32,
1972 guest_host: &str,
1973 guest_port: u16,
1974 backlog: Option<u32>,
1975 ) -> Result<Self, SidecarError> {
1976 let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
1977 let spec = match guest_addr {
1978 SocketAddr::V4(_) => SocketSpec::tcp(),
1979 SocketAddr::V6(_) => SocketSpec::new(SocketDomain::Inet6, SocketType::Stream),
1980 };
1981 let socket_id = kernel
1982 .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
1983 .map_err(kernel_error)?;
1984 kernel
1985 .socket_bind_inet(
1986 EXECUTION_DRIVER_NAME,
1987 kernel_pid,
1988 socket_id,
1989 InetSocketAddress::new(guest_addr.ip().to_string(), guest_addr.port()),
1990 )
1991 .map_err(kernel_error)?;
1992 kernel
1993 .socket_listen(
1994 EXECUTION_DRIVER_NAME,
1995 kernel_pid,
1996 socket_id,
1997 usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
1998 .expect("default backlog fits within usize"),
1999 )
2000 .map_err(kernel_error)?;
2001 Ok(Self {
2002 listener: None,
2003 kernel_socket_id: Some(socket_id),
2004 local_addr: Some(guest_addr),
2005 guest_local_addr: guest_addr,
2006 backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2007 .expect("default backlog fits within usize"),
2008 active_connection_ids: BTreeSet::new(),
2009 })
2010 }
2011
2012 pub(crate) fn local_addr(&self) -> SocketAddr {
2013 self.local_addr.unwrap_or(self.guest_local_addr)
2014 }
2015
2016 fn guest_local_addr(&self) -> SocketAddr {
2017 self.guest_local_addr
2018 }
2019
2020 fn poll(
2021 &mut self,
2022 kernel: &mut SidecarKernel,
2023 kernel_pid: u32,
2024 wait: Duration,
2025 ) -> Result<Option<JavascriptTcpListenerEvent>, SidecarError> {
2026 if let Some(socket_id) = self.kernel_socket_id {
2027 let result = kernel
2028 .poll_targets(
2029 EXECUTION_DRIVER_NAME,
2030 kernel_pid,
2031 vec![PollTargetEntry::socket(socket_id, POLLIN)],
2032 i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
2033 )
2034 .map_err(kernel_error)?;
2035 let revents = result
2036 .targets
2037 .first()
2038 .map(|entry| entry.revents)
2039 .unwrap_or_else(PollEvents::empty);
2040 if revents.is_empty() {
2041 return Ok(None);
2042 }
2043 let accepted_socket_id =
2044 match kernel.socket_accept(EXECUTION_DRIVER_NAME, kernel_pid, socket_id) {
2045 Ok(accepted_socket_id) => accepted_socket_id,
2046 Err(error) if error.code() == "EAGAIN" => return Ok(None),
2047 Err(error) => {
2048 return Ok(Some(JavascriptTcpListenerEvent::Error {
2049 code: Some(error.code().to_string()),
2050 message: error.to_string(),
2051 }));
2052 }
2053 };
2054 let accepted = kernel.socket_get(accepted_socket_id).ok_or_else(|| {
2055 SidecarError::InvalidState(format!(
2056 "accepted kernel TCP socket {accepted_socket_id} is missing"
2057 ))
2058 })?;
2059 let local_addr = accepted.local_address().ok_or_else(|| {
2060 SidecarError::InvalidState(format!(
2061 "accepted kernel TCP socket {accepted_socket_id} missing local address"
2062 ))
2063 })?;
2064 let remote_addr = accepted.peer_address().ok_or_else(|| {
2065 SidecarError::InvalidState(format!(
2066 "accepted kernel TCP socket {accepted_socket_id} missing peer address"
2067 ))
2068 })?;
2069 return Ok(Some(JavascriptTcpListenerEvent::Connection(
2070 PendingTcpSocket {
2071 stream: None,
2072 kernel_socket_id: Some(accepted_socket_id),
2073 preallocated: true,
2074 guest_local_addr: resolve_tcp_bind_addr(local_addr.host(), local_addr.port())?,
2075 guest_remote_addr: resolve_tcp_bind_addr(
2076 remote_addr.host(),
2077 remote_addr.port(),
2078 )?,
2079 },
2080 )));
2081 }
2082
2083 let deadline = Instant::now() + wait;
2084 loop {
2085 match self
2086 .listener
2087 .as_ref()
2088 .ok_or_else(|| {
2089 SidecarError::InvalidState(String::from("TCP listener socket missing"))
2090 })?
2091 .accept()
2092 {
2093 Ok((stream, remote_addr)) => {
2094 if self.active_connection_ids.len() >= self.backlog {
2095 let _ = stream.shutdown(Shutdown::Both);
2096 if wait.is_zero() || Instant::now() >= deadline {
2097 return Ok(None);
2098 }
2099 continue;
2100 }
2101 return Ok(Some(JavascriptTcpListenerEvent::Connection(
2102 PendingTcpSocket {
2103 stream: Some(stream),
2104 kernel_socket_id: None,
2105 preallocated: false,
2106 guest_local_addr: self.guest_local_addr,
2107 guest_remote_addr: SocketAddr::new(
2108 remote_addr.ip(),
2109 remote_addr.port(),
2110 ),
2111 },
2112 )));
2113 }
2114 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2115 if wait.is_zero() || Instant::now() >= deadline {
2116 return Ok(None);
2117 }
2118 thread::sleep(Duration::from_millis(10));
2119 }
2120 Err(error) => {
2121 return Ok(Some(JavascriptTcpListenerEvent::Error {
2122 code: io_error_code(&error),
2123 message: error.to_string(),
2124 }));
2125 }
2126 }
2127 }
2128 }
2129
2130 fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
2131 if let Some(socket_id) = self.kernel_socket_id {
2132 kernel
2133 .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
2134 .map_err(kernel_error)?;
2135 }
2136 Ok(())
2137 }
2138
2139 fn active_connection_count(&self) -> usize {
2140 self.active_connection_ids.len()
2141 }
2142
2143 fn register_connection(&mut self, socket_id: &str) {
2144 self.active_connection_ids.insert(socket_id.to_string());
2145 }
2146
2147 fn release_connection(&mut self, socket_id: &str) {
2148 self.active_connection_ids.remove(socket_id);
2149 }
2150}
2151
2152impl ActiveUdpSocket {
2155 fn new(
2156 kernel: &mut SidecarKernel,
2157 kernel_pid: u32,
2158 family: JavascriptUdpFamily,
2159 ) -> Result<Self, SidecarError> {
2160 let spec = match family {
2161 JavascriptUdpFamily::Ipv4 => SocketSpec::udp(),
2162 JavascriptUdpFamily::Ipv6 => SocketSpec::new(SocketDomain::Inet6, SocketType::Datagram),
2163 };
2164 let socket_id = kernel
2165 .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
2166 .map_err(kernel_error)?;
2167 Ok(Self {
2168 family,
2169 socket: None,
2170 kernel_socket_id: Some(socket_id),
2171 guest_local_addr: None,
2172 recv_buffer_size: 0,
2173 send_buffer_size: 0,
2174 })
2175 }
2176
2177 fn local_addr(&self) -> Option<SocketAddr> {
2178 self.guest_local_addr
2179 }
2180
2181 fn socket(&self) -> Result<&UdpSocket, SidecarError> {
2182 self.socket
2183 .as_ref()
2184 .ok_or_else(|| SidecarError::Execution(String::from("EBADF: bad file descriptor")))
2185 }
2186
2187 fn bind(
2188 &mut self,
2189 kernel: &mut SidecarKernel,
2190 kernel_pid: u32,
2191 host: Option<&str>,
2192 port: u16,
2193 context: &JavascriptSocketPathContext,
2194 ) -> Result<SocketAddr, SidecarError> {
2195 if self.socket.is_some() || self.guest_local_addr.is_some() {
2196 return Err(SidecarError::Execution(String::from(
2197 "EINVAL: secure-exec dgram socket is already bound",
2198 )));
2199 }
2200
2201 let (bind_host, guest_host, guest_family) = normalize_udp_bind_host(host, self.family)?;
2202 let guest_port = allocate_guest_listen_port(
2203 port,
2204 guest_family,
2205 &context.used_udp_guest_ports,
2206 context.listen_policy,
2207 )?;
2208 let local_addr = resolve_udp_bind_addr(guest_host, guest_port, self.family)?;
2209 if let Some(socket_id) = self.kernel_socket_id {
2210 kernel
2211 .socket_bind_inet(
2212 EXECUTION_DRIVER_NAME,
2213 kernel_pid,
2214 socket_id,
2215 InetSocketAddress::new(local_addr.ip().to_string(), local_addr.port()),
2216 )
2217 .map_err(kernel_error)?;
2218 } else {
2219 let bind_addr = resolve_udp_bind_addr(bind_host, 0, self.family)?;
2220 let socket = UdpSocket::bind(bind_addr).map_err(sidecar_net_error)?;
2221 socket.set_nonblocking(true).map_err(sidecar_net_error)?;
2222 self.socket = Some(socket);
2223 }
2224 self.guest_local_addr = Some(local_addr);
2225 Ok(local_addr)
2226 }
2227
2228 fn ensure_bound_for_send(
2229 &mut self,
2230 kernel: &mut SidecarKernel,
2231 kernel_pid: u32,
2232 context: &JavascriptSocketPathContext,
2233 ) -> Result<SocketAddr, SidecarError> {
2234 if let Some(local_addr) = self.local_addr() {
2235 return Ok(local_addr);
2236 }
2237
2238 self.bind(kernel, kernel_pid, None, 0, context)
2239 }
2240
2241 fn send_to<B>(
2242 &mut self,
2243 request: ActiveUdpSendToRequest<'_, B>,
2244 ) -> Result<(usize, SocketAddr), SidecarError>
2245 where
2246 B: NativeSidecarBridge + Send + 'static,
2247 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
2248 {
2249 let ActiveUdpSendToRequest {
2250 bridge,
2251 kernel,
2252 kernel_pid,
2253 vm_id,
2254 dns,
2255 host,
2256 port,
2257 context,
2258 contents,
2259 } = request;
2260 let remote_addr = resolve_udp_addr(UdpRemoteAddrRequest {
2261 bridge,
2262 kernel,
2263 vm_id,
2264 dns,
2265 host,
2266 port,
2267 family: self.family,
2268 context,
2269 })?;
2270 let local_addr = self.ensure_bound_for_send(kernel, kernel_pid, context)?;
2271 let written = if let Some(socket_id) = self.kernel_socket_id {
2272 if is_loopback_ip(remote_addr.ip()) && remote_addr.port() == port {
2273 kernel
2274 .socket_send_to_inet_loopback(
2275 EXECUTION_DRIVER_NAME,
2276 kernel_pid,
2277 socket_id,
2278 InetSocketAddress::new(remote_addr.ip().to_string(), remote_addr.port()),
2279 contents,
2280 )
2281 .map_err(kernel_error)?
2282 } else {
2283 return Err(SidecarError::Execution(String::from(
2284 "ERR_NOT_IMPLEMENTED: external UDP datagrams are not yet supported by the kernel-backed V8 bridge",
2285 )));
2286 }
2287 } else {
2288 let socket = self.socket.as_ref().ok_or_else(|| {
2289 SidecarError::InvalidState(String::from("UDP socket is not initialized"))
2290 })?;
2291 socket
2292 .send_to(contents, remote_addr)
2293 .map_err(sidecar_net_error)?
2294 };
2295 Ok((written, local_addr))
2296 }
2297
2298 fn poll(
2299 &self,
2300 kernel: &mut SidecarKernel,
2301 kernel_pid: u32,
2302 wait: Duration,
2303 ) -> Result<Option<JavascriptUdpSocketEvent>, SidecarError> {
2304 if let Some(socket_id) = self.kernel_socket_id {
2305 let result = kernel
2306 .poll_targets(
2307 EXECUTION_DRIVER_NAME,
2308 kernel_pid,
2309 vec![PollTargetEntry::socket(socket_id, POLLIN)],
2310 i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
2311 )
2312 .map_err(kernel_error)?;
2313 let revents = result
2314 .targets
2315 .first()
2316 .map(|entry| entry.revents)
2317 .unwrap_or_else(PollEvents::empty);
2318 if revents.is_empty() {
2319 return Ok(None);
2320 }
2321 return match kernel.socket_recv_datagram(
2322 EXECUTION_DRIVER_NAME,
2323 kernel_pid,
2324 socket_id,
2325 64 * 1024,
2326 ) {
2327 Ok(Some(datagram)) => {
2328 let (source_address, payload) = datagram.into_parts();
2329 let remote_addr = source_address
2330 .map(|source| {
2331 resolve_udp_bind_addr(source.host(), source.port(), self.family)
2332 })
2333 .transpose()?
2334 .unwrap_or_else(|| match self.family {
2335 JavascriptUdpFamily::Ipv4 => {
2336 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)
2337 }
2338 JavascriptUdpFamily::Ipv6 => {
2339 SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0)
2340 }
2341 });
2342 Ok(Some(JavascriptUdpSocketEvent::Message {
2343 data: payload,
2344 remote_addr,
2345 }))
2346 }
2347 Ok(None) => Ok(None),
2348 Err(error) if error.code() == "EAGAIN" => Ok(None),
2349 Err(error) => Ok(Some(JavascriptUdpSocketEvent::Error {
2350 code: Some(error.code().to_string()),
2351 message: error.to_string(),
2352 })),
2353 };
2354 }
2355 let socket = self.socket()?;
2356 let deadline = Instant::now() + wait;
2357 let mut buffer = vec![0_u8; 64 * 1024];
2358
2359 loop {
2360 match socket.recv_from(&mut buffer) {
2361 Ok((bytes_read, remote_addr)) => {
2362 return Ok(Some(JavascriptUdpSocketEvent::Message {
2363 data: buffer[..bytes_read].to_vec(),
2364 remote_addr,
2365 }));
2366 }
2367 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2368 if wait.is_zero() || Instant::now() >= deadline {
2369 return Ok(None);
2370 }
2371 thread::sleep(Duration::from_millis(10));
2372 }
2373 Err(error) => {
2374 return Ok(Some(JavascriptUdpSocketEvent::Error {
2375 code: io_error_code(&error),
2376 message: error.to_string(),
2377 }));
2378 }
2379 }
2380 }
2381 }
2382
2383 fn close(&mut self, kernel: &mut SidecarKernel, kernel_pid: u32) {
2384 if let Some(socket_id) = self.kernel_socket_id {
2385 let _ = kernel.socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id);
2386 }
2387 self.socket.take();
2388 self.guest_local_addr = None;
2389 }
2390
2391 fn set_buffer_size(&mut self, which: &str, size: usize) -> Result<(), SidecarError> {
2392 match which {
2393 "recv" => self.recv_buffer_size = size,
2394 "send" => self.send_buffer_size = size,
2395 other => {
2396 return Err(SidecarError::InvalidState(format!(
2397 "unsupported UDP buffer size kind {other}"
2398 )));
2399 }
2400 }
2401 if self.kernel_socket_id.is_some() {
2402 return Ok(());
2403 }
2404 let socket = self.socket()?;
2405 let socket = SockRef::from(socket);
2406 match which {
2407 "recv" => socket.set_recv_buffer_size(size).map_err(sidecar_net_error),
2408 "send" => socket.set_send_buffer_size(size).map_err(sidecar_net_error),
2409 other => Err(SidecarError::InvalidState(format!(
2410 "unsupported UDP buffer size kind {other}"
2411 ))),
2412 }
2413 }
2414
2415 fn get_buffer_size(&self, which: &str) -> Result<usize, SidecarError> {
2416 if self.kernel_socket_id.is_some() {
2417 return Ok(match which {
2418 "recv" => self.recv_buffer_size,
2419 "send" => self.send_buffer_size,
2420 other => {
2421 return Err(SidecarError::InvalidState(format!(
2422 "unsupported UDP buffer size kind {other}"
2423 )));
2424 }
2425 });
2426 }
2427 let socket = self.socket()?;
2428 let socket = SockRef::from(socket);
2429 match which {
2430 "recv" => socket.recv_buffer_size().map_err(sidecar_net_error),
2431 "send" => socket.send_buffer_size().map_err(sidecar_net_error),
2432 other => Err(SidecarError::InvalidState(format!(
2433 "unsupported UDP buffer size kind {other}"
2434 ))),
2435 }
2436 }
2437}
2438
2439impl ActiveExecution {
2442 pub(crate) fn uses_shared_v8_runtime(&self) -> bool {
2443 match self {
2444 Self::Javascript(execution) => execution.uses_shared_v8_runtime(),
2445 Self::Python(execution) => execution.uses_shared_v8_runtime(),
2446 Self::Wasm(execution) => execution.uses_shared_v8_runtime(),
2447 Self::Tool(_) => false,
2448 }
2449 }
2450
2451 pub(crate) fn child_pid(&self) -> u32 {
2452 match self {
2453 Self::Javascript(execution) => execution.child_pid(),
2454 Self::Python(execution) => execution.child_pid(),
2455 Self::Wasm(execution) => execution.child_pid(),
2456 Self::Tool(_) => 0,
2457 }
2458 }
2459
2460 pub(crate) fn write_stdin(&mut self, chunk: &[u8]) -> Result<(), SidecarError> {
2461 match self {
2462 Self::Javascript(execution) => execution
2463 .write_stdin(chunk)
2464 .map_err(|error| SidecarError::Execution(error.to_string())),
2465 Self::Python(execution) => execution
2466 .write_stdin(chunk)
2467 .map_err(|error| SidecarError::Execution(error.to_string())),
2468 Self::Wasm(execution) => execution
2469 .write_stdin(chunk)
2470 .map_err(|error| SidecarError::Execution(error.to_string())),
2471 Self::Tool(_) => Ok(()),
2472 }
2473 }
2474
2475 pub(crate) fn close_stdin(&mut self) -> Result<(), SidecarError> {
2476 match self {
2477 Self::Javascript(execution) => execution
2478 .close_stdin()
2479 .map_err(|error| SidecarError::Execution(error.to_string())),
2480 Self::Python(execution) => execution
2481 .close_stdin()
2482 .map_err(|error| SidecarError::Execution(error.to_string())),
2483 Self::Wasm(execution) => execution
2484 .close_stdin()
2485 .map_err(|error| SidecarError::Execution(error.to_string())),
2486 Self::Tool(_) => Ok(()),
2487 }
2488 }
2489
2490 pub(crate) fn respond_python_vfs_rpc_success(
2491 &mut self,
2492 id: u64,
2493 payload: PythonVfsRpcResponsePayload,
2494 ) -> Result<(), SidecarError> {
2495 match self {
2496 Self::Python(execution) => execution
2497 .respond_vfs_rpc_success(id, payload)
2498 .map_err(|error| SidecarError::Execution(error.to_string())),
2499 _ => Err(SidecarError::InvalidState(String::from(
2500 "only Python executions can service Python VFS RPC responses",
2501 ))),
2502 }
2503 }
2504
2505 pub(crate) fn respond_python_vfs_rpc_error(
2506 &mut self,
2507 id: u64,
2508 code: impl Into<String>,
2509 message: impl Into<String>,
2510 ) -> Result<(), SidecarError> {
2511 match self {
2512 Self::Python(execution) => execution
2513 .respond_vfs_rpc_error(id, code, message)
2514 .map_err(|error| SidecarError::Execution(error.to_string())),
2515 _ => Err(SidecarError::InvalidState(String::from(
2516 "only Python executions can service Python VFS RPC responses",
2517 ))),
2518 }
2519 }
2520
2521 pub(crate) fn send_javascript_stream_event(
2522 &self,
2523 event_type: &str,
2524 payload: Value,
2525 ) -> Result<(), SidecarError> {
2526 match self {
2527 Self::Javascript(execution) => execution
2528 .send_stream_event(event_type, payload)
2529 .map_err(|error| SidecarError::Execution(error.to_string())),
2530 Self::Wasm(execution) => execution
2531 .send_stream_event(event_type, payload)
2532 .map_err(|error| SidecarError::Execution(error.to_string())),
2533 _ => Err(SidecarError::InvalidState(String::from(
2534 "only embedded V8 executions can receive JavaScript stream events",
2535 ))),
2536 }
2537 }
2538
2539 pub(crate) fn javascript_v8_session_handle(&self) -> Option<V8SessionHandle> {
2540 match self {
2541 Self::Javascript(execution) => Some(execution.v8_session_handle()),
2542 Self::Wasm(execution) => Some(execution.v8_session_handle()),
2543 _ => None,
2544 }
2545 }
2546
2547 pub(crate) fn terminate(&mut self) -> Result<(), SidecarError> {
2548 match self {
2549 Self::Javascript(execution) => execution
2550 .terminate()
2551 .map_err(|error| SidecarError::Execution(error.to_string())),
2552 Self::Python(execution) => execution
2553 .kill()
2554 .map_err(|error| SidecarError::Execution(error.to_string())),
2555 Self::Wasm(execution) => execution
2556 .terminate()
2557 .map_err(|error| SidecarError::Execution(error.to_string())),
2558 Self::Tool(_) => Ok(()),
2559 }
2560 }
2561
2562 pub(crate) fn respond_javascript_sync_rpc_success(
2563 &mut self,
2564 id: u64,
2565 result: Value,
2566 ) -> Result<(), SidecarError> {
2567 match self {
2568 Self::Javascript(execution) => execution
2569 .respond_sync_rpc_success(id, result)
2570 .map_err(|error| SidecarError::Execution(error.to_string())),
2571 Self::Python(execution) => execution
2572 .respond_javascript_sync_rpc_success(id, result)
2573 .map_err(|error| SidecarError::Execution(error.to_string())),
2574 Self::Wasm(execution) => execution
2575 .respond_sync_rpc_success(id, result)
2576 .map_err(|error| SidecarError::Execution(error.to_string())),
2577 _ => Err(SidecarError::InvalidState(String::from(
2578 "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
2579 ))),
2580 }
2581 }
2582
2583 pub(crate) fn respond_javascript_sync_rpc_error(
2584 &mut self,
2585 id: u64,
2586 code: impl Into<String>,
2587 message: impl Into<String>,
2588 ) -> Result<(), SidecarError> {
2589 match self {
2590 Self::Javascript(execution) => execution
2591 .respond_sync_rpc_error(id, code, message)
2592 .map_err(|error| SidecarError::Execution(error.to_string())),
2593 Self::Python(execution) => execution
2594 .respond_javascript_sync_rpc_error(id, code, message)
2595 .map_err(|error| SidecarError::Execution(error.to_string())),
2596 Self::Wasm(execution) => execution
2597 .respond_sync_rpc_error(id, code, message)
2598 .map_err(|error| SidecarError::Execution(error.to_string())),
2599 _ => Err(SidecarError::InvalidState(String::from(
2600 "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
2601 ))),
2602 }
2603 }
2604
2605 pub(crate) async fn poll_event(
2606 &mut self,
2607 timeout: Duration,
2608 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
2609 match self {
2610 Self::Javascript(execution) => execution
2611 .poll_event(timeout)
2612 .await
2613 .map(|event| {
2614 event.map(|event| match event {
2615 JavascriptExecutionEvent::Stdout(chunk) => {
2616 ActiveExecutionEvent::Stdout(chunk)
2617 }
2618 JavascriptExecutionEvent::Stderr(chunk) => {
2619 ActiveExecutionEvent::Stderr(chunk)
2620 }
2621 JavascriptExecutionEvent::SyncRpcRequest(request) => {
2622 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2623 }
2624 JavascriptExecutionEvent::SignalState {
2625 signal,
2626 registration,
2627 } => ActiveExecutionEvent::SignalState {
2628 signal,
2629 registration: map_node_signal_registration(registration),
2630 },
2631 JavascriptExecutionEvent::Exited(code) => {
2632 ActiveExecutionEvent::Exited(code)
2633 }
2634 })
2635 })
2636 .map_err(|error| SidecarError::Execution(error.to_string())),
2637 Self::Python(execution) => execution
2638 .poll_event(timeout)
2639 .await
2640 .map(|event| {
2641 event.map(|event| match event {
2642 PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2643 PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2644 PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
2645 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2646 }
2647 PythonExecutionEvent::VfsRpcRequest(request) => {
2648 ActiveExecutionEvent::PythonVfsRpcRequest(request)
2649 }
2650 PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2651 })
2652 })
2653 .map_err(|error| SidecarError::Execution(error.to_string())),
2654 Self::Wasm(execution) => execution
2655 .poll_event(timeout)
2656 .await
2657 .map(|event| {
2658 event.map(|event| match event {
2659 WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2660 WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2661 WasmExecutionEvent::SyncRpcRequest(request) => {
2662 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2663 }
2664 WasmExecutionEvent::SignalState {
2665 signal,
2666 registration,
2667 } => ActiveExecutionEvent::SignalState {
2668 signal,
2669 registration: map_wasm_signal_registration(registration),
2670 },
2671 WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2672 })
2673 })
2674 .map_err(|error| SidecarError::Execution(error.to_string())),
2675 Self::Tool(execution) => {
2676 let _ = timeout;
2677 poll_tool_process_event(execution)
2678 }
2679 }
2680 }
2681
2682 pub(crate) fn poll_event_blocking(
2683 &mut self,
2684 timeout: Duration,
2685 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
2686 match self {
2687 Self::Javascript(execution) => execution
2688 .poll_event_blocking(timeout)
2689 .map(|event| {
2690 event.map(|event| match event {
2691 JavascriptExecutionEvent::Stdout(chunk) => {
2692 ActiveExecutionEvent::Stdout(chunk)
2693 }
2694 JavascriptExecutionEvent::Stderr(chunk) => {
2695 ActiveExecutionEvent::Stderr(chunk)
2696 }
2697 JavascriptExecutionEvent::SyncRpcRequest(request) => {
2698 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2699 }
2700 JavascriptExecutionEvent::SignalState {
2701 signal,
2702 registration,
2703 } => ActiveExecutionEvent::SignalState {
2704 signal,
2705 registration: map_node_signal_registration(registration),
2706 },
2707 JavascriptExecutionEvent::Exited(code) => {
2708 ActiveExecutionEvent::Exited(code)
2709 }
2710 })
2711 })
2712 .map_err(|error| SidecarError::Execution(error.to_string())),
2713 Self::Python(execution) => execution
2714 .poll_event_blocking(timeout)
2715 .map(|event| {
2716 event.map(|event| match event {
2717 PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2718 PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2719 PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
2720 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2721 }
2722 PythonExecutionEvent::VfsRpcRequest(request) => {
2723 ActiveExecutionEvent::PythonVfsRpcRequest(request)
2724 }
2725 PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2726 })
2727 })
2728 .map_err(|error| SidecarError::Execution(error.to_string())),
2729 Self::Wasm(execution) => execution
2730 .poll_event_blocking(timeout)
2731 .map(|event| {
2732 event.map(|event| match event {
2733 WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2734 WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2735 WasmExecutionEvent::SyncRpcRequest(request) => {
2736 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2737 }
2738 WasmExecutionEvent::SignalState {
2739 signal,
2740 registration,
2741 } => ActiveExecutionEvent::SignalState {
2742 signal,
2743 registration: map_wasm_signal_registration(registration),
2744 },
2745 WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2746 })
2747 })
2748 .map_err(|error| SidecarError::Execution(error.to_string())),
2749 Self::Tool(execution) => {
2750 let _ = timeout;
2751 poll_tool_process_event(execution)
2752 }
2753 }
2754 }
2755}
2756
2757struct ToolProcessEventRequest {
2758 sidecar_requests: SharedSidecarRequestClient,
2759 connection_id: String,
2760 session_id: String,
2761 vm_id: String,
2762 tool_resolution: ToolCommandResolution,
2763 cancelled: Arc<AtomicBool>,
2764 pending_events: Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
2765 events_overflowed: Arc<AtomicBool>,
2766}
2767
2768pub(crate) fn send_tool_process_event(
2769 pending_events: &Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
2770 events_overflowed: &AtomicBool,
2771 event: ActiveExecutionEvent,
2772) -> bool {
2773 let mut pending_events = pending_events
2774 .lock()
2775 .unwrap_or_else(|poisoned| poisoned.into_inner());
2776 if pending_events.len() >= MAX_PROCESS_EVENT_QUEUE {
2777 events_overflowed.store(true, Ordering::Relaxed);
2778 return false;
2779 }
2780 pending_events.push_back(event);
2781 true
2782}
2783
2784fn spawn_tool_process_events(request: ToolProcessEventRequest) {
2785 let ToolProcessEventRequest {
2786 sidecar_requests,
2787 connection_id,
2788 session_id,
2789 vm_id,
2790 tool_resolution,
2791 cancelled,
2792 pending_events,
2793 events_overflowed,
2794 } = request;
2795 std::thread::spawn(move || match tool_resolution {
2796 ToolCommandResolution::Failure(message) => {
2797 if !send_tool_process_event(
2798 &pending_events,
2799 &events_overflowed,
2800 ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
2801 ) {
2802 return;
2803 }
2804 let _ = send_tool_process_event(
2805 &pending_events,
2806 &events_overflowed,
2807 ActiveExecutionEvent::Exited(1),
2808 );
2809 }
2810 ToolCommandResolution::Invoke { request, timeout } => {
2811 let response = sidecar_requests.invoke(
2812 OwnershipScope::vm(connection_id.clone(), session_id.clone(), vm_id.clone()),
2813 SidecarRequestPayload::HostCallback(request.clone()),
2814 timeout,
2815 );
2816 if cancelled.load(Ordering::Relaxed) {
2817 return;
2818 }
2819
2820 match response {
2821 Ok(crate::protocol::SidecarResponsePayload::HostCallbackResult(result)) => {
2822 if let Some(value) = result.result {
2823 let value: serde_json::Value = serde_json::from_str(&value)
2824 .unwrap_or(serde_json::Value::String(value));
2825 let stdout = serde_json::to_vec(&json!({
2826 "ok": true,
2827 "result": value,
2828 }))
2829 .unwrap_or_else(|error| {
2830 format_tool_failure_output(&format!(
2831 "failed to serialize tool result: {error}"
2832 ))
2833 });
2834 if !send_tool_process_event(
2835 &pending_events,
2836 &events_overflowed,
2837 ActiveExecutionEvent::Stdout(stdout),
2838 ) {
2839 return;
2840 }
2841 let _ = send_tool_process_event(
2842 &pending_events,
2843 &events_overflowed,
2844 ActiveExecutionEvent::Exited(0),
2845 );
2846 } else {
2847 let message = result
2848 .error
2849 .unwrap_or_else(|| String::from("tool invocation returned no result"));
2850 if !send_tool_process_event(
2851 &pending_events,
2852 &events_overflowed,
2853 ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
2854 ) {
2855 return;
2856 }
2857 let _ = send_tool_process_event(
2858 &pending_events,
2859 &events_overflowed,
2860 ActiveExecutionEvent::Exited(1),
2861 );
2862 }
2863 }
2864 Ok(_) => {
2865 if !send_tool_process_event(
2866 &pending_events,
2867 &events_overflowed,
2868 ActiveExecutionEvent::Stderr(format_tool_failure_output(
2869 "unexpected sidecar tool response",
2870 )),
2871 ) {
2872 return;
2873 }
2874 let _ = send_tool_process_event(
2875 &pending_events,
2876 &events_overflowed,
2877 ActiveExecutionEvent::Exited(1),
2878 );
2879 }
2880 Err(error) => {
2881 if !send_tool_process_event(
2882 &pending_events,
2883 &events_overflowed,
2884 ActiveExecutionEvent::Stderr(format_tool_failure_output(
2885 &error.to_string(),
2886 )),
2887 ) {
2888 return;
2889 }
2890 let _ = send_tool_process_event(
2891 &pending_events,
2892 &events_overflowed,
2893 ActiveExecutionEvent::Exited(1),
2894 );
2895 }
2896 }
2897 }
2898 });
2899}
2900
2901impl<B> NativeSidecar<B>
2902where
2903 B: NativeSidecarBridge + Send + 'static,
2904 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
2905{
2906 pub(crate) async fn execute(
2907 &mut self,
2908 request: &RequestFrame,
2909 payload: ExecuteRequest,
2910 ) -> Result<DispatchResult, SidecarError> {
2911 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
2912 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
2913
2914 let vm = self
2915 .vms
2916 .get_mut(&vm_id)
2917 .ok_or_else(|| missing_vm_error(&vm_id))?;
2918 if vm.active_processes.contains_key(&payload.process_id) {
2919 return Err(SidecarError::InvalidState(format!(
2920 "VM {vm_id} already has an active process with id {}",
2921 payload.process_id
2922 )));
2923 }
2924
2925 if let Some(command) = payload.command.as_deref() {
2926 if let Some(tool_resolution) =
2927 resolve_tool_command(vm, command, &payload.args, payload.cwd.as_deref())?
2928 {
2929 let guest_cwd = payload
2930 .cwd
2931 .as_deref()
2932 .map(normalize_path)
2933 .unwrap_or_else(|| vm.guest_cwd.clone());
2934 let kernel_handle = vm
2935 .kernel
2936 .create_virtual_process(
2937 EXECUTION_DRIVER_NAME,
2938 TOOL_DRIVER_NAME,
2939 command,
2940 std::iter::once(command.to_owned())
2941 .chain(payload.args.iter().cloned())
2942 .collect(),
2943 VirtualProcessOptions {
2944 env: vm.guest_env.clone(),
2945 cwd: Some(guest_cwd.clone()),
2946 ..VirtualProcessOptions::default()
2947 },
2948 )
2949 .map_err(kernel_error)?;
2950 let kernel_pid = kernel_handle.pid();
2951 let tool_execution = ToolExecution::default();
2952 let cancelled = tool_execution.cancelled.clone();
2953 let pending_events = tool_execution.pending_events.clone();
2954 let events_overflowed = tool_execution.events_overflowed.clone();
2955 vm.active_processes.insert(
2956 payload.process_id.clone(),
2957 ActiveProcess::new(
2958 kernel_pid,
2959 kernel_handle,
2960 GuestRuntimeKind::JavaScript,
2961 ActiveExecution::Tool(tool_execution),
2962 )
2963 .with_guest_cwd(guest_cwd.clone())
2964 .with_host_cwd(resolve_vm_guest_path_to_host(vm, &guest_cwd)),
2965 );
2966 self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
2967 spawn_tool_process_events(ToolProcessEventRequest {
2968 sidecar_requests: self.sidecar_requests.clone(),
2969 connection_id: connection_id.clone(),
2970 session_id: session_id.clone(),
2971 vm_id: vm_id.clone(),
2972 tool_resolution,
2973 cancelled,
2974 pending_events,
2975 events_overflowed,
2976 });
2977
2978 return Ok(DispatchResult {
2979 response: self.respond(
2980 request,
2981 ResponsePayload::ProcessStarted(ProcessStartedResponse {
2982 process_id: payload.process_id,
2983 pid: Some(kernel_pid),
2984 }),
2985 ),
2986 events: Vec::new(),
2987 });
2988 }
2989 }
2990
2991 let resolved = resolve_execute_request(vm, &payload)?;
2992 let mut env = resolved.env.clone();
2993 let sandbox_root = normalize_host_path(&vm.cwd);
2994 env.insert(
2995 String::from(EXECUTION_SANDBOX_ROOT_ENV),
2996 sandbox_root.to_string_lossy().into_owned(),
2997 );
2998 if resolved.runtime == GuestRuntimeKind::JavaScript {
2999 env.insert(
3000 String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
3001 String::from("1"),
3002 );
3003 } else if resolved.runtime == GuestRuntimeKind::WebAssembly {
3004 env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
3005 }
3006 let argv = std::iter::once(resolved.entrypoint.clone())
3007 .chain(resolved.execution_args.iter().cloned())
3008 .collect::<Vec<_>>();
3009 let kernel_handle = vm
3010 .kernel
3011 .spawn_process(
3012 &resolved.command,
3013 argv,
3014 SpawnOptions {
3015 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
3016 cwd: Some(resolved.guest_cwd.clone()),
3017 ..SpawnOptions::default()
3018 },
3019 )
3020 .map_err(kernel_error)?;
3021 let kernel_pid = kernel_handle.pid();
3022
3023 let (execution, process_env) = match resolved.runtime {
3024 GuestRuntimeKind::JavaScript => {
3025 let inline_code = load_javascript_entrypoint_source(
3026 vm,
3027 &resolved.host_cwd,
3028 &resolved.entrypoint,
3029 &env,
3030 );
3031 prepare_javascript_shadow(vm, &resolved)?;
3032
3033 let context =
3034 self.javascript_engine
3035 .create_context(CreateJavascriptContextRequest {
3036 vm_id: vm_id.clone(),
3037 bootstrap_module: None,
3038 compile_cache_root: Some(self.cache_root.join("node-compile-cache")),
3039 });
3040 let module_reader = build_module_reader(vm)
3041 .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
3042 let execution = self
3043 .javascript_engine
3044 .start_execution_with_module_reader(
3045 StartJavascriptExecutionRequest {
3046 vm_id: vm_id.clone(),
3047 context_id: context.context_id,
3048 argv: std::iter::once(resolved.entrypoint.clone())
3049 .chain(resolved.execution_args.iter().cloned())
3050 .collect(),
3051 env: env.clone(),
3052 cwd: resolved.host_cwd.clone(),
3053 inline_code,
3054 },
3055 module_reader,
3056 )
3057 .map_err(javascript_error)?;
3058 (ActiveExecution::Javascript(execution), env.clone())
3059 }
3060 GuestRuntimeKind::Python => {
3061 let python_file_path = python_file_entrypoint(&resolved.entrypoint);
3062 let pyodide_dist_path = self
3063 .python_engine
3064 .bundled_pyodide_dist_path_for_vm(&vm_id)
3065 .map_err(python_error)?;
3066 let pyodide_cache_path = pyodide_dist_path
3067 .parent()
3068 .and_then(Path::parent)
3069 .unwrap_or(pyodide_dist_path.as_path())
3070 .join("pyodide-package-cache");
3071 add_runtime_guest_path_mapping(
3072 &mut env,
3073 PYTHON_PYODIDE_GUEST_ROOT,
3074 &pyodide_dist_path,
3075 );
3076 add_runtime_guest_path_mapping(
3077 &mut env,
3078 PYTHON_PYODIDE_CACHE_GUEST_ROOT,
3079 &pyodide_cache_path,
3080 );
3081 add_runtime_host_access_path(
3082 &mut env,
3083 "AGENT_OS_EXTRA_FS_READ_PATHS",
3084 &pyodide_dist_path,
3085 true,
3086 );
3087 add_runtime_host_access_path(
3088 &mut env,
3089 "AGENT_OS_EXTRA_FS_READ_PATHS",
3090 &pyodide_cache_path,
3091 true,
3092 );
3093 add_runtime_host_access_path(
3094 &mut env,
3095 "AGENT_OS_EXTRA_FS_WRITE_PATHS",
3096 &pyodide_cache_path,
3097 false,
3098 );
3099 let context = self
3100 .python_engine
3101 .create_context(CreatePythonContextRequest {
3102 vm_id: vm_id.clone(),
3103 pyodide_dist_path,
3104 });
3105 let execution = self
3106 .python_engine
3107 .start_execution(StartPythonExecutionRequest {
3108 vm_id: vm_id.clone(),
3109 context_id: context.context_id,
3110 code: resolved.entrypoint.clone(),
3111 file_path: python_file_path,
3112 env: env.clone(),
3113 cwd: resolved.host_cwd.clone(),
3114 })
3115 .map_err(python_error)?;
3116 (ActiveExecution::Python(execution), env.clone())
3117 }
3118 GuestRuntimeKind::WebAssembly => {
3119 env.insert(
3120 String::from("AGENT_OS_VIRTUAL_PROCESS_PID"),
3121 kernel_pid.to_string(),
3122 );
3123 env.insert(
3124 String::from("AGENT_OS_VIRTUAL_PROCESS_PPID"),
3125 String::from("0"),
3126 );
3127 apply_wasm_limit_env(&mut env, vm.kernel.resource_limits());
3128 let wasm_permission_tier = resolved.wasm_permission_tier.unwrap_or_else(|| {
3129 resolve_wasm_permission_tier(
3130 vm,
3131 Some(&resolved.command),
3132 None,
3133 &resolved.entrypoint,
3134 )
3135 });
3136 let context = self.wasm_engine.create_context(CreateWasmContextRequest {
3137 vm_id: vm_id.clone(),
3138 module_path: Some(resolved.entrypoint.clone()),
3139 });
3140 let execution = self
3141 .wasm_engine
3142 .start_execution(StartWasmExecutionRequest {
3143 vm_id: vm_id.clone(),
3144 context_id: context.context_id,
3145 argv: resolved.process_args.clone(),
3146 env: env.clone(),
3147 cwd: resolved.host_cwd.clone(),
3148 permission_tier: execution_wasm_permission_tier(wasm_permission_tier),
3149 })
3150 .map_err(wasm_error)?;
3151 (ActiveExecution::Wasm(execution), env)
3152 }
3153 };
3154 let child_pid = execution.child_pid();
3155 let kernel_stdin_writer_fd = install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?;
3156 vm.active_processes.insert(
3157 payload.process_id.clone(),
3158 ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
3159 .with_kernel_stdin_writer_fd(kernel_stdin_writer_fd)
3160 .with_guest_cwd(resolved.guest_cwd.clone())
3161 .with_env(process_env)
3162 .with_host_cwd(resolved.host_cwd.clone()),
3163 );
3164 self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
3165
3166 Ok(DispatchResult {
3167 response: self.respond(
3168 request,
3169 ResponsePayload::ProcessStarted(ProcessStartedResponse {
3170 process_id: payload.process_id,
3171 pid: Some(if child_pid == 0 {
3172 kernel_pid
3173 } else {
3174 child_pid
3175 }),
3176 }),
3177 ),
3178 events: Vec::new(),
3179 })
3180 }
3181
3182 pub(crate) async fn write_stdin(
3183 &mut self,
3184 request: &RequestFrame,
3185 payload: WriteStdinRequest,
3186 ) -> Result<DispatchResult, SidecarError> {
3187 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3188 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3189
3190 let vm = self
3191 .vms
3192 .get_mut(&vm_id)
3193 .ok_or_else(|| missing_vm_error(&vm_id))?;
3194 let process = vm
3195 .active_processes
3196 .get_mut(&payload.process_id)
3197 .ok_or_else(|| {
3198 SidecarError::InvalidState(format!(
3199 "VM {vm_id} has no active process {}",
3200 payload.process_id
3201 ))
3202 })?;
3203 process.execution.write_stdin(&payload.chunk)?;
3204 write_kernel_process_stdin(&mut vm.kernel, process, &payload.chunk)?;
3205
3206 Ok(DispatchResult {
3207 response: self.respond(
3208 request,
3209 ResponsePayload::StdinWritten(StdinWrittenResponse {
3210 process_id: payload.process_id,
3211 accepted_bytes: payload.chunk.len() as u64,
3212 }),
3213 ),
3214 events: Vec::new(),
3215 })
3216 }
3217
3218 pub(crate) async fn close_stdin(
3219 &mut self,
3220 request: &RequestFrame,
3221 payload: CloseStdinRequest,
3222 ) -> Result<DispatchResult, SidecarError> {
3223 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3224 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3225
3226 let vm = self
3227 .vms
3228 .get_mut(&vm_id)
3229 .ok_or_else(|| missing_vm_error(&vm_id))?;
3230 let process = vm
3231 .active_processes
3232 .get_mut(&payload.process_id)
3233 .ok_or_else(|| {
3234 SidecarError::InvalidState(format!(
3235 "VM {vm_id} has no active process {}",
3236 payload.process_id
3237 ))
3238 })?;
3239 process.execution.close_stdin()?;
3240 close_kernel_process_stdin(&mut vm.kernel, process)?;
3241
3242 Ok(DispatchResult {
3243 response: self.respond(
3244 request,
3245 ResponsePayload::StdinClosed(StdinClosedResponse {
3246 process_id: payload.process_id,
3247 }),
3248 ),
3249 events: Vec::new(),
3250 })
3251 }
3252
3253 pub(crate) async fn kill_process(
3254 &mut self,
3255 request: &RequestFrame,
3256 payload: KillProcessRequest,
3257 ) -> Result<DispatchResult, SidecarError> {
3258 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3259 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3260 self.kill_process_internal(&vm_id, &payload.process_id, &payload.signal)?;
3261
3262 Ok(DispatchResult {
3263 response: self.respond(
3264 request,
3265 ResponsePayload::ProcessKilled(ProcessKilledResponse {
3266 process_id: payload.process_id,
3267 }),
3268 ),
3269 events: Vec::new(),
3270 })
3271 }
3272
3273 pub(crate) async fn find_listener(
3274 &mut self,
3275 request: &RequestFrame,
3276 payload: FindListenerRequest,
3277 ) -> Result<DispatchResult, SidecarError> {
3278 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3279 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3280 require_vm_inspection_permission(
3281 &self.bridge,
3282 &vm_id,
3283 "network.inspect",
3284 "network",
3285 &socket_query_resource(SocketQueryKind::TcpListener, &payload),
3286 )?;
3287
3288 let listener =
3289 find_socket_state_entry(self.vms.get(&vm_id), SocketQueryKind::TcpListener, &payload)?;
3290
3291 Ok(DispatchResult {
3292 response: self.respond(
3293 request,
3294 ResponsePayload::ListenerSnapshot(ListenerSnapshotResponse { listener }),
3295 ),
3296 events: Vec::new(),
3297 })
3298 }
3299
3300 pub(crate) async fn get_process_snapshot(
3301 &mut self,
3302 request: &RequestFrame,
3303 _payload: GetProcessSnapshotRequest,
3304 ) -> Result<DispatchResult, SidecarError> {
3305 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3306 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3307 require_vm_inspection_permission(
3308 &self.bridge,
3309 &vm_id,
3310 "process.inspect",
3311 "process",
3312 "process://snapshot",
3313 )?;
3314
3315 let processes = self
3316 .vms
3317 .get_mut(&vm_id)
3318 .map(|vm| {
3319 prune_exited_process_snapshots(vm);
3320 snapshot_vm_processes(vm)
3321 })
3322 .unwrap_or_default();
3323
3324 Ok(DispatchResult {
3325 response: self.respond(
3326 request,
3327 ResponsePayload::ProcessSnapshot(ProcessSnapshotResponse { processes }),
3328 ),
3329 events: Vec::new(),
3330 })
3331 }
3332
3333 pub(crate) async fn find_bound_udp(
3334 &mut self,
3335 request: &RequestFrame,
3336 payload: FindBoundUdpRequest,
3337 ) -> Result<DispatchResult, SidecarError> {
3338 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3339 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3340
3341 let lookup_request = FindListenerRequest {
3342 host: payload.host,
3343 port: payload.port,
3344 path: None,
3345 };
3346 require_vm_inspection_permission(
3347 &self.bridge,
3348 &vm_id,
3349 "network.inspect",
3350 "network",
3351 &socket_query_resource(SocketQueryKind::UdpBound, &lookup_request),
3352 )?;
3353 let socket = find_socket_state_entry(
3354 self.vms.get(&vm_id),
3355 SocketQueryKind::UdpBound,
3356 &lookup_request,
3357 )?;
3358
3359 Ok(DispatchResult {
3360 response: self.respond(
3361 request,
3362 ResponsePayload::BoundUdpSnapshot(BoundUdpSnapshotResponse { socket }),
3363 ),
3364 events: Vec::new(),
3365 })
3366 }
3367
3368 pub(crate) async fn vm_fetch(
3369 &mut self,
3370 request: &RequestFrame,
3371 payload: VmFetchRequest,
3372 ) -> Result<DispatchResult, SidecarError> {
3373 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3374 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3375
3376 let vm = self
3377 .vms
3378 .get_mut(&vm_id)
3379 .ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
3380 let target_path = if payload.path.starts_with('/') {
3381 payload.path.clone()
3382 } else {
3383 format!("/{}", payload.path)
3384 };
3385 let request_url = Url::parse(&format!("http://127.0.0.1:{}{target_path}", payload.port))
3386 .map_err(|error| {
3387 SidecarError::InvalidState(format!(
3388 "invalid vm.fetch target {target_path:?}: {error}"
3389 ))
3390 })?;
3391 let header_values: BTreeMap<String, Value> = serde_json::from_str(&payload.headers_json)
3392 .map_err(|error| {
3393 SidecarError::InvalidState(format!(
3394 "vm.fetch headers_json must be valid JSON: {error}"
3395 ))
3396 })?;
3397 let options = JavascriptHttpRequestOptions {
3398 method: Some(payload.method),
3399 headers: header_values,
3400 body: payload.body,
3401 reject_unauthorized: None,
3402 };
3403 let headers = parse_http_header_collection(&options.headers, "vm.fetch headers")?;
3404 let Some((target_process_id, server_id)) =
3405 vm.active_processes
3406 .iter()
3407 .find_map(|(process_id, process)| {
3408 process
3409 .http_servers
3410 .iter()
3411 .find(|(_, server)| server.guest_local_addr.port() == payload.port)
3412 .map(|(server_id, _)| (process_id.clone(), *server_id))
3413 })
3414 else {
3415 return Err(SidecarError::Execution(format!(
3416 "vm.fetch could not find a guest HTTP listener on port {}",
3417 payload.port
3418 )));
3419 };
3420 let socket_paths = build_javascript_socket_path_context(vm)?;
3421 let resource_limits = vm.kernel.resource_limits().clone();
3422 let process = vm
3423 .active_processes
3424 .get_mut(&target_process_id)
3425 .ok_or_else(|| {
3426 SidecarError::InvalidState(format!(
3427 "vm.fetch target process disappeared: {target_process_id}"
3428 ))
3429 })?;
3430 let request_json = serialize_http_loopback_request(&request_url, &options, &headers)?;
3431 let request_id = {
3432 let server = process.http_servers.get_mut(&server_id).ok_or_else(|| {
3433 SidecarError::InvalidState(format!(
3434 "vm.fetch target server disappeared: {server_id}"
3435 ))
3436 })?;
3437 server.next_request_id += 1;
3438 server.next_request_id
3439 };
3440 process
3441 .pending_http_requests
3442 .insert((server_id, request_id), None);
3443 process.execution.send_javascript_stream_event(
3444 "http_request",
3445 json!({
3446 "serverId": server_id,
3447 "requestId": request_id,
3448 "request": request_json,
3449 }),
3450 )?;
3451 let response_json = wait_for_loopback_http_response(LoopbackHttpResponseWaitRequest {
3452 bridge: &self.bridge,
3453 vm_id: &vm_id,
3454 dns: &vm.dns,
3455 socket_paths: &socket_paths,
3456 kernel: &mut vm.kernel,
3457 process,
3458 resource_limits: &resource_limits,
3459 request_key: (server_id, request_id),
3460 })?;
3461
3462 let response = self.respond(
3463 request,
3464 ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
3465 );
3466 ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
3467
3468 Ok(DispatchResult {
3469 response,
3470 events: Vec::new(),
3471 })
3472 }
3473
3474 pub(crate) async fn get_signal_state(
3475 &mut self,
3476 request: &RequestFrame,
3477 payload: GetSignalStateRequest,
3478 ) -> Result<DispatchResult, SidecarError> {
3479 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3480 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3481
3482 let handlers = self
3483 .vms
3484 .get(&vm_id)
3485 .and_then(|vm| vm.signal_states.get(&payload.process_id))
3486 .cloned()
3487 .unwrap_or_default();
3488
3489 Ok(DispatchResult {
3490 response: self.respond(
3491 request,
3492 ResponsePayload::SignalState(SignalStateResponse {
3493 process_id: payload.process_id,
3494 handlers: handlers.into_iter().collect(),
3495 }),
3496 ),
3497 events: Vec::new(),
3498 })
3499 }
3500
3501 pub(crate) async fn get_zombie_timer_count(
3502 &mut self,
3503 request: &RequestFrame,
3504 _payload: GetZombieTimerCountRequest,
3505 ) -> Result<DispatchResult, SidecarError> {
3506 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3507 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3508
3509 let count = self
3510 .vms
3511 .get(&vm_id)
3512 .map(|vm| vm.kernel.zombie_timer_count() as u64)
3513 .unwrap_or_default();
3514
3515 Ok(DispatchResult {
3516 response: self.respond(
3517 request,
3518 ResponsePayload::ZombieTimerCount(ZombieTimerCountResponse { count }),
3519 ),
3520 events: Vec::new(),
3521 })
3522 }
3523
3524 pub(crate) fn kill_process_internal(
3525 &mut self,
3526 vm_id: &str,
3527 process_id: &str,
3528 signal: &str,
3529 ) -> Result<(), SidecarError> {
3530 let signal_name = signal.to_owned();
3531 let signal = parse_signal(signal)?;
3532 let vm = self
3533 .vms
3534 .get_mut(vm_id)
3535 .ok_or_else(|| SidecarError::InvalidState(format!("unknown sidecar VM {vm_id}")))?;
3536 let process = vm.active_processes.get_mut(process_id).ok_or_else(|| {
3537 SidecarError::InvalidState(format!("VM {vm_id} has no active process {process_id}"))
3538 })?;
3539 let kernel_pid = process.kernel_pid;
3540
3541 enum KillBehavior {
3542 Tool,
3543 SharedV8StateOnly,
3544 SharedV8Continue,
3545 SharedV8Terminate,
3546 SharedV8DispatchOrTerminate,
3547 Noop,
3548 HostPid(u32),
3549 }
3550
3551 let behavior = match &process.execution {
3552 ActiveExecution::Tool(_) => KillBehavior::Tool,
3553 ActiveExecution::Javascript(execution)
3554 if execution.uses_shared_v8_runtime() && matches!(signal, 0 | libc::SIGSTOP) =>
3555 {
3556 KillBehavior::SharedV8StateOnly
3557 }
3558 ActiveExecution::Javascript(execution)
3559 if execution.uses_shared_v8_runtime() && signal == libc::SIGCONT =>
3560 {
3561 KillBehavior::SharedV8Continue
3562 }
3563 ActiveExecution::Wasm(execution)
3564 if execution.uses_shared_v8_runtime()
3565 && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
3566 {
3567 KillBehavior::SharedV8StateOnly
3568 }
3569 ActiveExecution::Python(execution)
3570 if execution.uses_shared_v8_runtime()
3571 && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
3572 {
3573 KillBehavior::SharedV8StateOnly
3574 }
3575 ActiveExecution::Javascript(execution)
3576 if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
3577 {
3578 KillBehavior::SharedV8Terminate
3579 }
3580 ActiveExecution::Wasm(execution)
3581 if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
3582 {
3583 KillBehavior::SharedV8Terminate
3584 }
3585 ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime() => {
3586 KillBehavior::SharedV8DispatchOrTerminate
3587 }
3588 ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime() => {
3589 KillBehavior::SharedV8Terminate
3590 }
3591 ActiveExecution::Python(execution) if execution.uses_shared_v8_runtime() => {
3592 KillBehavior::SharedV8Terminate
3593 }
3594 ActiveExecution::Javascript(execution) if execution.child_pid() == 0 => {
3595 KillBehavior::Noop
3596 }
3597 _ => KillBehavior::HostPid(process.execution.child_pid()),
3598 };
3599
3600 match behavior {
3601 KillBehavior::Tool => {
3602 let ActiveExecution::Tool(execution) = &process.execution else {
3603 unreachable!("kill behavior must match tool execution");
3604 };
3605 if signal != 0 {
3606 execution.cancelled.store(true, Ordering::Relaxed);
3607 process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
3608 128 + signal,
3609 ))?;
3610 }
3611 }
3612 KillBehavior::SharedV8StateOnly => {
3613 if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
3614 vm.kernel
3615 .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
3616 .map_err(kernel_error)?;
3617 }
3618 }
3619 KillBehavior::SharedV8Continue => {
3620 vm.kernel
3621 .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
3622 .map_err(kernel_error)?;
3623 if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
3624 process.execution.terminate()?;
3625 }
3626 }
3627 KillBehavior::SharedV8Terminate => {
3628 if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
3629 close_kernel_process_stdin(&mut vm.kernel, process)?;
3630 }
3631 process.execution.terminate()?;
3632 let needs_synthetic_exit = matches!(process.execution, ActiveExecution::Wasm(_))
3633 || (signal == SIGKILL
3634 && matches!(process.execution, ActiveExecution::Javascript(_)));
3635 if signal != 0 && needs_synthetic_exit {
3636 process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
3637 128 + signal,
3638 ))?;
3639 }
3640 }
3641 KillBehavior::SharedV8DispatchOrTerminate => {
3642 if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
3643 process.execution.terminate()?;
3644 }
3645 }
3646 KillBehavior::Noop => {}
3647 KillBehavior::HostPid(pid) => {
3648 if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
3649 close_kernel_process_stdin(&mut vm.kernel, process)?;
3650 }
3651 signal_runtime_process(pid, signal)?;
3652 }
3653 }
3654 emit_security_audit_event(
3655 &self.bridge,
3656 vm_id,
3657 "security.process.kill",
3658 audit_fields([
3659 (String::from("source"), String::from("control_plane")),
3660 (String::from("source_pid"), String::from("0")),
3661 (String::from("target_pid"), process.kernel_pid.to_string()),
3662 (String::from("process_id"), process_id.to_owned()),
3663 (String::from("signal"), signal_name),
3664 (
3665 String::from("host_pid"),
3666 process.execution.child_pid().to_string(),
3667 ),
3668 ]),
3669 );
3670 Ok(())
3671 }
3672
3673 pub async fn pump_process_events(
3674 &mut self,
3675 ownership: &OwnershipScope,
3676 ) -> Result<bool, SidecarError> {
3677 let mut emitted_any = false;
3678
3679 let mut queued_envelopes = Vec::new();
3680 {
3681 let pending_capacity = self.pending_process_event_capacity();
3682 let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
3683 SidecarError::InvalidState(String::from("process event receiver unavailable"))
3684 })?;
3685 loop {
3686 if queued_envelopes.len() >= pending_capacity {
3687 if receiver.is_empty() {
3688 break;
3689 }
3690 return Err(process_event_queue_overflow_error());
3691 }
3692 match receiver.try_recv() {
3693 Ok(envelope) => {
3694 queued_envelopes.push(envelope);
3695 emitted_any = true;
3696 }
3697 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
3698 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
3699 }
3700 }
3701 }
3702 for envelope in queued_envelopes {
3703 self.queue_pending_process_event(envelope)?;
3704 }
3705
3706 let vm_ids = self.vm_ids_for_scope(ownership)?;
3707 for vm_id in vm_ids {
3708 while let Some(vm) = self.vms.get(&vm_id) {
3709 let connection_id = vm.connection_id.clone();
3710 let session_id = vm.session_id.clone();
3711 let process_ids = self
3712 .vms
3713 .get(&vm_id)
3714 .map(|vm| vm.active_processes.keys().cloned().collect::<Vec<_>>())
3715 .unwrap_or_default();
3716 let mut emitted_this_pass = false;
3717
3718 for process_id in process_ids {
3719 if self
3720 .vms
3721 .get(&vm_id)
3722 .is_some_and(|vm| vm.detached_child_processes.contains(&process_id))
3723 {
3724 continue;
3725 }
3726 enum ProcessPollResult {
3727 Event(Box<Option<ActiveExecutionEvent>>),
3728 RecoverClosedChannel,
3729 }
3730 let poll_result = {
3731 let Some(vm) = self.vms.get_mut(&vm_id) else {
3732 continue;
3733 };
3734 let Some(process) = vm.active_processes.get_mut(&process_id) else {
3735 continue;
3736 };
3737 if let Some(event) = process.pending_execution_events.pop_front() {
3738 ProcessPollResult::Event(Box::new(Some(event)))
3739 } else {
3740 match process.execution.poll_event(Duration::ZERO).await {
3741 Ok(event) => ProcessPollResult::Event(Box::new(event)),
3742 Err(SidecarError::Execution(message))
3743 if (process.runtime == GuestRuntimeKind::JavaScript
3744 && closed_javascript_event_channel(&message))
3745 || (process.runtime == GuestRuntimeKind::Python
3746 && closed_python_event_channel(&message))
3747 || (process.runtime == GuestRuntimeKind::WebAssembly
3748 && closed_wasm_event_channel(&message)) =>
3749 {
3750 ProcessPollResult::RecoverClosedChannel
3751 }
3752 Err(other) => return Err(other),
3753 }
3754 }
3755 };
3756 let event = match poll_result {
3757 ProcessPollResult::Event(event) => *event,
3758 ProcessPollResult::RecoverClosedChannel => {
3759 self.recover_closed_root_runtime_process_event(&vm_id, &process_id)?
3760 }
3761 };
3762
3763 let Some(event) = event else {
3764 continue;
3765 };
3766
3767 self.queue_pending_process_event(ProcessEventEnvelope {
3768 connection_id: connection_id.clone(),
3769 session_id: session_id.clone(),
3770 vm_id: vm_id.clone(),
3771 process_id: process_id.clone(),
3772 event,
3773 })?;
3774 emitted_any = true;
3775 emitted_this_pass = true;
3776 }
3777
3778 if !emitted_this_pass {
3779 break;
3780 }
3781 }
3782
3783 if self.pump_detached_child_process_events(&vm_id)? {
3784 emitted_any = true;
3785 }
3786 }
3787
3788 Ok(emitted_any)
3789 }
3790
3791 fn recover_closed_root_runtime_process_event(
3792 &mut self,
3793 vm_id: &str,
3794 process_id: &str,
3795 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
3796 let Some(vm) = self.vms.get_mut(vm_id) else {
3797 return Ok(None);
3798 };
3799 let Some(process) = vm.active_processes.get(process_id) else {
3800 return Ok(None);
3801 };
3802 if process.execution.uses_shared_v8_runtime() {
3803 return Ok(None);
3804 }
3805 if process.runtime != GuestRuntimeKind::JavaScript
3806 && process.runtime != GuestRuntimeKind::Python
3807 && process.runtime != GuestRuntimeKind::WebAssembly
3808 {
3809 return Ok(None);
3810 }
3811 let runtime_child_pid = process.execution.child_pid();
3812 if runtime_child_pid == 0 {
3813 return Ok(None);
3814 }
3815 if let Some(status) = runtime_child_exit_status(runtime_child_pid)? {
3816 return Ok(Some(ActiveExecutionEvent::Exited(status)));
3817 }
3818 if runtime_child_is_alive(runtime_child_pid)? {
3819 return Ok(None);
3820 }
3821 Ok(Some(ActiveExecutionEvent::Exited(0)))
3822 }
3823
3824 fn active_process_by_path<'a>(
3825 process: &'a ActiveProcess,
3826 child_path: &[&str],
3827 ) -> Option<&'a ActiveProcess> {
3828 let mut current = process;
3829 for child_id in child_path {
3830 current = current.child_processes.get(*child_id)?;
3831 }
3832 Some(current)
3833 }
3834
3835 fn active_process_by_path_mut<'a>(
3836 process: &'a mut ActiveProcess,
3837 child_path: &[&str],
3838 ) -> Option<&'a mut ActiveProcess> {
3839 let mut current = process;
3840 for child_id in child_path {
3841 current = current.child_processes.get_mut(*child_id)?;
3842 }
3843 Some(current)
3844 }
3845
3846 fn active_process_by_owned_path_mut<'a>(
3847 process: &'a mut ActiveProcess,
3848 child_path: &[String],
3849 ) -> Option<&'a mut ActiveProcess> {
3850 let mut current = process;
3851 for child_id in child_path {
3852 current = current.child_processes.get_mut(child_id)?;
3853 }
3854 Some(current)
3855 }
3856
3857 fn active_process_path_by_kernel_pid(
3858 process: &ActiveProcess,
3859 kernel_pid: u32,
3860 ) -> Option<Vec<String>> {
3861 if process.kernel_pid == kernel_pid {
3862 return Some(Vec::new());
3863 }
3864
3865 for (child_id, child) in &process.child_processes {
3866 let Some(mut path) = Self::active_process_path_by_kernel_pid(child, kernel_pid) else {
3867 continue;
3868 };
3869 path.insert(0, child_id.clone());
3870 return Some(path);
3871 }
3872
3873 None
3874 }
3875
3876 fn descendant_parent_process<'a>(
3877 vm: &'a VmState,
3878 process_id: &str,
3879 child_path: &[&str],
3880 ) -> Option<&'a ActiveProcess> {
3881 let root = vm.active_processes.get(process_id)?;
3882 Self::active_process_by_path(root, child_path)
3883 }
3884
3885 fn descendant_parent_process_mut<'a>(
3886 vm: &'a mut VmState,
3887 process_id: &str,
3888 child_path: &[&str],
3889 ) -> Option<&'a mut ActiveProcess> {
3890 let root = vm.active_processes.get_mut(process_id)?;
3891 Self::active_process_by_path_mut(root, child_path)
3892 }
3893
3894 fn child_process_path_label(process_id: &str, child_path: &[&str]) -> String {
3895 if child_path.is_empty() {
3896 process_id.to_owned()
3897 } else {
3898 format!("{process_id}/{}", child_path.join("/"))
3899 }
3900 }
3901
3902 fn adopt_detached_child_processes(
3903 current_process_id: &str,
3904 process: &mut ActiveProcess,
3905 ) -> Vec<(String, ActiveProcess)> {
3906 let mut adopted = Vec::new();
3907 let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
3908 for child_id in child_ids {
3909 let child_process_id = format!("{current_process_id}/{child_id}");
3910 let Some(mut child) = process.child_processes.remove(&child_id) else {
3911 continue;
3912 };
3913 if child.detached {
3914 adopted.push((child_process_id, child));
3915 continue;
3916 }
3917
3918 adopted.extend(Self::adopt_detached_child_processes(
3919 &child_process_id,
3920 &mut child,
3921 ));
3922 process.child_processes.insert(child_id, child);
3923 }
3924 adopted
3925 }
3926
3927 fn child_process_signal_key<'a>(process_id: &'a str, child_path: &[&'a str]) -> &'a str {
3928 child_path.last().copied().unwrap_or(process_id)
3929 }
3930
3931 fn resolve_detached_child_process_path(
3932 vm: &VmState,
3933 detached_process_id: &str,
3934 ) -> Option<(String, Vec<String>)> {
3935 let root_process_id = vm
3936 .active_processes
3937 .keys()
3938 .filter(|candidate| {
3939 detached_process_id == candidate.as_str()
3940 || detached_process_id
3941 .strip_prefix(candidate.as_str())
3942 .is_some_and(|remainder| remainder.starts_with('/'))
3943 })
3944 .max_by_key(|candidate| candidate.len())?
3945 .clone();
3946
3947 let remainder = detached_process_id
3948 .strip_prefix(root_process_id.as_str())
3949 .unwrap_or_default();
3950 if remainder.is_empty() {
3951 return Some((root_process_id, Vec::new()));
3952 }
3953
3954 Some((
3955 root_process_id,
3956 remainder
3957 .trim_start_matches('/')
3958 .split('/')
3959 .map(str::to_owned)
3960 .collect(),
3961 ))
3962 }
3963
3964 fn pump_detached_child_process_events(&mut self, vm_id: &str) -> Result<bool, SidecarError> {
3965 let detached_process_ids = self
3966 .vms
3967 .get(vm_id)
3968 .map(|vm| {
3969 vm.detached_child_processes
3970 .iter()
3971 .cloned()
3972 .collect::<Vec<_>>()
3973 })
3974 .unwrap_or_default();
3975 let mut emitted_any = false;
3976 for detached_process_id in detached_process_ids {
3977 let Some((root_process_id, child_path)) = self
3978 .vms
3979 .get(vm_id)
3980 .and_then(|vm| Self::resolve_detached_child_process_path(vm, &detached_process_id))
3981 else {
3982 if let Some(vm) = self.vms.get_mut(vm_id) {
3983 vm.detached_child_processes.remove(&detached_process_id);
3984 }
3985 continue;
3986 };
3987 if child_path.is_empty() {
3988 loop {
3989 enum ProcessPollResult {
3990 Event(Box<Option<ActiveExecutionEvent>>),
3991 RecoverClosedChannel,
3992 }
3993 let poll_result = {
3994 let Some(vm) = self.vms.get_mut(vm_id) else {
3995 break;
3996 };
3997 let Some(process) = vm.active_processes.get_mut(&root_process_id) else {
3998 break;
3999 };
4000 if let Some(event) = process.pending_execution_events.pop_front() {
4001 ProcessPollResult::Event(Box::new(Some(event)))
4002 } else {
4003 match process.execution.poll_event_blocking(Duration::ZERO) {
4004 Ok(event) => ProcessPollResult::Event(Box::new(event)),
4005 Err(SidecarError::Execution(message))
4006 if (process.runtime == GuestRuntimeKind::JavaScript
4007 && closed_javascript_event_channel(&message))
4008 || (process.runtime == GuestRuntimeKind::Python
4009 && closed_python_event_channel(&message))
4010 || (process.runtime == GuestRuntimeKind::WebAssembly
4011 && closed_wasm_event_channel(&message)) =>
4012 {
4013 ProcessPollResult::RecoverClosedChannel
4014 }
4015 Err(error) => return Err(error),
4016 }
4017 }
4018 };
4019 let event = match poll_result {
4020 ProcessPollResult::Event(event) => *event,
4021 ProcessPollResult::RecoverClosedChannel => {
4022 self.recover_closed_root_runtime_process_event(vm_id, &root_process_id)?
4023 }
4024 };
4025 let Some(event) = event else {
4026 break;
4027 };
4028 let Some((connection_id, session_id)) = self
4029 .vms
4030 .get(vm_id)
4031 .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
4032 else {
4033 break;
4034 };
4035 match event {
4036 ActiveExecutionEvent::Stdout(chunk) => {
4037 self.queue_pending_process_event(ProcessEventEnvelope {
4038 connection_id,
4039 session_id,
4040 vm_id: vm_id.to_owned(),
4041 process_id: detached_process_id.clone(),
4042 event: ActiveExecutionEvent::Stdout(chunk),
4043 })?;
4044 emitted_any = true;
4045 }
4046 ActiveExecutionEvent::Stderr(chunk) => {
4047 self.queue_pending_process_event(ProcessEventEnvelope {
4048 connection_id,
4049 session_id,
4050 vm_id: vm_id.to_owned(),
4051 process_id: detached_process_id.clone(),
4052 event: ActiveExecutionEvent::Stderr(chunk),
4053 })?;
4054 emitted_any = true;
4055 }
4056 ActiveExecutionEvent::Exited(exit_code) => {
4057 if let Some(vm) = self.vms.get_mut(vm_id) {
4058 vm.detached_child_processes.remove(&detached_process_id);
4059 }
4060 self.queue_pending_process_event(ProcessEventEnvelope {
4061 connection_id,
4062 session_id,
4063 vm_id: vm_id.to_owned(),
4064 process_id: detached_process_id.clone(),
4065 event: ActiveExecutionEvent::Exited(exit_code),
4066 })?;
4067 emitted_any = true;
4068 break;
4069 }
4070 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
4071 self.handle_javascript_sync_rpc_request(
4072 vm_id,
4073 &root_process_id,
4074 request,
4075 )?;
4076 }
4077 ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
4078 self.handle_python_vfs_rpc_request(vm_id, &root_process_id, *request)?;
4079 }
4080 ActiveExecutionEvent::SignalState {
4081 signal,
4082 registration,
4083 } => {
4084 if let Some(vm) = self.vms.get_mut(vm_id) {
4085 vm.signal_states
4086 .entry(root_process_id.clone())
4087 .or_default()
4088 .insert(signal, registration);
4089 }
4090 }
4091 }
4092 }
4093 continue;
4094 }
4095
4096 let parent_path = child_path[..child_path.len() - 1]
4097 .iter()
4098 .map(String::as_str)
4099 .collect::<Vec<_>>();
4100 let child_process_id = child_path.last().expect("child path cannot be empty");
4101
4102 loop {
4103 let event = match self.poll_descendant_javascript_child_process(
4104 vm_id,
4105 &root_process_id,
4106 &parent_path,
4107 child_process_id,
4108 0,
4109 ) {
4110 Ok(event) => event,
4111 Err(SidecarError::InvalidState(message))
4112 if message.contains("unknown child process")
4113 || message.contains("unknown child process path") =>
4114 {
4115 if let Some(vm) = self.vms.get_mut(vm_id) {
4116 vm.detached_child_processes.remove(&detached_process_id);
4117 }
4118 break;
4119 }
4120 Err(error) if is_javascript_child_process_gone_error(&error) => {
4121 if let Some(vm) = self.vms.get_mut(vm_id) {
4122 vm.detached_child_processes.remove(&detached_process_id);
4123 }
4124 break;
4125 }
4126 Err(error) => return Err(error),
4127 };
4128
4129 let Some(event_type) = event.get("type").and_then(Value::as_str) else {
4130 break;
4131 };
4132 let Some((connection_id, session_id)) = self
4133 .vms
4134 .get(vm_id)
4135 .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
4136 else {
4137 break;
4138 };
4139
4140 let envelope = match event_type {
4141 "stdout" => Some(ProcessEventEnvelope {
4142 connection_id: connection_id.clone(),
4143 session_id: session_id.clone(),
4144 vm_id: vm_id.to_owned(),
4145 process_id: detached_process_id.clone(),
4146 event: ActiveExecutionEvent::Stdout(javascript_sync_rpc_bytes_arg(
4147 &[event.get("data").cloned().unwrap_or(Value::Null)],
4148 0,
4149 "detached child_process stdout",
4150 )?),
4151 }),
4152 "stderr" => Some(ProcessEventEnvelope {
4153 connection_id: connection_id.clone(),
4154 session_id: session_id.clone(),
4155 vm_id: vm_id.to_owned(),
4156 process_id: detached_process_id.clone(),
4157 event: ActiveExecutionEvent::Stderr(javascript_sync_rpc_bytes_arg(
4158 &[event.get("data").cloned().unwrap_or(Value::Null)],
4159 0,
4160 "detached child_process stderr",
4161 )?),
4162 }),
4163 "exit" => {
4164 if let Some(vm) = self.vms.get_mut(vm_id) {
4165 vm.detached_child_processes.remove(&detached_process_id);
4166 }
4167 Some(ProcessEventEnvelope {
4168 connection_id,
4169 session_id,
4170 vm_id: vm_id.to_owned(),
4171 process_id: detached_process_id.clone(),
4172 event: ActiveExecutionEvent::Exited(
4173 event
4174 .get("exitCode")
4175 .and_then(Value::as_i64)
4176 .map(|value| value as i32)
4177 .unwrap_or(1),
4178 ),
4179 })
4180 }
4181 _ => None,
4182 };
4183
4184 let Some(envelope) = envelope else {
4185 break;
4186 };
4187 self.queue_pending_process_event(envelope)?;
4188 emitted_any = true;
4189
4190 if event_type == "exit" {
4191 break;
4192 }
4193 }
4194 }
4195
4196 Ok(emitted_any)
4197 }
4198 pub(crate) fn drain_queued_descendant_javascript_child_process_events(
4199 &mut self,
4200 vm_id: &str,
4201 process_id: &str,
4202 child_path: &[&str],
4203 ) -> Result<(), SidecarError> {
4204 if child_path.is_empty() {
4205 return Ok(());
4206 }
4207 let target_process_id = Self::child_process_path_label(process_id, child_path);
4208 let mut child_capacity = self
4209 .vms
4210 .get(vm_id)
4211 .and_then(|vm| vm.active_processes.get(process_id))
4212 .and_then(|root| descendant_pending_execution_event_capacity(root, child_path));
4213
4214 let mut deferred = VecDeque::new();
4215 while let Some(envelope) = self.pending_process_events.pop_front() {
4216 if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
4217 if matches!(child_capacity, Some(0)) {
4218 self.pending_process_events.push_front(envelope);
4219 while let Some(deferred_envelope) = deferred.pop_back() {
4220 self.pending_process_events.push_front(deferred_envelope);
4221 }
4222 return Err(process_event_queue_overflow_error());
4223 }
4224 if let Some(vm) = self.vms.get_mut(vm_id) {
4225 if let Some(root) = vm.active_processes.get_mut(process_id) {
4226 if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
4227 child.queue_pending_execution_event(envelope.event)?;
4228 child_capacity = child_capacity.map(|capacity| capacity - 1);
4229 continue;
4230 }
4231 }
4232 }
4233 }
4234 deferred.push_back(envelope);
4235 }
4236 self.pending_process_events = deferred;
4237
4238 let mut queued = Vec::new();
4239 {
4240 let transfer_capacity = self
4241 .pending_process_event_capacity()
4242 .min(child_capacity.unwrap_or(usize::MAX));
4243 let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
4244 SidecarError::InvalidState(String::from("process event receiver unavailable"))
4245 })?;
4246 loop {
4247 if queued.len() >= transfer_capacity {
4248 if receiver.is_empty() {
4249 break;
4250 }
4251 return Err(process_event_queue_overflow_error());
4252 }
4253 match receiver.try_recv() {
4254 Ok(envelope) => queued.push(envelope),
4255 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
4256 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
4257 }
4258 }
4259 }
4260 for envelope in queued {
4261 if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
4262 if let Some(vm) = self.vms.get_mut(vm_id) {
4263 if let Some(root) = vm.active_processes.get_mut(process_id) {
4264 if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
4265 child.queue_pending_execution_event(envelope.event)?;
4266 continue;
4267 }
4268 }
4269 }
4270 }
4271 self.queue_pending_process_event(envelope)?;
4272 }
4273
4274 Ok(())
4275 }
4276
4277 pub(crate) fn handle_execution_event(
4278 &mut self,
4279 vm_id: &str,
4280 process_id: &str,
4281 event: ActiveExecutionEvent,
4282 ) -> Result<Option<EventFrame>, SidecarError> {
4283 let Some(vm) = self.vms.get(vm_id) else {
4284 log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
4285 return Ok(None);
4286 };
4287 if !vm.active_processes.contains_key(process_id) {
4288 log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
4289 return Ok(None);
4290 }
4291 let (connection_id, session_id) = { (vm.connection_id.clone(), vm.session_id.clone()) };
4292 let ownership = OwnershipScope::vm(&connection_id, &session_id, vm_id);
4293
4294 if self.capture_extension_process_output_event(vm_id, process_id, &event) {
4295 return Ok(None);
4296 }
4297
4298 match event {
4299 ActiveExecutionEvent::Stdout(chunk) => Ok(Some(EventFrame::new(
4300 ownership,
4301 EventPayload::ProcessOutput(ProcessOutputEvent {
4302 process_id: process_id.to_owned(),
4303 channel: StreamChannel::Stdout,
4304 chunk,
4305 }),
4306 ))),
4307 ActiveExecutionEvent::Stderr(chunk) => Ok(Some(EventFrame::new(
4308 ownership,
4309 EventPayload::ProcessOutput(ProcessOutputEvent {
4310 process_id: process_id.to_owned(),
4311 channel: StreamChannel::Stderr,
4312 chunk,
4313 }),
4314 ))),
4315 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
4316 self.handle_javascript_sync_rpc_request(vm_id, process_id, request)?;
4317 Ok(None)
4318 }
4319 ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
4320 self.handle_python_vfs_rpc_request(vm_id, process_id, *request)?;
4321 Ok(None)
4322 }
4323 ActiveExecutionEvent::SignalState {
4324 signal,
4325 registration,
4326 } => {
4327 let Some(vm) = self.vms.get_mut(vm_id) else {
4328 return Ok(None);
4329 };
4330 if !vm.active_processes.contains_key(process_id) {
4331 return Ok(None);
4332 }
4333 vm.signal_states
4334 .entry(process_id.to_owned())
4335 .or_default()
4336 .insert(signal, registration);
4337 Ok(None)
4338 }
4339 ActiveExecutionEvent::Exited(exit_code) => {
4340 let became_idle = self
4341 .finish_active_process_exit(vm_id, process_id, exit_code)?
4342 .unwrap_or(false);
4343
4344 if became_idle {
4345 self.bridge.emit_lifecycle(vm_id, LifecycleState::Ready)?;
4346 }
4347
4348 Ok(Some(EventFrame::new(
4349 ownership,
4350 EventPayload::ProcessExited(ProcessExitedEvent {
4351 process_id: process_id.to_owned(),
4352 exit_code,
4353 }),
4354 )))
4355 }
4356 }
4357 }
4358
4359 pub(crate) fn finish_active_process_exit(
4360 &mut self,
4361 vm_id: &str,
4362 process_id: &str,
4363 exit_code: i32,
4364 ) -> Result<Option<bool>, SidecarError> {
4365 let Some(vm) = self.vms.get_mut(vm_id) else {
4366 log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
4367 return Ok(None);
4368 };
4369 if !vm.active_processes.contains_key(process_id) {
4370 log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
4371 return Ok(None);
4372 }
4373
4374 prune_exited_process_snapshots(vm);
4375 let process_table = vm.kernel.list_processes();
4376 let Some(mut process) = vm.active_processes.remove(process_id) else {
4377 return Ok(None);
4378 };
4379 if let Some(info) = process_table.get(&process.kernel_pid) {
4380 vm.exited_process_snapshots
4381 .push_back(ExitedProcessSnapshot {
4382 captured_at: Instant::now(),
4383 process: build_process_snapshot_entry(
4384 process_id,
4385 &process,
4386 info,
4387 Some(exit_code),
4388 ),
4389 });
4390 }
4391 let detached_children = Self::adopt_detached_child_processes(process_id, &mut process);
4392 sync_process_host_writes_to_kernel(vm, &process)?;
4393 terminate_child_process_tree(&mut vm.kernel, &mut process);
4394 process.kernel_handle.finish(exit_code);
4395 let _ = vm.kernel.wait_and_reap(process.kernel_pid);
4396 vm.signal_states.remove(process_id);
4397 for (detached_process_id, detached_child) in detached_children {
4398 vm.detached_child_processes
4399 .insert(detached_process_id.clone());
4400 vm.active_processes
4401 .insert(detached_process_id, detached_child);
4402 }
4403 let became_idle = vm.active_processes.is_empty();
4404 self.prune_extension_process_resource(process_id);
4405
4406 Ok(Some(became_idle))
4407 }
4408
4409 pub(crate) fn drain_process_events_blocking_with_limit(
4410 &mut self,
4411 vm_id: &str,
4412 process_id: &str,
4413 max_events: usize,
4414 ) -> Result<Vec<ActiveExecutionEvent>, SidecarError> {
4415 let mut events = Vec::new();
4416 if max_events == 0 {
4417 return Ok(events);
4418 }
4419 let mut deadline = Instant::now() + Duration::from_millis(150);
4420
4421 loop {
4422 if events.len() >= max_events {
4423 break;
4424 }
4425 let event = {
4426 let Some(vm) = self.vms.get_mut(vm_id) else {
4427 break;
4428 };
4429 let Some(process) = vm.active_processes.get_mut(process_id) else {
4430 break;
4431 };
4432 if let Some(event) = process.pending_execution_events.pop_front() {
4433 Some(event)
4434 } else {
4435 match process.execution.poll_event_blocking(Duration::ZERO) {
4436 Ok(event) => event,
4437 Err(SidecarError::Execution(_)) => None,
4438 Err(other) => return Err(other),
4439 }
4440 }
4441 };
4442
4443 let Some(event) = event else {
4444 if Instant::now() >= deadline {
4445 break;
4446 }
4447 let blocking_wait = deadline.saturating_duration_since(Instant::now());
4448 if blocking_wait.is_zero() {
4449 break;
4450 }
4451 if events.len() >= max_events {
4452 break;
4453 }
4454 let delayed_event = {
4455 let Some(vm) = self.vms.get_mut(vm_id) else {
4456 break;
4457 };
4458 let Some(process) = vm.active_processes.get_mut(process_id) else {
4459 break;
4460 };
4461 if let Some(event) = process.pending_execution_events.pop_front() {
4462 Some(event)
4463 } else {
4464 match process.execution.poll_event_blocking(blocking_wait) {
4465 Ok(event) => event,
4466 Err(SidecarError::Execution(_)) => None,
4467 Err(other) => return Err(other),
4468 }
4469 }
4470 };
4471 let Some(event) = delayed_event else {
4472 break;
4473 };
4474 events.push(event);
4475 deadline = Instant::now() + Duration::from_millis(150);
4476 continue;
4477 };
4478 events.push(event);
4479 deadline = Instant::now() + Duration::from_millis(150);
4480 }
4481
4482 Ok(events)
4483 }
4484
4485 pub(crate) fn handle_python_vfs_rpc_request(
4486 &mut self,
4487 vm_id: &str,
4488 process_id: &str,
4489 request: PythonVfsRpcRequest,
4490 ) -> Result<(), SidecarError> {
4491 match request.method {
4492 PythonVfsRpcMethod::Read
4493 | PythonVfsRpcMethod::Write
4494 | PythonVfsRpcMethod::Stat
4495 | PythonVfsRpcMethod::ReadDir
4496 | PythonVfsRpcMethod::Mkdir => {
4497 filesystem_handle_python_vfs_rpc_request(self, vm_id, process_id, request)
4498 }
4499 PythonVfsRpcMethod::HttpRequest => {
4500 self.handle_python_http_rpc_request(vm_id, process_id, request)
4501 }
4502 PythonVfsRpcMethod::DnsLookup => {
4503 self.handle_python_dns_rpc_request(vm_id, process_id, request)
4504 }
4505 PythonVfsRpcMethod::SubprocessRun => {
4506 self.handle_python_subprocess_rpc_request(vm_id, process_id, request)
4507 }
4508 }
4509 }
4510
4511 fn handle_python_http_rpc_request(
4512 &mut self,
4513 vm_id: &str,
4514 process_id: &str,
4515 request: PythonVfsRpcRequest,
4516 ) -> Result<(), SidecarError> {
4517 let Some(vm) = self.vms.get(vm_id) else {
4518 return Ok(());
4519 };
4520 if !vm.active_processes.contains_key(process_id) {
4521 return Ok(());
4522 }
4523 let response = (|| {
4524 let url_text = request.url.as_deref().ok_or_else(|| {
4525 SidecarError::InvalidState(String::from("python httpRequest requires a url"))
4526 })?;
4527 let url = Url::parse(url_text)
4528 .map_err(|error| SidecarError::Execution(format!("ERR_INVALID_URL: {error}")))?;
4529 let host = url.host_str().ok_or_else(|| {
4530 SidecarError::Execution(String::from("ERR_INVALID_URL: missing host"))
4531 })?;
4532 let port = url.port_or_known_default().ok_or_else(|| {
4533 SidecarError::Execution(String::from("ERR_INVALID_URL: missing port"))
4534 })?;
4535 self.bridge.require_network_access(
4536 vm_id,
4537 NetworkOperation::Http,
4538 format_tcp_resource(host, port),
4539 )?;
4540 let pinned_addresses = if let Ok(literal_ip) = host.parse::<IpAddr>() {
4547 filter_dns_safe_ip_addrs(vec![literal_ip], host)?
4548 } else {
4549 filter_dns_safe_ip_addrs(
4550 resolve_dns_ip_addrs(
4551 &self.bridge,
4552 &vm.kernel,
4553 vm_id,
4554 &vm.dns,
4555 host,
4556 DnsLookupPolicy::SkipPermissions,
4557 )?,
4558 host,
4559 )?
4560 };
4561 let mut headers = BTreeMap::new();
4562 for (name, value) in &request.headers {
4563 headers.insert(name.clone(), Value::String(value.clone()));
4564 }
4565 let options = JavascriptHttpRequestOptions {
4566 method: Some(
4567 request
4568 .http_method
4569 .clone()
4570 .unwrap_or_else(|| String::from("GET")),
4571 ),
4572 headers,
4573 body: request.body_base64.as_deref().map(|body| {
4574 String::from_utf8(
4575 base64::engine::general_purpose::STANDARD
4576 .decode(body)
4577 .unwrap_or_default(),
4578 )
4579 .unwrap_or_default()
4580 }),
4581 reject_unauthorized: None,
4582 };
4583 let headers =
4584 parse_http_header_collection(&options.headers, "python httpRequest headers")?;
4585 let response =
4586 issue_outbound_http_request(&url, &options, &headers, &pinned_addresses)?;
4587 let payload_json = response.as_str().ok_or_else(|| {
4588 SidecarError::Execution(String::from(
4589 "python httpRequest returned a non-string response payload",
4590 ))
4591 })?;
4592 let payload: Value = serde_json::from_str(payload_json).map_err(|error| {
4593 SidecarError::Execution(format!(
4594 "python httpRequest response must be valid JSON: {error}"
4595 ))
4596 })?;
4597 let header_map = payload
4598 .get("headers")
4599 .and_then(Value::as_array)
4600 .map(|entries| {
4601 let mut normalized = BTreeMap::<String, Vec<String>>::new();
4602 for entry in entries {
4603 let Some(pair) = entry.as_array() else {
4604 continue;
4605 };
4606 let Some(name) = pair.first().and_then(Value::as_str) else {
4607 continue;
4608 };
4609 let Some(value) = pair.get(1).and_then(Value::as_str) else {
4610 continue;
4611 };
4612 normalized
4613 .entry(name.to_owned())
4614 .or_default()
4615 .push(value.to_owned());
4616 }
4617 normalized
4618 })
4619 .unwrap_or_default();
4620 Ok(PythonVfsRpcResponsePayload::Http {
4621 status: payload
4622 .get("status")
4623 .and_then(Value::as_u64)
4624 .map(|value| value as u16)
4625 .unwrap_or_default(),
4626 reason: payload
4627 .get("statusText")
4628 .and_then(Value::as_str)
4629 .unwrap_or_default()
4630 .to_owned(),
4631 url: payload
4632 .get("url")
4633 .and_then(Value::as_str)
4634 .unwrap_or(url_text)
4635 .to_owned(),
4636 headers: header_map,
4637 body_base64: payload
4638 .get("body")
4639 .and_then(Value::as_str)
4640 .unwrap_or_default()
4641 .to_owned(),
4642 })
4643 })();
4644
4645 self.respond_python_rpc(vm_id, process_id, request.id, response)
4646 }
4647
4648 fn handle_python_dns_rpc_request(
4649 &mut self,
4650 vm_id: &str,
4651 process_id: &str,
4652 request: PythonVfsRpcRequest,
4653 ) -> Result<(), SidecarError> {
4654 let Some(vm) = self.vms.get(vm_id) else {
4655 return Ok(());
4656 };
4657 if !vm.active_processes.contains_key(process_id) {
4658 return Ok(());
4659 }
4660 let response = (|| {
4661 let hostname = request.hostname.as_deref().ok_or_else(|| {
4662 SidecarError::InvalidState(String::from("python dnsLookup requires a hostname"))
4663 })?;
4664 let mut addresses = filter_dns_safe_ip_addrs(
4665 resolve_dns_ip_addrs(
4666 &self.bridge,
4667 &vm.kernel,
4668 vm_id,
4669 &vm.dns,
4670 hostname,
4671 DnsLookupPolicy::CheckPermissions,
4672 )?,
4673 hostname,
4674 )?;
4675 if let Some(family) = request.family {
4676 addresses.retain(|address| {
4677 matches!((family, address), (4, IpAddr::V4(_)) | (6, IpAddr::V6(_)))
4678 });
4679 }
4680 Ok(PythonVfsRpcResponsePayload::DnsLookup {
4681 addresses: addresses
4682 .into_iter()
4683 .map(|address| address.to_string())
4684 .collect(),
4685 })
4686 })();
4687
4688 self.respond_python_rpc(vm_id, process_id, request.id, response)
4689 }
4690
4691 fn handle_python_subprocess_rpc_request(
4692 &mut self,
4693 vm_id: &str,
4694 process_id: &str,
4695 request: PythonVfsRpcRequest,
4696 ) -> Result<(), SidecarError> {
4697 let command = request.command.clone().ok_or_else(|| {
4698 SidecarError::InvalidState(String::from("python subprocessRun requires a command"))
4699 })?;
4700 let (internal_bootstrap_env, cwd) = {
4701 let Some(vm) = self.vms.get(vm_id) else {
4702 return Ok(());
4703 };
4704 let Some(process) = vm.active_processes.get(process_id) else {
4705 return Ok(());
4706 };
4707 let cwd = request.cwd.clone().or_else(|| {
4708 guest_runtime_path_for_host_path(
4709 &vm.guest_env,
4710 &vm.host_cwd,
4711 &process.host_cwd.to_string_lossy(),
4712 )
4713 });
4714 (
4715 sanitize_javascript_child_process_internal_bootstrap_env(&vm.guest_env),
4716 cwd,
4717 )
4718 };
4719 let response = self
4720 .spawn_javascript_child_process_sync(
4721 vm_id,
4722 process_id,
4723 JavascriptChildProcessSpawnRequest {
4724 command,
4725 args: request.args.clone(),
4726 options: JavascriptChildProcessSpawnOptions {
4727 cwd,
4728 env: request.env.clone(),
4729 input: None,
4730 internal_bootstrap_env,
4731 shell: request.shell,
4732 detached: false,
4733 stdio: vec![
4734 String::from("pipe"),
4735 String::from("pipe"),
4736 String::from("pipe"),
4737 ],
4738 timeout: None,
4739 kill_signal: None,
4740 },
4741 },
4742 request.max_buffer,
4743 )
4744 .map(|payload| PythonVfsRpcResponsePayload::SubprocessRun {
4745 exit_code: payload
4746 .get("code")
4747 .and_then(Value::as_i64)
4748 .map(|value| value as i32)
4749 .unwrap_or(1),
4750 stdout: payload
4751 .get("stdout")
4752 .and_then(Value::as_str)
4753 .unwrap_or_default()
4754 .to_owned(),
4755 stderr: payload
4756 .get("stderr")
4757 .and_then(Value::as_str)
4758 .unwrap_or_default()
4759 .to_owned(),
4760 max_buffer_exceeded: payload
4761 .get("maxBufferExceeded")
4762 .and_then(Value::as_bool)
4763 .unwrap_or(false),
4764 });
4765
4766 self.respond_python_rpc(vm_id, process_id, request.id, response)
4767 }
4768
4769 fn respond_python_rpc(
4770 &mut self,
4771 vm_id: &str,
4772 process_id: &str,
4773 request_id: u64,
4774 response: Result<PythonVfsRpcResponsePayload, SidecarError>,
4775 ) -> Result<(), SidecarError> {
4776 let Some(vm) = self.vms.get_mut(vm_id) else {
4777 return Ok(());
4778 };
4779 let Some(process) = vm.active_processes.get_mut(process_id) else {
4780 return Ok(());
4781 };
4782 let result = match response {
4783 Ok(payload) => process
4784 .execution
4785 .respond_python_vfs_rpc_success(request_id, payload),
4786 Err(error) => process.execution.respond_python_vfs_rpc_error(
4787 request_id,
4788 "ERR_AGENT_OS_PYTHON_VFS_RPC",
4789 error.to_string(),
4790 ),
4791 };
4792 match result {
4793 Ok(()) => Ok(()),
4794 Err(error) if is_broken_pipe_error(&error) => Ok(()),
4795 Err(error) => Err(error),
4796 }
4797 }
4798
4799 pub(crate) fn resolve_javascript_child_process_execution(
4800 &self,
4801 vm: &VmState,
4802 parent_env: &BTreeMap<String, String>,
4803 parent_guest_cwd: &str,
4804 parent_host_cwd: &Path,
4805 request: &JavascriptChildProcessSpawnRequest,
4806 ) -> Result<ResolvedChildProcessExecution, SidecarError> {
4807 let mut runtime_env = parent_env.clone();
4808 runtime_env.extend(request.options.internal_bootstrap_env.clone());
4809 let (guest_cwd, host_cwd_override) = request
4810 .options
4811 .cwd
4812 .as_deref()
4813 .map(|cwd| {
4814 let normalized_parent_host_cwd = normalize_host_path(parent_host_cwd);
4815 let requested_host_cwd = normalize_host_path(Path::new(cwd));
4816 if path_is_within_root(&requested_host_cwd, &normalized_parent_host_cwd) {
4817 let relative = requested_host_cwd
4818 .strip_prefix(&normalized_parent_host_cwd)
4819 .unwrap_or_else(|_| Path::new(""));
4820 let relative = relative.to_string_lossy().replace('\\', "/");
4821 let guest_cwd = if relative.is_empty() {
4822 parent_guest_cwd.to_owned()
4823 } else {
4824 normalize_path(&format!("{parent_guest_cwd}/{relative}"))
4825 };
4826 (guest_cwd, Some(requested_host_cwd))
4827 } else if Path::new(cwd).is_relative() {
4828 (
4829 normalize_path(&format!("{parent_guest_cwd}/{cwd}")),
4830 Some(normalize_host_path(&parent_host_cwd.join(cwd))),
4831 )
4832 } else {
4833 (normalize_path(cwd), None)
4834 }
4835 })
4836 .unwrap_or_else(|| (parent_guest_cwd.to_owned(), None));
4837 let inherited_host_cwd = (host_cwd_override.is_none() && guest_cwd == parent_guest_cwd)
4838 .then(|| normalize_host_path(parent_host_cwd));
4839 let host_cwd = host_cwd_override
4840 .or(inherited_host_cwd)
4841 .or_else(|| {
4842 host_runtime_path_for_guest_path_with_env(
4843 vm,
4844 &runtime_env,
4845 &guest_cwd,
4846 parent_host_cwd,
4847 )
4848 })
4849 .unwrap_or_else(|| {
4850 let candidate = PathBuf::from(&guest_cwd);
4851 if guest_cwd == parent_guest_cwd {
4852 normalize_host_path(parent_host_cwd)
4853 } else if candidate.is_absolute() {
4854 shadow_path_for_guest(vm, &guest_cwd)
4855 } else {
4856 vm.host_cwd.clone()
4857 }
4858 });
4859 let mut env = parent_env.clone();
4860 env.extend(request.options.env.clone());
4861 env.remove("AGENT_OS_GUEST_ENTRYPOINT");
4864 env.remove("AGENT_OS_NODE_EVAL");
4865
4866 let (command, process_args) = if request.options.shell {
4867 let tokens = tokenize_shell_free_command(&request.command);
4868 let requires_shell = command_requires_shell(&request.command)
4869 || tokens.first().is_some_and(|command| {
4870 is_posix_shell_builtin(command) || shell_first_token_requires_shell(command)
4871 });
4872 if requires_shell {
4873 if !vm.command_guest_paths.contains_key("sh") {
4874 return Err(SidecarError::InvalidState(format!(
4875 "shell-mode child_process command requires /bin/sh, which is not \
4876 installed in this VM (install a software package that provides sh, \
4877 for example @secure-exec/coreutils): {}",
4878 request.command
4879 )));
4880 }
4881 (
4882 String::from("sh"),
4883 vec![String::from("-c"), request.command.clone()],
4884 )
4885 } else {
4886 let Some((command, args)) = tokens.split_first() else {
4887 return Err(SidecarError::InvalidState(String::from(
4888 "child_process shell command must not be empty",
4889 )));
4890 };
4891 (command.clone(), args.to_vec())
4892 }
4893 } else {
4894 (request.command.clone(), request.args.clone())
4895 };
4896 let process_args = apply_shell_cwd_prefix(&command, process_args, &guest_cwd);
4897 if is_tool_command(vm, &command) {
4898 let command = normalized_tool_command_name(&command).unwrap_or(command);
4899 return Ok(ResolvedChildProcessExecution {
4900 command: command.clone(),
4901 process_args: std::iter::once(command.clone())
4902 .chain(process_args.iter().cloned())
4903 .collect(),
4904 runtime: GuestRuntimeKind::JavaScript,
4905 entrypoint: command,
4906 execution_args: process_args,
4907 env,
4908 guest_cwd,
4909 host_cwd,
4910 wasm_permission_tier: None,
4911 tool_command: true,
4912 });
4913 }
4914
4915 if is_path_like_specifier(&command)
4916 && matches!(
4917 Path::new(&command).extension().and_then(|ext| ext.to_str()),
4918 Some("js" | "mjs" | "cjs" | "ts" | "mts" | "cts")
4919 )
4920 {
4921 let guest_entrypoint = if command.starts_with('/') {
4922 normalize_path(&command)
4923 } else if command.starts_with("file:") {
4924 normalize_path(command.trim_start_matches("file:"))
4925 } else {
4926 normalize_path(&format!("{guest_cwd}/{command}"))
4927 };
4928 let host_entrypoint = if command.starts_with("./") || command.starts_with("../") {
4929 host_cwd.join(&command)
4930 } else {
4931 host_runtime_path_for_guest_path_with_env(
4932 vm,
4933 &runtime_env,
4934 &guest_entrypoint,
4935 parent_host_cwd,
4936 )
4937 .unwrap_or_else(|| {
4938 let candidate = PathBuf::from(&guest_entrypoint);
4939 if candidate.is_absolute() {
4940 candidate
4941 } else {
4942 host_cwd.join(&guest_entrypoint)
4943 }
4944 })
4945 };
4946 env.insert(String::from("AGENT_OS_GUEST_ENTRYPOINT"), guest_entrypoint);
4947 let guest_entrypoint = env.get("AGENT_OS_GUEST_ENTRYPOINT").cloned();
4948 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
4949
4950 return Ok(ResolvedChildProcessExecution {
4951 command: command.clone(),
4952 process_args: std::iter::once(command)
4953 .chain(process_args.iter().cloned())
4954 .collect(),
4955 runtime: GuestRuntimeKind::JavaScript,
4956 entrypoint: host_entrypoint.to_string_lossy().into_owned(),
4957 execution_args: process_args,
4958 env,
4959 guest_cwd,
4960 host_cwd,
4961 wasm_permission_tier: None,
4962 tool_command: false,
4963 });
4964 }
4965
4966 if is_node_runtime_command(&command) {
4967 if let Some(cli) = resolve_host_node_cli_entrypoint(&command) {
4968 env.insert(
4969 String::from("AGENT_OS_NODE_EVAL"),
4970 build_host_node_cli_eval(&cli),
4971 );
4972 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
4973 add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
4974 add_runtime_host_access_path(
4975 &mut env,
4976 "AGENT_OS_EXTRA_FS_READ_PATHS",
4977 &cli.package_root,
4978 true,
4979 );
4980
4981 return Ok(ResolvedChildProcessExecution {
4982 command: command.clone(),
4983 process_args: std::iter::once(command.clone())
4984 .chain(process_args.iter().cloned())
4985 .collect(),
4986 runtime: GuestRuntimeKind::JavaScript,
4987 entrypoint: String::from("-e"),
4988 execution_args: std::iter::once(cli.guest_entrypoint.clone())
4989 .chain(process_args.iter().cloned())
4990 .collect(),
4991 env,
4992 guest_cwd,
4993 host_cwd,
4994 wasm_permission_tier: None,
4995 tool_command: false,
4996 });
4997 }
4998
4999 if process_args.is_empty() {
5000 env.insert(String::from("AGENT_OS_NODE_EVAL"), String::new());
5001 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5002
5003 return Ok(ResolvedChildProcessExecution {
5004 command: command.clone(),
5005 process_args: vec![command.clone()],
5006 runtime: GuestRuntimeKind::JavaScript,
5007 entrypoint: String::from("-e"),
5008 execution_args: Vec::new(),
5009 env,
5010 guest_cwd,
5011 host_cwd,
5012 wasm_permission_tier: None,
5013 tool_command: false,
5014 });
5015 }
5016
5017 if let Some((entrypoint, execution_args)) =
5018 resolve_special_node_cli_invocation(&process_args, &mut env)
5019 {
5020 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5021
5022 return Ok(ResolvedChildProcessExecution {
5023 command: command.clone(),
5024 process_args: std::iter::once(command.clone())
5025 .chain(process_args.iter().cloned())
5026 .collect(),
5027 runtime: GuestRuntimeKind::JavaScript,
5028 entrypoint,
5029 execution_args,
5030 env,
5031 guest_cwd,
5032 host_cwd,
5033 wasm_permission_tier: None,
5034 tool_command: false,
5035 });
5036 }
5037
5038 let Some(entrypoint_specifier) = process_args.first() else {
5039 return Err(SidecarError::InvalidState(format!(
5040 "{command} child_process spawn requires an entrypoint"
5041 )));
5042 };
5043
5044 let (entrypoint, execution_args) = if is_path_like_specifier(entrypoint_specifier) {
5045 let guest_entrypoint = if entrypoint_specifier.starts_with('/') {
5046 normalize_path(entrypoint_specifier)
5047 } else if entrypoint_specifier.starts_with("file:") {
5048 normalize_path(entrypoint_specifier.trim_start_matches("file:"))
5049 } else {
5050 normalize_path(&format!("{guest_cwd}/{entrypoint_specifier}"))
5051 };
5052 let host_entrypoint = if entrypoint_specifier.starts_with("./")
5053 || entrypoint_specifier.starts_with("../")
5054 {
5055 host_cwd.join(entrypoint_specifier)
5056 } else {
5057 host_runtime_path_for_guest_path_with_env(
5058 vm,
5059 &runtime_env,
5060 &guest_entrypoint,
5061 parent_host_cwd,
5062 )
5063 .unwrap_or_else(|| {
5064 let candidate = PathBuf::from(&guest_entrypoint);
5065 if candidate.is_absolute() {
5066 candidate
5067 } else {
5068 host_cwd.join(&guest_entrypoint)
5069 }
5070 })
5071 };
5072 env.insert(String::from("AGENT_OS_GUEST_ENTRYPOINT"), guest_entrypoint);
5073 (
5074 host_entrypoint.to_string_lossy().into_owned(),
5075 process_args.iter().skip(1).cloned().collect(),
5076 )
5077 } else {
5078 (
5079 entrypoint_specifier.clone(),
5080 process_args.iter().skip(1).cloned().collect(),
5081 )
5082 };
5083 let guest_entrypoint = env.get("AGENT_OS_GUEST_ENTRYPOINT").cloned();
5084 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
5085
5086 return Ok(ResolvedChildProcessExecution {
5087 command: command.clone(),
5088 process_args: std::iter::once(command)
5089 .chain(process_args.iter().cloned())
5090 .collect(),
5091 runtime: GuestRuntimeKind::JavaScript,
5092 entrypoint,
5093 execution_args,
5094 env,
5095 guest_cwd,
5096 host_cwd,
5097 wasm_permission_tier: None,
5098 tool_command: false,
5099 });
5100 }
5101
5102 if command == PYTHON_COMMAND {
5103 return Err(SidecarError::InvalidState(String::from(
5104 "nested python child_process execution is not supported yet",
5105 )));
5106 }
5107
5108 let guest_entrypoint = resolve_guest_command_entrypoint(
5109 vm,
5110 &guest_cwd,
5111 &command,
5112 env.get("PATH").map(String::as_str),
5113 )
5114 .ok_or_else(|| SidecarError::InvalidState(format!("command not found: {command}")))?;
5115 let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
5116 let wasm_permission_tier = vm.command_permissions.get(&command).copied().or_else(|| {
5117 Path::new(&guest_entrypoint)
5118 .file_name()
5119 .and_then(|name| name.to_str())
5120 .and_then(|name| vm.command_permissions.get(name).copied())
5121 });
5122 if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
5123 resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
5124 {
5125 prepare_guest_runtime_env(
5126 vm,
5127 &mut env,
5128 &guest_cwd,
5129 &host_cwd,
5130 Some(javascript_guest_entrypoint),
5131 )?;
5132
5133 return Ok(ResolvedChildProcessExecution {
5134 command: command.clone(),
5135 process_args: std::iter::once(command)
5136 .chain(process_args.iter().cloned())
5137 .collect(),
5138 runtime: GuestRuntimeKind::JavaScript,
5139 entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
5140 execution_args: process_args,
5141 env,
5142 guest_cwd,
5143 host_cwd,
5144 wasm_permission_tier: None,
5145 tool_command: false,
5146 });
5147 }
5148 prepare_guest_runtime_env(
5149 vm,
5150 &mut env,
5151 &guest_cwd,
5152 &host_cwd,
5153 Some(guest_entrypoint.clone()),
5154 )?;
5155
5156 Ok(ResolvedChildProcessExecution {
5157 command: command.clone(),
5158 process_args: std::iter::once(command)
5159 .chain(process_args.iter().cloned())
5160 .collect(),
5161 runtime: GuestRuntimeKind::WebAssembly,
5162 entrypoint: host_entrypoint.to_string_lossy().into_owned(),
5163 execution_args: process_args,
5164 env,
5165 guest_cwd,
5166 host_cwd,
5167 wasm_permission_tier,
5168 tool_command: false,
5169 })
5170 }
5171
5172 pub(crate) fn spawn_javascript_child_process(
5173 &mut self,
5174 vm_id: &str,
5175 process_id: &str,
5176 request: JavascriptChildProcessSpawnRequest,
5177 ) -> Result<Value, SidecarError> {
5178 let resolved = {
5179 let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
5180 let parent = vm
5181 .active_processes
5182 .get(process_id)
5183 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5184 self.resolve_javascript_child_process_execution(
5185 vm,
5186 &parent.env,
5187 &parent.guest_cwd,
5188 &parent.host_cwd,
5189 &request,
5190 )?
5191 };
5192 let (parent_kernel_pid, child_process_id) = {
5193 let vm = self
5194 .vms
5195 .get_mut(vm_id)
5196 .ok_or_else(|| missing_vm_error(vm_id))?;
5197 let process = vm
5198 .active_processes
5199 .get_mut(process_id)
5200 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5201 (process.kernel_pid, process.allocate_child_process_id())
5202 };
5203 let sidecar_requests = self.sidecar_requests.clone();
5204 let vm = self
5205 .vms
5206 .get_mut(vm_id)
5207 .ok_or_else(|| missing_vm_error(vm_id))?;
5208 let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
5209 .tool_command
5210 {
5211 let tool_resolution = resolve_tool_command(
5212 vm,
5213 &resolved.command,
5214 &resolved.execution_args,
5215 Some(&resolved.guest_cwd),
5216 )?
5217 .ok_or_else(|| {
5218 SidecarError::InvalidState(format!(
5219 "tool command no longer resolves: {}",
5220 resolved.command
5221 ))
5222 })?;
5223 let kernel_handle = vm
5224 .kernel
5225 .create_virtual_process(
5226 EXECUTION_DRIVER_NAME,
5227 TOOL_DRIVER_NAME,
5228 &resolved.command,
5229 resolved.process_args.clone(),
5230 VirtualProcessOptions {
5231 parent_pid: Some(parent_kernel_pid),
5232 env: resolved.env.clone(),
5233 cwd: Some(resolved.guest_cwd.clone()),
5234 },
5235 )
5236 .map_err(kernel_error)?;
5237 let kernel_pid = kernel_handle.pid();
5238 let tool_execution = ToolExecution::default();
5239 let cancelled = tool_execution.cancelled.clone();
5240 let pending_events = tool_execution.pending_events.clone();
5241 let events_overflowed = tool_execution.events_overflowed.clone();
5242 spawn_tool_process_events(ToolProcessEventRequest {
5243 sidecar_requests: sidecar_requests.clone(),
5244 connection_id: vm.connection_id.clone(),
5245 session_id: vm.session_id.clone(),
5246 vm_id: vm_id.to_owned(),
5247 tool_resolution,
5248 cancelled,
5249 pending_events,
5250 events_overflowed,
5251 });
5252 (
5253 kernel_pid,
5254 kernel_handle,
5255 ActiveExecution::Tool(tool_execution),
5256 None,
5257 )
5258 } else {
5259 let kernel_command = match resolved.runtime {
5260 GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
5261 GuestRuntimeKind::WebAssembly => WASM_COMMAND,
5262 GuestRuntimeKind::Python => {
5263 unreachable!("python child_process execution is rejected")
5264 }
5265 };
5266 let kernel_handle = vm
5267 .kernel
5268 .spawn_process(
5269 kernel_command,
5270 resolved.process_args.clone(),
5271 SpawnOptions {
5272 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
5273 parent_pid: Some(parent_kernel_pid),
5274 env: resolved.env.clone(),
5275 cwd: Some(resolved.guest_cwd.clone()),
5276 },
5277 )
5278 .map_err(kernel_error)?;
5279 let kernel_pid = kernel_handle.pid();
5280 if request.options.detached {
5281 vm.kernel
5282 .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
5283 .map_err(kernel_error)?;
5284 }
5285 let mut execution_env = resolved.env.clone();
5286 execution_env.insert(
5287 String::from(EXECUTION_SANDBOX_ROOT_ENV),
5288 normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
5289 );
5290
5291 let execution = match resolved.runtime {
5292 GuestRuntimeKind::JavaScript => {
5293 execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
5294 &request.options.internal_bootstrap_env,
5295 ));
5296 execution_env.insert(
5297 String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
5298 String::from("1"),
5299 );
5300 execution_env.insert(
5301 String::from("AGENT_OS_VIRTUAL_PROCESS_PID"),
5302 kernel_pid.to_string(),
5303 );
5304 execution_env.insert(
5305 String::from("AGENT_OS_VIRTUAL_PROCESS_PPID"),
5306 parent_kernel_pid.to_string(),
5307 );
5308 let context =
5309 self.javascript_engine
5310 .create_context(CreateJavascriptContextRequest {
5311 vm_id: vm_id.to_owned(),
5312 bootstrap_module: None,
5313 compile_cache_root: Some(
5314 self.cache_root.join("node-compile-cache"),
5315 ),
5316 });
5317 let inline_code = load_javascript_entrypoint_source(
5318 vm,
5319 &resolved.host_cwd,
5320 &resolved.entrypoint,
5321 &execution_env,
5322 );
5323
5324 let module_reader = build_module_reader(vm)
5325 .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
5326 let execution = self
5327 .javascript_engine
5328 .start_execution_with_module_reader(
5329 StartJavascriptExecutionRequest {
5330 vm_id: vm_id.to_owned(),
5331 context_id: context.context_id,
5332 argv: std::iter::once(resolved.entrypoint.clone())
5333 .chain(resolved.execution_args.clone())
5334 .collect(),
5335 env: execution_env,
5336 cwd: resolved.host_cwd.clone(),
5337 inline_code,
5338 },
5339 module_reader,
5340 )
5341 .map_err(javascript_error)?;
5342 ActiveExecution::Javascript(execution)
5343 }
5344 GuestRuntimeKind::WebAssembly => {
5345 execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
5346 execution_env.insert(
5347 String::from("AGENT_OS_VIRTUAL_PROCESS_PID"),
5348 kernel_pid.to_string(),
5349 );
5350 execution_env.insert(
5351 String::from("AGENT_OS_VIRTUAL_PROCESS_PPID"),
5352 parent_kernel_pid.to_string(),
5353 );
5354 apply_wasm_limit_env(&mut execution_env, vm.kernel.resource_limits());
5355 let context = self.wasm_engine.create_context(CreateWasmContextRequest {
5356 vm_id: vm_id.to_owned(),
5357 module_path: Some(resolved.entrypoint.clone()),
5358 });
5359 let execution = self
5360 .wasm_engine
5361 .start_execution(StartWasmExecutionRequest {
5362 vm_id: vm_id.to_owned(),
5363 context_id: context.context_id,
5364 argv: resolved.process_args.clone(),
5365 env: execution_env,
5366 cwd: resolved.host_cwd.clone(),
5367 permission_tier: execution_wasm_permission_tier(
5368 resolved
5369 .wasm_permission_tier
5370 .unwrap_or(WasmPermissionTier::Full),
5371 ),
5372 })
5373 .map_err(wasm_error)?;
5374 ActiveExecution::Wasm(execution)
5375 }
5376 GuestRuntimeKind::Python => {
5377 unreachable!("python child_process execution is rejected")
5378 }
5379 };
5380 let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
5381 "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5382 "ignore" => {
5383 vm.kernel
5384 .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
5385 .map_err(kernel_error)?;
5386 None
5387 }
5388 "inherit" => None,
5389 _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5390 };
5391 (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
5392 };
5393
5394 let process = vm
5395 .active_processes
5396 .get_mut(process_id)
5397 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5398 process.child_processes.insert(
5399 child_process_id.clone(),
5400 ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
5401 .with_detached(request.options.detached)
5402 .with_guest_cwd(resolved.guest_cwd.clone())
5403 .with_env(resolved.env.clone())
5404 .with_host_cwd(resolved.host_cwd.clone()),
5405 );
5406 if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
5407 process
5408 .child_processes
5409 .get_mut(&child_process_id)
5410 .ok_or_else(|| {
5411 SidecarError::InvalidState(format!(
5412 "child process {child_process_id} disappeared during spawn"
5413 ))
5414 })?
5415 .kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
5416 }
5417 Ok(json!({
5418 "childId": child_process_id,
5419 "pid": kernel_pid,
5420 "command": resolved.command,
5421 "args": resolved.process_args,
5422 }))
5423 }
5424
5425 pub(crate) fn spawn_javascript_child_process_sync(
5426 &mut self,
5427 vm_id: &str,
5428 process_id: &str,
5429 request: JavascriptChildProcessSpawnRequest,
5430 max_buffer: Option<usize>,
5431 ) -> Result<Value, SidecarError> {
5432 let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
5433 let timeout_deadline = request
5434 .options
5435 .timeout
5436 .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
5437 let timeout_signal = request
5438 .options
5439 .kill_signal
5440 .clone()
5441 .unwrap_or_else(|| String::from("SIGTERM"));
5442 let spawned = self.spawn_javascript_child_process(vm_id, process_id, request)?;
5443 let child_process_id = spawned
5444 .get("childId")
5445 .and_then(Value::as_str)
5446 .ok_or_else(|| {
5447 SidecarError::InvalidState(String::from(
5448 "child_process.spawn_sync response is missing childId",
5449 ))
5450 })?
5451 .to_owned();
5452
5453 if let Some(input) = sync_input.as_deref() {
5454 self.write_javascript_child_process_stdin(vm_id, process_id, &child_process_id, input)?;
5455 }
5456 self.close_javascript_child_process_stdin(vm_id, process_id, &child_process_id)?;
5457
5458 let max_buffer = max_buffer.unwrap_or(1024 * 1024);
5459 let mut stdout = Vec::new();
5460 let mut stderr = Vec::new();
5461 let mut max_buffer_exceeded = false;
5462 let mut kill_sent = false;
5463 let mut timed_out = false;
5464
5465 let exit_code = loop {
5466 let wait_ms = if let Some(deadline) = timeout_deadline {
5467 let now = Instant::now();
5468 if now >= deadline {
5469 if !kill_sent {
5470 timed_out = true;
5471 self.kill_javascript_child_process(
5472 vm_id,
5473 process_id,
5474 &child_process_id,
5475 &timeout_signal,
5476 )?;
5477 kill_sent = true;
5478 }
5479 0
5480 } else {
5481 u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
5482 .unwrap_or(50)
5483 }
5484 } else {
5485 50
5486 };
5487 let event =
5488 self.poll_javascript_child_process(vm_id, process_id, &child_process_id, wait_ms)?;
5489 if event.is_null() {
5490 continue;
5491 }
5492
5493 match event.get("type").and_then(Value::as_str) {
5494 Some("stdout") => {
5495 let chunk = javascript_sync_rpc_bytes_arg(
5496 &[event.get("data").cloned().unwrap_or(Value::Null)],
5497 0,
5498 "child_process.spawn_sync stdout",
5499 )?;
5500 stdout.extend_from_slice(&chunk);
5501 if stdout.len() > max_buffer && !kill_sent {
5502 max_buffer_exceeded = true;
5503 self.kill_javascript_child_process(
5504 vm_id,
5505 process_id,
5506 &child_process_id,
5507 "SIGTERM",
5508 )?;
5509 kill_sent = true;
5510 }
5511 }
5512 Some("stderr") => {
5513 let chunk = javascript_sync_rpc_bytes_arg(
5514 &[event.get("data").cloned().unwrap_or(Value::Null)],
5515 0,
5516 "child_process.spawn_sync stderr",
5517 )?;
5518 stderr.extend_from_slice(&chunk);
5519 if stderr.len() > max_buffer && !kill_sent {
5520 max_buffer_exceeded = true;
5521 self.kill_javascript_child_process(
5522 vm_id,
5523 process_id,
5524 &child_process_id,
5525 "SIGTERM",
5526 )?;
5527 kill_sent = true;
5528 }
5529 }
5530 Some("exit") => {
5531 break event
5532 .get("exitCode")
5533 .and_then(Value::as_i64)
5534 .map(|value| value as i32)
5535 .unwrap_or(1);
5536 }
5537 _ => {}
5538 }
5539 };
5540
5541 Ok(json!({
5542 "stdout": String::from_utf8_lossy(&stdout),
5543 "stderr": String::from_utf8_lossy(&stderr),
5544 "code": exit_code,
5545 "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
5546 "timedOut": timed_out,
5547 "maxBufferExceeded": max_buffer_exceeded,
5548 }))
5549 }
5550
5551 fn spawn_descendant_javascript_child_process(
5552 &mut self,
5553 vm_id: &str,
5554 process_id: &str,
5555 current_process_path: &[&str],
5556 request: JavascriptChildProcessSpawnRequest,
5557 ) -> Result<Value, SidecarError> {
5558 let current_process_label =
5559 Self::child_process_path_label(process_id, current_process_path);
5560 let (resolved, parent_kernel_pid) = {
5561 let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
5562 let root = vm
5563 .active_processes
5564 .get(process_id)
5565 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5566 let parent =
5567 Self::active_process_by_path(root, current_process_path).ok_or_else(|| {
5568 SidecarError::InvalidState(format!(
5569 "unknown child process path {current_process_label} during nested spawn"
5570 ))
5571 })?;
5572 (
5573 self.resolve_javascript_child_process_execution(
5574 vm,
5575 &parent.env,
5576 &parent.guest_cwd,
5577 &parent.host_cwd,
5578 &request,
5579 )?,
5580 parent.kernel_pid,
5581 )
5582 };
5583
5584 let sidecar_requests = self.sidecar_requests.clone();
5585 let vm = self
5586 .vms
5587 .get_mut(vm_id)
5588 .ok_or_else(|| missing_vm_error(vm_id))?;
5589 let child_process_id = {
5590 let root = vm
5591 .active_processes
5592 .get_mut(process_id)
5593 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5594 let parent =
5595 Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
5596 SidecarError::InvalidState(format!(
5597 "unknown child process path {current_process_label} during nested spawn"
5598 ))
5599 })?;
5600 parent.allocate_child_process_id()
5601 };
5602 let mut child_path = current_process_path.to_vec();
5603 child_path.push(child_process_id.as_str());
5604 let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
5605 .tool_command
5606 {
5607 let tool_resolution = resolve_tool_command(
5608 vm,
5609 &resolved.command,
5610 &resolved.execution_args,
5611 Some(&resolved.guest_cwd),
5612 )?
5613 .ok_or_else(|| {
5614 SidecarError::InvalidState(format!(
5615 "tool command no longer resolves: {}",
5616 resolved.command
5617 ))
5618 })?;
5619 let kernel_handle = vm
5620 .kernel
5621 .create_virtual_process(
5622 EXECUTION_DRIVER_NAME,
5623 TOOL_DRIVER_NAME,
5624 &resolved.command,
5625 resolved.process_args.clone(),
5626 VirtualProcessOptions {
5627 parent_pid: Some(parent_kernel_pid),
5628 env: resolved.env.clone(),
5629 cwd: Some(resolved.guest_cwd.clone()),
5630 },
5631 )
5632 .map_err(kernel_error)?;
5633 let kernel_pid = kernel_handle.pid();
5634 let tool_execution = ToolExecution::default();
5635 let cancelled = tool_execution.cancelled.clone();
5636 let pending_events = tool_execution.pending_events.clone();
5637 let events_overflowed = tool_execution.events_overflowed.clone();
5638 spawn_tool_process_events(ToolProcessEventRequest {
5639 sidecar_requests: sidecar_requests.clone(),
5640 connection_id: vm.connection_id.clone(),
5641 session_id: vm.session_id.clone(),
5642 vm_id: vm_id.to_owned(),
5643 tool_resolution,
5644 cancelled,
5645 pending_events,
5646 events_overflowed,
5647 });
5648 (
5649 kernel_pid,
5650 kernel_handle,
5651 ActiveExecution::Tool(tool_execution),
5652 None,
5653 )
5654 } else {
5655 let kernel_command = match resolved.runtime {
5656 GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
5657 GuestRuntimeKind::WebAssembly => WASM_COMMAND,
5658 GuestRuntimeKind::Python => {
5659 unreachable!("python child_process execution is rejected")
5660 }
5661 };
5662 let kernel_handle = vm
5663 .kernel
5664 .spawn_process(
5665 kernel_command,
5666 resolved.process_args.clone(),
5667 SpawnOptions {
5668 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
5669 parent_pid: Some(parent_kernel_pid),
5670 env: resolved.env.clone(),
5671 cwd: Some(resolved.guest_cwd.clone()),
5672 },
5673 )
5674 .map_err(kernel_error)?;
5675 let kernel_pid = kernel_handle.pid();
5676 if request.options.detached {
5677 vm.kernel
5678 .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
5679 .map_err(kernel_error)?;
5680 }
5681 let mut execution_env = resolved.env.clone();
5682 execution_env.insert(
5683 String::from(EXECUTION_SANDBOX_ROOT_ENV),
5684 normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
5685 );
5686 let execution = match resolved.runtime {
5687 GuestRuntimeKind::JavaScript => {
5688 execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
5689 &request.options.internal_bootstrap_env,
5690 ));
5691 execution_env.insert(
5692 String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
5693 String::from("1"),
5694 );
5695 execution_env.insert(
5696 String::from("AGENT_OS_VIRTUAL_PROCESS_PID"),
5697 kernel_pid.to_string(),
5698 );
5699 execution_env.insert(
5700 String::from("AGENT_OS_VIRTUAL_PROCESS_PPID"),
5701 parent_kernel_pid.to_string(),
5702 );
5703 let context =
5704 self.javascript_engine
5705 .create_context(CreateJavascriptContextRequest {
5706 vm_id: vm_id.to_owned(),
5707 bootstrap_module: None,
5708 compile_cache_root: Some(
5709 self.cache_root.join("node-compile-cache"),
5710 ),
5711 });
5712 let inline_code = load_javascript_entrypoint_source(
5713 vm,
5714 &resolved.host_cwd,
5715 &resolved.entrypoint,
5716 &execution_env,
5717 );
5718
5719 let module_reader = build_module_reader(vm)
5720 .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
5721 let execution = self
5722 .javascript_engine
5723 .start_execution_with_module_reader(
5724 StartJavascriptExecutionRequest {
5725 vm_id: vm_id.to_owned(),
5726 context_id: context.context_id,
5727 argv: std::iter::once(resolved.entrypoint.clone())
5728 .chain(resolved.execution_args.clone())
5729 .collect(),
5730 env: execution_env,
5731 cwd: resolved.host_cwd.clone(),
5732 inline_code,
5733 },
5734 module_reader,
5735 )
5736 .map_err(javascript_error)?;
5737 ActiveExecution::Javascript(execution)
5738 }
5739 GuestRuntimeKind::WebAssembly => {
5740 execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
5741 execution_env.insert(
5742 String::from("AGENT_OS_VIRTUAL_PROCESS_PID"),
5743 kernel_pid.to_string(),
5744 );
5745 execution_env.insert(
5746 String::from("AGENT_OS_VIRTUAL_PROCESS_PPID"),
5747 parent_kernel_pid.to_string(),
5748 );
5749 apply_wasm_limit_env(&mut execution_env, vm.kernel.resource_limits());
5750 let context = self.wasm_engine.create_context(CreateWasmContextRequest {
5751 vm_id: vm_id.to_owned(),
5752 module_path: Some(resolved.entrypoint.clone()),
5753 });
5754 let execution = self
5755 .wasm_engine
5756 .start_execution(StartWasmExecutionRequest {
5757 vm_id: vm_id.to_owned(),
5758 context_id: context.context_id,
5759 argv: resolved.process_args.clone(),
5760 env: execution_env,
5761 cwd: resolved.host_cwd.clone(),
5762 permission_tier: execution_wasm_permission_tier(
5763 resolved
5764 .wasm_permission_tier
5765 .unwrap_or(WasmPermissionTier::Full),
5766 ),
5767 })
5768 .map_err(wasm_error)?;
5769 ActiveExecution::Wasm(execution)
5770 }
5771 GuestRuntimeKind::Python => {
5772 unreachable!("python child_process execution is rejected")
5773 }
5774 };
5775 let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
5776 "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5777 "ignore" => {
5778 vm.kernel
5779 .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
5780 .map_err(kernel_error)?;
5781 None
5782 }
5783 "inherit" => None,
5784 _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5785 };
5786 (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
5787 };
5788
5789 let root = vm
5790 .active_processes
5791 .get_mut(process_id)
5792 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5793 let parent =
5794 Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
5795 SidecarError::InvalidState(format!(
5796 "unknown child process path {current_process_label} during nested spawn"
5797 ))
5798 })?;
5799 parent.child_processes.insert(
5800 child_process_id.clone(),
5801 ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
5802 .with_detached(request.options.detached)
5803 .with_guest_cwd(resolved.guest_cwd.clone())
5804 .with_env(resolved.env.clone())
5805 .with_host_cwd(resolved.host_cwd.clone()),
5806 );
5807 if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
5808 parent
5809 .child_processes
5810 .get_mut(&child_process_id)
5811 .ok_or_else(|| {
5812 SidecarError::InvalidState(format!(
5813 "child process {child_process_id} disappeared during nested spawn"
5814 ))
5815 })?
5816 .kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
5817 }
5818 Ok(json!({
5819 "childId": child_process_id,
5820 "pid": kernel_pid,
5821 "command": resolved.command,
5822 "args": resolved.process_args,
5823 }))
5824 }
5825
5826 fn spawn_descendant_javascript_child_process_sync(
5827 &mut self,
5828 vm_id: &str,
5829 process_id: &str,
5830 current_process_path: &[&str],
5831 request: JavascriptChildProcessSpawnRequest,
5832 max_buffer: Option<usize>,
5833 ) -> Result<Value, SidecarError> {
5834 let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
5835 let timeout_deadline = request
5836 .options
5837 .timeout
5838 .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
5839 let timeout_signal = request
5840 .options
5841 .kill_signal
5842 .clone()
5843 .unwrap_or_else(|| String::from("SIGTERM"));
5844 let spawned = self.spawn_descendant_javascript_child_process(
5845 vm_id,
5846 process_id,
5847 current_process_path,
5848 request,
5849 )?;
5850 let child_process_id = spawned
5851 .get("childId")
5852 .and_then(Value::as_str)
5853 .ok_or_else(|| {
5854 SidecarError::InvalidState(String::from(
5855 "child_process.spawn_sync response is missing childId",
5856 ))
5857 })?
5858 .to_owned();
5859
5860 if let Some(input) = sync_input.as_deref() {
5861 self.write_descendant_javascript_child_process_stdin(
5862 vm_id,
5863 process_id,
5864 current_process_path,
5865 &child_process_id,
5866 input,
5867 )?;
5868 }
5869 self.close_descendant_javascript_child_process_stdin(
5870 vm_id,
5871 process_id,
5872 current_process_path,
5873 &child_process_id,
5874 )?;
5875
5876 let max_buffer = max_buffer.unwrap_or(1024 * 1024);
5877 let mut stdout = Vec::new();
5878 let mut stderr = Vec::new();
5879 let mut max_buffer_exceeded = false;
5880 let mut kill_sent = false;
5881 let mut timed_out = false;
5882
5883 let exit_code = loop {
5884 let wait_ms = if let Some(deadline) = timeout_deadline {
5885 let now = Instant::now();
5886 if now >= deadline {
5887 if !kill_sent {
5888 timed_out = true;
5889 self.kill_descendant_javascript_child_process(
5890 vm_id,
5891 process_id,
5892 current_process_path,
5893 &child_process_id,
5894 &timeout_signal,
5895 )?;
5896 kill_sent = true;
5897 }
5898 0
5899 } else {
5900 u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
5901 .unwrap_or(50)
5902 }
5903 } else {
5904 50
5905 };
5906 let event = self.poll_descendant_javascript_child_process(
5907 vm_id,
5908 process_id,
5909 current_process_path,
5910 &child_process_id,
5911 wait_ms,
5912 )?;
5913 if event.is_null() {
5914 continue;
5915 }
5916
5917 match event.get("type").and_then(Value::as_str) {
5918 Some("stdout") => {
5919 let chunk = javascript_sync_rpc_bytes_arg(
5920 &[event.get("data").cloned().unwrap_or(Value::Null)],
5921 0,
5922 "child_process.spawn_sync stdout",
5923 )?;
5924 stdout.extend_from_slice(&chunk);
5925 if stdout.len() > max_buffer && !kill_sent {
5926 max_buffer_exceeded = true;
5927 self.kill_descendant_javascript_child_process(
5928 vm_id,
5929 process_id,
5930 current_process_path,
5931 &child_process_id,
5932 "SIGTERM",
5933 )?;
5934 kill_sent = true;
5935 }
5936 }
5937 Some("stderr") => {
5938 let chunk = javascript_sync_rpc_bytes_arg(
5939 &[event.get("data").cloned().unwrap_or(Value::Null)],
5940 0,
5941 "child_process.spawn_sync stderr",
5942 )?;
5943 stderr.extend_from_slice(&chunk);
5944 if stderr.len() > max_buffer && !kill_sent {
5945 max_buffer_exceeded = true;
5946 self.kill_descendant_javascript_child_process(
5947 vm_id,
5948 process_id,
5949 current_process_path,
5950 &child_process_id,
5951 "SIGTERM",
5952 )?;
5953 kill_sent = true;
5954 }
5955 }
5956 Some("exit") => {
5957 break event
5958 .get("exitCode")
5959 .and_then(Value::as_i64)
5960 .map(|value| value as i32)
5961 .unwrap_or(1);
5962 }
5963 _ => {}
5964 }
5965 };
5966
5967 Ok(json!({
5968 "stdout": String::from_utf8_lossy(&stdout),
5969 "stderr": String::from_utf8_lossy(&stderr),
5970 "code": exit_code,
5971 "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
5972 "timedOut": timed_out,
5973 "maxBufferExceeded": max_buffer_exceeded,
5974 }))
5975 }
5976
5977 fn handle_descendant_javascript_child_process_rpc(
5978 &mut self,
5979 vm_id: &str,
5980 process_id: &str,
5981 current_process_path: &[&str],
5982 request: &JavascriptSyncRpcRequest,
5983 ) -> Result<Value, SidecarError> {
5984 match request.method.as_str() {
5985 "child_process.spawn" => {
5986 let Some(vm) = self.vms.get(vm_id) else {
5987 return Ok(Value::Null);
5988 };
5989 let (payload, _) = parse_javascript_child_process_spawn_request(vm, &request.args)?;
5990 self.spawn_descendant_javascript_child_process(
5991 vm_id,
5992 process_id,
5993 current_process_path,
5994 payload,
5995 )
5996 }
5997 "child_process.spawn_sync" => {
5998 let Some(vm) = self.vms.get(vm_id) else {
5999 return Ok(Value::Null);
6000 };
6001 let (payload, max_buffer) =
6002 parse_javascript_child_process_spawn_request(vm, &request.args)?;
6003 self.spawn_descendant_javascript_child_process_sync(
6004 vm_id,
6005 process_id,
6006 current_process_path,
6007 payload,
6008 max_buffer,
6009 )
6010 }
6011 "child_process.poll" => {
6012 let child_process_id =
6013 javascript_sync_rpc_arg_str(&request.args, 0, "child_process.poll child id")?;
6014 let wait_ms = javascript_sync_rpc_arg_u64_optional(
6015 &request.args,
6016 1,
6017 "child_process.poll wait ms",
6018 )?
6019 .unwrap_or_default();
6020 self.poll_descendant_javascript_child_process(
6021 vm_id,
6022 process_id,
6023 current_process_path,
6024 child_process_id,
6025 wait_ms,
6026 )
6027 }
6028 "child_process.write_stdin" => {
6029 let child_process_id = javascript_sync_rpc_arg_str(
6030 &request.args,
6031 0,
6032 "child_process.write_stdin child id",
6033 )?;
6034 let chunk = javascript_sync_rpc_bytes_arg(
6035 &request.args,
6036 1,
6037 "child_process.write_stdin chunk",
6038 )?;
6039 self.write_descendant_javascript_child_process_stdin(
6040 vm_id,
6041 process_id,
6042 current_process_path,
6043 child_process_id,
6044 &chunk,
6045 )?;
6046 Ok(Value::Null)
6047 }
6048 "child_process.close_stdin" => {
6049 let child_process_id = javascript_sync_rpc_arg_str(
6050 &request.args,
6051 0,
6052 "child_process.close_stdin child id",
6053 )?;
6054 self.close_descendant_javascript_child_process_stdin(
6055 vm_id,
6056 process_id,
6057 current_process_path,
6058 child_process_id,
6059 )?;
6060 Ok(Value::Null)
6061 }
6062 "child_process.kill" => {
6063 let child_process_id =
6064 javascript_sync_rpc_arg_str(&request.args, 0, "child_process.kill child id")?;
6065 let signal =
6066 javascript_sync_rpc_arg_str(&request.args, 1, "child_process.kill signal")?;
6067 self.kill_descendant_javascript_child_process(
6068 vm_id,
6069 process_id,
6070 current_process_path,
6071 child_process_id,
6072 signal,
6073 )?;
6074 Ok(Value::Null)
6075 }
6076 _ => Err(SidecarError::InvalidState(format!(
6077 "unsupported nested child process RPC method {}",
6078 request.method
6079 ))),
6080 }
6081 }
6082
6083 fn poll_descendant_javascript_child_process(
6084 &mut self,
6085 vm_id: &str,
6086 process_id: &str,
6087 current_process_path: &[&str],
6088 child_process_id: &str,
6089 wait_ms: u64,
6090 ) -> Result<Value, SidecarError> {
6091 let mut child_path = current_process_path.to_vec();
6092 child_path.push(child_process_id);
6093 let child_gone_error = || javascript_child_process_gone_error(process_id, &child_path);
6094 let deadline = Instant::now() + Duration::from_millis(wait_ms);
6095 let mut polled_once = false;
6096
6097 loop {
6098 self.drain_queued_descendant_javascript_child_process_events(
6099 vm_id,
6100 process_id,
6101 &child_path,
6102 )?;
6103 enum ChildPollResult {
6104 Event(Box<Option<ActiveExecutionEvent>>),
6105 RecoverRuntimeExit,
6106 Timeout,
6107 }
6108 let wait = if wait_ms == 0 {
6109 Duration::ZERO
6110 } else {
6111 deadline.saturating_duration_since(Instant::now())
6112 };
6113 let poll_result = {
6114 let Some(vm) = self.vms.get_mut(vm_id) else {
6115 return Ok(Value::Null);
6116 };
6117 let Some(parent) =
6118 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6119 else {
6120 return Err(child_gone_error());
6121 };
6122 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6123 return Err(child_gone_error());
6124 };
6125 if let Some(event) = child.pending_execution_events.pop_front() {
6126 ChildPollResult::Event(Box::new(Some(event)))
6127 } else if polled_once && wait.is_zero() {
6128 ChildPollResult::Timeout
6129 } else {
6130 polled_once = true;
6131 match child.execution.poll_event_blocking(wait) {
6132 Ok(Some(event)) => ChildPollResult::Event(Box::new(Some(event))),
6133 Ok(None) => ChildPollResult::RecoverRuntimeExit,
6134 Err(SidecarError::Execution(message))
6135 if (child.runtime == GuestRuntimeKind::JavaScript
6136 && closed_javascript_event_channel(&message))
6137 || (child.runtime == GuestRuntimeKind::Python
6138 && closed_python_event_channel(&message))
6139 || (child.runtime == GuestRuntimeKind::WebAssembly
6140 && closed_wasm_event_channel(&message)) =>
6141 {
6142 ChildPollResult::RecoverRuntimeExit
6143 }
6144 Err(error) => return Err(error),
6145 }
6146 }
6147 };
6148 let event = match poll_result {
6149 ChildPollResult::Event(event) => *event,
6150 ChildPollResult::Timeout => return Ok(Value::Null),
6151 ChildPollResult::RecoverRuntimeExit => self
6152 .recover_descendant_runtime_child_process_event(
6153 vm_id,
6154 process_id,
6155 current_process_path,
6156 child_process_id,
6157 wait.as_millis().try_into().unwrap_or(u64::MAX),
6158 )?,
6159 };
6160
6161 let Some(event) = event else {
6162 return Ok(Value::Null);
6163 };
6164
6165 match event {
6166 ActiveExecutionEvent::Stdout(chunk) => {
6167 return Ok(json!({
6168 "type": "stdout",
6169 "data": javascript_sync_rpc_bytes_value(&chunk),
6170 }));
6171 }
6172 ActiveExecutionEvent::Stderr(chunk) => {
6173 return Ok(json!({
6174 "type": "stderr",
6175 "data": javascript_sync_rpc_bytes_value(&chunk),
6176 }));
6177 }
6178 ActiveExecutionEvent::Exited(exit_code) => {
6179 let had_trailing_events = {
6180 let Some(vm) = self.vms.get_mut(vm_id) else {
6181 return Ok(Value::Null);
6182 };
6183 let Some(parent) = Self::descendant_parent_process_mut(
6184 vm,
6185 process_id,
6186 current_process_path,
6187 ) else {
6188 return Ok(Value::Null);
6189 };
6190 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6191 return Ok(Value::Null);
6192 };
6193 let deadline = Instant::now() + Duration::from_millis(150);
6194 loop {
6195 let wait = deadline.saturating_duration_since(Instant::now());
6196 let next = poll_child_execution_after_exit(child, wait)?;
6197 let Some(next) = next else {
6198 break;
6199 };
6200 if matches!(next, ActiveExecutionEvent::Exited(_)) {
6201 continue;
6202 }
6203 child.queue_pending_execution_event(next)?;
6204 if Instant::now() >= deadline {
6205 break;
6206 }
6207 }
6208 if !child.pending_execution_events.is_empty() {
6209 child.queue_pending_execution_event(ActiveExecutionEvent::Exited(
6210 exit_code,
6211 ))?;
6212 true
6213 } else {
6214 false
6215 }
6216 };
6217 if had_trailing_events {
6218 continue;
6219 }
6220
6221 let parent_signal_key =
6222 Self::child_process_signal_key(process_id, current_process_path);
6223 let Some(vm) = self.vms.get_mut(vm_id) else {
6224 return Ok(Value::Null);
6225 };
6226 let signal_name = {
6227 let Some(parent) = Self::descendant_parent_process_mut(
6228 vm,
6229 process_id,
6230 current_process_path,
6231 ) else {
6232 return Ok(Value::Null);
6233 };
6234 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6235 return Ok(Value::Null);
6236 };
6237 child.pending_self_signal_exit.take().and_then(|signal| {
6238 if exit_code == 128 + signal {
6239 canonical_signal_name(signal).map(str::to_owned)
6240 } else {
6241 None
6242 }
6243 })
6244 };
6245 let (parent_runtime_pid, parent_v8_signal_session, should_signal_parent) = {
6246 let Some(parent) =
6247 Self::descendant_parent_process(vm, process_id, current_process_path)
6248 else {
6249 return Ok(Value::Null);
6250 };
6251 (
6252 parent.execution.child_pid(),
6253 parent.execution.javascript_v8_session_handle().filter(|_| {
6254 matches!(
6255 &parent.execution,
6256 ActiveExecution::Javascript(execution)
6257 if execution.uses_shared_v8_runtime()
6258 )
6259 }),
6260 vm.signal_states
6261 .get(parent_signal_key)
6262 .and_then(|handlers| handlers.get(&(libc::SIGCHLD as u32)))
6263 .is_some_and(|registration| {
6264 registration.action != SignalDispositionAction::Default
6265 }),
6266 )
6267 };
6268 let Some(parent) =
6269 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6270 else {
6271 return Ok(Value::Null);
6272 };
6273 let Some(mut child) = parent.child_processes.remove(child_process_id) else {
6274 return Ok(Value::Null);
6275 };
6276 let child_process_label =
6277 Self::child_process_path_label(process_id, &child_path);
6278 let detached_children =
6279 Self::adopt_detached_child_processes(&child_process_label, &mut child);
6280 sync_process_host_writes_to_kernel(vm, &child)?;
6281 terminate_child_process_tree(&mut vm.kernel, &mut child);
6282 child.kernel_handle.finish(exit_code);
6283 let _ = vm.kernel.wait_and_reap(child.kernel_pid);
6284 vm.signal_states.remove(child_process_id);
6285 for (detached_process_id, detached_child) in detached_children {
6286 vm.detached_child_processes
6287 .insert(detached_process_id.clone());
6288 vm.active_processes
6289 .insert(detached_process_id, detached_child);
6290 }
6291 if should_signal_parent {
6292 if let Some(session) = parent_v8_signal_session {
6293 dispatch_v8_session_signal_async(session, libc::SIGCHLD);
6294 } else {
6295 signal_runtime_process(parent_runtime_pid, libc::SIGCHLD)?;
6296 }
6297 }
6298 let mut payload = Map::new();
6299 payload.insert(String::from("type"), Value::String(String::from("exit")));
6300 payload.insert(String::from("exitCode"), Value::from(exit_code));
6301 if let Some(signal_name) = signal_name {
6302 payload.insert(String::from("signal"), Value::String(signal_name));
6303 }
6304 return Ok(Value::Object(payload));
6305 }
6306 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
6307 let mut current_child_path = current_process_path.to_vec();
6308 current_child_path.push(child_process_id);
6309 let response = if request.method == "process.signal_state" {
6310 let (signal, registration) =
6311 parse_process_signal_state_request(&request.args)?;
6312 let Some(vm) = self.vms.get_mut(vm_id) else {
6313 return Ok(Value::Null);
6314 };
6315 let signal_key =
6316 Self::child_process_signal_key(process_id, ¤t_child_path)
6317 .to_owned();
6318 apply_process_signal_state_update(
6319 &mut vm.signal_states,
6320 &signal_key,
6321 signal,
6322 registration,
6323 );
6324 Ok(Value::Null)
6325 } else if request.method == "process.kill" {
6326 self.handle_descendant_process_kill_rpc(
6327 vm_id,
6328 process_id,
6329 current_process_path,
6330 child_process_id,
6331 &request,
6332 )
6333 } else if request.method.starts_with("child_process.") {
6334 self.handle_descendant_javascript_child_process_rpc(
6335 vm_id,
6336 process_id,
6337 ¤t_child_path,
6338 &request,
6339 )
6340 } else {
6341 let Some(vm) = self.vms.get_mut(vm_id) else {
6342 return Ok(Value::Null);
6343 };
6344 let resource_limits = vm.kernel.resource_limits().clone();
6345 let network_counts = vm_network_resource_counts(vm);
6346 let socket_paths = build_javascript_socket_path_context(vm)?;
6347 let Some(root) = vm.active_processes.get_mut(process_id) else {
6348 return Ok(Value::Null);
6349 };
6350 let Some(parent) =
6351 Self::active_process_by_path_mut(root, current_process_path)
6352 else {
6353 return Ok(Value::Null);
6354 };
6355 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6356 return Ok(Value::Null);
6357 };
6358 service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
6359 bridge: &self.bridge,
6360 vm_id,
6361 dns: &vm.dns,
6362 socket_paths: &socket_paths,
6363 kernel: &mut vm.kernel,
6364 process: child,
6365 sync_request: &request,
6366 resource_limits: &resource_limits,
6367 network_counts,
6368 })
6369 };
6370
6371 let Some(vm) = self.vms.get_mut(vm_id) else {
6372 return Ok(Value::Null);
6373 };
6374 let Some(parent) =
6375 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6376 else {
6377 return Ok(Value::Null);
6378 };
6379 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6380 return Ok(Value::Null);
6381 };
6382 let parent_signal_event = response.as_ref().ok().and_then(|result| {
6383 let target_path_label =
6384 Self::child_process_path_label(process_id, current_process_path);
6385 if request.method != "process.kill"
6386 || result.get("action").and_then(Value::as_str) != Some("user")
6387 || result.get("targetProcessPath").and_then(Value::as_str)
6388 != Some(target_path_label.as_str())
6389 {
6390 return None;
6391 }
6392 Some(json!({
6393 "type": "signal",
6394 "signal": result.get("signal").and_then(Value::as_str).unwrap_or_default(),
6395 "number": result.get("number").and_then(Value::as_i64).unwrap_or_default(),
6396 }))
6397 });
6398 match response {
6399 Ok(result) => child
6400 .execution
6401 .respond_javascript_sync_rpc_success(request.id, result)
6402 .or_else(ignore_stale_javascript_sync_rpc_response)?,
6403 Err(error) => child
6404 .execution
6405 .respond_javascript_sync_rpc_error(
6406 request.id,
6407 javascript_sync_rpc_error_code(&error),
6408 error.to_string(),
6409 )
6410 .or_else(ignore_stale_javascript_sync_rpc_response)?,
6411 }
6412 if let Some(event) = parent_signal_event {
6413 return Ok(event);
6414 }
6415 }
6416 ActiveExecutionEvent::PythonVfsRpcRequest(_) => {
6417 return Err(SidecarError::InvalidState(String::from(
6418 "nested Python child_process execution is not supported yet",
6419 )));
6420 }
6421 ActiveExecutionEvent::SignalState {
6422 signal,
6423 registration,
6424 } => {
6425 let Some(vm) = self.vms.get_mut(vm_id) else {
6426 return Ok(Value::Null);
6427 };
6428 let signal_key =
6429 Self::child_process_signal_key(process_id, &child_path).to_owned();
6430 apply_process_signal_state_update(
6431 &mut vm.signal_states,
6432 &signal_key,
6433 signal,
6434 registration.clone(),
6435 );
6436 return Ok(json!({
6437 "type": "signal_state",
6438 "signal": signal,
6439 "registration": registration,
6440 }));
6441 }
6442 }
6443 }
6444 }
6445
6446 fn recover_descendant_runtime_child_process_event(
6447 &mut self,
6448 vm_id: &str,
6449 process_id: &str,
6450 current_process_path: &[&str],
6451 child_process_id: &str,
6452 wait_ms: u64,
6453 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
6454 let (
6455 parent_kernel_pid,
6456 child_kernel_pid,
6457 child_runtime_pid,
6458 child_runtime,
6459 child_shared_runtime,
6460 ) = {
6461 let mut child_path = current_process_path.to_vec();
6462 child_path.push(child_process_id);
6463 let Some(vm) = self.vms.get_mut(vm_id) else {
6464 return Ok(None);
6465 };
6466 let Some(parent) =
6467 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6468 else {
6469 return Err(javascript_child_process_gone_error(process_id, &child_path));
6470 };
6471 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6472 return Err(javascript_child_process_gone_error(process_id, &child_path));
6473 };
6474 (
6475 parent.kernel_pid,
6476 child.kernel_pid,
6477 child.execution.child_pid(),
6478 child.runtime.clone(),
6479 child.execution.uses_shared_v8_runtime(),
6480 )
6481 };
6482 if child_runtime != GuestRuntimeKind::JavaScript
6483 && child_runtime != GuestRuntimeKind::Python
6484 && child_runtime != GuestRuntimeKind::WebAssembly
6485 {
6486 return Ok(None);
6487 }
6488 let wait_deadline = Instant::now() + Duration::from_millis(wait_ms.min(25));
6489 loop {
6490 let Some(vm) = self.vms.get_mut(vm_id) else {
6491 return Ok(None);
6492 };
6493 if let Some(process_info) = vm.kernel.list_processes().get(&child_kernel_pid) {
6494 if process_info.status == ProcessStatus::Exited {
6495 return Ok(Some(ActiveExecutionEvent::Exited(
6496 process_info.exit_code.unwrap_or(0),
6497 )));
6498 }
6499 }
6500 if let Some(wait_result) = vm
6501 .kernel
6502 .waitpid_with_options(
6503 EXECUTION_DRIVER_NAME,
6504 parent_kernel_pid,
6505 child_kernel_pid as i32,
6506 WaitPidFlags::WNOHANG,
6507 )
6508 .map_err(kernel_error)?
6509 {
6510 return Ok(Some(ActiveExecutionEvent::Exited(wait_result.status)));
6511 }
6512
6513 if !child_shared_runtime && child_runtime_pid != 0 {
6514 if let Some(status) = runtime_child_exit_status(child_runtime_pid)? {
6515 return Ok(Some(ActiveExecutionEvent::Exited(status)));
6516 }
6517 if !runtime_child_is_alive(child_runtime_pid)? {
6518 return Ok(Some(ActiveExecutionEvent::Exited(0)));
6519 }
6520 }
6521 if Instant::now() >= wait_deadline {
6522 return Ok(None);
6523 }
6524 std::thread::sleep(Duration::from_millis(5));
6525 }
6526 }
6527
6528 fn write_descendant_javascript_child_process_stdin(
6529 &mut self,
6530 vm_id: &str,
6531 process_id: &str,
6532 current_process_path: &[&str],
6533 child_process_id: &str,
6534 chunk: &[u8],
6535 ) -> Result<(), SidecarError> {
6536 let mut child_path = current_process_path.to_vec();
6537 child_path.push(child_process_id);
6538 let Some(vm) = self.vms.get_mut(vm_id) else {
6539 return Err(javascript_child_process_gone_error(process_id, &child_path));
6540 };
6541 let Some(root) = vm.active_processes.get_mut(process_id) else {
6542 return Err(javascript_child_process_gone_error(process_id, &child_path));
6543 };
6544 let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6545 return Err(javascript_child_process_gone_error(process_id, &child_path));
6546 };
6547 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6548 return Err(javascript_child_process_gone_error(process_id, &child_path));
6549 };
6550 if let Err(error) = child.execution.write_stdin(chunk) {
6551 if is_broken_pipe_error(&error) {
6552 return Ok(());
6553 }
6554 return Err(error);
6555 }
6556 write_kernel_process_stdin(&mut vm.kernel, child, chunk)
6557 }
6558
6559 fn close_descendant_javascript_child_process_stdin(
6560 &mut self,
6561 vm_id: &str,
6562 process_id: &str,
6563 current_process_path: &[&str],
6564 child_process_id: &str,
6565 ) -> Result<(), SidecarError> {
6566 let mut child_path = current_process_path.to_vec();
6567 child_path.push(child_process_id);
6568 let Some(vm) = self.vms.get_mut(vm_id) else {
6569 return Err(javascript_child_process_gone_error(process_id, &child_path));
6570 };
6571 let Some(root) = vm.active_processes.get_mut(process_id) else {
6572 return Err(javascript_child_process_gone_error(process_id, &child_path));
6573 };
6574 let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6575 return Err(javascript_child_process_gone_error(process_id, &child_path));
6576 };
6577 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6578 return Err(javascript_child_process_gone_error(process_id, &child_path));
6579 };
6580 child.execution.close_stdin()?;
6581 close_kernel_process_stdin(&mut vm.kernel, child)
6582 }
6583
6584 fn kill_descendant_javascript_child_process(
6585 &mut self,
6586 vm_id: &str,
6587 process_id: &str,
6588 current_process_path: &[&str],
6589 child_process_id: &str,
6590 signal: &str,
6591 ) -> Result<(), SidecarError> {
6592 let signal_name = signal.to_owned();
6593 let signal = parse_signal(signal)?;
6594 let Some(vm) = self.vms.get_mut(vm_id) else {
6595 return Ok(());
6596 };
6597 let Some(root) = vm.active_processes.get_mut(process_id) else {
6598 return Ok(());
6599 };
6600 let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6601 return Ok(());
6602 };
6603 let source_pid = parent.kernel_pid;
6604 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6605 return Ok(());
6606 };
6607 terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
6608 let child_process_label = if current_process_path.is_empty() {
6609 child_process_id.to_owned()
6610 } else {
6611 format!("{}/{}", current_process_path.join("/"), child_process_id)
6612 };
6613 emit_security_audit_event(
6614 &self.bridge,
6615 vm_id,
6616 "security.process.kill",
6617 audit_fields([
6618 (String::from("source"), String::from("guest_child_process")),
6619 (String::from("source_pid"), source_pid.to_string()),
6620 (String::from("target_pid"), child.kernel_pid.to_string()),
6621 (String::from("process_id"), process_id.to_owned()),
6622 (String::from("child_process_id"), child_process_label),
6623 (String::from("signal"), signal_name),
6624 ]),
6625 );
6626 Ok(())
6627 }
6628
6629 fn handle_descendant_process_kill_rpc(
6630 &mut self,
6631 vm_id: &str,
6632 process_id: &str,
6633 current_process_path: &[&str],
6634 child_process_id: &str,
6635 request: &JavascriptSyncRpcRequest,
6636 ) -> Result<Value, SidecarError> {
6637 let target_pid = javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
6638 let signal_name = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
6639 let signal = parse_signal(signal_name)?;
6640
6641 let mut source_path = current_process_path.to_vec();
6642 source_path.push(child_process_id);
6643
6644 if signal != 0 && target_pid < 0 {
6645 let pgid = target_pid.unsigned_abs();
6646 let caller_kernel_pid = {
6647 let Some(vm) = self.vms.get(vm_id) else {
6648 return Err(SidecarError::InvalidState(String::from(
6649 "ESRCH: unknown VM during process.kill",
6650 )));
6651 };
6652 let Some(root) = vm.active_processes.get(process_id) else {
6653 return Err(SidecarError::InvalidState(format!(
6654 "ESRCH: unknown process {process_id} during process.kill",
6655 )));
6656 };
6657 let Some(source) = Self::active_process_by_path(root, &source_path) else {
6658 return Err(SidecarError::InvalidState(format!(
6659 "ESRCH: unknown child process {child_process_id} during process.kill",
6660 )));
6661 };
6662 source.kernel_pid
6663 };
6664 let caller_is_member =
6665 self.signal_vm_process_group(vm_id, caller_kernel_pid, pgid, signal_name)?;
6666 if !caller_is_member {
6667 return Ok(Value::Null);
6668 }
6669 let Some(vm) = self.vms.get_mut(vm_id) else {
6670 return Ok(Value::Null);
6671 };
6672 let Some(root) = vm.active_processes.get_mut(process_id) else {
6673 return Ok(Value::Null);
6674 };
6675 let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
6676 return Ok(Value::Null);
6677 };
6678 source.pending_self_signal_exit = None;
6679 if !matches!(
6680 canonical_signal_name(signal),
6681 Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
6682 ) {
6683 source.pending_self_signal_exit = Some(signal);
6684 }
6685 return Ok(json!({
6686 "self": true,
6687 "action": "default",
6688 }));
6689 }
6690
6691 let Some(vm) = self.vms.get_mut(vm_id) else {
6692 return Err(SidecarError::InvalidState(String::from(
6693 "ESRCH: unknown VM during process.kill",
6694 )));
6695 };
6696
6697 if signal == 0 {
6698 vm.kernel
6699 .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
6700 .map_err(kernel_error)?;
6701 return Ok(Value::Null);
6702 }
6703
6704 let target_kernel_pid = u32::try_from(target_pid).map_err(|_| {
6705 SidecarError::InvalidState(format!("EINVAL: invalid process pid {target_pid}"))
6706 })?;
6707 let (source_pid, located_target_path) = {
6708 let Some(root) = vm.active_processes.get(process_id) else {
6709 return Err(SidecarError::InvalidState(format!(
6710 "ESRCH: unknown process {process_id} during process.kill",
6711 )));
6712 };
6713 let Some(source) = Self::active_process_by_path(root, &source_path) else {
6714 return Err(SidecarError::InvalidState(format!(
6715 "ESRCH: unknown child process {child_process_id} during process.kill",
6716 )));
6717 };
6718 vm.kernel
6719 .signal_process(EXECUTION_DRIVER_NAME, target_pid, 0)
6720 .map_err(kernel_error)?;
6721 (
6722 source.kernel_pid,
6723 Self::active_process_path_by_kernel_pid(root, target_kernel_pid),
6724 )
6725 };
6726 let Some(target_path) = located_target_path else {
6727 self.signal_vm_kernel_pid(vm_id, target_kernel_pid, signal_name)?;
6731 return Ok(Value::Null);
6732 };
6733 let Some(vm) = self.vms.get_mut(vm_id) else {
6734 return Err(SidecarError::InvalidState(String::from(
6735 "ESRCH: unknown VM during process.kill",
6736 )));
6737 };
6738
6739 if source_pid == target_kernel_pid {
6740 let Some(root) = vm.active_processes.get_mut(process_id) else {
6741 return Ok(Value::Null);
6742 };
6743 let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
6744 return Ok(Value::Null);
6745 };
6746 source.pending_self_signal_exit = None;
6747 if !matches!(
6748 canonical_signal_name(signal),
6749 Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
6750 ) {
6751 source.pending_self_signal_exit = Some(signal);
6752 }
6753 return Ok(json!({
6754 "self": true,
6755 "action": "default",
6756 }));
6757 }
6758
6759 let signal_key = target_path.last().map(String::as_str).unwrap_or(process_id);
6760 let registration = vm
6761 .signal_states
6762 .get(signal_key)
6763 .and_then(|handlers| handlers.get(&(signal as u32)))
6764 .cloned();
6765
6766 let action = match registration
6767 .as_ref()
6768 .map(|registration| ®istration.action)
6769 {
6770 Some(SignalDispositionAction::Ignore) => "ignore",
6771 Some(SignalDispositionAction::User) => {
6772 let Some(root) = vm.active_processes.get_mut(process_id) else {
6773 return Ok(Value::Null);
6774 };
6775 let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
6776 else {
6777 return Err(SidecarError::InvalidState(format!(
6778 "ESRCH: unknown process pid {target_pid}"
6779 )));
6780 };
6781 if let Some(session) = target.execution.javascript_v8_session_handle().filter(
6782 |_| matches!(&target.execution, ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime())
6783 || matches!(&target.execution, ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime()),
6784 ) {
6785 dispatch_v8_session_signal_async(session, signal);
6786 } else if !dispatch_v8_process_signal(target, signal)? {
6787 return Err(SidecarError::InvalidState(format!(
6788 "unsupported guest signal delivery for pid {target_pid}"
6789 )));
6790 }
6791 "user"
6792 }
6793 Some(SignalDispositionAction::Default) | None
6794 if matches!(
6795 canonical_signal_name(signal),
6796 Some("SIGWINCH" | "SIGCHLD" | "SIGURG")
6797 ) =>
6798 {
6799 "ignore"
6800 }
6801 Some(SignalDispositionAction::Default) | None => {
6802 let Some(root) = vm.active_processes.get_mut(process_id) else {
6803 return Ok(Value::Null);
6804 };
6805 let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
6806 else {
6807 return Err(SidecarError::InvalidState(format!(
6808 "ESRCH: unknown process pid {target_pid}"
6809 )));
6810 };
6811 apply_active_process_default_signal(&mut vm.kernel, target, signal)?;
6812 "default"
6813 }
6814 };
6815
6816 let target_path_label = Self::child_process_path_label(
6817 process_id,
6818 &target_path.iter().map(String::as_str).collect::<Vec<_>>(),
6819 );
6820 emit_security_audit_event(
6821 &self.bridge,
6822 vm_id,
6823 "security.process.kill",
6824 audit_fields([
6825 (String::from("source"), String::from("guest_process")),
6826 (String::from("source_pid"), source_pid.to_string()),
6827 (String::from("target_pid"), target_pid.to_string()),
6828 (String::from("process_id"), process_id.to_owned()),
6829 (
6830 String::from("target_process_path"),
6831 target_path_label.clone(),
6832 ),
6833 (String::from("signal"), signal_name.to_owned()),
6834 ]),
6835 );
6836
6837 Ok(json!({
6838 "self": false,
6839 "action": action,
6840 "signal": signal_name,
6841 "number": signal,
6842 "targetProcessPath": target_path_label,
6843 }))
6844 }
6845
6846 pub(crate) fn poll_javascript_child_process(
6847 &mut self,
6848 vm_id: &str,
6849 process_id: &str,
6850 child_process_id: &str,
6851 wait_ms: u64,
6852 ) -> Result<Value, SidecarError> {
6853 self.poll_descendant_javascript_child_process(
6854 vm_id,
6855 process_id,
6856 &[],
6857 child_process_id,
6858 wait_ms,
6859 )
6860 }
6861
6862 pub(crate) fn write_javascript_child_process_stdin(
6863 &mut self,
6864 vm_id: &str,
6865 process_id: &str,
6866 child_process_id: &str,
6867 chunk: &[u8],
6868 ) -> Result<(), SidecarError> {
6869 let Some(vm) = self.vms.get_mut(vm_id) else {
6870 return Err(javascript_child_process_gone_error(
6871 process_id,
6872 &[child_process_id],
6873 ));
6874 };
6875 let Some(child) = vm
6876 .active_processes
6877 .get_mut(process_id)
6878 .ok_or_else(|| missing_process_error(vm_id, process_id))?
6879 .child_processes
6880 .get_mut(child_process_id)
6881 else {
6882 return Err(javascript_child_process_gone_error(
6883 process_id,
6884 &[child_process_id],
6885 ));
6886 };
6887 if let Err(error) = child.execution.write_stdin(chunk) {
6888 if is_broken_pipe_error(&error) {
6889 return Ok(());
6890 }
6891 return Err(error);
6892 }
6893 write_kernel_process_stdin(&mut vm.kernel, child, chunk)
6894 }
6895
6896 pub(crate) fn close_javascript_child_process_stdin(
6897 &mut self,
6898 vm_id: &str,
6899 process_id: &str,
6900 child_process_id: &str,
6901 ) -> Result<(), SidecarError> {
6902 let Some(vm) = self.vms.get_mut(vm_id) else {
6903 return Err(javascript_child_process_gone_error(
6904 process_id,
6905 &[child_process_id],
6906 ));
6907 };
6908 let Some(child) = vm
6909 .active_processes
6910 .get_mut(process_id)
6911 .ok_or_else(|| missing_process_error(vm_id, process_id))?
6912 .child_processes
6913 .get_mut(child_process_id)
6914 else {
6915 return Err(javascript_child_process_gone_error(
6916 process_id,
6917 &[child_process_id],
6918 ));
6919 };
6920 child.execution.close_stdin()?;
6921 close_kernel_process_stdin(&mut vm.kernel, child)
6922 }
6923
6924 pub(crate) fn kill_javascript_child_process(
6925 &mut self,
6926 vm_id: &str,
6927 process_id: &str,
6928 child_process_id: &str,
6929 signal: &str,
6930 ) -> Result<(), SidecarError> {
6931 let signal_name = signal.to_owned();
6932 let signal = parse_signal(signal)?;
6933 let Some(vm) = self.vms.get_mut(vm_id) else {
6934 return Ok(());
6935 };
6936 let process = vm
6937 .active_processes
6938 .get_mut(process_id)
6939 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
6940 let source_pid = process.kernel_pid;
6941 let child = process
6942 .child_processes
6943 .get_mut(child_process_id)
6944 .ok_or_else(|| {
6945 SidecarError::InvalidState(format!(
6946 "unknown child process {child_process_id} during kill"
6947 ))
6948 })?;
6949 terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
6950 emit_security_audit_event(
6951 &self.bridge,
6952 vm_id,
6953 "security.process.kill",
6954 audit_fields([
6955 (String::from("source"), String::from("guest_child_process")),
6956 (String::from("source_pid"), source_pid.to_string()),
6957 (String::from("target_pid"), child.kernel_pid.to_string()),
6958 (String::from("process_id"), process_id.to_owned()),
6959 (
6960 String::from("child_process_id"),
6961 child_process_id.to_owned(),
6962 ),
6963 (String::from("signal"), signal_name),
6964 ]),
6965 );
6966 Ok(())
6967 }
6968
6969 pub(crate) fn signal_vm_kernel_pid(
6975 &mut self,
6976 vm_id: &str,
6977 target_kernel_pid: u32,
6978 signal_name: &str,
6979 ) -> Result<(), SidecarError> {
6980 let signal = parse_signal(signal_name)?;
6981 let located = {
6982 let Some(vm) = self.vms.get(vm_id) else {
6983 return Err(SidecarError::InvalidState(String::from(
6984 "ESRCH: unknown VM during process.kill",
6985 )));
6986 };
6987 let alive = vm
6988 .kernel
6989 .list_processes()
6990 .get(&target_kernel_pid)
6991 .is_some_and(|info| info.status != ProcessStatus::Exited);
6992 if !alive {
6993 return Err(SidecarError::InvalidState(format!(
6994 "ESRCH: no such process {target_kernel_pid}"
6995 )));
6996 }
6997 vm.active_processes.iter().find_map(|(process_id, root)| {
6998 Self::active_process_path_by_kernel_pid(root, target_kernel_pid)
6999 .map(|path| (process_id.clone(), path))
7000 })
7001 };
7002
7003 match located {
7004 Some((process_id, path)) if path.is_empty() => {
7005 self.kill_process_internal(vm_id, &process_id, signal_name)
7006 }
7007 Some((process_id, path)) => {
7008 let Some(vm) = self.vms.get_mut(vm_id) else {
7009 return Ok(());
7010 };
7011 let Some(root) = vm.active_processes.get_mut(&process_id) else {
7012 return Ok(());
7013 };
7014 let Some(target) = Self::active_process_by_owned_path_mut(root, &path) else {
7015 return Err(SidecarError::InvalidState(format!(
7016 "ESRCH: no such process {target_kernel_pid}"
7017 )));
7018 };
7019 terminate_tracked_child_process_for_signal(&mut vm.kernel, target, signal)?;
7020 emit_security_audit_event(
7021 &self.bridge,
7022 vm_id,
7023 "security.process.kill",
7024 audit_fields([
7025 (String::from("source"), String::from("guest_process")),
7026 (String::from("target_pid"), target_kernel_pid.to_string()),
7027 (String::from("process_id"), process_id),
7028 (String::from("signal"), signal_name.to_owned()),
7029 ]),
7030 );
7031 Ok(())
7032 }
7033 None => {
7034 let Some(vm) = self.vms.get_mut(vm_id) else {
7035 return Ok(());
7036 };
7037 let target_pid = i32::try_from(target_kernel_pid).map_err(|_| {
7038 SidecarError::InvalidState(format!(
7039 "EINVAL: invalid process pid {target_kernel_pid}"
7040 ))
7041 })?;
7042 vm.kernel
7043 .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
7044 .map_err(kernel_error)?;
7045 emit_security_audit_event(
7046 &self.bridge,
7047 vm_id,
7048 "security.process.kill",
7049 audit_fields([
7050 (String::from("source"), String::from("guest_process")),
7051 (String::from("target_pid"), target_kernel_pid.to_string()),
7052 (String::from("signal"), signal_name.to_owned()),
7053 ]),
7054 );
7055 Ok(())
7056 }
7057 }
7058 }
7059
7060 pub(crate) fn signal_vm_process_group(
7065 &mut self,
7066 vm_id: &str,
7067 caller_kernel_pid: u32,
7068 pgid: u32,
7069 signal_name: &str,
7070 ) -> Result<bool, SidecarError> {
7071 parse_signal(signal_name)?;
7072 let members = {
7073 let Some(vm) = self.vms.get(vm_id) else {
7074 return Err(SidecarError::InvalidState(String::from(
7075 "ESRCH: unknown VM during process.kill",
7076 )));
7077 };
7078 vm.kernel
7079 .list_processes()
7080 .into_iter()
7081 .filter(|(_, info)| info.pgid == pgid && info.status != ProcessStatus::Exited)
7082 .map(|(pid, _)| pid)
7083 .collect::<Vec<_>>()
7084 };
7085 if members.is_empty() {
7086 return Err(SidecarError::InvalidState(format!(
7087 "ESRCH: no such process group {pgid}"
7088 )));
7089 }
7090
7091 let mut caller_is_member = false;
7092 for member_pid in members {
7093 if member_pid == caller_kernel_pid {
7094 caller_is_member = true;
7095 continue;
7096 }
7097 match self.signal_vm_kernel_pid(vm_id, member_pid, signal_name) {
7098 Ok(()) => {}
7099 Err(error) if sidecar_error_is_esrch(&error) => {}
7102 Err(error) => return Err(error),
7103 }
7104 }
7105 Ok(caller_is_member)
7106 }
7107}
7108
7109fn terminate_tracked_child_process_for_signal(
7114 kernel: &mut SidecarKernel,
7115 child: &mut ActiveProcess,
7116 signal: i32,
7117) -> Result<(), SidecarError> {
7118 let should_terminate_shared_runtime = child.execution.uses_shared_v8_runtime()
7119 && signal != 0
7120 && !matches!(
7121 signal,
7122 libc::SIGHUP
7123 | libc::SIGINT
7124 | libc::SIGTERM
7125 | libc::SIGCHLD
7126 | libc::SIGWINCH
7127 | libc::SIGSTOP
7128 | libc::SIGCONT
7129 );
7130 if should_terminate_shared_runtime {
7131 child.execution.terminate()?;
7132 child.pending_self_signal_exit = Some(signal);
7133 child.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
7134 } else {
7135 kernel
7136 .kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, signal)
7137 .map_err(kernel_error)?;
7138 }
7139 Ok(())
7140}
7141
7142fn sidecar_error_is_esrch(error: &SidecarError) -> bool {
7143 error.to_string().contains("ESRCH")
7144}
7145
7146fn apply_active_process_default_signal(
7147 kernel: &mut SidecarKernel,
7148 process: &mut ActiveProcess,
7149 signal: i32,
7150) -> Result<(), SidecarError> {
7151 if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
7152 return kernel
7153 .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
7154 .map_err(kernel_error);
7155 }
7156
7157 if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
7158 close_kernel_process_stdin(kernel, process)?;
7159 }
7160
7161 if process.execution.uses_shared_v8_runtime() {
7162 process.execution.terminate()?;
7163 if signal != 0 && matches!(process.execution, ActiveExecution::Wasm(_)) {
7164 process.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
7165 }
7166 return Ok(());
7167 }
7168
7169 kernel
7170 .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
7171 .map_err(kernel_error)
7172}
7173
7174fn map_wasm_signal_registration(
7175 registration: secure_exec_execution::wasm::WasmSignalHandlerRegistration,
7176) -> SignalHandlerRegistration {
7177 SignalHandlerRegistration {
7178 action: match registration.action {
7179 secure_exec_execution::wasm::WasmSignalDispositionAction::Default => {
7180 crate::protocol::SignalDispositionAction::Default
7181 }
7182 secure_exec_execution::wasm::WasmSignalDispositionAction::Ignore => {
7183 crate::protocol::SignalDispositionAction::Ignore
7184 }
7185 secure_exec_execution::wasm::WasmSignalDispositionAction::User => {
7186 crate::protocol::SignalDispositionAction::User
7187 }
7188 },
7189 mask: registration.mask,
7190 flags: registration.flags,
7191 }
7192}
7193
7194fn parse_process_signal_state_request(
7195 args: &[Value],
7196) -> Result<(u32, SignalHandlerRegistration), SidecarError> {
7197 let signal = javascript_sync_rpc_arg_u32(args, 0, "process.signal_state signal")?;
7198 let action = javascript_sync_rpc_arg_str(args, 1, "process.signal_state action")?;
7199 let mask_json = javascript_sync_rpc_arg_str(args, 2, "process.signal_state mask")?;
7200 let flags = javascript_sync_rpc_arg_u32(args, 3, "process.signal_state flags")?;
7201 let mask: Vec<u32> = serde_json::from_str(mask_json).map_err(|error| {
7202 SidecarError::InvalidState(format!(
7203 "process.signal_state mask must be valid JSON: {error}"
7204 ))
7205 })?;
7206 let action = match action.trim().to_ascii_lowercase().as_str() {
7207 "default" => SignalDispositionAction::Default,
7208 "ignore" => SignalDispositionAction::Ignore,
7209 "user" => SignalDispositionAction::User,
7210 other => {
7211 return Err(SidecarError::InvalidState(format!(
7212 "unsupported process.signal_state action {other}"
7213 )));
7214 }
7215 };
7216
7217 Ok((
7218 signal,
7219 SignalHandlerRegistration {
7220 action,
7221 mask,
7222 flags,
7223 },
7224 ))
7225}
7226
7227fn apply_process_signal_state_update(
7228 signal_states: &mut BTreeMap<String, BTreeMap<u32, SignalHandlerRegistration>>,
7229 process_id: &str,
7230 signal: u32,
7231 registration: SignalHandlerRegistration,
7232) {
7233 if registration.action == SignalDispositionAction::Default
7234 && registration.mask.is_empty()
7235 && registration.flags == 0
7236 {
7237 let remove_process_entry = signal_states
7238 .get_mut(process_id)
7239 .map(|handlers| {
7240 handlers.remove(&signal);
7241 handlers.is_empty()
7242 })
7243 .unwrap_or(false);
7244 if remove_process_entry {
7245 signal_states.remove(process_id);
7246 }
7247 return;
7248 }
7249
7250 signal_states
7251 .entry(process_id.to_owned())
7252 .or_default()
7253 .insert(signal, registration);
7254}
7255
7256fn map_node_signal_registration(
7257 registration: NodeSignalHandlerRegistration,
7258) -> SignalHandlerRegistration {
7259 SignalHandlerRegistration {
7260 action: match registration.action {
7261 NodeSignalDispositionAction::Default => SignalDispositionAction::Default,
7262 NodeSignalDispositionAction::Ignore => SignalDispositionAction::Ignore,
7263 NodeSignalDispositionAction::User => SignalDispositionAction::User,
7264 },
7265 mask: registration.mask,
7266 flags: registration.flags,
7267 }
7268}
7269
7270fn javascript_child_process_sync_input_bytes(
7271 value: Option<&Value>,
7272) -> Result<Option<Vec<u8>>, SidecarError> {
7273 let Some(value) = value else {
7274 return Ok(None);
7275 };
7276
7277 match value {
7278 Value::Null => Ok(None),
7279 Value::String(text) => Ok(Some(text.as_bytes().to_vec())),
7280 other => javascript_sync_rpc_bytes_arg(
7281 std::slice::from_ref(other),
7282 0,
7283 "child_process.spawn_sync input",
7284 )
7285 .map(Some),
7286 }
7287}
7288
7289fn resolve_execute_request(
7294 vm: &VmState,
7295 payload: &ExecuteRequest,
7296) -> Result<ResolvedChildProcessExecution, SidecarError> {
7297 let payload_env: BTreeMap<String, String> = payload
7298 .env
7299 .iter()
7300 .map(|(k, v)| (k.clone(), v.clone()))
7301 .collect();
7302 if let Some(command) = payload.command.as_deref() {
7303 return resolve_command_execution(
7304 vm,
7305 command,
7306 &payload.args,
7307 &payload_env,
7308 payload.cwd.as_deref(),
7309 payload.wasm_permission_tier,
7310 );
7311 }
7312
7313 let runtime = payload.runtime.clone().ok_or_else(|| {
7314 SidecarError::InvalidState(String::from("execute requires either command or runtime"))
7315 })?;
7316 let entrypoint = payload.entrypoint.clone().ok_or_else(|| {
7317 SidecarError::InvalidState(String::from(
7318 "execute requires either command or entrypoint",
7319 ))
7320 })?;
7321 let (guest_cwd, host_cwd, allow_host_path_overrides) =
7322 resolve_execution_cwds(vm, payload.cwd.as_deref());
7323 let mut env = vm.guest_env.clone();
7324 env.extend(payload_env.clone());
7325
7326 let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint);
7327 if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7328 let requested_cwd = payload.cwd.as_deref().unwrap_or(guest_cwd.as_str());
7329 return Err(SidecarError::InvalidState(format!(
7330 "execution cwd {requested_cwd} is outside sandbox root {}",
7331 vm.host_cwd.to_string_lossy()
7332 )));
7333 }
7334 let host_entrypoint_override = allow_host_path_overrides
7335 .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint))
7336 .flatten();
7337
7338 let guest_entrypoint = host_entrypoint_override
7339 .as_ref()
7340 .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7341 .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, &entrypoint));
7342 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7343
7344 Ok(ResolvedChildProcessExecution {
7345 command: match runtime {
7346 GuestRuntimeKind::JavaScript => String::from(JAVASCRIPT_COMMAND),
7347 GuestRuntimeKind::Python => String::from(PYTHON_COMMAND),
7348 GuestRuntimeKind::WebAssembly => String::from(WASM_COMMAND),
7349 },
7350 process_args: std::iter::once(entrypoint.clone())
7351 .chain(payload.args.iter().cloned())
7352 .collect(),
7353 runtime,
7354 entrypoint: host_entrypoint_override
7355 .map(|(_, host_entrypoint)| host_entrypoint)
7356 .unwrap_or(entrypoint),
7357 execution_args: payload.args.clone(),
7358 env,
7359 guest_cwd,
7360 host_cwd,
7361 wasm_permission_tier: payload.wasm_permission_tier,
7362 tool_command: false,
7363 })
7364}
7365
7366fn resolve_command_execution(
7367 vm: &VmState,
7368 command: &str,
7369 args: &[String],
7370 extra_env: &BTreeMap<String, String>,
7371 cwd: Option<&str>,
7372 explicit_wasm_permission_tier: Option<WasmPermissionTier>,
7373) -> Result<ResolvedChildProcessExecution, SidecarError> {
7374 let (guest_cwd, host_cwd, allow_host_path_overrides) = resolve_execution_cwds(vm, cwd);
7375 let mut env = vm.guest_env.clone();
7376 env.extend(extra_env.clone());
7377 let args = apply_shell_cwd_prefix(command, args.to_vec(), &guest_cwd);
7378
7379 if is_tool_command(vm, command) {
7380 let command = normalized_tool_command_name(command).unwrap_or_else(|| command.to_owned());
7381 return Ok(ResolvedChildProcessExecution {
7382 command: command.clone(),
7383 process_args: std::iter::once(command.clone())
7384 .chain(args.iter().cloned())
7385 .collect(),
7386 runtime: GuestRuntimeKind::JavaScript,
7387 entrypoint: command,
7388 execution_args: args,
7389 env,
7390 guest_cwd,
7391 host_cwd,
7392 wasm_permission_tier: None,
7393 tool_command: true,
7394 });
7395 }
7396
7397 if is_node_runtime_command(command) {
7398 if let Some(cli) = resolve_host_node_cli_entrypoint(command) {
7399 env.insert(
7400 String::from("AGENT_OS_NODE_EVAL"),
7401 build_host_node_cli_eval(&cli),
7402 );
7403 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7404 add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
7405 add_runtime_host_access_path(
7406 &mut env,
7407 "AGENT_OS_EXTRA_FS_READ_PATHS",
7408 &cli.package_root,
7409 true,
7410 );
7411
7412 return Ok(ResolvedChildProcessExecution {
7413 command: String::from(JAVASCRIPT_COMMAND),
7414 process_args: std::iter::once(command.to_owned())
7415 .chain(args.iter().cloned())
7416 .collect(),
7417 runtime: GuestRuntimeKind::JavaScript,
7418 entrypoint: String::from("-e"),
7419 execution_args: std::iter::once(cli.guest_entrypoint.clone())
7420 .chain(args.iter().cloned())
7421 .collect(),
7422 env,
7423 guest_cwd,
7424 host_cwd,
7425 wasm_permission_tier: None,
7426 tool_command: false,
7427 });
7428 }
7429
7430 if args.is_empty() {
7431 env.insert(String::from("AGENT_OS_NODE_EVAL"), String::new());
7432 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7433
7434 return Ok(ResolvedChildProcessExecution {
7435 command: String::from(JAVASCRIPT_COMMAND),
7436 process_args: vec![command.to_owned()],
7437 runtime: GuestRuntimeKind::JavaScript,
7438 entrypoint: String::from("-e"),
7439 execution_args: Vec::new(),
7440 env,
7441 guest_cwd,
7442 host_cwd,
7443 wasm_permission_tier: None,
7444 tool_command: false,
7445 });
7446 }
7447
7448 if let Some((entrypoint, execution_args)) =
7449 resolve_special_node_cli_invocation(&args, &mut env)
7450 {
7451 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7452
7453 return Ok(ResolvedChildProcessExecution {
7454 command: String::from(JAVASCRIPT_COMMAND),
7455 process_args: std::iter::once(command.to_owned())
7456 .chain(args.iter().cloned())
7457 .collect(),
7458 runtime: GuestRuntimeKind::JavaScript,
7459 entrypoint,
7460 execution_args,
7461 env,
7462 guest_cwd,
7463 host_cwd,
7464 wasm_permission_tier: None,
7465 tool_command: false,
7466 });
7467 }
7468
7469 let Some(entrypoint_specifier) = args.first() else {
7470 return Err(SidecarError::InvalidState(format!(
7471 "{command} execution requires an entrypoint"
7472 )));
7473 };
7474
7475 let (entrypoint, execution_args, guest_entrypoint) = {
7476 let requested_host_entrypoint =
7477 resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier);
7478 if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7479 let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
7480 return Err(SidecarError::InvalidState(format!(
7481 "execution cwd {requested_cwd} is outside sandbox root {}",
7482 vm.host_cwd.to_string_lossy()
7483 )));
7484 }
7485 let host_entrypoint_override = allow_host_path_overrides
7486 .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier))
7487 .flatten();
7488 let guest_entrypoint = host_entrypoint_override
7489 .as_ref()
7490 .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7491 .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, entrypoint_specifier));
7492 let entrypoint = host_entrypoint_override.map_or_else(
7493 || {
7494 guest_entrypoint.as_ref().map_or_else(
7495 || entrypoint_specifier.clone(),
7496 |guest_entrypoint| {
7497 resolve_vm_guest_path_to_host(vm, guest_entrypoint)
7498 .to_string_lossy()
7499 .into_owned()
7500 },
7501 )
7502 },
7503 |(_, host_entrypoint)| host_entrypoint,
7504 );
7505 (
7506 entrypoint,
7507 args.iter().skip(1).cloned().collect(),
7508 guest_entrypoint,
7509 )
7510 };
7511
7512 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7513
7514 return Ok(ResolvedChildProcessExecution {
7515 command: String::from(JAVASCRIPT_COMMAND),
7516 process_args: std::iter::once(command.to_owned())
7517 .chain(args.iter().cloned())
7518 .collect(),
7519 runtime: GuestRuntimeKind::JavaScript,
7520 entrypoint,
7521 execution_args,
7522 env,
7523 guest_cwd,
7524 host_cwd,
7525 wasm_permission_tier: None,
7526 tool_command: false,
7527 });
7528 }
7529
7530 if command.ends_with(".js") || command.ends_with(".mjs") || command.ends_with(".cjs") {
7531 let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, command);
7532 if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7533 let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
7534 return Err(SidecarError::InvalidState(format!(
7535 "execution cwd {requested_cwd} is outside sandbox root {}",
7536 vm.host_cwd.to_string_lossy()
7537 )));
7538 }
7539 let host_entrypoint_override = allow_host_path_overrides
7540 .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, command))
7541 .flatten();
7542 let guest_entrypoint = host_entrypoint_override
7543 .as_ref()
7544 .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7545 .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, command));
7546 let entrypoint = host_entrypoint_override.map_or_else(
7547 || {
7548 guest_entrypoint.as_ref().map_or_else(
7549 || command.to_owned(),
7550 |guest_entrypoint| {
7551 resolve_vm_guest_path_to_host(vm, guest_entrypoint)
7552 .to_string_lossy()
7553 .into_owned()
7554 },
7555 )
7556 },
7557 |(_, host_entrypoint)| host_entrypoint,
7558 );
7559 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7560
7561 return Ok(ResolvedChildProcessExecution {
7562 command: String::from(JAVASCRIPT_COMMAND),
7563 process_args: std::iter::once(command.to_owned())
7564 .chain(args.iter().cloned())
7565 .collect(),
7566 runtime: GuestRuntimeKind::JavaScript,
7567 entrypoint,
7568 execution_args: args.to_vec(),
7569 env,
7570 guest_cwd,
7571 host_cwd,
7572 wasm_permission_tier: None,
7573 tool_command: false,
7574 });
7575 }
7576
7577 let guest_entrypoint = resolve_guest_command_entrypoint(
7578 vm,
7579 &guest_cwd,
7580 command,
7581 env.get("PATH").map(String::as_str),
7582 )
7583 .ok_or_else(|| {
7584 SidecarError::InvalidState(format!(
7585 "command not found on native sidecar path: {command}"
7586 ))
7587 })?;
7588 let wasm_permission_tier = explicit_wasm_permission_tier
7589 .or_else(|| vm.command_permissions.get(command).copied())
7590 .or_else(|| {
7591 Path::new(&guest_entrypoint)
7592 .file_name()
7593 .and_then(|name| name.to_str())
7594 .and_then(|name| vm.command_permissions.get(name).copied())
7595 });
7596
7597 let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
7598 if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
7599 resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
7600 {
7601 prepare_guest_runtime_env(
7602 vm,
7603 &mut env,
7604 &guest_cwd,
7605 &host_cwd,
7606 Some(javascript_guest_entrypoint),
7607 )?;
7608
7609 return Ok(ResolvedChildProcessExecution {
7610 command: command.to_owned(),
7611 process_args: std::iter::once(command.to_owned())
7612 .chain(args.iter().cloned())
7613 .collect(),
7614 runtime: GuestRuntimeKind::JavaScript,
7615 entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
7616 execution_args: args.to_vec(),
7617 env,
7618 guest_cwd,
7619 host_cwd,
7620 wasm_permission_tier: None,
7621 tool_command: false,
7622 });
7623 }
7624 prepare_guest_runtime_env(
7625 vm,
7626 &mut env,
7627 &guest_cwd,
7628 &host_cwd,
7629 Some(guest_entrypoint.clone()),
7630 )?;
7631
7632 Ok(ResolvedChildProcessExecution {
7633 command: command.to_owned(),
7634 process_args: std::iter::once(command.to_owned())
7635 .chain(args.iter().cloned())
7636 .collect(),
7637 runtime: GuestRuntimeKind::WebAssembly,
7638 entrypoint: host_entrypoint.to_string_lossy().into_owned(),
7639 execution_args: args.to_vec(),
7640 env,
7641 guest_cwd,
7642 host_cwd,
7643 wasm_permission_tier,
7644 tool_command: false,
7645 })
7646}
7647
7648const MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH: usize = 4;
7649
7650fn resolve_javascript_command_entrypoint(
7651 vm: &VmState,
7652 guest_entrypoint: &str,
7653 host_entrypoint: &Path,
7654) -> Option<(String, PathBuf)> {
7655 resolve_javascript_command_entrypoint_inner(
7656 vm,
7657 guest_entrypoint,
7658 host_entrypoint,
7659 MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH,
7660 )
7661}
7662
7663fn resolve_javascript_command_entrypoint_inner(
7664 vm: &VmState,
7665 guest_entrypoint: &str,
7666 host_entrypoint: &Path,
7667 redirects_remaining: usize,
7668) -> Option<(String, PathBuf)> {
7669 if redirects_remaining > 0 {
7670 let symlink_target = fs::symlink_metadata(host_entrypoint)
7671 .ok()
7672 .filter(|metadata| metadata.file_type().is_symlink())
7673 .and_then(|_| fs::read_link(host_entrypoint).ok());
7674 if let Some(symlink_target) = symlink_target {
7675 let guest_parent = Path::new(guest_entrypoint)
7676 .parent()
7677 .and_then(|path| path.to_str())
7678 .unwrap_or("/");
7679 let symlink_guest_entrypoint = if symlink_target.is_absolute() {
7680 normalize_path(&symlink_target.to_string_lossy())
7681 } else {
7682 normalize_path(&format!(
7683 "{guest_parent}/{}",
7684 symlink_target.to_string_lossy().replace('\\', "/")
7685 ))
7686 };
7687 let symlink_host_entrypoint =
7688 resolve_vm_guest_path_to_host(vm, &symlink_guest_entrypoint);
7689 return resolve_javascript_command_entrypoint_inner(
7690 vm,
7691 &symlink_guest_entrypoint,
7692 &symlink_host_entrypoint,
7693 redirects_remaining - 1,
7694 );
7695 }
7696 }
7697
7698 let script = load_executable_script_preview(host_entrypoint)?;
7699 let interpreter = parse_script_interpreter_name(&script);
7700
7701 if interpreter.is_none() && is_probable_javascript_entrypoint(host_entrypoint, &script) {
7702 return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
7703 }
7704
7705 let interpreter = interpreter?;
7706 if interpreter == "node" {
7707 return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
7708 }
7709
7710 if redirects_remaining == 0 || !matches!(interpreter.as_str(), "sh" | "bash" | "dash") {
7711 return None;
7712 }
7713
7714 let shim_target = parse_node_shell_shim_target(&script)?;
7715 let guest_parent = Path::new(guest_entrypoint)
7716 .parent()
7717 .and_then(|path| path.to_str())
7718 .unwrap_or("/");
7719 let shim_guest_entrypoint = normalize_path(&format!("{guest_parent}/{shim_target}"));
7720 let shim_host_entrypoint = resolve_vm_guest_path_to_host(vm, &shim_guest_entrypoint);
7721 resolve_javascript_command_entrypoint_inner(
7722 vm,
7723 &shim_guest_entrypoint,
7724 &shim_host_entrypoint,
7725 redirects_remaining - 1,
7726 )
7727}
7728
7729fn load_executable_script_preview(path: &Path) -> Option<String> {
7730 let bytes = fs::read(path).ok()?;
7731 let preview_len = bytes.len().min(16 * 1024);
7732 Some(String::from_utf8_lossy(&bytes[..preview_len]).into_owned())
7733}
7734
7735fn parse_script_interpreter_name(script: &str) -> Option<String> {
7736 let shebang = script.lines().next()?.strip_prefix("#!")?.trim();
7737 let mut tokens = shebang.split_whitespace();
7738 let command = tokens.next()?;
7739 let command_name = Path::new(command).file_name()?.to_str()?;
7740 if command_name == "env" {
7741 for token in tokens {
7742 if token.starts_with('-') {
7743 continue;
7744 }
7745 return Path::new(token)
7746 .file_name()
7747 .and_then(|name| name.to_str())
7748 .map(ToOwned::to_owned);
7749 }
7750 return None;
7751 }
7752
7753 Some(command_name.to_owned())
7754}
7755
7756fn parse_node_shell_shim_target(script: &str) -> Option<String> {
7757 for line in script.lines() {
7758 let trimmed = line.trim();
7759 if !trimmed.starts_with("exec ") {
7760 continue;
7761 }
7762
7763 let mut remaining = trimmed;
7764 while let Some(start) = remaining.find("\"$basedir/") {
7765 let after_prefix = &remaining[start + "\"$basedir/".len()..];
7766 let end = after_prefix.find('"')?;
7767 let candidate = &after_prefix[..end];
7768 remaining = &after_prefix[end + 1..];
7769
7770 if candidate.is_empty() || candidate == "node" || candidate.ends_with("/node") {
7771 continue;
7772 }
7773
7774 return Some(candidate.to_owned());
7775 }
7776 }
7777
7778 None
7779}
7780
7781fn is_probable_javascript_entrypoint(path: &Path, script: &str) -> bool {
7782 let extension = path
7783 .extension()
7784 .and_then(|value| value.to_str())
7785 .unwrap_or_default();
7786 if matches!(extension, "js" | "cjs" | "mjs") {
7787 return true;
7788 }
7789
7790 if !path
7791 .components()
7792 .any(|component| component.as_os_str() == "node_modules")
7793 {
7794 return false;
7795 }
7796
7797 let preview = script.trim_start_matches('\u{feff}').trim_start();
7798 !preview.is_empty()
7799 && !preview.starts_with("#!")
7800 && (preview.starts_with("\"use strict\"")
7801 || preview.starts_with("'use strict'")
7802 || preview.starts_with("import ")
7803 || preview.starts_with("export ")
7804 || preview.starts_with("const ")
7805 || preview.starts_with("let ")
7806 || preview.starts_with("var ")
7807 || preview.starts_with("Object.defineProperty(exports")
7808 || preview.starts_with("module.exports")
7809 || preview.starts_with("require("))
7810}
7811
7812fn resolve_guest_execution_cwd(vm: &VmState, value: Option<&str>) -> String {
7813 value
7814 .map(normalize_path)
7815 .unwrap_or_else(|| vm.guest_cwd.clone())
7816}
7817
7818fn resolve_execution_cwds(vm: &VmState, value: Option<&str>) -> (String, PathBuf, bool) {
7819 if let Some(raw_cwd) = value {
7820 let normalized_vm_host_cwd = normalize_host_path(&vm.host_cwd);
7821 let requested_host_cwd = normalize_host_path(Path::new(raw_cwd));
7822 if path_is_within_root(&requested_host_cwd, &normalized_vm_host_cwd) {
7823 let relative = requested_host_cwd
7824 .strip_prefix(&normalized_vm_host_cwd)
7825 .unwrap_or_else(|_| Path::new(""));
7826 let relative = relative.to_string_lossy().replace('\\', "/");
7827 let guest_cwd = if relative.is_empty() {
7828 String::from("/")
7829 } else {
7830 normalize_path(&format!("/{relative}"))
7831 };
7832 return (guest_cwd, requested_host_cwd, true);
7833 }
7834 }
7835
7836 let guest_cwd = resolve_guest_execution_cwd(vm, value);
7837 let host_cwd = if value.is_none() {
7838 vm.host_cwd.clone()
7839 } else {
7840 resolve_vm_guest_path_to_host(vm, &guest_cwd)
7841 };
7842 (guest_cwd, host_cwd, value.is_none())
7843}
7844
7845fn resolve_vm_guest_path_to_host(vm: &VmState, guest_path: &str) -> PathBuf {
7846 host_mount_path_for_guest_path(vm, guest_path)
7847 .unwrap_or_else(|| shadow_path_for_guest(vm, guest_path))
7848}
7849
7850fn shadow_path_for_guest(vm: &VmState, guest_path: &str) -> PathBuf {
7851 let normalized = normalize_path(guest_path);
7852 let relative = normalized.trim_start_matches('/');
7853 if relative.is_empty() {
7854 return vm.cwd.clone();
7855 }
7856 vm.cwd.join(relative)
7857}
7858
7859fn apply_shell_cwd_prefix(command: &str, mut args: Vec<String>, guest_cwd: &str) -> Vec<String> {
7860 if guest_cwd == "/" || !is_shell_command(command) {
7861 return args;
7862 }
7863
7864 let Some(flag) = args.first() else {
7865 return args;
7866 };
7867 if !matches!(flag.as_str(), "-c" | "-lc") || args.len() < 2 {
7868 return args;
7869 }
7870
7871 let command_text = args[1].clone();
7872 let quoted_cwd = shell_single_quote(guest_cwd);
7873 args[1] = format!("cd {quoted_cwd} && {command_text}");
7874 args
7875}
7876
7877fn is_shell_command(command: &str) -> bool {
7878 Path::new(command)
7879 .file_name()
7880 .and_then(|name| name.to_str())
7881 .unwrap_or(command)
7882 .trim_end_matches(".exe")
7883 .eq("sh")
7884 || Path::new(command)
7885 .file_name()
7886 .and_then(|name| name.to_str())
7887 .unwrap_or(command)
7888 .trim_end_matches(".exe")
7889 .eq("bash")
7890}
7891
7892fn shell_single_quote(value: &str) -> String {
7893 if value.is_empty() {
7894 return String::from("''");
7895 }
7896 format!("'{}'", value.replace('\'', "'\"'\"'"))
7897}
7898
7899pub(crate) fn sync_active_process_host_writes_to_kernel(
7900 vm: &mut VmState,
7901) -> Result<(), SidecarError> {
7902 if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
7903 let shadow_root = vm.cwd.clone();
7904 sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
7905 }
7906
7907 let normalized_vm_root = normalize_host_path(&vm.cwd);
7908 let extra_roots = collect_active_process_host_sync_roots(vm, &normalized_vm_root);
7909 for (host_cwd, guest_cwd) in extra_roots {
7910 sync_host_directory_tree_to_kernel(vm, &host_cwd, &guest_cwd)?;
7911 }
7912
7913 Ok(())
7914}
7915
7916fn collect_active_process_host_sync_roots(
7917 vm: &VmState,
7918 normalized_vm_root: &Path,
7919) -> Vec<(PathBuf, String)> {
7920 let mut roots = Vec::new();
7921 let mut seen = BTreeSet::new();
7922
7923 for process in vm.active_processes.values() {
7924 collect_process_host_sync_roots(process, normalized_vm_root, &mut seen, &mut roots);
7925 }
7926
7927 roots
7928}
7929
7930fn collect_process_host_sync_roots(
7931 process: &ActiveProcess,
7932 normalized_vm_root: &Path,
7933 seen: &mut BTreeSet<(PathBuf, String)>,
7934 roots: &mut Vec<(PathBuf, String)>,
7935) {
7936 let normalized_host_cwd = normalize_host_path(&process.host_cwd);
7937 if !path_is_within_root(&normalized_host_cwd, normalized_vm_root) {
7938 let guest_cwd = normalize_path(&process.guest_cwd);
7939 if seen.insert((normalized_host_cwd.clone(), guest_cwd.clone())) {
7940 roots.push((normalized_host_cwd, guest_cwd));
7941 }
7942 }
7943
7944 for child in process.child_processes.values() {
7945 collect_process_host_sync_roots(child, normalized_vm_root, seen, roots);
7946 }
7947}
7948
7949fn sync_process_host_writes_to_kernel(
7950 vm: &mut VmState,
7951 process: &ActiveProcess,
7952) -> Result<(), SidecarError> {
7953 if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
7954 let shadow_root = vm.cwd.clone();
7955 sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
7956 }
7957
7958 if !path_is_within_root(
7959 &normalize_host_path(&process.host_cwd),
7960 &normalize_host_path(&vm.cwd),
7961 ) {
7962 sync_host_directory_tree_to_kernel(vm, &process.host_cwd, &process.guest_cwd)?;
7963 }
7964
7965 Ok(())
7966}
7967
7968fn sync_host_directory_tree_to_kernel(
7969 vm: &mut VmState,
7970 host_root: &Path,
7971 guest_root: &str,
7972) -> Result<(), SidecarError> {
7973 let normalized_host_root = normalize_host_path(host_root);
7974 let normalized_guest_root = normalize_path(guest_root);
7975 let mut synced_file_times = BTreeMap::new();
7976 sync_host_directory_tree_to_kernel_inner(
7977 vm,
7978 &normalized_host_root,
7979 &normalized_host_root,
7980 &normalized_guest_root,
7981 &mut synced_file_times,
7982 )
7983}
7984
7985fn sync_host_directory_tree_to_kernel_inner(
7986 vm: &mut VmState,
7987 host_root: &Path,
7988 current_host_dir: &Path,
7989 guest_root: &str,
7990 synced_file_times: &mut BTreeMap<(u64, u64), (u64, u64)>,
7991) -> Result<(), SidecarError> {
7992 let entries = match fs::read_dir(current_host_dir) {
7993 Ok(entries) => entries,
7994 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
7995 Err(error) => {
7996 return Err(SidecarError::Io(format!(
7997 "failed to read host shadow directory {}: {error}",
7998 current_host_dir.display()
7999 )));
8000 }
8001 };
8002
8003 for entry in entries {
8004 let entry = entry.map_err(|error| {
8005 SidecarError::Io(format!(
8006 "failed to read host shadow entry in {}: {error}",
8007 current_host_dir.display()
8008 ))
8009 })?;
8010 let host_path = entry.path();
8011 let file_type = entry.file_type().map_err(|error| {
8012 SidecarError::Io(format!(
8013 "failed to stat host shadow entry {}: {error}",
8014 host_path.display()
8015 ))
8016 })?;
8017 let relative_path = host_path
8018 .strip_prefix(host_root)
8019 .map_err(|error| {
8020 SidecarError::InvalidState(format!(
8021 "failed to relativize host shadow path {} against {}: {error}",
8022 host_path.display(),
8023 host_root.display()
8024 ))
8025 })?
8026 .to_string_lossy()
8027 .replace('\\', "/");
8028 let guest_path = if guest_root == "/" {
8029 normalize_path(&format!("/{relative_path}"))
8030 } else {
8031 normalize_path(&format!(
8032 "{}/{}",
8033 guest_root.trim_end_matches('/'),
8034 relative_path
8035 ))
8036 };
8037
8038 if should_skip_shadow_sync_path(vm, &guest_path) {
8039 continue;
8040 }
8041
8042 if file_type.is_dir() {
8043 let metadata = entry.metadata().map_err(|error| {
8044 SidecarError::Io(format!(
8045 "failed to read host shadow metadata {}: {error}",
8046 host_path.display()
8047 ))
8048 })?;
8049 if !is_shadow_bootstrap_dir(&guest_path)
8050 && !vm.kernel.exists(&guest_path).unwrap_or(false)
8051 {
8052 vm.kernel.mkdir(&guest_path, true).map_err(|error| {
8053 SidecarError::InvalidState(format!(
8054 "failed to sync host shadow directory {} to guest {}: {}",
8055 host_path.display(),
8056 guest_path,
8057 kernel_error(error)
8058 ))
8059 })?;
8060 vm.kernel
8061 .chmod(&guest_path, host_shadow_mode(&metadata))
8062 .map_err(|error| {
8063 SidecarError::InvalidState(format!(
8064 "failed to sync host shadow directory mode {} to guest {}: {}",
8065 host_path.display(),
8066 guest_path,
8067 kernel_error(error)
8068 ))
8069 })?;
8070 }
8071 sync_host_directory_tree_to_kernel_inner(
8072 vm,
8073 host_root,
8074 &host_path,
8075 guest_root,
8076 synced_file_times,
8077 )?;
8078 continue;
8079 }
8080
8081 if file_type.is_file() {
8082 let metadata = entry.metadata().map_err(|error| {
8083 SidecarError::Io(format!(
8084 "failed to read host shadow metadata {}: {error}",
8085 host_path.display()
8086 ))
8087 })?;
8088 let timestamp_key = (metadata.dev(), metadata.ino());
8089 let (atime_ms, mtime_ms) =
8090 *synced_file_times.entry(timestamp_key).or_insert_with(|| {
8091 (
8092 metadata_time_ms(metadata.atime(), metadata.atime_nsec()),
8093 metadata_time_ms(metadata.mtime(), metadata.mtime_nsec()),
8094 )
8095 });
8096 let desired_mode = host_shadow_mode(&metadata);
8097 let bytes = read_host_shadow_file(&host_path, desired_mode).map_err(|error| {
8098 SidecarError::Io(format!(
8099 "failed to read host shadow file {}: {error}",
8100 host_path.display()
8101 ))
8102 })?;
8103 vm.kernel.write_file(&guest_path, bytes).map_err(|error| {
8104 SidecarError::InvalidState(format!(
8105 "failed to sync host shadow file {} to guest {}: {}",
8106 host_path.display(),
8107 guest_path,
8108 kernel_error(error)
8109 ))
8110 })?;
8111 vm.kernel
8112 .chmod(&guest_path, desired_mode)
8113 .map_err(|error| {
8114 SidecarError::InvalidState(format!(
8115 "failed to sync host shadow file mode {} to guest {}: {}",
8116 host_path.display(),
8117 guest_path,
8118 kernel_error(error)
8119 ))
8120 })?;
8121 vm.kernel
8122 .utimes(&guest_path, atime_ms, mtime_ms)
8123 .map_err(|error| {
8124 SidecarError::InvalidState(format!(
8125 "failed to sync host shadow file times {} to guest {}: {}",
8126 host_path.display(),
8127 guest_path,
8128 kernel_error(error)
8129 ))
8130 })?;
8131 continue;
8132 }
8133
8134 if file_type.is_symlink() {
8135 let target = match fs::read_link(&host_path) {
8136 Ok(target) => target,
8137 Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
8138 Err(error) => {
8139 return Err(SidecarError::Io(format!(
8140 "failed to read host shadow symlink {}: {error}",
8141 host_path.display()
8142 )));
8143 }
8144 };
8145 replace_kernel_symlink(vm, &guest_path, &target.to_string_lossy())?;
8146 }
8147 }
8148
8149 Ok(())
8150}
8151
8152fn replace_kernel_symlink(
8153 vm: &mut VmState,
8154 guest_path: &str,
8155 target: &str,
8156) -> Result<(), SidecarError> {
8157 if vm.kernel.symlink(target, guest_path).is_ok() {
8158 return Ok(());
8159 }
8160
8161 if let Ok(existing_target) = vm.kernel.read_link(guest_path) {
8162 if existing_target == target {
8163 return Ok(());
8164 }
8165 }
8166
8167 let _ = vm.kernel.remove_file(guest_path);
8168 let _ = vm.kernel.remove_dir(guest_path);
8169 vm.kernel
8170 .symlink(target, guest_path)
8171 .map_err(kernel_error)?;
8172 Ok(())
8173}
8174
8175fn host_shadow_mode(metadata: &fs::Metadata) -> u32 {
8176 metadata.permissions().mode() & 0o7777
8177}
8178
8179fn read_host_shadow_file(host_path: &Path, mode: u32) -> std::io::Result<Vec<u8>> {
8185 match fs::read(host_path) {
8186 Ok(bytes) => Ok(bytes),
8187 Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
8188 fs::set_permissions(host_path, fs::Permissions::from_mode(mode | 0o400))?;
8189 let result = fs::read(host_path);
8190 fs::set_permissions(host_path, fs::Permissions::from_mode(mode))?;
8191 result
8192 }
8193 Err(error) => Err(error),
8194 }
8195}
8196
8197fn metadata_time_ms(seconds: i64, nanos: i64) -> u64 {
8198 let seconds = seconds.max(0) as u64;
8199 let nanos = nanos.max(0) as u64;
8200 seconds
8201 .saturating_mul(1_000)
8202 .saturating_add(nanos / 1_000_000)
8203}
8204
8205fn is_shadow_bootstrap_dir(path: &str) -> bool {
8206 matches!(
8207 path,
8208 "/dev"
8209 | "/proc"
8210 | "/tmp"
8211 | "/bin"
8212 | "/lib"
8213 | "/sbin"
8214 | "/boot"
8215 | "/etc"
8216 | "/root"
8217 | "/run"
8218 | "/srv"
8219 | "/sys"
8220 | "/opt"
8221 | "/mnt"
8222 | "/media"
8223 | "/home"
8224 | "/home/user"
8225 | "/usr"
8226 | "/usr/bin"
8227 | "/usr/games"
8228 | "/usr/include"
8229 | "/usr/lib"
8230 | "/usr/libexec"
8231 | "/usr/man"
8232 | "/usr/local"
8233 | "/usr/local/bin"
8234 | "/usr/sbin"
8235 | "/usr/share"
8236 | "/usr/share/man"
8237 | "/var"
8238 | "/var/cache"
8239 | "/var/empty"
8240 | "/var/lib"
8241 | "/var/lock"
8242 | "/var/log"
8243 | "/var/run"
8244 | "/var/spool"
8245 | "/var/tmp"
8246 | "/etc/agentos"
8247 )
8248}
8249
8250#[cfg(test)]
8251mod shadow_sync_tests {
8252 use super::{is_protected_agentos_shadow_sync_path, is_shadow_bootstrap_dir};
8253
8254 #[test]
8255 fn shadow_bootstrap_sync_skips_virtual_home_tree() {
8256 assert!(is_shadow_bootstrap_dir("/home"));
8257 assert!(is_shadow_bootstrap_dir("/home/user"));
8258 }
8259
8260 #[test]
8261 fn protected_agentos_paths_are_not_shadow_synced() {
8262 assert!(is_protected_agentos_shadow_sync_path("/etc/agentos"));
8263 assert!(is_protected_agentos_shadow_sync_path(
8264 "/etc/agentos/instructions.md"
8265 ));
8266 assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos-copy"));
8267 assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos.md"));
8268 }
8269}
8270
8271fn is_kernel_owned_shadow_sync_path(path: &str) -> bool {
8272 matches!(path, "/dev" | "/proc" | "/sys")
8273 || path.starts_with("/dev/")
8274 || path.starts_with("/proc/")
8275 || path.starts_with("/sys/")
8276}
8277
8278pub(crate) fn is_protected_agentos_shadow_sync_path(path: &str) -> bool {
8279 path == "/etc/agentos" || path.starts_with("/etc/agentos/")
8280}
8281
8282fn should_skip_shadow_sync_path(vm: &VmState, guest_path: &str) -> bool {
8283 is_kernel_owned_shadow_sync_path(guest_path)
8284 || is_protected_agentos_shadow_sync_path(guest_path)
8285 || host_mount_path_for_guest_path_from_mounts(&vm.configuration.mounts, guest_path)
8286 .is_some()
8287}
8288
8289fn resolve_path_like_guest_specifier(cwd: &str, specifier: &str) -> String {
8290 if specifier.starts_with("file://") {
8291 normalize_path(specifier.trim_start_matches("file://"))
8292 } else if specifier.starts_with("file:") {
8293 normalize_path(specifier.trim_start_matches("file:"))
8294 } else if specifier.starts_with('/') {
8295 normalize_path(specifier)
8296 } else {
8297 normalize_path(&format!("{cwd}/{specifier}"))
8298 }
8299}
8300
8301fn guest_entrypoint_for_specifier(cwd: &str, specifier: &str) -> Option<String> {
8302 is_path_like_specifier(specifier).then(|| resolve_path_like_guest_specifier(cwd, specifier))
8303}
8304
8305fn is_node_runtime_command(command: &str) -> bool {
8306 matches!(command, "node" | "npm" | "npx")
8307 || Path::new(command)
8308 .file_name()
8309 .and_then(|name| name.to_str())
8310 .is_some_and(|name| matches!(name, "node" | "npm" | "npx"))
8311}
8312
8313fn resolve_special_node_cli_invocation(
8314 args: &[String],
8315 env: &mut BTreeMap<String, String>,
8316) -> Option<(String, Vec<String>)> {
8317 let first = args.first()?;
8318 match first.as_str() {
8319 "-e" | "--eval" => {
8320 env.insert(
8321 String::from("AGENT_OS_NODE_EVAL"),
8322 args.get(1).cloned().unwrap_or_default(),
8323 );
8324 Some((first.clone(), args.iter().skip(2).cloned().collect()))
8325 }
8326 "-v" | "--version" => {
8327 env.insert(
8328 String::from("AGENT_OS_NODE_EVAL"),
8329 String::from("console.log(process.version);"),
8330 );
8331 Some((String::from("-e"), args.to_vec()))
8332 }
8333 _ => None,
8334 }
8335}
8336
8337fn node_runtime_command_name(command: &str) -> Option<&str> {
8338 let name = Path::new(command)
8339 .file_name()
8340 .and_then(|name| name.to_str())?;
8341 matches!(name, "node" | "npm" | "npx").then_some(name)
8342}
8343
8344struct ResolvedHostNodeCliEntrypoint {
8345 command_name: String,
8346 guest_root: String,
8347 guest_entrypoint: String,
8348 package_root: PathBuf,
8349}
8350
8351fn resolve_host_node_cli_entrypoint(command: &str) -> Option<ResolvedHostNodeCliEntrypoint> {
8352 let command_name = node_runtime_command_name(command)?;
8353 if !matches!(command_name, "npm" | "npx") {
8354 return None;
8355 }
8356
8357 let path = std::env::var_os("PATH")?;
8358 for root in std::env::split_paths(&path) {
8359 let candidate = root.join(command_name);
8360 if !candidate.is_file() {
8361 continue;
8362 }
8363 let entrypoint = candidate.canonicalize().ok().unwrap_or(candidate);
8364 let package_root = entrypoint.parent()?.parent()?.to_path_buf();
8365 let guest_root = format!("/__secure_exec/node-runtime/{command_name}");
8366 let relative_entrypoint = entrypoint.strip_prefix(&package_root).ok()?;
8367 let guest_entrypoint = normalize_path(&format!(
8368 "{guest_root}/{}",
8369 relative_entrypoint.to_string_lossy().replace('\\', "/")
8370 ));
8371 return Some(ResolvedHostNodeCliEntrypoint {
8372 command_name: command_name.to_owned(),
8373 guest_root,
8374 guest_entrypoint,
8375 package_root,
8376 });
8377 }
8378
8379 None
8380}
8381
8382fn build_host_node_cli_eval(cli: &ResolvedHostNodeCliEntrypoint) -> String {
8383 let guest_npm_main = normalize_path(&format!("{}/lib/npm.js", cli.guest_root));
8384 let guest_npm_cli = normalize_path(&format!("{}/bin/npm-cli.js", cli.guest_root));
8385 let guest_package_json = normalize_path(&format!("{}/package.json", cli.guest_root));
8386 let guest_display_module = normalize_path(&format!("{}/lib/utils/display.js", cli.guest_root));
8387 let guest_log_file_module =
8388 normalize_path(&format!("{}/lib/utils/log-file.js", cli.guest_root));
8389 let debug_preamble = "const __agentOsDebugNpmCli = !!process.env.CODEX_DEBUG_NPM_CLI; const __agentOsDebugLog = (...args) => { if (__agentOsDebugNpmCli) { console.error('[secure-exec npm debug]', ...args); } }; const __agentOsIsProcessExitError = (error) => !!(error && typeof error === 'object' && (error._isProcessExit === true || error.name === 'ProcessExitError')); const __agentOsResolveExitCode = (code) => Number.isFinite(code) ? code : (Number.isFinite(process.exitCode) ? process.exitCode : 0); const __agentOsFinish = (code) => { process.exitCode = __agentOsResolveExitCode(code); }; if (__agentOsDebugNpmCli) { const __agentOsWrapAsyncFsMethod = (__agentOsTarget, __agentOsMethod) => { const __agentOsOriginal = __agentOsTarget[__agentOsMethod]; if (typeof __agentOsOriginal !== 'function' || __agentOsOriginal.__agentOsDebugWrapped) { return; } const __agentOsWrapped = async (...args) => { const target = args.length > 0 ? args[0] : '<none>'; __agentOsDebugLog(`fs.${__agentOsMethod}:start`, String(target)); try { const result = await __agentOsOriginal.apply(__agentOsTarget, args); __agentOsDebugLog(`fs.${__agentOsMethod}:done`, String(target)); return result; } catch (error) { __agentOsDebugLog(`fs.${__agentOsMethod}:error`, String(target), error && error.stack ? error.stack : String(error)); throw error; } }; __agentOsWrapped.__agentOsDebugWrapped = true; __agentOsTarget[__agentOsMethod] = __agentOsWrapped; }; const __agentOsWrapSyncFsMethod = (__agentOsTarget, __agentOsMethod) => { const __agentOsOriginal = __agentOsTarget[__agentOsMethod]; if (typeof __agentOsOriginal !== 'function' || __agentOsOriginal.__agentOsDebugWrapped) { return; } const __agentOsWrapped = (...args) => { const target = args.length > 0 ? args[0] : '<none>'; __agentOsDebugLog(`fs.${__agentOsMethod}:start`, String(target)); try { const result = __agentOsOriginal.apply(__agentOsTarget, args); __agentOsDebugLog(`fs.${__agentOsMethod}:done`, String(target)); return result; } catch (error) { __agentOsDebugLog(`fs.${__agentOsMethod}:error`, String(target), error && error.stack ? error.stack : String(error)); throw error; } }; __agentOsWrapped.__agentOsDebugWrapped = true; __agentOsTarget[__agentOsMethod] = __agentOsWrapped; }; const __agentOsFsPromiseModules = [require('fs/promises'), require('node:fs/promises')]; for (const __agentOsFsPromises of __agentOsFsPromiseModules) { for (const __agentOsMethod of ['access', 'lstat', 'mkdir', 'open', 'readFile', 'readdir', 'readlink', 'realpath', 'rename', 'rm', 'rmdir', 'stat', 'symlink', 'unlink', 'writeFile']) { __agentOsWrapAsyncFsMethod(__agentOsFsPromises, __agentOsMethod); } } const __agentOsFsModules = [require('fs'), require('node:fs')]; for (const __agentOsFs of __agentOsFsModules) { for (const __agentOsMethod of ['accessSync', 'existsSync', 'lstatSync', 'mkdirSync', 'openSync', 'readFileSync', 'readdirSync', 'readlinkSync', 'realpathSync', 'renameSync', 'rmSync', 'rmdirSync', 'statSync', 'symlinkSync', 'unlinkSync', 'writeFileSync']) { __agentOsWrapSyncFsMethod(__agentOsFs, __agentOsMethod); } } }";
8390 let display_stub = format!(
8391 "const __agentOsDisplayModulePath = require.resolve({display_module}); const __agentOsLogFileModulePath = require.resolve({log_file_module}); const __agentOsColorPassthrough = new Proxy((value) => value, {{ get: () => __agentOsColorPassthrough, apply: (_target, _thisArg, args) => args[0] }}); class __AgentOsNpmDisplayStub {{ constructor() {{ this.chalk = {{ noColor: __agentOsColorPassthrough, stdout: __agentOsColorPassthrough, stderr: __agentOsColorPassthrough }}; this._logPaused = true; this._logBuffer = []; this._outputBuffer = []; this._write = (stream, values) => {{ if (!Array.isArray(values) || values.length === 0) {{ return; }} const text = values.map((value) => typeof value === 'string' ? value : String(value)).join(' '); if (text.length === 0) {{ return; }} const normalized = text.replace(/\\r\\n/g, '\\n'); if (/^\\n?> npx\\n> /u.test(normalized)) {{ return; }} stream.write(text.endsWith('\\n') ? text : `${{text}}\\n`); }}; this._inputHandler = (level, ...args) => {{ if (level !== 'read') {{ return; }} const [resolve, reject, callback] = args; Promise.resolve().then(() => callback()).then(resolve, reject); }}; this._logHandler = (level, ...args) => {{ if (level === 'resume') {{ this._logPaused = false; for (const entry of this._logBuffer.splice(0)) {{ this._write(process.stderr, entry); }} return; }} if (level === 'pause') {{ this._logPaused = true; return; }} if (this._logPaused) {{ this._logBuffer.push(args); return; }} this._write(process.stderr, args); }}; this._outputHandler = (level, ...args) => {{ if (level === 'buffer') {{ this._outputBuffer.push(['standard', args]); return; }} if (level === 'flush') {{ for (const [bufferLevel, bufferArgs] of this._outputBuffer.splice(0)) {{ this._write(bufferLevel === 'error' ? process.stderr : process.stdout, bufferArgs); }} return; }} this._write(level === 'error' ? process.stderr : process.stdout, args); }}; process.on('input', this._inputHandler); process.on('log', this._logHandler); process.on('output', this._outputHandler); }} async load() {{ process.emit('log', 'resume'); process.emit('output', 'flush'); }} off() {{ if (this._inputHandler) {{ process.off('input', this._inputHandler); }} if (this._logHandler) {{ process.off('log', this._logHandler); }} if (this._outputHandler) {{ process.off('output', this._outputHandler); }} this._logBuffer.length = 0; this._outputBuffer.length = 0; }} }} class __AgentOsNpmLogFileStub {{ constructor() {{ this.files = []; }} async load() {{ return []; }} off() {{}} }} globalThis._moduleCache[__agentOsDisplayModulePath] = {{ exports: __AgentOsNpmDisplayStub }}; globalThis._moduleCache[__agentOsLogFileModulePath] = {{ exports: __AgentOsNpmLogFileStub }};",
8392 display_module = serde_json::to_string(&guest_display_module)
8393 .unwrap_or_else(|_| format!("\"{guest_display_module}\"")),
8394 log_file_module = serde_json::to_string(&guest_log_file_module)
8395 .unwrap_or_else(|_| format!("\"{guest_log_file_module}\"")),
8396 );
8397 let registry_fetch_stub = "const { createRequire: __agentOsCreateRequire } = require('module'); const __agentOsNpmRequire = __agentOsCreateRequire(require.resolve(__AGENT_OS_NPM_MAIN__)); try { const __agentOsMinipassFetchPath = __agentOsNpmRequire.resolve('minipass-fetch'); const __agentOsMinipassFetch = __agentOsNpmRequire(__agentOsMinipassFetchPath); const { FetchError: __agentOsFetchError, Headers: __agentOsFetchHeaders, Request: __agentOsFetchRequest, Response: __agentOsFetchResponse, AbortError: __agentOsAbortError } = __agentOsMinipassFetch; const { Minipass: __agentOsMinipass } = __agentOsNpmRequire('minipass'); const __agentOsCreateBinaryMinipass = () => new __agentOsMinipass({ objectMode: false, encoding: null }); const __agentOsCloneBuffer = (buffer) => Buffer.isBuffer(buffer) ? Buffer.from(buffer) : Buffer.from(buffer ?? []); const __agentOsBufferToArrayBuffer = (buffer) => { const bytes = __agentOsCloneBuffer(buffer); return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); }; const __agentOsAttachBufferedBodyMethods = (response, responseBuffer) => { const __agentOsReadBuffer = async () => __agentOsCloneBuffer(responseBuffer); response.__agentOsBufferedBody = __agentOsCloneBuffer(responseBuffer); response.buffer = __agentOsReadBuffer; response.text = async () => (await __agentOsReadBuffer()).toString('utf8'); response.json = async () => JSON.parse(await response.text()); response.arrayBuffer = async () => __agentOsBufferToArrayBuffer(await __agentOsReadBuffer()); response.clone = () => { const clonedBody = __agentOsCreateBinaryMinipass(); const clonedBuffer = __agentOsCloneBuffer(responseBuffer); clonedBody.end(clonedBuffer); const clonedResponse = new __agentOsFetchResponse(clonedBody, { url: response.url, status: response.status, statusText: response.statusText, headers: response.headers, size: response.size, timeout: response.timeout, counter: response.counter, trailer: response.trailer }); return __agentOsAttachBufferedBodyMethods(clonedResponse, clonedBuffer); }; return response; }; const __agentOsNormalizeHeaders = (__agentOsHeaders) => { const normalized = {}; __agentOsHeaders.forEach((value, key) => { if (normalized[key] === undefined) { normalized[key] = value; return; } if (Array.isArray(normalized[key])) { normalized[key].push(value); return; } normalized[key] = [normalized[key], value]; }); return normalized; }; const __agentOsPatchedMinipassFetch = async (input, opts = {}) => { const request = input instanceof __agentOsFetchRequest ? input : new __agentOsFetchRequest(input, opts); const __agentOsController = !request.signal && typeof AbortController === 'function' ? new AbortController() : null; const __agentOsSignal = request.signal ?? __agentOsController?.signal; let __agentOsTimer = null; if (__agentOsController && Number.isFinite(request.timeout) && request.timeout > 0) { __agentOsTimer = setTimeout(() => __agentOsController.abort(new Error(`network timeout at: ${request.url}`)), request.timeout); __agentOsTimer.unref?.(); } try { const requestHeaders = {}; request.headers.forEach((value, key) => { requestHeaders[key] = value; }); const response = await fetch(request.url, { method: request.method, headers: requestHeaders, body: request.body ?? undefined, redirect: request.redirect ?? opts.redirect ?? 'follow', signal: __agentOsSignal, ...(request.body ? { duplex: 'half' } : {}) }); const responseBody = __agentOsCreateBinaryMinipass(); const contentType = String(response.headers.get('content-type') || '').toLowerCase(); const responseBuffer = contentType.includes('json') ? Buffer.from(JSON.stringify(await response.json())) : contentType.startsWith('text/') ? Buffer.from(await response.text()) : Buffer.from(await response.arrayBuffer()); responseBody.end(responseBuffer); return __agentOsAttachBufferedBodyMethods(new __agentOsFetchResponse(responseBody, { url: response.url, status: response.status, statusText: response.statusText, headers: __agentOsNormalizeHeaders(response.headers), size: request.size, timeout: request.timeout, counter: request.counter ?? opts.counter ?? 0, trailer: Promise.resolve(new __agentOsFetchHeaders()) }), responseBuffer); } catch (error) { if (error instanceof Error) { throw error; } throw new __agentOsFetchError(String(error), 'system', error); } finally { if (__agentOsTimer) { clearTimeout(__agentOsTimer); } } }; globalThis.__agentOsPatchedMinipassFetch = __agentOsPatchedMinipassFetch; __agentOsPatchedMinipassFetch.isRedirect = typeof __agentOsMinipassFetch.isRedirect === 'function' ? __agentOsMinipassFetch.isRedirect.bind(__agentOsMinipassFetch) : (code) => code === 301 || code === 302 || code === 303 || code === 307 || code === 308; __agentOsPatchedMinipassFetch.FetchError = __agentOsFetchError; __agentOsPatchedMinipassFetch.Headers = __agentOsFetchHeaders; __agentOsPatchedMinipassFetch.Request = __agentOsFetchRequest; __agentOsPatchedMinipassFetch.Response = __agentOsFetchResponse; __agentOsPatchedMinipassFetch.AbortError = __agentOsAbortError; globalThis._moduleCache[__agentOsMinipassFetchPath] = { exports: __agentOsPatchedMinipassFetch }; __agentOsDebugLog('patched-minipass-fetch', __agentOsMinipassFetchPath); const __agentOsCheckResponsePath = __agentOsNpmRequire.resolve('npm-registry-fetch/lib/check-response.js'); const __agentOsCheckResponse = __agentOsNpmRequire(__agentOsCheckResponsePath); const __agentOsEnsureResponseBodyStream = (response) => { if (!response || (response.body && typeof response.body.on === 'function')) { return response; } const body = __agentOsCreateBinaryMinipass(); const finishWithError = (error) => body.emit('error', error instanceof Error ? error : new Error(String(error))); try { if (typeof response.buffer === 'function') { Promise.resolve(response.buffer()).then((buffer) => body.end(buffer), finishWithError); } else if (Buffer.isBuffer(response.body) || typeof response.body === 'string') { body.end(response.body); } else if (response.body && typeof response.body[Symbol.asyncIterator] === 'function') { (async () => { try { for await (const chunk of response.body) { body.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } body.end(); } catch (error) { finishWithError(error); body.end(); } })(); } else { body.end(); } } catch (error) { finishWithError(error); body.end(); } return new __agentOsFetchResponse(body, response); }; globalThis._moduleCache[__agentOsCheckResponsePath] = { exports: (payload) => { const normalized = { ...payload, res: __agentOsEnsureResponseBodyStream(payload.res) }; __agentOsDebugLog('check-response-body', normalized.res && normalized.res.status, typeof (normalized.res && normalized.res.body), normalized.res && normalized.res.body && typeof normalized.res.body.on, normalized.res && normalized.res.body && normalized.res.body.constructor && normalized.res.body.constructor.name, !!(normalized.res && normalized.res.__agentOsBufferedBody), normalized.res && typeof normalized.res.json); return __agentOsCheckResponse(normalized); } }; __agentOsDebugLog('patched-check-response', __agentOsCheckResponsePath); } catch (error) { __agentOsDebugLog('patch-minipass-fetch-failed', error && error.stack ? error.stack : String(error)); } try { const __agentOsRegistryFetchPath = __agentOsNpmRequire.resolve('npm-registry-fetch'); const __agentOsRegistryFetch = __agentOsNpmRequire(__agentOsRegistryFetchPath); const __agentOsWrapRegistryFetch = (fn) => { const wrapResult = (promise) => Promise.resolve(promise).then((res) => { __agentOsDebugLog('registry-fetch-result', res && res.status, typeof (res && res.body), res && res.body && typeof res.body.on, res && res.body && res.body.constructor && res.body.constructor.name, !!(res && res.__agentOsBufferedBody), res && typeof res.json); return res; }); const wrapped = (uri, opts = {}) => wrapResult(globalThis.__agentOsPatchedMinipassFetch(uri, { method: opts.method, headers: opts.headers, body: opts.body, redirect: opts.redirect, signal: opts.signal, timeout: opts.timeout, size: opts.size, counter: opts.counter })); if (typeof fn.json === 'function') { wrapped.json = (uri, opts = {}) => wrapped(uri, opts).then((res) => res.json()); } if (fn.json && typeof fn.json.stream === 'function') { wrapped.json = wrapped.json || {}; wrapped.json.stream = (uri, path, opts = {}) => fn.json.stream(uri, path, { ...opts, agent: false }); } if (typeof fn.pickRegistry === 'function') { wrapped.pickRegistry = fn.pickRegistry.bind(fn); } if (typeof fn.getAuth === 'function') { wrapped.getAuth = fn.getAuth.bind(fn); } return wrapped; }; globalThis._moduleCache[__agentOsRegistryFetchPath] = { exports: __agentOsWrapRegistryFetch(__agentOsRegistryFetch) }; __agentOsDebugLog('patched-npm-registry-fetch', __agentOsRegistryFetchPath); } catch (error) { __agentOsDebugLog('patch-npm-registry-fetch-failed', error && error.stack ? error.stack : String(error)); }";
8398 match cli.command_name.as_str() {
8399 "npx" => format!(
8400 "{debug_preamble} {display_stub} {registry_fetch_stub} process.argv[1] = require.resolve({npm_cli}); process.argv.splice(2, 0, 'exec'); __agentOsDebugLog('argv', JSON.stringify(process.argv), 'cwd', process.cwd()); (async () => {{ const pkg = require({package_json}); if (process.argv.includes('--version') || process.argv.includes('-v')) {{ __agentOsDebugLog('version-shortcut'); console.log(pkg.version); __agentOsFinish(0); return; }} const Npm = require({npm_main}); const npm = new Npm(); __agentOsDebugLog('before-load'); const loaded = await npm.load(); __agentOsDebugLog('after-load', loaded && loaded.command, JSON.stringify(loaded && loaded.args)); if (!loaded.exec) {{ __agentOsDebugLog('no-exec'); __agentOsFinish(); return; }} if (!loaded.command) {{ __agentOsDebugLog('no-command'); const {{ output }} = require('proc-log'); output.standard(npm.usage); __agentOsFinish(1); return; }} __agentOsDebugLog('before-exec', loaded.command, JSON.stringify(loaded.args)); await npm.exec(loaded.command, loaded.args); __agentOsDebugLog('after-exec', __agentOsResolveExitCode()); __agentOsFinish(); }})().catch((error) => {{ if (__agentOsIsProcessExitError(error)) {{ __agentOsDebugLog('process-exit-error', __agentOsResolveExitCode(error.code)); __agentOsFinish(error.code); return; }} console.error(error && error.stack ? error.stack : String(error)); __agentOsFinish(error && typeof error === 'object' && Number.isFinite(error.exitCode) ? error.exitCode : 1); }});",
8401 debug_preamble = debug_preamble,
8402 display_stub = display_stub,
8403 registry_fetch_stub = registry_fetch_stub.replace(
8404 "__AGENT_OS_NPM_MAIN__",
8405 &serde_json::to_string(&guest_npm_main)
8406 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8407 ),
8408 npm_main = serde_json::to_string(&guest_npm_main)
8409 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8410 npm_cli = serde_json::to_string(&guest_npm_cli)
8411 .unwrap_or_else(|_| format!("\"{guest_npm_cli}\"")),
8412 package_json = serde_json::to_string(&guest_package_json)
8413 .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
8414 ),
8415 _ => format!(
8416 "{debug_preamble} {display_stub} {registry_fetch_stub} __agentOsDebugLog('argv', JSON.stringify(process.argv), 'cwd', process.cwd()); (async () => {{ const pkg = require({package_json}); if (process.argv.includes('--version') || process.argv.includes('-v')) {{ __agentOsDebugLog('version-shortcut'); console.log(pkg.version); __agentOsFinish(0); return; }} const Npm = require({npm_main}); const npm = new Npm(); __agentOsDebugLog('before-load'); const loaded = await npm.load(); __agentOsDebugLog('after-load', loaded && loaded.command, JSON.stringify(loaded && loaded.args)); if (!loaded.exec) {{ __agentOsDebugLog('no-exec'); __agentOsFinish(); return; }} if (!loaded.command) {{ __agentOsDebugLog('no-command'); const {{ output }} = require('proc-log'); output.standard(npm.usage); __agentOsFinish(1); return; }} __agentOsDebugLog('before-exec', loaded.command, JSON.stringify(loaded.args)); await npm.exec(loaded.command, loaded.args); __agentOsDebugLog('after-exec', __agentOsResolveExitCode()); __agentOsFinish(); }})().catch((error) => {{ if (__agentOsIsProcessExitError(error)) {{ __agentOsDebugLog('process-exit-error', __agentOsResolveExitCode(error.code)); __agentOsFinish(error.code); return; }} console.error(error && error.stack ? error.stack : String(error)); __agentOsFinish(error && typeof error === 'object' && Number.isFinite(error.exitCode) ? error.exitCode : 1); }});",
8417 debug_preamble = debug_preamble,
8418 display_stub = display_stub,
8419 registry_fetch_stub = registry_fetch_stub.replace(
8420 "__AGENT_OS_NPM_MAIN__",
8421 &serde_json::to_string(&guest_npm_main)
8422 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8423 ),
8424 npm_main = serde_json::to_string(&guest_npm_main)
8425 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8426 package_json = serde_json::to_string(&guest_package_json)
8427 .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
8428 ),
8429 }
8430}
8431
8432fn resolve_guest_command_entrypoint(
8433 vm: &VmState,
8434 guest_cwd: &str,
8435 command: &str,
8436 path_env: Option<&str>,
8437) -> Option<String> {
8438 if !is_path_like_specifier(command) {
8439 if let Some(entrypoint) = vm.command_guest_paths.get(command) {
8440 return Some(entrypoint.clone());
8441 }
8442
8443 for search_dir in guest_command_search_dirs(vm, guest_cwd, path_env) {
8444 let candidate = normalize_path(&format!("{search_dir}/{command}"));
8445 if let Some(entrypoint) = resolve_guest_command_path_candidate(vm, &candidate) {
8446 return Some(entrypoint);
8447 }
8448 }
8449
8450 return None;
8451 }
8452
8453 let normalized = resolve_path_like_guest_specifier(guest_cwd, command);
8454 resolve_guest_command_path_candidate(vm, &normalized).or_else(|| {
8455 let parent_dir = Path::new(&normalized).parent()?.to_str()?;
8459 if !guest_command_search_dirs(vm, guest_cwd, path_env)
8460 .iter()
8461 .any(|search_dir| normalize_path(search_dir) == normalize_path(parent_dir))
8462 {
8463 return None;
8464 }
8465
8466 let file_name = Path::new(&normalized).file_name()?.to_str()?;
8467 vm.command_guest_paths.get(file_name).cloned()
8468 })
8469}
8470
8471fn guest_command_search_dirs(vm: &VmState, guest_cwd: &str, path_env: Option<&str>) -> Vec<String> {
8472 let mut search_dirs = Vec::new();
8473 let mut seen = BTreeSet::new();
8474
8475 if let Some(path) = path_env.or_else(|| vm.guest_env.get("PATH").map(String::as_str)) {
8476 for segment in path.split(':') {
8477 let trimmed = segment.trim();
8478 if trimmed.is_empty() {
8479 continue;
8480 }
8481 let normalized = if trimmed.starts_with('/') {
8482 normalize_path(trimmed)
8483 } else {
8484 normalize_path(&format!("{guest_cwd}/{trimmed}"))
8485 };
8486 if seen.insert(normalized.clone()) {
8487 search_dirs.push(normalized);
8488 }
8489 }
8490 }
8491
8492 for fallback in ["/bin", "/usr/bin", "/usr/local/bin"] {
8493 let normalized = String::from(fallback);
8494 if seen.insert(normalized.clone()) {
8495 search_dirs.push(normalized);
8496 }
8497 }
8498
8499 search_dirs
8500}
8501
8502fn resolve_guest_command_path_candidate(vm: &VmState, candidate: &str) -> Option<String> {
8503 if candidate.starts_with("/bin/")
8504 || candidate.starts_with("/usr/bin/")
8505 || candidate.starts_with("/usr/local/bin/")
8506 || candidate.starts_with("/__secure_exec/commands/")
8507 {
8508 if let Some(file_name) = Path::new(candidate)
8509 .file_name()
8510 .and_then(|name| name.to_str())
8511 {
8512 if let Some(guest_entrypoint) = vm.command_guest_paths.get(file_name) {
8513 return Some(guest_entrypoint.clone());
8514 }
8515 }
8516 }
8517
8518 if vm
8519 .kernel
8520 .exists(candidate)
8521 .ok()
8522 .is_some_and(|exists| exists)
8523 {
8524 return Some(normalize_path(candidate));
8525 }
8526
8527 resolve_vm_guest_path_to_host(vm, candidate)
8528 .is_file()
8529 .then(|| normalize_path(candidate))
8530}
8531
8532fn resolve_host_entrypoint_within_vm_host_cwd(
8533 vm: &VmState,
8534 specifier: &str,
8535) -> Option<(String, String)> {
8536 let candidate = Path::new(specifier);
8537 if !candidate.is_absolute() {
8538 return None;
8539 }
8540
8541 let normalized_entrypoint = normalize_host_path(candidate);
8542 let normalized_host_cwd = normalize_host_path(&vm.host_cwd);
8543 if !path_is_within_root(&normalized_entrypoint, &normalized_host_cwd) {
8544 return None;
8545 }
8546
8547 let relative = normalized_entrypoint
8548 .strip_prefix(&normalized_host_cwd)
8549 .ok()?
8550 .to_string_lossy()
8551 .replace('\\', "/");
8552 let guest_entrypoint = if relative.is_empty() {
8553 String::from("/")
8554 } else {
8555 normalize_path(&format!("/{relative}"))
8556 };
8557 Some((
8558 guest_entrypoint,
8559 normalized_entrypoint.to_string_lossy().into_owned(),
8560 ))
8561}
8562
8563fn prepare_guest_runtime_env(
8564 vm: &VmState,
8565 env: &mut BTreeMap<String, String>,
8566 guest_cwd: &str,
8567 host_cwd: &Path,
8568 guest_entrypoint: Option<String>,
8569) -> Result<(), SidecarError> {
8570 let user = vm.kernel.user_profile();
8571 let resource_limits = vm.kernel.resource_limits();
8572 let path_mappings = runtime_guest_path_mappings(vm);
8573 let read_paths = expand_host_access_paths(
8574 std::iter::once(vm.cwd.clone())
8575 .chain(
8576 path_mappings
8577 .iter()
8578 .map(|mapping| PathBuf::from(&mapping.host_path)),
8579 )
8580 .chain(std::iter::once(host_cwd.to_path_buf()))
8581 .collect::<Vec<_>>()
8582 .as_slice(),
8583 );
8584 let write_paths = dedupe_host_paths(
8585 std::iter::once(vm.cwd.clone())
8586 .chain(std::iter::once(host_cwd.to_path_buf()))
8587 .chain(runtime_guest_writable_host_paths(vm))
8588 .collect::<Vec<_>>()
8589 .as_slice(),
8590 );
8591 let allowed_node_builtins = configured_allowed_node_builtins(vm);
8592 let loopback_exempt_ports = configured_loopback_exempt_ports(vm);
8593
8594 env.insert(
8595 String::from("AGENT_OS_GUEST_PATH_MAPPINGS"),
8596 serde_json::to_string(&path_mappings).map_err(|error| {
8597 SidecarError::InvalidState(format!("failed to encode guest path mappings: {error}"))
8598 })?,
8599 );
8600 env.entry(String::from(EXECUTION_SANDBOX_ROOT_ENV))
8601 .or_insert_with(|| normalize_host_path(&vm.cwd).to_string_lossy().into_owned());
8602 env.insert(
8603 String::from("AGENT_OS_EXTRA_FS_READ_PATHS"),
8604 serde_json::to_string(
8605 &read_paths
8606 .iter()
8607 .map(|path| path.to_string_lossy().into_owned())
8608 .collect::<Vec<_>>(),
8609 )
8610 .map_err(|error| {
8611 SidecarError::InvalidState(format!("failed to encode read paths: {error}"))
8612 })?,
8613 );
8614 env.insert(
8615 String::from("AGENT_OS_EXTRA_FS_WRITE_PATHS"),
8616 serde_json::to_string(
8617 &write_paths
8618 .iter()
8619 .map(|path| path.to_string_lossy().into_owned())
8620 .collect::<Vec<_>>(),
8621 )
8622 .map_err(|error| {
8623 SidecarError::InvalidState(format!("failed to encode write paths: {error}"))
8624 })?,
8625 );
8626 env.insert(
8627 String::from("AGENT_OS_ALLOWED_NODE_BUILTINS"),
8628 serde_json::to_string(&allowed_node_builtins).map_err(|error| {
8629 SidecarError::InvalidState(format!("failed to encode allowed builtins: {error}"))
8630 })?,
8631 );
8632 env.insert(
8635 String::from("AGENT_OS_JS_PLATFORM"),
8636 js_runtime_platform_env(vm).to_owned(),
8637 );
8638 if let Some(resolution) = js_runtime_module_resolution_env(vm) {
8640 env.insert(
8641 String::from("AGENT_OS_JS_MODULE_RESOLUTION"),
8642 resolution.to_owned(),
8643 );
8644 }
8645 if let Some(allowlist) = js_runtime_enforced_builtins(vm) {
8649 env.insert(
8650 String::from("AGENT_OS_JS_BUILTIN_ALLOWLIST"),
8651 serde_json::to_string(&allowlist).map_err(|error| {
8652 SidecarError::InvalidState(format!(
8653 "failed to encode jsRuntime builtin allow-list: {error}"
8654 ))
8655 })?,
8656 );
8657 }
8658 env.insert(
8659 String::from("AGENT_OS_VIRTUAL_OS_USER"),
8660 user.username.clone(),
8661 );
8662 env.insert(
8663 String::from("AGENT_OS_VIRTUAL_OS_HOMEDIR"),
8664 user.homedir.clone(),
8665 );
8666 env.insert(
8667 String::from("AGENT_OS_VIRTUAL_OS_SHELL"),
8668 user.shell.clone(),
8669 );
8670 env.insert(
8671 String::from("AGENT_OS_VIRTUAL_OS_CPU_COUNT"),
8672 virtual_os_cpu_count(resource_limits).to_string(),
8673 );
8674 env.insert(
8675 String::from("AGENT_OS_VIRTUAL_OS_TOTALMEM"),
8676 virtual_os_totalmem_bytes(resource_limits).to_string(),
8677 );
8678 env.insert(
8679 String::from("AGENT_OS_VIRTUAL_OS_FREEMEM"),
8680 virtual_os_freemem_bytes(resource_limits).to_string(),
8681 );
8682 env.insert(
8683 String::from("AGENT_OS_VIRTUAL_PROCESS_UID"),
8684 user.uid.to_string(),
8685 );
8686 env.insert(
8687 String::from("AGENT_OS_VIRTUAL_PROCESS_GID"),
8688 user.gid.to_string(),
8689 );
8690 env.entry(String::from("HOME"))
8691 .or_insert_with(|| user.homedir.clone());
8692 env.entry(String::from("USER"))
8693 .or_insert_with(|| user.username.clone());
8694 env.entry(String::from("LOGNAME"))
8695 .or_insert_with(|| user.username.clone());
8696 env.entry(String::from("SHELL"))
8697 .or_insert_with(|| user.shell.clone());
8698 env.entry(String::from("PATH")).or_insert_with(|| {
8699 vm.guest_env
8700 .get("PATH")
8701 .cloned()
8702 .unwrap_or_else(|| crate::vm::DEFAULT_GUEST_PATH_ENV.to_owned())
8703 });
8704 env.entry(String::from("TMPDIR"))
8705 .or_insert_with(|| String::from("/tmp"));
8706 env.insert(String::from("PWD"), guest_cwd.to_owned());
8707 if !loopback_exempt_ports.is_empty() {
8708 env.insert(
8709 String::from(LOOPBACK_EXEMPT_PORTS_ENV),
8710 serde_json::to_string(&loopback_exempt_ports).map_err(|error| {
8711 SidecarError::InvalidState(format!("failed to encode loopback exemptions: {error}"))
8712 })?,
8713 );
8714 }
8715 if let Some(guest_entrypoint) = guest_entrypoint {
8716 env.insert(String::from("AGENT_OS_GUEST_ENTRYPOINT"), guest_entrypoint);
8717 }
8718 Ok(())
8719}
8720
8721fn virtual_os_cpu_count(resource_limits: &ResourceLimits) -> usize {
8722 resource_limits.virtual_cpu_count.unwrap_or(1).max(1)
8723}
8724
8725fn virtual_os_totalmem_bytes(resource_limits: &ResourceLimits) -> u64 {
8726 resource_limits
8727 .max_wasm_memory_bytes
8728 .unwrap_or(1024 * 1024 * 1024)
8729}
8730
8731fn virtual_os_freemem_bytes(resource_limits: &ResourceLimits) -> u64 {
8732 resource_limits
8733 .max_wasm_memory_bytes
8734 .unwrap_or(512 * 1024 * 1024)
8735}
8736
8737fn js_runtime_platform(vm: &VmState) -> vm_config::JsRuntimePlatform {
8740 vm.configuration
8741 .js_runtime
8742 .as_ref()
8743 .map(|cfg| cfg.platform)
8744 .unwrap_or(vm_config::JsRuntimePlatform::Node)
8745}
8746
8747fn js_runtime_platform_env(vm: &VmState) -> &'static str {
8750 match js_runtime_platform(vm) {
8751 vm_config::JsRuntimePlatform::Node => "node",
8752 vm_config::JsRuntimePlatform::Browser => "browser",
8753 vm_config::JsRuntimePlatform::Neutral => "neutral",
8754 vm_config::JsRuntimePlatform::Bare => "bare",
8755 }
8756}
8757
8758fn js_runtime_module_resolution_env(vm: &VmState) -> Option<&'static str> {
8761 let resolution = vm
8762 .configuration
8763 .js_runtime
8764 .as_ref()
8765 .map(|cfg| cfg.module_resolution)
8766 .unwrap_or(vm_config::JsModuleResolution::Node);
8767 match resolution {
8768 vm_config::JsModuleResolution::Node => None,
8769 vm_config::JsModuleResolution::Relative => Some("relative"),
8770 vm_config::JsModuleResolution::None => Some("none"),
8771 }
8772}
8773
8774fn js_runtime_enforced_builtins(vm: &VmState) -> Option<Vec<String>> {
8778 if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
8779 return Some(Vec::new());
8780 }
8781 vm.configuration
8782 .js_runtime
8783 .as_ref()
8784 .and_then(|cfg| cfg.allowed_builtins.clone())
8785}
8786
8787fn configured_allowed_node_builtins(vm: &VmState) -> Vec<String> {
8788 if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
8790 return Vec::new();
8791 }
8792 let configured = match vm
8795 .configuration
8796 .js_runtime
8797 .as_ref()
8798 .and_then(|cfg| cfg.allowed_builtins.as_ref())
8799 {
8800 Some(list) => list.clone(),
8801 None => DEFAULT_ALLOWED_NODE_BUILTINS
8802 .iter()
8803 .map(|value| (*value).to_owned())
8804 .collect::<Vec<_>>(),
8805 };
8806 dedupe_strings(&configured)
8807}
8808
8809fn configured_loopback_exempt_ports(vm: &VmState) -> Vec<String> {
8810 if !vm.configuration.loopback_exempt_ports.is_empty() {
8811 return vm
8812 .configuration
8813 .loopback_exempt_ports
8814 .iter()
8815 .map(ToString::to_string)
8816 .collect();
8817 }
8818
8819 vm.create_loopback_exempt_ports
8820 .iter()
8821 .map(ToString::to_string)
8822 .collect()
8823}
8824
8825fn mount_config_host_path(config: &str) -> Option<String> {
8827 serde_json::from_str::<Value>(config)
8828 .ok()?
8829 .get("hostPath")
8830 .and_then(Value::as_str)
8831 .map(str::to_owned)
8832}
8833
8834fn runtime_guest_writable_host_paths(vm: &VmState) -> Vec<PathBuf> {
8835 vm.configuration
8836 .mounts
8837 .iter()
8838 .filter(|mount| !mount.read_only)
8839 .filter_map(|mount| {
8840 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
8841 .then(|| mount_config_host_path(&mount.plugin.config))
8842 .flatten()
8843 .map(PathBuf::from)
8844 })
8845 .collect()
8846}
8847
8848fn runtime_guest_path_mappings(vm: &VmState) -> Vec<RuntimeGuestPathMapping> {
8849 let mut mappings = vm
8850 .configuration
8851 .mounts
8852 .iter()
8853 .filter_map(|mount| {
8854 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
8855 .then(|| {
8856 mount_config_host_path(&mount.plugin.config).map(|host_path| {
8857 RuntimeGuestPathMapping {
8858 guest_path: normalize_path(&mount.guest_path),
8859 host_path,
8860 read_only: mount.read_only,
8861 }
8862 })
8863 })
8864 .flatten()
8865 })
8866 .collect::<Vec<_>>();
8867 let mut command_root_mappings = vm
8868 .command_guest_paths
8869 .values()
8870 .filter_map(|guest_path| {
8871 Path::new(guest_path)
8872 .parent()
8873 .and_then(|parent| parent.to_str())
8874 .map(normalize_path)
8875 })
8876 .collect::<BTreeSet<_>>()
8877 .into_iter()
8878 .map(|guest_path| RuntimeGuestPathMapping {
8879 host_path: resolve_vm_guest_path_to_host(vm, &guest_path)
8880 .to_string_lossy()
8881 .into_owned(),
8882 guest_path,
8883 read_only: false,
8884 })
8885 .collect::<Vec<_>>();
8886 mappings.append(&mut command_root_mappings);
8887 let mut extra_node_modules_roots = mappings
8888 .iter()
8889 .filter(|mapping| mapping.guest_path.starts_with("/root/node_modules/"))
8890 .filter_map(|mapping| {
8891 host_node_modules_root(Path::new(&mapping.host_path)).map(|host_root| {
8892 RuntimeGuestPathMapping {
8893 guest_path: String::from("/root/node_modules"),
8894 host_path: host_root.to_string_lossy().into_owned(),
8895 read_only: mapping.read_only,
8896 }
8897 })
8898 })
8899 .collect::<Vec<_>>();
8900 mappings.append(&mut extra_node_modules_roots);
8901 mappings.push(RuntimeGuestPathMapping {
8902 guest_path: String::from("/"),
8903 host_path: vm.cwd.to_string_lossy().into_owned(),
8904 read_only: false,
8905 });
8906 mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.guest_path.len()));
8907 mappings.dedup_by(|left, right| {
8908 left.guest_path == right.guest_path && left.host_path == right.host_path
8909 });
8910 mappings
8911}
8912
8913fn build_module_reader(vm: &VmState) -> Option<crate::plugins::host_dir::HostDirModuleReader> {
8924 let mut pairs: Vec<(String, PathBuf)> = vm
8925 .configuration
8926 .mounts
8927 .iter()
8928 .filter(|mount| mount.read_only)
8929 .filter(|mount| (mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
8930 .filter_map(|mount| {
8931 mount_config_host_path(&mount.plugin.config)
8932 .map(|host_path| (normalize_path(&mount.guest_path), PathBuf::from(host_path)))
8933 })
8934 .collect();
8935
8936 let extra_roots: Vec<(String, PathBuf)> = pairs
8940 .iter()
8941 .filter(|(guest_path, _)| guest_path.starts_with("/root/node_modules/"))
8942 .filter_map(|(_, host_path)| {
8943 host_node_modules_root(host_path).map(|root| (String::from("/root/node_modules"), root))
8944 })
8945 .collect();
8946 pairs.extend(extra_roots);
8947
8948 crate::plugins::host_dir::HostDirModuleReader::from_mounts(pairs)
8949}
8950
8951fn host_node_modules_root(path: &Path) -> Option<PathBuf> {
8952 if let Some(root) = path
8953 .ancestors()
8954 .filter(|candidate| {
8955 candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
8956 })
8957 .last()
8958 .map(Path::to_path_buf)
8959 {
8960 return Some(root);
8961 }
8962
8963 fs::canonicalize(path)
8964 .ok()?
8965 .ancestors()
8966 .filter(|candidate| {
8967 candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
8968 })
8969 .last()
8970 .map(Path::to_path_buf)
8971}
8972
8973#[cfg(test)]
8974mod runtime_guest_path_mapping_tests {
8975 use super::{host_node_modules_root, javascript_sync_rpc_option_bool};
8976 use serde_json::json;
8977 use std::fs;
8978 use std::time::{SystemTime, UNIX_EPOCH};
8979
8980 #[test]
8981 fn host_node_modules_root_prefers_workspace_root_over_pnpm_package_node_modules() {
8982 let unique = SystemTime::now()
8983 .duration_since(UNIX_EPOCH)
8984 .expect("clock should be monotonic")
8985 .as_nanos();
8986 let temp = std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-{unique}"));
8987 let workspace_node_modules = temp.join("node_modules");
8988 let package_root = workspace_node_modules
8989 .join(".pnpm")
8990 .join("example@1.0.0")
8991 .join("node_modules")
8992 .join("@scope")
8993 .join("pkg");
8994 fs::create_dir_all(&package_root).expect("package root should be created");
8995
8996 let resolved =
8997 host_node_modules_root(&package_root).expect("node_modules root should resolve");
8998
8999 assert_eq!(resolved, workspace_node_modules);
9000
9001 fs::remove_dir_all(&temp).expect("temp tree should be removed");
9002 }
9003
9004 #[test]
9005 fn host_node_modules_root_preserves_symlinked_workspace_node_modules_path() {
9006 let unique = SystemTime::now()
9007 .duration_since(UNIX_EPOCH)
9008 .expect("clock should be monotonic")
9009 .as_nanos();
9010 let temp =
9011 std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-symlink-{unique}"));
9012 let workspace_node_modules = temp.join("node_modules");
9013 let package_link = workspace_node_modules.join("@scope").join("pkg");
9014 let real_package = temp.join("registry").join("agent").join("pkg");
9015 fs::create_dir_all(package_link.parent().expect("package parent should exist"))
9016 .expect("scoped parent should be created");
9017 fs::create_dir_all(&real_package).expect("real package root should be created");
9018 std::os::unix::fs::symlink(&real_package, &package_link)
9019 .expect("package symlink should be created");
9020
9021 let resolved =
9022 host_node_modules_root(&package_link).expect("node_modules root should resolve");
9023
9024 assert_eq!(resolved, workspace_node_modules);
9025
9026 fs::remove_dir_all(&temp).expect("temp tree should be removed");
9027 }
9028
9029 #[test]
9030 fn javascript_sync_rpc_option_bool_accepts_boolean_recursive_argument() {
9031 assert_eq!(
9032 javascript_sync_rpc_option_bool(&[json!("/workspace"), json!(true)], 1, "recursive"),
9033 Some(true)
9034 );
9035 assert_eq!(
9036 javascript_sync_rpc_option_bool(
9037 &[json!("/workspace"), json!({ "recursive": false })],
9038 1,
9039 "recursive"
9040 ),
9041 Some(false)
9042 );
9043 }
9044}
9045
9046#[cfg(test)]
9047mod kernel_poll_sync_rpc_tests {
9048 use super::{
9049 service_javascript_kernel_poll_sync_rpc, ActiveExecution, ActiveProcess,
9050 JavascriptSyncRpcRequest, KernelPollFdResponse, SidecarKernel, ToolExecution,
9051 EXECUTION_DRIVER_NAME, JAVASCRIPT_COMMAND,
9052 };
9053 use secure_exec_kernel::command_registry::CommandDriver;
9054 use secure_exec_kernel::kernel::{KernelVmConfig, SpawnOptions};
9055 use secure_exec_kernel::mount_table::MountTable;
9056 use secure_exec_kernel::permissions::Permissions;
9057 use secure_exec_kernel::poll::{POLLHUP, POLLIN};
9058 use secure_exec_kernel::vfs::MemoryFileSystem;
9059 use serde_json::{json, Value};
9060 #[test]
9061 fn javascript_kernel_poll_sync_rpc_reports_multiple_kernel_fds() {
9062 let mut config = KernelVmConfig::new("vm-js-kernel-poll");
9063 config.permissions = Permissions::allow_all();
9064 let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config);
9065 kernel
9066 .register_driver(CommandDriver::new(
9067 EXECUTION_DRIVER_NAME,
9068 [JAVASCRIPT_COMMAND],
9069 ))
9070 .expect("register execution driver");
9071
9072 let kernel_handle = kernel
9073 .spawn_process(
9074 JAVASCRIPT_COMMAND,
9075 Vec::new(),
9076 SpawnOptions {
9077 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
9078 ..SpawnOptions::default()
9079 },
9080 )
9081 .expect("spawn javascript kernel process");
9082 let pid = kernel_handle.pid();
9083
9084 let (stdin_read_fd, stdin_write_fd) = kernel
9085 .open_pipe(EXECUTION_DRIVER_NAME, pid)
9086 .expect("open kernel stdin pipe");
9087 kernel
9088 .fd_dup2(EXECUTION_DRIVER_NAME, pid, stdin_read_fd, 0)
9089 .expect("dup stdin pipe onto fd 0");
9090 kernel
9091 .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_read_fd)
9092 .expect("close original stdin read fd");
9093
9094 let process = ActiveProcess::new(
9095 pid,
9096 kernel_handle,
9097 super::GuestRuntimeKind::JavaScript,
9098 ActiveExecution::Tool(ToolExecution::default()),
9099 );
9100
9101 kernel
9102 .fd_write(EXECUTION_DRIVER_NAME, pid, stdin_write_fd, b"poll-ready")
9103 .expect("write kernel stdin payload");
9104 kernel
9105 .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_write_fd)
9106 .expect("close kernel stdin writer");
9107
9108 let response = service_javascript_kernel_poll_sync_rpc(
9109 &mut kernel,
9110 &process,
9111 &JavascriptSyncRpcRequest {
9112 id: 1,
9113 method: String::from("__kernel_poll"),
9114 args: vec![
9115 json!([
9116 { "fd": 0, "events": POLLIN.bits() },
9117 { "fd": 1, "events": POLLIN.bits() }
9118 ]),
9119 json!(250),
9120 ],
9121 },
9122 )
9123 .expect("poll kernel fds");
9124
9125 assert_eq!(response["readyCount"], Value::from(1));
9126 let fds: Vec<KernelPollFdResponse> =
9127 serde_json::from_value(response["fds"].clone()).expect("kernel poll fd response");
9128 assert_eq!(
9129 fds,
9130 vec![
9131 KernelPollFdResponse {
9132 fd: 0,
9133 events: POLLIN.bits(),
9134 revents: (POLLIN | POLLHUP).bits(),
9135 },
9136 KernelPollFdResponse {
9137 fd: 1,
9138 events: POLLIN.bits(),
9139 revents: 0,
9140 },
9141 ]
9142 );
9143
9144 process.kernel_handle.finish(0);
9145 kernel.waitpid(pid).expect("wait javascript kernel process");
9146 }
9147}
9148
9149fn dedupe_strings(values: &[String]) -> Vec<String> {
9150 let mut seen = BTreeSet::new();
9151 let mut deduped = Vec::new();
9152 for value in values {
9153 if seen.insert(value.clone()) {
9154 deduped.push(value.clone());
9155 }
9156 }
9157 deduped
9158}
9159
9160fn dedupe_host_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
9161 let mut seen = BTreeSet::new();
9162 let mut deduped = Vec::new();
9163 for path in paths {
9164 let normalized = normalize_host_path(path);
9165 let key = normalized.to_string_lossy().into_owned();
9166 if seen.insert(key) {
9167 deduped.push(normalized);
9168 }
9169 }
9170 deduped
9171}
9172
9173fn expand_host_access_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
9174 let mut expanded = Vec::new();
9175 let mut seen = BTreeSet::new();
9176
9177 let mut add_path = |candidate: PathBuf| {
9178 let normalized = normalize_host_path(&candidate);
9179 let key = normalized.to_string_lossy().into_owned();
9180 if seen.insert(key) {
9181 expanded.push(normalized);
9182 }
9183 };
9184
9185 for host_path in paths {
9186 add_path(host_path.clone());
9187 if let Ok(realpath) = fs::canonicalize(host_path) {
9188 add_path(realpath);
9189 }
9190
9191 if host_path.file_name().and_then(|name| name.to_str()) != Some("node_modules") {
9192 continue;
9193 }
9194
9195 let mut current = host_path.parent();
9196 while let Some(parent) = current {
9197 let candidate = parent.join("node_modules");
9198 if candidate.exists() {
9199 add_path(candidate.clone());
9200 if let Ok(realpath) = fs::canonicalize(&candidate) {
9201 add_path(realpath);
9202 }
9203 }
9204 current = parent.parent();
9205 }
9206 }
9207
9208 expanded
9209}
9210
9211fn prepare_javascript_shadow(
9212 vm: &mut VmState,
9213 resolved: &ResolvedChildProcessExecution,
9214) -> Result<(), SidecarError> {
9215 let guest_entrypoint = resolved
9216 .env
9217 .get("AGENT_OS_GUEST_ENTRYPOINT")
9218 .cloned()
9219 .or_else(|| {
9227 resolve_host_entrypoint_within_vm_host_cwd(vm, &resolved.entrypoint)
9228 .map(|(guest_entrypoint, _)| guest_entrypoint)
9229 })
9230 .or_else(|| {
9231 resolved
9232 .entrypoint
9233 .starts_with('/')
9234 .then(|| normalize_path(&resolved.entrypoint))
9235 });
9236 let Some(guest_entrypoint) = guest_entrypoint else {
9237 return Ok(());
9238 };
9239 if host_mount_path_for_guest_path(vm, &guest_entrypoint).is_some() {
9240 return Ok(());
9241 }
9242 if vm.kernel.lstat(&guest_entrypoint).is_err() {
9243 let host_entrypoint = {
9244 let candidate = Path::new(&resolved.entrypoint);
9245 if candidate.is_absolute() {
9246 candidate.to_path_buf()
9247 } else {
9248 resolved.host_cwd.join(candidate)
9249 }
9250 };
9251 if host_entrypoint.exists() {
9252 materialize_host_path_to_shadow(vm, &guest_entrypoint, &host_entrypoint)?;
9253 return sync_shadow_entrypoint_into_kernel(vm, &guest_entrypoint);
9258 }
9259 }
9260 materialize_guest_path_to_shadow(vm, &guest_entrypoint)
9261}
9262
9263fn sync_shadow_entrypoint_into_kernel(
9268 vm: &mut VmState,
9269 guest_entrypoint: &str,
9270) -> Result<(), SidecarError> {
9271 if vm.kernel.exists(guest_entrypoint).unwrap_or(false) {
9272 return Ok(());
9273 }
9274 let shadow_path = shadow_path_for_guest(vm, guest_entrypoint);
9275 let bytes = match fs::read(&shadow_path) {
9276 Ok(bytes) => bytes,
9277 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
9278 Err(error) => {
9279 return Err(SidecarError::Io(format!(
9280 "failed to read staged shadow entrypoint {}: {error}",
9281 shadow_path.display()
9282 )));
9283 }
9284 };
9285 if let Some(parent) = guest_parent_path(guest_entrypoint) {
9286 if !vm.kernel.exists(&parent).unwrap_or(false) {
9287 vm.kernel.mkdir(&parent, true).map_err(kernel_error)?;
9288 }
9289 }
9290 vm.kernel
9291 .write_file(guest_entrypoint, bytes)
9292 .map_err(kernel_error)?;
9293 Ok(())
9294}
9295
9296fn guest_parent_path(guest_path: &str) -> Option<String> {
9297 let parent = Path::new(guest_path).parent()?;
9298 let parent = parent.to_string_lossy();
9299 if parent.is_empty() || parent == "/" {
9300 None
9301 } else {
9302 Some(parent.into_owned())
9303 }
9304}
9305
9306fn materialize_host_path_to_shadow(
9307 vm: &VmState,
9308 guest_path: &str,
9309 host_path: &Path,
9310) -> Result<(), SidecarError> {
9311 let shadow_path = shadow_path_for_guest(vm, guest_path);
9312 let metadata = fs::symlink_metadata(host_path)
9313 .map_err(|error| SidecarError::Io(format!("failed to stat host entrypoint: {error}")))?;
9314
9315 if metadata.file_type().is_symlink() {
9316 if let Some(parent) = shadow_path.parent() {
9317 fs::create_dir_all(parent).map_err(|error| {
9318 SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
9319 })?;
9320 }
9321 let _ = fs::remove_file(&shadow_path);
9322 let _ = fs::remove_dir_all(&shadow_path);
9323 let target = fs::read_link(host_path)
9324 .map_err(|error| SidecarError::Io(format!("failed to read host symlink: {error}")))?;
9325 std::os::unix::fs::symlink(&target, &shadow_path)
9326 .map_err(|error| SidecarError::Io(format!("failed to mirror host symlink: {error}")))?;
9327 return Ok(());
9328 }
9329
9330 if metadata.is_dir() {
9331 fs::create_dir_all(&shadow_path).map_err(|error| {
9332 SidecarError::Io(format!("failed to create shadow directory: {error}"))
9333 })?;
9334 fs::set_permissions(
9335 &shadow_path,
9336 fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
9337 )
9338 .map_err(|error| {
9339 SidecarError::Io(format!(
9340 "failed to set shadow directory mode on {}: {error}",
9341 shadow_path.display()
9342 ))
9343 })?;
9344 return Ok(());
9345 }
9346
9347 if let Some(parent) = shadow_path.parent() {
9348 fs::create_dir_all(parent).map_err(|error| {
9349 SidecarError::Io(format!("failed to create shadow parent: {error}"))
9350 })?;
9351 }
9352 let bytes = fs::read(host_path)
9353 .map_err(|error| SidecarError::Io(format!("failed to read host entrypoint: {error}")))?;
9354 fs::write(&shadow_path, bytes).map_err(|error| {
9355 SidecarError::Io(format!(
9356 "failed to mirror host file into shadow root: {error}"
9357 ))
9358 })?;
9359 fs::set_permissions(
9360 &shadow_path,
9361 fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
9362 )
9363 .map_err(|error| {
9364 SidecarError::Io(format!(
9365 "failed to set shadow file mode on {}: {error}",
9366 shadow_path.display()
9367 ))
9368 })?;
9369 Ok(())
9370}
9371
9372fn materialize_guest_path_to_shadow(
9373 vm: &mut VmState,
9374 guest_path: &str,
9375) -> Result<(), SidecarError> {
9376 let stat = vm.kernel.lstat(guest_path).map_err(kernel_error)?;
9377 let shadow_path = shadow_path_for_guest(vm, guest_path);
9378
9379 if stat.is_symbolic_link {
9380 if let Some(parent) = shadow_path.parent() {
9381 fs::create_dir_all(parent).map_err(|error| {
9382 SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
9383 })?;
9384 }
9385 let _ = fs::remove_file(&shadow_path);
9386 let _ = fs::remove_dir_all(&shadow_path);
9387 let target = vm.kernel.read_link(guest_path).map_err(kernel_error)?;
9388 std::os::unix::fs::symlink(&target, &shadow_path)
9389 .map_err(|error| SidecarError::Io(format!("failed to mirror symlink: {error}")))?;
9390 return Ok(());
9391 }
9392
9393 if stat.is_directory {
9394 fs::create_dir_all(&shadow_path).map_err(|error| {
9395 SidecarError::Io(format!("failed to create shadow directory: {error}"))
9396 })?;
9397 fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
9398 |error| {
9399 SidecarError::Io(format!(
9400 "failed to set shadow directory mode on {}: {error}",
9401 shadow_path.display()
9402 ))
9403 },
9404 )?;
9405 return Ok(());
9406 }
9407
9408 if let Some(parent) = shadow_path.parent() {
9409 fs::create_dir_all(parent).map_err(|error| {
9410 SidecarError::Io(format!("failed to create shadow parent: {error}"))
9411 })?;
9412 }
9413 let bytes = vm.kernel.read_file(guest_path).map_err(kernel_error)?;
9414 fs::write(&shadow_path, bytes).map_err(|error| {
9415 SidecarError::Io(format!(
9416 "failed to mirror guest file into shadow root: {error}"
9417 ))
9418 })?;
9419 fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
9420 |error| {
9421 SidecarError::Io(format!(
9422 "failed to set shadow file mode on {}: {error}",
9423 shadow_path.display()
9424 ))
9425 },
9426 )?;
9427 Ok(())
9428}
9429
9430fn load_javascript_entrypoint_source(
9431 vm: &mut VmState,
9432 host_cwd: &Path,
9433 entrypoint: &str,
9434 env: &BTreeMap<String, String>,
9435) -> Option<String> {
9436 let mut read_guest_file = |path: &str| {
9437 vm.kernel
9438 .read_file(path)
9439 .ok()
9440 .and_then(|bytes| String::from_utf8(bytes).ok())
9441 };
9442
9443 if let Some(source) = env
9444 .get("AGENT_OS_GUEST_ENTRYPOINT")
9445 .filter(|path| path.starts_with('/'))
9446 .and_then(|path| read_guest_file(path))
9447 {
9448 return Some(source);
9449 }
9450
9451 if entrypoint.starts_with('/') {
9452 if let Some(source) = read_guest_file(entrypoint) {
9453 return Some(source);
9454 }
9455 }
9456
9457 let host_entrypoint = if Path::new(entrypoint).is_absolute() {
9458 PathBuf::from(entrypoint)
9459 } else {
9460 host_cwd.join(entrypoint)
9461 };
9462 let normalized_entrypoint = normalize_host_path(&host_entrypoint);
9463 let sandbox_root = normalize_host_path(&vm.cwd);
9464 let host_cwd = normalize_host_path(&vm.host_cwd);
9465 if !path_is_within_root(&normalized_entrypoint, &sandbox_root)
9466 && !path_is_within_root(&normalized_entrypoint, &host_cwd)
9467 {
9468 return None;
9469 }
9470
9471 fs::read_to_string(&normalized_entrypoint).ok()
9472}
9473
9474fn apply_wasm_limit_env(env: &mut BTreeMap<String, String>, limits: &ResourceLimits) {
9475 if let Some(limit) = limits.max_wasm_fuel {
9476 env.insert(String::from(WASM_MAX_FUEL_ENV), limit.to_string());
9477 }
9478 if let Some(limit) = limits.max_wasm_memory_bytes {
9479 env.insert(String::from(WASM_MAX_MEMORY_BYTES_ENV), limit.to_string());
9480 }
9481 if let Some(limit) = limits.max_wasm_stack_bytes {
9482 env.insert(String::from(WASM_MAX_STACK_BYTES_ENV), limit.to_string());
9483 }
9484}
9485
9486fn emit_dns_resolution_event<B>(
9487 bridge: &SharedBridge<B>,
9488 vm_id: &str,
9489 hostname: &str,
9490 source: KernelDnsResolutionSource,
9491 addresses: &[IpAddr],
9492 dns: &VmDnsConfig,
9493) where
9494 B: NativeSidecarBridge + Send + 'static,
9495 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9496{
9497 let _ = emit_structured_event(
9498 bridge,
9499 vm_id,
9500 "network.dns.resolved",
9501 audit_fields([
9502 ("hostname", hostname.to_owned()),
9503 ("source", source.as_str().to_owned()),
9504 (
9505 "addresses",
9506 addresses
9507 .iter()
9508 .map(ToString::to_string)
9509 .collect::<Vec<_>>()
9510 .join(","),
9511 ),
9512 ("address_count", addresses.len().to_string()),
9513 ("resolver_count", dns.name_servers.len().to_string()),
9514 (
9515 "resolvers",
9516 dns.name_servers
9517 .iter()
9518 .map(ToString::to_string)
9519 .collect::<Vec<_>>()
9520 .join(","),
9521 ),
9522 ]),
9523 );
9524}
9525
9526fn emit_dns_record_resolution_event<B>(
9527 bridge: &SharedBridge<B>,
9528 vm_id: &str,
9529 hostname: &str,
9530 resolution: &DnsRecordResolution,
9531 dns: &VmDnsConfig,
9532) where
9533 B: NativeSidecarBridge + Send + 'static,
9534 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9535{
9536 if let Some(addresses) = dns_resolution_ip_addrs(resolution.records()) {
9537 emit_dns_resolution_event(
9538 bridge,
9539 vm_id,
9540 hostname,
9541 resolution.source(),
9542 &addresses,
9543 dns,
9544 );
9545 return;
9546 }
9547
9548 let _ = emit_structured_event(
9549 bridge,
9550 vm_id,
9551 "network.dns.resolved",
9552 audit_fields([
9553 ("hostname", hostname.to_owned()),
9554 ("source", resolution.source().as_str().to_owned()),
9555 (
9556 "addresses",
9557 resolution
9558 .records()
9559 .iter()
9560 .map(summarize_dns_record)
9561 .collect::<Vec<_>>()
9562 .join(","),
9563 ),
9564 ("address_count", resolution.records().len().to_string()),
9565 ("resolver_count", dns.name_servers.len().to_string()),
9566 (
9567 "resolvers",
9568 dns.name_servers
9569 .iter()
9570 .map(ToString::to_string)
9571 .collect::<Vec<_>>()
9572 .join(","),
9573 ),
9574 ]),
9575 );
9576}
9577
9578fn emit_dns_resolution_failure_event<B>(
9579 bridge: &SharedBridge<B>,
9580 vm_id: &str,
9581 hostname: &str,
9582 dns: &VmDnsConfig,
9583 error: &SidecarError,
9584) where
9585 B: NativeSidecarBridge + Send + 'static,
9586 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9587{
9588 let _ = emit_structured_event(
9589 bridge,
9590 vm_id,
9591 "network.dns.resolve_failed",
9592 audit_fields([
9593 ("hostname", hostname.to_owned()),
9594 ("reason", error.to_string()),
9595 ("resolver_count", dns.name_servers.len().to_string()),
9596 (
9597 "resolvers",
9598 dns.name_servers
9599 .iter()
9600 .map(ToString::to_string)
9601 .collect::<Vec<_>>()
9602 .join(","),
9603 ),
9604 ]),
9605 );
9606}
9607
9608fn parse_dns_record_type(rrtype: &str) -> Result<RecordType, SidecarError> {
9609 match rrtype {
9610 "A" => Ok(RecordType::A),
9611 "AAAA" => Ok(RecordType::AAAA),
9612 "MX" => Ok(RecordType::MX),
9613 "TXT" => Ok(RecordType::TXT),
9614 "SRV" => Ok(RecordType::SRV),
9615 "CNAME" => Ok(RecordType::CNAME),
9616 "PTR" => Ok(RecordType::PTR),
9617 "NS" => Ok(RecordType::NS),
9618 "SOA" => Ok(RecordType::SOA),
9619 "NAPTR" => Ok(RecordType::NAPTR),
9620 "CAA" => Ok(RecordType::CAA),
9621 "ANY" => Ok(RecordType::ANY),
9622 other => Err(SidecarError::Execution(format!(
9623 "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
9624 ))),
9625 }
9626}
9627
9628fn dns_resolution_to_node_value(
9629 resolution: &DnsRecordResolution,
9630 requested_type: &str,
9631) -> Result<Value, SidecarError> {
9632 let safe_ips = dns_resolution_safe_ip_set(resolution.records(), resolution.hostname())?;
9633 match requested_type {
9634 "A" | "AAAA" => Ok(Value::Array(
9635 resolution
9636 .records()
9637 .iter()
9638 .filter_map(|record| dns_record_ip_string(record, &safe_ips))
9639 .map(Value::String)
9640 .collect(),
9641 )),
9642 "MX" => Ok(Value::Array(
9643 resolution
9644 .records()
9645 .iter()
9646 .filter_map(|record| match record.data() {
9647 RData::MX(mx) => Some(json!({
9648 "priority": mx.preference,
9649 "exchange": normalize_dns_name_for_node(&mx.exchange),
9650 "type": "MX",
9651 })),
9652 _ => None,
9653 })
9654 .collect(),
9655 )),
9656 "TXT" => Ok(Value::Array(
9657 resolution
9658 .records()
9659 .iter()
9660 .filter_map(|record| match record.data() {
9661 RData::TXT(txt) => Some(Value::Array(
9662 txt.txt_data
9663 .iter()
9664 .map(|entry| Value::String(String::from_utf8_lossy(entry).into_owned()))
9665 .collect(),
9666 )),
9667 _ => None,
9668 })
9669 .collect(),
9670 )),
9671 "SRV" => Ok(Value::Array(
9672 resolution
9673 .records()
9674 .iter()
9675 .filter_map(|record| match record.data() {
9676 RData::SRV(srv) => Some(json!({
9677 "priority": srv.priority,
9678 "weight": srv.weight,
9679 "port": srv.port,
9680 "name": normalize_dns_name_for_node(&srv.target),
9681 "type": "SRV",
9682 })),
9683 _ => None,
9684 })
9685 .collect(),
9686 )),
9687 "CNAME" => Ok(Value::Array(
9688 resolution
9689 .records()
9690 .iter()
9691 .filter_map(|record| match record.data() {
9692 RData::CNAME(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9693 _ => None,
9694 })
9695 .collect(),
9696 )),
9697 "PTR" => Ok(Value::Array(
9698 resolution
9699 .records()
9700 .iter()
9701 .filter_map(|record| match record.data() {
9702 RData::PTR(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9703 _ => None,
9704 })
9705 .collect(),
9706 )),
9707 "NS" => Ok(Value::Array(
9708 resolution
9709 .records()
9710 .iter()
9711 .filter_map(|record| match record.data() {
9712 RData::NS(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9713 _ => None,
9714 })
9715 .collect(),
9716 )),
9717 "SOA" => resolution
9718 .records()
9719 .iter()
9720 .find_map(|record| match record.data() {
9721 RData::SOA(soa) => Some(json!({
9722 "nsname": normalize_dns_name_for_node(&soa.mname),
9723 "hostmaster": normalize_dns_name_for_node(&soa.rname),
9724 "serial": soa.serial,
9725 "refresh": soa.refresh,
9726 "retry": soa.retry,
9727 "expire": soa.expire,
9728 "minttl": soa.minimum,
9729 })),
9730 _ => None,
9731 })
9732 .ok_or_else(|| {
9733 SidecarError::Execution(String::from("failed to resolve DNS SOA record"))
9734 }),
9735 "NAPTR" => Ok(Value::Array(
9736 resolution
9737 .records()
9738 .iter()
9739 .filter_map(|record| match record.data() {
9740 RData::NAPTR(naptr) => Some(json!({
9741 "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
9742 "service": String::from_utf8_lossy(&naptr.services).into_owned(),
9743 "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
9744 "replacement": normalize_dns_name_for_node(&naptr.replacement),
9745 "order": naptr.order,
9746 "preference": naptr.preference,
9747 })),
9748 _ => None,
9749 })
9750 .collect(),
9751 )),
9752 "CAA" => Ok(Value::Array(
9753 resolution
9754 .records()
9755 .iter()
9756 .filter_map(|record| match record.data() {
9757 RData::CAA(caa) => {
9758 let mut value = serde_json::Map::new();
9759 value.insert(
9760 "critical".to_owned(),
9761 Value::from(u8::from(caa.issuer_critical)),
9762 );
9763 value.insert("type".to_owned(), Value::String(String::from("CAA")));
9764 if caa.tag.eq_ignore_ascii_case("iodef") {
9765 value.insert(
9766 "iodef".to_owned(),
9767 Value::String(
9768 caa.value_as_iodef()
9769 .map(|url| url.to_string())
9770 .unwrap_or_else(|_| {
9771 String::from_utf8_lossy(&caa.value).into_owned()
9772 }),
9773 ),
9774 );
9775 } else if let Ok((issuer, _params)) = caa.value_as_issue() {
9776 let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
9777 "issuewild"
9778 } else {
9779 "issue"
9780 };
9781 value.insert(
9782 field.to_owned(),
9783 Value::String(
9784 issuer.as_ref().map(ToString::to_string).unwrap_or_else(|| {
9785 String::from_utf8_lossy(&caa.value).into_owned()
9786 }),
9787 ),
9788 );
9789 } else {
9790 value.insert(
9791 caa.tag.to_ascii_lowercase(),
9792 Value::String(String::from_utf8_lossy(&caa.value).into_owned()),
9793 );
9794 }
9795 Some(Value::Object(value))
9796 }
9797 _ => None,
9798 })
9799 .collect(),
9800 )),
9801 "ANY" => Ok(Value::Array(
9802 resolution
9803 .records()
9804 .iter()
9805 .filter_map(|record| dns_any_record_to_value(record, &safe_ips))
9806 .collect(),
9807 )),
9808 other => Err(SidecarError::Execution(format!(
9809 "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
9810 ))),
9811 }
9812}
9813
9814fn dns_resolution_safe_ip_set(
9815 records: &[Record],
9816 hostname: &str,
9817) -> Result<BTreeSet<IpAddr>, SidecarError> {
9818 let ips = records
9819 .iter()
9820 .filter_map(dns_record_ip_addr)
9821 .collect::<Vec<_>>();
9822 if ips.is_empty() {
9823 return Ok(BTreeSet::new());
9824 }
9825 Ok(filter_dns_safe_ip_addrs(ips, hostname)?
9826 .into_iter()
9827 .collect())
9828}
9829
9830fn dns_resolution_ip_addrs(records: &[Record]) -> Option<Vec<IpAddr>> {
9831 let ips = records
9832 .iter()
9833 .filter_map(dns_record_ip_addr)
9834 .collect::<Vec<_>>();
9835 if ips.is_empty() {
9836 return None;
9837 }
9838 Some(ips)
9839}
9840
9841fn dns_record_ip_addr(record: &Record) -> Option<IpAddr> {
9842 match record.data() {
9843 RData::A(address) => Some(IpAddr::V4(**address)),
9844 RData::AAAA(address) => Some(IpAddr::V6(**address)),
9845 _ => None,
9846 }
9847}
9848
9849fn dns_record_ip_string(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<String> {
9850 let ip = dns_record_ip_addr(record)?;
9851 safe_ips.contains(&ip).then(|| ip.to_string())
9852}
9853
9854fn dns_any_record_to_value(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<Value> {
9855 let value = match record.data() {
9856 RData::A(_) | RData::AAAA(_) => json!({
9857 "address": dns_record_ip_string(record, safe_ips)?,
9858 "ttl": record.ttl(),
9859 "type": record.record_type().to_string(),
9860 }),
9861 RData::MX(mx) => json!({
9862 "exchange": normalize_dns_name_for_node(&mx.exchange),
9863 "priority": mx.preference,
9864 "type": "MX",
9865 }),
9866 RData::TXT(txt) => json!({
9867 "entries": txt
9868 .txt_data
9869 .iter()
9870 .map(|entry| String::from_utf8_lossy(entry).into_owned())
9871 .collect::<Vec<_>>(),
9872 "type": "TXT",
9873 }),
9874 RData::SRV(srv) => json!({
9875 "name": normalize_dns_name_for_node(&srv.target),
9876 "port": srv.port,
9877 "priority": srv.priority,
9878 "weight": srv.weight,
9879 "type": "SRV",
9880 }),
9881 RData::CNAME(name) => json!({
9882 "value": normalize_dns_name_for_node(&name.0),
9883 "type": "CNAME",
9884 }),
9885 RData::PTR(name) => json!({
9886 "value": normalize_dns_name_for_node(&name.0),
9887 "type": "PTR",
9888 }),
9889 RData::NS(name) => json!({
9890 "value": normalize_dns_name_for_node(&name.0),
9891 "type": "NS",
9892 }),
9893 RData::SOA(soa) => json!({
9894 "nsname": normalize_dns_name_for_node(&soa.mname),
9895 "hostmaster": normalize_dns_name_for_node(&soa.rname),
9896 "serial": soa.serial,
9897 "refresh": soa.refresh,
9898 "retry": soa.retry,
9899 "expire": soa.expire,
9900 "minttl": soa.minimum,
9901 "type": "SOA",
9902 }),
9903 RData::NAPTR(naptr) => json!({
9904 "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
9905 "service": String::from_utf8_lossy(&naptr.services).into_owned(),
9906 "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
9907 "replacement": normalize_dns_name_for_node(&naptr.replacement),
9908 "order": naptr.order,
9909 "preference": naptr.preference,
9910 "type": "NAPTR",
9911 }),
9912 RData::CAA(caa) => {
9913 let mut value = serde_json::Map::new();
9914 value.insert(
9915 "critical".to_owned(),
9916 Value::from(u8::from(caa.issuer_critical)),
9917 );
9918 value.insert("type".to_owned(), Value::String(String::from("CAA")));
9919 if caa.tag.eq_ignore_ascii_case("iodef") {
9920 value.insert(
9921 "iodef".to_owned(),
9922 Value::String(
9923 caa.value_as_iodef()
9924 .map(|url| url.to_string())
9925 .unwrap_or_else(|_| String::from_utf8_lossy(&caa.value).into_owned()),
9926 ),
9927 );
9928 } else if let Ok((issuer, _params)) = caa.value_as_issue() {
9929 let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
9930 "issuewild"
9931 } else {
9932 "issue"
9933 };
9934 value.insert(
9935 field.to_owned(),
9936 Value::String(
9937 issuer
9938 .as_ref()
9939 .map(ToString::to_string)
9940 .unwrap_or_else(|| String::from_utf8_lossy(&caa.value).into_owned()),
9941 ),
9942 );
9943 }
9944 Value::Object(value)
9945 }
9946 _ => return None,
9947 };
9948 Some(value)
9949}
9950
9951fn normalize_dns_name_for_node(name: &impl ToString) -> String {
9952 name.to_string().trim_end_matches('.').to_owned()
9953}
9954
9955fn summarize_dns_record(record: &Record) -> String {
9956 match record.data() {
9957 RData::A(_) | RData::AAAA(_) => record.data().to_string(),
9958 _ => format!("{} {}", record.record_type(), record.data()),
9959 }
9960}
9961
9962fn find_socket_state_entry(
9970 vm: Option<&VmState>,
9971 kind: SocketQueryKind,
9972 request: &FindListenerRequest,
9973) -> Result<Option<SocketStateEntry>, SidecarError> {
9974 let vm = vm.ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
9975
9976 for (process_id, process) in &vm.active_processes {
9977 if let Some(path) = request.path.as_deref() {
9978 if matches!(kind, SocketQueryKind::TcpListener) {
9979 for listener in process.unix_listeners.values() {
9980 if listener.path() != path {
9981 continue;
9982 }
9983 return Ok(Some(SocketStateEntry {
9984 process_id: process_id.to_owned(),
9985 host: None,
9986 port: None,
9987 path: Some(path.to_owned()),
9988 }));
9989 }
9990 }
9991 }
9992
9993 if request.path.is_none() {
9994 if let Some(entry) =
9995 find_kernel_socket_state_entry(&vm.kernel, process_id, process, kind, request)?
9996 {
9997 return Ok(Some(entry));
9998 }
9999
10000 match kind {
10001 SocketQueryKind::TcpListener => {
10002 for listener in process.tcp_listeners.values() {
10003 if listener.kernel_socket_id.is_some() {
10004 continue;
10005 }
10006 let local_addr = listener.guest_local_addr();
10007 let local_host = local_addr.ip().to_string();
10008 if !socket_host_matches(request.host.as_deref(), &local_host) {
10009 continue;
10010 }
10011 if let Some(port) = request.port {
10012 if local_addr.port() != port {
10013 continue;
10014 }
10015 }
10016 return Ok(Some(SocketStateEntry {
10017 process_id: process_id.to_owned(),
10018 host: Some(local_host),
10019 port: Some(local_addr.port()),
10020 path: None,
10021 }));
10022 }
10023 }
10024 SocketQueryKind::UdpBound => {
10025 for socket in process.udp_sockets.values() {
10026 if socket.kernel_socket_id.is_some() {
10027 continue;
10028 }
10029 let Some(local_addr) = socket.local_addr() else {
10030 continue;
10031 };
10032 let local_host = local_addr.ip().to_string();
10033 if !socket_host_matches(request.host.as_deref(), &local_host) {
10034 continue;
10035 }
10036 if let Some(port) = request.port {
10037 if local_addr.port() != port {
10038 continue;
10039 }
10040 }
10041 return Ok(Some(SocketStateEntry {
10042 process_id: process_id.to_owned(),
10043 host: Some(local_host),
10044 port: Some(local_addr.port()),
10045 path: None,
10046 }));
10047 }
10048 }
10049 }
10050 }
10051
10052 let child_pid = process.execution.child_pid();
10053 let inodes = socket_inodes_for_pid(child_pid)?;
10054 if inodes.is_empty() {
10055 continue;
10056 }
10057
10058 if let Some(path) = request.path.as_deref() {
10059 if let Some(listener) = find_unix_socket_for_pid(child_pid, &inodes, path, process_id)?
10060 {
10061 return Ok(Some(listener));
10062 }
10063 continue;
10064 }
10065
10066 let table_paths = match kind {
10067 SocketQueryKind::TcpListener => [
10068 format!("/proc/{child_pid}/net/tcp"),
10069 format!("/proc/{child_pid}/net/tcp6"),
10070 ],
10071 SocketQueryKind::UdpBound => [
10072 format!("/proc/{child_pid}/net/udp"),
10073 format!("/proc/{child_pid}/net/udp6"),
10074 ],
10075 };
10076 for table_path in table_paths {
10077 if let Some(entry) = find_inet_socket_for_pid(
10078 &table_path,
10079 &inodes,
10080 kind,
10081 request.host.as_deref(),
10082 request.port,
10083 process_id,
10084 )? {
10085 return Ok(Some(entry));
10086 }
10087 }
10088 }
10089
10090 Ok(None)
10091}
10092
10093fn require_vm_inspection_permission<B>(
10094 bridge: &SharedBridge<B>,
10095 vm_id: &str,
10096 capability: &str,
10097 domain: &str,
10098 resource: &str,
10099) -> Result<(), SidecarError>
10100where
10101 B: NativeSidecarBridge + Send + 'static,
10102 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
10103{
10104 let decision = bridge.static_permission_decision(vm_id, capability, domain, Some(resource));
10105 if decision.as_ref().is_some_and(|decision| decision.allow) {
10106 return Ok(());
10107 }
10108
10109 let reason = decision
10110 .and_then(|decision| decision.reason)
10111 .unwrap_or_else(|| format!("{capability} permission required"));
10112 Err(SidecarError::Execution(format!(
10113 "EACCES: permission denied, {resource}: {reason}"
10114 )))
10115}
10116
10117fn socket_query_resource(kind: SocketQueryKind, request: &FindListenerRequest) -> String {
10118 if let Some(path) = request.path.as_deref() {
10119 return format!("unix://{path}");
10120 }
10121
10122 let host = request.host.as_deref().unwrap_or("*");
10123 let port = request
10124 .port
10125 .map_or_else(|| String::from("*"), |port| port.to_string());
10126 match kind {
10127 SocketQueryKind::TcpListener => format!("tcp://{host}:{port}"),
10128 SocketQueryKind::UdpBound => format!("udp://{host}:{port}"),
10129 }
10130}
10131
10132fn snapshot_vm_processes(vm: &VmState) -> Vec<ProcessSnapshotEntry> {
10133 let process_table = vm.kernel.list_processes();
10134 snapshot_vm_processes_inner(vm, &process_table)
10135}
10136
10137fn snapshot_vm_processes_inner(
10138 vm: &VmState,
10139 process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
10140) -> Vec<ProcessSnapshotEntry> {
10141 let mut entries = Vec::new();
10142
10143 for (process_id, process) in &vm.active_processes {
10144 collect_process_snapshot_entries(process_id, process, process_table, &mut entries);
10145 }
10146
10147 for exited in &vm.exited_process_snapshots {
10148 entries.push(exited.process.clone());
10149 }
10150
10151 entries
10152}
10153
10154fn prune_exited_process_snapshots(vm: &mut VmState) {
10155 let cutoff = Instant::now() - EXITED_PROCESS_SNAPSHOT_RETENTION;
10156 while vm
10157 .exited_process_snapshots
10158 .front()
10159 .is_some_and(|snapshot| snapshot.captured_at < cutoff)
10160 {
10161 vm.exited_process_snapshots.pop_front();
10162 }
10163}
10164
10165fn build_process_snapshot_entry(
10166 process_id: &str,
10167 process: &ActiveProcess,
10168 info: &secure_exec_kernel::process_table::ProcessInfo,
10169 exit_code: Option<i32>,
10170) -> ProcessSnapshotEntry {
10171 ProcessSnapshotEntry {
10172 process_id: process_id.to_owned(),
10173 pid: info.pid,
10174 ppid: info.ppid,
10175 pgid: info.pgid,
10176 sid: info.sid,
10177 driver: info.driver.clone(),
10178 command: info.command.clone(),
10179 args: Vec::new(),
10180 cwd: process.guest_cwd.clone(),
10181 status: if exit_code.is_some() {
10182 ProcessSnapshotStatus::Exited
10183 } else {
10184 match info.status {
10185 ProcessStatus::Running => ProcessSnapshotStatus::Running,
10186 ProcessStatus::Stopped => ProcessSnapshotStatus::Stopped,
10187 ProcessStatus::Exited => ProcessSnapshotStatus::Exited,
10188 }
10189 },
10190 exit_code: exit_code.or(info.exit_code),
10191 }
10192}
10193
10194fn collect_process_snapshot_entries(
10195 process_id: &str,
10196 process: &ActiveProcess,
10197 process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
10198 entries: &mut Vec<ProcessSnapshotEntry>,
10199) {
10200 if let Some(info) = process_table.get(&process.kernel_pid) {
10201 entries.push(build_process_snapshot_entry(
10202 process_id, process, info, None,
10203 ));
10204 }
10205
10206 for (child_id, child) in &process.child_processes {
10207 let child_process_id = format!("{process_id}/{child_id}");
10208 collect_process_snapshot_entries(&child_process_id, child, process_table, entries);
10209 }
10210}
10211
10212fn find_kernel_socket_state_entry(
10213 kernel: &SidecarKernel,
10214 process_id: &str,
10215 process: &ActiveProcess,
10216 kind: SocketQueryKind,
10217 request: &FindListenerRequest,
10218) -> Result<Option<SocketStateEntry>, SidecarError> {
10219 let entry = match kind {
10220 SocketQueryKind::TcpListener => process
10221 .tcp_listeners
10222 .values()
10223 .filter_map(|listener| listener.kernel_socket_id)
10224 .find_map(|socket_id| {
10225 kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
10226 }),
10227 SocketQueryKind::UdpBound => process
10228 .udp_sockets
10229 .values()
10230 .filter_map(|socket| socket.kernel_socket_id)
10231 .find_map(|socket_id| {
10232 kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
10233 }),
10234 };
10235
10236 if entry.is_some() {
10237 return Ok(entry);
10238 }
10239
10240 for child in process.child_processes.values() {
10241 if let Some(entry) =
10242 find_kernel_socket_state_entry(kernel, process_id, child, kind, request)?
10243 {
10244 return Ok(Some(entry));
10245 }
10246 }
10247
10248 Ok(None)
10249}
10250
10251fn kernel_socket_state_entry(
10252 kernel: &SidecarKernel,
10253 process_id: &str,
10254 socket_id: SocketId,
10255 kind: SocketQueryKind,
10256 request: &FindListenerRequest,
10257) -> Option<SocketStateEntry> {
10258 let record = kernel.socket_get(socket_id)?;
10259 let local_address = record.local_address()?;
10260 match kind {
10261 SocketQueryKind::TcpListener if record.state() == SocketState::Listening => {}
10262 SocketQueryKind::TcpListener => return None,
10263 SocketQueryKind::UdpBound => {}
10264 }
10265
10266 if !socket_host_matches(request.host.as_deref(), local_address.host()) {
10267 return None;
10268 }
10269 if request
10270 .port
10271 .is_some_and(|port| local_address.port() != port)
10272 {
10273 return None;
10274 }
10275
10276 Some(SocketStateEntry {
10277 process_id: process_id.to_owned(),
10278 host: Some(local_address.host().to_owned()),
10279 port: Some(local_address.port()),
10280 path: None,
10281 })
10282}
10283
10284fn socket_inodes_for_pid(pid: u32) -> Result<BTreeSet<u64>, SidecarError> {
10285 let fd_dir = PathBuf::from(format!("/proc/{pid}/fd"));
10286 let entries = match fs::read_dir(&fd_dir) {
10287 Ok(entries) => entries,
10288 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(BTreeSet::new()),
10289 Err(error) => {
10290 return Err(SidecarError::Io(format!(
10291 "failed to read socket descriptors for process {pid}: {error}"
10292 )));
10293 }
10294 };
10295
10296 let mut inodes = BTreeSet::new();
10297 for entry in entries {
10298 let entry = entry.map_err(|error| {
10299 SidecarError::Io(format!(
10300 "failed to inspect fd entry for process {pid}: {error}"
10301 ))
10302 })?;
10303 let target = match fs::read_link(entry.path()) {
10304 Ok(target) => target,
10305 Err(_) => continue,
10306 };
10307 if let Some(inode) = parse_socket_inode(&target) {
10308 inodes.insert(inode);
10309 }
10310 }
10311
10312 Ok(inodes)
10313}
10314
10315fn parse_socket_inode(target: &Path) -> Option<u64> {
10316 let value = target.to_string_lossy();
10317 let trimmed = value.strip_prefix("socket:[")?.strip_suffix(']')?;
10318 trimmed.parse().ok()
10319}
10320
10321fn unix_socket_path(addr: &UnixSocketAddr) -> Option<String> {
10322 addr.as_pathname()
10323 .map(|path| path.to_string_lossy().into_owned())
10324}
10325
10326fn find_unix_socket_for_pid(
10327 pid: u32,
10328 inodes: &BTreeSet<u64>,
10329 path: &str,
10330 process_id: &str,
10331) -> Result<Option<SocketStateEntry>, SidecarError> {
10332 let table_path = format!("/proc/{pid}/net/unix");
10333 let contents = match fs::read_to_string(&table_path) {
10334 Ok(contents) => contents,
10335 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
10336 Err(error) => {
10337 return Err(SidecarError::Io(format!(
10338 "failed to inspect unix sockets for process {pid}: {error}"
10339 )));
10340 }
10341 };
10342
10343 for line in contents.lines().skip(1) {
10344 let columns = line.split_whitespace().collect::<Vec<_>>();
10345 if columns.len() < 8 {
10346 continue;
10347 }
10348 let Ok(inode) = columns[6].parse::<u64>() else {
10349 continue;
10350 };
10351 if !inodes.contains(&inode) || columns[7] != path {
10352 continue;
10353 }
10354 return Ok(Some(SocketStateEntry {
10355 process_id: process_id.to_owned(),
10356 host: None,
10357 port: None,
10358 path: Some(path.to_owned()),
10359 }));
10360 }
10361
10362 Ok(None)
10363}
10364
10365fn find_inet_socket_for_pid(
10366 table_path: &str,
10367 inodes: &BTreeSet<u64>,
10368 kind: SocketQueryKind,
10369 requested_host: Option<&str>,
10370 requested_port: Option<u16>,
10371 process_id: &str,
10372) -> Result<Option<SocketStateEntry>, SidecarError> {
10373 for entry in parse_proc_net_entries(table_path)? {
10374 if !inodes.contains(&entry.inode) {
10375 continue;
10376 }
10377 if matches!(kind, SocketQueryKind::TcpListener) && entry.state != "0A" {
10378 continue;
10379 }
10380 if !socket_host_matches(requested_host, &entry.local_host) {
10381 continue;
10382 }
10383 if let Some(port) = requested_port {
10384 if entry.local_port != port {
10385 continue;
10386 }
10387 }
10388 return Ok(Some(SocketStateEntry {
10389 process_id: process_id.to_owned(),
10390 host: Some(entry.local_host),
10391 port: Some(entry.local_port),
10392 path: None,
10393 }));
10394 }
10395
10396 Ok(None)
10397}
10398
10399fn is_unspecified_socket_host(host: &str) -> bool {
10400 host == "0.0.0.0" || host == "::"
10401}
10402
10403fn is_loopback_socket_host(host: &str) -> bool {
10404 host == "127.0.0.1" || host == "::1" || host.eq_ignore_ascii_case("localhost")
10405}
10406
10407pub(crate) fn vm_network_resource_counts(vm: &VmState) -> NetworkResourceCounts {
10408 let snapshot = vm.kernel.resource_snapshot();
10409 let mut counts = NetworkResourceCounts {
10410 sockets: snapshot.sockets,
10411 connections: snapshot.socket_connections,
10412 };
10413 for process in vm.active_processes.values() {
10414 let process_counts = process.sidecar_only_network_resource_counts();
10415 counts.sockets += process_counts.sockets;
10416 counts.connections += process_counts.connections;
10417 }
10418 counts
10419}
10420
10421fn collect_javascript_socket_port_state(
10422 kernel: &SidecarKernel,
10423 process: &ActiveProcess,
10424 tcp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10425 udp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10426 udp_host_to_guest: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10427 used_tcp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10428 used_udp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10429) {
10430 for (family, port) in process.tcp_port_reservations.values() {
10431 used_tcp_ports.entry(*family).or_default().insert(*port);
10432 }
10433
10434 let mut record_tcp_listener = |guest_addr: SocketAddr, host_port: u16| {
10435 let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10436 used_tcp_ports
10437 .entry(family)
10438 .or_default()
10439 .insert(guest_addr.port());
10440 tcp_guest_to_host.insert((family, guest_addr.port()), host_port);
10443 };
10444
10445 for listener in process.tcp_listeners.values() {
10446 let local_addr = listener
10447 .kernel_socket_id
10448 .and_then(|socket_id| kernel.socket_get(socket_id))
10449 .and_then(|record| record.local_address().cloned())
10450 .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
10451 .unwrap_or_else(|| listener.guest_local_addr());
10452 record_tcp_listener(local_addr, local_addr.port());
10453 }
10454
10455 for server in process.http_servers.values() {
10456 let host_port = match server.listener.local_addr() {
10457 Ok(addr) => addr.port(),
10458 Err(_) => continue,
10459 };
10460 record_tcp_listener(server.guest_local_addr, host_port);
10461 }
10462
10463 if let Ok(http2) = process.http2.shared.lock() {
10464 for server in http2.servers.values() {
10465 record_tcp_listener(server.guest_local_addr, server.actual_local_addr.port());
10466 }
10467 }
10468
10469 for socket in process.tcp_sockets.values() {
10470 let guest_addr = socket
10471 .kernel_socket_id
10472 .and_then(|socket_id| kernel.socket_get(socket_id))
10473 .and_then(|record| record.local_address().cloned())
10474 .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
10475 .unwrap_or(socket.guest_local_addr);
10476 let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10477 used_tcp_ports
10478 .entry(family)
10479 .or_default()
10480 .insert(guest_addr.port());
10481 }
10482
10483 for socket in process.udp_sockets.values() {
10484 let guest_addr = socket
10485 .kernel_socket_id
10486 .and_then(|socket_id| kernel.socket_get(socket_id))
10487 .and_then(|record| record.local_address().cloned())
10488 .and_then(|address| {
10489 resolve_udp_bind_addr(address.host(), address.port(), socket.family).ok()
10490 })
10491 .or_else(|| socket.local_addr());
10492 let Some(guest_addr) = guest_addr else {
10493 continue;
10494 };
10495 let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10496 used_udp_ports
10497 .entry(family)
10498 .or_default()
10499 .insert(guest_addr.port());
10500 if let Some(host_addr) = socket
10501 .socket
10502 .as_ref()
10503 .and_then(|socket| socket.local_addr().ok())
10504 {
10505 if is_loopback_ip(guest_addr.ip()) {
10506 udp_guest_to_host.insert((family, guest_addr.port()), host_addr.port());
10507 udp_host_to_guest.insert((family, host_addr.port()), guest_addr.port());
10508 }
10509 } else if socket.kernel_socket_id.is_some() && is_loopback_ip(guest_addr.ip()) {
10510 udp_guest_to_host.insert((family, guest_addr.port()), guest_addr.port());
10511 udp_host_to_guest.insert((family, guest_addr.port()), guest_addr.port());
10512 }
10513 }
10514
10515 for child in process.child_processes.values() {
10516 collect_javascript_socket_port_state(
10517 kernel,
10518 child,
10519 tcp_guest_to_host,
10520 udp_guest_to_host,
10521 udp_host_to_guest,
10522 used_tcp_ports,
10523 used_udp_ports,
10524 );
10525 }
10526}
10527
10528pub(crate) fn build_javascript_socket_path_context(
10529 vm: &VmState,
10530) -> Result<JavascriptSocketPathContext, SidecarError> {
10531 let mut loopback_exempt_ports = vm.create_loopback_exempt_ports.clone();
10532 loopback_exempt_ports.extend(vm.configuration.loopback_exempt_ports.iter().copied());
10533 let mut tcp_loopback_guest_to_host_ports = BTreeMap::new();
10534 let mut udp_loopback_guest_to_host_ports = BTreeMap::new();
10535 let mut udp_loopback_host_to_guest_ports = BTreeMap::new();
10536 let mut used_tcp_guest_ports = BTreeMap::new();
10537 let mut used_udp_guest_ports = BTreeMap::new();
10538 for process in vm.active_processes.values() {
10539 collect_javascript_socket_port_state(
10540 &vm.kernel,
10541 process,
10542 &mut tcp_loopback_guest_to_host_ports,
10543 &mut udp_loopback_guest_to_host_ports,
10544 &mut udp_loopback_host_to_guest_ports,
10545 &mut used_tcp_guest_ports,
10546 &mut used_udp_guest_ports,
10547 );
10548 }
10549 Ok(JavascriptSocketPathContext {
10550 sandbox_root: vm.cwd.clone(),
10551 mounts: vm.configuration.mounts.clone(),
10552 listen_policy: vm.listen_policy,
10553 loopback_exempt_ports,
10554 tcp_loopback_guest_to_host_ports,
10555 udp_loopback_guest_to_host_ports,
10556 udp_loopback_host_to_guest_ports,
10557 used_tcp_guest_ports,
10558 used_udp_guest_ports,
10559 })
10560}
10561
10562fn check_network_resource_limit(
10563 limit: Option<usize>,
10564 current: usize,
10565 additional: usize,
10566 label: &str,
10567) -> Result<(), SidecarError> {
10568 if let Some(limit) = limit {
10569 if current.saturating_add(additional) > limit {
10570 return Err(SidecarError::Execution(format!(
10571 "EAGAIN: maximum {label} count reached"
10572 )));
10573 }
10574 }
10575 Ok(())
10576}
10577
10578fn normalize_tcp_listen_host(
10579 host: Option<&str>,
10580) -> Result<(JavascriptSocketFamily, &'static str, &'static str), SidecarError> {
10581 match host.unwrap_or("127.0.0.1") {
10582 "127.0.0.1" | "localhost" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "127.0.0.1")),
10583 "::1" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::1")),
10584 "0.0.0.0" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "0.0.0.0")),
10585 "::" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::")),
10586 other => Err(SidecarError::Execution(format!(
10587 "EACCES: TCP listeners must bind to loopback or unspecified addresses, got {other}"
10588 ))),
10589 }
10590}
10591
10592fn normalize_udp_bind_host(
10593 host: Option<&str>,
10594 family: JavascriptUdpFamily,
10595) -> Result<(&'static str, &'static str, JavascriptSocketFamily), SidecarError> {
10596 match (family, host) {
10597 (JavascriptUdpFamily::Ipv4, None) | (JavascriptUdpFamily::Ipv4, Some("0.0.0.0")) => {
10598 Ok(("127.0.0.1", "0.0.0.0", JavascriptSocketFamily::Ipv4))
10599 }
10600 (JavascriptUdpFamily::Ipv4, Some("127.0.0.1"))
10601 | (JavascriptUdpFamily::Ipv4, Some("localhost")) => {
10602 Ok(("127.0.0.1", "127.0.0.1", JavascriptSocketFamily::Ipv4))
10603 }
10604 (JavascriptUdpFamily::Ipv6, None) | (JavascriptUdpFamily::Ipv6, Some("::")) => {
10605 Ok(("::1", "::", JavascriptSocketFamily::Ipv6))
10606 }
10607 (JavascriptUdpFamily::Ipv6, Some("::1"))
10608 | (JavascriptUdpFamily::Ipv6, Some("localhost")) => {
10609 Ok(("::1", "::1", JavascriptSocketFamily::Ipv6))
10610 }
10611 (JavascriptUdpFamily::Ipv4, Some(other)) => Err(SidecarError::Execution(format!(
10612 "EACCES: udp4 sockets must bind to 127.0.0.1 or 0.0.0.0, got {other}"
10613 ))),
10614 (JavascriptUdpFamily::Ipv6, Some(other)) => Err(SidecarError::Execution(format!(
10615 "EACCES: udp6 sockets must bind to ::1 or ::, got {other}"
10616 ))),
10617 }
10618}
10619
10620fn allocate_guest_listen_port(
10621 requested_port: u16,
10622 family: JavascriptSocketFamily,
10623 used_ports: &BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10624 policy: VmListenPolicy,
10625) -> Result<u16, SidecarError> {
10626 let is_allowed = |port: u16| {
10627 port >= policy.port_min
10628 && port <= policy.port_max
10629 && (policy.allow_privileged || port >= 1024)
10630 };
10631 let used = used_ports.get(&family);
10632
10633 if requested_port != 0 {
10634 if !is_allowed(requested_port) {
10635 let reason = if requested_port < 1024 && !policy.allow_privileged {
10636 format!(
10637 "EACCES: privileged listen port {requested_port} requires {}=true",
10638 VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY
10639 )
10640 } else {
10641 format!(
10642 "EACCES: listen port {requested_port} is outside the allowed range {}-{}",
10643 policy.port_min, policy.port_max
10644 )
10645 };
10646 return Err(SidecarError::Execution(reason));
10647 }
10648 if used.is_some_and(|ports| ports.contains(&requested_port)) {
10649 return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
10650 libc::EADDRINUSE,
10651 )));
10652 }
10653 return Ok(requested_port);
10654 }
10655
10656 let allocation_start = policy
10657 .port_min
10658 .max(if policy.allow_privileged { 1 } else { 1024 });
10659 for candidate in allocation_start..=policy.port_max {
10660 if used.is_some_and(|ports| ports.contains(&candidate)) {
10661 continue;
10662 }
10663 return Ok(candidate);
10664 }
10665
10666 Err(sidecar_net_error(std::io::Error::from_raw_os_error(
10667 libc::EADDRINUSE,
10668 )))
10669}
10670
10671fn socket_host_matches(requested: Option<&str>, actual: &str) -> bool {
10672 match requested {
10673 None => true,
10674 Some(requested) if requested == actual => true,
10675 Some(requested)
10676 if is_unspecified_socket_host(requested) && is_unspecified_socket_host(actual) =>
10677 {
10678 true
10679 }
10680 Some(requested) if is_unspecified_socket_host(requested) => is_loopback_socket_host(actual),
10681 Some(requested) if requested.eq_ignore_ascii_case("localhost") => {
10682 is_loopback_socket_host(actual)
10683 }
10684 _ => false,
10685 }
10686}
10687
10688fn parse_proc_net_entries(table_path: &str) -> Result<Vec<ProcNetEntry>, SidecarError> {
10689 let contents = match fs::read_to_string(table_path) {
10690 Ok(contents) => contents,
10691 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
10692 Err(error) => {
10693 return Err(SidecarError::Io(format!(
10694 "failed to inspect socket table {table_path}: {error}"
10695 )));
10696 }
10697 };
10698
10699 let mut entries = Vec::new();
10700 for line in contents.lines().skip(1) {
10701 let columns = line.split_whitespace().collect::<Vec<_>>();
10702 if columns.len() < 10 {
10703 continue;
10704 }
10705 let Some((host, port)) = parse_proc_ip_port(columns[1]) else {
10706 continue;
10707 };
10708 let Ok(inode) = columns[9].parse::<u64>() else {
10709 continue;
10710 };
10711 entries.push(ProcNetEntry {
10712 local_host: host,
10713 local_port: port,
10714 state: columns[3].to_owned(),
10715 inode,
10716 });
10717 }
10718
10719 Ok(entries)
10720}
10721
10722fn parse_proc_ip_port(value: &str) -> Option<(String, u16)> {
10723 let (raw_ip, raw_port) = value.split_once(':')?;
10724 let port = u16::from_str_radix(raw_port, 16).ok()?;
10725 let host = match raw_ip.len() {
10726 8 => {
10727 let raw = u32::from_str_radix(raw_ip, 16).ok()?;
10728 Ipv4Addr::from(raw.to_le_bytes()).to_string()
10729 }
10730 32 => {
10731 let mut bytes = [0_u8; 16];
10732 for (index, chunk) in raw_ip.as_bytes().chunks(8).enumerate() {
10733 let word = u32::from_str_radix(std::str::from_utf8(chunk).ok()?, 16).ok()?;
10734 bytes[index * 4..(index + 1) * 4].copy_from_slice(&word.to_le_bytes());
10735 }
10736 Ipv6Addr::from(bytes).to_string()
10737 }
10738 _ => return None,
10739 };
10740 Some((host, port))
10741}
10742
10743fn python_file_entrypoint(entrypoint: &str) -> Option<PathBuf> {
10744 let path = Path::new(entrypoint);
10745 (path.extension().and_then(|extension| extension.to_str()) == Some("py"))
10746 .then(|| path.to_path_buf())
10747}
10748
10749fn add_runtime_guest_path_mapping(
10750 env: &mut BTreeMap<String, String>,
10751 guest_path: &str,
10752 host_path: &Path,
10753) {
10754 let mut mappings = env
10755 .get("AGENT_OS_GUEST_PATH_MAPPINGS")
10756 .and_then(|value| serde_json::from_str::<Vec<Value>>(value).ok())
10757 .unwrap_or_default();
10758 mappings.retain(|mapping| {
10759 mapping
10760 .get("guestPath")
10761 .and_then(Value::as_str)
10762 .map(|existing| normalize_path(existing) != normalize_path(guest_path))
10763 .unwrap_or(true)
10764 });
10765 mappings.push(json!({
10766 "guestPath": normalize_path(guest_path),
10767 "hostPath": host_path.display().to_string(),
10768 }));
10769 if let Ok(serialized) = serde_json::to_string(&mappings) {
10770 env.insert(String::from("AGENT_OS_GUEST_PATH_MAPPINGS"), serialized);
10771 }
10772}
10773
10774fn add_runtime_host_access_path(
10775 env: &mut BTreeMap<String, String>,
10776 key: &str,
10777 host_path: &Path,
10778 expand: bool,
10779) {
10780 let existing = env
10781 .get(key)
10782 .and_then(|value| serde_json::from_str::<Vec<String>>(value).ok())
10783 .unwrap_or_default()
10784 .into_iter()
10785 .map(PathBuf::from)
10786 .collect::<Vec<_>>();
10787 let mut paths = existing;
10788 paths.push(host_path.to_path_buf());
10789 let normalized = if expand {
10790 expand_host_access_paths(&paths)
10791 } else {
10792 dedupe_host_paths(&paths)
10793 };
10794 let serialized = normalized
10795 .iter()
10796 .map(|path| path.to_string_lossy().into_owned())
10797 .collect::<Vec<_>>();
10798 if let Ok(serialized) = serde_json::to_string(&serialized) {
10799 env.insert(key.to_owned(), serialized);
10800 }
10801}
10802
10803fn is_path_like_specifier(specifier: &str) -> bool {
10806 specifier.starts_with('/')
10807 || specifier.starts_with("./")
10808 || specifier.starts_with("../")
10809 || specifier.starts_with("file:")
10810}
10811
10812fn execution_wasm_permission_tier(tier: WasmPermissionTier) -> ExecutionWasmPermissionTier {
10813 match tier {
10814 WasmPermissionTier::Full => ExecutionWasmPermissionTier::Full,
10815 WasmPermissionTier::ReadWrite => ExecutionWasmPermissionTier::ReadWrite,
10816 WasmPermissionTier::ReadOnly => ExecutionWasmPermissionTier::ReadOnly,
10817 WasmPermissionTier::Isolated => ExecutionWasmPermissionTier::Isolated,
10818 }
10819}
10820
10821fn resolve_wasm_permission_tier(
10822 vm: &VmState,
10823 command_name: Option<&str>,
10824 explicit_tier: Option<WasmPermissionTier>,
10825 entrypoint: &str,
10826) -> WasmPermissionTier {
10827 explicit_tier
10828 .or_else(|| command_name.and_then(|command| vm.command_permissions.get(command).copied()))
10829 .or_else(|| {
10830 Path::new(entrypoint)
10831 .file_name()
10832 .and_then(|name| name.to_str())
10833 .and_then(|command| vm.command_permissions.get(command).copied())
10834 })
10835 .unwrap_or(WasmPermissionTier::Full)
10836}
10837
10838fn tokenize_shell_free_command(command: &str) -> Vec<String> {
10839 command
10840 .split_whitespace()
10841 .filter(|segment| !segment.is_empty())
10842 .map(str::to_owned)
10843 .collect()
10844}
10845
10846fn is_posix_shell_builtin(command: &str) -> bool {
10847 matches!(
10848 command,
10849 "." | ":"
10850 | "break"
10851 | "cd"
10852 | "continue"
10853 | "eval"
10854 | "exec"
10855 | "exit"
10856 | "export"
10857 | "readonly"
10858 | "return"
10859 | "set"
10860 | "shift"
10861 | "times"
10862 | "trap"
10863 | "umask"
10864 | "unset"
10865 )
10866}
10867
10868fn shell_first_token_requires_shell(token: &str) -> bool {
10874 token.contains('=') || is_shell_reserved_word(token)
10875}
10876
10877fn is_shell_reserved_word(token: &str) -> bool {
10878 matches!(
10879 token,
10880 "if" | "then"
10881 | "elif"
10882 | "else"
10883 | "fi"
10884 | "for"
10885 | "in"
10886 | "do"
10887 | "done"
10888 | "while"
10889 | "until"
10890 | "case"
10891 | "esac"
10892 | "{"
10893 | "}"
10894 | "!"
10895 )
10896}
10897
10898fn command_requires_shell(command: &str) -> bool {
10899 command.chars().any(|ch| {
10900 matches!(
10901 ch,
10902 '|' | '&'
10903 | ';'
10904 | '<'
10905 | '>'
10906 | '('
10907 | ')'
10908 | '$'
10909 | '`'
10910 | '*'
10911 | '?'
10912 | '['
10913 | ']'
10914 | '{'
10915 | '}'
10916 | '~'
10917 | '\''
10918 | '"'
10919 | '\\'
10920 | '\n'
10921 )
10922 })
10923}
10924
10925fn host_mount_path_for_guest_path(vm: &VmState, guest_path: &str) -> Option<PathBuf> {
10926 let normalized = normalize_path(guest_path);
10927
10928 let mut mounts = vm
10929 .configuration
10930 .mounts
10931 .iter()
10932 .filter_map(|mount| {
10933 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
10934 .then(|| {
10935 mount_config_host_path(&mount.plugin.config)
10936 .map(|host_path| (mount.guest_path.as_str(), host_path))
10937 })
10938 .flatten()
10939 })
10940 .collect::<Vec<_>>();
10941 mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
10942
10943 for (guest_root, host_root) in mounts {
10944 if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
10945 continue;
10946 }
10947
10948 let suffix = normalized
10949 .strip_prefix(guest_root)
10950 .unwrap_or_default()
10951 .trim_start_matches('/');
10952 let mut path = PathBuf::from(host_root);
10953 if !suffix.is_empty() {
10954 path.push(suffix);
10955 }
10956 return Some(path);
10957 }
10958
10959 None
10960}
10961
10962fn host_runtime_path_for_guest_path_with_env(
10963 vm: &VmState,
10964 runtime_env: &BTreeMap<String, String>,
10965 guest_path: &str,
10966 default_host_cwd: &Path,
10967) -> Option<PathBuf> {
10968 if let Some(path) = host_mount_path_for_guest_path(vm, guest_path) {
10969 return Some(path);
10970 }
10971 if let Some(path) = host_path_from_runtime_guest_mappings(runtime_env, guest_path) {
10972 return Some(path);
10973 }
10974
10975 let normalized = normalize_path(guest_path);
10976 let virtual_home = runtime_env
10977 .get("AGENT_OS_VIRTUAL_OS_HOMEDIR")
10978 .or_else(|| vm.guest_env.get("AGENT_OS_VIRTUAL_OS_HOMEDIR"))
10979 .filter(|value| value.starts_with('/'))
10980 .cloned()
10981 .unwrap_or_else(|| String::from("/root"));
10982
10983 if normalized == virtual_home || normalized.starts_with(&format!("{virtual_home}/")) {
10984 let suffix = normalized
10985 .strip_prefix(&virtual_home)
10986 .unwrap_or_default()
10987 .trim_start_matches('/');
10988 let mut host_path = default_host_cwd.to_path_buf();
10989 if !suffix.is_empty() {
10990 host_path.push(suffix);
10991 }
10992 return Some(host_path);
10993 }
10994
10995 None
10996}
10997
10998#[derive(Deserialize, Serialize)]
10999struct RuntimeGuestPathMapping {
11000 #[serde(rename = "guestPath")]
11001 guest_path: String,
11002 #[serde(rename = "hostPath")]
11003 host_path: String,
11004 #[serde(rename = "readOnly", default)]
11005 read_only: bool,
11006}
11007
11008pub(crate) fn host_path_from_runtime_guest_mappings(
11009 runtime_env: &BTreeMap<String, String>,
11010 guest_path: &str,
11011) -> Option<PathBuf> {
11012 let mappings = runtime_env
11013 .get("AGENT_OS_GUEST_PATH_MAPPINGS")
11014 .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
11015 let normalized = normalize_path(guest_path);
11016
11017 let mut sorted_mappings = mappings
11018 .into_iter()
11019 .filter_map(|mapping| {
11020 (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
11021 normalize_path(&mapping.guest_path),
11022 PathBuf::from(mapping.host_path),
11023 ))
11024 })
11025 .collect::<Vec<_>>();
11026 sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.0.len()));
11027
11028 for (guest_root, mut host_root) in sorted_mappings {
11029 if guest_root != "/"
11030 && normalized != guest_root
11031 && !normalized.starts_with(&format!("{guest_root}/"))
11032 {
11033 continue;
11034 }
11035 if guest_root == "/" && !normalized.starts_with('/') {
11036 continue;
11037 }
11038
11039 if host_root.is_relative() {
11040 host_root = std::env::current_dir().ok()?.join(host_root);
11041 }
11042
11043 let suffix = if guest_root == "/" {
11044 normalized.trim_start_matches('/')
11045 } else {
11046 normalized
11047 .strip_prefix(&guest_root)
11048 .unwrap_or_default()
11049 .trim_start_matches('/')
11050 };
11051 if !suffix.is_empty() {
11052 host_root.push(suffix);
11053 }
11054 return Some(host_root);
11055 }
11056
11057 None
11058}
11059
11060fn guest_runtime_path_for_host_path(
11061 runtime_env: &BTreeMap<String, String>,
11062 cwd: &Path,
11063 host_path: &str,
11064) -> Option<String> {
11065 let resolved = if host_path.starts_with("file://") {
11066 PathBuf::from(host_path.trim_start_matches("file://"))
11067 } else if host_path.starts_with("file:") {
11068 PathBuf::from(host_path.trim_start_matches("file:"))
11069 } else {
11070 let candidate = PathBuf::from(host_path);
11071 if candidate.is_absolute() {
11072 candidate
11073 } else if host_path.starts_with("./") || host_path.starts_with("../") {
11074 cwd.join(candidate)
11075 } else {
11076 return None;
11077 }
11078 };
11079 let normalized = normalize_host_path(&resolved);
11080
11081 if let Some(path) = guest_path_from_runtime_host_mappings(runtime_env, &normalized) {
11082 return Some(path);
11083 }
11084
11085 let normalized_cwd = normalize_host_path(cwd);
11086 if !path_is_within_root(&normalized, &normalized_cwd) {
11087 return None;
11088 }
11089
11090 let virtual_home = runtime_env
11091 .get("AGENT_OS_VIRTUAL_OS_HOMEDIR")
11092 .filter(|value| value.starts_with('/'))
11093 .cloned()
11094 .unwrap_or_else(|| String::from("/root"));
11095 let suffix = normalized
11096 .strip_prefix(&normalized_cwd)
11097 .ok()?
11098 .to_string_lossy()
11099 .replace('\\', "/")
11100 .trim_start_matches('/')
11101 .to_owned();
11102
11103 Some(if suffix.is_empty() {
11104 virtual_home
11105 } else {
11106 normalize_path(&format!("{virtual_home}/{suffix}"))
11107 })
11108}
11109
11110fn guest_path_from_runtime_host_mappings(
11111 runtime_env: &BTreeMap<String, String>,
11112 host_path: &Path,
11113) -> Option<String> {
11114 let mappings = runtime_env
11115 .get("AGENT_OS_GUEST_PATH_MAPPINGS")
11116 .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
11117 let normalized = normalize_host_path(host_path);
11118
11119 let mut sorted_mappings = mappings
11120 .into_iter()
11121 .filter_map(|mapping| {
11122 (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
11123 normalize_path(&mapping.guest_path),
11124 normalize_host_path(Path::new(&mapping.host_path)),
11125 ))
11126 })
11127 .collect::<Vec<_>>();
11128 sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.1.as_os_str().len()));
11129
11130 for (guest_root, host_root) in sorted_mappings {
11131 if !path_is_within_root(&normalized, &host_root) {
11132 continue;
11133 }
11134 let suffix = normalized
11135 .strip_prefix(&host_root)
11136 .ok()?
11137 .to_string_lossy()
11138 .replace('\\', "/")
11139 .trim_start_matches('/')
11140 .to_owned();
11141
11142 return Some(if suffix.is_empty() {
11143 guest_root
11144 } else if guest_root == "/" {
11145 normalize_path(&format!("/{suffix}"))
11146 } else {
11147 normalize_path(&format!("{guest_root}/{suffix}"))
11148 });
11149 }
11150
11151 None
11152}
11153
11154fn host_mount_path_for_guest_path_from_mounts(
11155 mounts: &[crate::protocol::MountDescriptor],
11156 guest_path: &str,
11157) -> Option<PathBuf> {
11158 let normalized = normalize_path(guest_path);
11159
11160 let mut host_mounts = mounts
11161 .iter()
11162 .filter_map(|mount| {
11163 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11164 .then(|| {
11165 mount_config_host_path(&mount.plugin.config)
11166 .map(|host_path| (mount.guest_path.as_str(), host_path))
11167 })
11168 .flatten()
11169 })
11170 .collect::<Vec<_>>();
11171 host_mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
11172
11173 for (guest_root, host_root) in host_mounts {
11174 if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
11175 continue;
11176 }
11177
11178 let suffix = normalized
11179 .strip_prefix(guest_root)
11180 .unwrap_or_default()
11181 .trim_start_matches('/');
11182 let mut path = PathBuf::from(host_root);
11183 if !suffix.is_empty() {
11184 path.push(suffix);
11185 }
11186 return Some(path);
11187 }
11188
11189 None
11190}
11191
11192#[cfg(test)]
11193mod host_mount_path_for_guest_path_from_mounts_tests {
11194 use super::host_mount_path_for_guest_path_from_mounts;
11195 use crate::protocol::{MountDescriptor, MountPluginDescriptor};
11196 use serde_json::json;
11197 use std::path::PathBuf;
11198
11199 #[test]
11200 fn resolves_module_access_mount_paths() {
11201 let mounts = vec![MountDescriptor {
11202 guest_path: String::from("/root/node_modules"),
11203 read_only: true,
11204 plugin: MountPluginDescriptor {
11205 id: String::from("module_access"),
11206 config: json!({
11207 "hostPath": "/tmp/workspace/node_modules",
11208 })
11209 .to_string(),
11210 },
11211 }];
11212
11213 let resolved =
11214 host_mount_path_for_guest_path_from_mounts(&mounts, "/root/node_modules/pkg/index.js")
11215 .expect("module_access mount should resolve");
11216
11217 assert_eq!(
11218 resolved,
11219 PathBuf::from("/tmp/workspace/node_modules/pkg/index.js")
11220 );
11221 }
11222}
11223
11224fn resolve_guest_socket_host_path(
11225 context: &JavascriptSocketPathContext,
11226 guest_path: &str,
11227) -> PathBuf {
11228 if let Some(path) = host_mount_path_for_guest_path_from_mounts(&context.mounts, guest_path) {
11229 return path;
11230 }
11231
11232 let normalized = normalize_path(guest_path);
11233 let mut host_path = context.sandbox_root.clone();
11234 let suffix = normalized.trim_start_matches('/');
11235 if !suffix.is_empty() {
11236 host_path.push(suffix);
11237 }
11238 host_path
11239}
11240
11241fn ensure_kernel_parent_directories(
11242 kernel: &mut SidecarKernel,
11243 path: &str,
11244) -> Result<(), SidecarError> {
11245 let parent = dirname(path);
11246 if parent != "/" && !kernel.exists(&parent).map_err(kernel_error)? {
11247 kernel.mkdir(&parent, true).map_err(kernel_error)?;
11248 }
11249 Ok(())
11250}
11251
11252pub(crate) fn sanitize_javascript_child_process_internal_bootstrap_env(
11256 env: &BTreeMap<String, String>,
11257) -> BTreeMap<String, String> {
11258 const ALLOWED_KEYS: &[&str] = &[
11259 "AGENT_OS_ALLOWED_NODE_BUILTINS",
11260 "AGENT_OS_GUEST_PATH_MAPPINGS",
11261 "AGENT_OS_LOOPBACK_EXEMPT_PORTS",
11262 "AGENT_OS_VIRTUAL_PROCESS_EXEC_PATH",
11263 "AGENT_OS_VIRTUAL_PROCESS_UID",
11264 "AGENT_OS_VIRTUAL_PROCESS_GID",
11265 "AGENT_OS_VIRTUAL_PROCESS_VERSION",
11266 ];
11267
11268 env.iter()
11269 .filter(|(key, _)| {
11270 ALLOWED_KEYS.contains(&key.as_str()) || key.starts_with("AGENT_OS_VIRTUAL_OS_")
11271 })
11272 .map(|(key, value)| (key.clone(), value.clone()))
11273 .collect()
11274}
11275
11276fn resolve_tcp_bind_addr(host: &str, port: u16) -> Result<SocketAddr, SidecarError> {
11281 (host, port)
11282 .to_socket_addrs()
11283 .map_err(sidecar_net_error)?
11284 .next()
11285 .ok_or_else(|| {
11286 SidecarError::Execution(format!("failed to resolve TCP bind address {host}:{port}"))
11287 })
11288}
11289
11290pub(crate) fn format_dns_resource(hostname: &str) -> String {
11291 format!("dns://{hostname}")
11292}
11293
11294pub(crate) fn format_tcp_resource(host: &str, port: u16) -> String {
11295 format!("tcp://{host}:{port}")
11296}
11297
11298fn is_loopback_ip(ip: IpAddr) -> bool {
11299 match ip {
11300 IpAddr::V4(ip) => ip.is_loopback(),
11301 IpAddr::V6(ip) => {
11302 ip.is_loopback()
11303 || ip
11304 .to_ipv4_mapped()
11305 .is_some_and(|mapped| mapped.is_loopback())
11306 }
11307 }
11308}
11309
11310fn loopback_cidr(ip: IpAddr) -> &'static str {
11311 match ip {
11312 IpAddr::V4(ip) if ip.is_loopback() => "127.0.0.0/8",
11313 IpAddr::V6(ip)
11314 if ip
11315 .to_ipv4_mapped()
11316 .is_some_and(|mapped| mapped.is_loopback()) =>
11317 {
11318 "127.0.0.0/8"
11319 }
11320 IpAddr::V6(_) => "::1/128",
11321 IpAddr::V4(_) => "127.0.0.0/8",
11322 }
11323}
11324
11325fn ipv4_compatible_embedded(ip: Ipv6Addr) -> Option<Ipv4Addr> {
11331 let segments = ip.segments();
11332 if segments[0..6].iter().any(|&s| s != 0) {
11333 return None;
11334 }
11335 let embedded = (u32::from(segments[6]) << 16) | u32::from(segments[7]);
11336 if embedded == 0 || embedded == 1 {
11339 return None;
11340 }
11341 Some(Ipv4Addr::from(embedded))
11342}
11343
11344fn restricted_non_loopback_ip_range(ip: IpAddr) -> Option<(&'static str, &'static str)> {
11345 match ip {
11346 IpAddr::V4(ip) => {
11347 if ip.is_unspecified() {
11348 return Some(("0.0.0.0/32", "unspecified"));
11351 }
11352 let [first, second, ..] = ip.octets();
11353 match (first, second) {
11354 (10, _) => Some(("10.0.0.0/8", "private")),
11355 (100, 64..=127) => Some(("100.64.0.0/10", "carrier-grade-nat")),
11356 (172, 16..=31) => Some(("172.16.0.0/12", "private")),
11357 (192, 168) => Some(("192.168.0.0/16", "private")),
11358 (169, 254) => Some(("169.254.0.0/16", "link-local")),
11359 (224..=239, _) => Some(("224.0.0.0/4", "multicast")),
11364 (240..=255, _) => Some(("240.0.0.0/4", "reserved")),
11365 _ => None,
11366 }
11367 }
11368 IpAddr::V6(ip) => {
11369 if let Some(mapped) = ip.to_ipv4_mapped() {
11370 return restricted_non_loopback_ip_range(IpAddr::V4(mapped));
11371 }
11372 if let Some(compat) = ipv4_compatible_embedded(ip) {
11379 return restricted_non_loopback_ip_range(IpAddr::V4(compat));
11380 }
11381
11382 if ip.is_unspecified() {
11383 return Some(("::/128", "unspecified"));
11386 }
11387
11388 let segments = ip.segments();
11389 if (segments[0] & 0xfe00) == 0xfc00 {
11390 return Some(("fc00::/7", "unique-local"));
11391 }
11392 if (segments[0] & 0xffc0) == 0xfe80 {
11393 return Some(("fe80::/10", "link-local"));
11394 }
11395 None
11396 }
11397 }
11398}
11399
11400fn blocked_dns_resolution_error(
11401 resource: &str,
11402 ip: IpAddr,
11403 cidr: &str,
11404 label: &str,
11405) -> SidecarError {
11406 SidecarError::Execution(format!(
11407 "EACCES: blocked outbound network access to {resource}: {ip} is within restricted {label} range {cidr}"
11408 ))
11409}
11410
11411fn blocked_loopback_connect_error(resource: &str, ip: IpAddr, port: u16) -> SidecarError {
11412 SidecarError::Execution(format!(
11413 "EACCES: blocked outbound network access to {resource}: {ip} is loopback ({}) and port {port} is not owned by this VM and is not listed in {LOOPBACK_EXEMPT_PORTS_ENV}",
11414 loopback_cidr(ip)
11415 ))
11416}
11417
11418fn filter_dns_safe_ip_addrs(
11419 addresses: Vec<IpAddr>,
11420 hostname: &str,
11421) -> Result<Vec<IpAddr>, SidecarError> {
11422 let resource = format_dns_resource(hostname);
11423 let mut allowed = Vec::new();
11424 let mut blocked = None;
11425
11426 for ip in addresses {
11427 if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
11428 blocked.get_or_insert((ip, cidr, label));
11429 continue;
11430 }
11431 allowed.push(ip);
11432 }
11433
11434 if allowed.is_empty() {
11435 let (ip, cidr, label) = blocked.expect("blocked DNS results should capture a reason");
11436 return Err(blocked_dns_resolution_error(&resource, ip, cidr, label));
11437 }
11438
11439 Ok(allowed)
11440}
11441
11442fn loopback_connect_allowed(context: &JavascriptSocketPathContext, port: u16) -> bool {
11443 context.loopback_port_allowed(port)
11444}
11445
11446fn filter_tcp_connect_ip_addrs(
11447 addresses: Vec<IpAddr>,
11448 host: &str,
11449 port: u16,
11450 context: &JavascriptSocketPathContext,
11451) -> Result<Vec<IpAddr>, SidecarError> {
11452 let resource = format_tcp_resource(host, port);
11453 let mut allowed = Vec::new();
11454 let mut blocked = None;
11455
11456 for ip in addresses {
11457 if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
11458 blocked.get_or_insert_with(|| blocked_dns_resolution_error(&resource, ip, cidr, label));
11459 continue;
11460 }
11461 if is_loopback_ip(ip) && !loopback_connect_allowed(context, port) {
11462 blocked.get_or_insert_with(|| blocked_loopback_connect_error(&resource, ip, port));
11463 continue;
11464 }
11465 allowed.push(ip);
11466 }
11467
11468 if allowed.is_empty() {
11469 return Err(blocked.expect("blocked TCP connect results should capture a reason"));
11470 }
11471
11472 Ok(allowed)
11473}
11474
11475fn resolve_tcp_connect_addr<B>(
11476 bridge: &SharedBridge<B>,
11477 kernel: &SidecarKernel,
11478 vm_id: &str,
11479 dns: &VmDnsConfig,
11480 host: &str,
11481 port: u16,
11482 context: &JavascriptSocketPathContext,
11483) -> Result<ResolvedTcpConnectAddr, SidecarError>
11484where
11485 B: NativeSidecarBridge + Send + 'static,
11486 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11487{
11488 let allowed = filter_tcp_connect_ip_addrs(
11489 resolve_dns_ip_addrs(
11490 bridge,
11491 kernel,
11492 vm_id,
11493 dns,
11494 host,
11495 DnsLookupPolicy::SkipPermissions,
11496 )?,
11497 host,
11498 port,
11499 context,
11500 )?;
11501 let ip = allowed
11502 .iter()
11503 .copied()
11504 .find(|candidate| {
11505 let family = JavascriptSocketFamily::from_ip(*candidate);
11506 context.translate_tcp_loopback_port(family, port).is_some()
11507 })
11508 .or_else(|| allowed.iter().copied().find(IpAddr::is_ipv4))
11511 .or_else(|| allowed.first().copied())
11512 .ok_or_else(|| {
11513 SidecarError::Execution(format!("failed to resolve TCP address {host}:{port}"))
11514 })?;
11515 let family = JavascriptSocketFamily::from_ip(ip);
11516 let use_kernel_loopback =
11517 is_loopback_ip(ip) && context.translate_tcp_loopback_port(family, port).is_some();
11518 let actual_port = if is_loopback_ip(ip) {
11519 context
11520 .translate_tcp_loopback_port(family, port)
11521 .unwrap_or(port)
11522 } else {
11523 port
11524 };
11525 Ok(ResolvedTcpConnectAddr {
11526 actual_addr: SocketAddr::new(ip, actual_port),
11527 guest_remote_addr: SocketAddr::new(ip, port),
11528 use_kernel_loopback,
11529 })
11530}
11531
11532fn resolve_dns_ip_addrs<B>(
11533 bridge: &SharedBridge<B>,
11534 kernel: &SidecarKernel,
11535 vm_id: &str,
11536 dns: &VmDnsConfig,
11537 hostname: &str,
11538 policy: DnsLookupPolicy,
11539) -> Result<Vec<IpAddr>, SidecarError>
11540where
11541 B: NativeSidecarBridge + Send + 'static,
11542 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11543{
11544 let resolution = match kernel.resolve_dns(hostname, policy) {
11545 Ok(resolution) => resolution,
11546 Err(error) => {
11547 let sidecar_error = kernel_error(error.clone());
11548 if error.code() != "EACCES" {
11549 emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
11550 }
11551 return Err(sidecar_error);
11552 }
11553 };
11554 emit_dns_resolution_event(
11555 bridge,
11556 vm_id,
11557 hostname,
11558 resolution.source(),
11559 resolution.addresses(),
11560 dns,
11561 );
11562 Ok(resolution.addresses().to_vec())
11563}
11564
11565fn resolve_dns_records<B>(
11566 bridge: &SharedBridge<B>,
11567 kernel: &SidecarKernel,
11568 vm_id: &str,
11569 dns: &VmDnsConfig,
11570 hostname: &str,
11571 record_type: RecordType,
11572 policy: DnsLookupPolicy,
11573) -> Result<DnsRecordResolution, SidecarError>
11574where
11575 B: NativeSidecarBridge + Send + 'static,
11576 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11577{
11578 let resolution = match kernel.resolve_dns_records(hostname, record_type, policy) {
11579 Ok(resolution) => resolution,
11580 Err(error) => {
11581 let sidecar_error = kernel_error(error.clone());
11582 if error.code() != "EACCES" {
11583 emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
11584 }
11585 return Err(sidecar_error);
11586 }
11587 };
11588 emit_dns_record_resolution_event(bridge, vm_id, hostname, &resolution, dns);
11589 Ok(resolution)
11590}
11591
11592fn filter_dns_ip_addrs(
11593 addresses: Vec<IpAddr>,
11594 family: Option<u8>,
11595) -> Result<Vec<IpAddr>, SidecarError> {
11596 let filtered: Vec<_> = match family.unwrap_or(0) {
11597 0 => addresses,
11598 4 => addresses
11599 .into_iter()
11600 .filter(|ip| matches!(ip, IpAddr::V4(_)))
11601 .collect(),
11602 6 => addresses
11603 .into_iter()
11604 .filter(|ip| matches!(ip, IpAddr::V6(_)))
11605 .collect(),
11606 other => {
11607 return Err(SidecarError::InvalidState(format!(
11608 "unsupported dns family {other}"
11609 )));
11610 }
11611 };
11612
11613 if filtered.is_empty() {
11614 return Err(SidecarError::Execution(String::from(
11615 "failed to resolve DNS address for requested family",
11616 )));
11617 }
11618
11619 Ok(filtered)
11620}
11621
11622fn resolve_udp_bind_addr(
11623 host: &str,
11624 port: u16,
11625 family: JavascriptUdpFamily,
11626) -> Result<SocketAddr, SidecarError> {
11627 (host, port)
11628 .to_socket_addrs()
11629 .map_err(sidecar_net_error)?
11630 .find(|addr| family.matches_addr(addr))
11631 .ok_or_else(|| {
11632 SidecarError::Execution(format!(
11633 "failed to resolve {} UDP bind address {host}:{port}",
11634 family.socket_type()
11635 ))
11636 })
11637}
11638
11639fn resolve_udp_addr<B>(request: UdpRemoteAddrRequest<'_, B>) -> Result<SocketAddr, SidecarError>
11640where
11641 B: NativeSidecarBridge + Send + 'static,
11642 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11643{
11644 let UdpRemoteAddrRequest {
11645 bridge,
11646 kernel,
11647 vm_id,
11648 dns,
11649 host,
11650 port,
11651 family,
11652 context,
11653 } = request;
11654 resolve_dns_ip_addrs(
11655 bridge,
11656 kernel,
11657 vm_id,
11658 dns,
11659 host,
11660 DnsLookupPolicy::SkipPermissions,
11661 )?
11662 .into_iter()
11663 .map(|ip| {
11664 let family_key = JavascriptSocketFamily::from_ip(ip);
11665 let actual_port = if is_loopback_ip(ip) {
11666 context
11667 .translate_udp_loopback_port(family_key, port)
11668 .unwrap_or(port)
11669 } else {
11670 port
11671 };
11672 SocketAddr::new(ip, actual_port)
11673 })
11674 .find(|addr| family.matches_addr(addr))
11675 .ok_or_else(|| {
11676 SidecarError::Execution(format!(
11677 "failed to resolve {} UDP address {host}:{port}",
11678 family.socket_type()
11679 ))
11680 })
11681}
11682
11683fn socket_addr_family(addr: &SocketAddr) -> &'static str {
11684 match addr {
11685 SocketAddr::V4(_) => "IPv4",
11686 SocketAddr::V6(_) => "IPv6",
11687 }
11688}
11689
11690fn javascript_net_timeout_value() -> Value {
11691 Value::String(String::from(JAVASCRIPT_NET_TIMEOUT_SENTINEL))
11692}
11693
11694fn javascript_net_json_string(value: Value, label: &str) -> Result<Value, SidecarError> {
11695 serde_json::to_string(&value)
11696 .map(Value::String)
11697 .map_err(|error| {
11698 SidecarError::InvalidState(format!("failed to serialize {label} payload: {error}"))
11699 })
11700}
11701
11702fn javascript_net_read_value(
11703 event: Option<JavascriptTcpSocketEvent>,
11704) -> Result<Value, SidecarError> {
11705 match event {
11706 Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(Value::String(
11707 base64::engine::general_purpose::STANDARD.encode(chunk),
11708 )),
11709 Some(JavascriptTcpSocketEvent::End | JavascriptTcpSocketEvent::Close { .. }) => {
11710 Ok(Value::Null)
11711 }
11712 Some(JavascriptTcpSocketEvent::Error { code, message }) => {
11713 let detail = code.unwrap_or_else(|| String::from("socket read"));
11714 Err(SidecarError::Execution(format!("{detail}: {message}")))
11715 }
11716 None => Ok(javascript_net_timeout_value()),
11717 }
11718}
11719
11720fn io_error_code(error: &std::io::Error) -> Option<String> {
11721 match error.raw_os_error() {
11722 Some(libc::EADDRINUSE) => Some(String::from("EADDRINUSE")),
11723 Some(libc::EADDRNOTAVAIL) => Some(String::from("EADDRNOTAVAIL")),
11724 Some(libc::ECONNREFUSED) => Some(String::from("ECONNREFUSED")),
11725 Some(libc::ECONNRESET) => Some(String::from("ECONNRESET")),
11726 Some(libc::EINVAL) => Some(String::from("EINVAL")),
11727 Some(libc::EPIPE) => Some(String::from("EPIPE")),
11728 Some(libc::ETIMEDOUT) => Some(String::from("ETIMEDOUT")),
11729 Some(libc::EHOSTUNREACH) => Some(String::from("EHOSTUNREACH")),
11730 Some(libc::ENETUNREACH) => Some(String::from("ENETUNREACH")),
11731 _ => None,
11732 }
11733}
11734
11735fn sidecar_net_error(error: std::io::Error) -> SidecarError {
11736 let message = match io_error_code(&error) {
11737 Some(code) => format!("{code}: {error}"),
11738 None => error.to_string(),
11739 };
11740 SidecarError::Execution(message)
11741}
11742
11743fn tls_provider() -> Arc<rustls::crypto::CryptoProvider> {
11744 Arc::new(aws_lc_rs::default_provider())
11745}
11746
11747fn tls_local_certificates(
11748 options: &JavascriptTlsBridgeOptions,
11749) -> Result<Vec<Vec<u8>>, SidecarError> {
11750 let Some(certificates) = options.cert.as_ref() else {
11751 return Ok(Vec::new());
11752 };
11753 tls_material_entries(certificates)
11754}
11755
11756fn tls_material_entries(material: &JavascriptTlsMaterial) -> Result<Vec<Vec<u8>>, SidecarError> {
11757 match material {
11758 JavascriptTlsMaterial::Single(entry) => tls_data_value(entry).map(|value| vec![value]),
11759 JavascriptTlsMaterial::Many(entries) => entries.iter().map(tls_data_value).collect(),
11760 }
11761}
11762
11763fn tls_data_value(value: &JavascriptTlsDataValue) -> Result<Vec<u8>, SidecarError> {
11764 match value {
11765 JavascriptTlsDataValue::Buffer { data } => base64::engine::general_purpose::STANDARD
11766 .decode(data)
11767 .map_err(|error| {
11768 SidecarError::InvalidState(format!("TLS material contains invalid base64: {error}"))
11769 }),
11770 JavascriptTlsDataValue::String { data } => Ok(data.as_bytes().to_vec()),
11771 }
11772}
11773
11774fn tls_certificates_from_material(
11775 material: &JavascriptTlsMaterial,
11776) -> Result<Vec<CertificateDer<'static>>, SidecarError> {
11777 let mut certificates = Vec::new();
11778 for entry in tls_material_entries(material)? {
11779 let mut reader = std::io::BufReader::new(Cursor::new(entry.clone()));
11780 let parsed = rustls_pemfile::certs(&mut reader)
11781 .collect::<Result<Vec<_>, _>>()
11782 .map_err(sidecar_net_error)?;
11783 if parsed.is_empty() {
11784 certificates.push(CertificateDer::from(entry));
11785 } else {
11786 certificates.extend(parsed);
11787 }
11788 }
11789 if certificates.is_empty() {
11790 return Err(SidecarError::InvalidState(String::from(
11791 "TLS certificate material did not contain any certificates",
11792 )));
11793 }
11794 Ok(certificates)
11795}
11796
11797fn tls_private_key_from_material(
11798 material: &JavascriptTlsMaterial,
11799) -> Result<PrivateKeyDer<'static>, SidecarError> {
11800 for entry in tls_material_entries(material)? {
11801 let mut reader = std::io::BufReader::new(Cursor::new(entry));
11802 if let Some(key) = rustls_pemfile::private_key(&mut reader).map_err(sidecar_net_error)? {
11803 return Ok(key);
11804 }
11805 }
11806 Err(SidecarError::InvalidState(String::from(
11807 "TLS private key material did not contain a supported key",
11808 )))
11809}
11810
11811fn tls_root_store(options: &JavascriptTlsBridgeOptions) -> Result<RootCertStore, SidecarError> {
11812 let mut roots = RootCertStore::empty();
11813 if let Some(ca) = options.ca.as_ref() {
11814 for certificate in tls_certificates_from_material(ca)? {
11815 roots.add(certificate).map_err(|error| {
11816 SidecarError::InvalidState(format!("failed to add TLS CA certificate: {error}"))
11817 })?;
11818 }
11819 return Ok(roots);
11820 }
11821
11822 for certificate in rustls_native_certs::load_native_certs().certs {
11823 roots.add(certificate).map_err(|error| {
11824 SidecarError::InvalidState(format!(
11825 "failed to add native TLS certificate to root store: {error}"
11826 ))
11827 })?;
11828 }
11829 Ok(roots)
11830}
11831
11832fn build_client_tls_stream(
11833 stream: TcpStream,
11834 options: &JavascriptTlsBridgeOptions,
11835) -> Result<rustls::StreamOwned<ClientConnection, TcpStream>, SidecarError> {
11836 let config = build_client_tls_config(options)?;
11837 let server_name = options
11838 .servername
11839 .clone()
11840 .unwrap_or_else(|| String::from("localhost"));
11841 let server_name = ServerName::try_from(server_name)
11842 .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
11843 stream
11844 .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
11845 .map_err(sidecar_net_error)?;
11846 stream
11847 .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
11848 .map_err(sidecar_net_error)?;
11849 let mut tls_stream = rustls::StreamOwned::new(
11850 ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
11851 SidecarError::Execution(format!("failed to start TLS client: {error}"))
11852 })?,
11853 stream,
11854 );
11855 while tls_stream.conn.is_handshaking() {
11856 tls_stream
11857 .conn
11858 .complete_io(&mut tls_stream.sock)
11859 .map_err(sidecar_net_error)?;
11860 }
11861 tls_stream
11862 .sock
11863 .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
11864 .map_err(sidecar_net_error)?;
11865 tls_stream
11866 .sock
11867 .set_write_timeout(None)
11868 .map_err(sidecar_net_error)?;
11869 Ok(tls_stream)
11870}
11871
11872fn build_client_loopback_tls_stream(
11873 transport: crate::state::LoopbackTlsEndpoint,
11874 options: &JavascriptTlsBridgeOptions,
11875) -> Result<rustls::StreamOwned<ClientConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
11876{
11877 let config = build_client_tls_config(options)?;
11878 let server_name = options
11879 .servername
11880 .clone()
11881 .unwrap_or_else(|| String::from("localhost"));
11882 let server_name = ServerName::try_from(server_name)
11883 .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
11884 let mut tls_stream = rustls::StreamOwned::new(
11885 ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
11886 SidecarError::Execution(format!("failed to start TLS client: {error}"))
11887 })?,
11888 transport,
11889 );
11890 match tls_stream.conn.complete_io(&mut tls_stream.sock) {
11891 Ok(_) => {}
11892 Err(error)
11893 if matches!(
11894 error.kind(),
11895 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
11896 ) => {}
11897 Err(error) => return Err(sidecar_net_error(error)),
11898 }
11899 Ok(tls_stream)
11900}
11901
11902fn build_client_tls_config(
11903 options: &JavascriptTlsBridgeOptions,
11904) -> Result<ClientConfig, SidecarError> {
11905 let provider = tls_provider();
11906 let builder = ClientConfig::builder_with_provider(provider.clone())
11907 .with_safe_default_protocol_versions()
11908 .map_err(|error| {
11909 SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
11910 })?;
11911
11912 let mut config = if options.reject_unauthorized == Some(false) {
11913 let verifier = Arc::new(InsecureTlsVerifier {
11914 supported_schemes: provider
11915 .signature_verification_algorithms
11916 .supported_schemes(),
11917 });
11918 builder
11919 .dangerous()
11920 .with_custom_certificate_verifier(verifier)
11921 .with_no_client_auth()
11922 } else {
11923 builder
11924 .with_root_certificates(tls_root_store(options)?)
11925 .with_no_client_auth()
11926 };
11927
11928 if let Some(protocols) = options.alpn_protocols.as_ref() {
11929 config.alpn_protocols = protocols
11930 .iter()
11931 .map(|protocol| protocol.as_bytes().to_vec())
11932 .collect();
11933 }
11934 Ok(config)
11935}
11936
11937fn build_server_tls_stream(
11938 stream: TcpStream,
11939 options: &JavascriptTlsBridgeOptions,
11940) -> Result<rustls::StreamOwned<ServerConnection, TcpStream>, SidecarError> {
11941 let config = build_server_tls_config(options)?;
11942 stream
11943 .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
11944 .map_err(sidecar_net_error)?;
11945 stream
11946 .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
11947 .map_err(sidecar_net_error)?;
11948 let mut tls_stream = rustls::StreamOwned::new(
11949 ServerConnection::new(Arc::new(config)).map_err(|error| {
11950 SidecarError::Execution(format!("failed to start TLS server: {error}"))
11951 })?,
11952 stream,
11953 );
11954 while tls_stream.conn.is_handshaking() {
11955 tls_stream
11956 .conn
11957 .complete_io(&mut tls_stream.sock)
11958 .map_err(sidecar_net_error)?;
11959 }
11960 tls_stream
11961 .sock
11962 .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
11963 .map_err(sidecar_net_error)?;
11964 tls_stream
11965 .sock
11966 .set_write_timeout(None)
11967 .map_err(sidecar_net_error)?;
11968 Ok(tls_stream)
11969}
11970
11971fn build_server_loopback_tls_stream(
11972 transport: crate::state::LoopbackTlsEndpoint,
11973 options: &JavascriptTlsBridgeOptions,
11974) -> Result<rustls::StreamOwned<ServerConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
11975{
11976 let config = build_server_tls_config(options)?;
11977 Ok(rustls::StreamOwned::new(
11978 ServerConnection::new(Arc::new(config)).map_err(|error| {
11979 SidecarError::Execution(format!("failed to start TLS server: {error}"))
11980 })?,
11981 transport,
11982 ))
11983}
11984
11985fn build_server_tls_config(
11986 options: &JavascriptTlsBridgeOptions,
11987) -> Result<ServerConfig, SidecarError> {
11988 let certificates = tls_certificates_from_material(options.cert.as_ref().ok_or_else(|| {
11989 SidecarError::InvalidState(String::from("TLS server upgrade requires a certificate"))
11990 })?)?;
11991 let key = tls_private_key_from_material(options.key.as_ref().ok_or_else(|| {
11992 SidecarError::InvalidState(String::from("TLS server upgrade requires a private key"))
11993 })?)?;
11994
11995 let mut config = ServerConfig::builder_with_provider(tls_provider())
11996 .with_safe_default_protocol_versions()
11997 .map_err(|error| {
11998 SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
11999 })?
12000 .with_no_client_auth()
12001 .with_single_cert(certificates, key)
12002 .map_err(|error| {
12003 SidecarError::InvalidState(format!("invalid TLS server config: {error}"))
12004 })?;
12005
12006 if let Some(protocols) = options.alpn_protocols.as_ref() {
12007 config.alpn_protocols = protocols
12008 .iter()
12009 .map(|protocol| protocol.as_bytes().to_vec())
12010 .collect();
12011 }
12012 Ok(config)
12013}
12014
12015fn tls_protocol_name(version: rustls::ProtocolVersion) -> String {
12016 match version {
12017 rustls::ProtocolVersion::TLSv1_2 => String::from("TLSv1.2"),
12018 rustls::ProtocolVersion::TLSv1_3 => String::from("TLSv1.3"),
12019 other => other
12020 .as_str()
12021 .map(str::to_owned)
12022 .unwrap_or_else(|| format!("{other:?}")),
12023 }
12024}
12025
12026fn tls_cipher_bridge_value(suite: rustls::SupportedCipherSuite) -> Value {
12027 tls_bridge_object(vec![
12028 (
12029 "name",
12030 suite
12031 .suite()
12032 .as_str()
12033 .map(|value| Value::String(value.to_owned()))
12034 .unwrap_or(Value::Null),
12035 ),
12036 (
12037 "standardName",
12038 suite
12039 .suite()
12040 .as_str()
12041 .map(|value| Value::String(value.to_owned()))
12042 .unwrap_or(Value::Null),
12043 ),
12044 (
12045 "version",
12046 Value::String(if suite.tls13().is_some() {
12047 String::from("TLSv1.3")
12048 } else {
12049 String::from("TLSv1.2")
12050 }),
12051 ),
12052 ])
12053}
12054
12055fn tls_certificate_bridge_value(certificate: &[u8], detailed: bool) -> Value {
12056 let mut fields = vec![("raw", tls_bridge_buffer_value(certificate))];
12057 if detailed {
12058 fields.push(("issuerCertificate", tls_bridge_undefined_value()));
12059 }
12060 tls_bridge_object(fields)
12061}
12062
12063fn tls_bridge_buffer_value(bytes: &[u8]) -> Value {
12064 json!({
12065 "type": "buffer",
12066 "data": base64::engine::general_purpose::STANDARD.encode(bytes),
12067 })
12068}
12069
12070fn tls_bridge_object(entries: Vec<(&str, Value)>) -> Value {
12071 let value = entries
12072 .into_iter()
12073 .map(|(key, value)| (key.to_owned(), value))
12074 .collect::<serde_json::Map<String, Value>>();
12075 json!({
12076 "type": "object",
12077 "id": 1,
12078 "value": value,
12079 })
12080}
12081
12082fn tls_bridge_undefined_value() -> Value {
12083 json!({
12084 "type": "undefined",
12085 })
12086}
12087
12088fn spawn_tcp_socket_reader(
12089 stream: TcpStream,
12090 sender: Sender<JavascriptTcpSocketEvent>,
12091 tls_mode: Arc<AtomicBool>,
12092 saw_local_shutdown: Arc<AtomicBool>,
12093 saw_remote_end: Arc<AtomicBool>,
12094 close_notified: Arc<AtomicBool>,
12095) {
12096 thread::spawn(move || {
12097 let mut stream = stream;
12098 let mut buffer = vec![0_u8; 64 * 1024];
12099 loop {
12100 if tls_mode.load(Ordering::SeqCst) {
12101 break;
12102 }
12103 match stream.read(&mut buffer) {
12104 Ok(0) => {
12105 saw_remote_end.store(true, Ordering::SeqCst);
12106 let _ = sender.send(JavascriptTcpSocketEvent::End);
12107 if saw_local_shutdown.load(Ordering::SeqCst)
12108 && !close_notified.swap(true, Ordering::SeqCst)
12109 {
12110 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12111 }
12112 break;
12113 }
12114 Ok(bytes_read) => {
12115 if sender
12116 .send(JavascriptTcpSocketEvent::Data(
12117 buffer[..bytes_read].to_vec(),
12118 ))
12119 .is_err()
12120 {
12121 break;
12122 }
12123 }
12124 Err(error)
12125 if matches!(
12126 error.kind(),
12127 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12128 ) =>
12129 {
12130 continue;
12131 }
12132 Err(error) => {
12133 let code = io_error_code(&error);
12134 let _ = sender.send(JavascriptTcpSocketEvent::Error {
12135 code,
12136 message: error.to_string(),
12137 });
12138 if !close_notified.swap(true, Ordering::SeqCst) {
12139 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12140 }
12141 break;
12142 }
12143 }
12144 }
12145 });
12146}
12147
12148fn spawn_tls_socket_reader(
12149 tls_stream: Arc<Mutex<Option<ActiveTlsStream>>>,
12150 sender: Sender<JavascriptTcpSocketEvent>,
12151 saw_local_shutdown: Arc<AtomicBool>,
12152 saw_remote_end: Arc<AtomicBool>,
12153 close_notified: Arc<AtomicBool>,
12154) {
12155 thread::spawn(move || {
12156 let mut buffer = vec![0_u8; 64 * 1024];
12157 loop {
12158 let read_result = {
12159 let mut guard = match tls_stream.lock() {
12160 Ok(guard) => guard,
12161 Err(_) => return,
12162 };
12163 let Some(stream) = guard.as_mut() else {
12164 return;
12165 };
12166 stream.read(&mut buffer)
12167 };
12168
12169 match read_result {
12170 Ok(0) => {
12171 saw_remote_end.store(true, Ordering::SeqCst);
12172 let _ = sender.send(JavascriptTcpSocketEvent::End);
12173 if saw_local_shutdown.load(Ordering::SeqCst)
12174 && !close_notified.swap(true, Ordering::SeqCst)
12175 {
12176 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12177 }
12178 break;
12179 }
12180 Ok(bytes_read) => {
12181 if sender
12182 .send(JavascriptTcpSocketEvent::Data(
12183 buffer[..bytes_read].to_vec(),
12184 ))
12185 .is_err()
12186 {
12187 break;
12188 }
12189 }
12190 Err(error)
12191 if matches!(
12192 error.kind(),
12193 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12194 ) =>
12195 {
12196 std::thread::sleep(Duration::from_millis(1));
12199 continue;
12200 }
12201 Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
12202 saw_remote_end.store(true, Ordering::SeqCst);
12203 let _ = sender.send(JavascriptTcpSocketEvent::End);
12204 if saw_local_shutdown.load(Ordering::SeqCst)
12205 && !close_notified.swap(true, Ordering::SeqCst)
12206 {
12207 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12208 }
12209 break;
12210 }
12211 Err(error) => {
12212 let code = io_error_code(&error);
12213 let _ = sender.send(JavascriptTcpSocketEvent::Error {
12214 code,
12215 message: error.to_string(),
12216 });
12217 if !close_notified.swap(true, Ordering::SeqCst) {
12218 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12219 }
12220 break;
12221 }
12222 }
12223 }
12224 });
12225}
12226
12227fn spawn_unix_socket_reader(
12228 stream: UnixStream,
12229 sender: Sender<JavascriptTcpSocketEvent>,
12230 saw_local_shutdown: Arc<AtomicBool>,
12231 saw_remote_end: Arc<AtomicBool>,
12232 close_notified: Arc<AtomicBool>,
12233) {
12234 thread::spawn(move || {
12235 let mut stream = stream;
12236 let mut buffer = vec![0_u8; 64 * 1024];
12237 loop {
12238 match stream.read(&mut buffer) {
12239 Ok(0) => {
12240 saw_remote_end.store(true, Ordering::SeqCst);
12241 let _ = sender.send(JavascriptTcpSocketEvent::End);
12242 if saw_local_shutdown.load(Ordering::SeqCst)
12243 && !close_notified.swap(true, Ordering::SeqCst)
12244 {
12245 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12246 }
12247 break;
12248 }
12249 Ok(bytes_read) => {
12250 if sender
12251 .send(JavascriptTcpSocketEvent::Data(
12252 buffer[..bytes_read].to_vec(),
12253 ))
12254 .is_err()
12255 {
12256 break;
12257 }
12258 }
12259 Err(error) => {
12260 let code = io_error_code(&error);
12261 let _ = sender.send(JavascriptTcpSocketEvent::Error {
12262 code,
12263 message: error.to_string(),
12264 });
12265 if !close_notified.swap(true, Ordering::SeqCst) {
12266 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12267 }
12268 break;
12269 }
12270 }
12271 }
12272 });
12273}
12274
12275fn terminate_child_process_tree(kernel: &mut SidecarKernel, process: &mut ActiveProcess) {
12276 let sqlite_database_ids = process.sqlite_databases.keys().copied().collect::<Vec<_>>();
12277 for database_id in sqlite_database_ids {
12278 let _ = close_sqlite_database(kernel, process, database_id);
12279 }
12280 process.sqlite_statements.clear();
12281 process.http_servers.clear();
12282 process.pending_http_requests.clear();
12283 if let Ok(mut http2) = process.http2.shared.lock() {
12284 let sessions = http2.sessions.values().cloned().collect::<Vec<_>>();
12285 http2.server_events.clear();
12286 http2.session_events.clear();
12287 http2.streams.clear();
12288 http2.servers.clear();
12289 http2.sessions.clear();
12290 drop(http2);
12291 for session in sessions {
12292 let (respond_to, _rx) = mpsc::channel();
12293 let _ = session.command_tx.send(Http2SessionCommand::Close {
12294 abrupt: true,
12295 respond_to,
12296 });
12297 }
12298 }
12299
12300 let listener_ids = process.tcp_listeners.keys().cloned().collect::<Vec<_>>();
12301 for listener_id in listener_ids {
12302 if let Some(listener) = process.tcp_listeners.remove(&listener_id) {
12303 let _ = listener.close(kernel, process.kernel_pid);
12304 }
12305 }
12306
12307 let sockets = process.tcp_sockets.keys().cloned().collect::<Vec<_>>();
12308 for socket_id in sockets {
12309 if let Some(socket) = process.tcp_sockets.remove(&socket_id) {
12310 let _ = socket.close(kernel, process.kernel_pid);
12311 }
12312 }
12313
12314 let unix_listener_ids = process.unix_listeners.keys().cloned().collect::<Vec<_>>();
12315 for listener_id in unix_listener_ids {
12316 if let Some(listener) = process.unix_listeners.remove(&listener_id) {
12317 let _ = listener.close();
12318 }
12319 }
12320
12321 let unix_sockets = process.unix_sockets.keys().cloned().collect::<Vec<_>>();
12322 for socket_id in unix_sockets {
12323 if let Some(socket) = process.unix_sockets.remove(&socket_id) {
12324 let _ = socket.close();
12325 }
12326 }
12327
12328 let udp_socket_ids = process.udp_sockets.keys().cloned().collect::<Vec<_>>();
12329 for socket_id in udp_socket_ids {
12330 if let Some(mut socket) = process.udp_sockets.remove(&socket_id) {
12331 socket.close(kernel, process.kernel_pid);
12332 }
12333 }
12334
12335 let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
12336 for child_id in child_ids {
12337 let Some(mut child) = process.child_processes.remove(&child_id) else {
12338 continue;
12339 };
12340 terminate_child_process_tree(kernel, &mut child);
12341 let _ = kernel.kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, SIGTERM);
12342 let _ = signal_runtime_process(child.execution.child_pid(), SIGTERM);
12343 child.kernel_handle.finish(0);
12344 let _ = kernel.wait_and_reap(child.kernel_pid);
12345 }
12346}
12347
12348fn service_javascript_sqlite_sync_rpc(
12349 kernel: &mut SidecarKernel,
12350 process: &mut ActiveProcess,
12351 request: &JavascriptSyncRpcRequest,
12352) -> Result<Value, SidecarError> {
12353 match request.method.as_str() {
12354 "sqlite.constants" => Ok(json!({})),
12355 "sqlite.open" => sqlite_open_database(kernel, process, request),
12356 "sqlite.close" => {
12357 let database_id =
12358 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.close database id")?;
12359 close_sqlite_database(kernel, process, database_id)?;
12360 Ok(Value::Null)
12361 }
12362 "sqlite.exec" => sqlite_exec_database(kernel, process, request),
12363 "sqlite.query" => sqlite_query_database(process, request),
12364 "sqlite.prepare" => sqlite_prepare_statement(process, request),
12365 "sqlite.location" => {
12366 let database_id =
12367 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.location database id")?;
12368 let database = sqlite_database(process, database_id)?;
12369 Ok(database
12370 .vm_path
12371 .as_ref()
12372 .map(|path| Value::String(path.clone()))
12373 .unwrap_or(Value::Null))
12374 }
12375 "sqlite.checkpoint" => {
12376 let database_id =
12377 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.checkpoint database id")?;
12378 let kernel_pid = process.kernel_pid;
12379 let database = sqlite_database_mut(process, database_id)?;
12380 sqlite_sync_database(kernel, kernel_pid, database)?;
12381 Ok(Value::Null)
12382 }
12383 "sqlite.statement.run" => sqlite_run_statement(kernel, process, request),
12384 "sqlite.statement.get" => sqlite_get_statement(process, request),
12385 "sqlite.statement.all" | "sqlite.statement.iterate" => {
12386 sqlite_all_statement(process, request)
12387 }
12388 "sqlite.statement.columns" => sqlite_statement_columns(process, request),
12389 "sqlite.statement.setReturnArrays" => {
12390 let statement_id = javascript_sync_rpc_arg_u64(
12391 &request.args,
12392 0,
12393 "sqlite.statement.setReturnArrays statement id",
12394 )?;
12395 let enabled = javascript_sync_rpc_arg_bool(
12396 &request.args,
12397 1,
12398 "sqlite.statement.setReturnArrays enabled",
12399 )?;
12400 sqlite_statement_mut(process, statement_id)?.return_arrays = enabled;
12401 Ok(Value::Null)
12402 }
12403 "sqlite.statement.setReadBigInts" => {
12404 let statement_id = javascript_sync_rpc_arg_u64(
12405 &request.args,
12406 0,
12407 "sqlite.statement.setReadBigInts statement id",
12408 )?;
12409 let enabled = javascript_sync_rpc_arg_bool(
12410 &request.args,
12411 1,
12412 "sqlite.statement.setReadBigInts enabled",
12413 )?;
12414 sqlite_statement_mut(process, statement_id)?.read_bigints = enabled;
12415 Ok(Value::Null)
12416 }
12417 "sqlite.statement.setAllowBareNamedParameters" => {
12418 let statement_id = javascript_sync_rpc_arg_u64(
12419 &request.args,
12420 0,
12421 "sqlite.statement.setAllowBareNamedParameters statement id",
12422 )?;
12423 let enabled = javascript_sync_rpc_arg_bool(
12424 &request.args,
12425 1,
12426 "sqlite.statement.setAllowBareNamedParameters enabled",
12427 )?;
12428 sqlite_statement_mut(process, statement_id)?.allow_bare_named_parameters = enabled;
12429 Ok(Value::Null)
12430 }
12431 "sqlite.statement.setAllowUnknownNamedParameters" => {
12432 let statement_id = javascript_sync_rpc_arg_u64(
12433 &request.args,
12434 0,
12435 "sqlite.statement.setAllowUnknownNamedParameters statement id",
12436 )?;
12437 let enabled = javascript_sync_rpc_arg_bool(
12438 &request.args,
12439 1,
12440 "sqlite.statement.setAllowUnknownNamedParameters enabled",
12441 )?;
12442 sqlite_statement_mut(process, statement_id)?.allow_unknown_named_parameters = enabled;
12443 Ok(Value::Null)
12444 }
12445 "sqlite.statement.finalize" => {
12446 let statement_id = javascript_sync_rpc_arg_u64(
12447 &request.args,
12448 0,
12449 "sqlite.statement.finalize statement id",
12450 )?;
12451 process
12452 .sqlite_statements
12453 .remove(&statement_id)
12454 .ok_or_else(|| {
12455 SidecarError::InvalidState(format!(
12456 "sqlite statement handle not found: {statement_id}"
12457 ))
12458 })?;
12459 Ok(Value::Null)
12460 }
12461 other => Err(SidecarError::InvalidState(format!(
12462 "unsupported JavaScript sqlite sync RPC method {other}"
12463 ))),
12464 }
12465}
12466
12467fn sqlite_open_database(
12468 kernel: &mut SidecarKernel,
12469 process: &mut ActiveProcess,
12470 request: &JavascriptSyncRpcRequest,
12471) -> Result<Value, SidecarError> {
12472 ensure_per_process_state_handle_capacity(process.sqlite_databases.len(), "sqlite database")?;
12473 let path = request.args.first().and_then(Value::as_str);
12474 let vm_path = path.filter(|value| !value.is_empty() && *value != ":memory:");
12475 let options = request.args.get(1);
12476 let read_only = sqlite_option_bool(options, "readOnly").unwrap_or(false);
12477 let create = sqlite_option_bool(options, "create").unwrap_or(!read_only);
12478 let timeout_ms = sqlite_option_u64(options, "timeout");
12479
12480 process.next_sqlite_database_id += 1;
12481 let database_id = process.next_sqlite_database_id;
12482
12483 let host_path = if vm_path.is_some() {
12484 Some(
12485 std::env::temp_dir()
12486 .join(format!(
12487 "secure-exec-sidecar-sqlite-{}-{database_id}",
12488 process.kernel_pid
12489 ))
12490 .join("database.sqlite"),
12491 )
12492 } else {
12493 None
12494 };
12495
12496 if let Some(host_path) = host_path.as_ref() {
12497 if let Some(parent) = host_path.parent() {
12498 fs::create_dir_all(parent).map_err(|error| {
12499 SidecarError::Io(format!(
12500 "failed to prepare sqlite temp directory {}: {error}",
12501 parent.display()
12502 ))
12503 })?;
12504 }
12505 }
12506
12507 if let (Some(vm_path), Some(host_path)) = (vm_path, host_path.as_ref()) {
12508 if kernel
12509 .exists_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
12510 .map_err(kernel_error)?
12511 {
12512 let contents = kernel
12513 .read_file_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
12514 .map_err(kernel_error)?;
12515 fs::write(host_path, contents).map_err(|error| {
12516 SidecarError::Io(format!(
12517 "failed to materialize sqlite database {}: {error}",
12518 host_path.display()
12519 ))
12520 })?;
12521 } else if read_only && !create {
12522 return Err(SidecarError::InvalidState(format!(
12523 "sqlite database does not exist: {vm_path}"
12524 )));
12525 }
12526 }
12527
12528 let target = host_path
12529 .as_ref()
12530 .map(|path| path.to_string_lossy().into_owned())
12531 .unwrap_or_else(|| String::from(":memory:"));
12532 let mut flags = if read_only {
12533 SqliteOpenFlags::SQLITE_OPEN_READ_ONLY
12534 } else {
12535 SqliteOpenFlags::SQLITE_OPEN_READ_WRITE
12536 };
12537 if create && !read_only {
12538 flags |= SqliteOpenFlags::SQLITE_OPEN_CREATE;
12539 }
12540
12541 let connection = SqliteConnection::open_with_flags(&target, flags).map_err(|error| {
12542 SidecarError::InvalidState(format!(
12543 "sqlite database open failed for {}: {error}",
12544 vm_path.unwrap_or(":memory:")
12545 ))
12546 })?;
12547 if let Some(timeout_ms) = timeout_ms {
12548 connection
12549 .busy_timeout(Duration::from_millis(timeout_ms))
12550 .map_err(sqlite_error)?;
12551 }
12552 if host_path.is_some() && !read_only {
12553 let _ = connection.pragma_update(None, "journal_mode", "WAL");
12554 }
12555
12556 process.sqlite_databases.insert(
12557 database_id,
12558 ActiveSqliteDatabase {
12559 connection,
12560 host_path,
12561 vm_path: vm_path.map(String::from),
12562 dirty: false,
12563 transaction_depth: 0,
12564 read_only,
12565 },
12566 );
12567
12568 Ok(json!(database_id))
12569}
12570
12571fn sqlite_exec_database(
12572 kernel: &mut SidecarKernel,
12573 process: &mut ActiveProcess,
12574 request: &JavascriptSyncRpcRequest,
12575) -> Result<Value, SidecarError> {
12576 let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.exec database id")?;
12577 let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.exec sql")?;
12578 let kernel_pid = process.kernel_pid;
12579 let database = sqlite_database_mut(process, database_id)?;
12580 let before = database.connection.total_changes();
12581 database
12582 .connection
12583 .execute_batch(sql)
12584 .map_err(sqlite_error)?;
12585 mark_sqlite_mutation(database, sql);
12586 sqlite_sync_database(kernel, kernel_pid, database)?;
12587 Ok(json!(database
12588 .connection
12589 .total_changes()
12590 .saturating_sub(before)))
12591}
12592
12593fn sqlite_query_database(
12594 process: &mut ActiveProcess,
12595 request: &JavascriptSyncRpcRequest,
12596) -> Result<Value, SidecarError> {
12597 let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.query database id")?;
12598 let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.query sql")?;
12599 let params = request.args.get(2);
12600 let options = request.args.get(3);
12601 let return_arrays = sqlite_option_bool(options, "returnArrays").unwrap_or(false);
12602 let read_bigints = sqlite_option_bool(options, "readBigInts").unwrap_or(false);
12603 let database = sqlite_database_mut(process, database_id)?;
12604 sqlite_query_rows(
12605 &mut database.connection,
12606 sql,
12607 params,
12608 return_arrays,
12609 read_bigints,
12610 true,
12611 false,
12612 )
12613}
12614
12615fn sqlite_prepare_statement(
12616 process: &mut ActiveProcess,
12617 request: &JavascriptSyncRpcRequest,
12618) -> Result<Value, SidecarError> {
12619 ensure_per_process_state_handle_capacity(process.sqlite_statements.len(), "sqlite statement")?;
12620 let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.prepare database id")?;
12621 let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.prepare sql")?;
12622 let _ = sqlite_database(process, database_id)?;
12623 process.next_sqlite_statement_id += 1;
12624 let statement_id = process.next_sqlite_statement_id;
12625 process.sqlite_statements.insert(
12626 statement_id,
12627 ActiveSqliteStatement {
12628 database_id,
12629 sql: sql.to_owned(),
12630 return_arrays: false,
12631 read_bigints: false,
12632 allow_bare_named_parameters: false,
12633 allow_unknown_named_parameters: false,
12634 },
12635 );
12636 Ok(json!(statement_id))
12637}
12638
12639fn sqlite_run_statement(
12640 kernel: &mut SidecarKernel,
12641 process: &mut ActiveProcess,
12642 request: &JavascriptSyncRpcRequest,
12643) -> Result<Value, SidecarError> {
12644 let statement_id =
12645 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.run statement id")?;
12646 let params = request.args.get(1);
12647 let statement_state = sqlite_statement(process, statement_id)?.clone();
12648 let kernel_pid = process.kernel_pid;
12649 let database = sqlite_database_mut(process, statement_state.database_id)?;
12650 let before = database.connection.total_changes();
12651 {
12652 let mut statement = database
12653 .connection
12654 .prepare(&statement_state.sql)
12655 .map_err(sqlite_error)?;
12656 bind_sqlite_parameters(
12657 &mut statement,
12658 params,
12659 statement_state.allow_bare_named_parameters,
12660 statement_state.allow_unknown_named_parameters,
12661 )?;
12662 statement.raw_execute().map_err(sqlite_error)?;
12663 }
12664 let changes = database.connection.total_changes().saturating_sub(before);
12665 let last_insert_rowid = database.connection.last_insert_rowid();
12666 mark_sqlite_mutation(database, &statement_state.sql);
12667 sqlite_sync_database(kernel, kernel_pid, database)?;
12668 let result = json!({
12669 "changes": changes,
12670 "lastInsertRowid": encode_sqlite_integer(last_insert_rowid, true),
12671 });
12672 Ok(result)
12673}
12674
12675fn sqlite_get_statement(
12676 process: &mut ActiveProcess,
12677 request: &JavascriptSyncRpcRequest,
12678) -> Result<Value, SidecarError> {
12679 let statement_id =
12680 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.get statement id")?;
12681 let params = request.args.get(1);
12682 let statement_state = sqlite_statement(process, statement_id)?.clone();
12683 let database = sqlite_database_mut(process, statement_state.database_id)?;
12684 let rows = sqlite_query_rows(
12685 &mut database.connection,
12686 &statement_state.sql,
12687 params,
12688 statement_state.return_arrays,
12689 statement_state.read_bigints,
12690 statement_state.allow_bare_named_parameters,
12691 statement_state.allow_unknown_named_parameters,
12692 )?;
12693 Ok(rows
12694 .as_array()
12695 .and_then(|rows| rows.first().cloned())
12696 .unwrap_or(Value::Null))
12697}
12698
12699fn sqlite_all_statement(
12700 process: &mut ActiveProcess,
12701 request: &JavascriptSyncRpcRequest,
12702) -> Result<Value, SidecarError> {
12703 let statement_id =
12704 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.all statement id")?;
12705 let params = request.args.get(1);
12706 let statement_state = sqlite_statement(process, statement_id)?.clone();
12707 let database = sqlite_database_mut(process, statement_state.database_id)?;
12708 sqlite_query_rows(
12709 &mut database.connection,
12710 &statement_state.sql,
12711 params,
12712 statement_state.return_arrays,
12713 statement_state.read_bigints,
12714 statement_state.allow_bare_named_parameters,
12715 statement_state.allow_unknown_named_parameters,
12716 )
12717}
12718
12719fn sqlite_statement_columns(
12720 process: &mut ActiveProcess,
12721 request: &JavascriptSyncRpcRequest,
12722) -> Result<Value, SidecarError> {
12723 let statement_id =
12724 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.columns statement id")?;
12725 let statement_state = sqlite_statement(process, statement_id)?.clone();
12726 let database = sqlite_database_mut(process, statement_state.database_id)?;
12727 let statement = database
12728 .connection
12729 .prepare(&statement_state.sql)
12730 .map_err(sqlite_error)?;
12731 Ok(Value::Array(
12732 statement
12733 .column_names()
12734 .iter()
12735 .map(|name| json!({ "name": name }))
12736 .collect(),
12737 ))
12738}
12739
12740fn sqlite_query_rows(
12741 connection: &mut SqliteConnection,
12742 sql: &str,
12743 params: Option<&Value>,
12744 return_arrays: bool,
12745 read_bigints: bool,
12746 allow_bare_named_parameters: bool,
12747 allow_unknown_named_parameters: bool,
12748) -> Result<Value, SidecarError> {
12749 let mut statement = connection.prepare(sql).map_err(sqlite_error)?;
12750 let column_names = statement
12751 .column_names()
12752 .iter()
12753 .map(|name| (*name).to_owned())
12754 .collect::<Vec<_>>();
12755 let column_count = statement.column_count();
12756 bind_sqlite_parameters(
12757 &mut statement,
12758 params,
12759 allow_bare_named_parameters,
12760 allow_unknown_named_parameters,
12761 )?;
12762 let mut rows = statement.raw_query();
12763 let mut encoded_rows = Vec::new();
12764 while let Some(row) = rows.next().map_err(sqlite_error)? {
12765 encoded_rows.push(encode_sqlite_row(
12766 row,
12767 &column_names,
12768 column_count,
12769 return_arrays,
12770 read_bigints,
12771 )?);
12772 }
12773 Ok(Value::Array(encoded_rows))
12774}
12775
12776fn encode_sqlite_row(
12777 row: &rusqlite::Row<'_>,
12778 column_names: &[String],
12779 column_count: usize,
12780 return_arrays: bool,
12781 read_bigints: bool,
12782) -> Result<Value, SidecarError> {
12783 if return_arrays {
12784 let mut values = Vec::with_capacity(column_count);
12785 for index in 0..column_count {
12786 values.push(encode_sqlite_value_ref(
12787 row.get_ref(index).map_err(sqlite_error)?,
12788 read_bigints,
12789 )?);
12790 }
12791 return Ok(Value::Array(values));
12792 }
12793
12794 let mut object = Map::with_capacity(column_count);
12795 for (index, name) in column_names.iter().enumerate() {
12796 object.insert(
12797 name.clone(),
12798 encode_sqlite_value_ref(row.get_ref(index).map_err(sqlite_error)?, read_bigints)?,
12799 );
12800 }
12801 Ok(Value::Object(object))
12802}
12803
12804fn encode_sqlite_value_ref(
12805 value: SqliteValueRef<'_>,
12806 read_bigints: bool,
12807) -> Result<Value, SidecarError> {
12808 Ok(match value {
12809 SqliteValueRef::Null => Value::Null,
12810 SqliteValueRef::Integer(number) => encode_sqlite_integer(number, read_bigints),
12811 SqliteValueRef::Real(number) => json!(number),
12812 SqliteValueRef::Text(text) => Value::String(String::from_utf8_lossy(text).into_owned()),
12813 SqliteValueRef::Blob(bytes) => json!({
12814 "__agentosSqliteType": "uint8array",
12815 "value": base64::engine::general_purpose::STANDARD.encode(bytes),
12816 }),
12817 })
12818}
12819
12820fn encode_sqlite_integer(number: i64, read_bigints: bool) -> Value {
12821 if read_bigints || number.abs() > SQLITE_JS_SAFE_INTEGER_MAX {
12822 json!({
12823 "__agentosSqliteType": "bigint",
12824 "value": number.to_string(),
12825 })
12826 } else {
12827 json!(number)
12828 }
12829}
12830
12831fn bind_sqlite_parameters(
12832 statement: &mut SqliteStatement<'_>,
12833 params: Option<&Value>,
12834 allow_bare_named_parameters: bool,
12835 allow_unknown_named_parameters: bool,
12836) -> Result<(), SidecarError> {
12837 let Some(params) = params else {
12838 return Ok(());
12839 };
12840 match params {
12841 Value::Null => Ok(()),
12842 Value::Array(values) => {
12843 for (index, value) in values.iter().enumerate() {
12844 statement
12845 .raw_bind_parameter(index + 1, decode_sqlite_parameter(value)?)
12846 .map_err(sqlite_error)?;
12847 }
12848 Ok(())
12849 }
12850 Value::Object(map)
12851 if map
12852 .get("__agentosSqliteType")
12853 .and_then(Value::as_str)
12854 .is_none() =>
12855 {
12856 for (key, value) in map {
12857 let index =
12858 resolve_sqlite_parameter_index(statement, key, allow_bare_named_parameters)?;
12859 let Some(index) = index else {
12860 if allow_unknown_named_parameters {
12861 continue;
12862 }
12863 return Err(SidecarError::InvalidState(format!(
12864 "sqlite named parameter not found: {key}"
12865 )));
12866 };
12867 statement
12868 .raw_bind_parameter(index, decode_sqlite_parameter(value)?)
12869 .map_err(sqlite_error)?;
12870 }
12871 Ok(())
12872 }
12873 other => statement
12874 .raw_bind_parameter(1, decode_sqlite_parameter(other)?)
12875 .map_err(sqlite_error),
12876 }
12877}
12878
12879fn resolve_sqlite_parameter_index(
12880 statement: &mut SqliteStatement<'_>,
12881 key: &str,
12882 allow_bare_named_parameters: bool,
12883) -> Result<Option<usize>, SidecarError> {
12884 let mut candidates = vec![key.to_owned()];
12885 if allow_bare_named_parameters
12886 && !key.starts_with(':')
12887 && !key.starts_with('@')
12888 && !key.starts_with('$')
12889 {
12890 candidates.push(format!(":{key}"));
12891 candidates.push(format!("@{key}"));
12892 candidates.push(format!("${key}"));
12893 }
12894 for candidate in candidates {
12895 if let Some(index) = statement
12896 .parameter_index(&candidate)
12897 .map_err(sqlite_error)?
12898 {
12899 return Ok(Some(index));
12900 }
12901 }
12902 Ok(None)
12903}
12904
12905fn decode_sqlite_parameter(value: &Value) -> Result<rusqlite::types::Value, SidecarError> {
12906 Ok(match value {
12907 Value::Null => rusqlite::types::Value::Null,
12908 Value::Bool(value) => rusqlite::types::Value::Integer(i64::from(*value)),
12909 Value::Number(value) => match (value.as_i64(), value.as_f64()) {
12910 (Some(integer), _) => rusqlite::types::Value::Integer(integer),
12911 (_, Some(real)) => rusqlite::types::Value::Real(real),
12912 _ => {
12913 return Err(SidecarError::InvalidState(String::from(
12914 "sqlite parameter number is not representable",
12915 )));
12916 }
12917 },
12918 Value::String(value) => rusqlite::types::Value::Text(value.clone()),
12919 Value::Array(_) => {
12920 return Err(SidecarError::InvalidState(String::from(
12921 "sqlite parameters do not support nested arrays",
12922 )));
12923 }
12924 Value::Object(map) => match map.get("__agentosSqliteType").and_then(Value::as_str) {
12925 Some("bigint") => rusqlite::types::Value::Integer(
12926 map.get("value")
12927 .and_then(Value::as_str)
12928 .ok_or_else(|| {
12929 SidecarError::InvalidState(String::from(
12930 "sqlite bigint parameter missing string value",
12931 ))
12932 })?
12933 .parse::<i64>()
12934 .map_err(|error| {
12935 SidecarError::InvalidState(format!(
12936 "sqlite bigint parameter is not a signed 64-bit integer: {error}"
12937 ))
12938 })?,
12939 ),
12940 Some("uint8array") => rusqlite::types::Value::Blob(
12941 base64::engine::general_purpose::STANDARD
12942 .decode(map.get("value").and_then(Value::as_str).ok_or_else(|| {
12943 SidecarError::InvalidState(String::from(
12944 "sqlite blob parameter missing base64 value",
12945 ))
12946 })?)
12947 .map_err(|error| {
12948 SidecarError::InvalidState(format!(
12949 "sqlite blob parameter contains invalid base64: {error}"
12950 ))
12951 })?,
12952 ),
12953 Some(other) => {
12954 return Err(SidecarError::InvalidState(format!(
12955 "unsupported sqlite tagged parameter type {other}"
12956 )));
12957 }
12958 None => {
12959 return Err(SidecarError::InvalidState(String::from(
12960 "sqlite named parameter objects must be passed as the top-level params object",
12961 )));
12962 }
12963 },
12964 })
12965}
12966
12967fn close_sqlite_database(
12968 kernel: &mut SidecarKernel,
12969 process: &mut ActiveProcess,
12970 database_id: u64,
12971) -> Result<(), SidecarError> {
12972 let mut database = process
12973 .sqlite_databases
12974 .remove(&database_id)
12975 .ok_or_else(|| {
12976 SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
12977 })?;
12978 process
12979 .sqlite_statements
12980 .retain(|_, statement| statement.database_id != database_id);
12981 sqlite_sync_database(kernel, process.kernel_pid, &mut database)?;
12982 let host_path = database.host_path.clone();
12983 drop(database);
12984 cleanup_sqlite_host_artifacts(host_path.as_deref())?;
12985 Ok(())
12986}
12987
12988fn ensure_per_process_state_handle_capacity(len: usize, label: &str) -> Result<(), SidecarError> {
12989 if len >= MAX_PER_PROCESS_STATE_HANDLES {
12990 return Err(SidecarError::InvalidState(format!(
12991 "{label} handle limit exceeded: limit is {MAX_PER_PROCESS_STATE_HANDLES}"
12992 )));
12993 }
12994 Ok(())
12995}
12996
12997fn sqlite_sync_database(
12998 kernel: &mut SidecarKernel,
12999 kernel_pid: u32,
13000 database: &mut ActiveSqliteDatabase,
13001) -> Result<(), SidecarError> {
13002 if !database.dirty
13003 || database.transaction_depth > 0
13004 || database.read_only
13005 || database.host_path.is_none()
13006 || database.vm_path.is_none()
13007 {
13008 return Ok(());
13009 }
13010
13011 let _ = database
13012 .connection
13013 .execute_batch("PRAGMA wal_checkpoint(TRUNCATE)");
13014 let host_path = database.host_path.as_ref().expect("sqlite host path");
13015 if !host_path.exists() {
13016 return Ok(());
13017 }
13018 ensure_vm_parent_dir(
13019 kernel,
13020 kernel_pid,
13021 database.vm_path.as_deref().expect("sqlite vm path"),
13022 )?;
13023 let contents = fs::read(host_path).map_err(|error| {
13024 SidecarError::Io(format!(
13025 "failed to read sqlite temp database {}: {error}",
13026 host_path.display()
13027 ))
13028 })?;
13029 kernel
13030 .write_file_for_process(
13031 EXECUTION_DRIVER_NAME,
13032 kernel_pid,
13033 database.vm_path.as_deref().expect("sqlite vm path"),
13034 contents,
13035 None,
13036 )
13037 .map_err(kernel_error)?;
13038 database.dirty = false;
13039 Ok(())
13040}
13041
13042fn cleanup_sqlite_host_artifacts(host_path: Option<&Path>) -> Result<(), SidecarError> {
13043 let Some(host_path) = host_path else {
13044 return Ok(());
13045 };
13046 let parent = host_path.parent().map(PathBuf::from);
13047 for suffix in ["", "-wal", "-shm"] {
13048 let path = PathBuf::from(format!("{}{}", host_path.display(), suffix));
13049 if path.exists() {
13050 fs::remove_file(&path).map_err(|error| {
13051 SidecarError::Io(format!(
13052 "failed to remove sqlite temp artifact {}: {error}",
13053 path.display()
13054 ))
13055 })?;
13056 }
13057 }
13058 if let Some(parent) = parent {
13059 let _ = fs::remove_dir_all(parent);
13060 }
13061 Ok(())
13062}
13063
13064fn ensure_vm_parent_dir(
13065 kernel: &mut SidecarKernel,
13066 kernel_pid: u32,
13067 path: &str,
13068) -> Result<(), SidecarError> {
13069 let parent = dirname(path);
13070 if parent == "/" || parent == "." {
13071 return Ok(());
13072 }
13073 let mut current = String::new();
13074 for segment in parent.split('/').filter(|segment| !segment.is_empty()) {
13075 current.push('/');
13076 current.push_str(segment);
13077 if !kernel
13078 .exists_for_process(EXECUTION_DRIVER_NAME, kernel_pid, ¤t)
13079 .map_err(kernel_error)?
13080 {
13081 kernel
13082 .mkdir_for_process(EXECUTION_DRIVER_NAME, kernel_pid, ¤t, false, None)
13083 .map_err(kernel_error)?;
13084 }
13085 }
13086 Ok(())
13087}
13088
13089fn sqlite_database(
13090 process: &ActiveProcess,
13091 database_id: u64,
13092) -> Result<&ActiveSqliteDatabase, SidecarError> {
13093 process.sqlite_databases.get(&database_id).ok_or_else(|| {
13094 SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13095 })
13096}
13097
13098fn sqlite_database_mut(
13099 process: &mut ActiveProcess,
13100 database_id: u64,
13101) -> Result<&mut ActiveSqliteDatabase, SidecarError> {
13102 process
13103 .sqlite_databases
13104 .get_mut(&database_id)
13105 .ok_or_else(|| {
13106 SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13107 })
13108}
13109
13110fn sqlite_statement(
13111 process: &ActiveProcess,
13112 statement_id: u64,
13113) -> Result<&ActiveSqliteStatement, SidecarError> {
13114 process.sqlite_statements.get(&statement_id).ok_or_else(|| {
13115 SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
13116 })
13117}
13118
13119fn sqlite_statement_mut(
13120 process: &mut ActiveProcess,
13121 statement_id: u64,
13122) -> Result<&mut ActiveSqliteStatement, SidecarError> {
13123 process
13124 .sqlite_statements
13125 .get_mut(&statement_id)
13126 .ok_or_else(|| {
13127 SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
13128 })
13129}
13130
13131fn mark_sqlite_mutation(database: &mut ActiveSqliteDatabase, sql: &str) {
13132 let normalized = sql.trim_start().to_ascii_lowercase();
13133 if normalized.starts_with("begin") || normalized.starts_with("savepoint") {
13134 database.dirty = true;
13135 database.transaction_depth += 1;
13136 return;
13137 }
13138 if normalized.starts_with("commit") || normalized.starts_with("release savepoint") {
13139 database.dirty = true;
13140 database.transaction_depth = database.transaction_depth.saturating_sub(1);
13141 return;
13142 }
13143 if normalized.starts_with("rollback") && !normalized.starts_with("rollback to") {
13144 database.dirty = true;
13145 database.transaction_depth = database.transaction_depth.saturating_sub(1);
13146 return;
13147 }
13148 if normalized.starts_with("insert")
13149 || normalized.starts_with("update")
13150 || normalized.starts_with("delete")
13151 || normalized.starts_with("replace")
13152 || normalized.starts_with("create")
13153 || normalized.starts_with("alter")
13154 || normalized.starts_with("drop")
13155 || normalized.starts_with("vacuum")
13156 || normalized.starts_with("reindex")
13157 || normalized.starts_with("analyze")
13158 || normalized.starts_with("attach")
13159 || normalized.starts_with("detach")
13160 || normalized.starts_with("pragma")
13161 {
13162 database.dirty = true;
13163 }
13164}
13165
13166fn sqlite_option_bool(options: Option<&Value>, key: &str) -> Option<bool> {
13167 options
13168 .and_then(|value| value.get(key))
13169 .and_then(Value::as_bool)
13170}
13171
13172fn sqlite_option_u64(options: Option<&Value>, key: &str) -> Option<u64> {
13173 options
13174 .and_then(|value| value.get(key))
13175 .and_then(Value::as_u64)
13176}
13177
13178fn sqlite_error(error: rusqlite::Error) -> SidecarError {
13179 SidecarError::InvalidState(format!("sqlite error: {error}"))
13180}
13181
13182pub(crate) fn javascript_sync_rpc_arg_str<'a>(
13183 args: &'a [Value],
13184 index: usize,
13185 label: &str,
13186) -> Result<&'a str, SidecarError> {
13187 args.get(index)
13188 .and_then(Value::as_str)
13189 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a string argument")))
13190}
13191
13192pub(crate) fn javascript_sync_rpc_arg_bool(
13193 args: &[Value],
13194 index: usize,
13195 label: &str,
13196) -> Result<bool, SidecarError> {
13197 args.get(index)
13198 .and_then(Value::as_bool)
13199 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a boolean argument")))
13200}
13201
13202pub(crate) fn javascript_sync_rpc_encoding(args: &[Value]) -> Option<String> {
13203 args.get(1).and_then(|value| {
13204 value.as_str().map(str::to_owned).or_else(|| {
13205 value
13206 .get("encoding")
13207 .and_then(Value::as_str)
13208 .map(str::to_owned)
13209 })
13210 })
13211}
13212
13213pub(crate) fn javascript_sync_rpc_option_bool(
13214 args: &[Value],
13215 index: usize,
13216 key: &str,
13217) -> Option<bool> {
13218 let value = args.get(index)?;
13219 if key == "recursive" {
13220 if let Some(boolean) = value.as_bool() {
13221 return Some(boolean);
13222 }
13223 }
13224 value.get(key).and_then(Value::as_bool)
13225}
13226
13227pub(crate) fn javascript_sync_rpc_option_u32(
13228 args: &[Value],
13229 index: usize,
13230 key: &str,
13231) -> Result<Option<u32>, SidecarError> {
13232 let Some(value) = args.get(index).and_then(|value| {
13233 if value.is_object() {
13234 value.get(key)
13235 } else if key == "mode" && value.is_number() {
13236 Some(value)
13237 } else {
13238 None
13239 }
13240 }) else {
13241 return Ok(None);
13242 };
13243 if value.is_null() {
13244 return Ok(None);
13245 }
13246
13247 let numeric = value
13248 .as_u64()
13249 .or_else(|| {
13250 value
13251 .as_f64()
13252 .filter(|number| number.is_finite() && *number >= 0.0)
13253 .map(|number| number as u64)
13254 })
13255 .ok_or_else(|| SidecarError::InvalidState(format!("{key} must be numeric")))?;
13256
13257 u32::try_from(numeric)
13258 .map(Some)
13259 .map_err(|_| SidecarError::InvalidState(format!("{key} must fit within u32")))
13260}
13261
13262pub(crate) fn javascript_sync_rpc_arg_u32(
13263 args: &[Value],
13264 index: usize,
13265 label: &str,
13266) -> Result<u32, SidecarError> {
13267 let value = javascript_sync_rpc_arg_u64(args, index, label)?;
13268 u32::try_from(value)
13269 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
13270}
13271
13272pub(crate) fn javascript_sync_rpc_arg_i32(
13273 args: &[Value],
13274 index: usize,
13275 label: &str,
13276) -> Result<i32, SidecarError> {
13277 let Some(value) = args.get(index) else {
13278 return Err(SidecarError::InvalidState(format!("{label} is required")));
13279 };
13280
13281 let numeric = value
13282 .as_i64()
13283 .or_else(|| {
13284 value
13285 .as_f64()
13286 .filter(|number| number.is_finite())
13287 .map(|number| number as i64)
13288 })
13289 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))?;
13290
13291 i32::try_from(numeric)
13292 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within i32")))
13293}
13294
13295pub(crate) fn javascript_sync_rpc_arg_u32_optional(
13296 args: &[Value],
13297 index: usize,
13298 label: &str,
13299) -> Result<Option<u32>, SidecarError> {
13300 javascript_sync_rpc_arg_u64_optional(args, index, label)?
13301 .map(|value| {
13302 u32::try_from(value)
13303 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
13304 })
13305 .transpose()
13306}
13307
13308pub(crate) fn javascript_sync_rpc_arg_u64(
13309 args: &[Value],
13310 index: usize,
13311 label: &str,
13312) -> Result<u64, SidecarError> {
13313 let Some(value) = args.get(index) else {
13314 return Err(SidecarError::InvalidState(format!("{label} is required")));
13315 };
13316
13317 value
13318 .as_u64()
13319 .or_else(|| {
13320 value
13321 .as_f64()
13322 .filter(|number| number.is_finite() && *number >= 0.0)
13323 .map(|number| number as u64)
13324 })
13325 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))
13326}
13327
13328pub(crate) fn javascript_sync_rpc_arg_u64_optional(
13329 args: &[Value],
13330 index: usize,
13331 label: &str,
13332) -> Result<Option<u64>, SidecarError> {
13333 let Some(value) = args.get(index) else {
13334 return Ok(None);
13335 };
13336 if value.is_null() {
13337 return Ok(None);
13338 }
13339 javascript_sync_rpc_arg_u64(args, index, label).map(Some)
13340}
13341
13342pub(crate) fn javascript_sync_rpc_bytes_arg(
13343 args: &[Value],
13344 index: usize,
13345 label: &str,
13346) -> Result<Vec<u8>, SidecarError> {
13347 let Some(value) = args.get(index) else {
13348 return Err(SidecarError::InvalidState(format!("{label} is required")));
13349 };
13350
13351 if let Some(text) = value.as_str() {
13352 return Ok(text.as_bytes().to_vec());
13353 }
13354
13355 let Some(base64_value) = value
13356 .get("__agentOsType")
13357 .and_then(Value::as_str)
13358 .filter(|kind| *kind == "bytes")
13359 .and_then(|_| value.get("base64"))
13360 .and_then(Value::as_str)
13361 else {
13362 return Err(SidecarError::InvalidState(format!(
13363 "{label} must be a string or encoded bytes payload"
13364 )));
13365 };
13366
13367 base64::engine::general_purpose::STANDARD
13368 .decode(base64_value)
13369 .map_err(|error| {
13370 SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
13371 })
13372}
13373
13374pub(crate) fn javascript_sync_rpc_bytes_value(bytes: &[u8]) -> Value {
13375 json!({
13376 "__agentOsType": "bytes",
13377 "base64": base64::engine::general_purpose::STANDARD.encode(bytes),
13378 })
13379}
13380
13381#[derive(Debug, Deserialize)]
13382struct KernelPollFdRequest {
13383 fd: u32,
13384 events: u16,
13385}
13386
13387#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
13388struct KernelPollFdResponse {
13389 fd: u32,
13390 events: u16,
13391 revents: u16,
13392}
13393
13394fn javascript_sync_rpc_base64_arg(
13395 args: &[Value],
13396 index: usize,
13397 label: &str,
13398) -> Result<Vec<u8>, SidecarError> {
13399 let value = javascript_sync_rpc_arg_str(args, index, label)?;
13400 base64::engine::general_purpose::STANDARD
13401 .decode(value)
13402 .map_err(|error| {
13403 SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
13404 })
13405}
13406
13407pub(crate) fn service_javascript_sync_rpc<B>(
13408 request: JavascriptSyncRpcServiceRequest<'_, B>,
13409) -> Result<Value, SidecarError>
13410where
13411 B: NativeSidecarBridge + Send + 'static,
13412 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
13413{
13414 let JavascriptSyncRpcServiceRequest {
13415 bridge,
13416 vm_id,
13417 dns,
13418 socket_paths,
13419 kernel,
13420 process,
13421 sync_request: request,
13422 resource_limits,
13423 network_counts,
13424 } = request;
13425 match request.method.as_str() {
13426 "_resolveModule"
13429 | "_resolveModuleSync"
13430 | "__resolve_module"
13431 | "_batchResolveModules"
13432 | "__batch_resolve_modules"
13433 | "_loadFile"
13434 | "_loadFileSync"
13435 | "__load_file"
13436 | "_moduleFormat"
13437 | "__module_format" => service_javascript_module_sync_rpc(kernel, process, request),
13438 "_loadPolyfill" | "__load_polyfill" => {
13440 service_javascript_internal_bridge_sync_rpc(process, request)
13441 }
13442 "__kernel_stdin_read" => match &process.execution {
13443 ActiveExecution::Javascript(execution) => execution
13444 .read_kernel_stdin_sync_rpc(request)
13445 .map_err(|error| SidecarError::Execution(error.to_string())),
13446 ActiveExecution::Python(_) | ActiveExecution::Wasm(_) | ActiveExecution::Tool(_) => {
13447 service_javascript_kernel_stdin_sync_rpc(kernel, process, request)
13448 }
13449 },
13450 "__kernel_stdio_write" => {
13451 service_javascript_kernel_stdio_write_sync_rpc(kernel, process, request)
13452 }
13453 "__kernel_poll" => service_javascript_kernel_poll_sync_rpc(kernel, process, request),
13454 "__pty_set_raw_mode" => {
13455 service_javascript_pty_set_raw_mode_sync_rpc(kernel, process, request)
13456 }
13457 "crypto.hashDigest"
13458 | "crypto.hmacDigest"
13459 | "crypto.pbkdf2"
13460 | "crypto.scrypt"
13461 | "crypto.cipheriv"
13462 | "crypto.decipheriv"
13463 | "crypto.cipherivCreate"
13464 | "crypto.cipherivUpdate"
13465 | "crypto.cipherivFinal"
13466 | "crypto.sign"
13467 | "crypto.verify"
13468 | "crypto.asymmetricOp"
13469 | "crypto.createKeyObject"
13470 | "crypto.generateKeyPairSync"
13471 | "crypto.generateKeySync"
13472 | "crypto.generatePrimeSync"
13473 | "crypto.diffieHellman"
13474 | "crypto.diffieHellmanGroup"
13475 | "crypto.diffieHellmanSessionCreate"
13476 | "crypto.diffieHellmanSessionCall"
13477 | "crypto.diffieHellmanSessionDestroy"
13478 | "crypto.subtle" => service_javascript_crypto_sync_rpc(process, request),
13479 "dns.lookup" | "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
13480 service_javascript_dns_sync_rpc(bridge, kernel, vm_id, dns, request)
13481 }
13482 "net.http_listen" | "net.http_close" | "net.http_wait" | "net.http_respond" => {
13483 service_javascript_net_sync_rpc(JavascriptNetSyncRpcServiceRequest {
13484 bridge,
13485 vm_id,
13486 dns,
13487 socket_paths,
13488 kernel,
13489 process,
13490 sync_request: request,
13491 resource_limits,
13492 network_counts,
13493 })
13494 }
13495 "net.http2_server_listen"
13496 | "net.http2_server_poll"
13497 | "net.http2_server_close"
13498 | "net.http2_server_respond"
13499 | "net.http2_server_wait"
13500 | "net.http2_session_connect"
13501 | "net.http2_session_request"
13502 | "net.http2_session_settings"
13503 | "net.http2_session_set_local_window_size"
13504 | "net.http2_session_goaway"
13505 | "net.http2_session_close"
13506 | "net.http2_session_destroy"
13507 | "net.http2_session_poll"
13508 | "net.http2_session_wait"
13509 | "net.http2_stream_respond"
13510 | "net.http2_stream_push_stream"
13511 | "net.http2_stream_write"
13512 | "net.http2_stream_end"
13513 | "net.http2_stream_close"
13514 | "net.http2_stream_pause"
13515 | "net.http2_stream_resume"
13516 | "net.http2_stream_respond_with_file" => {
13517 service_javascript_http2_sync_rpc(JavascriptHttp2SyncRpcServiceRequest {
13518 bridge,
13519 kernel,
13520 vm_id,
13521 dns,
13522 socket_paths,
13523 process,
13524 sync_request: request,
13525 resource_limits,
13526 network_counts,
13527 })
13528 }
13529 "net.connect"
13530 | "net.reserve_tcp_port"
13531 | "net.release_tcp_port"
13532 | "net.listen"
13533 | "net.poll"
13534 | "net.socket_wait_connect"
13535 | "net.socket_read"
13536 | "net.socket_set_no_delay"
13537 | "net.socket_set_keep_alive"
13538 | "net.socket_upgrade_tls"
13539 | "net.socket_get_tls_client_hello"
13540 | "net.socket_tls_query"
13541 | "net.server_poll"
13542 | "net.server_accept"
13543 | "net.server_connections"
13544 | "net.upgrade_socket_write"
13545 | "net.upgrade_socket_end"
13546 | "net.upgrade_socket_destroy"
13547 | "net.write"
13548 | "net.shutdown"
13549 | "net.destroy"
13550 | "net.server_close"
13551 | "tls.get_ciphers" => {
13552 service_javascript_net_sync_rpc(JavascriptNetSyncRpcServiceRequest {
13553 bridge,
13554 vm_id,
13555 dns,
13556 socket_paths,
13557 kernel,
13558 process,
13559 sync_request: request,
13560 resource_limits,
13561 network_counts,
13562 })
13563 }
13564 "dgram.createSocket"
13565 | "dgram.bind"
13566 | "dgram.send"
13567 | "dgram.poll"
13568 | "dgram.close"
13569 | "dgram.address"
13570 | "dgram.setBufferSize"
13571 | "dgram.getBufferSize" => {
13572 service_javascript_dgram_sync_rpc(JavascriptDgramSyncRpcServiceRequest {
13573 bridge,
13574 kernel,
13575 vm_id,
13576 dns,
13577 socket_paths,
13578 process,
13579 sync_request: request,
13580 resource_limits,
13581 network_counts,
13582 })
13583 }
13584 "sqlite.constants"
13585 | "sqlite.open"
13586 | "sqlite.close"
13587 | "sqlite.exec"
13588 | "sqlite.query"
13589 | "sqlite.prepare"
13590 | "sqlite.location"
13591 | "sqlite.checkpoint"
13592 | "sqlite.statement.run"
13593 | "sqlite.statement.get"
13594 | "sqlite.statement.all"
13595 | "sqlite.statement.iterate"
13596 | "sqlite.statement.columns"
13597 | "sqlite.statement.setReturnArrays"
13598 | "sqlite.statement.setReadBigInts"
13599 | "sqlite.statement.setAllowBareNamedParameters"
13600 | "sqlite.statement.setAllowUnknownNamedParameters"
13601 | "sqlite.statement.finalize" => {
13602 service_javascript_sqlite_sync_rpc(kernel, process, request)
13603 }
13604 "process.kill" => {
13605 let target_pid =
13606 javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
13607 let signal = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
13608 let parsed_signal = parse_signal(signal)?;
13609 if parsed_signal == 0 {
13610 kernel
13611 .signal_process(EXECUTION_DRIVER_NAME, target_pid, parsed_signal)
13612 .map_err(kernel_error)?;
13613 return Ok(Value::Null);
13614 }
13615 let process_pid = i32::try_from(process.kernel_pid)
13616 .map_err(|_| SidecarError::InvalidState("process pid exceeds i32".into()))?;
13617 if target_pid != process_pid {
13618 return Err(SidecarError::InvalidState(format!(
13619 "unknown process pid {target_pid}"
13620 )));
13621 }
13622 process.pending_self_signal_exit = None;
13623 if parsed_signal != 0
13624 && !matches!(
13625 canonical_signal_name(parsed_signal),
13626 Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
13627 )
13628 {
13629 process.pending_self_signal_exit = Some(parsed_signal);
13630 }
13631 Ok(json!({
13632 "self": true,
13633 "action": "default",
13634 }))
13635 }
13636 "process.umask" => {
13637 let new_mask = javascript_sync_rpc_arg_u32_optional(&request.args, 0, "process umask")?;
13638 kernel
13639 .umask(EXECUTION_DRIVER_NAME, process.kernel_pid, new_mask)
13640 .map(|mask| json!(mask))
13641 .map_err(kernel_error)
13642 }
13643 "fs.chmodSync" | "fs.promises.chmod" => {
13644 let response =
13645 service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request)?;
13646 mirror_process_chmod_to_host(process, request)?;
13647 Ok(response)
13648 }
13649 _ => service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request),
13650 }
13651}
13652
13653fn service_javascript_internal_bridge_sync_rpc(
13654 process: &ActiveProcess,
13655 request: &JavascriptSyncRpcRequest,
13656) -> Result<Value, SidecarError> {
13657 let method = match request.method.as_str() {
13661 "_loadPolyfill" | "__load_polyfill" => "_loadPolyfill",
13662 other => {
13663 return Err(SidecarError::InvalidState(format!(
13664 "unsupported JavaScript internal bridge method {other}"
13665 )));
13666 }
13667 };
13668
13669 handle_internal_bridge_call_from_host_context(
13670 &process.host_cwd,
13671 &process.guest_cwd,
13672 &process.env,
13673 method,
13674 &request.args,
13675 )
13676 .ok_or_else(|| {
13677 SidecarError::InvalidState(format!(
13678 "JavaScript internal bridge method {method} returned no value"
13679 ))
13680 })
13681}
13682
13683fn mirror_process_chmod_to_host(
13684 process: &ActiveProcess,
13685 request: &JavascriptSyncRpcRequest,
13686) -> Result<(), SidecarError> {
13687 let guest_path = javascript_sync_rpc_arg_str(&request.args, 0, "filesystem chmod path")?;
13688 let mode = javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem chmod mode")? & 0o7777;
13689 let Some(host_path) = resolve_process_guest_path_to_host(process, guest_path) else {
13690 return Ok(());
13691 };
13692 if !host_path.exists() {
13693 return Ok(());
13694 }
13695 fs::set_permissions(&host_path, fs::Permissions::from_mode(mode)).map_err(|error| {
13696 SidecarError::Io(format!(
13697 "failed to mirror chmod to host path {}: {error}",
13698 host_path.display()
13699 ))
13700 })
13701}
13702
13703fn resolve_process_guest_path_to_host(
13704 process: &ActiveProcess,
13705 guest_path: &str,
13706) -> Option<PathBuf> {
13707 let normalized_guest_path = if guest_path.starts_with('/') {
13708 normalize_path(guest_path)
13709 } else {
13710 normalize_path(&format!(
13711 "{}/{}",
13712 process.guest_cwd.trim_end_matches('/'),
13713 guest_path
13714 ))
13715 };
13716 if let Some(host_path) =
13717 host_path_from_runtime_guest_mappings(&process.env, &normalized_guest_path)
13718 {
13719 return Some(host_path);
13720 }
13721 let normalized_guest_cwd = normalize_path(&process.guest_cwd);
13722 let mut host_root = normalize_host_path(&process.host_cwd);
13723 for _ in normalized_guest_cwd
13724 .trim_start_matches('/')
13725 .split('/')
13726 .filter(|segment| !segment.is_empty())
13727 {
13728 host_root = host_root.parent()?.to_path_buf();
13729 }
13730 if normalized_guest_path == "/" {
13731 Some(host_root)
13732 } else {
13733 Some(host_root.join(normalized_guest_path.trim_start_matches('/')))
13734 }
13735}
13736
13737pub(crate) fn service_javascript_crypto_sync_rpc(
13738 process: &mut ActiveProcess,
13739 request: &JavascriptSyncRpcRequest,
13740) -> Result<Value, SidecarError> {
13741 match request.method.as_str() {
13742 "crypto.hashDigest" => {
13743 let algorithm = javascript_crypto_digest_algorithm(
13744 &request.args,
13745 0,
13746 "crypto.hashDigest algorithm",
13747 )?;
13748 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hashDigest data")?;
13749 Ok(Value::String(
13750 base64::engine::general_purpose::STANDARD.encode(algorithm.digest(&data)),
13751 ))
13752 }
13753 "crypto.hmacDigest" => {
13754 let algorithm = javascript_crypto_digest_algorithm(
13755 &request.args,
13756 0,
13757 "crypto.hmacDigest algorithm",
13758 )?;
13759 let key = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hmacDigest key")?;
13760 let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.hmacDigest data")?;
13761 Ok(Value::String(
13762 base64::engine::general_purpose::STANDARD.encode(algorithm.hmac(&key, &data)?),
13763 ))
13764 }
13765 "crypto.pbkdf2" => {
13766 let password =
13767 javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.pbkdf2 password")?;
13768 let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.pbkdf2 salt")?;
13769 let iterations =
13770 javascript_sync_rpc_arg_u32(&request.args, 2, "crypto.pbkdf2 iterations")?;
13771 if iterations == 0 {
13772 return Err(SidecarError::InvalidState(String::from(
13773 "crypto.pbkdf2 iterations must be greater than zero",
13774 )));
13775 }
13776 let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
13777 &request.args,
13778 3,
13779 "crypto.pbkdf2 key length",
13780 )?)
13781 .map_err(|_| {
13782 SidecarError::InvalidState(String::from(
13783 "crypto.pbkdf2 key length must fit within usize",
13784 ))
13785 })?;
13786 let algorithm =
13787 javascript_crypto_digest_algorithm(&request.args, 4, "crypto.pbkdf2 digest")?;
13788 let mut output = vec![0u8; key_len];
13789 algorithm.pbkdf2(&password, &salt, iterations, &mut output);
13790 Ok(Value::String(
13791 base64::engine::general_purpose::STANDARD.encode(output),
13792 ))
13793 }
13794 "crypto.scrypt" => {
13795 let password =
13796 javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.scrypt password")?;
13797 let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.scrypt salt")?;
13798 let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
13799 &request.args,
13800 2,
13801 "crypto.scrypt key length",
13802 )?)
13803 .map_err(|_| {
13804 SidecarError::InvalidState(String::from(
13805 "crypto.scrypt key length must fit within usize",
13806 ))
13807 })?;
13808 let options_json =
13809 javascript_sync_rpc_arg_str(&request.args, 3, "crypto.scrypt options")?;
13810 let options: JavascriptScryptOptions =
13811 serde_json::from_str(options_json).map_err(|error| {
13812 SidecarError::InvalidState(format!(
13813 "crypto.scrypt options must be valid JSON: {error}"
13814 ))
13815 })?;
13816 let cost = options.cost.unwrap_or(DEFAULT_SCRYPT_COST);
13817 if cost == 0 || !cost.is_power_of_two() {
13818 return Err(SidecarError::InvalidState(String::from(
13819 "crypto.scrypt cost must be a positive power of two",
13820 )));
13821 }
13822 let log_n = u8::try_from(cost.ilog2()).map_err(|_| {
13823 SidecarError::InvalidState(String::from(
13824 "crypto.scrypt cost exceeds supported parameter range",
13825 ))
13826 })?;
13827 let params = ScryptParams::new(
13828 log_n,
13829 options.block_size.unwrap_or(DEFAULT_SCRYPT_BLOCK_SIZE),
13830 options
13831 .parallelization
13832 .unwrap_or(DEFAULT_SCRYPT_PARALLELIZATION),
13833 key_len,
13834 )
13835 .map_err(|error| {
13836 SidecarError::InvalidState(format!("crypto.scrypt options are invalid: {error}"))
13837 })?;
13838 let mut output = vec![0u8; key_len];
13839 scrypt(&password, &salt, ¶ms, &mut output).map_err(|error| {
13840 SidecarError::Execution(format!("crypto.scrypt failed: {error}"))
13841 })?;
13842 Ok(Value::String(
13843 base64::engine::general_purpose::STANDARD.encode(output),
13844 ))
13845 }
13846 "crypto.cipheriv" => service_javascript_crypto_cipheriv_sync_rpc(request),
13847 "crypto.decipheriv" => service_javascript_crypto_decipheriv_sync_rpc(request),
13848 "crypto.cipherivCreate" => {
13849 service_javascript_crypto_cipheriv_create_sync_rpc(process, request)
13850 }
13851 "crypto.cipherivUpdate" => {
13852 service_javascript_crypto_cipheriv_update_sync_rpc(process, request)
13853 }
13854 "crypto.cipherivFinal" => {
13855 service_javascript_crypto_cipheriv_final_sync_rpc(process, request)
13856 }
13857 "crypto.sign" => service_javascript_crypto_sign_sync_rpc(request),
13858 "crypto.verify" => service_javascript_crypto_verify_sync_rpc(request),
13859 "crypto.asymmetricOp" => service_javascript_crypto_asymmetric_op_sync_rpc(request),
13860 "crypto.createKeyObject" => service_javascript_crypto_create_key_object_sync_rpc(request),
13861 "crypto.generateKeyPairSync" => {
13862 service_javascript_crypto_generate_key_pair_sync_rpc(request)
13863 }
13864 "crypto.generateKeySync" => service_javascript_crypto_generate_key_sync_rpc(request),
13865 "crypto.generatePrimeSync" => service_javascript_crypto_generate_prime_sync_rpc(request),
13866 "crypto.diffieHellman" => service_javascript_crypto_diffie_hellman_sync_rpc(request),
13867 "crypto.diffieHellmanGroup" => {
13868 service_javascript_crypto_diffie_hellman_group_sync_rpc(request)
13869 }
13870 "crypto.diffieHellmanSessionCreate" => {
13871 service_javascript_crypto_diffie_hellman_session_create_sync_rpc(process, request)
13872 }
13873 "crypto.diffieHellmanSessionCall" => {
13874 service_javascript_crypto_diffie_hellman_session_call_sync_rpc(process, request)
13875 }
13876 "crypto.diffieHellmanSessionDestroy" => {
13877 service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(process, request)
13878 }
13879 "crypto.subtle" => service_javascript_crypto_subtle_sync_rpc(request),
13880 _ => Err(SidecarError::InvalidState(format!(
13881 "unsupported JavaScript crypto sync RPC method {}",
13882 request.method
13883 ))),
13884 }
13885}
13886
13887fn javascript_crypto_digest_algorithm(
13888 args: &[Value],
13889 index: usize,
13890 label: &str,
13891) -> Result<JavascriptCryptoDigestAlgorithm, SidecarError> {
13892 JavascriptCryptoDigestAlgorithm::parse(javascript_sync_rpc_arg_str(args, index, label)?)
13893}
13894
13895impl JavascriptCryptoDigestAlgorithm {
13896 fn parse(value: &str) -> Result<Self, SidecarError> {
13897 match value.trim().to_ascii_lowercase().replace('-', "").as_str() {
13898 "md5" => Ok(Self::Md5),
13899 "sha1" => Ok(Self::Sha1),
13900 "sha256" => Ok(Self::Sha256),
13901 "sha512" => Ok(Self::Sha512),
13902 _ => Err(SidecarError::InvalidState(format!(
13903 "unsupported crypto digest algorithm {value}"
13904 ))),
13905 }
13906 }
13907
13908 fn digest(self, data: &[u8]) -> Vec<u8> {
13909 match self {
13910 Self::Md5 => Md5::digest(data).to_vec(),
13911 Self::Sha1 => Sha1::digest(data).to_vec(),
13912 Self::Sha256 => Sha256::digest(data).to_vec(),
13913 Self::Sha512 => Sha512::digest(data).to_vec(),
13914 }
13915 }
13916
13917 fn hmac(self, key: &[u8], data: &[u8]) -> Result<Vec<u8>, SidecarError> {
13918 match self {
13919 Self::Md5 => {
13920 let mut mac = Hmac::<Md5>::new_from_slice(key).map_err(|error| {
13921 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
13922 })?;
13923 mac.update(data);
13924 Ok(mac.finalize().into_bytes().to_vec())
13925 }
13926 Self::Sha1 => {
13927 let mut mac = Hmac::<Sha1>::new_from_slice(key).map_err(|error| {
13928 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
13929 })?;
13930 mac.update(data);
13931 Ok(mac.finalize().into_bytes().to_vec())
13932 }
13933 Self::Sha256 => {
13934 let mut mac = Hmac::<Sha256>::new_from_slice(key).map_err(|error| {
13935 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
13936 })?;
13937 mac.update(data);
13938 Ok(mac.finalize().into_bytes().to_vec())
13939 }
13940 Self::Sha512 => {
13941 let mut mac = Hmac::<Sha512>::new_from_slice(key).map_err(|error| {
13942 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
13943 })?;
13944 mac.update(data);
13945 Ok(mac.finalize().into_bytes().to_vec())
13946 }
13947 }
13948 }
13949
13950 fn pbkdf2(self, password: &[u8], salt: &[u8], iterations: u32, output: &mut [u8]) {
13951 match self {
13952 Self::Md5 => pbkdf2_hmac::<Md5>(password, salt, iterations, output),
13953 Self::Sha1 => pbkdf2_hmac::<Sha1>(password, salt, iterations, output),
13954 Self::Sha256 => pbkdf2_hmac::<Sha256>(password, salt, iterations, output),
13955 Self::Sha512 => pbkdf2_hmac::<Sha512>(password, salt, iterations, output),
13956 }
13957 }
13958}
13959
13960#[derive(Debug, Clone)]
13961enum JavascriptCryptoKeyMaterial {
13962 Private(PKey<Private>),
13963 Public(PKey<Public>),
13964 Secret(Vec<u8>),
13965}
13966
13967#[derive(Debug, Clone, Deserialize, Serialize)]
13968struct JavascriptSerializedSandboxKeyObject {
13969 #[serde(rename = "type")]
13970 kind: String,
13971 #[serde(skip_serializing_if = "Option::is_none")]
13972 pem: Option<String>,
13973 #[serde(skip_serializing_if = "Option::is_none")]
13974 raw: Option<String>,
13975 #[serde(skip_serializing_if = "Option::is_none", rename = "asymmetricKeyType")]
13976 asymmetric_key_type: Option<String>,
13977 #[serde(
13978 skip_serializing_if = "Option::is_none",
13979 rename = "asymmetricKeyDetails"
13980 )]
13981 asymmetric_key_details: Option<Map<String, Value>>,
13982 #[serde(skip_serializing_if = "Option::is_none")]
13983 jwk: Option<Value>,
13984}
13985
13986#[derive(Debug, Clone)]
13987struct JavascriptDirectKeyInput {
13988 key: JavascriptCryptoKeyMaterial,
13989 padding: Option<Padding>,
13990}
13991
13992fn service_javascript_crypto_cipheriv_sync_rpc(
13993 request: &JavascriptSyncRpcRequest,
13994) -> Result<Value, SidecarError> {
13995 service_javascript_crypto_cipheriv_inner(request, false)
13996}
13997
13998fn service_javascript_crypto_decipheriv_sync_rpc(
13999 request: &JavascriptSyncRpcRequest,
14000) -> Result<Value, SidecarError> {
14001 service_javascript_crypto_cipheriv_inner(request, true)
14002}
14003
14004fn service_javascript_crypto_cipheriv_create_sync_rpc(
14005 process: &mut ActiveProcess,
14006 request: &JavascriptSyncRpcRequest,
14007) -> Result<Value, SidecarError> {
14008 ensure_per_process_state_handle_capacity(process.cipher_sessions.len(), "cipher session")?;
14009 let mode = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.cipherivCreate mode")?;
14010 let decrypt = mode == "decipher";
14011 let algorithm =
14012 javascript_sync_rpc_arg_str(&request.args, 1, "crypto.cipherivCreate algorithm")?;
14013 let key = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.cipherivCreate key")?;
14014 let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 3, "crypto.cipherivCreate iv")?;
14015 let options =
14016 javascript_sync_rpc_json_arg_optional(&request.args, 4, "crypto.cipherivCreate options")?;
14017 let auth_tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options.as_ref())?;
14018 let context = javascript_crypto_build_cipher_context(
14019 algorithm,
14020 &key,
14021 iv.as_deref(),
14022 decrypt,
14023 options.as_ref(),
14024 )?;
14025 process.next_cipher_session_id += 1;
14026 let session_id = process.next_cipher_session_id;
14027 process.cipher_sessions.insert(
14028 session_id,
14029 ActiveCipherSession {
14030 algorithm: algorithm.to_string(),
14031 auth_tag_len,
14032 context,
14033 },
14034 );
14035 Ok(json!(session_id))
14036}
14037
14038fn service_javascript_crypto_cipheriv_update_sync_rpc(
14039 process: &mut ActiveProcess,
14040 request: &JavascriptSyncRpcRequest,
14041) -> Result<Value, SidecarError> {
14042 let session_id =
14043 javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivUpdate session id")?;
14044 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.cipherivUpdate data")?;
14045 let session = process
14046 .cipher_sessions
14047 .get_mut(&session_id)
14048 .ok_or_else(|| {
14049 SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
14050 })?;
14051 let result = javascript_crypto_cipher_update(&mut session.context, &data)?;
14052 Ok(Value::String(
14053 base64::engine::general_purpose::STANDARD.encode(result),
14054 ))
14055}
14056
14057fn service_javascript_crypto_cipheriv_final_sync_rpc(
14058 process: &mut ActiveProcess,
14059 request: &JavascriptSyncRpcRequest,
14060) -> Result<Value, SidecarError> {
14061 let session_id =
14062 javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivFinal session id")?;
14063 let mut session = process.cipher_sessions.remove(&session_id).ok_or_else(|| {
14064 SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
14065 })?;
14066 let data = javascript_crypto_cipher_finalize(&mut session.context)?;
14067 let mut response = Map::new();
14068 response.insert(
14069 String::from("data"),
14070 Value::String(base64::engine::general_purpose::STANDARD.encode(data)),
14071 );
14072 if javascript_crypto_is_aead(&session.algorithm) {
14073 let mut auth_tag = vec![0_u8; session.auth_tag_len];
14074 session
14075 .context
14076 .get_tag(&mut auth_tag)
14077 .map_err(javascript_crypto_openssl_error)?;
14078 response.insert(
14079 String::from("authTag"),
14080 Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
14081 );
14082 }
14083 Ok(Value::String(serde_json::to_string(&response).map_err(
14084 |error| SidecarError::InvalidState(format!("serialize cipher final response: {error}")),
14085 )?))
14086}
14087
14088fn service_javascript_crypto_sign_sync_rpc(
14089 request: &JavascriptSyncRpcRequest,
14090) -> Result<Value, SidecarError> {
14091 let algorithm = request.args.first().and_then(Value::as_str);
14092 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.sign data")?;
14093 let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.sign key")?;
14094 let key_input =
14095 javascript_crypto_parse_direct_key_input(key_json, Some("private"), "crypto.sign key")?;
14096 let private_key = javascript_crypto_expect_private_key(key_input.key, "crypto.sign key")?;
14097 let mut signer = javascript_crypto_new_signer(algorithm, &private_key)?;
14098 if let Some(padding) = key_input.padding {
14099 signer
14100 .set_rsa_padding(padding)
14101 .map_err(javascript_crypto_openssl_error)?;
14102 }
14103 signer
14104 .update(&data)
14105 .map_err(javascript_crypto_openssl_error)?;
14106 Ok(Value::String(
14107 base64::engine::general_purpose::STANDARD.encode(
14108 signer
14109 .sign_to_vec()
14110 .map_err(javascript_crypto_openssl_error)?,
14111 ),
14112 ))
14113}
14114
14115fn service_javascript_crypto_verify_sync_rpc(
14116 request: &JavascriptSyncRpcRequest,
14117) -> Result<Value, SidecarError> {
14118 let algorithm = request.args.first().and_then(Value::as_str);
14119 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.verify data")?;
14120 let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.verify key")?;
14121 let signature = javascript_sync_rpc_base64_arg(&request.args, 3, "crypto.verify signature")?;
14122 let key_input =
14123 javascript_crypto_parse_direct_key_input(key_json, Some("public"), "crypto.verify key")?;
14124 let public_key = javascript_crypto_expect_public_key(key_input.key, "crypto.verify key")?;
14125 let mut verifier = javascript_crypto_new_verifier(algorithm, &public_key)?;
14126 if let Some(padding) = key_input.padding {
14127 verifier
14128 .set_rsa_padding(padding)
14129 .map_err(javascript_crypto_openssl_error)?;
14130 }
14131 verifier
14132 .update(&data)
14133 .map_err(javascript_crypto_openssl_error)?;
14134 Ok(json!(verifier
14135 .verify(&signature)
14136 .map_err(javascript_crypto_openssl_error)?))
14137}
14138
14139fn service_javascript_crypto_asymmetric_op_sync_rpc(
14140 request: &JavascriptSyncRpcRequest,
14141) -> Result<Value, SidecarError> {
14142 let operation = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.asymmetricOp operation")?;
14143 let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.asymmetricOp key")?;
14144 let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.asymmetricOp data")?;
14145 let expect_kind = match operation {
14146 "publicEncrypt" | "publicDecrypt" => Some("public"),
14147 "privateEncrypt" | "privateDecrypt" => Some("private"),
14148 other => {
14149 return Err(SidecarError::InvalidState(format!(
14150 "Unsupported asymmetric crypto operation: {other}"
14151 )));
14152 }
14153 };
14154 let key_input =
14155 javascript_crypto_parse_direct_key_input(key_json, expect_kind, "crypto.asymmetricOp key")?;
14156 let padding = key_input.padding.unwrap_or(Padding::PKCS1);
14157 let mut output = vec![0_u8; javascript_crypto_rsa_output_size(&key_input.key)?];
14158 let written = match (operation, key_input.key) {
14159 ("publicEncrypt", JavascriptCryptoKeyMaterial::Public(key))
14160 | ("publicDecrypt", JavascriptCryptoKeyMaterial::Public(key)) => {
14161 let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
14162 if operation == "publicEncrypt" {
14163 rsa.public_encrypt(&data, &mut output, padding)
14164 .map_err(javascript_crypto_openssl_error)?
14165 } else {
14166 rsa.public_decrypt(&data, &mut output, padding)
14167 .map_err(javascript_crypto_openssl_error)?
14168 }
14169 }
14170 ("privateEncrypt", JavascriptCryptoKeyMaterial::Private(key))
14171 | ("privateDecrypt", JavascriptCryptoKeyMaterial::Private(key)) => {
14172 let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
14173 if operation == "privateEncrypt" {
14174 rsa.private_encrypt(&data, &mut output, padding)
14175 .map_err(javascript_crypto_openssl_error)?
14176 } else {
14177 rsa.private_decrypt(&data, &mut output, padding)
14178 .map_err(javascript_crypto_openssl_error)?
14179 }
14180 }
14181 _ => {
14182 return Err(SidecarError::InvalidState(format!(
14183 "{operation} requires an RSA {} key",
14184 expect_kind.unwrap_or("asymmetric")
14185 )));
14186 }
14187 };
14188 output.truncate(written);
14189 Ok(Value::String(
14190 base64::engine::general_purpose::STANDARD.encode(output),
14191 ))
14192}
14193
14194fn service_javascript_crypto_create_key_object_sync_rpc(
14195 request: &JavascriptSyncRpcRequest,
14196) -> Result<Value, SidecarError> {
14197 let operation =
14198 javascript_sync_rpc_arg_str(&request.args, 0, "crypto.createKeyObject operation")?;
14199 let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.createKeyObject key")?;
14200 let expected = match operation {
14201 "createPrivateKey" => Some("private"),
14202 "createPublicKey" => Some("public"),
14203 other => {
14204 return Err(SidecarError::InvalidState(format!(
14205 "Unsupported key creation operation: {other}"
14206 )));
14207 }
14208 };
14209 let key_input =
14210 javascript_crypto_parse_direct_key_input(key_json, expected, "crypto.createKeyObject key")?;
14211 Ok(Value::String(
14212 serde_json::to_string(&javascript_crypto_serialize_sandbox_key_object(
14213 &key_input.key,
14214 )?)
14215 .map_err(|error| {
14216 SidecarError::InvalidState(format!("serialize crypto key object: {error}"))
14217 })?,
14218 ))
14219}
14220
14221fn service_javascript_crypto_generate_key_pair_sync_rpc(
14222 request: &JavascriptSyncRpcRequest,
14223) -> Result<Value, SidecarError> {
14224 let key_type =
14225 javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeyPairSync type")?;
14226 let options = javascript_crypto_parse_serialized_options_arg(
14227 &request.args,
14228 1,
14229 "crypto.generateKeyPairSync options",
14230 )?
14231 .unwrap_or(Value::Object(Map::new()));
14232 let public_encoding = options.get("publicKeyEncoding").cloned();
14233 let private_encoding = options.get("privateKeyEncoding").cloned();
14234
14235 let private_key = match key_type {
14236 "rsa" => {
14237 let bits = options
14238 .get("modulusLength")
14239 .and_then(Value::as_u64)
14240 .unwrap_or(2048) as u32;
14241 let exponent = options
14242 .get("publicExponent")
14243 .map(|value| javascript_crypto_u32_from_bridge_value(value, "rsa publicExponent"))
14244 .transpose()?
14245 .unwrap_or(65_537);
14246 let exponent = BigNum::from_u32(exponent).map_err(javascript_crypto_openssl_error)?;
14247 let rsa =
14248 Rsa::generate_with_e(bits, &exponent).map_err(javascript_crypto_openssl_error)?;
14249 PKey::from_rsa(rsa).map_err(javascript_crypto_openssl_error)?
14250 }
14251 "ec" => {
14252 let named_curve = options
14253 .get("namedCurve")
14254 .and_then(Value::as_str)
14255 .ok_or_else(|| {
14256 SidecarError::InvalidState(String::from(
14257 "crypto.generateKeyPairSync ec requires namedCurve",
14258 ))
14259 })?;
14260 let group = EcGroup::from_curve_name(javascript_crypto_curve_nid(named_curve)?)
14261 .map_err(javascript_crypto_openssl_error)?;
14262 let key = EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?;
14263 PKey::from_ec_key(key).map_err(javascript_crypto_openssl_error)?
14264 }
14265 "ed25519" => PKey::generate_ed25519().map_err(javascript_crypto_openssl_error)?,
14266 "x25519" => PKey::generate_x25519().map_err(javascript_crypto_openssl_error)?,
14267 other => {
14268 return Err(SidecarError::InvalidState(format!(
14269 "unsupported crypto key pair type {other}"
14270 )));
14271 }
14272 };
14273 let public_key = PKey::public_key_from_pem(
14274 &private_key
14275 .public_key_to_pem()
14276 .map_err(javascript_crypto_openssl_error)?,
14277 )
14278 .map_err(javascript_crypto_openssl_error)?;
14279 let response = if public_encoding.is_some() || private_encoding.is_some() {
14280 json!({
14281 "publicKey": javascript_crypto_serialize_encoded_key_value_public(&public_key, public_encoding.as_ref())?,
14282 "privateKey": javascript_crypto_serialize_encoded_key_value_private(&private_key, private_encoding.as_ref())?,
14283 })
14284 } else {
14285 json!({
14286 "publicKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(public_key))?,
14287 "privateKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(private_key))?,
14288 })
14289 };
14290 Ok(Value::String(serde_json::to_string(&response).map_err(
14291 |error| SidecarError::InvalidState(format!("serialize generated key pair: {error}")),
14292 )?))
14293}
14294
14295fn service_javascript_crypto_generate_key_sync_rpc(
14296 request: &JavascriptSyncRpcRequest,
14297) -> Result<Value, SidecarError> {
14298 let key_type = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeySync type")?;
14299 let options = javascript_crypto_parse_serialized_options_arg(
14300 &request.args,
14301 1,
14302 "crypto.generateKeySync options",
14303 )?
14304 .unwrap_or(Value::Object(Map::new()));
14305 let bit_length = options
14306 .get("length")
14307 .and_then(Value::as_u64)
14308 .ok_or_else(|| {
14309 SidecarError::InvalidState(String::from(
14310 "crypto.generateKeySync options.length is required",
14311 ))
14312 })? as usize;
14313 let mut raw = vec![0_u8; bit_length.div_ceil(8)];
14314 rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
14315 let serialized = match key_type {
14316 "hmac" => javascript_crypto_serialize_sandbox_key_object(
14317 &JavascriptCryptoKeyMaterial::Secret(raw),
14318 )?,
14319 "aes" => javascript_crypto_serialize_sandbox_key_object(
14320 &JavascriptCryptoKeyMaterial::Secret(raw),
14321 )?,
14322 other => {
14323 return Err(SidecarError::InvalidState(format!(
14324 "unsupported crypto.generateKeySync type {other}"
14325 )));
14326 }
14327 };
14328 Ok(Value::String(serde_json::to_string(&serialized).map_err(
14329 |error| SidecarError::InvalidState(format!("serialize generated key: {error}")),
14330 )?))
14331}
14332
14333fn service_javascript_crypto_generate_prime_sync_rpc(
14334 request: &JavascriptSyncRpcRequest,
14335) -> Result<Value, SidecarError> {
14336 let bits =
14337 javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.generatePrimeSync size")? as i32;
14338 let options = javascript_crypto_parse_serialized_options_arg(
14339 &request.args,
14340 1,
14341 "crypto.generatePrimeSync options",
14342 )?
14343 .unwrap_or(Value::Object(Map::new()));
14344 let safe = options
14345 .get("safe")
14346 .and_then(Value::as_bool)
14347 .unwrap_or(false);
14348 let add = options
14349 .get("add")
14350 .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime add"))
14351 .transpose()?;
14352 let rem = options
14353 .get("rem")
14354 .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime rem"))
14355 .transpose()?;
14356 let mut prime = BigNum::new().map_err(javascript_crypto_openssl_error)?;
14357 prime
14358 .generate_prime(bits, safe, add.as_deref(), rem.as_deref())
14359 .map_err(javascript_crypto_openssl_error)?;
14360 let payload = if options
14361 .get("bigint")
14362 .and_then(Value::as_bool)
14363 .unwrap_or(false)
14364 {
14365 json!({
14366 "__type": "bigint",
14367 "value": prime.to_dec_str().map_err(javascript_crypto_openssl_error)?.to_string(),
14368 })
14369 } else {
14370 json!({
14371 "__type": "buffer",
14372 "value": base64::engine::general_purpose::STANDARD.encode(prime.to_vec()),
14373 })
14374 };
14375 Ok(Value::String(serde_json::to_string(&payload).map_err(
14376 |error| SidecarError::InvalidState(format!("serialize generated prime: {error}")),
14377 )?))
14378}
14379
14380fn service_javascript_crypto_diffie_hellman_sync_rpc(
14381 request: &JavascriptSyncRpcRequest,
14382) -> Result<Value, SidecarError> {
14383 let options = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellman options")?;
14384 let parsed: Value = serde_json::from_str(options).map_err(|error| {
14385 SidecarError::InvalidState(format!(
14386 "crypto.diffieHellman options must be valid JSON: {error}"
14387 ))
14388 })?;
14389 let private_key = javascript_crypto_parse_key_material_value(
14390 parsed.get("privateKey").ok_or_else(|| {
14391 SidecarError::InvalidState(String::from("crypto.diffieHellman missing privateKey"))
14392 })?,
14393 Some("private"),
14394 "crypto.diffieHellman privateKey",
14395 )?;
14396 let public_key = javascript_crypto_parse_key_material_value(
14397 parsed.get("publicKey").ok_or_else(|| {
14398 SidecarError::InvalidState(String::from("crypto.diffieHellman missing publicKey"))
14399 })?,
14400 Some("public"),
14401 "crypto.diffieHellman publicKey",
14402 )?;
14403 let private_key =
14404 javascript_crypto_expect_private_key(private_key, "crypto.diffieHellman privateKey")?;
14405 let public_key =
14406 javascript_crypto_expect_public_key(public_key, "crypto.diffieHellman publicKey")?;
14407 let mut deriver = Deriver::new(&private_key).map_err(javascript_crypto_openssl_error)?;
14408 deriver
14409 .set_peer(&public_key)
14410 .map_err(javascript_crypto_openssl_error)?;
14411 let secret = deriver
14412 .derive_to_vec()
14413 .map_err(javascript_crypto_openssl_error)?;
14414 Ok(Value::String(
14415 serde_json::to_string(&json!({
14416 "__type": "buffer",
14417 "value": base64::engine::general_purpose::STANDARD.encode(secret),
14418 }))
14419 .map_err(|error| {
14420 SidecarError::InvalidState(format!("serialize derived secret: {error}"))
14421 })?,
14422 ))
14423}
14424
14425fn service_javascript_crypto_diffie_hellman_group_sync_rpc(
14426 request: &JavascriptSyncRpcRequest,
14427) -> Result<Value, SidecarError> {
14428 let name = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellmanGroup name")?;
14429 let params = javascript_crypto_named_dh_group(name)?;
14430 let response = json!({
14431 "prime": {
14432 "__type": "buffer",
14433 "value": base64::engine::general_purpose::STANDARD.encode(params.prime_p().to_vec()),
14434 },
14435 "generator": {
14436 "__type": "buffer",
14437 "value": base64::engine::general_purpose::STANDARD.encode(params.generator().to_vec()),
14438 },
14439 });
14440 Ok(Value::String(serde_json::to_string(&response).map_err(
14441 |error| {
14442 SidecarError::InvalidState(format!("serialize diffieHellmanGroup response: {error}"))
14443 },
14444 )?))
14445}
14446
14447fn service_javascript_crypto_diffie_hellman_session_create_sync_rpc(
14448 process: &mut ActiveProcess,
14449 request: &JavascriptSyncRpcRequest,
14450) -> Result<Value, SidecarError> {
14451 ensure_per_process_state_handle_capacity(
14452 process.diffie_hellman_sessions.len(),
14453 "diffie-hellman session",
14454 )?;
14455 let raw = javascript_sync_rpc_arg_str(
14456 &request.args,
14457 0,
14458 "crypto.diffieHellmanSessionCreate request",
14459 )?;
14460 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14461 SidecarError::InvalidState(format!(
14462 "crypto.diffieHellmanSessionCreate request must be valid JSON: {error}"
14463 ))
14464 })?;
14465 let session = match parsed.get("type").and_then(Value::as_str) {
14466 Some("group") => {
14467 let name = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
14468 SidecarError::InvalidState(String::from(
14469 "crypto.diffieHellmanSessionCreate group requires name",
14470 ))
14471 })?;
14472 ActiveDiffieHellmanSession::Dh(ActiveDhSession {
14473 params: javascript_crypto_named_dh_group(name)?,
14474 key_pair: None,
14475 })
14476 }
14477 Some("dh") => {
14478 let args = parsed
14479 .get("args")
14480 .and_then(Value::as_array)
14481 .ok_or_else(|| {
14482 SidecarError::InvalidState(String::from(
14483 "crypto.diffieHellmanSessionCreate dh requires args",
14484 ))
14485 })?;
14486 let params = javascript_crypto_build_dh_params(args)?;
14487 ActiveDiffieHellmanSession::Dh(ActiveDhSession {
14488 params,
14489 key_pair: None,
14490 })
14491 }
14492 Some("ecdh") => {
14493 let curve = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
14494 SidecarError::InvalidState(String::from(
14495 "crypto.diffieHellmanSessionCreate ecdh requires name",
14496 ))
14497 })?;
14498 ActiveDiffieHellmanSession::Ecdh(ActiveEcdhSession {
14499 curve: curve.to_string(),
14500 key_pair: None,
14501 })
14502 }
14503 other => {
14504 return Err(SidecarError::InvalidState(format!(
14505 "Unsupported Diffie-Hellman session type: {}",
14506 other.unwrap_or("<missing>")
14507 )));
14508 }
14509 };
14510 process.next_diffie_hellman_session_id += 1;
14511 let session_id = process.next_diffie_hellman_session_id;
14512 process.diffie_hellman_sessions.insert(session_id, session);
14513 Ok(json!(session_id))
14514}
14515
14516fn service_javascript_crypto_diffie_hellman_session_call_sync_rpc(
14517 process: &mut ActiveProcess,
14518 request: &JavascriptSyncRpcRequest,
14519) -> Result<Value, SidecarError> {
14520 let session_id = javascript_sync_rpc_arg_u64(
14521 &request.args,
14522 0,
14523 "crypto.diffieHellmanSessionCall session id",
14524 )?;
14525 let raw =
14526 javascript_sync_rpc_arg_str(&request.args, 1, "crypto.diffieHellmanSessionCall request")?;
14527 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14528 SidecarError::InvalidState(format!(
14529 "crypto.diffieHellmanSessionCall request must be valid JSON: {error}"
14530 ))
14531 })?;
14532 let method = parsed
14533 .get("method")
14534 .and_then(Value::as_str)
14535 .ok_or_else(|| {
14536 SidecarError::InvalidState(String::from(
14537 "crypto.diffieHellmanSessionCall request missing method",
14538 ))
14539 })?;
14540 let args = parsed
14541 .get("args")
14542 .and_then(Value::as_array)
14543 .cloned()
14544 .unwrap_or_default();
14545 let session = process
14546 .diffie_hellman_sessions
14547 .get_mut(&session_id)
14548 .ok_or_else(|| {
14549 SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
14550 })?;
14551 let (result, has_result) = match session {
14552 ActiveDiffieHellmanSession::Dh(session) => {
14553 javascript_crypto_call_dh_session(session, method, &args)?
14554 }
14555 ActiveDiffieHellmanSession::Ecdh(session) => {
14556 javascript_crypto_call_ecdh_session(session, method, &args)?
14557 }
14558 };
14559 Ok(Value::String(
14560 serde_json::to_string(&json!({
14561 "result": result,
14562 "hasResult": has_result,
14563 }))
14564 .map_err(|error| {
14565 SidecarError::InvalidState(format!("serialize diffie session result: {error}"))
14566 })?,
14567 ))
14568}
14569
14570fn service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(
14571 process: &mut ActiveProcess,
14572 request: &JavascriptSyncRpcRequest,
14573) -> Result<Value, SidecarError> {
14574 let session_id = javascript_sync_rpc_arg_u64(
14575 &request.args,
14576 0,
14577 "crypto.diffieHellmanSessionDestroy session id",
14578 )?;
14579 process
14580 .diffie_hellman_sessions
14581 .remove(&session_id)
14582 .ok_or_else(|| {
14583 SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
14584 })?;
14585 Ok(Value::Null)
14586}
14587
14588fn service_javascript_crypto_subtle_sync_rpc(
14589 request: &JavascriptSyncRpcRequest,
14590) -> Result<Value, SidecarError> {
14591 let raw = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.subtle request")?;
14592 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14593 SidecarError::InvalidState(format!("crypto.subtle request must be valid JSON: {error}"))
14594 })?;
14595 let op = parsed.get("op").and_then(Value::as_str).ok_or_else(|| {
14596 SidecarError::InvalidState(String::from("crypto.subtle request missing op"))
14597 })?;
14598 match op {
14599 "digest" => {
14600 let algorithm = parsed
14601 .get("algorithm")
14602 .and_then(Value::as_str)
14603 .ok_or_else(|| {
14604 SidecarError::InvalidState(String::from(
14605 "crypto.subtle.digest missing algorithm",
14606 ))
14607 })?;
14608 let data = parsed.get("data").and_then(Value::as_str).ok_or_else(|| {
14609 SidecarError::InvalidState(String::from("crypto.subtle.digest missing data"))
14610 })?;
14611 let bytes = base64::engine::general_purpose::STANDARD
14612 .decode(data)
14613 .map_err(|error| {
14614 SidecarError::InvalidState(format!("crypto.subtle.digest data base64: {error}"))
14615 })?;
14616 let digest = JavascriptCryptoDigestAlgorithm::parse(algorithm)?.digest(&bytes);
14617 Ok(Value::String(
14618 serde_json::to_string(&json!({
14619 "data": base64::engine::general_purpose::STANDARD.encode(digest),
14620 }))
14621 .map_err(|error| {
14622 SidecarError::InvalidState(format!("serialize crypto.subtle digest: {error}"))
14623 })?,
14624 ))
14625 }
14626 "generateKey" => {
14627 let algorithm = parsed.get("algorithm").ok_or_else(|| {
14628 SidecarError::InvalidState(String::from(
14629 "crypto.subtle.generateKey missing algorithm",
14630 ))
14631 })?;
14632 let name =
14633 javascript_crypto_subtle_algorithm_name(algorithm, "crypto.subtle.generateKey")?;
14634 if !matches!(name, "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW") {
14635 return Err(SidecarError::InvalidState(format!(
14636 "Unsupported key algorithm: {name}"
14637 )));
14638 }
14639 let length_bits = algorithm
14640 .get("length")
14641 .and_then(Value::as_u64)
14642 .ok_or_else(|| {
14643 SidecarError::InvalidState(String::from(
14644 "crypto.subtle.generateKey AES algorithm requires length",
14645 ))
14646 })?;
14647 if length_bits % 8 != 0 {
14648 return Err(SidecarError::InvalidState(String::from(
14649 "crypto.subtle.generateKey length must be byte-aligned",
14650 )));
14651 }
14652 let length_bytes = usize::try_from(length_bits / 8).map_err(|_| {
14653 SidecarError::InvalidState(String::from(
14654 "crypto.subtle.generateKey length is too large",
14655 ))
14656 })?;
14657 let mut raw = vec![0_u8; length_bytes];
14658 rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
14659 let key = javascript_crypto_serialize_subtle_secret_key(
14660 &raw,
14661 javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
14662 parsed
14663 .get("extractable")
14664 .and_then(Value::as_bool)
14665 .unwrap_or(false),
14666 parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
14667 )?;
14668 Ok(Value::String(
14669 serde_json::to_string(&json!({ "key": key })).map_err(|error| {
14670 SidecarError::InvalidState(format!(
14671 "serialize crypto.subtle generated key: {error}"
14672 ))
14673 })?,
14674 ))
14675 }
14676 "importKey" => {
14677 let format = parsed
14678 .get("format")
14679 .and_then(Value::as_str)
14680 .ok_or_else(|| {
14681 SidecarError::InvalidState(String::from(
14682 "crypto.subtle.importKey missing format",
14683 ))
14684 })?;
14685 if format != "raw" {
14686 return Err(SidecarError::InvalidState(format!(
14687 "Unsupported import format: {format}"
14688 )));
14689 }
14690 let key_data = parsed
14691 .get("keyData")
14692 .and_then(Value::as_str)
14693 .ok_or_else(|| {
14694 SidecarError::InvalidState(String::from(
14695 "crypto.subtle.importKey missing keyData",
14696 ))
14697 })?;
14698 let raw = base64::engine::general_purpose::STANDARD
14699 .decode(key_data)
14700 .map_err(|error| {
14701 SidecarError::InvalidState(format!(
14702 "crypto.subtle.importKey keyData base64: {error}"
14703 ))
14704 })?;
14705 let algorithm = parsed.get("algorithm").ok_or_else(|| {
14706 SidecarError::InvalidState(String::from(
14707 "crypto.subtle.importKey missing algorithm",
14708 ))
14709 })?;
14710 let key = javascript_crypto_serialize_subtle_secret_key(
14711 &raw,
14712 javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
14713 parsed
14714 .get("extractable")
14715 .and_then(Value::as_bool)
14716 .unwrap_or(false),
14717 parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
14718 )?;
14719 Ok(Value::String(
14720 serde_json::to_string(&json!({ "key": key })).map_err(|error| {
14721 SidecarError::InvalidState(format!(
14722 "serialize crypto.subtle imported key: {error}"
14723 ))
14724 })?,
14725 ))
14726 }
14727 "exportKey" => {
14728 let format = parsed
14729 .get("format")
14730 .and_then(Value::as_str)
14731 .ok_or_else(|| {
14732 SidecarError::InvalidState(String::from(
14733 "crypto.subtle.exportKey missing format",
14734 ))
14735 })?;
14736 if format != "raw" {
14737 return Err(SidecarError::InvalidState(format!(
14738 "Unsupported export format: {format}"
14739 )));
14740 }
14741 let raw = javascript_crypto_subtle_key_raw(
14742 parsed.get("key").ok_or_else(|| {
14743 SidecarError::InvalidState(String::from("crypto.subtle.exportKey missing key"))
14744 })?,
14745 "crypto.subtle.exportKey key",
14746 )?;
14747 Ok(Value::String(
14748 serde_json::to_string(&json!({
14749 "data": base64::engine::general_purpose::STANDARD.encode(raw),
14750 }))
14751 .map_err(|error| {
14752 SidecarError::InvalidState(format!("serialize crypto.subtle export: {error}"))
14753 })?,
14754 ))
14755 }
14756 "encrypt" | "decrypt" => service_javascript_crypto_subtle_aes_crypt_sync_rpc(op, &parsed),
14757 _ => Err(SidecarError::InvalidState(format!(
14758 "Unsupported subtle operation: {op}"
14759 ))),
14760 }
14761}
14762
14763fn javascript_crypto_subtle_algorithm_name<'a>(
14764 algorithm: &'a Value,
14765 label: &str,
14766) -> Result<&'a str, SidecarError> {
14767 if let Some(name) = algorithm.as_str() {
14768 return Ok(name);
14769 }
14770 algorithm
14771 .get("name")
14772 .and_then(Value::as_str)
14773 .ok_or_else(|| SidecarError::InvalidState(format!("{label} algorithm missing name")))
14774}
14775
14776fn javascript_crypto_normalize_subtle_secret_algorithm(
14777 algorithm: Value,
14778 raw: &[u8],
14779) -> Result<Value, SidecarError> {
14780 let mut object = match algorithm {
14781 Value::String(name) => {
14782 let mut object = Map::new();
14783 object.insert(String::from("name"), Value::String(name));
14784 object
14785 }
14786 Value::Object(object) => object,
14787 _ => {
14788 return Err(SidecarError::InvalidState(String::from(
14789 "crypto.subtle secret algorithm must be a string or object",
14790 )));
14791 }
14792 };
14793 let name = object
14794 .get("name")
14795 .and_then(Value::as_str)
14796 .ok_or_else(|| {
14797 SidecarError::InvalidState(String::from("crypto.subtle secret algorithm missing name"))
14798 })?
14799 .to_string();
14800 if matches!(name.as_str(), "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW")
14801 && !object.contains_key("length")
14802 {
14803 object.insert(String::from("length"), json!(raw.len() * 8));
14804 }
14805 Ok(Value::Object(object))
14806}
14807
14808fn javascript_crypto_serialize_subtle_secret_key(
14809 raw: &[u8],
14810 algorithm: Value,
14811 extractable: bool,
14812 usages: Value,
14813) -> Result<Value, SidecarError> {
14814 let raw_base64 = base64::engine::general_purpose::STANDARD.encode(raw);
14815 let source_key_object_data = javascript_crypto_serialize_sandbox_key_object(
14816 &JavascriptCryptoKeyMaterial::Secret(raw.to_vec()),
14817 )?;
14818 Ok(json!({
14819 "type": "secret",
14820 "algorithm": algorithm,
14821 "extractable": extractable,
14822 "usages": usages,
14823 "_raw": raw_base64,
14824 "_sourceKeyObjectData": source_key_object_data,
14825 }))
14826}
14827
14828fn javascript_crypto_subtle_key_raw(key: &Value, label: &str) -> Result<Vec<u8>, SidecarError> {
14829 let raw = key.get("_raw").and_then(Value::as_str).ok_or_else(|| {
14830 SidecarError::InvalidState(format!("{label} must be a raw secret CryptoKey"))
14831 })?;
14832 base64::engine::general_purpose::STANDARD
14833 .decode(raw)
14834 .map_err(|error| SidecarError::InvalidState(format!("{label} raw base64: {error}")))
14835}
14836
14837fn service_javascript_crypto_subtle_aes_crypt_sync_rpc(
14838 op: &str,
14839 parsed: &Value,
14840) -> Result<Value, SidecarError> {
14841 let algorithm = parsed.get("algorithm").ok_or_else(|| {
14842 SidecarError::InvalidState(format!("crypto.subtle.{op} missing algorithm"))
14843 })?;
14844 let name = javascript_crypto_subtle_algorithm_name(algorithm, &format!("crypto.subtle.{op}"))?;
14845 if name != "AES-GCM" {
14846 return Err(SidecarError::InvalidState(format!(
14847 "Unsupported subtle AES operation algorithm: {name}"
14848 )));
14849 }
14850 let key = javascript_crypto_subtle_key_raw(
14851 parsed
14852 .get("key")
14853 .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing key")))?,
14854 &format!("crypto.subtle.{op} key"),
14855 )?;
14856 let iv = algorithm.get("iv").and_then(Value::as_str).ok_or_else(|| {
14857 SidecarError::InvalidState(format!("crypto.subtle.{op} AES-GCM missing iv"))
14858 })?;
14859 let iv = base64::engine::general_purpose::STANDARD
14860 .decode(iv)
14861 .map_err(|error| {
14862 SidecarError::InvalidState(format!("crypto.subtle.{op} iv base64: {error}"))
14863 })?;
14864 let data = parsed
14865 .get("data")
14866 .and_then(Value::as_str)
14867 .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing data")))?;
14868 let mut data = base64::engine::general_purpose::STANDARD
14869 .decode(data)
14870 .map_err(|error| {
14871 SidecarError::InvalidState(format!("crypto.subtle.{op} data base64: {error}"))
14872 })?;
14873 let tag_len = javascript_crypto_subtle_aes_gcm_tag_len(algorithm)?;
14874 let mut options = Map::new();
14875 options.insert(String::from("authTagLength"), json!(tag_len));
14876 if let Some(additional_data) = algorithm.get("additionalData").and_then(Value::as_str) {
14877 options.insert(
14878 String::from("aad"),
14879 Value::String(additional_data.to_string()),
14880 );
14881 }
14882 let decrypt = op == "decrypt";
14883 if decrypt {
14884 if data.len() < tag_len {
14885 return Err(SidecarError::InvalidState(String::from(
14886 "crypto.subtle.decrypt AES-GCM data shorter than auth tag",
14887 )));
14888 }
14889 let auth_tag = data.split_off(data.len() - tag_len);
14890 options.insert(
14891 String::from("authTag"),
14892 Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
14893 );
14894 }
14895 let cipher_name = format!("aes-{}-gcm", key.len() * 8);
14896 let mut context = javascript_crypto_build_cipher_context(
14897 &cipher_name,
14898 &key,
14899 Some(&iv),
14900 decrypt,
14901 Some(&Value::Object(options)),
14902 )?;
14903 let mut output = javascript_crypto_cipher_update(&mut context, &data)?;
14904 output.extend(javascript_crypto_cipher_finalize(&mut context)?);
14905 if !decrypt {
14906 let mut auth_tag = vec![0_u8; tag_len];
14907 context
14908 .get_tag(&mut auth_tag)
14909 .map_err(javascript_crypto_openssl_error)?;
14910 output.extend(auth_tag);
14911 }
14912 Ok(Value::String(
14913 serde_json::to_string(&json!({
14914 "data": base64::engine::general_purpose::STANDARD.encode(output),
14915 }))
14916 .map_err(|error| {
14917 SidecarError::InvalidState(format!("serialize crypto.subtle {op}: {error}"))
14918 })?,
14919 ))
14920}
14921
14922fn javascript_crypto_subtle_aes_gcm_tag_len(algorithm: &Value) -> Result<usize, SidecarError> {
14923 let tag_bits = algorithm
14924 .get("tagLength")
14925 .and_then(Value::as_u64)
14926 .unwrap_or(128);
14927 if !tag_bits.is_multiple_of(8) {
14928 return Err(SidecarError::InvalidState(String::from(
14929 "crypto.subtle AES-GCM tagLength must be byte-aligned",
14930 )));
14931 }
14932 usize::try_from(tag_bits / 8).map_err(|_| {
14933 SidecarError::InvalidState(String::from("crypto.subtle AES-GCM tagLength too large"))
14934 })
14935}
14936
14937fn service_javascript_crypto_cipheriv_inner(
14938 request: &JavascriptSyncRpcRequest,
14939 decrypt: bool,
14940) -> Result<Value, SidecarError> {
14941 let label = if decrypt {
14942 "crypto.decipheriv"
14943 } else {
14944 "crypto.cipheriv"
14945 };
14946 let algorithm = javascript_sync_rpc_arg_str(&request.args, 0, &format!("{label} algorithm"))?;
14947 let key = javascript_sync_rpc_base64_arg(&request.args, 1, &format!("{label} key"))?;
14948 let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 2, &format!("{label} iv"))?;
14949 let data = javascript_sync_rpc_base64_arg(&request.args, 3, &format!("{label} data"))?;
14950 let options =
14951 javascript_sync_rpc_json_arg_optional(&request.args, 4, &format!("{label} options"))?;
14952 let auth_tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options.as_ref())?;
14953 let mut context = javascript_crypto_build_cipher_context(
14954 algorithm,
14955 &key,
14956 iv.as_deref(),
14957 decrypt,
14958 options.as_ref(),
14959 )?;
14960 let payload = javascript_crypto_cipher_update(&mut context, &data)?;
14961 let final_bytes = javascript_crypto_cipher_finalize(&mut context)?;
14962 if decrypt {
14963 let mut output = payload;
14964 output.extend(final_bytes);
14965 return Ok(Value::String(
14966 base64::engine::general_purpose::STANDARD.encode(output),
14967 ));
14968 }
14969
14970 let mut response = Map::new();
14971 let mut encrypted = payload;
14972 encrypted.extend(final_bytes);
14973 response.insert(
14974 String::from("data"),
14975 Value::String(base64::engine::general_purpose::STANDARD.encode(encrypted)),
14976 );
14977 if javascript_crypto_is_aead(algorithm) {
14978 let mut auth_tag = vec![0_u8; auth_tag_len];
14979 context
14980 .get_tag(&mut auth_tag)
14981 .map_err(javascript_crypto_openssl_error)?;
14982 response.insert(
14983 String::from("authTag"),
14984 Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
14985 );
14986 }
14987 Ok(Value::String(serde_json::to_string(&response).map_err(
14988 |error| SidecarError::InvalidState(format!("serialize {label} response: {error}")),
14989 )?))
14990}
14991
14992fn javascript_sync_rpc_base64_arg_optional(
14993 args: &[Value],
14994 index: usize,
14995 label: &str,
14996) -> Result<Option<Vec<u8>>, SidecarError> {
14997 if args.get(index).is_none() || args[index].is_null() {
14998 return Ok(None);
14999 }
15000 javascript_sync_rpc_base64_arg(args, index, label).map(Some)
15001}
15002
15003fn javascript_sync_rpc_json_arg_optional(
15004 args: &[Value],
15005 index: usize,
15006 label: &str,
15007) -> Result<Option<Value>, SidecarError> {
15008 if args.get(index).is_none() || args[index].is_null() {
15009 return Ok(None);
15010 }
15011 let raw = javascript_sync_rpc_arg_str(args, index, label)?;
15012 serde_json::from_str(raw)
15013 .map(Some)
15014 .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
15015}
15016
15017fn javascript_crypto_parse_direct_key_input(
15018 raw: &str,
15019 expected: Option<&str>,
15020 label: &str,
15021) -> Result<JavascriptDirectKeyInput, SidecarError> {
15022 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
15023 SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
15024 })?;
15025 let padding = match parsed.as_object().and_then(|value| value.get("padding")) {
15026 Some(value) => javascript_crypto_padding_from_value(value)?,
15027 None => None,
15028 };
15029 Ok(JavascriptDirectKeyInput {
15030 key: javascript_crypto_parse_key_material_value(&parsed, expected, label)?,
15031 padding,
15032 })
15033}
15034
15035fn javascript_crypto_parse_key_material_value(
15036 value: &Value,
15037 expected: Option<&str>,
15038 label: &str,
15039) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15040 if let Some(object) = value.as_object() {
15041 if object.get("__type").and_then(Value::as_str) == Some("keyObject") {
15042 let serialized = object.get("value").ok_or_else(|| {
15043 SidecarError::InvalidState(format!("{label} keyObject is missing a value"))
15044 })?;
15045 return javascript_crypto_parse_serialized_key_object(serialized, expected, label);
15046 }
15047 if object.contains_key("type") && (object.contains_key("pem") || object.contains_key("raw"))
15048 {
15049 return javascript_crypto_parse_serialized_key_object(value, expected, label);
15050 }
15051 if let Some(source) = object.get("key") {
15052 return javascript_crypto_parse_key_source(
15053 source,
15054 object.get("format").and_then(Value::as_str),
15055 object.get("type").and_then(Value::as_str),
15056 expected,
15057 label,
15058 );
15059 }
15060 }
15061 javascript_crypto_parse_key_source(value, None, None, expected, label)
15062}
15063
15064fn javascript_crypto_parse_key_source(
15065 source: &Value,
15066 format: Option<&str>,
15067 kind: Option<&str>,
15068 expected: Option<&str>,
15069 label: &str,
15070) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15071 match source {
15072 Value::String(pem) => javascript_crypto_parse_key_from_pem(pem.as_bytes(), expected, label),
15073 Value::Object(object) if object.get("__type").and_then(Value::as_str) == Some("buffer") => {
15074 let data = javascript_crypto_decode_bridge_buffer(source, label)?;
15075 javascript_crypto_parse_key_from_bytes(&data, format, kind, expected, label)
15076 }
15077 Value::Object(_) => {
15078 if format == Some("jwk") {
15079 return Err(SidecarError::InvalidState(format!(
15080 "{label} jwk inputs are not supported yet"
15081 )));
15082 }
15083 Err(SidecarError::InvalidState(format!(
15084 "{label} has an unsupported key shape"
15085 )))
15086 }
15087 _ => Err(SidecarError::InvalidState(format!(
15088 "{label} has an unsupported key value"
15089 ))),
15090 }
15091}
15092
15093fn javascript_crypto_parse_key_from_pem(
15094 pem: &[u8],
15095 expected: Option<&str>,
15096 label: &str,
15097) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15098 match expected {
15099 Some("private") => PKey::private_key_from_pem(pem)
15100 .map(JavascriptCryptoKeyMaterial::Private)
15101 .map_err(|error| {
15102 SidecarError::InvalidState(format!("{label} private key is invalid: {error}"))
15103 }),
15104 Some("public") => PKey::public_key_from_pem(pem)
15105 .map(JavascriptCryptoKeyMaterial::Public)
15106 .map_err(|error| {
15107 SidecarError::InvalidState(format!("{label} public key is invalid: {error}"))
15108 }),
15109 _ => PKey::private_key_from_pem(pem)
15110 .map(JavascriptCryptoKeyMaterial::Private)
15111 .or_else(|_| PKey::public_key_from_pem(pem).map(JavascriptCryptoKeyMaterial::Public))
15112 .map_err(|error| {
15113 SidecarError::InvalidState(format!("{label} PEM key is invalid: {error}"))
15114 }),
15115 }
15116}
15117
15118fn javascript_crypto_parse_key_from_bytes(
15119 der: &[u8],
15120 format: Option<&str>,
15121 kind: Option<&str>,
15122 expected: Option<&str>,
15123 label: &str,
15124) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15125 match (format.unwrap_or("der"), kind.or(expected)) {
15126 ("der", Some("pkcs8")) | ("der", Some("private")) => PKey::private_key_from_der(der)
15127 .map(JavascriptCryptoKeyMaterial::Private)
15128 .map_err(|error| {
15129 SidecarError::InvalidState(format!("{label} private key DER is invalid: {error}"))
15130 }),
15131 ("der", Some("spki")) | ("der", Some("public")) => PKey::public_key_from_der(der)
15132 .map(JavascriptCryptoKeyMaterial::Public)
15133 .map_err(|error| {
15134 SidecarError::InvalidState(format!("{label} public key DER is invalid: {error}"))
15135 }),
15136 _ => Err(SidecarError::InvalidState(format!(
15137 "{label} unsupported key bytes format"
15138 ))),
15139 }
15140}
15141
15142fn javascript_crypto_parse_serialized_key_object(
15143 value: &Value,
15144 expected: Option<&str>,
15145 label: &str,
15146) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15147 let serialized: JavascriptSerializedSandboxKeyObject = serde_json::from_value(value.clone())
15148 .map_err(|error| {
15149 SidecarError::InvalidState(format!("{label} keyObject is invalid: {error}"))
15150 })?;
15151 match serialized.kind.as_str() {
15152 "secret" => {
15153 if expected == Some("public") || expected == Some("private") {
15154 return Err(SidecarError::InvalidState(format!(
15155 "{label} expected an asymmetric key"
15156 )));
15157 }
15158 Ok(JavascriptCryptoKeyMaterial::Secret(
15159 base64::engine::general_purpose::STANDARD
15160 .decode(serialized.raw.unwrap_or_default())
15161 .map_err(|error| {
15162 SidecarError::InvalidState(format!(
15163 "{label} secret key contains invalid base64: {error}"
15164 ))
15165 })?,
15166 ))
15167 }
15168 "private" => {
15169 let pem = serialized.pem.ok_or_else(|| {
15170 SidecarError::InvalidState(format!("{label} private keyObject is missing pem"))
15171 })?;
15172 javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("private"), label)
15173 }
15174 "public" => {
15175 let pem = serialized.pem.ok_or_else(|| {
15176 SidecarError::InvalidState(format!("{label} public keyObject is missing pem"))
15177 })?;
15178 javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("public"), label)
15179 }
15180 other => Err(SidecarError::InvalidState(format!(
15181 "{label} has unsupported keyObject type {other}"
15182 ))),
15183 }
15184}
15185
15186fn javascript_crypto_expect_private_key(
15187 key: JavascriptCryptoKeyMaterial,
15188 label: &str,
15189) -> Result<PKey<Private>, SidecarError> {
15190 match key {
15191 JavascriptCryptoKeyMaterial::Private(key) => Ok(key),
15192 _ => Err(SidecarError::InvalidState(format!(
15193 "{label} requires a private key"
15194 ))),
15195 }
15196}
15197
15198fn javascript_crypto_expect_public_key(
15199 key: JavascriptCryptoKeyMaterial,
15200 label: &str,
15201) -> Result<PKey<Public>, SidecarError> {
15202 match key {
15203 JavascriptCryptoKeyMaterial::Public(key) => Ok(key),
15204 JavascriptCryptoKeyMaterial::Private(key) => {
15205 let pem = key
15206 .public_key_to_pem()
15207 .map_err(javascript_crypto_openssl_error)?;
15208 PKey::public_key_from_pem(&pem).map_err(javascript_crypto_openssl_error)
15209 }
15210 _ => Err(SidecarError::InvalidState(format!(
15211 "{label} requires a public key"
15212 ))),
15213 }
15214}
15215
15216fn javascript_crypto_new_signer<'a>(
15217 algorithm: Option<&'a str>,
15218 key: &'a PKey<Private>,
15219) -> Result<Signer<'a>, SidecarError> {
15220 if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
15221 return Signer::new_without_digest(key).map_err(javascript_crypto_openssl_error);
15222 }
15223 Signer::new(
15224 javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
15225 SidecarError::InvalidState(String::from("crypto.sign requires a digest algorithm"))
15226 })?)?,
15227 key,
15228 )
15229 .map_err(javascript_crypto_openssl_error)
15230}
15231
15232fn javascript_crypto_new_verifier<'a>(
15233 algorithm: Option<&'a str>,
15234 key: &'a PKey<Public>,
15235) -> Result<Verifier<'a>, SidecarError> {
15236 if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
15237 return Verifier::new_without_digest(key).map_err(javascript_crypto_openssl_error);
15238 }
15239 Verifier::new(
15240 javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
15241 SidecarError::InvalidState(String::from("crypto.verify requires a digest algorithm"))
15242 })?)?,
15243 key,
15244 )
15245 .map_err(javascript_crypto_openssl_error)
15246}
15247
15248fn javascript_crypto_message_digest_from_name(name: &str) -> Result<MessageDigest, SidecarError> {
15249 match name.trim().to_ascii_lowercase().replace('-', "").as_str() {
15250 "md5" => Ok(MessageDigest::md5()),
15251 "sha1" => Ok(MessageDigest::sha1()),
15252 "sha256" => Ok(MessageDigest::sha256()),
15253 "sha384" => Ok(MessageDigest::sha384()),
15254 "sha512" => Ok(MessageDigest::sha512()),
15255 other => Err(SidecarError::InvalidState(format!(
15256 "unsupported crypto digest algorithm {other}"
15257 ))),
15258 }
15259}
15260
15261fn javascript_crypto_padding_from_value(value: &Value) -> Result<Option<Padding>, SidecarError> {
15262 let Some(number) = value.as_i64() else {
15263 return Ok(None);
15264 };
15265 let padding = match number {
15266 1 => Padding::PKCS1,
15267 3 => Padding::NONE,
15268 4 => Padding::PKCS1_OAEP,
15269 6 => Padding::PKCS1_PSS,
15270 other => {
15271 return Err(SidecarError::InvalidState(format!(
15272 "unsupported RSA padding constant {other}"
15273 )));
15274 }
15275 };
15276 Ok(Some(padding))
15277}
15278
15279fn javascript_crypto_decode_bridge_buffer(
15280 value: &Value,
15281 label: &str,
15282) -> Result<Vec<u8>, SidecarError> {
15283 let base64_value = value
15284 .as_object()
15285 .filter(|object| object.get("__type").and_then(Value::as_str) == Some("buffer"))
15286 .and_then(|object| object.get("value"))
15287 .and_then(Value::as_str)
15288 .ok_or_else(|| {
15289 SidecarError::InvalidState(format!("{label} must be a serialized bridge buffer"))
15290 })?;
15291 base64::engine::general_purpose::STANDARD
15292 .decode(base64_value)
15293 .map_err(|error| {
15294 SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
15295 })
15296}
15297
15298fn javascript_crypto_serialize_sandbox_key_object(
15299 key: &JavascriptCryptoKeyMaterial,
15300) -> Result<Value, SidecarError> {
15301 let serialized = match key {
15302 JavascriptCryptoKeyMaterial::Private(key) => JavascriptSerializedSandboxKeyObject {
15303 kind: String::from("private"),
15304 pem: Some(
15305 String::from_utf8(
15306 key.private_key_to_pem_pkcs8()
15307 .map_err(javascript_crypto_openssl_error)?,
15308 )
15309 .map_err(|error| {
15310 SidecarError::InvalidState(format!("private key PEM is not utf8: {error}"))
15311 })?,
15312 ),
15313 raw: None,
15314 asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
15315 asymmetric_key_details: None,
15316 jwk: None,
15317 },
15318 JavascriptCryptoKeyMaterial::Public(key) => JavascriptSerializedSandboxKeyObject {
15319 kind: String::from("public"),
15320 pem: Some(
15321 String::from_utf8(
15322 key.public_key_to_pem()
15323 .map_err(javascript_crypto_openssl_error)?,
15324 )
15325 .map_err(|error| {
15326 SidecarError::InvalidState(format!("public key PEM is not utf8: {error}"))
15327 })?,
15328 ),
15329 raw: None,
15330 asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
15331 asymmetric_key_details: None,
15332 jwk: None,
15333 },
15334 JavascriptCryptoKeyMaterial::Secret(raw) => JavascriptSerializedSandboxKeyObject {
15335 kind: String::from("secret"),
15336 pem: None,
15337 raw: Some(base64::engine::general_purpose::STANDARD.encode(raw)),
15338 asymmetric_key_type: None,
15339 asymmetric_key_details: None,
15340 jwk: None,
15341 },
15342 };
15343 serde_json::to_value(serialized)
15344 .map_err(|error| SidecarError::InvalidState(format!("serialize key object: {error}")))
15345}
15346
15347fn javascript_crypto_pkey_type_name(id: PKeyId) -> Option<String> {
15348 match id {
15349 PKeyId::RSA => Some(String::from("rsa")),
15350 PKeyId::EC => Some(String::from("ec")),
15351 PKeyId::ED25519 => Some(String::from("ed25519")),
15352 PKeyId::ED448 => Some(String::from("ed448")),
15353 PKeyId::X25519 => Some(String::from("x25519")),
15354 PKeyId::X448 => Some(String::from("x448")),
15355 PKeyId::DH => Some(String::from("dh")),
15356 _ => None,
15357 }
15358}
15359
15360fn javascript_crypto_rsa_output_size(
15361 key: &JavascriptCryptoKeyMaterial,
15362) -> Result<usize, SidecarError> {
15363 match key {
15364 JavascriptCryptoKeyMaterial::Private(key) => key
15365 .rsa()
15366 .map(|rsa| rsa.size() as usize)
15367 .map_err(javascript_crypto_openssl_error),
15368 JavascriptCryptoKeyMaterial::Public(key) => key
15369 .rsa()
15370 .map(|rsa| rsa.size() as usize)
15371 .map_err(javascript_crypto_openssl_error),
15372 JavascriptCryptoKeyMaterial::Secret(_) => Err(SidecarError::InvalidState(String::from(
15373 "RSA operations require an asymmetric key",
15374 ))),
15375 }
15376}
15377
15378fn javascript_crypto_parse_serialized_options_arg(
15379 args: &[Value],
15380 index: usize,
15381 label: &str,
15382) -> Result<Option<Value>, SidecarError> {
15383 let Some(raw) = args.get(index).and_then(Value::as_str) else {
15384 return Ok(None);
15385 };
15386 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
15387 SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
15388 })?;
15389 if parsed.get("hasOptions").and_then(Value::as_bool) == Some(true) {
15390 Ok(parsed.get("options").cloned())
15391 } else {
15392 Ok(None)
15393 }
15394}
15395
15396fn javascript_crypto_u32_from_bridge_value(
15397 value: &Value,
15398 label: &str,
15399) -> Result<u32, SidecarError> {
15400 if let Some(number) = value.as_u64() {
15401 return u32::try_from(number)
15402 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")));
15403 }
15404 let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
15405 if bytes.len() > 4 {
15406 return Err(SidecarError::InvalidState(format!(
15407 "{label} buffer is too large for u32"
15408 )));
15409 }
15410 Ok(bytes
15411 .into_iter()
15412 .fold(0_u32, |acc, byte| (acc << 8) | u32::from(byte)))
15413}
15414
15415fn javascript_crypto_bignum_from_bridge_value(
15416 value: &Value,
15417 label: &str,
15418) -> Result<BigNum, SidecarError> {
15419 if let Some(object) = value.as_object() {
15420 if object.get("__type").and_then(Value::as_str) == Some("bigint") {
15421 let decimal = object.get("value").and_then(Value::as_str).ok_or_else(|| {
15422 SidecarError::InvalidState(format!("{label} bigint is missing a value"))
15423 })?;
15424 return BigNum::from_dec_str(decimal).map_err(javascript_crypto_openssl_error);
15425 }
15426 }
15427 let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
15428 BigNum::from_slice(&bytes).map_err(javascript_crypto_openssl_error)
15429}
15430
15431fn javascript_crypto_curve_nid(name: &str) -> Result<Nid, SidecarError> {
15432 match name {
15433 "prime256v1" | "P-256" => Ok(Nid::X9_62_PRIME256V1),
15434 "secp384r1" | "P-384" => Ok(Nid::SECP384R1),
15435 "secp521r1" | "P-521" => Ok(Nid::SECP521R1),
15436 "secp256k1" => Ok(Nid::SECP256K1),
15437 other => Err(SidecarError::InvalidState(format!(
15438 "unsupported EC curve {other}"
15439 ))),
15440 }
15441}
15442
15443fn javascript_crypto_named_dh_group(name: &str) -> Result<Dh<Params>, SidecarError> {
15444 match name {
15445 "modp2" => Dh::get_1024_160().map_err(javascript_crypto_openssl_error),
15446 "modp14" | "modp15" | "modp16" | "modp17" | "modp18" => {
15447 Dh::get_2048_256().map_err(javascript_crypto_openssl_error)
15448 }
15449 other => Err(SidecarError::InvalidState(format!(
15450 "unsupported Diffie-Hellman group {other}"
15451 ))),
15452 }
15453}
15454
15455fn javascript_crypto_clone_dh_params(params: &Dh<Params>) -> Result<Dh<Params>, SidecarError> {
15456 Dh::from_pqg(
15457 params
15458 .prime_p()
15459 .to_owned()
15460 .map_err(javascript_crypto_openssl_error)?,
15461 params
15462 .prime_q()
15463 .map(|value| value.to_owned().map_err(javascript_crypto_openssl_error))
15464 .transpose()?,
15465 params
15466 .generator()
15467 .to_owned()
15468 .map_err(javascript_crypto_openssl_error)?,
15469 )
15470 .map_err(javascript_crypto_openssl_error)
15471}
15472
15473fn javascript_crypto_build_dh_params(args: &[Value]) -> Result<Dh<Params>, SidecarError> {
15474 let Some(first) = args.first() else {
15475 return Err(SidecarError::InvalidState(String::from(
15476 "Diffie-Hellman session args are required",
15477 )));
15478 };
15479 if let Some(bits) = first.as_u64() {
15480 let generator = args
15481 .get(1)
15482 .map(|value| javascript_crypto_u32_from_bridge_value(value, "Diffie-Hellman generator"))
15483 .transpose()?
15484 .unwrap_or(2);
15485 return Dh::generate_params(bits as u32, generator)
15486 .map_err(javascript_crypto_openssl_error);
15487 }
15488 let prime = javascript_crypto_bignum_from_bridge_value(first, "Diffie-Hellman prime")?;
15489 let generator = args
15490 .get(1)
15491 .map(|value| javascript_crypto_bignum_from_bridge_value(value, "Diffie-Hellman generator"))
15492 .transpose()?
15493 .unwrap_or(BigNum::from_u32(2).map_err(javascript_crypto_openssl_error)?);
15494 Dh::from_pqg(prime, None, generator).map_err(javascript_crypto_openssl_error)
15495}
15496
15497fn javascript_crypto_call_dh_session(
15498 session: &mut ActiveDhSession,
15499 method: &str,
15500 args: &[Value],
15501) -> Result<(Value, bool), SidecarError> {
15502 match method {
15503 "verifyError" => Ok((Value::Null, false)),
15504 "generateKeys" => {
15505 if session.key_pair.is_none() {
15506 session.key_pair = Some(
15507 javascript_crypto_clone_dh_params(&session.params)?
15508 .generate_key()
15509 .map_err(javascript_crypto_openssl_error)?,
15510 );
15511 }
15512 let public = session
15513 .key_pair
15514 .as_ref()
15515 .expect("dh key pair")
15516 .public_key()
15517 .to_vec();
15518 Ok((javascript_crypto_bridge_buffer_value(&public), true))
15519 }
15520 "computeSecret" => {
15521 if session.key_pair.is_none() {
15522 session.key_pair = Some(
15523 javascript_crypto_clone_dh_params(&session.params)?
15524 .generate_key()
15525 .map_err(javascript_crypto_openssl_error)?,
15526 );
15527 }
15528 let peer = javascript_crypto_bignum_from_bridge_value(
15529 args.first().ok_or_else(|| {
15530 SidecarError::InvalidState(String::from(
15531 "computeSecret requires peer public key",
15532 ))
15533 })?,
15534 "Diffie-Hellman peer public key",
15535 )?;
15536 let secret = session
15537 .key_pair
15538 .as_ref()
15539 .expect("dh key pair")
15540 .compute_key(&peer)
15541 .map_err(javascript_crypto_openssl_error)?;
15542 Ok((javascript_crypto_bridge_buffer_value(&secret), true))
15543 }
15544 "getPrime" => Ok((
15545 javascript_crypto_bridge_buffer_value(&session.params.prime_p().to_vec()),
15546 true,
15547 )),
15548 "getGenerator" => Ok((
15549 javascript_crypto_bridge_buffer_value(&session.params.generator().to_vec()),
15550 true,
15551 )),
15552 "getPublicKey" => {
15553 if session.key_pair.is_none() {
15554 session.key_pair = Some(
15555 javascript_crypto_clone_dh_params(&session.params)?
15556 .generate_key()
15557 .map_err(javascript_crypto_openssl_error)?,
15558 );
15559 }
15560 Ok((
15561 javascript_crypto_bridge_buffer_value(
15562 &session
15563 .key_pair
15564 .as_ref()
15565 .expect("dh key pair")
15566 .public_key()
15567 .to_vec(),
15568 ),
15569 true,
15570 ))
15571 }
15572 "getPrivateKey" => {
15573 if session.key_pair.is_none() {
15574 session.key_pair = Some(
15575 javascript_crypto_clone_dh_params(&session.params)?
15576 .generate_key()
15577 .map_err(javascript_crypto_openssl_error)?,
15578 );
15579 }
15580 Ok((
15581 javascript_crypto_bridge_buffer_value(
15582 &session
15583 .key_pair
15584 .as_ref()
15585 .expect("dh key pair")
15586 .private_key()
15587 .to_vec(),
15588 ),
15589 true,
15590 ))
15591 }
15592 other => Err(SidecarError::InvalidState(format!(
15593 "Unsupported Diffie-Hellman method: {other}"
15594 ))),
15595 }
15596}
15597
15598fn javascript_crypto_call_ecdh_session(
15599 session: &mut ActiveEcdhSession,
15600 method: &str,
15601 args: &[Value],
15602) -> Result<(Value, bool), SidecarError> {
15603 let nid = javascript_crypto_curve_nid(&session.curve)?;
15604 let group = EcGroup::from_curve_name(nid).map_err(javascript_crypto_openssl_error)?;
15605 match method {
15606 "verifyError" => Ok((Value::Null, false)),
15607 "generateKeys" => {
15608 if session.key_pair.is_none() {
15609 session.key_pair =
15610 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15611 }
15612 let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15613 let bytes = session
15614 .key_pair
15615 .as_ref()
15616 .expect("ecdh key pair")
15617 .public_key()
15618 .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
15619 .map_err(javascript_crypto_openssl_error)?;
15620 Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
15621 }
15622 "computeSecret" => {
15623 if session.key_pair.is_none() {
15624 session.key_pair =
15625 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15626 }
15627 let peer_bytes = javascript_crypto_decode_bridge_buffer(
15628 args.first().ok_or_else(|| {
15629 SidecarError::InvalidState(String::from(
15630 "computeSecret requires peer public key",
15631 ))
15632 })?,
15633 "ECDH peer public key",
15634 )?;
15635 let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15636 let peer_point = EcPoint::from_bytes(&group, &peer_bytes, &mut ctx)
15637 .map_err(javascript_crypto_openssl_error)?;
15638 let peer_key = EcKey::from_public_key(&group, &peer_point)
15639 .map_err(javascript_crypto_openssl_error)?;
15640 let private =
15641 PKey::from_ec_key(session.key_pair.as_ref().expect("ecdh key pair").to_owned())
15642 .map_err(javascript_crypto_openssl_error)?;
15643 let peer = PKey::from_ec_key(peer_key).map_err(javascript_crypto_openssl_error)?;
15644 let mut deriver = Deriver::new(&private).map_err(javascript_crypto_openssl_error)?;
15645 deriver
15646 .set_peer(&peer)
15647 .map_err(javascript_crypto_openssl_error)?;
15648 let secret = deriver
15649 .derive_to_vec()
15650 .map_err(javascript_crypto_openssl_error)?;
15651 Ok((javascript_crypto_bridge_buffer_value(&secret), true))
15652 }
15653 "getPublicKey" => {
15654 if session.key_pair.is_none() {
15655 session.key_pair =
15656 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15657 }
15658 let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15659 let bytes = session
15660 .key_pair
15661 .as_ref()
15662 .expect("ecdh key pair")
15663 .public_key()
15664 .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
15665 .map_err(javascript_crypto_openssl_error)?;
15666 Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
15667 }
15668 "getPrivateKey" => {
15669 if session.key_pair.is_none() {
15670 session.key_pair =
15671 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15672 }
15673 Ok((
15674 javascript_crypto_bridge_buffer_value(
15675 &session
15676 .key_pair
15677 .as_ref()
15678 .expect("ecdh key pair")
15679 .private_key()
15680 .to_vec(),
15681 ),
15682 true,
15683 ))
15684 }
15685 other => Err(SidecarError::InvalidState(format!(
15686 "Unsupported Diffie-Hellman method: {other}"
15687 ))),
15688 }
15689}
15690
15691fn javascript_crypto_serialize_encoded_key_value_public(
15692 key: &PKey<Public>,
15693 encoding: Option<&Value>,
15694) -> Result<Value, SidecarError> {
15695 if let Some(encoding) = encoding {
15696 let format = encoding
15697 .get("format")
15698 .and_then(Value::as_str)
15699 .unwrap_or("pem");
15700 return Ok(match format {
15701 "der" => json!({
15702 "kind": "buffer",
15703 "value": base64::engine::general_purpose::STANDARD
15704 .encode(key.public_key_to_der().map_err(javascript_crypto_openssl_error)?),
15705 }),
15706 _ => json!({
15707 "kind": "string",
15708 "value": String::from_utf8(
15709 key.public_key_to_pem().map_err(javascript_crypto_openssl_error)?,
15710 )
15711 .map_err(|error| SidecarError::InvalidState(format!("public key PEM utf8: {error}")))?,
15712 }),
15713 });
15714 }
15715 javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(
15716 key.to_owned(),
15717 ))
15718}
15719
15720fn javascript_crypto_serialize_encoded_key_value_private(
15721 key: &PKey<Private>,
15722 encoding: Option<&Value>,
15723) -> Result<Value, SidecarError> {
15724 if let Some(encoding) = encoding {
15725 let format = encoding
15726 .get("format")
15727 .and_then(Value::as_str)
15728 .unwrap_or("pem");
15729 return Ok(match format {
15730 "der" => json!({
15731 "kind": "buffer",
15732 "value": base64::engine::general_purpose::STANDARD
15733 .encode(key.private_key_to_der().map_err(javascript_crypto_openssl_error)?),
15734 }),
15735 _ => json!({
15736 "kind": "string",
15737 "value": String::from_utf8(
15738 key.private_key_to_pem_pkcs8().map_err(javascript_crypto_openssl_error)?,
15739 )
15740 .map_err(|error| SidecarError::InvalidState(format!("private key PEM utf8: {error}")))?,
15741 }),
15742 });
15743 }
15744 javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(
15745 key.to_owned(),
15746 ))
15747}
15748
15749fn javascript_crypto_bridge_buffer_value(bytes: &[u8]) -> Value {
15750 json!({
15751 "__type": "buffer",
15752 "value": base64::engine::general_purpose::STANDARD.encode(bytes),
15753 })
15754}
15755
15756fn javascript_crypto_build_cipher_context(
15757 algorithm: &str,
15758 key: &[u8],
15759 iv: Option<&[u8]>,
15760 decrypt: bool,
15761 options: Option<&Value>,
15762) -> Result<Crypter, SidecarError> {
15763 let cipher = javascript_crypto_cipher_from_name(algorithm)?;
15764 let mode = if decrypt {
15765 Mode::Decrypt
15766 } else {
15767 Mode::Encrypt
15768 };
15769 let mut context =
15770 Crypter::new(cipher, mode, key, iv).map_err(javascript_crypto_openssl_error)?;
15771 if let Some(auto_padding) = options
15772 .and_then(|value| value.get("autoPadding"))
15773 .and_then(Value::as_bool)
15774 {
15775 context.pad(auto_padding);
15776 }
15777 if javascript_crypto_is_aead(algorithm) {
15778 if let Some(aad) = options
15779 .and_then(|value| value.get("aad"))
15780 .and_then(Value::as_str)
15781 {
15782 context
15783 .aad_update(
15784 &base64::engine::general_purpose::STANDARD
15785 .decode(aad)
15786 .map_err(|error| {
15787 SidecarError::InvalidState(format!(
15788 "cipher aad contains invalid base64: {error}"
15789 ))
15790 })?,
15791 )
15792 .map_err(javascript_crypto_openssl_error)?;
15793 }
15794 if decrypt {
15795 if let Some(auth_tag) = options
15796 .and_then(|value| value.get("authTag"))
15797 .and_then(Value::as_str)
15798 {
15799 let decoded = base64::engine::general_purpose::STANDARD
15800 .decode(auth_tag)
15801 .map_err(|error| {
15802 SidecarError::InvalidState(format!(
15803 "cipher authTag contains invalid base64: {error}"
15804 ))
15805 })?;
15806 context
15807 .set_tag(&decoded)
15808 .map_err(javascript_crypto_openssl_error)?;
15809 }
15810 }
15811 }
15812 Ok(context)
15813}
15814
15815fn javascript_crypto_requested_aead_tag_len(
15816 algorithm: &str,
15817 options: Option<&Value>,
15818) -> Result<usize, SidecarError> {
15819 if !javascript_crypto_is_aead(algorithm) {
15820 return Ok(0);
15821 }
15822 let requested = options
15823 .and_then(|value| value.get("authTagLength"))
15824 .and_then(Value::as_u64)
15825 .unwrap_or(javascript_crypto_aead_tag_len(algorithm) as u64);
15826 usize::try_from(requested).map_err(|_| {
15827 SidecarError::InvalidState(String::from("cipher authTagLength must fit within usize"))
15828 })
15829}
15830
15831fn javascript_crypto_cipher_update(
15832 context: &mut Crypter,
15833 data: &[u8],
15834) -> Result<Vec<u8>, SidecarError> {
15835 let mut output = vec![0_u8; data.len() + 32];
15836 let written = context
15837 .update(data, &mut output)
15838 .map_err(javascript_crypto_openssl_error)?;
15839 output.truncate(written);
15840 Ok(output)
15841}
15842
15843fn javascript_crypto_cipher_finalize(context: &mut Crypter) -> Result<Vec<u8>, SidecarError> {
15844 let mut output = vec![0_u8; 32];
15845 let written = context
15846 .finalize(&mut output)
15847 .map_err(javascript_crypto_openssl_error)?;
15848 output.truncate(written);
15849 Ok(output)
15850}
15851
15852fn javascript_crypto_cipher_from_name(name: &str) -> Result<Cipher, SidecarError> {
15853 match name.to_ascii_lowercase().as_str() {
15854 "aes-128-cbc" => Ok(Cipher::aes_128_cbc()),
15855 "aes-192-cbc" => Ok(Cipher::aes_192_cbc()),
15856 "aes-256-cbc" => Ok(Cipher::aes_256_cbc()),
15857 "aes-128-ctr" => Ok(Cipher::aes_128_ctr()),
15858 "aes-192-ctr" => Ok(Cipher::aes_192_ctr()),
15859 "aes-256-ctr" => Ok(Cipher::aes_256_ctr()),
15860 "aes-128-gcm" => Ok(Cipher::aes_128_gcm()),
15861 "aes-192-gcm" => Ok(Cipher::aes_192_gcm()),
15862 "aes-256-gcm" => Ok(Cipher::aes_256_gcm()),
15863 other => Err(SidecarError::InvalidState(format!(
15864 "unsupported crypto cipher algorithm {other}"
15865 ))),
15866 }
15867}
15868
15869fn javascript_crypto_is_aead(algorithm: &str) -> bool {
15870 algorithm.to_ascii_lowercase().ends_with("-gcm")
15871}
15872
15873fn javascript_crypto_aead_tag_len(_algorithm: &str) -> usize {
15874 16
15875}
15876
15877fn javascript_crypto_openssl_error(error: openssl::error::ErrorStack) -> SidecarError {
15878 SidecarError::Execution(format!("crypto operation failed: {error}"))
15879}
15880
15881fn service_javascript_kernel_stdin_sync_rpc(
15882 kernel: &mut SidecarKernel,
15883 process: &mut ActiveProcess,
15884 request: &JavascriptSyncRpcRequest,
15885) -> Result<Value, SidecarError> {
15886 let max_bytes =
15887 javascript_sync_rpc_arg_u64_optional(&request.args, 0, "__kernel_stdin_read max bytes")?
15888 .map(|value| value.clamp(1, DEFAULT_KERNEL_STDIN_READ_MAX_BYTES as u64) as usize)
15889 .unwrap_or(DEFAULT_KERNEL_STDIN_READ_MAX_BYTES);
15890 let timeout_ms =
15891 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_stdin_read timeout ms")?
15892 .unwrap_or(DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS);
15893
15894 match kernel
15895 .fd_read_with_timeout_result(
15896 EXECUTION_DRIVER_NAME,
15897 process.kernel_pid,
15898 0,
15899 max_bytes,
15900 Some(Duration::from_millis(timeout_ms)),
15901 )
15902 .map_err(kernel_error)
15903 {
15904 Ok(Some(chunk)) if !chunk.is_empty() => Ok(json!({
15905 "dataBase64": base64::engine::general_purpose::STANDARD.encode(chunk),
15906 })),
15907 Ok(Some(_)) => Ok(Value::Null),
15908 Ok(None) => Ok(json!({
15909 "done": true,
15910 })),
15911 Err(SidecarError::Kernel(error)) if error.starts_with("EAGAIN:") => Ok(Value::Null),
15912 Err(error) => Err(error),
15913 }
15914}
15915
15916fn service_javascript_pty_set_raw_mode_sync_rpc(
15917 kernel: &mut SidecarKernel,
15918 process: &mut ActiveProcess,
15919 request: &JavascriptSyncRpcRequest,
15920) -> Result<Value, SidecarError> {
15921 let enabled = javascript_sync_rpc_arg_bool(&request.args, 0, "__pty_set_raw_mode enabled")?;
15922 kernel
15923 .pty_set_discipline(
15924 EXECUTION_DRIVER_NAME,
15925 process.kernel_pid,
15926 0,
15927 LineDisciplineConfig {
15928 canonical: Some(!enabled),
15929 echo: Some(!enabled),
15930 isig: Some(!enabled),
15931 },
15932 )
15933 .map_err(kernel_error)?;
15934 Ok(Value::Null)
15935}
15936
15937fn service_javascript_kernel_stdio_write_sync_rpc(
15938 kernel: &mut SidecarKernel,
15939 process: &mut ActiveProcess,
15940 request: &JavascriptSyncRpcRequest,
15941) -> Result<Value, SidecarError> {
15942 let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_stdio_write fd")?;
15943 let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "__kernel_stdio_write chunk")?;
15944
15945 let written = match fd {
15946 1 => kernel
15947 .write_process_stdout(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
15948 .map_err(kernel_error)?,
15949 2 => kernel
15950 .write_process_stderr(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
15951 .map_err(kernel_error)?,
15952 other => {
15953 return Err(SidecarError::InvalidState(format!(
15954 "__kernel_stdio_write only supports fd 1/2, got {other}"
15955 )));
15956 }
15957 };
15958
15959 let event = if fd == 1 {
15960 ActiveExecutionEvent::Stdout(chunk)
15961 } else {
15962 ActiveExecutionEvent::Stderr(chunk)
15963 };
15964 process.queue_pending_execution_event(event)?;
15965
15966 Ok(json!(written))
15967}
15968
15969fn service_javascript_kernel_poll_sync_rpc(
15970 kernel: &mut SidecarKernel,
15971 process: &ActiveProcess,
15972 request: &JavascriptSyncRpcRequest,
15973) -> Result<Value, SidecarError> {
15974 let fd_requests: Vec<KernelPollFdRequest> = serde_json::from_value(
15975 request
15976 .args
15977 .first()
15978 .cloned()
15979 .unwrap_or_else(|| Value::Array(Vec::new())),
15980 )
15981 .map_err(|error| {
15982 SidecarError::InvalidState(format!(
15983 "__kernel_poll fd list must be a JSON array of {{ fd, events }} objects: {error}"
15984 ))
15985 })?;
15986 let timeout_ms =
15987 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_poll timeout ms")?
15988 .unwrap_or_default();
15989 let timeout_ms = i32::try_from(timeout_ms).map_err(|_| {
15990 SidecarError::InvalidState(String::from("__kernel_poll timeout ms must fit within i32"))
15991 })?;
15992
15993 let poll_fds = fd_requests
15994 .iter()
15995 .map(|entry| PollFd {
15996 fd: entry.fd,
15997 events: PollEvents::from_bits(entry.events),
15998 revents: PollEvents::empty(),
15999 })
16000 .collect::<Vec<_>>();
16001 let result = kernel
16002 .poll_fds(
16003 EXECUTION_DRIVER_NAME,
16004 process.kernel_pid,
16005 poll_fds,
16006 timeout_ms,
16007 )
16008 .map_err(kernel_error)?;
16009
16010 Ok(json!({
16011 "readyCount": result.ready_count,
16012 "fds": result
16013 .fds
16014 .into_iter()
16015 .map(|entry| KernelPollFdResponse {
16016 fd: entry.fd,
16017 events: entry.events.bits(),
16018 revents: entry.revents.bits(),
16019 })
16020 .collect::<Vec<_>>(),
16021 }))
16022}
16023
16024fn install_kernel_stdin_pipe(kernel: &mut SidecarKernel, pid: u32) -> Result<u32, SidecarError> {
16025 let (read_fd, write_fd) = kernel
16026 .open_pipe(EXECUTION_DRIVER_NAME, pid)
16027 .map_err(kernel_error)?;
16028 kernel
16029 .fd_dup2(EXECUTION_DRIVER_NAME, pid, read_fd, 0)
16030 .map_err(kernel_error)?;
16031 kernel
16032 .fd_close(EXECUTION_DRIVER_NAME, pid, read_fd)
16033 .map_err(kernel_error)?;
16034 Ok(write_fd)
16035}
16036
16037fn javascript_child_process_stdin_mode(request: &JavascriptChildProcessSpawnRequest) -> &str {
16038 request
16039 .options
16040 .stdio
16041 .first()
16042 .map(String::as_str)
16043 .unwrap_or("pipe")
16044}
16045
16046pub(crate) fn write_kernel_process_stdin(
16047 kernel: &mut SidecarKernel,
16048 process: &mut ActiveProcess,
16049 chunk: &[u8],
16050) -> Result<(), SidecarError> {
16051 if process.runtime == GuestRuntimeKind::JavaScript {
16052 return Ok(());
16053 }
16054 let Some(writer_fd) = process.kernel_stdin_writer_fd else {
16055 return Ok(());
16056 };
16057 kernel
16058 .fd_write(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd, chunk)
16059 .map(|_| ())
16060 .map_err(kernel_error)
16061}
16062
16063pub(crate) fn close_kernel_process_stdin(
16064 kernel: &mut SidecarKernel,
16065 process: &mut ActiveProcess,
16066) -> Result<(), SidecarError> {
16067 let Some(writer_fd) = process.kernel_stdin_writer_fd.take() else {
16068 return Ok(());
16069 };
16070 kernel
16071 .fd_close(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd)
16072 .map_err(kernel_error)
16073}
16074
16075fn parse_http_header_collection(
16076 headers: &BTreeMap<String, Value>,
16077 label: &str,
16078) -> Result<HttpHeaderCollection, SidecarError> {
16079 let mut normalized = BTreeMap::<String, Vec<String>>::new();
16080 let mut raw_pairs = Vec::new();
16081
16082 for (raw_name, value) in headers {
16083 let normalized_name = raw_name.to_ascii_lowercase();
16084 let values = match value {
16085 Value::String(text) => vec![text.clone()],
16086 Value::Array(values) => values
16087 .iter()
16088 .map(|entry| {
16089 entry.as_str().map(str::to_owned).ok_or_else(|| {
16090 SidecarError::InvalidState(format!(
16091 "{label} header {raw_name} must contain only strings"
16092 ))
16093 })
16094 })
16095 .collect::<Result<Vec<_>, _>>()?,
16096 other => {
16097 return Err(SidecarError::InvalidState(format!(
16098 "{label} header {raw_name} must be a string or string array, received {other}"
16099 )));
16100 }
16101 };
16102 raw_pairs.extend(
16103 values
16104 .iter()
16105 .cloned()
16106 .map(|entry| (raw_name.clone(), entry)),
16107 );
16108 normalized
16109 .entry(normalized_name)
16110 .or_default()
16111 .extend(values);
16112 }
16113
16114 Ok(HttpHeaderCollection {
16115 normalized,
16116 raw_pairs,
16117 })
16118}
16119
16120fn http_headers_json(headers: &HttpHeaderCollection) -> Value {
16121 let map = headers
16122 .normalized
16123 .iter()
16124 .map(|(name, values)| {
16125 let value = if values.len() == 1 {
16126 Value::String(values[0].clone())
16127 } else {
16128 Value::Array(values.iter().cloned().map(Value::String).collect())
16129 };
16130 (name.clone(), value)
16131 })
16132 .collect::<Map<String, Value>>();
16133 Value::Object(map)
16134}
16135
16136fn http_raw_headers_json(headers: &HttpHeaderCollection) -> Value {
16137 Value::Array(
16138 headers
16139 .raw_pairs
16140 .iter()
16141 .flat_map(|(name, value)| [Value::String(name.clone()), Value::String(value.clone())])
16142 .collect(),
16143 )
16144}
16145
16146fn is_loopback_request_host(host: &str) -> bool {
16147 let bare = host
16148 .strip_prefix('[')
16149 .and_then(|value| value.strip_suffix(']'))
16150 .unwrap_or(host);
16151 matches!(bare, "localhost" | "127.0.0.1" | "::1")
16152}
16153
16154fn serialize_http_loopback_request(
16155 url: &Url,
16156 options: &JavascriptHttpRequestOptions,
16157 headers: &HttpHeaderCollection,
16158) -> Result<String, SidecarError> {
16159 let body_base64 = options
16160 .body
16161 .as_ref()
16162 .map(|body| base64::engine::general_purpose::STANDARD.encode(body.as_bytes()));
16163 serde_json::to_string(&json!({
16164 "method": options.method.clone().unwrap_or_else(|| String::from("GET")),
16165 "url": http_request_target(url),
16166 "headers": http_headers_json(headers),
16167 "rawHeaders": http_raw_headers_json(headers),
16168 "bodyBase64": body_base64,
16169 }))
16170 .map_err(|error| SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}")))
16171}
16172
16173fn http_request_target(url: &Url) -> String {
16174 let path = if url.path().is_empty() {
16175 "/"
16176 } else {
16177 url.path()
16178 };
16179 format!(
16180 "{path}{}",
16181 url.query()
16182 .map(|query| format!("?{query}"))
16183 .unwrap_or_default()
16184 )
16185}
16186
16187fn outbound_http_response_json(url: &Url, response: ureq::Response) -> Result<Value, SidecarError> {
16188 let status = response.status();
16189 let status_text = response.status_text().to_owned();
16190 let mut header_pairs = Vec::new();
16191 let mut raw_headers = Vec::new();
16192 for raw_name in response.headers_names() {
16193 for value in response.all(&raw_name) {
16194 header_pairs.push(json!([raw_name.to_ascii_lowercase(), value]));
16195 raw_headers.push(Value::String(raw_name.clone()));
16196 raw_headers.push(Value::String(value.to_owned()));
16197 }
16198 }
16199 let mut reader = response.into_reader();
16200 let mut body = Vec::new();
16201 reader.read_to_end(&mut body).map_err(|error| {
16202 SidecarError::Execution(format!("failed to read HTTP response: {error}"))
16203 })?;
16204 serde_json::to_string(&json!({
16205 "status": status,
16206 "statusText": status_text,
16207 "headers": header_pairs,
16208 "rawHeaders": raw_headers,
16209 "body": base64::engine::general_purpose::STANDARD.encode(body),
16210 "bodyEncoding": "base64",
16211 "url": url.as_str(),
16212 }))
16213 .map(Value::String)
16214 .map_err(|error| SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}")))
16215}
16216
16217fn split_netloc(netloc: &str) -> Option<(&str, u16)> {
16221 let (host, port) = netloc.rsplit_once(':')?;
16222 let port: u16 = port.parse().ok()?;
16223 let host = host
16224 .strip_prefix('[')
16225 .and_then(|rest| rest.strip_suffix(']'))
16226 .unwrap_or(host);
16227 Some((host, port))
16228}
16229
16230fn issue_outbound_http_request(
16231 url: &Url,
16232 options: &JavascriptHttpRequestOptions,
16233 headers: &HttpHeaderCollection,
16234 pinned_addresses: &[IpAddr],
16235) -> Result<Value, SidecarError> {
16236 let method = options.method.as_deref().unwrap_or("GET");
16237 let pinned_host = url.host_str().map(str::to_owned);
16246 let pinned: Vec<IpAddr> = pinned_addresses.to_vec();
16247 let resolver = move |netloc: &str| -> std::io::Result<Vec<SocketAddr>> {
16248 let (host, port) = split_netloc(netloc).ok_or_else(|| {
16249 std::io::Error::new(
16250 std::io::ErrorKind::InvalidInput,
16251 format!("invalid network location: {netloc}"),
16252 )
16253 })?;
16254 let expected_host = pinned_host.as_deref();
16255 if expected_host != Some(host) {
16256 return Err(std::io::Error::new(
16257 std::io::ErrorKind::PermissionDenied,
16258 format!(
16259 "EACCES: outbound HTTP resolver pinned to {expected_host:?}, refusing {host}"
16260 ),
16261 ));
16262 }
16263 if pinned.is_empty() {
16264 return Err(std::io::Error::new(
16265 std::io::ErrorKind::PermissionDenied,
16266 "EACCES: no egress-vetted address available for outbound HTTP request",
16267 ));
16268 }
16269 Ok(pinned.iter().map(|ip| SocketAddr::new(*ip, port)).collect())
16270 };
16271 let mut agent_builder = ureq::AgentBuilder::new()
16272 .resolver(resolver)
16273 .timeout_connect(Duration::from_secs(5))
16274 .timeout_read(Duration::from_secs(15))
16275 .timeout_write(Duration::from_secs(15));
16276 if url.scheme() == "https" {
16277 let tls_options = JavascriptTlsBridgeOptions {
16278 is_server: false,
16279 servername: url.host_str().map(str::to_owned),
16280 alpn_protocols: Some(vec![String::from("http/1.1")]),
16281 reject_unauthorized: options.reject_unauthorized,
16282 ..JavascriptTlsBridgeOptions::default()
16283 };
16284 agent_builder = agent_builder.tls_config(Arc::new(build_client_tls_config(&tls_options)?));
16285 }
16286 let agent = agent_builder.build();
16287 let mut request = agent.request_url(method, url);
16288 for (name, values) in &headers.normalized {
16289 if name == "host" {
16290 continue;
16291 }
16292 let header_value = values.join(", ");
16293 request = request.set(name, &header_value);
16294 }
16295 let response = match options.body.as_deref() {
16296 Some(body) => request.send_string(body),
16297 None => request.call(),
16298 };
16299
16300 match response {
16301 Ok(response) => outbound_http_response_json(url, response),
16302 Err(ureq::Error::Status(_, response)) => outbound_http_response_json(url, response),
16303 Err(ureq::Error::Transport(error)) => Err(SidecarError::Execution(format!(
16304 "ERR_HTTP_REQUEST_FAILED: {error}"
16305 ))),
16306 }
16307}
16308
16309fn wait_for_loopback_http_response<B>(
16310 request: LoopbackHttpResponseWaitRequest<'_, B>,
16311) -> Result<String, SidecarError>
16312where
16313 B: NativeSidecarBridge + Send + 'static,
16314 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16315{
16316 let LoopbackHttpResponseWaitRequest {
16317 bridge,
16318 vm_id,
16319 dns,
16320 socket_paths,
16321 kernel,
16322 process,
16323 resource_limits,
16324 request_key,
16325 } = request;
16326 let deadline = Instant::now() + HTTP_LOOPBACK_REQUEST_TIMEOUT;
16327 loop {
16328 if let Some(response) = process
16329 .pending_http_requests
16330 .get(&request_key)
16331 .and_then(|response| response.clone())
16332 {
16333 process.pending_http_requests.remove(&request_key);
16334 return Ok(response);
16335 }
16336
16337 if Instant::now() >= deadline {
16338 process.pending_http_requests.remove(&request_key);
16339 return Err(SidecarError::Execution(String::from(
16340 "HTTP loopback request timed out waiting for net.http_respond",
16341 )));
16342 }
16343
16344 let Some(event) = process
16345 .execution
16346 .poll_event_blocking(Duration::from_millis(10))
16347 .map_err(|error| SidecarError::Execution(error.to_string()))?
16348 else {
16349 continue;
16350 };
16351
16352 match event {
16353 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
16354 let network_counts = process.network_resource_counts();
16355 let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
16356 bridge,
16357 vm_id,
16358 dns,
16359 socket_paths,
16360 kernel,
16361 process,
16362 sync_request: &request,
16363 resource_limits,
16364 network_counts,
16365 });
16366 match response {
16367 Ok(result) => process
16368 .execution
16369 .respond_javascript_sync_rpc_success(request.id, result)
16370 .or_else(ignore_stale_javascript_sync_rpc_response)?,
16371 Err(error) => process
16372 .execution
16373 .respond_javascript_sync_rpc_error(
16374 request.id,
16375 javascript_sync_rpc_error_code(&error),
16376 error.to_string(),
16377 )
16378 .or_else(ignore_stale_javascript_sync_rpc_response)?,
16379 }
16380 }
16381 ActiveExecutionEvent::Exited(code) => {
16382 process.pending_http_requests.remove(&request_key);
16383 return Err(SidecarError::Execution(format!(
16384 "HTTP loopback server exited before responding (exit code {code})"
16385 )));
16386 }
16387 ActiveExecutionEvent::Stdout(_)
16388 | ActiveExecutionEvent::Stderr(_)
16389 | ActiveExecutionEvent::PythonVfsRpcRequest(_)
16390 | ActiveExecutionEvent::SignalState { .. } => {}
16391 }
16392 }
16393}
16394
16395fn ensure_vm_fetch_response_within_limit(
16396 response_json: &str,
16397 operation: &str,
16398) -> Result<(), SidecarError> {
16399 let size = response_json.len();
16400 if size > VM_FETCH_BUFFER_LIMIT_BYTES {
16401 return Err(SidecarError::Execution(format!(
16402 "{operation} payload is {size} bytes, limit is {VM_FETCH_BUFFER_LIMIT_BYTES}"
16403 )));
16404 }
16405 Ok(())
16406}
16407
16408pub(crate) fn ensure_vm_fetch_response_frame_within_limit(
16409 response: &ResponseFrame,
16410 max_frame_bytes: usize,
16411) -> Result<(), SidecarError> {
16412 let max_frame_bytes = max_frame_bytes.min(VM_FETCH_BUFFER_LIMIT_BYTES);
16413 let frame = crate::protocol::to_generated_protocol_frame(
16414 &crate::protocol::ProtocolFrame::Response(response.clone()),
16415 )
16416 .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))?;
16417 let WireProtocolFrame::ResponseFrame(_) = &frame else {
16418 return Err(SidecarError::FrameTooLarge(String::from(
16419 "vm fetch response converted to non-response wire frame",
16420 )));
16421 };
16422 WireFrameCodec::new(max_frame_bytes)
16423 .encode(&frame)
16424 .map(|_| ())
16425 .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))
16426}
16427
16428fn service_javascript_dns_sync_rpc<B>(
16429 bridge: &SharedBridge<B>,
16430 kernel: &SidecarKernel,
16431 vm_id: &str,
16432 dns: &VmDnsConfig,
16433 request: &JavascriptSyncRpcRequest,
16434) -> Result<Value, SidecarError>
16435where
16436 B: NativeSidecarBridge + Send + 'static,
16437 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16438{
16439 match request.method.as_str() {
16440 "dns.lookup" => {
16441 let payload = request
16442 .args
16443 .first()
16444 .cloned()
16445 .ok_or_else(|| {
16446 SidecarError::InvalidState(String::from(
16447 "dns.lookup requires a request payload",
16448 ))
16449 })
16450 .and_then(|value| {
16451 serde_json::from_value::<JavascriptDnsLookupRequest>(value).map_err(|error| {
16452 SidecarError::InvalidState(format!("invalid dns.lookup payload: {error}"))
16453 })
16454 })?;
16455 let addresses = filter_dns_ip_addrs(
16456 resolve_dns_ip_addrs(
16457 bridge,
16458 kernel,
16459 vm_id,
16460 dns,
16461 &payload.hostname,
16462 DnsLookupPolicy::CheckPermissions,
16463 )?,
16464 payload.family,
16465 )?;
16466 let addresses = filter_dns_safe_ip_addrs(addresses, &payload.hostname)?;
16467 Ok(Value::Array(
16468 addresses
16469 .into_iter()
16470 .map(|ip| {
16471 json!({
16472 "address": ip.to_string(),
16473 "family": if ip.is_ipv6() { 6 } else { 4 },
16474 })
16475 })
16476 .collect(),
16477 ))
16478 }
16479 "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
16480 let payload = request
16481 .args
16482 .first()
16483 .cloned()
16484 .ok_or_else(|| {
16485 SidecarError::InvalidState(String::from(
16486 "dns.resolve requires a request payload",
16487 ))
16488 })
16489 .and_then(|value| {
16490 serde_json::from_value::<JavascriptDnsResolveRequest>(value).map_err(|error| {
16491 SidecarError::InvalidState(format!("invalid dns.resolve payload: {error}"))
16492 })
16493 })?;
16494 let requested_type = match request.method.as_str() {
16495 "dns.resolve4" => String::from("A"),
16496 "dns.resolve6" => String::from("AAAA"),
16497 _ => payload
16498 .rrtype
16499 .as_deref()
16500 .unwrap_or("A")
16501 .to_ascii_uppercase(),
16502 };
16503 let record_type = parse_dns_record_type(&requested_type)?;
16504 let resolution = resolve_dns_records(
16505 bridge,
16506 kernel,
16507 vm_id,
16508 dns,
16509 &payload.hostname,
16510 record_type,
16511 DnsLookupPolicy::CheckPermissions,
16512 )?;
16513 dns_resolution_to_node_value(&resolution, &requested_type)
16514 }
16515 other => Err(SidecarError::InvalidState(format!(
16516 "unsupported JavaScript dns sync RPC method {other}"
16517 ))),
16518 }
16519}
16520
16521fn service_javascript_dgram_sync_rpc<B>(
16522 request: JavascriptDgramSyncRpcServiceRequest<'_, B>,
16523) -> Result<Value, SidecarError>
16524where
16525 B: NativeSidecarBridge + Send + 'static,
16526 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16527{
16528 let JavascriptDgramSyncRpcServiceRequest {
16529 bridge,
16530 kernel,
16531 vm_id,
16532 dns,
16533 socket_paths,
16534 process,
16535 sync_request: request,
16536 resource_limits,
16537 network_counts,
16538 } = request;
16539 match request.method.as_str() {
16540 "dgram.createSocket" => {
16541 check_network_resource_limit(
16542 resource_limits.max_sockets,
16543 network_counts.sockets,
16544 1,
16545 "socket",
16546 )?;
16547 let payload = request
16548 .args
16549 .first()
16550 .cloned()
16551 .ok_or_else(|| {
16552 SidecarError::InvalidState(String::from(
16553 "dgram.createSocket requires a request payload",
16554 ))
16555 })
16556 .and_then(|value| {
16557 serde_json::from_value::<JavascriptDgramCreateSocketRequest>(value).map_err(
16558 |error| {
16559 SidecarError::InvalidState(format!(
16560 "invalid dgram.createSocket payload: {error}"
16561 ))
16562 },
16563 )
16564 })?;
16565 let family = JavascriptUdpFamily::from_socket_type(&payload.socket_type)?;
16566 let socket_id = process.allocate_udp_socket_id();
16567 process.udp_sockets.insert(
16568 socket_id.clone(),
16569 ActiveUdpSocket::new(kernel, process.kernel_pid, family)?,
16570 );
16571 Ok(json!({
16572 "socketId": socket_id,
16573 "type": family.socket_type(),
16574 }))
16575 }
16576 "dgram.bind" => {
16577 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.bind socket id")?;
16578 let payload = request
16579 .args
16580 .get(1)
16581 .cloned()
16582 .ok_or_else(|| {
16583 SidecarError::InvalidState(String::from(
16584 "dgram.bind requires a request payload",
16585 ))
16586 })
16587 .and_then(|value| {
16588 serde_json::from_value::<JavascriptDgramBindRequest>(value).map_err(|error| {
16589 SidecarError::InvalidState(format!("invalid dgram.bind payload: {error}"))
16590 })
16591 })?;
16592 let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
16593 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
16594 })?;
16595 let local_addr = socket.bind(
16596 kernel,
16597 process.kernel_pid,
16598 payload.address.as_deref(),
16599 payload.port,
16600 socket_paths,
16601 )?;
16602 Ok(json!({
16603 "localAddress": local_addr.ip().to_string(),
16604 "localPort": local_addr.port(),
16605 "family": socket_addr_family(&local_addr),
16606 }))
16607 }
16608 "dgram.send" => {
16609 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.send socket id")?;
16610 let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "dgram.send payload")?;
16611 let payload = request
16612 .args
16613 .get(2)
16614 .cloned()
16615 .ok_or_else(|| {
16616 SidecarError::InvalidState(String::from(
16617 "dgram.send requires a request payload",
16618 ))
16619 })
16620 .and_then(|value| {
16621 serde_json::from_value::<JavascriptDgramSendRequest>(value).map_err(|error| {
16622 SidecarError::InvalidState(format!("invalid dgram.send payload: {error}"))
16623 })
16624 })?;
16625 let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
16626 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
16627 })?;
16628 let (written, local_addr) = socket.send_to(ActiveUdpSendToRequest {
16629 bridge,
16630 kernel,
16631 kernel_pid: process.kernel_pid,
16632 vm_id,
16633 dns,
16634 host: payload.address.as_deref().unwrap_or("localhost"),
16635 port: payload.port,
16636 context: socket_paths,
16637 contents: &chunk,
16638 })?;
16639 Ok(json!({
16640 "bytes": written,
16641 "localAddress": local_addr.ip().to_string(),
16642 "localPort": local_addr.port(),
16643 "family": socket_addr_family(&local_addr),
16644 }))
16645 }
16646 "dgram.poll" => {
16647 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.poll socket id")?;
16648 let wait_ms =
16649 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "dgram.poll wait ms")?
16650 .unwrap_or_default();
16651 let event = {
16652 let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
16653 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
16654 })?;
16655 socket.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?
16656 };
16657
16658 match event {
16659 Some(JavascriptUdpSocketEvent::Message { data, remote_addr }) => {
16660 let family = JavascriptSocketFamily::from_ip(remote_addr.ip());
16661 let guest_remote_port = if is_loopback_ip(remote_addr.ip()) {
16662 socket_paths
16663 .guest_udp_port_for_host_port(family, remote_addr.port())
16664 .unwrap_or(remote_addr.port())
16665 } else {
16666 remote_addr.port()
16667 };
16668 Ok(json!({
16669 "type": "message",
16670 "data": javascript_sync_rpc_bytes_value(&data),
16671 "remoteAddress": remote_addr.ip().to_string(),
16672 "remotePort": guest_remote_port,
16673 "remoteFamily": socket_addr_family(&remote_addr),
16674 }))
16675 }
16676 Some(JavascriptUdpSocketEvent::Error { code, message }) => Ok(json!({
16677 "type": "error",
16678 "code": code,
16679 "message": message,
16680 })),
16681 None => Ok(Value::Null),
16682 }
16683 }
16684 "dgram.close" => {
16685 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.close socket id")?;
16686 let mut socket = process.udp_sockets.remove(socket_id).ok_or_else(|| {
16687 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
16688 })?;
16689 socket.close(kernel, process.kernel_pid);
16690 Ok(Value::Null)
16691 }
16692 "dgram.address" => {
16693 let socket_id =
16694 javascript_sync_rpc_arg_str(&request.args, 0, "dgram.address socket id")?;
16695 let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
16696 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
16697 })?;
16698 let local_addr = socket.local_addr().ok_or_else(|| {
16699 SidecarError::Execution(String::from("EBADF: bad file descriptor"))
16700 })?;
16701 javascript_net_json_string(
16702 json!({
16703 "address": local_addr.ip().to_string(),
16704 "port": local_addr.port(),
16705 "family": socket_addr_family(&local_addr),
16706 }),
16707 "dgram.address",
16708 )
16709 }
16710 "dgram.setBufferSize" => {
16711 let socket_id =
16712 javascript_sync_rpc_arg_str(&request.args, 0, "dgram.setBufferSize socket id")?;
16713 let which =
16714 javascript_sync_rpc_arg_str(&request.args, 1, "dgram.setBufferSize buffer kind")?;
16715 let size = javascript_sync_rpc_arg_u64(&request.args, 2, "dgram.setBufferSize size")?;
16716 let size = usize::try_from(size).map_err(|_| {
16717 SidecarError::InvalidState(String::from(
16718 "dgram.setBufferSize size must fit within usize",
16719 ))
16720 })?;
16721 let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
16722 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
16723 })?;
16724 socket.set_buffer_size(which, size)?;
16725 Ok(Value::Null)
16726 }
16727 "dgram.getBufferSize" => {
16728 let socket_id =
16729 javascript_sync_rpc_arg_str(&request.args, 0, "dgram.getBufferSize socket id")?;
16730 let which =
16731 javascript_sync_rpc_arg_str(&request.args, 1, "dgram.getBufferSize buffer kind")?;
16732 let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
16733 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
16734 })?;
16735 let size = socket.get_buffer_size(which)?;
16736 Ok(json!(size))
16737 }
16738 other => Err(SidecarError::InvalidState(format!(
16739 "unsupported JavaScript dgram sync RPC method {other}"
16740 ))),
16741 }
16742}
16743
16744#[derive(Debug)]
16745struct ClientHttp2StreamState {
16746 send_stream: Option<h2::SendStream<Bytes>>,
16747}
16748
16749#[derive(Debug)]
16750struct ServerHttp2StreamState {
16751 send_response: Option<ServerHttp2Responder>,
16752 send_stream: Option<h2::SendStream<Bytes>>,
16753}
16754
16755#[derive(Debug)]
16756enum ServerHttp2Responder {
16757 Regular(server::SendResponse<Bytes>),
16758 Pushed(server::SendPushedResponse<Bytes>),
16759}
16760
16761const HTTP2_DEFAULT_WINDOW_SIZE: u32 = 65_535;
16762const HTTP2_POLL_DELAY: Duration = Duration::from_millis(10);
16763
16764fn http2_runtime_snapshot() -> Http2RuntimeSnapshot {
16765 Http2RuntimeSnapshot {
16766 effective_local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
16767 local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
16768 remote_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
16769 next_stream_id: 1,
16770 outbound_queue_size: 1,
16771 deflate_dynamic_table_size: 0,
16772 inflate_dynamic_table_size: 0,
16773 }
16774}
16775
16776fn http2_snapshot_json(snapshot: &Http2SessionSnapshot) -> Result<String, SidecarError> {
16777 serde_json::to_string(snapshot)
16778 .map_err(|error| SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}")))
16779}
16780
16781fn http2_event_value(event: &Http2BridgeEvent) -> Result<Value, SidecarError> {
16782 serde_json::to_string(event)
16783 .map(Value::String)
16784 .map_err(|error| SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}")))
16785}
16786
16787fn push_http2_server_event(
16788 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
16789 server_id: u64,
16790 event: Http2BridgeEvent,
16791) {
16792 if let Ok(mut state) = shared.lock() {
16793 state
16794 .server_events
16795 .entry(server_id)
16796 .or_default()
16797 .push_back(event);
16798 }
16799}
16800
16801fn push_http2_session_event(
16802 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
16803 session_id: u64,
16804 event: Http2BridgeEvent,
16805) {
16806 if let Ok(mut state) = shared.lock() {
16807 state
16808 .session_events
16809 .entry(session_id)
16810 .or_default()
16811 .push_back(event);
16812 }
16813}
16814
16815fn pop_http2_event(
16816 queue: &mut BTreeMap<u64, VecDeque<Http2BridgeEvent>>,
16817 id: u64,
16818) -> Option<Http2BridgeEvent> {
16819 queue.get_mut(&id).and_then(VecDeque::pop_front)
16820}
16821
16822fn wait_for_http2_event(
16823 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
16824 id: u64,
16825 is_server: bool,
16826 wait_ms: u64,
16827) -> Option<Http2BridgeEvent> {
16828 let deadline = Instant::now() + Duration::from_millis(wait_ms);
16829 loop {
16830 if let Ok(mut state) = shared.lock() {
16831 let queue = if is_server {
16832 &mut state.server_events
16833 } else {
16834 &mut state.session_events
16835 };
16836 if let Some(event) = pop_http2_event(queue, id) {
16837 return Some(event);
16838 }
16839 }
16840 if wait_ms == 0 || Instant::now() >= deadline {
16841 return None;
16842 }
16843 thread::sleep(HTTP2_POLL_DELAY);
16844 }
16845}
16846
16847fn next_http2_session_id(shared: &mut crate::state::Http2SharedState) -> u64 {
16848 shared.next_session_id += 1;
16849 shared.next_session_id
16850}
16851
16852fn next_http2_stream_id(shared: &mut crate::state::Http2SharedState) -> u64 {
16853 shared.next_stream_id += 1;
16854 shared.next_stream_id
16855}
16856
16857fn http2_reason(code: Option<u32>) -> Reason {
16858 code.unwrap_or(Reason::NO_ERROR.into()).into()
16859}
16860
16861fn http2_error_payload(message: impl Into<String>) -> String {
16862 serde_json::to_string(&json!({
16863 "name": "Error",
16864 "code": "ERR_HTTP2_ERROR",
16865 "message": message.into(),
16866 }))
16867 .unwrap_or_else(|_| {
16868 String::from(
16869 "{\"name\":\"Error\",\"code\":\"ERR_HTTP2_ERROR\",\"message\":\"HTTP/2 bridge error\"}",
16870 )
16871 })
16872}
16873
16874fn http2_socket_snapshot(local_addr: SocketAddr, remote_addr: SocketAddr) -> Http2SocketSnapshot {
16875 Http2SocketSnapshot {
16876 encrypted: false,
16877 allow_half_open: false,
16878 local_address: Some(local_addr.ip().to_string()),
16879 local_port: Some(local_addr.port()),
16880 local_family: Some(socket_addr_family(&local_addr).to_string()),
16881 remote_address: Some(remote_addr.ip().to_string()),
16882 remote_port: Some(remote_addr.port()),
16883 remote_family: Some(socket_addr_family(&remote_addr).to_string()),
16884 servername: None,
16885 alpn_protocol: Some(String::from("h2c")),
16886 }
16887}
16888
16889fn http2_wait_result(kind: &str, id: u64) -> Value {
16890 json!({
16891 "kind": kind,
16892 "id": id,
16893 })
16894}
16895
16896fn is_http2_terminal_event(event: &Http2BridgeEvent, is_server: bool, id: u64) -> bool {
16897 if is_server {
16898 event.kind == "serverClose" && event.id == id
16899 } else {
16900 event.kind == "sessionClose" && event.id == id
16901 }
16902}
16903
16904fn dispatch_http2_wait_loop(
16905 process: &ActiveProcess,
16906 id: u64,
16907 is_server: bool,
16908) -> Result<Value, SidecarError> {
16909 loop {
16910 if let Some(event) = wait_for_http2_event(&process.http2.shared, id, is_server, 50) {
16911 let payload = serde_json::to_value(&event).map_err(|error| {
16912 SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}"))
16913 })?;
16914 process
16915 .execution
16916 .send_javascript_stream_event("http2", payload.clone())?;
16917 if is_http2_terminal_event(&event, is_server, id) {
16918 return Ok(payload);
16919 }
16920 continue;
16921 }
16922
16923 let exists = process
16924 .http2
16925 .shared
16926 .lock()
16927 .map(|state| {
16928 if is_server {
16929 state.servers.contains_key(&id)
16930 } else {
16931 state.sessions.contains_key(&id)
16932 }
16933 })
16934 .unwrap_or(false);
16935 if !exists {
16936 return Ok(if is_server {
16937 http2_wait_result("serverClose", id)
16938 } else {
16939 http2_wait_result("sessionClose", id)
16940 });
16941 }
16942 }
16943}
16944
16945fn dispatch_http_wait_loop(process: &ActiveProcess, server_id: u64) -> Result<Value, SidecarError> {
16946 loop {
16947 if !process.http_servers.contains_key(&server_id) {
16948 return Ok(json!({
16949 "kind": "serverClose",
16950 "id": server_id,
16951 }));
16952 }
16953 thread::sleep(Duration::from_millis(25));
16954 }
16955}
16956
16957fn http2_settings_from_value(settings: &BTreeMap<String, Value>) -> BTreeMap<String, Value> {
16958 settings.clone()
16959}
16960
16961fn parse_http2_headers_json(
16962 headers_json: &str,
16963 label: &str,
16964) -> Result<BTreeMap<String, Value>, SidecarError> {
16965 serde_json::from_str::<BTreeMap<String, Value>>(headers_json)
16966 .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
16967}
16968
16969fn apply_http2_header_values(
16970 header_map: &mut HeaderMap,
16971 name: &str,
16972 value: &Value,
16973) -> Result<(), SidecarError> {
16974 let header_name = HeaderName::from_bytes(name.as_bytes()).map_err(|error| {
16975 SidecarError::InvalidState(format!("invalid HTTP/2 header name {name:?}: {error}"))
16976 })?;
16977 match value {
16978 Value::Array(values) => {
16979 for value in values {
16980 apply_http2_header_values(header_map, name, value)?;
16981 }
16982 }
16983 Value::String(text) => {
16984 let value = HeaderValue::from_str(text).map_err(|error| {
16985 SidecarError::InvalidState(format!(
16986 "invalid HTTP/2 header value for {name}: {error}"
16987 ))
16988 })?;
16989 header_map.append(header_name.clone(), value);
16990 }
16991 Value::Number(number) => {
16992 let value = HeaderValue::from_str(&number.to_string()).map_err(|error| {
16993 SidecarError::InvalidState(format!(
16994 "invalid HTTP/2 numeric header value for {name}: {error}"
16995 ))
16996 })?;
16997 header_map.append(header_name.clone(), value);
16998 }
16999 Value::Bool(boolean) => {
17000 let value = HeaderValue::from_str(if *boolean { "true" } else { "false" }).map_err(
17001 |error| {
17002 SidecarError::InvalidState(format!(
17003 "invalid HTTP/2 boolean header value for {name}: {error}"
17004 ))
17005 },
17006 )?;
17007 header_map.append(header_name.clone(), value);
17008 }
17009 Value::Null => {}
17010 Value::Object(_) => {
17011 return Err(SidecarError::InvalidState(format!(
17012 "unsupported HTTP/2 header object value for {name}"
17013 )));
17014 }
17015 }
17016 Ok(())
17017}
17018
17019fn build_http2_request(headers_json: &str) -> Result<Request<()>, SidecarError> {
17020 let headers = parse_http2_headers_json(headers_json, "HTTP/2 request headers")?;
17021 let method = headers
17022 .get(":method")
17023 .and_then(Value::as_str)
17024 .unwrap_or("GET");
17025 let path = headers.get(":path").and_then(Value::as_str).unwrap_or("/");
17026 let mut builder = Request::builder()
17027 .method(Method::from_bytes(method.as_bytes()).map_err(|error| {
17028 SidecarError::InvalidState(format!("invalid HTTP/2 method {method:?}: {error}"))
17029 })?)
17030 .uri(path.parse::<Uri>().map_err(|error| {
17031 SidecarError::InvalidState(format!("invalid HTTP/2 path {path:?}: {error}"))
17032 })?);
17033 {
17034 let header_map = builder.headers_mut().expect("request header map");
17035 for (name, value) in &headers {
17036 if name.starts_with(':') {
17037 continue;
17038 }
17039 apply_http2_header_values(header_map, name, value)?;
17040 }
17041 }
17042 builder
17043 .body(())
17044 .map_err(|error| SidecarError::InvalidState(format!("invalid HTTP/2 request: {error}")))
17045}
17046
17047fn build_http2_response(headers_json: &str) -> Result<Response<()>, SidecarError> {
17048 let headers = parse_http2_headers_json(headers_json, "HTTP/2 response headers")?;
17049 let status = headers
17050 .get(":status")
17051 .and_then(Value::as_u64)
17052 .or_else(|| {
17053 headers
17054 .get(":status")
17055 .and_then(Value::as_str)
17056 .and_then(|value| value.parse::<u16>().ok().map(u64::from))
17057 })
17058 .unwrap_or(200);
17059 let mut builder = Response::builder().status(status as u16);
17060 {
17061 let header_map = builder.headers_mut().expect("response header map");
17062 for (name, value) in &headers {
17063 if name.starts_with(':') {
17064 continue;
17065 }
17066 apply_http2_header_values(header_map, name, value)?;
17067 }
17068 }
17069 builder.body(()).map_err(|error| {
17070 SidecarError::InvalidState(format!("invalid HTTP/2 response headers: {error}"))
17071 })
17072}
17073
17074fn serialize_http2_headers_map(
17075 pseudo: BTreeMap<String, Value>,
17076 headers: &HeaderMap,
17077) -> Result<String, SidecarError> {
17078 let mut serialized = pseudo;
17079 for (name, value) in headers {
17080 let name = name.as_str().to_string();
17081 let value = Value::String(
17082 value
17083 .to_str()
17084 .map_err(|error| {
17085 SidecarError::Execution(format!("invalid HTTP/2 header value: {error}"))
17086 })?
17087 .to_owned(),
17088 );
17089 match serialized.get_mut(&name) {
17090 Some(Value::Array(values)) => values.push(value),
17091 Some(existing) => {
17092 let first = existing.clone();
17093 *existing = Value::Array(vec![first, value]);
17094 }
17095 None => {
17096 serialized.insert(name, value);
17097 }
17098 }
17099 }
17100 serde_json::to_string(&serialized)
17101 .map_err(|error| SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}")))
17102}
17103
17104fn serialize_http2_request_headers(
17105 request: &Request<h2::RecvStream>,
17106) -> Result<String, SidecarError> {
17107 let mut pseudo = BTreeMap::new();
17108 pseudo.insert(
17109 String::from(":method"),
17110 Value::String(request.method().as_str().to_string()),
17111 );
17112 pseudo.insert(
17113 String::from(":path"),
17114 Value::String(
17115 request
17116 .uri()
17117 .path_and_query()
17118 .map(|value| value.as_str().to_string())
17119 .unwrap_or_else(|| String::from("/")),
17120 ),
17121 );
17122 serialize_http2_headers_map(pseudo, request.headers())
17123}
17124
17125fn serialize_http2_response_headers(
17126 response: &Response<h2::RecvStream>,
17127) -> Result<String, SidecarError> {
17128 let mut pseudo = BTreeMap::new();
17129 pseudo.insert(
17130 String::from(":status"),
17131 Value::Number(serde_json::Number::from(response.status().as_u16())),
17132 );
17133 serialize_http2_headers_map(pseudo, response.headers())
17134}
17135
17136fn remove_http2_session_resources(
17137 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17138 session_id: u64,
17139) {
17140 if let Ok(mut state) = shared.lock() {
17141 state.sessions.remove(&session_id);
17142 state.session_events.remove(&session_id);
17143 let stream_ids = state
17144 .streams
17145 .iter()
17146 .filter_map(|(stream_id, stream)| {
17147 (stream.session_id == session_id).then_some(*stream_id)
17148 })
17149 .collect::<Vec<_>>();
17150 for stream_id in stream_ids {
17151 state.streams.remove(&stream_id);
17152 }
17153 }
17154}
17155
17156fn spawn_http2_client_session(
17157 shared: Arc<Mutex<crate::state::Http2SharedState>>,
17158 session_id: u64,
17159 remote_addr: SocketAddr,
17160 tls: Option<JavascriptTlsBridgeOptions>,
17161 snapshot: Arc<Mutex<Http2SessionSnapshot>>,
17162 mut command_rx: UnboundedReceiver<Http2SessionCommand>,
17163) {
17164 thread::spawn(move || {
17165 let runtime = match TokioRuntimeBuilder::new_current_thread()
17166 .enable_all()
17167 .build()
17168 {
17169 Ok(runtime) => runtime,
17170 Err(error) => {
17171 push_http2_session_event(
17172 &shared,
17173 session_id,
17174 Http2BridgeEvent {
17175 kind: String::from("sessionError"),
17176 id: session_id,
17177 data: Some(http2_error_payload(error.to_string())),
17178 ..Http2BridgeEvent::default()
17179 },
17180 );
17181 remove_http2_session_resources(&shared, session_id);
17182 return;
17183 }
17184 };
17185
17186 runtime.block_on(async move {
17187 let stream = match tokio::net::TcpStream::connect(remote_addr).await {
17188 Ok(stream) => stream,
17189 Err(error) => {
17190 push_http2_session_event(
17191 &shared,
17192 session_id,
17193 Http2BridgeEvent {
17194 kind: String::from("sessionError"),
17195 id: session_id,
17196 data: Some(http2_error_payload(error.to_string())),
17197 ..Http2BridgeEvent::default()
17198 },
17199 );
17200 remove_http2_session_resources(&shared, session_id);
17201 return;
17202 }
17203 };
17204
17205 let local_addr = match stream.local_addr() {
17206 Ok(addr) => addr,
17207 Err(error) => {
17208 push_http2_session_event(
17209 &shared,
17210 session_id,
17211 Http2BridgeEvent {
17212 kind: String::from("sessionError"),
17213 id: session_id,
17214 data: Some(http2_error_payload(error.to_string())),
17215 ..Http2BridgeEvent::default()
17216 },
17217 );
17218 remove_http2_session_resources(&shared, session_id);
17219 return;
17220 }
17221 };
17222
17223 {
17224 let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
17225 snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
17226 if let Some(options) = tls.as_ref() {
17227 snapshot_guard.encrypted = true;
17228 snapshot_guard.alpn_protocol = Some(String::from("h2"));
17229 snapshot_guard.socket.encrypted = true;
17230 snapshot_guard.socket.servername = options.servername.clone();
17231 snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
17232 }
17233 snapshot_guard.state = http2_runtime_snapshot();
17234 }
17235 if let Ok(snapshot_json) =
17236 http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
17237 {
17238 push_http2_session_event(
17239 &shared,
17240 session_id,
17241 Http2BridgeEvent {
17242 kind: String::from("sessionConnect"),
17243 id: session_id,
17244 data: Some(snapshot_json),
17245 ..Http2BridgeEvent::default()
17246 },
17247 );
17248 }
17249
17250 let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
17251 let server_name = match ServerName::try_from(
17252 options
17253 .servername
17254 .clone()
17255 .unwrap_or_else(|| String::from("localhost")),
17256 ) {
17257 Ok(server_name) => server_name,
17258 Err(_) => {
17259 push_http2_session_event(
17260 &shared,
17261 session_id,
17262 Http2BridgeEvent {
17263 kind: String::from("sessionError"),
17264 id: session_id,
17265 data: Some(http2_error_payload("invalid TLS servername")),
17266 ..Http2BridgeEvent::default()
17267 },
17268 );
17269 remove_http2_session_resources(&shared, session_id);
17270 return;
17271 }
17272 };
17273 let connector = match build_client_tls_config(options) {
17274 Ok(config) => TlsConnector::from(Arc::new(config)),
17275 Err(error) => {
17276 push_http2_session_event(
17277 &shared,
17278 session_id,
17279 Http2BridgeEvent {
17280 kind: String::from("sessionError"),
17281 id: session_id,
17282 data: Some(http2_error_payload(error.to_string())),
17283 ..Http2BridgeEvent::default()
17284 },
17285 );
17286 remove_http2_session_resources(&shared, session_id);
17287 return;
17288 }
17289 };
17290 match connector.connect(server_name, stream).await {
17291 Ok(tls_stream) => Box::pin(tls_stream),
17292 Err(error) => {
17293 push_http2_session_event(
17294 &shared,
17295 session_id,
17296 Http2BridgeEvent {
17297 kind: String::from("sessionError"),
17298 id: session_id,
17299 data: Some(http2_error_payload(error.to_string())),
17300 ..Http2BridgeEvent::default()
17301 },
17302 );
17303 remove_http2_session_resources(&shared, session_id);
17304 return;
17305 }
17306 }
17307 } else {
17308 Box::pin(stream)
17309 };
17310
17311 let (mut sender, connection) = match client::handshake(io).await {
17312 Ok(parts) => parts,
17313 Err(error) => {
17314 push_http2_session_event(
17315 &shared,
17316 session_id,
17317 Http2BridgeEvent {
17318 kind: String::from("sessionError"),
17319 id: session_id,
17320 data: Some(http2_error_payload(error.to_string())),
17321 ..Http2BridgeEvent::default()
17322 },
17323 );
17324 remove_http2_session_resources(&shared, session_id);
17325 return;
17326 }
17327 };
17328
17329 let (status_tx, mut status_rx) = unbounded_channel::<Result<(), String>>();
17330 tokio::spawn(async move {
17331 let _ = status_tx.send(connection.await.map_err(|error| error.to_string()));
17332 });
17333
17334 let streams: Arc<Mutex<BTreeMap<u64, ClientHttp2StreamState>>> =
17335 Arc::new(Mutex::new(BTreeMap::new()));
17336
17337 loop {
17338 tokio::select! {
17339 Some(result) = status_rx.recv() => {
17340 if let Err(message) = result {
17341 push_http2_session_event(
17342 &shared,
17343 session_id,
17344 Http2BridgeEvent {
17345 kind: String::from("sessionError"),
17346 id: session_id,
17347 data: Some(http2_error_payload(message)),
17348 ..Http2BridgeEvent::default()
17349 },
17350 );
17351 }
17352 push_http2_session_event(
17353 &shared,
17354 session_id,
17355 Http2BridgeEvent {
17356 kind: String::from("sessionClose"),
17357 id: session_id,
17358 ..Http2BridgeEvent::default()
17359 },
17360 );
17361 remove_http2_session_resources(&shared, session_id);
17362 break;
17363 }
17364 Some(command) = command_rx.recv() => {
17365 match command {
17366 Http2SessionCommand::Request { headers_json, options_json, respond_to } => {
17367 let request = match build_http2_request(&headers_json) {
17368 Ok(request) => request,
17369 Err(error) => {
17370 let _ = respond_to.send(Err(error.to_string()));
17371 continue;
17372 }
17373 };
17374 let options: JavascriptHttp2RequestOptions =
17375 serde_json::from_str(&options_json).unwrap_or_default();
17376 let stream_id = {
17377 let mut state = shared.lock().expect("http2 shared state");
17378 let stream_id = next_http2_stream_id(&mut state);
17379 state.streams.insert(
17380 stream_id,
17381 ActiveHttp2Stream {
17382 session_id,
17383 paused: Arc::new(AtomicBool::new(false)),
17384 },
17385 );
17386 stream_id
17387 };
17388 match sender.send_request(request, options.end_stream) {
17389 Ok((response_future, send_stream)) => {
17390 if !options.end_stream {
17391 streams
17392 .lock()
17393 .expect("http2 client streams")
17394 .insert(stream_id, ClientHttp2StreamState { send_stream: Some(send_stream) });
17395 }
17396 let shared_clone = Arc::clone(&shared);
17397 let snapshot_clone = Arc::clone(&snapshot);
17398 tokio::spawn(async move {
17399 match response_future.await {
17400 Ok(response) => {
17401 if let Ok(headers_json) = serialize_http2_response_headers(&response) {
17402 push_http2_session_event(
17403 &shared_clone,
17404 session_id,
17405 Http2BridgeEvent {
17406 kind: String::from("clientResponseHeaders"),
17407 id: stream_id,
17408 data: Some(headers_json),
17409 ..Http2BridgeEvent::default()
17410 },
17411 );
17412 }
17413 let mut body = response.into_body();
17414 while let Some(chunk) = body.data().await {
17415 match chunk {
17416 Ok(bytes) => {
17417 let paused = {
17418 let state = shared_clone.lock().expect("http2 shared state");
17419 state.streams.get(&stream_id).map(|stream| Arc::clone(&stream.paused))
17420 };
17421 if let Some(paused) = paused {
17422 while paused.load(Ordering::SeqCst) {
17423 tokio::time::sleep(HTTP2_POLL_DELAY).await;
17424 }
17425 }
17426 let _ = body.flow_control().release_capacity(bytes.len());
17427 push_http2_session_event(
17428 &shared_clone,
17429 session_id,
17430 Http2BridgeEvent {
17431 kind: String::from("clientData"),
17432 id: stream_id,
17433 data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
17434 ..Http2BridgeEvent::default()
17435 },
17436 );
17437 }
17438 Err(error) => {
17439 push_http2_session_event(
17440 &shared_clone,
17441 session_id,
17442 Http2BridgeEvent {
17443 kind: String::from("clientError"),
17444 id: stream_id,
17445 data: Some(http2_error_payload(error.to_string())),
17446 ..Http2BridgeEvent::default()
17447 },
17448 );
17449 break;
17450 }
17451 }
17452 }
17453 {
17454 let mut snapshot = snapshot_clone.lock().expect("http2 snapshot lock");
17455 snapshot.state.next_stream_id =
17456 snapshot.state.next_stream_id.saturating_add(2);
17457 }
17458 push_http2_session_event(
17459 &shared_clone,
17460 session_id,
17461 Http2BridgeEvent {
17462 kind: String::from("clientEnd"),
17463 id: stream_id,
17464 ..Http2BridgeEvent::default()
17465 },
17466 );
17467 push_http2_session_event(
17468 &shared_clone,
17469 session_id,
17470 Http2BridgeEvent {
17471 kind: String::from("clientClose"),
17472 id: stream_id,
17473 extra_number: Some(0),
17474 ..Http2BridgeEvent::default()
17475 },
17476 );
17477 if let Ok(mut state) = shared_clone.lock() {
17478 state.streams.remove(&stream_id);
17479 }
17480 }
17481 Err(error) => {
17482 push_http2_session_event(
17483 &shared_clone,
17484 session_id,
17485 Http2BridgeEvent {
17486 kind: String::from("clientError"),
17487 id: stream_id,
17488 data: Some(http2_error_payload(error.to_string())),
17489 ..Http2BridgeEvent::default()
17490 },
17491 );
17492 push_http2_session_event(
17493 &shared_clone,
17494 session_id,
17495 Http2BridgeEvent {
17496 kind: String::from("clientClose"),
17497 id: stream_id,
17498 extra_number: Some(u32::from(Reason::INTERNAL_ERROR) as u64),
17499 ..Http2BridgeEvent::default()
17500 },
17501 );
17502 if let Ok(mut state) = shared_clone.lock() {
17503 state.streams.remove(&stream_id);
17504 }
17505 }
17506 }
17507 });
17508 let _ = respond_to.send(Ok(json!(stream_id)));
17509 }
17510 Err(error) => {
17511 if let Ok(mut state) = shared.lock() {
17512 state.streams.remove(&stream_id);
17513 }
17514 let _ = respond_to.send(Err(error.to_string()));
17515 }
17516 }
17517 }
17518 Http2SessionCommand::Settings { settings_json, respond_to } => {
17519 let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
17520 .unwrap_or_default();
17521 {
17522 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
17523 snapshot.local_settings = http2_settings_from_value(&settings);
17524 }
17525 if let Ok(headers_json) = serde_json::to_string(&settings) {
17526 push_http2_session_event(
17527 &shared,
17528 session_id,
17529 Http2BridgeEvent {
17530 kind: String::from("sessionLocalSettings"),
17531 id: session_id,
17532 data: Some(headers_json.clone()),
17533 ..Http2BridgeEvent::default()
17534 },
17535 );
17536 push_http2_session_event(
17537 &shared,
17538 session_id,
17539 Http2BridgeEvent {
17540 kind: String::from("sessionSettingsAck"),
17541 id: session_id,
17542 ..Http2BridgeEvent::default()
17543 },
17544 );
17545 }
17546 let _ = respond_to.send(Ok(Value::Null));
17547 }
17548 Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
17549 {
17550 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
17551 snapshot.state.local_window_size = size;
17552 snapshot.state.effective_local_window_size = size;
17553 }
17554 let value = snapshot
17555 .lock()
17556 .ok()
17557 .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
17558 .map(Value::String)
17559 .unwrap_or(Value::Null);
17560 let _ = respond_to.send(Ok(value));
17561 }
17562 Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
17563 push_http2_session_event(
17564 &shared,
17565 session_id,
17566 Http2BridgeEvent {
17567 kind: String::from("sessionGoaway"),
17568 id: session_id,
17569 data: opaque_data.map(|value| {
17570 base64::engine::general_purpose::STANDARD.encode(value)
17571 }),
17572 extra_number: Some(error_code as u64),
17573 flags: Some(last_stream_id as u64),
17574 ..Http2BridgeEvent::default()
17575 },
17576 );
17577 let _ = respond_to.send(Ok(Value::Null));
17578 }
17579 Http2SessionCommand::Close { respond_to, .. } => {
17580 let _ = respond_to.send(Ok(Value::Null));
17581 push_http2_session_event(
17582 &shared,
17583 session_id,
17584 Http2BridgeEvent {
17585 kind: String::from("sessionClose"),
17586 id: session_id,
17587 ..Http2BridgeEvent::default()
17588 },
17589 );
17590 remove_http2_session_resources(&shared, session_id);
17591 break;
17592 }
17593 Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
17594 let result = streams
17595 .lock()
17596 .expect("http2 client streams")
17597 .get_mut(&stream_id)
17598 .and_then(|stream| stream.send_stream.as_mut())
17599 .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 client stream {stream_id}")))
17600 .and_then(|stream| stream.send_data(Bytes::from(chunk), end_stream).map_err(|error| SidecarError::Execution(error.to_string())));
17601 match result {
17602 Ok(()) => {
17603 if end_stream {
17604 streams.lock().expect("http2 client streams").remove(&stream_id);
17605 }
17606 let _ = respond_to.send(Ok(Value::Bool(true)));
17607 }
17608 Err(error) => {
17609 let _ = respond_to.send(Err(error.to_string()));
17610 }
17611 }
17612 }
17613 Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
17614 let mut streams = streams.lock().expect("http2 client streams");
17615 let Some(mut state) = streams.remove(&stream_id) else {
17616 let _ = respond_to.send(Err(format!("unknown HTTP/2 client stream {stream_id}")));
17617 continue;
17618 };
17619 if let Some(stream) = state.send_stream.as_mut() {
17620 stream.send_reset(http2_reason(error_code));
17621 }
17622 if let Ok(mut state) = shared.lock() {
17623 state.streams.remove(&stream_id);
17624 }
17625 push_http2_session_event(
17626 &shared,
17627 session_id,
17628 Http2BridgeEvent {
17629 kind: String::from("clientClose"),
17630 id: stream_id,
17631 extra_number: Some(u32::from(http2_reason(error_code)) as u64),
17632 ..Http2BridgeEvent::default()
17633 },
17634 );
17635 let _ = respond_to.send(Ok(Value::Null));
17636 }
17637 Http2SessionCommand::StreamRespond { respond_to, .. }
17638 | Http2SessionCommand::StreamPush { respond_to, .. }
17639 | Http2SessionCommand::StreamRespondWithFile { respond_to, .. } => {
17640 let _ = respond_to.send(Err(String::from("HTTP/2 client streams cannot send server responses")));
17641 }
17642 }
17643 }
17644 else => break,
17645 }
17646 }
17647 });
17648 });
17649}
17650
17651fn spawn_http2_server_session(
17652 shared: Arc<Mutex<crate::state::Http2SharedState>>,
17653 server_id: u64,
17654 session_id: u64,
17655 stream: TcpStream,
17656 tls: Option<JavascriptTlsBridgeOptions>,
17657 snapshot: Arc<Mutex<Http2SessionSnapshot>>,
17658 mut command_rx: UnboundedReceiver<Http2SessionCommand>,
17659) {
17660 thread::spawn(move || {
17661 let runtime = match TokioRuntimeBuilder::new_current_thread()
17662 .enable_all()
17663 .build()
17664 {
17665 Ok(runtime) => runtime,
17666 Err(error) => {
17667 push_http2_server_event(
17668 &shared,
17669 server_id,
17670 Http2BridgeEvent {
17671 kind: String::from("serverStreamError"),
17672 id: session_id,
17673 data: Some(http2_error_payload(error.to_string())),
17674 ..Http2BridgeEvent::default()
17675 },
17676 );
17677 remove_http2_session_resources(&shared, session_id);
17678 return;
17679 }
17680 };
17681
17682 runtime.block_on(async move {
17683 if let Err(error) = stream.set_nonblocking(true) {
17684 push_http2_server_event(
17685 &shared,
17686 server_id,
17687 Http2BridgeEvent {
17688 kind: String::from("serverStreamError"),
17689 id: session_id,
17690 data: Some(http2_error_payload(error.to_string())),
17691 ..Http2BridgeEvent::default()
17692 },
17693 );
17694 remove_http2_session_resources(&shared, session_id);
17695 return;
17696 }
17697 let stream = match tokio::net::TcpStream::from_std(stream) {
17698 Ok(stream) => stream,
17699 Err(error) => {
17700 push_http2_server_event(
17701 &shared,
17702 server_id,
17703 Http2BridgeEvent {
17704 kind: String::from("serverStreamError"),
17705 id: session_id,
17706 data: Some(http2_error_payload(error.to_string())),
17707 ..Http2BridgeEvent::default()
17708 },
17709 );
17710 remove_http2_session_resources(&shared, session_id);
17711 return;
17712 }
17713 };
17714 let local_addr = match stream.local_addr() {
17715 Ok(addr) => addr,
17716 Err(error) => {
17717 push_http2_server_event(
17718 &shared,
17719 server_id,
17720 Http2BridgeEvent {
17721 kind: String::from("serverStreamError"),
17722 id: session_id,
17723 data: Some(http2_error_payload(error.to_string())),
17724 ..Http2BridgeEvent::default()
17725 },
17726 );
17727 remove_http2_session_resources(&shared, session_id);
17728 return;
17729 }
17730 };
17731 let remote_addr = match stream.peer_addr() {
17732 Ok(addr) => addr,
17733 Err(error) => {
17734 push_http2_server_event(
17735 &shared,
17736 server_id,
17737 Http2BridgeEvent {
17738 kind: String::from("serverStreamError"),
17739 id: session_id,
17740 data: Some(http2_error_payload(error.to_string())),
17741 ..Http2BridgeEvent::default()
17742 },
17743 );
17744 remove_http2_session_resources(&shared, session_id);
17745 return;
17746 }
17747 };
17748 {
17749 let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
17750 snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
17751 if tls.is_some() {
17752 snapshot_guard.encrypted = true;
17753 snapshot_guard.alpn_protocol = Some(String::from("h2"));
17754 snapshot_guard.socket.encrypted = true;
17755 snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
17756 }
17757 snapshot_guard.state = http2_runtime_snapshot();
17758 }
17759 if let Ok(snapshot_json) =
17760 http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
17761 {
17762 push_http2_server_event(
17763 &shared,
17764 server_id,
17765 Http2BridgeEvent {
17766 kind: String::from(if tls.is_some() {
17767 "serverSecureConnection"
17768 } else {
17769 "serverConnection"
17770 }),
17771 id: server_id,
17772 data: Some(serde_json::to_string(&http2_socket_snapshot(local_addr, remote_addr)).unwrap_or_default()),
17773 ..Http2BridgeEvent::default()
17774 },
17775 );
17776 push_http2_server_event(
17777 &shared,
17778 server_id,
17779 Http2BridgeEvent {
17780 kind: String::from("serverSession"),
17781 id: server_id,
17782 data: Some(snapshot_json),
17783 extra_number: Some(session_id),
17784 ..Http2BridgeEvent::default()
17785 },
17786 );
17787 }
17788
17789 let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
17790 let acceptor = match build_server_tls_config(options) {
17791 Ok(config) => TlsAcceptor::from(Arc::new(config)),
17792 Err(error) => {
17793 push_http2_server_event(
17794 &shared,
17795 server_id,
17796 Http2BridgeEvent {
17797 kind: String::from("serverStreamError"),
17798 id: session_id,
17799 data: Some(http2_error_payload(error.to_string())),
17800 ..Http2BridgeEvent::default()
17801 },
17802 );
17803 remove_http2_session_resources(&shared, session_id);
17804 return;
17805 }
17806 };
17807 match acceptor.accept(stream).await {
17808 Ok(tls_stream) => Box::pin(tls_stream),
17809 Err(error) => {
17810 push_http2_server_event(
17811 &shared,
17812 server_id,
17813 Http2BridgeEvent {
17814 kind: String::from("serverStreamError"),
17815 id: session_id,
17816 data: Some(http2_error_payload(error.to_string())),
17817 ..Http2BridgeEvent::default()
17818 },
17819 );
17820 remove_http2_session_resources(&shared, session_id);
17821 return;
17822 }
17823 }
17824 } else {
17825 Box::pin(stream)
17826 };
17827
17828 let mut connection = match server::handshake(io).await {
17829 Ok(connection) => connection,
17830 Err(error) => {
17831 push_http2_server_event(
17832 &shared,
17833 server_id,
17834 Http2BridgeEvent {
17835 kind: String::from("serverStreamError"),
17836 id: session_id,
17837 data: Some(http2_error_payload(error.to_string())),
17838 ..Http2BridgeEvent::default()
17839 },
17840 );
17841 remove_http2_session_resources(&shared, session_id);
17842 return;
17843 }
17844 };
17845
17846 let streams: Arc<Mutex<BTreeMap<u64, ServerHttp2StreamState>>> =
17847 Arc::new(Mutex::new(BTreeMap::new()));
17848
17849 loop {
17850 tokio::select! {
17851 incoming = connection.accept() => {
17852 match incoming {
17853 Some(Ok((request, respond))) => {
17854 let headers_json = match serialize_http2_request_headers(&request) {
17855 Ok(headers) => headers,
17856 Err(error) => {
17857 push_http2_server_event(
17858 &shared,
17859 server_id,
17860 Http2BridgeEvent {
17861 kind: String::from("serverStreamError"),
17862 id: server_id,
17863 data: Some(http2_error_payload(error.to_string())),
17864 ..Http2BridgeEvent::default()
17865 },
17866 );
17867 continue;
17868 }
17869 };
17870 let stream_id = {
17871 let mut state = shared.lock().expect("http2 shared state");
17872 let stream_id = next_http2_stream_id(&mut state);
17873 state.streams.insert(
17874 stream_id,
17875 ActiveHttp2Stream {
17876 session_id,
17877 paused: Arc::new(AtomicBool::new(false)),
17878 },
17879 );
17880 stream_id
17881 };
17882 streams.lock().expect("http2 server streams").insert(
17883 stream_id,
17884 ServerHttp2StreamState {
17885 send_response: Some(ServerHttp2Responder::Regular(respond)),
17886 send_stream: None,
17887 },
17888 );
17889 let snapshot_json = snapshot
17890 .lock()
17891 .ok()
17892 .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok());
17893 push_http2_server_event(
17894 &shared,
17895 server_id,
17896 Http2BridgeEvent {
17897 kind: String::from("serverStream"),
17898 id: server_id,
17899 data: Some(stream_id.to_string()),
17900 extra: snapshot_json,
17901 extra_number: Some(session_id),
17902 extra_headers: Some(headers_json),
17903 flags: Some(0),
17904 },
17905 );
17906 let shared_clone = Arc::clone(&shared);
17907 tokio::spawn(async move {
17908 let mut body = request.into_body();
17909 while let Some(chunk) = body.data().await {
17910 match chunk {
17911 Ok(bytes) => {
17912 let paused = {
17913 let state = shared_clone.lock().expect("http2 shared state");
17914 state.streams.get(&stream_id).map(|stream| Arc::clone(&stream.paused))
17915 };
17916 if let Some(paused) = paused {
17917 while paused.load(Ordering::SeqCst) {
17918 tokio::time::sleep(HTTP2_POLL_DELAY).await;
17919 }
17920 }
17921 let _ = body.flow_control().release_capacity(bytes.len());
17922 push_http2_server_event(
17923 &shared_clone,
17924 server_id,
17925 Http2BridgeEvent {
17926 kind: String::from("serverStreamData"),
17927 id: stream_id,
17928 data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
17929 ..Http2BridgeEvent::default()
17930 },
17931 );
17932 }
17933 Err(error) => {
17934 push_http2_server_event(
17935 &shared_clone,
17936 server_id,
17937 Http2BridgeEvent {
17938 kind: String::from("serverStreamError"),
17939 id: stream_id,
17940 data: Some(http2_error_payload(error.to_string())),
17941 ..Http2BridgeEvent::default()
17942 },
17943 );
17944 break;
17945 }
17946 }
17947 }
17948 push_http2_server_event(
17949 &shared_clone,
17950 server_id,
17951 Http2BridgeEvent {
17952 kind: String::from("serverStreamEnd"),
17953 id: stream_id,
17954 ..Http2BridgeEvent::default()
17955 },
17956 );
17957 });
17958 }
17959 Some(Err(error)) => {
17960 push_http2_server_event(
17961 &shared,
17962 server_id,
17963 Http2BridgeEvent {
17964 kind: String::from("serverStreamError"),
17965 id: server_id,
17966 data: Some(http2_error_payload(error.to_string())),
17967 ..Http2BridgeEvent::default()
17968 },
17969 );
17970 break;
17971 }
17972 None => {
17973 push_http2_server_event(
17974 &shared,
17975 server_id,
17976 Http2BridgeEvent {
17977 kind: String::from("sessionClose"),
17978 id: session_id,
17979 ..Http2BridgeEvent::default()
17980 },
17981 );
17982 remove_http2_session_resources(&shared, session_id);
17983 break;
17984 }
17985 }
17986 }
17987 Some(command) = command_rx.recv() => {
17988 match command {
17989 Http2SessionCommand::Settings { settings_json, respond_to } => {
17990 let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
17991 .unwrap_or_default();
17992 if let Some(initial_window_size) = settings
17993 .get("initialWindowSize")
17994 .and_then(Value::as_u64)
17995 {
17996 let _ = connection.set_initial_window_size(initial_window_size as u32);
17997 }
17998 {
17999 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18000 snapshot.local_settings = http2_settings_from_value(&settings);
18001 }
18002 if let Ok(headers_json) = serde_json::to_string(&settings) {
18003 push_http2_session_event(
18004 &shared,
18005 session_id,
18006 Http2BridgeEvent {
18007 kind: String::from("sessionLocalSettings"),
18008 id: session_id,
18009 data: Some(headers_json),
18010 ..Http2BridgeEvent::default()
18011 },
18012 );
18013 }
18014 let _ = respond_to.send(Ok(Value::Null));
18015 }
18016 Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
18017 connection.set_target_window_size(size);
18018 {
18019 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18020 snapshot.state.local_window_size = size;
18021 snapshot.state.effective_local_window_size = size;
18022 }
18023 let value = snapshot
18024 .lock()
18025 .ok()
18026 .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
18027 .map(Value::String)
18028 .unwrap_or(Value::Null);
18029 let _ = respond_to.send(Ok(value));
18030 }
18031 Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
18032 connection.abrupt_shutdown(http2_reason(Some(error_code)));
18033 push_http2_session_event(
18034 &shared,
18035 session_id,
18036 Http2BridgeEvent {
18037 kind: String::from("sessionGoaway"),
18038 id: session_id,
18039 data: opaque_data.map(|value| {
18040 base64::engine::general_purpose::STANDARD.encode(value)
18041 }),
18042 extra_number: Some(error_code as u64),
18043 flags: Some(last_stream_id as u64),
18044 ..Http2BridgeEvent::default()
18045 },
18046 );
18047 let _ = respond_to.send(Ok(Value::Null));
18048 }
18049 Http2SessionCommand::Close { abrupt, respond_to } => {
18050 if abrupt {
18051 connection.abrupt_shutdown(Reason::NO_ERROR);
18052 } else {
18053 connection.graceful_shutdown();
18054 }
18055 let _ = respond_to.send(Ok(Value::Null));
18056 push_http2_session_event(
18057 &shared,
18058 session_id,
18059 Http2BridgeEvent {
18060 kind: String::from("sessionClose"),
18061 id: session_id,
18062 ..Http2BridgeEvent::default()
18063 },
18064 );
18065 remove_http2_session_resources(&shared, session_id);
18066 break;
18067 }
18068 Http2SessionCommand::StreamRespond { stream_id, headers_json, respond_to } => {
18069 let response = match build_http2_response(&headers_json) {
18070 Ok(response) => response,
18071 Err(error) => {
18072 let _ = respond_to.send(Err(error.to_string()));
18073 continue;
18074 }
18075 };
18076 let mut streams = streams.lock().expect("http2 server streams");
18077 let Some(state) = streams.get_mut(&stream_id) else {
18078 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18079 continue;
18080 };
18081 let Some(send_response) = state.send_response.as_mut() else {
18082 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
18083 continue;
18084 };
18085 match match send_response {
18086 ServerHttp2Responder::Regular(send_response) => {
18087 send_response.send_response(response, false)
18088 }
18089 ServerHttp2Responder::Pushed(send_response) => {
18090 send_response.send_response(response, false)
18091 }
18092 } {
18093 Ok(send_stream) => {
18094 state.send_stream = Some(send_stream);
18095 state.send_response = None;
18096 let _ = respond_to.send(Ok(Value::Null));
18097 }
18098 Err(error) => {
18099 let _ = respond_to.send(Err(error.to_string()));
18100 }
18101 }
18102 }
18103 Http2SessionCommand::StreamPush { stream_id, headers_json, respond_to } => {
18104 let request = match build_http2_request(&headers_json) {
18105 Ok(request) => request,
18106 Err(error) => {
18107 let _ = respond_to.send(Err(error.to_string()));
18108 continue;
18109 }
18110 };
18111 let mut streams_guard = streams.lock().expect("http2 server streams");
18112 let Some(state) = streams_guard.get_mut(&stream_id) else {
18113 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18114 continue;
18115 };
18116 let Some(send_response) = state.send_response.as_mut() else {
18117 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} cannot push after responding")));
18118 continue;
18119 };
18120 let ServerHttp2Responder::Regular(send_response) = send_response else {
18121 let _ = respond_to.send(Err(format!("HTTP/2 pushed stream {stream_id} cannot create nested push promises")));
18122 continue;
18123 };
18124 match send_response.push_request(request) {
18125 Ok(pushed) => {
18126 let pushed_stream_id = {
18127 let mut state = shared.lock().expect("http2 shared state");
18128 let pushed_stream_id = next_http2_stream_id(&mut state);
18129 state.streams.insert(
18130 pushed_stream_id,
18131 ActiveHttp2Stream {
18132 session_id,
18133 paused: Arc::new(AtomicBool::new(false)),
18134 },
18135 );
18136 pushed_stream_id
18137 };
18138 streams_guard.insert(
18139 pushed_stream_id,
18140 ServerHttp2StreamState {
18141 send_response: Some(ServerHttp2Responder::Pushed(pushed)),
18142 send_stream: None,
18143 },
18144 );
18145 let _ = respond_to.send(Ok(json!({
18146 "streamId": pushed_stream_id,
18147 "headers": headers_json,
18148 }).to_string().into()));
18149 }
18150 Err(error) => {
18151 let _ = respond_to.send(Err(error.to_string()));
18152 }
18153 }
18154 }
18155 Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
18156 let mut streams = streams.lock().expect("http2 server streams");
18157 let Some(state) = streams.get_mut(&stream_id) else {
18158 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18159 continue;
18160 };
18161 let Some(send_stream) = state.send_stream.as_mut() else {
18162 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} has not sent response headers")));
18163 continue;
18164 };
18165 match send_stream.send_data(Bytes::from(chunk), end_stream) {
18166 Ok(()) => {
18167 if end_stream {
18168 streams.remove(&stream_id);
18169 if let Ok(mut state) = shared.lock() {
18170 state.streams.remove(&stream_id);
18171 }
18172 push_http2_server_event(
18173 &shared,
18174 server_id,
18175 Http2BridgeEvent {
18176 kind: String::from("serverStreamClose"),
18177 id: stream_id,
18178 extra_number: Some(0),
18179 ..Http2BridgeEvent::default()
18180 },
18181 );
18182 }
18183 let _ = respond_to.send(Ok(Value::Bool(true)));
18184 }
18185 Err(error) => {
18186 let _ = respond_to.send(Err(error.to_string()));
18187 }
18188 }
18189 }
18190 Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
18191 let mut streams_guard = streams.lock().expect("http2 server streams");
18192 let Some(mut state) = streams_guard.remove(&stream_id) else {
18193 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18194 continue;
18195 };
18196 let reason = http2_reason(error_code);
18197 if let Some(send_stream) = state.send_stream.as_mut() {
18198 send_stream.send_reset(reason);
18199 }
18200 if let Some(send_response) = state.send_response.as_mut() {
18201 match send_response {
18202 ServerHttp2Responder::Regular(send_response) => {
18203 send_response.send_reset(reason)
18204 }
18205 ServerHttp2Responder::Pushed(send_response) => {
18206 send_response.send_reset(reason)
18207 }
18208 }
18209 }
18210 if let Ok(mut shared_guard) = shared.lock() {
18211 shared_guard.streams.remove(&stream_id);
18212 }
18213 push_http2_server_event(
18214 &shared,
18215 server_id,
18216 Http2BridgeEvent {
18217 kind: String::from("serverStreamClose"),
18218 id: stream_id,
18219 extra_number: Some(u32::from(reason) as u64),
18220 ..Http2BridgeEvent::default()
18221 },
18222 );
18223 let _ = respond_to.send(Ok(Value::Null));
18224 }
18225 Http2SessionCommand::StreamRespondWithFile { stream_id, body, headers_json, options_json, respond_to } => {
18226 let options: JavascriptHttp2FileResponseOptions =
18227 serde_json::from_str(&options_json).unwrap_or_default();
18228 let response = match build_http2_response(&headers_json) {
18229 Ok(response) => response,
18230 Err(error) => {
18231 let _ = respond_to.send(Err(error.to_string()));
18232 continue;
18233 }
18234 };
18235 let offset = usize::try_from(options.offset.unwrap_or_default()).unwrap_or(0);
18236 let body = if offset >= body.len() {
18237 Vec::new()
18238 } else {
18239 let body = &body[offset..];
18240 match options.length {
18241 Some(length) if length >= 0 => {
18242 body[..body.len().min(length as usize)].to_vec()
18243 }
18244 _ => body.to_vec(),
18245 }
18246 };
18247 let mut streams_guard = streams.lock().expect("http2 server streams");
18248 let Some(state) = streams_guard.get_mut(&stream_id) else {
18249 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18250 continue;
18251 };
18252 let Some(send_response) = state.send_response.as_mut() else {
18253 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
18254 continue;
18255 };
18256 match match send_response {
18257 ServerHttp2Responder::Regular(send_response) => {
18258 send_response.send_response(response, body.is_empty())
18259 }
18260 ServerHttp2Responder::Pushed(send_response) => {
18261 send_response.send_response(response, body.is_empty())
18262 }
18263 } {
18264 Ok(mut send_stream) => {
18265 state.send_response = None;
18266 if body.is_empty() {
18267 streams_guard.remove(&stream_id);
18268 if let Ok(mut shared_guard) = shared.lock() {
18269 shared_guard.streams.remove(&stream_id);
18270 }
18271 } else {
18272 if let Err(error) = send_stream.send_data(Bytes::from(body), true) {
18273 let _ = respond_to.send(Err(error.to_string()));
18274 continue;
18275 }
18276 streams_guard.remove(&stream_id);
18277 if let Ok(mut shared_guard) = shared.lock() {
18278 shared_guard.streams.remove(&stream_id);
18279 }
18280 }
18281 push_http2_server_event(
18282 &shared,
18283 server_id,
18284 Http2BridgeEvent {
18285 kind: String::from("serverStreamClose"),
18286 id: stream_id,
18287 extra_number: Some(0),
18288 ..Http2BridgeEvent::default()
18289 },
18290 );
18291 let _ = respond_to.send(Ok(Value::Null));
18292 }
18293 Err(error) => {
18294 let _ = respond_to.send(Err(error.to_string()));
18295 }
18296 }
18297 }
18298 Http2SessionCommand::Request { respond_to, .. } => {
18299 let _ = respond_to.send(Err(String::from("HTTP/2 server sessions cannot initiate client requests")));
18300 }
18301 }
18302 }
18303 else => break,
18304 }
18305 }
18306 });
18307 });
18308}
18309
18310fn spawn_http2_server_accept_loop(
18311 shared: Arc<Mutex<crate::state::Http2SharedState>>,
18312 server_id: u64,
18313 listener: TcpListener,
18314) {
18315 thread::spawn(move || {
18316 let listener = listener;
18317 loop {
18318 let closed = shared
18319 .lock()
18320 .ok()
18321 .and_then(|state| {
18322 state
18323 .servers
18324 .get(&server_id)
18325 .map(|server| server.closed.load(Ordering::SeqCst))
18326 })
18327 .unwrap_or(true);
18328 if closed {
18329 break;
18330 }
18331 match listener.accept() {
18332 Ok((stream, _)) => {
18333 let (command_tx, command_rx) = unbounded_channel();
18334 let (guest_local_addr, secure, tls) = {
18335 let state = shared.lock().expect("http2 shared state");
18336 let server = state.servers.get(&server_id).expect("http2 server state");
18337 (server.guest_local_addr, server.secure, server.tls.clone())
18338 };
18339 let (local_addr, remote_addr) = match (stream.local_addr(), stream.peer_addr())
18340 {
18341 (Ok(local_addr), Ok(remote_addr)) => (local_addr, remote_addr),
18342 _ => continue,
18343 };
18344 let session_snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
18345 encrypted: secure,
18346 alpn_protocol: Some(if secure {
18347 String::from("h2")
18348 } else {
18349 String::from("h2c")
18350 }),
18351 local_settings: BTreeMap::new(),
18352 remote_settings: BTreeMap::new(),
18353 state: http2_runtime_snapshot(),
18354 socket: Http2SocketSnapshot {
18355 local_address: Some(guest_local_addr.ip().to_string()),
18356 local_port: Some(guest_local_addr.port()),
18357 local_family: Some(socket_addr_family(&guest_local_addr).to_string()),
18358 remote_address: Some(remote_addr.ip().to_string()),
18359 remote_port: Some(remote_addr.port()),
18360 remote_family: Some(socket_addr_family(&remote_addr).to_string()),
18361 ..http2_socket_snapshot(local_addr, remote_addr)
18362 },
18363 ..Http2SessionSnapshot::default()
18364 }));
18365 let session_id = {
18366 let mut state = shared.lock().expect("http2 shared state");
18367 let session_id = next_http2_session_id(&mut state);
18368 state
18369 .sessions
18370 .insert(session_id, ActiveHttp2Session { command_tx });
18371 session_id
18372 };
18373 spawn_http2_server_session(
18374 Arc::clone(&shared),
18375 server_id,
18376 session_id,
18377 stream,
18378 tls,
18379 session_snapshot,
18380 command_rx,
18381 );
18382 }
18383 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
18384 thread::sleep(HTTP2_POLL_DELAY);
18385 }
18386 Err(error) => {
18387 push_http2_server_event(
18388 &shared,
18389 server_id,
18390 Http2BridgeEvent {
18391 kind: String::from("serverStreamError"),
18392 id: server_id,
18393 data: Some(http2_error_payload(error.to_string())),
18394 ..Http2BridgeEvent::default()
18395 },
18396 );
18397 thread::sleep(HTTP2_POLL_DELAY);
18398 }
18399 }
18400 }
18401 });
18402}
18403
18404fn send_http2_command(
18405 session: &ActiveHttp2Session,
18406 command: impl FnOnce(Sender<Result<Value, String>>) -> Http2SessionCommand,
18407) -> Result<Value, SidecarError> {
18408 let (respond_to, response_rx) = mpsc::channel();
18409 session.command_tx.send(command(respond_to)).map_err(|_| {
18410 SidecarError::InvalidState(String::from("HTTP/2 session command channel closed"))
18411 })?;
18412 response_rx
18413 .recv_timeout(Duration::from_secs(30))
18414 .map_err(|_| {
18415 SidecarError::Execution(String::from("timed out waiting for HTTP/2 session command"))
18416 })?
18417 .map_err(SidecarError::Execution)
18418}
18419
18420fn parse_http2_server_listen_payload(
18421 request: &JavascriptSyncRpcRequest,
18422) -> Result<JavascriptHttp2ServerListenRequest, SidecarError> {
18423 let payload_json =
18424 javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_server_listen payload")?;
18425 serde_json::from_str(payload_json).map_err(|error| {
18426 SidecarError::InvalidState(format!(
18427 "net.http2_server_listen payload must be valid JSON: {error}"
18428 ))
18429 })
18430}
18431
18432fn parse_http2_connect_payload(
18433 request: &JavascriptSyncRpcRequest,
18434) -> Result<JavascriptHttp2SessionConnectRequest, SidecarError> {
18435 let payload_json =
18436 javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_session_connect payload")?;
18437 serde_json::from_str(payload_json).map_err(|error| {
18438 SidecarError::InvalidState(format!(
18439 "net.http2_session_connect payload must be valid JSON: {error}"
18440 ))
18441 })
18442}
18443
18444fn http2_session_for_id(
18445 process: &ActiveProcess,
18446 session_id: u64,
18447) -> Result<ActiveHttp2Session, SidecarError> {
18448 let shared = process
18449 .http2
18450 .shared
18451 .lock()
18452 .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
18453 shared
18454 .sessions
18455 .get(&session_id)
18456 .cloned()
18457 .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 session {session_id}")))
18458}
18459
18460fn http2_stream_for_id(
18461 process: &ActiveProcess,
18462 stream_id: u64,
18463) -> Result<ActiveHttp2Stream, SidecarError> {
18464 let shared = process
18465 .http2
18466 .shared
18467 .lock()
18468 .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
18469 shared
18470 .streams
18471 .get(&stream_id)
18472 .cloned()
18473 .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 stream {stream_id}")))
18474}
18475
18476fn service_javascript_http2_sync_rpc<B>(
18477 request: JavascriptHttp2SyncRpcServiceRequest<'_, B>,
18478) -> Result<Value, SidecarError>
18479where
18480 B: NativeSidecarBridge + Send + 'static,
18481 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
18482{
18483 let JavascriptHttp2SyncRpcServiceRequest {
18484 bridge,
18485 kernel,
18486 vm_id,
18487 dns,
18488 socket_paths,
18489 process,
18490 sync_request: request,
18491 resource_limits,
18492 network_counts,
18493 } = request;
18494 match request.method.as_str() {
18495 "net.http2_server_listen" => {
18496 check_network_resource_limit(
18497 resource_limits.max_sockets,
18498 network_counts.sockets,
18499 1,
18500 "socket",
18501 )?;
18502 let payload = parse_http2_server_listen_payload(request)?;
18503 let (family, bind_host, guest_host) =
18504 normalize_tcp_listen_host(payload.host.as_deref())?;
18505 let requested_port = payload.port.unwrap_or(0);
18506 bridge.require_network_access(
18507 vm_id,
18508 NetworkOperation::Listen,
18509 format_tcp_resource(bind_host, requested_port),
18510 )?;
18511 let port = allocate_guest_listen_port(
18512 requested_port,
18513 family,
18514 &socket_paths.used_tcp_guest_ports,
18515 socket_paths.listen_policy,
18516 )?;
18517 let mut listener =
18518 ActiveTcpListener::bind(bind_host, guest_host, port, payload.backlog)?;
18519 let guest_local_addr = listener.guest_local_addr();
18520 let closed = Arc::new(AtomicBool::new(false));
18521 {
18522 let mut state = process.http2.shared.lock().map_err(|_| {
18523 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
18524 })?;
18525 state.servers.insert(
18526 payload.server_id,
18527 ActiveHttp2Server {
18528 actual_local_addr: listener.local_addr(),
18529 guest_local_addr,
18530 secure: payload.secure,
18531 tls: payload.tls.clone().map(|mut tls| {
18532 tls.is_server = payload.secure;
18533 if payload.secure && tls.alpn_protocols.is_none() {
18534 tls.alpn_protocols = Some(vec![String::from("h2")]);
18535 }
18536 tls
18537 }),
18538 closed: Arc::clone(&closed),
18539 },
18540 );
18541 state.server_events.entry(payload.server_id).or_default();
18542 }
18543 spawn_http2_server_accept_loop(
18544 Arc::clone(&process.http2.shared),
18545 payload.server_id,
18546 listener.listener.take().ok_or_else(|| {
18547 SidecarError::InvalidState(String::from(
18548 "HTTP/2 listener missing host TCP socket",
18549 ))
18550 })?,
18551 );
18552 javascript_net_json_string(
18553 json!({
18554 "address": {
18555 "address": guest_local_addr.ip().to_string(),
18556 "family": socket_addr_family(&guest_local_addr),
18557 "port": guest_local_addr.port(),
18558 }
18559 }),
18560 "net.http2_server_listen",
18561 )
18562 }
18563 "net.http2_server_poll" => {
18564 let server_id =
18565 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_poll server id")?;
18566 let wait_ms = javascript_sync_rpc_arg_u64_optional(
18567 &request.args,
18568 1,
18569 "net.http2_server_poll wait ms",
18570 )?
18571 .unwrap_or_default();
18572 match wait_for_http2_event(&process.http2.shared, server_id, true, wait_ms) {
18573 Some(event) => http2_event_value(&event),
18574 None => Ok(Value::Null),
18575 }
18576 }
18577 "net.http2_server_wait" => {
18578 let server_id =
18579 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_wait server id")?;
18580 dispatch_http2_wait_loop(process, server_id, true)
18581 }
18582 "net.http2_server_close" => {
18583 let server_id =
18584 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_close server id")?;
18585 let server = {
18586 let mut state = process.http2.shared.lock().map_err(|_| {
18587 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
18588 })?;
18589 state.servers.remove(&server_id)
18590 }
18591 .ok_or_else(|| {
18592 SidecarError::InvalidState(format!("unknown HTTP/2 server {server_id}"))
18593 })?;
18594 server.closed.store(true, Ordering::SeqCst);
18595 push_http2_server_event(
18596 &process.http2.shared,
18597 server_id,
18598 Http2BridgeEvent {
18599 kind: String::from("serverClose"),
18600 id: server_id,
18601 ..Http2BridgeEvent::default()
18602 },
18603 );
18604 Ok(Value::Null)
18605 }
18606 "net.http2_server_respond" => {
18607 let server_id = javascript_sync_rpc_arg_u64(
18608 &request.args,
18609 0,
18610 "net.http2_server_respond server id",
18611 )?;
18612 let request_id = javascript_sync_rpc_arg_u64(
18613 &request.args,
18614 1,
18615 "net.http2_server_respond request id",
18616 )?;
18617 let response_json =
18618 javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_server_respond payload")?;
18619 ensure_vm_fetch_response_within_limit(response_json, "net.http2_server_respond")?;
18620 serde_json::from_str::<Value>(response_json).map_err(|error| {
18621 SidecarError::Execution(format!(
18622 "net.http2_server_respond payload must be valid JSON: {error}"
18623 ))
18624 })?;
18625 let Some(pending) = process
18626 .pending_http_requests
18627 .get_mut(&(server_id, request_id))
18628 else {
18629 return Err(SidecarError::InvalidState(format!(
18630 "unknown pending HTTP/2 request {request_id} for server {server_id}"
18631 )));
18632 };
18633 *pending = Some(response_json.to_owned());
18634 Ok(Value::Bool(true))
18635 }
18636 "net.http2_session_connect" => {
18637 check_network_resource_limit(
18638 resource_limits.max_sockets,
18639 network_counts.sockets,
18640 1,
18641 "socket",
18642 )?;
18643 check_network_resource_limit(
18644 resource_limits.max_connections,
18645 network_counts.connections,
18646 1,
18647 "connection",
18648 )?;
18649 let payload = parse_http2_connect_payload(request)?;
18650 let authority = payload.authority.clone().unwrap_or_else(|| {
18651 format!(
18652 "{}://{}:{}",
18653 payload.protocol.as_deref().unwrap_or("http"),
18654 payload.host.as_deref().unwrap_or("localhost"),
18655 payload.port.unwrap_or(80)
18656 )
18657 });
18658 let url = Url::parse(&authority).map_err(|error| {
18659 SidecarError::InvalidState(format!(
18660 "invalid HTTP/2 authority {authority:?}: {error}"
18661 ))
18662 })?;
18663 let secure = url.scheme() == "https" || payload.protocol.as_deref() == Some("https:");
18664 let host = payload
18665 .host
18666 .as_deref()
18667 .or_else(|| url.host_str())
18668 .unwrap_or("localhost");
18669 let port = payload.port.or_else(|| url.port()).unwrap_or(80);
18670 bridge.require_network_access(
18671 vm_id,
18672 NetworkOperation::Http,
18673 format_tcp_resource(host, port),
18674 )?;
18675 let resolved = {
18676 let shared = process.http2.shared.lock().map_err(|_| {
18677 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
18678 })?;
18679 shared
18680 .servers
18681 .values()
18682 .find(|server| {
18683 is_loopback_request_host(host) && server.guest_local_addr.port() == port
18684 })
18685 .map(|server| ResolvedTcpConnectAddr {
18686 actual_addr: server.actual_local_addr,
18687 guest_remote_addr: server.guest_local_addr,
18688 use_kernel_loopback: false,
18689 })
18690 };
18691 let resolved = match resolved {
18692 Some(resolved) => resolved,
18693 None => {
18694 resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, socket_paths)?
18695 }
18696 };
18697 let (command_tx, command_rx) = unbounded_channel();
18698 let snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
18699 encrypted: secure,
18700 alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
18701 local_settings: http2_settings_from_value(&payload.settings),
18702 remote_settings: BTreeMap::new(),
18703 state: http2_runtime_snapshot(),
18704 socket: Http2SocketSnapshot {
18705 encrypted: secure,
18706 remote_address: Some(resolved.guest_remote_addr.ip().to_string()),
18707 remote_port: Some(resolved.guest_remote_addr.port()),
18708 remote_family: Some(
18709 socket_addr_family(&resolved.guest_remote_addr).to_string(),
18710 ),
18711 servername: if secure {
18712 payload
18713 .tls
18714 .as_ref()
18715 .and_then(|tls| tls.servername.clone())
18716 .or_else(|| Some(host.to_string()))
18717 } else {
18718 None
18719 },
18720 alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
18721 ..Http2SocketSnapshot::default()
18722 },
18723 ..Http2SessionSnapshot::default()
18724 }));
18725 let session_id = {
18726 let mut state = process.http2.shared.lock().map_err(|_| {
18727 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
18728 })?;
18729 let session_id = next_http2_session_id(&mut state);
18730 state
18731 .sessions
18732 .insert(session_id, ActiveHttp2Session { command_tx });
18733 state.session_events.entry(session_id).or_default();
18734 session_id
18735 };
18736 spawn_http2_client_session(
18737 Arc::clone(&process.http2.shared),
18738 session_id,
18739 resolved.actual_addr,
18740 if secure {
18741 Some(payload.tls.unwrap_or(JavascriptTlsBridgeOptions {
18742 is_server: false,
18743 servername: Some(host.to_string()),
18744 alpn_protocols: Some(vec![String::from("h2")]),
18745 ..JavascriptTlsBridgeOptions::default()
18746 }))
18747 } else {
18748 None
18749 },
18750 Arc::clone(&snapshot),
18751 command_rx,
18752 );
18753 let snapshot_json =
18754 http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())?;
18755 javascript_net_json_string(
18756 json!({
18757 "sessionId": session_id,
18758 "state": snapshot_json,
18759 }),
18760 "net.http2_session_connect",
18761 )
18762 }
18763 "net.http2_session_request" => {
18764 let session_id = javascript_sync_rpc_arg_u64(
18765 &request.args,
18766 0,
18767 "net.http2_session_request session id",
18768 )?;
18769 let headers_json =
18770 javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_session_request headers")?;
18771 let options_json =
18772 javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_session_request options")?;
18773 let session = http2_session_for_id(process, session_id)?;
18774 send_http2_command(&session, |respond_to| Http2SessionCommand::Request {
18775 headers_json: headers_json.to_owned(),
18776 options_json: options_json.to_owned(),
18777 respond_to,
18778 })
18779 }
18780 "net.http2_session_settings" => {
18781 let session_id = javascript_sync_rpc_arg_u64(
18782 &request.args,
18783 0,
18784 "net.http2_session_settings session id",
18785 )?;
18786 let settings_json = javascript_sync_rpc_arg_str(
18787 &request.args,
18788 1,
18789 "net.http2_session_settings settings",
18790 )?;
18791 let session = http2_session_for_id(process, session_id)?;
18792 send_http2_command(&session, |respond_to| Http2SessionCommand::Settings {
18793 settings_json: settings_json.to_owned(),
18794 respond_to,
18795 })
18796 }
18797 "net.http2_session_set_local_window_size" => {
18798 let session_id = javascript_sync_rpc_arg_u64(
18799 &request.args,
18800 0,
18801 "net.http2_session_set_local_window_size session id",
18802 )?;
18803 let window_size = javascript_sync_rpc_arg_u64(
18804 &request.args,
18805 1,
18806 "net.http2_session_set_local_window_size window size",
18807 )?;
18808 let session = http2_session_for_id(process, session_id)?;
18809 send_http2_command(&session, |respond_to| {
18810 Http2SessionCommand::SetLocalWindowSize {
18811 size: window_size as u32,
18812 respond_to,
18813 }
18814 })
18815 }
18816 "net.http2_session_goaway" => {
18817 let session_id = javascript_sync_rpc_arg_u64(
18818 &request.args,
18819 0,
18820 "net.http2_session_goaway session id",
18821 )?;
18822 let error_code = javascript_sync_rpc_arg_u64(
18823 &request.args,
18824 1,
18825 "net.http2_session_goaway error code",
18826 )?;
18827 let last_stream_id = javascript_sync_rpc_arg_u64(
18828 &request.args,
18829 2,
18830 "net.http2_session_goaway last stream id",
18831 )?;
18832 let opaque_data = request
18833 .args
18834 .get(3)
18835 .and_then(Value::as_str)
18836 .map(|value| {
18837 base64::engine::general_purpose::STANDARD
18838 .decode(value)
18839 .map_err(|error| {
18840 SidecarError::InvalidState(format!("invalid GOAWAY payload: {error}"))
18841 })
18842 })
18843 .transpose()?;
18844 let session = http2_session_for_id(process, session_id)?;
18845 send_http2_command(&session, |respond_to| Http2SessionCommand::Goaway {
18846 error_code: error_code as u32,
18847 last_stream_id: last_stream_id as u32,
18848 opaque_data,
18849 respond_to,
18850 })
18851 }
18852 "net.http2_session_close" | "net.http2_session_destroy" => {
18853 let session_id = javascript_sync_rpc_arg_u64(
18854 &request.args,
18855 0,
18856 "net.http2_session_close session id",
18857 )?;
18858 let session = http2_session_for_id(process, session_id)?;
18859 send_http2_command(&session, |respond_to| Http2SessionCommand::Close {
18860 abrupt: request.method == "net.http2_session_destroy",
18861 respond_to,
18862 })
18863 }
18864 "net.http2_session_poll" => {
18865 let session_id =
18866 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_poll session id")?;
18867 let wait_ms = javascript_sync_rpc_arg_u64_optional(
18868 &request.args,
18869 1,
18870 "net.http2_session_poll wait ms",
18871 )?
18872 .unwrap_or_default();
18873 match wait_for_http2_event(&process.http2.shared, session_id, false, wait_ms) {
18874 Some(event) => http2_event_value(&event),
18875 None => Ok(Value::Null),
18876 }
18877 }
18878 "net.http2_session_wait" => {
18879 let session_id =
18880 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_wait session id")?;
18881 dispatch_http2_wait_loop(process, session_id, false)
18882 }
18883 "net.http2_stream_respond" => {
18884 let stream_id = javascript_sync_rpc_arg_u64(
18885 &request.args,
18886 0,
18887 "net.http2_stream_respond stream id",
18888 )?;
18889 let headers_json =
18890 javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_stream_respond headers")?;
18891 let stream = http2_stream_for_id(process, stream_id)?;
18892 let session = http2_session_for_id(process, stream.session_id)?;
18893 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamRespond {
18894 stream_id,
18895 headers_json: headers_json.to_owned(),
18896 respond_to,
18897 })
18898 }
18899 "net.http2_stream_push_stream" => {
18900 let stream_id = javascript_sync_rpc_arg_u64(
18901 &request.args,
18902 0,
18903 "net.http2_stream_push_stream stream id",
18904 )?;
18905 let headers_json = javascript_sync_rpc_arg_str(
18906 &request.args,
18907 1,
18908 "net.http2_stream_push_stream headers",
18909 )?;
18910 let _options_json = javascript_sync_rpc_arg_str(
18911 &request.args,
18912 2,
18913 "net.http2_stream_push_stream options",
18914 )?;
18915 let stream = http2_stream_for_id(process, stream_id)?;
18916 let session = http2_session_for_id(process, stream.session_id)?;
18917 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamPush {
18918 stream_id,
18919 headers_json: headers_json.to_owned(),
18920 respond_to,
18921 })
18922 }
18923 "net.http2_stream_write" => {
18924 let stream_id =
18925 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_write stream id")?;
18926 let chunk =
18927 javascript_sync_rpc_base64_arg(&request.args, 1, "net.http2_stream_write data")?;
18928 let stream = http2_stream_for_id(process, stream_id)?;
18929 let session = http2_session_for_id(process, stream.session_id)?;
18930 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
18931 stream_id,
18932 chunk,
18933 end_stream: false,
18934 respond_to,
18935 })
18936 }
18937 "net.http2_stream_end" => {
18938 let stream_id =
18939 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_end stream id")?;
18940 let chunk = request
18941 .args
18942 .get(1)
18943 .and_then(Value::as_str)
18944 .map(|value| {
18945 base64::engine::general_purpose::STANDARD
18946 .decode(value)
18947 .map_err(|error| {
18948 SidecarError::InvalidState(format!(
18949 "invalid HTTP/2 stream payload: {error}"
18950 ))
18951 })
18952 })
18953 .transpose()?
18954 .unwrap_or_default();
18955 let stream = http2_stream_for_id(process, stream_id)?;
18956 let session = http2_session_for_id(process, stream.session_id)?;
18957 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
18958 stream_id,
18959 chunk,
18960 end_stream: true,
18961 respond_to,
18962 })
18963 }
18964 "net.http2_stream_close" => {
18965 let stream_id =
18966 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_close stream id")?;
18967 let code = javascript_sync_rpc_arg_u64_optional(
18968 &request.args,
18969 1,
18970 "net.http2_stream_close error code",
18971 )?
18972 .map(|value| value as u32);
18973 let stream = http2_stream_for_id(process, stream_id)?;
18974 let session = http2_session_for_id(process, stream.session_id)?;
18975 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamClose {
18976 stream_id,
18977 error_code: code,
18978 respond_to,
18979 })
18980 }
18981 "net.http2_stream_pause" => {
18982 let stream_id =
18983 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_pause stream id")?;
18984 let stream = http2_stream_for_id(process, stream_id)?;
18985 stream.paused.store(true, Ordering::SeqCst);
18986 Ok(Value::Null)
18987 }
18988 "net.http2_stream_resume" => {
18989 let stream_id =
18990 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_resume stream id")?;
18991 let stream = http2_stream_for_id(process, stream_id)?;
18992 stream.paused.store(false, Ordering::SeqCst);
18993 Ok(Value::Null)
18994 }
18995 "net.http2_stream_respond_with_file" => {
18996 let stream_id = javascript_sync_rpc_arg_u64(
18997 &request.args,
18998 0,
18999 "net.http2_stream_respond_with_file stream id",
19000 )?;
19001 let path = javascript_sync_rpc_arg_str(
19002 &request.args,
19003 1,
19004 "net.http2_stream_respond_with_file path",
19005 )?;
19006 let headers_json = javascript_sync_rpc_arg_str(
19007 &request.args,
19008 2,
19009 "net.http2_stream_respond_with_file headers",
19010 )?;
19011 let options_json = javascript_sync_rpc_arg_str(
19012 &request.args,
19013 3,
19014 "net.http2_stream_respond_with_file options",
19015 )?;
19016 let stream = http2_stream_for_id(process, stream_id)?;
19017 let session = http2_session_for_id(process, stream.session_id)?;
19018 let guest_path = resolve_http2_file_response_guest_path(process, path);
19019 let body = kernel.read_file(&guest_path).map_err(kernel_error)?;
19020 send_http2_command(&session, |respond_to| {
19021 Http2SessionCommand::StreamRespondWithFile {
19022 stream_id,
19023 body,
19024 headers_json: headers_json.to_owned(),
19025 options_json: options_json.to_owned(),
19026 respond_to,
19027 }
19028 })
19029 }
19030 other => Err(SidecarError::InvalidState(format!(
19031 "unsupported JavaScript HTTP/2 sync RPC method {other}"
19032 ))),
19033 }
19034}
19035
19036const JAVASCRIPT_NET_POLL_MAX_WAIT: Duration = Duration::from_millis(50);
19037const EXITED_PROCESS_SNAPSHOT_RETENTION: Duration = Duration::from_secs(2);
19038
19039fn resolve_http2_file_response_guest_path(process: &ActiveProcess, path: &str) -> String {
19040 if Path::new(path).is_absolute() {
19041 normalize_path(path)
19042 } else {
19043 normalize_path(&format!("{}/{}", process.guest_cwd, path))
19044 }
19045}
19046
19047pub(crate) fn clamp_javascript_net_poll_wait(wait_ms: u64) -> Duration {
19048 if wait_ms == 0 {
19051 Duration::ZERO
19052 } else {
19053 Duration::from_millis(wait_ms).min(JAVASCRIPT_NET_POLL_MAX_WAIT)
19054 }
19055}
19056
19057pub(crate) fn service_javascript_net_sync_rpc<B>(
19058 request: JavascriptNetSyncRpcServiceRequest<'_, B>,
19059) -> Result<Value, SidecarError>
19060where
19061 B: NativeSidecarBridge + Send + 'static,
19062 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19063{
19064 let JavascriptNetSyncRpcServiceRequest {
19065 bridge,
19066 vm_id,
19067 dns,
19068 socket_paths,
19069 kernel,
19070 process,
19071 sync_request: request,
19072 resource_limits,
19073 network_counts,
19074 } = request;
19075 match request.method.as_str() {
19076 "net.http_listen" => {
19077 check_network_resource_limit(
19078 resource_limits.max_sockets,
19079 network_counts.sockets,
19080 1,
19081 "socket",
19082 )?;
19083 let payload_json =
19084 javascript_sync_rpc_arg_str(&request.args, 0, "net.http_listen payload")?;
19085 let payload: JavascriptHttpListenRequest =
19086 serde_json::from_str(payload_json).map_err(|error| {
19087 SidecarError::InvalidState(format!(
19088 "net.http_listen payload must be valid JSON: {error}"
19089 ))
19090 })?;
19091 let (family, bind_host, guest_host) =
19092 normalize_tcp_listen_host(payload.hostname.as_deref())?;
19093 let requested_port = payload.port.unwrap_or(0);
19094 bridge.require_network_access(
19095 vm_id,
19096 NetworkOperation::Listen,
19097 format_tcp_resource(bind_host, requested_port),
19098 )?;
19099 let port = allocate_guest_listen_port(
19100 requested_port,
19101 family,
19102 &socket_paths.used_tcp_guest_ports,
19103 socket_paths.listen_policy,
19104 )?;
19105 let mut listener = ActiveTcpListener::bind(
19106 bind_host,
19107 guest_host,
19108 port,
19109 Some(DEFAULT_JAVASCRIPT_NET_BACKLOG),
19110 )?;
19111 let guest_local_addr = listener.guest_local_addr();
19112 process.http_servers.insert(
19113 payload.server_id,
19114 ActiveHttpServer {
19115 listener: listener.listener.take().ok_or_else(|| {
19116 SidecarError::InvalidState(String::from(
19117 "HTTP listener missing host TCP socket",
19118 ))
19119 })?,
19120 guest_local_addr,
19121 next_request_id: 0,
19122 },
19123 );
19124 serde_json::to_string(&json!({
19125 "address": {
19126 "address": guest_local_addr.ip().to_string(),
19127 "family": socket_addr_family(&guest_local_addr),
19128 "port": guest_local_addr.port(),
19129 }
19130 }))
19131 .map(Value::String)
19132 .map_err(|error| {
19133 SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}"))
19134 })
19135 }
19136 "net.http_close" => {
19137 let server_id =
19138 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_close server id")?;
19139 let server = process.http_servers.remove(&server_id).ok_or_else(|| {
19140 SidecarError::InvalidState(format!("unknown HTTP server {server_id}"))
19141 })?;
19142 drop(server.listener);
19143 process
19144 .pending_http_requests
19145 .retain(|(pending_server_id, _), _| *pending_server_id != server_id);
19146 Ok(Value::Null)
19147 }
19148 "net.http_wait" => {
19149 let server_id =
19150 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_wait server id")?;
19151 dispatch_http_wait_loop(process, server_id)
19152 }
19153 "net.http_respond" => {
19154 let server_id =
19155 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_respond server id")?;
19156 let request_id =
19157 javascript_sync_rpc_arg_u64(&request.args, 1, "net.http_respond request id")?;
19158 let response_json =
19159 javascript_sync_rpc_arg_str(&request.args, 2, "net.http_respond payload")?;
19160 ensure_vm_fetch_response_within_limit(response_json, "net.http_respond")?;
19161 serde_json::from_str::<Value>(response_json).map_err(|error| {
19162 SidecarError::Execution(format!(
19163 "net.http_respond payload must be valid JSON: {error}"
19164 ))
19165 })?;
19166 let Some(pending) = process
19167 .pending_http_requests
19168 .get_mut(&(server_id, request_id))
19169 else {
19170 return Err(SidecarError::InvalidState(format!(
19171 "unknown pending HTTP request {request_id} for server {server_id}"
19172 )));
19173 };
19174 *pending = Some(response_json.to_owned());
19175 Ok(Value::Null)
19176 }
19177 "net.reserve_tcp_port" => {
19178 let payload = request
19179 .args
19180 .first()
19181 .cloned()
19182 .ok_or_else(|| {
19183 SidecarError::InvalidState(String::from(
19184 "net.reserve_tcp_port requires a request payload",
19185 ))
19186 })
19187 .and_then(|value| {
19188 serde_json::from_value::<JavascriptNetReserveTcpPortRequest>(value).map_err(
19189 |error| {
19190 SidecarError::InvalidState(format!(
19191 "invalid net.reserve_tcp_port payload: {error}"
19192 ))
19193 },
19194 )
19195 })?;
19196 let (family, _bind_host, guest_host) =
19197 normalize_tcp_listen_host(payload.host.as_deref())?;
19198 let requested_port = payload.port.unwrap_or(0);
19199 let port = allocate_guest_listen_port(
19200 requested_port,
19201 family,
19202 &socket_paths.used_tcp_guest_ports,
19203 socket_paths.listen_policy,
19204 )?;
19205 let reservation_id = process.allocate_tcp_port_reservation_id();
19206 process
19207 .tcp_port_reservations
19208 .insert(reservation_id.clone(), (family, port));
19209 Ok(json!({
19210 "reservationId": reservation_id,
19211 "localAddress": guest_host,
19212 "localPort": port,
19213 "family": match family {
19214 JavascriptSocketFamily::Ipv4 => "IPv4",
19215 JavascriptSocketFamily::Ipv6 => "IPv6",
19216 },
19217 }))
19218 }
19219 "net.release_tcp_port" => {
19220 let reservation_id =
19221 javascript_sync_rpc_arg_str(&request.args, 0, "net.release_tcp_port reservation")?;
19222 process.tcp_port_reservations.remove(reservation_id);
19223 Ok(Value::Null)
19224 }
19225 "net.connect" => {
19226 check_network_resource_limit(
19227 resource_limits.max_sockets,
19228 network_counts.sockets,
19229 1,
19230 "socket",
19231 )?;
19232 check_network_resource_limit(
19233 resource_limits.max_connections,
19234 network_counts.connections,
19235 1,
19236 "connection",
19237 )?;
19238 let payload = request
19239 .args
19240 .first()
19241 .cloned()
19242 .ok_or_else(|| {
19243 SidecarError::InvalidState(String::from(
19244 "net.connect requires a request payload",
19245 ))
19246 })
19247 .and_then(|value| {
19248 serde_json::from_value::<JavascriptNetConnectRequest>(value).map_err(|error| {
19249 SidecarError::InvalidState(format!("invalid net.connect payload: {error}"))
19250 })
19251 })?;
19252 if let Some(path) = payload.path.as_deref() {
19253 let guest_path = normalize_path(path);
19254 let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
19255 let socket = ActiveUnixSocket::connect(&host_path, &guest_path)?;
19256 let socket_id = process.allocate_unix_socket_id();
19257 process.unix_sockets.insert(socket_id.clone(), socket);
19258 Ok(json!({
19259 "socketId": socket_id,
19260 "remotePath": guest_path,
19261 }))
19262 } else {
19263 let port = payload.port.ok_or_else(|| {
19264 SidecarError::InvalidState(String::from(
19265 "net.connect requires either a path or port",
19266 ))
19267 })?;
19268 let host = payload.host.as_deref().unwrap_or("localhost");
19269 let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
19270 process
19271 .tcp_port_reservations
19272 .remove(id)
19273 .map(|reservation| (id.to_owned(), reservation))
19274 });
19275 bridge.require_network_access(
19276 vm_id,
19277 NetworkOperation::Http,
19278 format_tcp_resource(host, port),
19279 )?;
19280 let connect_result = ActiveTcpSocket::connect(ActiveTcpConnectRequest {
19281 bridge,
19282 kernel,
19283 kernel_pid: process.kernel_pid,
19284 vm_id,
19285 dns,
19286 host,
19287 port,
19288 local_address: payload.local_address.as_deref(),
19289 local_port: payload.local_port,
19290 local_reservation: local_reservation
19291 .as_ref()
19292 .map(|(_, reservation)| *reservation),
19293 context: socket_paths,
19294 });
19295 if let Err(error) = connect_result {
19296 if let Some((reservation_id, reservation)) = local_reservation {
19297 process
19298 .tcp_port_reservations
19299 .insert(reservation_id, reservation);
19300 }
19301 return Err(error);
19302 }
19303 let socket = connect_result?;
19304 let socket_id = process.allocate_tcp_socket_id();
19305 let local_addr = socket.guest_local_addr;
19306 let remote_addr = socket.guest_remote_addr;
19307 process.tcp_sockets.insert(socket_id.clone(), socket);
19308 Ok(json!({
19309 "socketId": socket_id,
19310 "localAddress": local_addr.ip().to_string(),
19311 "localPort": local_addr.port(),
19312 "remoteAddress": remote_addr.ip().to_string(),
19313 "remotePort": remote_addr.port(),
19314 "remoteFamily": socket_addr_family(&remote_addr),
19315 }))
19316 }
19317 }
19318 "net.listen" => {
19319 check_network_resource_limit(
19320 resource_limits.max_sockets,
19321 network_counts.sockets,
19322 1,
19323 "socket",
19324 )?;
19325 let payload = request
19326 .args
19327 .first()
19328 .cloned()
19329 .ok_or_else(|| {
19330 SidecarError::InvalidState(String::from(
19331 "net.listen requires a request payload",
19332 ))
19333 })
19334 .and_then(|value| match value {
19335 Value::String(json) => {
19336 serde_json::from_str::<JavascriptNetListenRequest>(&json).map_err(|error| {
19337 SidecarError::InvalidState(format!(
19338 "invalid net.listen payload: {error}"
19339 ))
19340 })
19341 }
19342 other => serde_json::from_value::<JavascriptNetListenRequest>(other).map_err(
19343 |error| {
19344 SidecarError::InvalidState(format!(
19345 "invalid net.listen payload: {error}"
19346 ))
19347 },
19348 ),
19349 })?;
19350 if let Some(path) = payload.path.as_deref() {
19351 let guest_path = normalize_path(path);
19352 if kernel.exists(&guest_path).map_err(kernel_error)? {
19353 return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
19354 libc::EADDRINUSE,
19355 )));
19356 }
19357
19358 let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
19359 let on_host_mount =
19360 host_mount_path_for_guest_path_from_mounts(&socket_paths.mounts, &guest_path)
19361 .is_some();
19362 let listener = ActiveUnixListener::bind(&host_path, &guest_path, payload.backlog)?;
19363 if !on_host_mount {
19364 ensure_kernel_parent_directories(kernel, &guest_path)?;
19365 kernel
19366 .write_file(&guest_path, Vec::new())
19367 .map_err(kernel_error)?;
19368 }
19369 let listener_id = process.allocate_unix_listener_id();
19370 process.unix_listeners.insert(listener_id.clone(), listener);
19371 Ok(json!({
19372 "serverId": listener_id,
19373 "path": guest_path,
19374 }))
19375 } else {
19376 let (family, bind_host, guest_host) =
19377 normalize_tcp_listen_host(payload.host.as_deref())?;
19378 let requested_port = payload.port.unwrap_or(0);
19379 bridge.require_network_access(
19380 vm_id,
19381 NetworkOperation::Listen,
19382 format_tcp_resource(bind_host, requested_port),
19383 )?;
19384 let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
19385 process
19386 .tcp_port_reservations
19387 .remove(id)
19388 .map(|reservation| (id.to_owned(), reservation))
19389 });
19390 let port = if requested_port != 0
19391 && local_reservation
19392 .as_ref()
19393 .map(|(_, reservation)| *reservation)
19394 == Some((family, requested_port))
19395 {
19396 requested_port
19397 } else {
19398 allocate_guest_listen_port(
19399 requested_port,
19400 family,
19401 &socket_paths.used_tcp_guest_ports,
19402 socket_paths.listen_policy,
19403 )?
19404 };
19405 let listener_result = ActiveTcpListener::bind_kernel(
19406 kernel,
19407 process.kernel_pid,
19408 guest_host,
19409 port,
19410 payload.backlog,
19411 );
19412 if let Err(error) = listener_result {
19413 if let Some((reservation_id, reservation)) = local_reservation {
19414 process
19415 .tcp_port_reservations
19416 .insert(reservation_id, reservation);
19417 }
19418 return Err(error);
19419 }
19420 let listener = listener_result?;
19421 let listener_id = process.allocate_tcp_listener_id();
19422 let local_addr = listener.guest_local_addr();
19423 process.tcp_listeners.insert(listener_id.clone(), listener);
19424 Ok(json!({
19425 "serverId": listener_id,
19426 "localAddress": local_addr.ip().to_string(),
19427 "localPort": local_addr.port(),
19428 "family": socket_addr_family(&local_addr),
19429 }))
19430 }
19431 }
19432 "net.poll" => {
19433 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.poll socket id")?;
19434 let wait_ms =
19435 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.poll wait ms")?
19436 .unwrap_or_default();
19437 let wait = clamp_javascript_net_poll_wait(wait_ms);
19438 let event = if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
19439 socket.poll(kernel, process.kernel_pid, wait)?
19440 } else if let Some(socket) = process.unix_sockets.get_mut(socket_id) {
19441 socket.poll(wait)?
19442 } else {
19443 return Err(SidecarError::InvalidState(format!(
19444 "unknown net socket {socket_id}"
19445 )));
19446 };
19447
19448 match event {
19449 Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(json!({
19450 "type": "data",
19451 "data": javascript_sync_rpc_bytes_value(&chunk),
19452 })),
19453 Some(JavascriptTcpSocketEvent::End) => Ok(json!({
19454 "type": "end",
19455 })),
19456 Some(JavascriptTcpSocketEvent::Error { code, message }) => Ok(json!({
19457 "type": "error",
19458 "code": code,
19459 "message": message,
19460 })),
19461 Some(JavascriptTcpSocketEvent::Close { had_error }) => {
19462 if let Some(socket) = process.tcp_sockets.remove(socket_id) {
19463 if let Some(listener_id) = socket.listener_id.as_deref() {
19464 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
19465 listener.release_connection(socket_id);
19466 }
19467 }
19468 } else if let Some(socket) = process.unix_sockets.remove(socket_id) {
19469 if let Some(listener_id) = socket.listener_id.as_deref() {
19470 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
19471 listener.release_connection(socket_id);
19472 }
19473 }
19474 }
19475 Ok(json!({
19476 "type": "close",
19477 "hadError": had_error,
19478 }))
19479 }
19480 None => Ok(Value::Null),
19481 }
19482 }
19483 "net.socket_wait_connect" => {
19484 let socket_id =
19485 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_wait_connect socket id")?;
19486 if let Some(socket) = process.tcp_sockets.get(socket_id) {
19487 javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
19488 } else {
19489 let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
19490 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
19491 })?;
19492 javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
19493 }
19494 }
19495 "net.socket_read" => {
19496 let socket_id =
19497 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_read socket id")?;
19498 if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
19499 javascript_net_read_value(socket.poll(
19500 kernel,
19501 process.kernel_pid,
19502 Duration::ZERO,
19503 )?)
19504 } else {
19505 let socket = process.unix_sockets.get_mut(socket_id).ok_or_else(|| {
19506 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
19507 })?;
19508 javascript_net_read_value(socket.poll(Duration::ZERO)?)
19509 }
19510 }
19511 "net.socket_set_no_delay" => {
19512 let socket_id =
19513 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_set_no_delay socket id")?;
19514 let enable =
19515 javascript_sync_rpc_arg_bool(&request.args, 1, "net.socket_set_no_delay enabled")?;
19516 if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
19517 socket.set_no_delay(enable)?;
19518 } else if !process.unix_sockets.contains_key(socket_id) {
19519 return Err(SidecarError::InvalidState(format!(
19520 "unknown net socket {socket_id}"
19521 )));
19522 }
19523 Ok(Value::Null)
19524 }
19525 "net.socket_set_keep_alive" => {
19526 let socket_id = javascript_sync_rpc_arg_str(
19527 &request.args,
19528 0,
19529 "net.socket_set_keep_alive socket id",
19530 )?;
19531 let enable = javascript_sync_rpc_arg_bool(
19532 &request.args,
19533 1,
19534 "net.socket_set_keep_alive enabled",
19535 )?;
19536 let initial_delay_secs = javascript_sync_rpc_arg_u64_optional(
19537 &request.args,
19538 2,
19539 "net.socket_set_keep_alive initial delay seconds",
19540 )?;
19541 if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
19542 socket.set_keep_alive(enable, initial_delay_secs)?;
19543 } else if !process.unix_sockets.contains_key(socket_id) {
19544 return Err(SidecarError::InvalidState(format!(
19545 "unknown net socket {socket_id}"
19546 )));
19547 }
19548 Ok(Value::Null)
19549 }
19550 "net.socket_upgrade_tls" => {
19551 let socket_id =
19552 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_upgrade_tls socket id")?;
19553 let options_json =
19554 javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_upgrade_tls options")?;
19555 let options: JavascriptTlsBridgeOptions =
19556 serde_json::from_str(options_json).map_err(|error| {
19557 SidecarError::InvalidState(format!(
19558 "net.socket_upgrade_tls options must be valid JSON: {error}"
19559 ))
19560 })?;
19561 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
19562 SidecarError::InvalidState(format!(
19563 "unknown TCP socket {socket_id} for TLS upgrade"
19564 ))
19565 })?;
19566 socket.upgrade_tls(vm_id, kernel, options)?;
19567 Ok(Value::Null)
19568 }
19569 "net.socket_get_tls_client_hello" => {
19570 let socket_id = javascript_sync_rpc_arg_str(
19571 &request.args,
19572 0,
19573 "net.socket_get_tls_client_hello socket id",
19574 )?;
19575 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
19576 SidecarError::InvalidState(format!(
19577 "unknown TCP socket {socket_id} for TLS client hello query"
19578 ))
19579 })?;
19580 socket.tls_client_hello_json(vm_id, kernel)
19581 }
19582 "net.socket_tls_query" => {
19583 let socket_id =
19584 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_tls_query socket id")?;
19585 let query =
19586 javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_tls_query query")?;
19587 let detailed = request
19588 .args
19589 .get(2)
19590 .and_then(Value::as_bool)
19591 .unwrap_or(false);
19592 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
19593 SidecarError::InvalidState(format!("unknown TCP socket {socket_id} for TLS query"))
19594 })?;
19595 socket.tls_query(query, detailed)
19596 }
19597 "net.server_poll" => {
19598 let listener_id =
19599 javascript_sync_rpc_arg_str(&request.args, 0, "net.server_poll listener id")?;
19600 let wait_ms =
19601 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.server_poll wait ms")?
19602 .unwrap_or_default();
19603 let tcp_event = if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
19604 Some(listener.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?)
19605 } else {
19606 None
19607 };
19608
19609 if let Some(event) = tcp_event {
19610 return match event {
19611 Some(JavascriptTcpListenerEvent::Connection(pending)) => {
19612 let PendingTcpSocket {
19613 stream,
19614 kernel_socket_id,
19615 preallocated,
19616 guest_local_addr,
19617 guest_remote_addr,
19618 } = pending;
19619 if !preallocated {
19620 if let Err(error) = check_network_resource_limit(
19621 resource_limits.max_sockets,
19622 network_counts.sockets,
19623 1,
19624 "socket",
19625 )
19626 .and_then(|()| {
19627 check_network_resource_limit(
19628 resource_limits.max_connections,
19629 network_counts.connections,
19630 1,
19631 "connection",
19632 )
19633 }) {
19634 if let Some(stream) = stream {
19635 let _ = stream.shutdown(Shutdown::Both);
19636 }
19637 return Ok(json!({
19638 "type": "error",
19639 "code": "EAGAIN",
19640 "message": error.to_string(),
19641 }));
19642 }
19643 }
19644 let socket = if let Some(stream) = stream {
19645 ActiveTcpSocket::from_stream(
19646 stream,
19647 Some(listener_id.to_string()),
19648 guest_local_addr,
19649 guest_remote_addr,
19650 )?
19651 } else {
19652 ActiveTcpSocket::from_kernel(
19653 kernel_socket_id.ok_or_else(|| {
19654 SidecarError::InvalidState(String::from(
19655 "kernel TCP accept missing socket id",
19656 ))
19657 })?,
19658 Some(listener_id.to_string()),
19659 guest_local_addr,
19660 guest_remote_addr,
19661 )
19662 };
19663 let socket_id = process.allocate_tcp_socket_id();
19664 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
19665 listener.register_connection(&socket_id);
19666 }
19667 process.tcp_sockets.insert(socket_id.clone(), socket);
19668 Ok(json!({
19669 "type": "connection",
19670 "socketId": socket_id,
19671 "localAddress": guest_local_addr.ip().to_string(),
19672 "localPort": guest_local_addr.port(),
19673 "remoteAddress": guest_remote_addr.ip().to_string(),
19674 "remotePort": guest_remote_addr.port(),
19675 "remoteFamily": socket_addr_family(&guest_remote_addr),
19676 }))
19677 }
19678 Some(JavascriptTcpListenerEvent::Error { code, message }) => Ok(json!({
19679 "type": "error",
19680 "code": code,
19681 "message": message,
19682 })),
19683 None => Ok(Value::Null),
19684 };
19685 }
19686
19687 let event = {
19688 let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
19689 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
19690 })?;
19691 listener.poll(Duration::from_millis(wait_ms))?
19692 };
19693
19694 match event {
19695 Some(JavascriptUnixListenerEvent::Connection(pending)) => {
19696 if let Err(error) = check_network_resource_limit(
19697 resource_limits.max_sockets,
19698 network_counts.sockets,
19699 1,
19700 "socket",
19701 )
19702 .and_then(|()| {
19703 check_network_resource_limit(
19704 resource_limits.max_connections,
19705 network_counts.connections,
19706 1,
19707 "connection",
19708 )
19709 }) {
19710 let _ = pending.stream.shutdown(Shutdown::Both);
19711 return Ok(json!({
19712 "type": "error",
19713 "code": "EAGAIN",
19714 "message": error.to_string(),
19715 }));
19716 }
19717 let socket = ActiveUnixSocket::from_stream(
19718 pending.stream,
19719 Some(listener_id.to_string()),
19720 pending.local_path.clone(),
19721 pending.remote_path.clone(),
19722 )?;
19723 let socket_id = process.allocate_unix_socket_id();
19724 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
19725 listener.register_connection(&socket_id);
19726 }
19727 process.unix_sockets.insert(socket_id.clone(), socket);
19728 Ok(json!({
19729 "type": "connection",
19730 "socketId": socket_id,
19731 "localPath": pending.local_path,
19732 "remotePath": pending.remote_path,
19733 }))
19734 }
19735 Some(JavascriptUnixListenerEvent::Error { code, message }) => Ok(json!({
19736 "type": "error",
19737 "code": code,
19738 "message": message,
19739 })),
19740 None => Ok(Value::Null),
19741 }
19742 }
19743 "net.server_accept" => {
19744 let listener_id =
19745 javascript_sync_rpc_arg_str(&request.args, 0, "net.server_accept listener id")?;
19746 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
19747 return match listener.poll(kernel, process.kernel_pid, Duration::ZERO)? {
19748 Some(JavascriptTcpListenerEvent::Connection(pending)) => {
19749 let PendingTcpSocket {
19750 stream,
19751 kernel_socket_id,
19752 preallocated,
19753 guest_local_addr,
19754 guest_remote_addr,
19755 } = pending;
19756 if !preallocated {
19757 check_network_resource_limit(
19758 resource_limits.max_sockets,
19759 network_counts.sockets,
19760 1,
19761 "socket",
19762 )?;
19763 check_network_resource_limit(
19764 resource_limits.max_connections,
19765 network_counts.connections,
19766 1,
19767 "connection",
19768 )?;
19769 }
19770 let info = json!({
19771 "localAddress": guest_local_addr.ip().to_string(),
19772 "localPort": guest_local_addr.port(),
19773 "localFamily": socket_addr_family(&guest_local_addr),
19774 "remoteAddress": guest_remote_addr.ip().to_string(),
19775 "remotePort": guest_remote_addr.port(),
19776 "remoteFamily": socket_addr_family(&guest_remote_addr),
19777 });
19778 let socket = if let Some(stream) = stream {
19779 ActiveTcpSocket::from_stream(
19780 stream,
19781 Some(listener_id.to_string()),
19782 guest_local_addr,
19783 guest_remote_addr,
19784 )?
19785 } else {
19786 ActiveTcpSocket::from_kernel(
19787 kernel_socket_id.ok_or_else(|| {
19788 SidecarError::InvalidState(String::from(
19789 "kernel TCP accept missing socket id",
19790 ))
19791 })?,
19792 Some(listener_id.to_string()),
19793 guest_local_addr,
19794 guest_remote_addr,
19795 )
19796 };
19797 let socket_id = process.allocate_tcp_socket_id();
19798 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
19799 listener.register_connection(&socket_id);
19800 }
19801 process.tcp_sockets.insert(socket_id.clone(), socket);
19802 javascript_net_json_string(
19803 json!({
19804 "socketId": socket_id,
19805 "info": info,
19806 }),
19807 "net.server_accept",
19808 )
19809 }
19810 Some(JavascriptTcpListenerEvent::Error { code, message }) => {
19811 let detail = code.unwrap_or_else(|| String::from("server accept"));
19812 Err(SidecarError::Execution(format!("{detail}: {message}")))
19813 }
19814 None => Ok(javascript_net_timeout_value()),
19815 };
19816 }
19817
19818 let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
19819 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
19820 })?;
19821 match listener.poll(Duration::ZERO)? {
19822 Some(JavascriptUnixListenerEvent::Connection(pending)) => {
19823 check_network_resource_limit(
19824 resource_limits.max_sockets,
19825 network_counts.sockets,
19826 1,
19827 "socket",
19828 )?;
19829 check_network_resource_limit(
19830 resource_limits.max_connections,
19831 network_counts.connections,
19832 1,
19833 "connection",
19834 )?;
19835 let info = json!({
19836 "localPath": pending.local_path.clone(),
19837 "remotePath": pending.remote_path.clone(),
19838 });
19839 let socket = ActiveUnixSocket::from_stream(
19840 pending.stream,
19841 Some(listener_id.to_string()),
19842 pending.local_path,
19843 pending.remote_path,
19844 )?;
19845 let socket_id = process.allocate_unix_socket_id();
19846 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
19847 listener.register_connection(&socket_id);
19848 }
19849 process.unix_sockets.insert(socket_id.clone(), socket);
19850 javascript_net_json_string(
19851 json!({
19852 "socketId": socket_id,
19853 "info": info,
19854 }),
19855 "net.server_accept",
19856 )
19857 }
19858 Some(JavascriptUnixListenerEvent::Error { code, message }) => {
19859 let detail = code.unwrap_or_else(|| String::from("server accept"));
19860 Err(SidecarError::Execution(format!("{detail}: {message}")))
19861 }
19862 None => Ok(javascript_net_timeout_value()),
19863 }
19864 }
19865 "net.server_connections" => {
19866 let listener_id = javascript_sync_rpc_arg_str(
19867 &request.args,
19868 0,
19869 "net.server_connections listener id",
19870 )?;
19871 if let Some(listener) = process.tcp_listeners.get(listener_id) {
19872 Ok(json!(listener.active_connection_count()))
19873 } else {
19874 let listener = process.unix_listeners.get(listener_id).ok_or_else(|| {
19875 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
19876 })?;
19877 Ok(json!(listener.active_connection_count()))
19878 }
19879 }
19880 "net.upgrade_socket_write" => {
19881 let socket_id = javascript_sync_rpc_arg_str(
19882 &request.args,
19883 0,
19884 "net.upgrade_socket_write socket id",
19885 )?;
19886 let chunk =
19887 javascript_sync_rpc_base64_arg(&request.args, 1, "net.upgrade_socket_write chunk")?;
19888 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
19889 SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
19890 })?;
19891 socket
19892 .write_all(kernel, process.kernel_pid, &chunk)
19893 .map(|written| json!(written))
19894 }
19895 "net.upgrade_socket_end" => {
19896 let socket_id =
19897 javascript_sync_rpc_arg_str(&request.args, 0, "net.upgrade_socket_end socket id")?;
19898 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
19899 SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
19900 })?;
19901 socket.shutdown_write(kernel, process.kernel_pid)?;
19902 Ok(Value::Null)
19903 }
19904 "net.upgrade_socket_destroy" => {
19905 let socket_id = javascript_sync_rpc_arg_str(
19906 &request.args,
19907 0,
19908 "net.upgrade_socket_destroy socket id",
19909 )?;
19910 let socket = process.tcp_sockets.remove(socket_id).ok_or_else(|| {
19911 SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
19912 })?;
19913 if let Some(listener_id) = socket.listener_id.as_deref() {
19914 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
19915 listener.release_connection(socket_id);
19916 }
19917 }
19918 let _ = socket.close(kernel, process.kernel_pid);
19919 Ok(Value::Null)
19920 }
19921 "net.write" => {
19922 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.write socket id")?;
19923 let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "net.write chunk")?;
19924 if let Some(socket) = process.tcp_sockets.get(socket_id) {
19925 socket
19926 .write_all(kernel, process.kernel_pid, &chunk)
19927 .map(|written| json!(written))
19928 } else {
19929 let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
19930 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
19931 })?;
19932 socket.write_all(&chunk).map(|written| json!(written))
19933 }
19934 }
19935 "net.shutdown" => {
19936 let socket_id =
19937 javascript_sync_rpc_arg_str(&request.args, 0, "net.shutdown socket id")?;
19938 if let Some(socket) = process.tcp_sockets.get(socket_id) {
19939 socket.shutdown_write(kernel, process.kernel_pid)?;
19940 } else {
19941 let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
19942 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
19943 })?;
19944 socket.shutdown_write()?;
19945 }
19946 Ok(Value::Null)
19947 }
19948 "net.destroy" => {
19949 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.destroy socket id")?;
19950 if let Some(socket) = process.tcp_sockets.remove(socket_id) {
19951 if let Some(listener_id) = socket.listener_id.as_deref() {
19952 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
19953 listener.release_connection(socket_id);
19954 }
19955 }
19956 let _ = socket.close(kernel, process.kernel_pid);
19957 Ok(Value::Null)
19958 } else {
19959 let socket = process.unix_sockets.remove(socket_id).ok_or_else(|| {
19960 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
19961 })?;
19962 if let Some(listener_id) = socket.listener_id.as_deref() {
19963 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
19964 listener.release_connection(socket_id);
19965 }
19966 }
19967 let _ = socket.close();
19968 Ok(Value::Null)
19969 }
19970 }
19971 "net.server_close" => {
19972 let listener_id =
19973 javascript_sync_rpc_arg_str(&request.args, 0, "net.server_close listener id")?;
19974 if let Some(listener) = process.tcp_listeners.remove(listener_id) {
19975 listener.close(kernel, process.kernel_pid)?;
19976 Ok(Value::Null)
19977 } else {
19978 let listener = process.unix_listeners.remove(listener_id).ok_or_else(|| {
19979 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
19980 })?;
19981 listener.close()?;
19982 Ok(Value::Null)
19983 }
19984 }
19985 "tls.get_ciphers" => javascript_net_json_string(
19986 Value::Array(
19987 tls_provider()
19988 .cipher_suites
19989 .iter()
19990 .filter_map(|suite| {
19991 suite
19992 .suite()
19993 .as_str()
19994 .map(|value| Value::String(value.to_owned()))
19995 })
19996 .collect(),
19997 ),
19998 "tls.get_ciphers",
19999 ),
20000 _ => Err(SidecarError::InvalidState(format!(
20001 "unsupported JavaScript net sync RPC method {}",
20002 request.method
20003 ))),
20004 }
20005}
20006
20007fn signal_name_for_stream_event(signal: i32) -> Option<&'static str> {
20008 match signal {
20009 libc::SIGHUP => Some("SIGHUP"),
20010 libc::SIGINT => Some("SIGINT"),
20011 libc::SIGUSR1 => Some("SIGUSR1"),
20012 libc::SIGALRM => Some("SIGALRM"),
20013 libc::SIGCONT => Some("SIGCONT"),
20014 libc::SIGTERM => Some("SIGTERM"),
20015 libc::SIGCHLD => Some("SIGCHLD"),
20016 libc::SIGWINCH => Some("SIGWINCH"),
20017 _ => None,
20018 }
20019}
20020
20021pub(crate) fn canonical_signal_name(signal: i32) -> Option<&'static str> {
20022 match signal {
20023 1 => Some("SIGHUP"),
20024 2 => Some("SIGINT"),
20025 3 => Some("SIGQUIT"),
20026 4 => Some("SIGILL"),
20027 5 => Some("SIGTRAP"),
20028 6 => Some("SIGABRT"),
20029 7 => Some("SIGBUS"),
20030 8 => Some("SIGFPE"),
20031 9 => Some("SIGKILL"),
20032 10 => Some("SIGUSR1"),
20033 11 => Some("SIGSEGV"),
20034 12 => Some("SIGUSR2"),
20035 13 => Some("SIGPIPE"),
20036 14 => Some("SIGALRM"),
20037 15 => Some("SIGTERM"),
20038 17 => Some("SIGCHLD"),
20039 18 => Some("SIGCONT"),
20040 19 => Some("SIGSTOP"),
20041 20 => Some("SIGTSTP"),
20042 21 => Some("SIGTTIN"),
20043 22 => Some("SIGTTOU"),
20044 23 => Some("SIGURG"),
20045 24 => Some("SIGXCPU"),
20046 25 => Some("SIGXFSZ"),
20047 26 => Some("SIGVTALRM"),
20048 27 => Some("SIGPROF"),
20049 28 => Some("SIGWINCH"),
20050 29 => Some("SIGIO"),
20051 30 => Some("SIGPWR"),
20052 31 => Some("SIGSYS"),
20053 _ => None,
20054 }
20055}
20056
20057fn dispatch_v8_process_signal(process: &ActiveProcess, signal: i32) -> Result<bool, SidecarError> {
20058 let Some(signal_name) = signal_name_for_stream_event(signal) else {
20059 return Ok(false);
20060 };
20061 process.execution.send_javascript_stream_event(
20062 "signal",
20063 json!({
20064 "signal": signal_name,
20065 "number": signal,
20066 "action": "default",
20067 }),
20068 )?;
20069 Ok(true)
20070}
20071
20072fn dispatch_v8_session_signal_async(session: V8SessionHandle, signal: i32) {
20073 let Some(signal_name) = signal_name_for_stream_event(signal).map(str::to_owned) else {
20074 return;
20075 };
20076 thread::spawn(move || {
20077 thread::sleep(Duration::from_millis(1));
20078 let payload = v8_runtime::json_to_cbor_payload(&json!({
20079 "signal": signal_name,
20080 "number": signal,
20081 "action": "default",
20082 }))
20083 .unwrap_or_default();
20084 let _ = session.send_stream_event("signal", payload);
20085 });
20086}
20087
20088pub(crate) fn parse_signal(signal: &str) -> Result<i32, SidecarError> {
20089 let trimmed = signal.trim();
20090 if trimmed.is_empty() {
20091 return Err(SidecarError::InvalidState(String::from(
20092 "kill_process requires a non-empty signal",
20093 )));
20094 }
20095
20096 if let Ok(value) = trimmed.parse::<i32>() {
20097 return match value {
20098 0..=31 => Ok(value),
20099 _ => Err(SidecarError::InvalidState(format!(
20100 "unsupported kill_process signal {signal}"
20101 ))),
20102 };
20103 }
20104
20105 let upper = trimmed.to_ascii_uppercase();
20106 let normalized = upper.strip_prefix("SIG").unwrap_or(&upper);
20107
20108 signal_number_from_name(normalized).ok_or_else(|| {
20109 SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
20110 })
20111}
20112
20113fn signal_number_from_name(signal: &str) -> Option<i32> {
20114 match signal {
20115 "0" => Some(0),
20116 "HUP" => Some(1),
20117 "INT" => Some(2),
20118 "QUIT" => Some(3),
20119 "ILL" => Some(4),
20120 "TRAP" => Some(5),
20121 "ABRT" | "IOT" => Some(6),
20122 "BUS" => Some(7),
20123 "FPE" => Some(8),
20124 "KILL" => Some(9),
20125 "USR1" => Some(10),
20126 "SEGV" => Some(11),
20127 "USR2" => Some(12),
20128 "PIPE" => Some(13),
20129 "ALRM" => Some(14),
20130 "TERM" => Some(15),
20131 "STKFLT" => Some(16),
20132 "CHLD" => Some(17),
20133 "CONT" => Some(18),
20134 "STOP" => Some(19),
20135 "TSTP" => Some(20),
20136 "TTIN" => Some(21),
20137 "TTOU" => Some(22),
20138 "URG" => Some(23),
20139 "XCPU" => Some(24),
20140 "XFSZ" => Some(25),
20141 "VTALRM" => Some(26),
20142 "PROF" => Some(27),
20143 "WINCH" => Some(28),
20144 "IO" | "POLL" => Some(29),
20145 "PWR" => Some(30),
20146 "SYS" => Some(31),
20147 _ => None,
20148 }
20149}
20150
20151pub(crate) fn runtime_child_is_alive(child_pid: u32) -> Result<bool, SidecarError> {
20152 Ok(runtime_child_exit_status(child_pid)?.is_none())
20153}
20154
20155fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
20156 if child_pid == 0 {
20157 return Ok(Some(0));
20158 }
20159
20160 let wait_flags = WaitPidFlag::WNOHANG
20161 | WaitPidFlag::WNOWAIT
20162 | WaitPidFlag::WEXITED
20163 | WaitPidFlag::WUNTRACED
20164 | WaitPidFlag::WCONTINUED;
20165 match wait_on_child(WaitId::Pid(Pid::from_raw(child_pid as i32)), wait_flags) {
20166 Ok(WaitStatus::StillAlive)
20167 | Ok(WaitStatus::Stopped(_, _))
20168 | Ok(WaitStatus::Continued(_)) => Ok(None),
20169 Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
20170 Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
20171 #[cfg(any(target_os = "linux", target_os = "android"))]
20172 Ok(WaitStatus::PtraceEvent(_, _, _) | WaitStatus::PtraceSyscall(_)) => Ok(None),
20173 Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
20174 Err(error) => Err(SidecarError::Execution(format!(
20175 "failed to inspect guest runtime process {child_pid}: {error}"
20176 ))),
20177 }
20178}
20179
20180pub(crate) fn signal_runtime_process(child_pid: u32, signal: i32) -> Result<(), SidecarError> {
20181 if child_pid == 0 {
20182 return Ok(());
20183 }
20184
20185 if !runtime_child_is_alive(child_pid)? {
20186 return Ok(());
20187 }
20188
20189 if signal == 0 {
20190 return Ok(());
20191 }
20192
20193 let parsed = Signal::try_from(signal).map_err(|_| {
20194 SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
20195 })?;
20196 let result = send_signal(Pid::from_raw(child_pid as i32), Some(parsed));
20197
20198 match result {
20199 Ok(()) => Ok(()),
20200 Err(nix::errno::Errno::ESRCH) => Ok(()),
20201 Err(error) => Err(SidecarError::Execution(format!(
20202 "failed to signal guest runtime process {child_pid}: {error}"
20203 ))),
20204 }
20205}
20206
20207pub(crate) fn error_code(error: &SidecarError) -> &'static str {
20208 match error {
20209 SidecarError::InvalidState(_) => "invalid_state",
20210 SidecarError::ProtocolVersionMismatch(_) => "protocol_version_mismatch",
20211 SidecarError::BridgeVersionMismatch(_) => "bridge_version_mismatch",
20212 SidecarError::Conflict(_) => "conflict",
20213 SidecarError::Unauthorized(_) => "unauthorized",
20214 SidecarError::Unsupported(_) => "unsupported",
20215 SidecarError::FrameTooLarge(_) => "frame_too_large",
20216 SidecarError::Kernel(_) => "kernel_error",
20217 SidecarError::Plugin(_) => "plugin_error",
20218 SidecarError::Execution(_) => "execution_error",
20219 SidecarError::Bridge(_) => "bridge_error",
20220 SidecarError::Io(_) => "io_error",
20221 }
20222}
20223
20224fn guest_errno_code(message: &str) -> Option<&str> {
20225 const TRUSTED_PREFIXES: &[&str] = &[
20226 "ERR_AGENT_OS_NODE_SYNC_RPC",
20227 "ERR_AGENT_OS_PYTHON_VFS_RPC",
20228 "ERR_AGENT_OS_BRIDGE",
20229 ];
20230
20231 let mut segments = message.split(':').map(str::trim);
20232 let first = segments.next()?;
20233 if is_guest_errno_segment(first) {
20234 return Some(first);
20235 }
20236
20237 if TRUSTED_PREFIXES.contains(&first) {
20238 let second = segments.next()?;
20239 if is_guest_errno_segment(second) {
20240 return Some(second);
20241 }
20242 }
20243
20244 None
20245}
20246
20247fn is_guest_errno_segment(segment: &str) -> bool {
20248 segment.len() >= 2
20249 && segment.starts_with('E')
20250 && !segment.starts_with("ERR_")
20251 && segment[1..]
20252 .bytes()
20253 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
20254}
20255
20256pub(crate) fn javascript_sync_rpc_error_code(error: &SidecarError) -> String {
20257 let message = error.to_string();
20258 if let Some(code) = guest_errno_code(&message) {
20259 return code.to_owned();
20260 }
20261 if message.starts_with("ERR_NATIVE_BINARY_NOT_SUPPORTED:") {
20262 return String::from("ERR_NATIVE_BINARY_NOT_SUPPORTED");
20263 }
20264
20265 let lower = message.to_ascii_lowercase();
20266 if lower.contains("no such file or directory")
20267 || lower.contains("entry not found")
20268 || lower.contains("not found")
20269 {
20270 return String::from("ENOENT");
20271 }
20272 if lower.contains("permission denied") {
20273 return String::from("EACCES");
20274 }
20275 if lower.contains("already exists")
20276 || lower.contains("already registered")
20277 || lower.contains("file exists")
20278 {
20279 return String::from("EEXIST");
20280 }
20281 if lower.contains("invalid argument") {
20282 return String::from("EINVAL");
20283 }
20284
20285 String::from("ERR_AGENT_OS_NODE_SYNC_RPC")
20286}
20287
20288pub(crate) fn ignore_stale_javascript_sync_rpc_response(
20289 error: SidecarError,
20290) -> Result<(), SidecarError> {
20291 match error {
20292 SidecarError::Execution(message)
20293 if message.ends_with("is no longer pending")
20294 && message.starts_with("sync RPC request ") =>
20295 {
20296 Ok(())
20297 }
20298 SidecarError::Execution(message) => {
20299 let lower = message.to_ascii_lowercase();
20300 if lower.contains("sync rpc response")
20301 && (lower.contains("broken pipe") || lower.contains("channel closed unexpectedly"))
20302 {
20303 Ok(())
20304 } else {
20305 Err(SidecarError::Execution(message))
20306 }
20307 }
20308 other => Err(other),
20309 }
20310}
20311
20312#[cfg(test)]
20313mod error_code_tests {
20314 use super::{guest_errno_code, javascript_sync_rpc_error_code, SidecarError};
20315
20316 #[test]
20317 fn guest_errno_code_rejects_guest_controlled_errno_segments() {
20318 assert_eq!(guest_errno_code("user said 'EACCES: denied'"), None);
20319 assert_eq!(
20320 guest_errno_code("prefix: user said 'EPERM': more text"),
20321 None
20322 );
20323 assert_eq!(guest_errno_code("ERR_AGENT_OS_FAKE: EACCES: denied"), None);
20324 }
20325
20326 #[test]
20327 fn guest_errno_code_accepts_trusted_secure_exec_prefixes() {
20328 assert_eq!(
20329 guest_errno_code("ERR_AGENT_OS_NODE_SYNC_RPC: EACCES: permission denied on /foo"),
20330 Some("EACCES")
20331 );
20332 assert_eq!(
20333 guest_errno_code("ERR_AGENT_OS_PYTHON_VFS_RPC: ENOENT: missing file"),
20334 Some("ENOENT")
20335 );
20336 assert_eq!(guest_errno_code("EEXIST: already exists"), Some("EEXIST"));
20337 }
20338
20339 #[test]
20340 fn javascript_sync_rpc_error_code_ignores_spoofed_errnos() {
20341 let error = SidecarError::Execution(String::from("user said 'EACCES: denied'"));
20342 assert_eq!(
20343 javascript_sync_rpc_error_code(&error),
20344 "ERR_AGENT_OS_NODE_SYNC_RPC"
20345 );
20346 }
20347
20348 #[test]
20349 fn javascript_sync_rpc_error_code_preserves_real_sidecar_errnos() {
20350 let error = SidecarError::Execution(String::from(
20351 "ERR_AGENT_OS_NODE_SYNC_RPC: EACCES: permission denied on /foo",
20352 ));
20353 assert_eq!(javascript_sync_rpc_error_code(&error), "EACCES");
20354 }
20355
20356 #[test]
20357 fn javascript_sync_rpc_error_code_maps_file_exists_messages() {
20358 let error = SidecarError::Io(String::from(
20359 "failed to create mapped guest directory /.next/server: File exists (os error 17)",
20360 ));
20361 assert_eq!(javascript_sync_rpc_error_code(&error), "EEXIST");
20362 }
20363
20364 #[test]
20365 fn javascript_sync_rpc_error_code_preserves_native_binary_rejections() {
20366 let error = SidecarError::Execution(String::from(
20367 "ERR_NATIVE_BINARY_NOT_SUPPORTED: refused to execute native ELF guest binary at /tmp/fake-rg inside the VM",
20368 ));
20369 assert_eq!(
20370 javascript_sync_rpc_error_code(&error),
20371 "ERR_NATIVE_BINARY_NOT_SUPPORTED"
20372 );
20373 }
20374}
20375#[cfg(test)]
20376mod ssrf_egress_classifier_tests {
20377 use super::{
20387 filter_dns_safe_ip_addrs, is_loopback_ip, restricted_non_loopback_ip_range, SidecarError,
20388 };
20389 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
20390
20391 fn assert_restricted(ip: IpAddr, expected_label: &str) {
20392 let classification = restricted_non_loopback_ip_range(ip);
20393 assert!(
20394 classification.is_some(),
20395 "{ip} must be classified as a restricted egress target"
20396 );
20397 let (_cidr, label) = classification.unwrap();
20398 assert_eq!(
20399 label, expected_label,
20400 "{ip} should be labelled {expected_label}, got {label}"
20401 );
20402 }
20403
20404 fn assert_dns_denied(ip: IpAddr, label: &str) {
20405 match filter_dns_safe_ip_addrs(vec![ip], "attacker.example") {
20406 Err(SidecarError::Execution(message)) => assert!(
20407 message.starts_with("EACCES:"),
20408 "{label}: egress filter must deny with EACCES, got: {message}"
20409 ),
20410 other => panic!("{label}: expected EACCES denial, got {other:?}"),
20411 }
20412 }
20413
20414 #[test]
20416 fn classifier_denies_unspecified_and_cgnat_targets() {
20417 assert_restricted(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "unspecified");
20419 assert_restricted(IpAddr::V6(Ipv6Addr::UNSPECIFIED), "unspecified");
20421
20422 assert_restricted(
20424 IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
20425 "carrier-grade-nat",
20426 );
20427 assert_restricted(
20428 IpAddr::V4(Ipv4Addr::new(100, 127, 255, 254)),
20429 "carrier-grade-nat",
20430 );
20431
20432 assert!(
20434 restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 63, 255, 255)))
20435 .is_none(),
20436 "100.63.255.255 is outside CGNAT and must remain allowed"
20437 );
20438 assert!(
20439 restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 128, 0, 0))).is_none(),
20440 "100.128.0.0 is outside CGNAT and must remain allowed"
20441 );
20442
20443 assert_dns_denied(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "0.0.0.0 (unspecified)");
20445 assert_dns_denied(IpAddr::V6(Ipv6Addr::UNSPECIFIED), ":: (unspecified)");
20446 assert_dns_denied(
20447 IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
20448 "100.64.0.1 (CGNAT)",
20449 );
20450 }
20451
20452 #[test]
20454 fn classifier_denies_ipv6_spelled_metadata_addresses() {
20455 let mapped = "::ffff:169.254.169.254".parse::<Ipv6Addr>().unwrap();
20458 assert_restricted(IpAddr::V6(mapped), "link-local");
20459
20460 let compat = "::169.254.169.254".parse::<Ipv6Addr>().unwrap();
20461 assert_restricted(IpAddr::V6(compat), "link-local");
20462
20463 assert_restricted(
20465 IpAddr::V6("::10.0.0.1".parse::<Ipv6Addr>().unwrap()),
20466 "private",
20467 );
20468 assert_restricted(
20469 IpAddr::V6("::100.64.0.1".parse::<Ipv6Addr>().unwrap()),
20470 "carrier-grade-nat",
20471 );
20472
20473 assert_eq!(
20477 restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::UNSPECIFIED)),
20478 Some(("::/128", "unspecified")),
20479 ":: must classify as unspecified, not via the IPv4-compat path"
20480 );
20481 assert!(
20482 restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::LOCALHOST)).is_none()
20483 || is_loopback_ip(IpAddr::V6(Ipv6Addr::LOCALHOST)),
20484 "::1 must not be classified as a restricted IPv4-compatible target"
20485 );
20486 assert!(
20487 restricted_non_loopback_ip_range(IpAddr::V6("::8.8.8.8".parse::<Ipv6Addr>().unwrap()))
20488 .is_none(),
20489 "::8.8.8.8 (public IPv4-compatible) must remain allowed"
20490 );
20491
20492 assert_dns_denied(
20494 IpAddr::V6("::169.254.169.254".parse::<Ipv6Addr>().unwrap()),
20495 "::169.254.169.254 (IPv4-compat metadata)",
20496 );
20497 }
20498
20499 #[test]
20501 fn classifier_denies_reserved_and_multicast_targets() {
20502 assert_restricted(IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)), "multicast");
20506 assert_restricted(IpAddr::V4(Ipv4Addr::new(239, 255, 255, 255)), "multicast");
20507 assert_restricted(IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)), "reserved");
20508 assert_restricted(IpAddr::V4(Ipv4Addr::BROADCAST), "reserved");
20510
20511 assert_restricted(
20513 IpAddr::V6("::224.0.0.1".parse::<Ipv6Addr>().unwrap()),
20514 "multicast",
20515 );
20516 assert_restricted(
20517 IpAddr::V6("::240.0.0.1".parse::<Ipv6Addr>().unwrap()),
20518 "reserved",
20519 );
20520
20521 assert!(
20523 restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(223, 255, 255, 255)))
20524 .is_none(),
20525 "223.255.255.255 is outside 224/4 and must remain allowed"
20526 );
20527
20528 assert_dns_denied(
20530 IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)),
20531 "240.0.0.1 (reserved)",
20532 );
20533 assert_dns_denied(
20534 IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)),
20535 "224.0.0.1 (multicast)",
20536 );
20537 }
20538}
20539
20540#[cfg(test)]
20549mod dns_rebinding_pin_tests {
20550 use super::{issue_outbound_http_request, split_netloc, JavascriptHttpRequestOptions};
20551 use std::collections::BTreeMap;
20552 use std::io::{Read, Write};
20553 use std::net::{IpAddr, Ipv4Addr, TcpListener};
20554 use std::thread;
20555 use url::Url;
20556
20557 fn empty_headers() -> super::HttpHeaderCollection {
20558 super::parse_http_header_collection(&BTreeMap::new(), "test headers")
20559 .expect("empty header collection")
20560 }
20561
20562 fn options() -> JavascriptHttpRequestOptions {
20563 JavascriptHttpRequestOptions {
20564 method: Some(String::from("GET")),
20565 headers: BTreeMap::new(),
20566 body: None,
20567 reject_unauthorized: None,
20568 }
20569 }
20570
20571 #[test]
20572 fn split_netloc_handles_hostnames_and_bracketed_ipv6() {
20573 assert_eq!(
20574 split_netloc("attacker.example:80"),
20575 Some(("attacker.example", 80))
20576 );
20577 assert_eq!(split_netloc("[::1]:443"), Some(("::1", 443)));
20578 assert_eq!(split_netloc("10.0.0.1:8080"), Some(("10.0.0.1", 8080)));
20579 assert_eq!(split_netloc("no-port"), None);
20580 assert_eq!(split_netloc("host:notaport"), None);
20581 }
20582
20583 #[test]
20589 fn outbound_http_connect_is_pinned_to_vetted_ip() {
20590 let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind loopback server");
20591 let port = listener.local_addr().expect("local addr").port();
20592 let server = thread::spawn(move || {
20593 let (mut stream, _) = listener.accept().expect("accept");
20594 let mut buf = [0u8; 1024];
20595 let _ = stream.read(&mut buf);
20596 stream
20597 .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi")
20598 .expect("write response");
20599 let _ = stream.flush();
20600 });
20601
20602 let url = Url::parse(&format!("http://attacker.example:{port}/")).expect("url");
20603 let pinned = vec![IpAddr::V4(Ipv4Addr::LOCALHOST)];
20604 let result = issue_outbound_http_request(&url, &options(), &empty_headers(), &pinned)
20605 .expect("pinned request should reach the vetted loopback target");
20606 let payload = result.as_str().expect("string payload");
20607 assert!(
20608 payload.contains("\"status\":200"),
20609 "expected 200 from pinned target, got: {payload}"
20610 );
20611 server.join().expect("server thread");
20612 }
20613
20614 #[test]
20618 fn outbound_http_refuses_when_no_vetted_address() {
20619 let url = Url::parse("https://attacker.example/").expect("url");
20620 let error = issue_outbound_http_request(&url, &options(), &empty_headers(), &[])
20621 .expect_err("empty pinned set must be refused");
20622 let message = error.to_string();
20623 assert!(
20624 message.contains("EACCES") || message.contains("ERR_HTTP_REQUEST_FAILED"),
20625 "expected an egress refusal, got: {message}"
20626 );
20627 }
20628}