1use secure_exec_vm_config as vm_config;
4
5use crate::filesystem::{
6 handle_python_vfs_rpc_request as filesystem_handle_python_vfs_rpc_request,
7 service_javascript_fs_sync_rpc, service_javascript_module_sync_rpc,
8};
9use crate::protocol::{
10 BoundUdpSnapshotResponse, CloseStdinRequest, EventFrame, EventPayload, ExecuteRequest,
11 FindBoundUdpRequest, FindListenerRequest, GetProcessSnapshotRequest, GetSignalStateRequest,
12 GetZombieTimerCountRequest, GuestRuntimeKind, JavascriptChildProcessSpawnOptions,
13 JavascriptChildProcessSpawnRequest, JavascriptDgramBindRequest,
14 JavascriptDgramCreateSocketRequest, JavascriptDgramSendRequest, JavascriptDnsLookupRequest,
15 JavascriptDnsResolveRequest, JavascriptNetConnectRequest, JavascriptNetListenRequest,
16 JavascriptNetReserveTcpPortRequest, KillProcessRequest, ListenerSnapshotResponse,
17 OwnershipScope, ProcessExitedEvent, ProcessKilledResponse, ProcessOutputEvent,
18 ProcessSnapshotEntry, ProcessSnapshotResponse, ProcessSnapshotStatus, ProcessStartedResponse,
19 RequestFrame, ResponseFrame, ResponsePayload, SidecarRequestPayload, SignalDispositionAction,
20 SignalHandlerRegistration, SignalStateResponse, SocketStateEntry, StdinClosedResponse,
21 StdinWrittenResponse, StreamChannel, VmFetchRequest, VmFetchResponse, WasmPermissionTier,
22 WriteStdinRequest, ZombieTimerCountResponse,
23};
24use crate::service::{
25 audit_fields, dirname, emit_security_audit_event, emit_structured_event, javascript_error,
26 kernel_error, log_stale_process_event, normalize_host_path, normalize_path,
27 parse_javascript_child_process_spawn_request, path_is_within_root,
28 process_event_queue_overflow_error, python_error, wasm_error, MAX_PROCESS_EVENT_QUEUE,
29};
30use crate::state::{
31 ActiveCipherSession, ActiveDhSession, ActiveDiffieHellmanSession, ActiveEcdhSession,
32 ActiveExecution, ActiveExecutionEvent, ActiveHttp2Server, ActiveHttp2Session,
33 ActiveHttp2Stream, ActiveHttpServer, ActiveMappedHostFd, ActiveProcess, ActiveSqliteDatabase,
34 ActiveSqliteStatement, ActiveTcpListener, ActiveTcpSocket, ActiveTlsState, ActiveTlsStream,
35 ActiveUdpSocket, ActiveUnixListener, ActiveUnixSocket, BridgeError, ExitedProcessSnapshot,
36 Http2BridgeEvent, Http2RuntimeSnapshot, Http2SessionCommand, Http2SessionSnapshot,
37 Http2SocketSnapshot, JavascriptHttpLoopbackTarget, JavascriptSocketFamily,
38 JavascriptSocketPathContext, JavascriptTcpListenerEvent, JavascriptTcpSocketEvent,
39 JavascriptTlsBridgeOptions, JavascriptTlsClientHello, JavascriptTlsDataValue,
40 JavascriptTlsMaterial, JavascriptUdpFamily, JavascriptUdpSocketEvent,
41 JavascriptUnixListenerEvent, NetworkResourceCounts, PendingTcpSocket, PendingUnixSocket,
42 ProcNetEntry, ProcessEventEnvelope, ResolvedChildProcessExecution, ResolvedTcpConnectAddr,
43 SharedBridge, SharedSidecarRequestClient, SidecarKernel, SocketQueryKind, ToolExecution,
44 VmDnsConfig, VmListenPolicy, VmState, DEFAULT_JAVASCRIPT_NET_BACKLOG, EXECUTION_DRIVER_NAME,
45 EXECUTION_SANDBOX_ROOT_ENV, JAVASCRIPT_COMMAND, LOOPBACK_EXEMPT_PORTS_ENV,
46 MAPPED_HOST_FD_START, PYTHON_COMMAND, TOOL_DRIVER_NAME,
47 VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY, WASM_COMMAND, WASM_STDIO_SYNC_RPC_ENV,
48};
49use crate::tools::{
50 format_tool_failure_output, is_tool_command, normalized_tool_command_name,
51 resolve_tool_command, ToolCommandResolution,
52};
53use crate::wire::{ProtocolFrame as WireProtocolFrame, WireFrameCodec, DEFAULT_MAX_FRAME_BYTES};
54use crate::{DispatchResult, NativeSidecar, NativeSidecarBridge, SidecarError};
55
56use base64::Engine;
57use bytes::Bytes;
58use h2::{client, server, Reason};
59use hickory_resolver::proto::rr::{RData, Record, RecordType};
60use hmac::{Hmac, Mac};
61use http::{HeaderMap, HeaderName, HeaderValue, Method, Request, Response, Uri};
62use md5::Md5;
63use nix::libc;
64use nix::sys::signal::{kill as send_signal, Signal};
65use nix::sys::wait::WaitStatus;
66#[cfg(not(target_os = "macos"))]
67use nix::sys::wait::{waitid as wait_on_child, Id as WaitId, WaitPidFlag};
68#[cfg(target_os = "macos")]
69use nix::sys::wait::{waitpid, WaitPidFlag};
70use nix::unistd::Pid;
71use openssl::bn::{BigNum, BigNumContext};
72use openssl::derive::Deriver;
73use openssl::dh::Dh;
74use openssl::ec::{EcGroup, EcKey, EcPoint, PointConversionForm};
75use openssl::hash::MessageDigest;
76use openssl::nid::Nid;
77use openssl::pkey::{Id as PKeyId, PKey, Params, Private, Public};
78use openssl::rand::rand_bytes;
79use openssl::rsa::{Padding, Rsa};
80use openssl::sign::{Signer, Verifier};
81use openssl::symm::{Cipher, Crypter, Mode};
82use pbkdf2::pbkdf2_hmac;
83use rusqlite::types::ValueRef as SqliteValueRef;
84use rusqlite::{
85 Connection as SqliteConnection, OpenFlags as SqliteOpenFlags, Statement as SqliteStatement,
86};
87use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
88use rustls::crypto::aws_lc_rs;
89use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
90use rustls::{
91 ClientConfig, ClientConnection, DigitallySignedStruct, RootCertStore, ServerConfig,
92 ServerConnection, SignatureScheme,
93};
94use scrypt::{scrypt, Params as ScryptParams};
95use secure_exec_bridge::LifecycleState;
96use secure_exec_execution::wasm::WasmExecutionError;
97use secure_exec_execution::{
98 javascript::handle_internal_bridge_call_from_host_context, v8_host::V8SessionHandle,
99 v8_runtime, CreateJavascriptContextRequest, CreatePythonContextRequest,
100 CreateWasmContextRequest, GuestRuntimeConfig, JavascriptExecutionEvent,
101 JavascriptExecutionLimits, JavascriptSyncRpcRequest, ModuleFsReader,
102 NodeSignalDispositionAction, NodeSignalHandlerRegistration, PythonExecutionEvent,
103 PythonExecutionLimits, PythonVfsRpcMethod, PythonVfsRpcRequest, PythonVfsRpcResponsePayload,
104 StartJavascriptExecutionRequest, StartPythonExecutionRequest, StartWasmExecutionRequest,
105 WasmExecutionEvent, WasmExecutionLimits, WasmPermissionTier as ExecutionWasmPermissionTier,
106};
107use secure_exec_kernel::dns::{
108 DnsLookupPolicy, DnsRecordResolution, DnsResolutionSource as KernelDnsResolutionSource,
109};
110use secure_exec_kernel::kernel::{KernelProcessHandle, SpawnOptions, VirtualProcessOptions};
111use secure_exec_kernel::permissions::NetworkOperation;
112use secure_exec_kernel::poll::{PollEvents, PollFd, PollTargetEntry, POLLERR, POLLHUP, POLLIN};
113use secure_exec_kernel::process_table::{ProcessStatus, WaitPidFlags, SIGKILL, SIGTERM};
114use secure_exec_kernel::pty::LineDisciplineConfig;
115use secure_exec_kernel::resource_accounting::ResourceLimits;
116use secure_exec_kernel::root_fs::RootFilesystemMode;
117use secure_exec_kernel::socket_table::{
118 InetSocketAddress, SocketDomain, SocketId, SocketShutdown as KernelSocketShutdown, SocketSpec,
119 SocketState, SocketType,
120};
121use serde::{Deserialize, Serialize};
122use serde_json::{json, Map, Value};
123use sha1::Sha1;
124use sha2::{digest::Digest, Sha256, Sha512};
125use socket2::{SockRef, TcpKeepalive};
126use std::collections::VecDeque;
127use std::collections::{BTreeMap, BTreeSet};
128use std::fmt;
129use std::fs;
130use std::io::{Cursor, Read, Write};
131use std::net::{
132 IpAddr, Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr, TcpListener, TcpStream, ToSocketAddrs,
133 UdpSocket,
134};
135use std::os::unix::fs::{MetadataExt, PermissionsExt};
136use std::os::unix::net::{SocketAddr as UnixSocketAddr, UnixListener, UnixStream};
137use std::path::{Path, PathBuf};
138use std::pin::Pin;
139use std::sync::atomic::{AtomicBool, Ordering};
140use std::sync::mpsc::{self, RecvTimeoutError, Sender};
141use std::sync::{Arc, Mutex, OnceLock, Weak};
142use std::thread;
143use std::time::{Duration, Instant};
144use tokio::io::{AsyncRead, AsyncWrite};
145use tokio::runtime::Builder as TokioRuntimeBuilder;
146use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
147use tokio_rustls::{TlsAcceptor, TlsConnector};
148use url::Url;
149
150const DEFAULT_KERNEL_STDIN_READ_MAX_BYTES: usize = 64 * 1024;
151const DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS: u64 = 100;
152const JAVASCRIPT_NET_TIMEOUT_SENTINEL: &str = "__secure_exec_net_timeout__";
153const PYTHON_PYODIDE_GUEST_ROOT: &str = "/__agent_os_pyodide";
154const PYTHON_PYODIDE_CACHE_GUEST_ROOT: &str = "/__agent_os_pyodide_cache";
155const TCP_SOCKET_POLL_TIMEOUT: Duration = Duration::from_millis(100);
156const TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5);
157const HTTP_LOOPBACK_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
158pub(crate) const MAX_PER_PROCESS_STATE_HANDLES: usize = 1024;
159const VM_FETCH_BUFFER_LIMIT_BYTES: usize = DEFAULT_MAX_FRAME_BYTES;
160const DEFAULT_SCRYPT_COST: u64 = 16_384;
161const DEFAULT_SCRYPT_BLOCK_SIZE: u32 = 8;
162const DEFAULT_SCRYPT_PARALLELIZATION: u32 = 1;
163const SQLITE_JS_SAFE_INTEGER_MAX: i64 = 9_007_199_254_740_991;
164const HTTP_LOOPBACK_REQUEST_TIMEOUT_MS_ENV: &str =
165 "SECURE_EXEC_TEST_HTTP_LOOPBACK_REQUEST_TIMEOUT_MS";
166
167trait Http2AsyncIo: AsyncRead + AsyncWrite + Unpin + Send {}
168
169impl<T> Http2AsyncIo for T where T: AsyncRead + AsyncWrite + Unpin + Send {}
170
171fn http_loopback_request_timeout() -> Duration {
172 static TIMEOUT: OnceLock<Duration> = OnceLock::new();
173 *TIMEOUT.get_or_init(|| {
174 std::env::var(HTTP_LOOPBACK_REQUEST_TIMEOUT_MS_ENV)
175 .ok()
176 .and_then(|value| value.parse::<u64>().ok())
177 .map(Duration::from_millis)
178 .unwrap_or(HTTP_LOOPBACK_REQUEST_TIMEOUT)
179 })
180}
181
182const DEFAULT_ALLOWED_NODE_BUILTINS: &[&str] = &[
183 "assert",
184 "buffer",
185 "console",
186 "child_process",
187 "crypto",
188 "dns",
189 "events",
190 "fs",
191 "http",
192 "http2",
193 "https",
194 "module",
195 "os",
196 "path",
197 "perf_hooks",
198 "querystring",
199 "sqlite",
200 "stream",
201 "string_decoder",
202 "timers",
203 "tls",
204 "tty",
205 "url",
206 "util",
207 "zlib",
208];
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211enum JavascriptCryptoDigestAlgorithm {
212 Md5,
213 Sha1,
214 Sha256,
215 Sha512,
216}
217
218#[derive(Debug, Default, Deserialize)]
219#[serde(default, rename_all = "camelCase")]
220struct JavascriptScryptOptions {
221 #[serde(alias = "N")]
222 cost: Option<u64>,
223 #[serde(alias = "r")]
224 block_size: Option<u32>,
225 #[serde(alias = "p")]
226 parallelization: Option<u32>,
227}
228
229#[derive(Debug, Deserialize)]
230#[serde(rename_all = "camelCase")]
231struct JavascriptHttpListenRequest {
232 server_id: u64,
233 #[serde(default)]
234 port: Option<u16>,
235 #[serde(default)]
236 hostname: Option<String>,
237}
238
239#[derive(Debug, Default, Deserialize)]
240#[serde(default, rename_all = "camelCase")]
241struct JavascriptHttpRequestOptions {
242 method: Option<String>,
243 headers: BTreeMap<String, Value>,
244 body: Option<String>,
245 reject_unauthorized: Option<bool>,
246}
247
248#[derive(Debug, Default, Deserialize)]
249#[serde(default, rename_all = "camelCase")]
250struct JavascriptHttp2ServerListenRequest {
251 server_id: u64,
252 secure: bool,
253 port: Option<u16>,
254 host: Option<String>,
255 backlog: Option<u32>,
256 timeout: Option<u64>,
257 settings: BTreeMap<String, Value>,
258 tls: Option<JavascriptTlsBridgeOptions>,
259}
260
261#[derive(Debug, Default, Deserialize)]
262#[serde(default, rename_all = "camelCase")]
263struct JavascriptHttp2SessionConnectRequest {
264 authority: Option<String>,
265 protocol: Option<String>,
266 host: Option<String>,
267 port: Option<u16>,
268 settings: BTreeMap<String, Value>,
269 tls: Option<JavascriptTlsBridgeOptions>,
270}
271
272#[derive(Debug, Default, Deserialize)]
273#[serde(default, rename_all = "camelCase")]
274struct JavascriptHttp2RequestOptions {
275 end_stream: bool,
276}
277
278#[derive(Debug, Default, Deserialize)]
279#[serde(default, rename_all = "camelCase")]
280struct JavascriptHttp2FileResponseOptions {
281 offset: Option<u64>,
282 length: Option<i64>,
283}
284
285#[derive(Debug, Clone)]
286struct HttpHeaderCollection {
287 normalized: BTreeMap<String, Vec<String>>,
288 raw_pairs: Vec<(String, String)>,
289}
290
291#[derive(Debug)]
292struct InsecureTlsVerifier {
293 supported_schemes: Vec<SignatureScheme>,
294}
295
296impl ServerCertVerifier for InsecureTlsVerifier {
297 fn verify_server_cert(
298 &self,
299 _end_entity: &CertificateDer<'_>,
300 _intermediates: &[CertificateDer<'_>],
301 _server_name: &ServerName<'_>,
302 _ocsp_response: &[u8],
303 _now: rustls::pki_types::UnixTime,
304 ) -> Result<ServerCertVerified, rustls::Error> {
305 Ok(ServerCertVerified::assertion())
306 }
307
308 fn verify_tls12_signature(
309 &self,
310 _message: &[u8],
311 _cert: &CertificateDer<'_>,
312 _dss: &DigitallySignedStruct,
313 ) -> Result<HandshakeSignatureValid, rustls::Error> {
314 Ok(HandshakeSignatureValid::assertion())
315 }
316
317 fn verify_tls13_signature(
318 &self,
319 _message: &[u8],
320 _cert: &CertificateDer<'_>,
321 _dss: &DigitallySignedStruct,
322 ) -> Result<HandshakeSignatureValid, rustls::Error> {
323 Ok(HandshakeSignatureValid::assertion())
324 }
325
326 fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
327 self.supported_schemes.clone()
328 }
329}
330
331impl ActiveProcess {
332 pub(crate) fn new(
333 kernel_pid: u32,
334 kernel_handle: KernelProcessHandle,
335 runtime: GuestRuntimeKind,
336 execution: ActiveExecution,
337 ) -> Self {
338 Self {
339 kernel_pid,
340 kernel_handle,
341 kernel_stdin_writer_fd: None,
342 runtime,
343 detached: false,
344 execution,
345 guest_cwd: String::from("/"),
346 env: BTreeMap::new(),
347 host_cwd: PathBuf::from("/"),
348 mapped_host_fds: BTreeMap::new(),
349 next_mapped_host_fd: MAPPED_HOST_FD_START,
350 pending_execution_events: VecDeque::new(),
351 pending_self_signal_exit: None,
352 child_processes: BTreeMap::new(),
353 next_child_process_id: 0,
354 http_servers: BTreeMap::new(),
355 pending_http_requests: BTreeMap::new(),
356 http2: Default::default(),
357 tcp_listeners: BTreeMap::new(),
358 next_tcp_listener_id: 0,
359 tcp_sockets: BTreeMap::new(),
360 next_tcp_socket_id: 0,
361 tcp_port_reservations: BTreeMap::new(),
362 next_tcp_port_reservation_id: 0,
363 unix_listeners: BTreeMap::new(),
364 next_unix_listener_id: 0,
365 unix_sockets: BTreeMap::new(),
366 next_unix_socket_id: 0,
367 udp_sockets: BTreeMap::new(),
368 next_udp_socket_id: 0,
369 cipher_sessions: BTreeMap::new(),
370 next_cipher_session_id: 0,
371 diffie_hellman_sessions: BTreeMap::new(),
372 next_diffie_hellman_session_id: 0,
373 sqlite_databases: BTreeMap::new(),
374 next_sqlite_database_id: 0,
375 sqlite_statements: BTreeMap::new(),
376 next_sqlite_statement_id: 0,
377 module_resolution_cache: secure_exec_execution::LocalModuleResolutionCache::default(),
378 }
379 }
380
381 pub(crate) fn queue_pending_execution_event(
382 &mut self,
383 event: ActiveExecutionEvent,
384 ) -> Result<(), SidecarError> {
385 if self.pending_execution_events.len() >= MAX_PROCESS_EVENT_QUEUE {
386 return Err(process_event_queue_overflow_error());
387 }
388 self.pending_execution_events.push_back(event);
389 Ok(())
390 }
391
392 pub(crate) fn with_host_cwd(mut self, host_cwd: PathBuf) -> Self {
393 self.host_cwd = host_cwd;
394 self
395 }
396
397 pub(crate) fn with_guest_cwd(mut self, guest_cwd: String) -> Self {
398 self.guest_cwd = guest_cwd;
399 self
400 }
401
402 pub(crate) fn with_env(mut self, env: BTreeMap<String, String>) -> Self {
403 self.env = env;
404 self
405 }
406
407 pub(crate) fn with_kernel_stdin_writer_fd(mut self, fd: u32) -> Self {
408 self.kernel_stdin_writer_fd = Some(fd);
409 self
410 }
411
412 pub(crate) fn with_detached(mut self, detached: bool) -> Self {
413 self.detached = detached;
414 self
415 }
416
417 pub(crate) fn allocate_mapped_host_fd(&mut self, fd: ActiveMappedHostFd) -> u32 {
418 let handle = self.next_mapped_host_fd;
419 self.next_mapped_host_fd = self
420 .next_mapped_host_fd
421 .checked_add(1)
422 .unwrap_or(MAPPED_HOST_FD_START);
423 self.mapped_host_fds.insert(handle, fd);
424 handle
425 }
426
427 pub(crate) fn mapped_host_fd(&self, fd: u32) -> Option<&ActiveMappedHostFd> {
428 self.mapped_host_fds.get(&fd)
429 }
430
431 pub(crate) fn mapped_host_fd_mut(&mut self, fd: u32) -> Option<&mut ActiveMappedHostFd> {
432 self.mapped_host_fds.get_mut(&fd)
433 }
434
435 pub(crate) fn close_mapped_host_fd(&mut self, fd: u32) -> bool {
436 self.mapped_host_fds.remove(&fd).is_some()
437 }
438
439 pub(crate) fn allocate_child_process_id(&mut self) -> String {
440 self.next_child_process_id += 1;
441 format!("child-{}", self.next_child_process_id)
442 }
443
444 fn allocate_tcp_listener_id(&mut self) -> String {
445 self.next_tcp_listener_id += 1;
446 format!("listener-{}", self.next_tcp_listener_id)
447 }
448
449 fn allocate_tcp_socket_id(&mut self) -> String {
450 self.next_tcp_socket_id += 1;
451 format!("socket-{}", self.next_tcp_socket_id)
452 }
453
454 fn allocate_tcp_port_reservation_id(&mut self) -> String {
455 self.next_tcp_port_reservation_id += 1;
456 format!("tcp-port-reservation-{}", self.next_tcp_port_reservation_id)
457 }
458
459 fn allocate_unix_listener_id(&mut self) -> String {
460 self.next_unix_listener_id += 1;
461 format!("unix-listener-{}", self.next_unix_listener_id)
462 }
463
464 fn allocate_unix_socket_id(&mut self) -> String {
465 self.next_unix_socket_id += 1;
466 format!("unix-socket-{}", self.next_unix_socket_id)
467 }
468
469 fn allocate_udp_socket_id(&mut self) -> String {
470 self.next_udp_socket_id += 1;
471 format!("udp-socket-{}", self.next_udp_socket_id)
472 }
473
474 pub(crate) fn network_resource_counts(&self) -> NetworkResourceCounts {
475 let mut counts = NetworkResourceCounts {
476 sockets: self.http_servers.len()
477 + self.tcp_listeners.len()
478 + self.tcp_sockets.len()
479 + self.unix_listeners.len()
480 + self.unix_sockets.len()
481 + self.udp_sockets.len(),
482 connections: self.tcp_sockets.len() + self.unix_sockets.len(),
483 };
484 if let Ok(http2) = self.http2.shared.lock() {
485 counts.sockets += http2.servers.len() + http2.sessions.len();
486 counts.connections += http2.sessions.len();
487 }
488
489 for child in self.child_processes.values() {
490 let child_counts = child.network_resource_counts();
491 counts.sockets += child_counts.sockets;
492 counts.connections += child_counts.connections;
493 }
494
495 counts
496 }
497
498 fn sidecar_only_network_resource_counts(&self) -> NetworkResourceCounts {
499 let mut counts = NetworkResourceCounts {
500 sockets: self.http_servers.len()
501 + self
502 .tcp_listeners
503 .values()
504 .filter(|listener| listener.kernel_socket_id.is_none())
505 .count()
506 + self
507 .tcp_sockets
508 .values()
509 .filter(|socket| socket.kernel_socket_id.is_none())
510 .count()
511 + self.unix_listeners.len()
512 + self.unix_sockets.len()
513 + self
514 .udp_sockets
515 .values()
516 .filter(|socket| socket.kernel_socket_id.is_none())
517 .count(),
518 connections: self
519 .tcp_sockets
520 .values()
521 .filter(|socket| socket.kernel_socket_id.is_none())
522 .count()
523 + self.unix_sockets.len(),
524 };
525 if let Ok(http2) = self.http2.shared.lock() {
526 counts.sockets += http2.servers.len() + http2.sessions.len();
527 counts.connections += http2.sessions.len();
528 }
529
530 for child in self.child_processes.values() {
531 let child_counts = child.sidecar_only_network_resource_counts();
532 counts.sockets += child_counts.sockets;
533 counts.connections += child_counts.connections;
534 }
535
536 counts
537 }
538}
539
540fn poll_tool_process_event(
541 execution: &ToolExecution,
542) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
543 let event = execution
544 .pending_events
545 .lock()
546 .unwrap_or_else(|poisoned| poisoned.into_inner())
547 .pop_front();
548 if event.is_some() {
549 return Ok(event);
550 }
551 if execution.events_overflowed.load(Ordering::Relaxed) {
552 return Err(process_event_queue_overflow_error());
553 }
554 Ok(None)
555}
556
557fn descendant_pending_execution_event_capacity(
558 root: &ActiveProcess,
559 child_path: &[&str],
560) -> Option<usize> {
561 let mut child = root;
562 for child_process_id in child_path {
563 child = child.child_processes.get(*child_process_id)?;
564 }
565 Some(MAX_PROCESS_EVENT_QUEUE.saturating_sub(child.pending_execution_events.len()))
566}
567
568fn poll_child_execution_after_exit(
569 child: &mut ActiveProcess,
570 wait: Duration,
571) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
572 match child.execution.poll_event_blocking(wait) {
573 Ok(event) => Ok(event),
574 Err(SidecarError::Execution(message))
575 if child.runtime == GuestRuntimeKind::WebAssembly
576 && message == WasmExecutionError::EventChannelClosed.to_string() =>
577 {
578 Ok(None)
579 }
580 Err(error) => Err(error),
581 }
582}
583
584fn closed_javascript_event_channel(message: &str) -> bool {
585 message == "guest JavaScript event channel closed unexpectedly"
586}
587
588fn closed_python_event_channel(message: &str) -> bool {
589 message == "guest Python event channel closed unexpectedly"
590}
591
592fn closed_wasm_event_channel(message: &str) -> bool {
593 message == WasmExecutionError::EventChannelClosed.to_string()
594}
595
596fn missing_vm_error(vm_id: &str) -> SidecarError {
597 SidecarError::InvalidState(format!("VM {vm_id} is no longer active"))
598}
599
600fn missing_process_error(vm_id: &str, process_id: &str) -> SidecarError {
601 SidecarError::InvalidState(format!(
602 "VM {vm_id} no longer has active process {process_id}"
603 ))
604}
605
606fn is_broken_pipe_error(error: &SidecarError) -> bool {
607 matches!(error, SidecarError::Execution(message) if message.contains("Broken pipe") || message.contains("os error 32") || message.contains("EPIPE"))
608}
609
610fn javascript_child_process_gone_error(process_id: &str, child_path: &[&str]) -> SidecarError {
611 let child_label = if child_path.is_empty() {
612 process_id.to_owned()
613 } else {
614 format!("{process_id}/{}", child_path.join("/"))
615 };
616 SidecarError::Execution(format!(
617 "ECHILD: child_process {child_label} is no longer available"
618 ))
619}
620
621fn is_javascript_child_process_gone_error(error: &SidecarError) -> bool {
622 matches!(
623 error,
624 SidecarError::Execution(message) if guest_errno_code(message) == Some("ECHILD")
625 )
626}
627
628fn loopback_tls_transport_registry(
629) -> &'static Mutex<BTreeMap<String, Weak<crate::state::LoopbackTlsTransportPair>>> {
630 static REGISTRY: OnceLock<
631 Mutex<BTreeMap<String, Weak<crate::state::LoopbackTlsTransportPair>>>,
632 > = OnceLock::new();
633 REGISTRY.get_or_init(|| Mutex::new(BTreeMap::new()))
634}
635
636fn loopback_tls_transport_key(
637 vm_id: &str,
638 socket_id: SocketId,
639 peer_socket_id: SocketId,
640) -> String {
641 let (lower, higher) = if socket_id <= peer_socket_id {
642 (socket_id, peer_socket_id)
643 } else {
644 (peer_socket_id, socket_id)
645 };
646 format!("{vm_id}:{lower}:{higher}")
647}
648
649fn loopback_tls_endpoint(
650 vm_id: &str,
651 socket_id: SocketId,
652 peer_socket_id: SocketId,
653) -> Result<crate::state::LoopbackTlsEndpoint, SidecarError> {
654 let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
655 let registry = loopback_tls_transport_registry();
656 let mut transports = registry.lock().map_err(|_| {
657 SidecarError::InvalidState(String::from(
658 "loopback TLS transport registry lock poisoned",
659 ))
660 })?;
661 transports.retain(|_, pair| pair.strong_count() > 0);
662 let pair = transports
663 .get(&key)
664 .and_then(Weak::upgrade)
665 .unwrap_or_else(|| {
666 let pair = Arc::new(crate::state::LoopbackTlsTransportPair {
667 state: Mutex::new(crate::state::LoopbackTlsTransportPairState::default()),
668 ready: std::sync::Condvar::new(),
669 });
670 transports.insert(key, Arc::downgrade(&pair));
671 pair
672 });
673 Ok(crate::state::LoopbackTlsEndpoint {
674 pair,
675 is_lower_socket: socket_id <= peer_socket_id,
676 })
677}
678
679impl crate::state::LoopbackTlsEndpoint {
680 fn shutdown_write(&self) -> Result<(), SidecarError> {
681 let mut state = self.pair.state.lock().map_err(|_| {
682 SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
683 })?;
684 if self.is_lower_socket {
685 state.lower_write_closed = true;
686 } else {
687 state.higher_write_closed = true;
688 }
689 self.pair.ready.notify_all();
690 Ok(())
691 }
692
693 fn close_endpoint(&self) -> Result<(), SidecarError> {
694 let mut state = self.pair.state.lock().map_err(|_| {
695 SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
696 })?;
697 if self.is_lower_socket {
698 state.lower_write_closed = true;
699 state.lower_closed = true;
700 } else {
701 state.higher_write_closed = true;
702 state.higher_closed = true;
703 }
704 self.pair.ready.notify_all();
705 Ok(())
706 }
707}
708
709fn parse_tls_client_hello_from_bytes(
710 buffer: &[u8],
711) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
712 if buffer.is_empty() {
713 return Ok(None);
714 }
715
716 let mut acceptor = rustls::server::Acceptor::default();
717 let mut cursor = Cursor::new(buffer);
718 acceptor.read_tls(&mut cursor).map_err(sidecar_net_error)?;
719 let Some(accepted) = acceptor.accept().map_err(|(error, _)| {
720 SidecarError::Execution(format!("failed to parse TLS client hello: {error}"))
721 })?
722 else {
723 return Ok(None);
724 };
725 let client_hello = accepted.client_hello();
726 let alpn_protocols = client_hello.alpn().map(|protocols| {
727 protocols
728 .filter_map(|protocol| String::from_utf8(protocol.to_vec()).ok())
729 .collect::<Vec<_>>()
730 });
731 Ok(Some(JavascriptTlsClientHello {
732 servername: client_hello.server_name().map(str::to_owned),
733 alpn_protocols,
734 }))
735}
736
737fn peek_loopback_tls_client_hello(
738 vm_id: &str,
739 socket_id: SocketId,
740 peer_socket_id: SocketId,
741) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
742 let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
743 let registry = loopback_tls_transport_registry();
744 let pair = registry
745 .lock()
746 .map_err(|_| {
747 SidecarError::InvalidState(String::from(
748 "loopback TLS transport registry lock poisoned",
749 ))
750 })?
751 .get(&key)
752 .and_then(Weak::upgrade);
753 let Some(pair) = pair else {
754 return Ok(None);
755 };
756 let is_lower_socket = socket_id <= peer_socket_id;
757 let state = pair.state.lock().map_err(|_| {
758 SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
759 })?;
760 let buffered = if is_lower_socket {
761 state.higher_to_lower.iter().copied().collect::<Vec<_>>()
762 } else {
763 state.lower_to_higher.iter().copied().collect::<Vec<_>>()
764 };
765 drop(state);
766 parse_tls_client_hello_from_bytes(&buffered)
767}
768
769fn wait_for_loopback_peer_socket_id(
770 kernel: &SidecarKernel,
771 socket_id: SocketId,
772) -> Option<SocketId> {
773 for _ in 0..50 {
774 if let Some(peer_socket_id) = kernel
775 .socket_get(socket_id)
776 .and_then(|record| record.peer_socket_id())
777 {
778 return Some(peer_socket_id);
779 }
780 std::thread::sleep(Duration::from_millis(10));
781 }
782 None
783}
784
785impl Drop for crate::state::LoopbackTlsEndpoint {
786 fn drop(&mut self) {
787 let _ = self.close_endpoint();
788 }
789}
790
791impl Read for crate::state::LoopbackTlsEndpoint {
792 fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
793 let mut state = self
794 .pair
795 .state
796 .lock()
797 .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
798
799 loop {
800 let (peer_write_closed, peer_closed) = if self.is_lower_socket {
801 (state.higher_write_closed, state.higher_closed)
802 } else {
803 (state.lower_write_closed, state.lower_closed)
804 };
805
806 let incoming = if self.is_lower_socket {
807 &mut state.higher_to_lower
808 } else {
809 &mut state.lower_to_higher
810 };
811
812 if !incoming.is_empty() {
813 let mut count = 0;
814 while count < buffer.len() {
815 let Some(byte) = incoming.pop_front() else {
816 break;
817 };
818 buffer[count] = byte;
819 count += 1;
820 }
821 return Ok(count);
822 }
823
824 if peer_write_closed || peer_closed {
825 return Ok(0);
826 }
827
828 let (next_state, wait_result) = self
829 .pair
830 .ready
831 .wait_timeout(state, TCP_SOCKET_POLL_TIMEOUT)
832 .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
833 state = next_state;
834 if wait_result.timed_out() {
835 return Err(std::io::Error::new(
836 std::io::ErrorKind::WouldBlock,
837 "loopback TLS transport read timed out",
838 ));
839 }
840 }
841 }
842}
843
844impl Write for crate::state::LoopbackTlsEndpoint {
845 fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
846 let mut state = self
847 .pair
848 .state
849 .lock()
850 .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
851
852 let peer_closed = if self.is_lower_socket {
853 state.higher_closed
854 } else {
855 state.lower_closed
856 };
857 let outgoing = if self.is_lower_socket {
858 &mut state.lower_to_higher
859 } else {
860 &mut state.higher_to_lower
861 };
862 if peer_closed {
863 return Err(std::io::Error::new(
864 std::io::ErrorKind::BrokenPipe,
865 "loopback TLS peer is closed",
866 ));
867 }
868
869 outgoing.extend(buffer.iter().copied());
870 self.pair.ready.notify_all();
871 Ok(buffer.len())
872 }
873
874 fn flush(&mut self) -> std::io::Result<()> {
875 Ok(())
876 }
877}
878
879struct ActiveTcpConnectRequest<'a, B> {
882 bridge: &'a SharedBridge<B>,
883 kernel: &'a mut SidecarKernel,
884 kernel_pid: u32,
885 vm_id: &'a str,
886 dns: &'a VmDnsConfig,
887 host: &'a str,
888 port: u16,
889 local_address: Option<&'a str>,
890 local_port: Option<u16>,
891 local_reservation: Option<(JavascriptSocketFamily, u16)>,
892 context: &'a JavascriptSocketPathContext,
893}
894
895struct ActiveUdpSendToRequest<'a, B> {
896 bridge: &'a SharedBridge<B>,
897 kernel: &'a mut SidecarKernel,
898 kernel_pid: u32,
899 vm_id: &'a str,
900 dns: &'a VmDnsConfig,
901 host: &'a str,
902 port: u16,
903 context: &'a JavascriptSocketPathContext,
904 contents: &'a [u8],
905}
906
907struct UdpRemoteAddrRequest<'a, B> {
908 bridge: &'a SharedBridge<B>,
909 kernel: &'a SidecarKernel,
910 vm_id: &'a str,
911 dns: &'a VmDnsConfig,
912 host: &'a str,
913 port: u16,
914 family: JavascriptUdpFamily,
915 context: &'a JavascriptSocketPathContext,
916}
917
918pub(crate) struct JavascriptSyncRpcServiceRequest<'a, B> {
919 pub(crate) bridge: &'a SharedBridge<B>,
920 pub(crate) vm_id: &'a str,
921 pub(crate) dns: &'a VmDnsConfig,
922 pub(crate) socket_paths: &'a JavascriptSocketPathContext,
923 pub(crate) kernel: &'a mut SidecarKernel,
924 pub(crate) process: &'a mut ActiveProcess,
925 pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
926 pub(crate) resource_limits: &'a ResourceLimits,
927 pub(crate) network_counts: NetworkResourceCounts,
928}
929
930pub(crate) struct JavascriptNetSyncRpcServiceRequest<'a, B> {
931 pub(crate) bridge: &'a SharedBridge<B>,
932 pub(crate) vm_id: &'a str,
933 pub(crate) dns: &'a VmDnsConfig,
934 pub(crate) socket_paths: &'a JavascriptSocketPathContext,
935 pub(crate) kernel: &'a mut SidecarKernel,
936 pub(crate) process: &'a mut ActiveProcess,
937 pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
938 pub(crate) resource_limits: &'a ResourceLimits,
939 pub(crate) network_counts: NetworkResourceCounts,
940}
941
942struct LoopbackHttpResponseWaitRequest<'a, B> {
943 bridge: &'a SharedBridge<B>,
944 vm_id: &'a str,
945 dns: &'a VmDnsConfig,
946 socket_paths: &'a JavascriptSocketPathContext,
947 kernel: &'a mut SidecarKernel,
948 process: &'a mut ActiveProcess,
949 resource_limits: &'a ResourceLimits,
950 request_key: (u64, u64),
951}
952
953pub(crate) struct LoopbackHttpDispatchRequest<'a, B> {
954 pub(crate) bridge: &'a SharedBridge<B>,
955 pub(crate) vm_id: &'a str,
956 pub(crate) dns: &'a VmDnsConfig,
957 pub(crate) socket_paths: &'a JavascriptSocketPathContext,
958 pub(crate) kernel: &'a mut SidecarKernel,
959 pub(crate) process: &'a mut ActiveProcess,
960 pub(crate) resource_limits: &'a ResourceLimits,
961 pub(crate) server_id: u64,
962 pub(crate) request_json: &'a str,
963}
964
965struct JavascriptDgramSyncRpcServiceRequest<'a, B> {
966 bridge: &'a SharedBridge<B>,
967 kernel: &'a mut SidecarKernel,
968 vm_id: &'a str,
969 dns: &'a VmDnsConfig,
970 socket_paths: &'a JavascriptSocketPathContext,
971 process: &'a mut ActiveProcess,
972 sync_request: &'a JavascriptSyncRpcRequest,
973 resource_limits: &'a ResourceLimits,
974 network_counts: NetworkResourceCounts,
975}
976
977struct JavascriptHttp2SyncRpcServiceRequest<'a, B> {
978 bridge: &'a SharedBridge<B>,
979 kernel: &'a mut SidecarKernel,
980 vm_id: &'a str,
981 dns: &'a VmDnsConfig,
982 socket_paths: &'a JavascriptSocketPathContext,
983 process: &'a mut ActiveProcess,
984 sync_request: &'a JavascriptSyncRpcRequest,
985 resource_limits: &'a ResourceLimits,
986 network_counts: NetworkResourceCounts,
987}
988
989impl ActiveTcpSocket {
990 fn connect<B>(request: ActiveTcpConnectRequest<'_, B>) -> Result<Self, SidecarError>
991 where
992 B: NativeSidecarBridge + Send + 'static,
993 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
994 {
995 let ActiveTcpConnectRequest {
996 bridge,
997 kernel,
998 kernel_pid,
999 vm_id,
1000 dns,
1001 host,
1002 port,
1003 local_address,
1004 local_port,
1005 local_reservation,
1006 context,
1007 } = request;
1008 let resolved = resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, context)?;
1009 if resolved.use_kernel_loopback {
1010 let family = JavascriptSocketFamily::from_ip(resolved.guest_remote_addr.ip());
1011 let requested_local_port = local_port.unwrap_or(0);
1012 let local_port = if requested_local_port != 0
1013 && local_reservation == Some((family, requested_local_port))
1014 {
1015 requested_local_port
1016 } else {
1017 allocate_guest_listen_port(
1018 requested_local_port,
1019 family,
1020 &context.used_tcp_guest_ports,
1021 context.listen_policy,
1022 )?
1023 };
1024 let local_ip = match (family, local_address) {
1025 (JavascriptSocketFamily::Ipv4, Some("0.0.0.0")) => {
1026 IpAddr::V4(Ipv4Addr::UNSPECIFIED)
1027 }
1028 (JavascriptSocketFamily::Ipv4, Some("127.0.0.1") | Some("localhost") | None) => {
1029 IpAddr::V4(Ipv4Addr::LOCALHOST)
1030 }
1031 (JavascriptSocketFamily::Ipv6, Some("::")) => IpAddr::V6(Ipv6Addr::UNSPECIFIED),
1032 (JavascriptSocketFamily::Ipv6, Some("::1") | Some("localhost") | None) => {
1033 IpAddr::V6(Ipv6Addr::LOCALHOST)
1034 }
1035 (JavascriptSocketFamily::Ipv4, Some(other)) => {
1036 return Err(SidecarError::Execution(format!(
1037 "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1038 )));
1039 }
1040 (JavascriptSocketFamily::Ipv6, Some(other)) => {
1041 return Err(SidecarError::Execution(format!(
1042 "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1043 )));
1044 }
1045 };
1046 let local_addr = SocketAddr::new(local_ip, local_port);
1047 let spec = match family {
1048 JavascriptSocketFamily::Ipv4 => SocketSpec::tcp(),
1049 JavascriptSocketFamily::Ipv6 => {
1050 SocketSpec::new(SocketDomain::Inet6, SocketType::Stream)
1051 }
1052 };
1053 let socket_id = kernel
1054 .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
1055 .map_err(kernel_error)?;
1056 kernel
1057 .socket_bind_inet(
1058 EXECUTION_DRIVER_NAME,
1059 kernel_pid,
1060 socket_id,
1061 InetSocketAddress::new(local_ip.to_string(), local_port),
1062 )
1063 .map_err(kernel_error)?;
1064 kernel
1065 .socket_connect_inet_loopback(
1066 EXECUTION_DRIVER_NAME,
1067 kernel_pid,
1068 socket_id,
1069 InetSocketAddress::new(
1070 resolved.guest_remote_addr.ip().to_string(),
1071 resolved.guest_remote_addr.port(),
1072 ),
1073 )
1074 .map_err(kernel_error)?;
1075 return Ok(Self::from_kernel(
1076 socket_id,
1077 None,
1078 local_addr,
1079 resolved.guest_remote_addr,
1080 ));
1081 }
1082
1083 let stream = TcpStream::connect_timeout(&resolved.actual_addr, Duration::from_secs(30))
1084 .map_err(sidecar_net_error)?;
1085 let guest_local_addr = stream.local_addr().map_err(sidecar_net_error)?;
1086 Self::from_stream(stream, None, guest_local_addr, resolved.guest_remote_addr)
1087 }
1088
1089 fn from_stream(
1090 stream: TcpStream,
1091 listener_id: Option<String>,
1092 guest_local_addr: SocketAddr,
1093 guest_remote_addr: SocketAddr,
1094 ) -> Result<Self, SidecarError> {
1095 let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
1096 read_stream
1097 .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
1098 .map_err(sidecar_net_error)?;
1099 let stream = Arc::new(Mutex::new(stream));
1100 let pending_read_stream = Arc::new(Mutex::new(Some(read_stream)));
1101 let (sender, events) = mpsc::channel();
1102 let tls_mode = Arc::new(AtomicBool::new(false));
1103 let tls_stream = Arc::new(Mutex::new(None));
1104 let tls_state = Arc::new(Mutex::new(None));
1105 let saw_local_shutdown = Arc::new(AtomicBool::new(false));
1106 let saw_remote_end = Arc::new(AtomicBool::new(false));
1107 let close_notified = Arc::new(AtomicBool::new(false));
1108
1109 Ok(Self {
1110 stream: Some(stream),
1111 pending_read_stream: Some(pending_read_stream),
1112 events: Some(events),
1113 event_sender: Some(sender),
1114 kernel_socket_id: None,
1115 no_delay: false,
1116 keep_alive: false,
1117 keep_alive_initial_delay_secs: None,
1118 guest_local_addr,
1119 guest_remote_addr,
1120 listener_id,
1121 tls_mode,
1122 tls_stream,
1123 tls_state,
1124 saw_local_shutdown,
1125 saw_remote_end,
1126 close_notified,
1127 })
1128 }
1129
1130 fn from_kernel(
1131 socket_id: SocketId,
1132 listener_id: Option<String>,
1133 guest_local_addr: SocketAddr,
1134 guest_remote_addr: SocketAddr,
1135 ) -> Self {
1136 let (sender, events) = mpsc::channel();
1137 Self {
1138 stream: None,
1139 pending_read_stream: None,
1140 events: Some(events),
1141 event_sender: Some(sender),
1142 kernel_socket_id: Some(socket_id),
1143 no_delay: false,
1144 keep_alive: false,
1145 keep_alive_initial_delay_secs: None,
1146 guest_local_addr,
1147 guest_remote_addr,
1148 listener_id,
1149 tls_mode: Arc::new(AtomicBool::new(false)),
1150 tls_stream: Arc::new(Mutex::new(None)),
1151 tls_state: Arc::new(Mutex::new(None)),
1152 saw_local_shutdown: Arc::new(AtomicBool::new(false)),
1153 saw_remote_end: Arc::new(AtomicBool::new(false)),
1154 close_notified: Arc::new(AtomicBool::new(false)),
1155 }
1156 }
1157
1158 fn poll(
1159 &mut self,
1160 kernel: &mut SidecarKernel,
1161 kernel_pid: u32,
1162 wait: Duration,
1163 ) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
1164 if self.tls_mode.load(Ordering::SeqCst) {
1165 self.ensure_tcp_reader()?;
1166 return match self
1167 .events
1168 .as_ref()
1169 .ok_or_else(|| {
1170 SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1171 })?
1172 .recv_timeout(wait)
1173 {
1174 Ok(event) => Ok(Some(event)),
1175 Err(RecvTimeoutError::Timeout) => Ok(None),
1176 Err(RecvTimeoutError::Disconnected) => Ok(None),
1177 };
1178 }
1179
1180 if let Some(socket_id) = self.kernel_socket_id {
1181 let result = kernel
1182 .poll_targets(
1183 EXECUTION_DRIVER_NAME,
1184 kernel_pid,
1185 vec![PollTargetEntry::socket(
1186 socket_id,
1187 POLLIN | POLLHUP | POLLERR,
1188 )],
1189 i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
1190 )
1191 .map_err(kernel_error)?;
1192 let revents = result
1193 .targets
1194 .first()
1195 .map(|entry| entry.revents)
1196 .unwrap_or_else(PollEvents::empty);
1197 if revents.is_empty() {
1198 return Ok(None);
1199 }
1200 if revents.intersects(POLLIN) {
1201 return match kernel.socket_read(
1202 EXECUTION_DRIVER_NAME,
1203 kernel_pid,
1204 socket_id,
1205 64 * 1024,
1206 ) {
1207 Ok(Some(bytes)) if !bytes.is_empty() => {
1208 Ok(Some(JavascriptTcpSocketEvent::Data(bytes)))
1209 }
1210 Ok(Some(_)) => Ok(Some(JavascriptTcpSocketEvent::Data(Vec::new()))),
1211 Ok(None) => Ok(Some(JavascriptTcpSocketEvent::End)),
1212 Err(error) if error.code() == "EAGAIN" => Ok(None),
1213 Err(error) => Ok(Some(JavascriptTcpSocketEvent::Error {
1214 code: Some(error.code().to_string()),
1215 message: error.to_string(),
1216 })),
1217 };
1218 }
1219 if revents.intersects(POLLHUP) {
1220 return Ok(Some(JavascriptTcpSocketEvent::End));
1221 }
1222 if revents.intersects(POLLERR) {
1223 return Ok(Some(JavascriptTcpSocketEvent::Error {
1224 code: Some(String::from("EPIPE")),
1225 message: String::from("kernel TCP socket reported POLLERR"),
1226 }));
1227 }
1228 return Ok(None);
1229 }
1230
1231 self.ensure_tcp_reader()?;
1232 match self
1233 .events
1234 .as_ref()
1235 .ok_or_else(|| {
1236 SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1237 })?
1238 .recv_timeout(wait)
1239 {
1240 Ok(event) => Ok(Some(event)),
1241 Err(RecvTimeoutError::Timeout) => Ok(None),
1242 Err(RecvTimeoutError::Disconnected) => Ok(None),
1243 }
1244 }
1245
1246 fn ensure_tcp_reader(&self) -> Result<(), SidecarError> {
1247 if self.kernel_socket_id.is_some() {
1248 return Ok(());
1249 }
1250 if self.tls_mode.load(Ordering::SeqCst) {
1251 return Ok(());
1252 }
1253 let read_stream = self
1254 .pending_read_stream
1255 .as_ref()
1256 .ok_or_else(|| {
1257 SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1258 })?
1259 .lock()
1260 .map_err(|_| {
1261 SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1262 })?
1263 .take();
1264 if let Some(read_stream) = read_stream {
1265 spawn_tcp_socket_reader(
1266 read_stream,
1267 self.event_sender
1268 .as_ref()
1269 .ok_or_else(|| {
1270 SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1271 })?
1272 .clone(),
1273 Arc::clone(&self.tls_mode),
1274 Arc::clone(&self.saw_local_shutdown),
1275 Arc::clone(&self.saw_remote_end),
1276 Arc::clone(&self.close_notified),
1277 );
1278 }
1279 Ok(())
1280 }
1281
1282 fn socket_info(&self) -> Value {
1283 json!({
1284 "localAddress": self.guest_local_addr.ip().to_string(),
1285 "localPort": self.guest_local_addr.port(),
1286 "localFamily": socket_addr_family(&self.guest_local_addr),
1287 "remoteAddress": self.guest_remote_addr.ip().to_string(),
1288 "remotePort": self.guest_remote_addr.port(),
1289 "remoteFamily": socket_addr_family(&self.guest_remote_addr),
1290 })
1291 }
1292
1293 fn set_no_delay(&mut self, enable: bool) -> Result<(), SidecarError> {
1294 self.no_delay = enable;
1295 if self.kernel_socket_id.is_some() {
1296 return Ok(());
1297 }
1298 let stream = self
1299 .stream
1300 .as_ref()
1301 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1302 .lock()
1303 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1304 stream.set_nodelay(enable).map_err(sidecar_net_error)
1305 }
1306
1307 fn set_keep_alive(
1308 &mut self,
1309 enable: bool,
1310 initial_delay_secs: Option<u64>,
1311 ) -> Result<(), SidecarError> {
1312 self.keep_alive = enable;
1313 self.keep_alive_initial_delay_secs = initial_delay_secs;
1314 if self.kernel_socket_id.is_some() {
1315 return Ok(());
1316 }
1317 let stream = self
1318 .stream
1319 .as_ref()
1320 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1321 .lock()
1322 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1323 let socket = SockRef::from(&*stream);
1324 socket.set_keepalive(enable).map_err(sidecar_net_error)?;
1325 if enable {
1326 if let Some(delay_secs) = initial_delay_secs.filter(|delay_secs| *delay_secs > 0) {
1327 socket
1328 .set_tcp_keepalive(
1329 &TcpKeepalive::new().with_time(Duration::from_secs(delay_secs)),
1330 )
1331 .map_err(sidecar_net_error)?;
1332 }
1333 }
1334 Ok(())
1335 }
1336
1337 fn upgrade_tls(
1338 &self,
1339 vm_id: &str,
1340 kernel: &SidecarKernel,
1341 options: JavascriptTlsBridgeOptions,
1342 ) -> Result<(), SidecarError> {
1343 if self.tls_mode.load(Ordering::SeqCst) {
1344 return Ok(());
1345 }
1346
1347 let client_hello = if options.is_server {
1348 self.peek_tls_client_hello(vm_id, kernel)?
1349 } else {
1350 None
1351 };
1352
1353 let tls_stream = if let Some(socket_id) = self.kernel_socket_id {
1354 let peer_socket_id = wait_for_loopback_peer_socket_id(kernel, socket_id)
1355 .ok_or_else(|| {
1356 SidecarError::Execution(format!(
1357 "ERR_NOT_IMPLEMENTED: kernel-backed loopback socket {socket_id} has no peer for TLS upgrade"
1358 ))
1359 })?;
1360 let endpoint = loopback_tls_endpoint(vm_id, socket_id, peer_socket_id)?;
1361 if options.is_server {
1362 ActiveTlsStream::LoopbackServer(build_server_loopback_tls_stream(
1363 endpoint, &options,
1364 )?)
1365 } else {
1366 ActiveTlsStream::LoopbackClient(build_client_loopback_tls_stream(
1367 endpoint, &options,
1368 )?)
1369 }
1370 } else {
1371 self.pending_read_stream
1372 .as_ref()
1373 .ok_or_else(|| {
1374 SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1375 })?
1376 .lock()
1377 .map_err(|_| {
1378 SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1379 })?
1380 .take();
1381 let stream = self
1382 .stream
1383 .as_ref()
1384 .ok_or_else(|| {
1385 SidecarError::InvalidState(String::from("TCP socket stream missing"))
1386 })?
1387 .lock()
1388 .map_err(|_| {
1389 SidecarError::InvalidState(String::from("TCP socket lock poisoned"))
1390 })?;
1391 let cloned = stream.try_clone().map_err(sidecar_net_error)?;
1392 drop(stream);
1393
1394 if options.is_server {
1395 ActiveTlsStream::Server(build_server_tls_stream(cloned, &options)?)
1396 } else {
1397 ActiveTlsStream::Client(build_client_tls_stream(cloned, &options)?)
1398 }
1399 };
1400
1401 let tls_state = ActiveTlsState {
1402 client_hello,
1403 local_certificates: tls_local_certificates(&options)?,
1404 session_reused: false,
1405 };
1406
1407 self.tls_mode.store(true, Ordering::SeqCst);
1408 {
1409 let mut state = self
1410 .tls_state
1411 .lock()
1412 .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?;
1413 *state = Some(tls_state);
1414 }
1415 {
1416 let mut stream = self.tls_stream.lock().map_err(|_| {
1417 SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
1418 })?;
1419 *stream = Some(tls_stream);
1420 }
1421
1422 spawn_tls_socket_reader(
1423 Arc::clone(&self.tls_stream),
1424 self.event_sender
1425 .as_ref()
1426 .ok_or_else(|| {
1427 SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1428 })?
1429 .clone(),
1430 Arc::clone(&self.saw_local_shutdown),
1431 Arc::clone(&self.saw_remote_end),
1432 Arc::clone(&self.close_notified),
1433 );
1434 Ok(())
1435 }
1436
1437 fn peek_tls_client_hello(
1438 &self,
1439 vm_id: &str,
1440 kernel: &SidecarKernel,
1441 ) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
1442 if let Some(socket_id) = self.kernel_socket_id {
1443 let Some(peer_socket_id) = kernel
1444 .socket_get(socket_id)
1445 .and_then(|record| record.peer_socket_id())
1446 else {
1447 return Ok(None);
1448 };
1449 return peek_loopback_tls_client_hello(vm_id, socket_id, peer_socket_id);
1450 }
1451
1452 let stream = self
1453 .stream
1454 .as_ref()
1455 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1456 .lock()
1457 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1458 let mut buffer = vec![0_u8; 16 * 1024];
1459 let bytes = match stream.peek(&mut buffer) {
1460 Ok(0) => return Ok(None),
1461 Ok(bytes) => bytes,
1462 Err(error)
1463 if matches!(
1464 error.kind(),
1465 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
1466 ) =>
1467 {
1468 return Ok(None);
1469 }
1470 Err(error) => return Err(sidecar_net_error(error)),
1471 };
1472 parse_tls_client_hello_from_bytes(&buffer[..bytes])
1473 }
1474
1475 fn tls_client_hello_json(
1476 &self,
1477 vm_id: &str,
1478 kernel: &SidecarKernel,
1479 ) -> Result<Value, SidecarError> {
1480 if let Some(client_hello) = self
1481 .tls_state
1482 .lock()
1483 .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
1484 .as_ref()
1485 .and_then(|state| state.client_hello.clone())
1486 {
1487 return javascript_net_json_string(
1488 serde_json::to_value(client_hello).map_err(|error| {
1489 SidecarError::InvalidState(format!(
1490 "failed to serialize TLS client hello: {error}"
1491 ))
1492 })?,
1493 "net.socket_get_tls_client_hello",
1494 );
1495 }
1496
1497 javascript_net_json_string(
1498 serde_json::to_value(
1499 self.peek_tls_client_hello(vm_id, kernel)?
1500 .unwrap_or_default(),
1501 )
1502 .map_err(|error| {
1503 SidecarError::InvalidState(format!("failed to serialize TLS client hello: {error}"))
1504 })?,
1505 "net.socket_get_tls_client_hello",
1506 )
1507 }
1508
1509 fn tls_query(&self, query: &str, detailed: bool) -> Result<Value, SidecarError> {
1510 let state = self
1511 .tls_state
1512 .lock()
1513 .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
1514 .clone();
1515 let mut tls_stream = self
1516 .tls_stream
1517 .lock()
1518 .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?;
1519 let Some(stream) = tls_stream.as_mut() else {
1520 return javascript_net_json_string(
1521 tls_bridge_undefined_value(),
1522 "net.socket_tls_query",
1523 );
1524 };
1525
1526 let payload = match query {
1527 "getSession" => tls_bridge_undefined_value(),
1528 "isSessionReused" => Value::Bool(
1529 state
1530 .as_ref()
1531 .is_some_and(|tls_state| tls_state.session_reused),
1532 ),
1533 "getPeerCertificate" => {
1534 let certificate = stream
1535 .peer_certificates()
1536 .and_then(|certificates| certificates.first())
1537 .map(|certificate| {
1538 tls_certificate_bridge_value(certificate.as_ref(), detailed)
1539 });
1540 certificate.unwrap_or_else(tls_bridge_undefined_value)
1541 }
1542 "getCertificate" => state
1543 .as_ref()
1544 .and_then(|tls_state| tls_state.local_certificates.first())
1545 .map(|certificate| tls_certificate_bridge_value(certificate, detailed))
1546 .unwrap_or_else(tls_bridge_undefined_value),
1547 "getProtocol" => stream
1548 .protocol_version()
1549 .map(tls_protocol_name)
1550 .map(Value::String)
1551 .unwrap_or(Value::Null),
1552 "getCipher" => stream
1553 .negotiated_cipher_suite()
1554 .map(tls_cipher_bridge_value)
1555 .unwrap_or_else(tls_bridge_undefined_value),
1556 other => {
1557 return Err(SidecarError::InvalidState(format!(
1558 "unsupported TLS query {other}"
1559 )));
1560 }
1561 };
1562 javascript_net_json_string(payload, "net.socket_tls_query")
1563 }
1564
1565 fn write_all(
1566 &self,
1567 kernel: &mut SidecarKernel,
1568 kernel_pid: u32,
1569 contents: &[u8],
1570 ) -> Result<usize, SidecarError> {
1571 if self.tls_mode.load(Ordering::SeqCst) {
1572 let mut tls_stream = self.tls_stream.lock().map_err(|_| {
1573 SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
1574 })?;
1575 let stream = tls_stream.as_mut().ok_or_else(|| {
1576 SidecarError::InvalidState(String::from("TLS stream missing for upgraded socket"))
1577 })?;
1578 stream.write_all(contents)?;
1579 return Ok(contents.len());
1580 }
1581 if let Some(socket_id) = self.kernel_socket_id {
1582 return kernel
1583 .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, contents)
1584 .map_err(kernel_error);
1585 }
1586
1587 let mut stream = self
1588 .stream
1589 .as_ref()
1590 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1591 .lock()
1592 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1593 stream.write_all(contents).map_err(sidecar_net_error)?;
1594 Ok(contents.len())
1595 }
1596
1597 fn shutdown_write(
1598 &self,
1599 kernel: &mut SidecarKernel,
1600 kernel_pid: u32,
1601 ) -> Result<(), SidecarError> {
1602 if self.tls_mode.load(Ordering::SeqCst) {
1603 if let Some(stream) = self
1604 .tls_stream
1605 .lock()
1606 .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
1607 .as_mut()
1608 {
1609 let _ = stream.send_close_notify();
1610 let _ = stream.shutdown_write();
1611 }
1612 if self.kernel_socket_id.is_some() {
1613 self.saw_local_shutdown.store(true, Ordering::SeqCst);
1614 return Ok(());
1615 }
1616 }
1617 if let Some(socket_id) = self.kernel_socket_id {
1618 return kernel
1619 .socket_shutdown(
1620 EXECUTION_DRIVER_NAME,
1621 kernel_pid,
1622 socket_id,
1623 KernelSocketShutdown::Write,
1624 )
1625 .map_err(kernel_error);
1626 }
1627 let stream = self
1628 .stream
1629 .as_ref()
1630 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1631 .lock()
1632 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1633 self.saw_local_shutdown.store(true, Ordering::SeqCst);
1634 match stream.shutdown(Shutdown::Write) {
1635 Ok(()) => {}
1636 Err(error) if error.kind() == std::io::ErrorKind::NotConnected => {}
1637 Err(error) => return Err(sidecar_net_error(error)),
1638 }
1639 if self.saw_remote_end.load(Ordering::SeqCst)
1640 && !self.close_notified.swap(true, Ordering::SeqCst)
1641 {
1642 let _ = self
1643 .event_sender
1644 .as_ref()
1645 .ok_or_else(|| {
1646 SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1647 })?
1648 .send(JavascriptTcpSocketEvent::Close { had_error: false });
1649 }
1650 Ok(())
1651 }
1652
1653 fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
1654 if self.tls_mode.load(Ordering::SeqCst) {
1655 if let Some(stream) = self
1656 .tls_stream
1657 .lock()
1658 .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
1659 .as_mut()
1660 {
1661 let _ = stream.send_close_notify();
1662 let _ = stream.close();
1663 }
1664 if self.kernel_socket_id.is_some() {
1665 return Ok(());
1666 }
1667 }
1668 if let Some(socket_id) = self.kernel_socket_id {
1669 return kernel
1670 .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
1671 .map_err(kernel_error);
1672 }
1673 let stream = self
1674 .stream
1675 .as_ref()
1676 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1677 .lock()
1678 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1679 stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
1680 }
1681}
1682
1683impl ActiveTlsStream {
1684 fn write_all(&mut self, contents: &[u8]) -> Result<(), SidecarError> {
1685 match self {
1686 Self::Client(stream) => {
1687 stream.write_all(contents).map_err(sidecar_net_error)?;
1688 stream.flush().map_err(sidecar_net_error)
1689 }
1690 Self::Server(stream) => {
1691 stream.write_all(contents).map_err(sidecar_net_error)?;
1692 stream.flush().map_err(sidecar_net_error)
1693 }
1694 Self::LoopbackClient(stream) => {
1695 stream.write_all(contents).map_err(sidecar_net_error)?;
1696 stream.flush().map_err(sidecar_net_error)
1697 }
1698 Self::LoopbackServer(stream) => {
1699 stream.write_all(contents).map_err(sidecar_net_error)?;
1700 stream.flush().map_err(sidecar_net_error)
1701 }
1702 }
1703 }
1704
1705 fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
1706 match self {
1707 Self::Client(stream) => stream.read(buffer),
1708 Self::Server(stream) => stream.read(buffer),
1709 Self::LoopbackClient(stream) => stream.read(buffer),
1710 Self::LoopbackServer(stream) => stream.read(buffer),
1711 }
1712 }
1713
1714 fn send_close_notify(&mut self) -> Result<(), SidecarError> {
1715 match self {
1716 Self::Client(stream) => {
1717 stream.conn.send_close_notify();
1718 let _ = stream.conn.complete_io(&mut stream.sock);
1719 }
1720 Self::Server(stream) => {
1721 stream.conn.send_close_notify();
1722 let _ = stream.conn.complete_io(&mut stream.sock);
1723 }
1724 Self::LoopbackClient(stream) => {
1725 stream.conn.send_close_notify();
1726 let _ = stream.conn.complete_io(&mut stream.sock);
1727 }
1728 Self::LoopbackServer(stream) => {
1729 stream.conn.send_close_notify();
1730 let _ = stream.conn.complete_io(&mut stream.sock);
1731 }
1732 }
1733 Ok(())
1734 }
1735
1736 fn shutdown_write(&mut self) -> Result<(), SidecarError> {
1737 match self {
1738 Self::Client(stream) => stream
1739 .sock
1740 .shutdown(Shutdown::Write)
1741 .map_err(sidecar_net_error),
1742 Self::Server(stream) => stream
1743 .sock
1744 .shutdown(Shutdown::Write)
1745 .map_err(sidecar_net_error),
1746 Self::LoopbackClient(stream) => stream.sock.shutdown_write(),
1747 Self::LoopbackServer(stream) => stream.sock.shutdown_write(),
1748 }
1749 }
1750
1751 fn close(&mut self) -> Result<(), SidecarError> {
1752 match self {
1753 Self::Client(stream) => stream
1754 .sock
1755 .shutdown(Shutdown::Both)
1756 .map_err(sidecar_net_error),
1757 Self::Server(stream) => stream
1758 .sock
1759 .shutdown(Shutdown::Both)
1760 .map_err(sidecar_net_error),
1761 Self::LoopbackClient(stream) => stream.sock.close_endpoint(),
1762 Self::LoopbackServer(stream) => stream.sock.close_endpoint(),
1763 }
1764 }
1765
1766 fn peer_certificates(&self) -> Option<&[CertificateDer<'static>]> {
1767 match self {
1768 Self::Client(stream) => stream.conn.peer_certificates(),
1769 Self::Server(stream) => stream.conn.peer_certificates(),
1770 Self::LoopbackClient(stream) => stream.conn.peer_certificates(),
1771 Self::LoopbackServer(stream) => stream.conn.peer_certificates(),
1772 }
1773 }
1774
1775 fn negotiated_cipher_suite(&self) -> Option<rustls::SupportedCipherSuite> {
1776 match self {
1777 Self::Client(stream) => stream.conn.negotiated_cipher_suite(),
1778 Self::Server(stream) => stream.conn.negotiated_cipher_suite(),
1779 Self::LoopbackClient(stream) => stream.conn.negotiated_cipher_suite(),
1780 Self::LoopbackServer(stream) => stream.conn.negotiated_cipher_suite(),
1781 }
1782 }
1783
1784 fn protocol_version(&self) -> Option<rustls::ProtocolVersion> {
1785 match self {
1786 Self::Client(stream) => stream.conn.protocol_version(),
1787 Self::Server(stream) => stream.conn.protocol_version(),
1788 Self::LoopbackClient(stream) => stream.conn.protocol_version(),
1789 Self::LoopbackServer(stream) => stream.conn.protocol_version(),
1790 }
1791 }
1792}
1793
1794impl ActiveUnixSocket {
1799 fn connect(host_path: &Path, guest_path: &str) -> Result<Self, SidecarError> {
1800 let stream = UnixStream::connect(host_path).map_err(sidecar_net_error)?;
1801 Self::from_stream(stream, None, None, Some(guest_path.to_owned()))
1802 }
1803
1804 fn from_stream(
1805 stream: UnixStream,
1806 listener_id: Option<String>,
1807 local_path: Option<String>,
1808 remote_path: Option<String>,
1809 ) -> Result<Self, SidecarError> {
1810 let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
1811 let stream = Arc::new(Mutex::new(stream));
1812 let (sender, events) = mpsc::channel();
1813 let saw_local_shutdown = Arc::new(AtomicBool::new(false));
1814 let saw_remote_end = Arc::new(AtomicBool::new(false));
1815 let close_notified = Arc::new(AtomicBool::new(false));
1816 spawn_unix_socket_reader(
1817 read_stream,
1818 sender.clone(),
1819 Arc::clone(&saw_local_shutdown),
1820 Arc::clone(&saw_remote_end),
1821 Arc::clone(&close_notified),
1822 );
1823
1824 Ok(Self {
1825 stream,
1826 events,
1827 event_sender: sender,
1828 listener_id,
1829 local_path,
1830 remote_path,
1831 saw_local_shutdown,
1832 saw_remote_end,
1833 close_notified,
1834 })
1835 }
1836
1837 fn poll(&mut self, wait: Duration) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
1838 match self.events.recv_timeout(wait) {
1839 Ok(event) => Ok(Some(event)),
1840 Err(RecvTimeoutError::Timeout) => Ok(None),
1841 Err(RecvTimeoutError::Disconnected) => Ok(None),
1842 }
1843 }
1844
1845 fn socket_info(&self) -> Value {
1846 json!({
1847 "localPath": self.local_path.clone(),
1848 "remotePath": self.remote_path.clone(),
1849 })
1850 }
1851
1852 fn write_all(&self, contents: &[u8]) -> Result<usize, SidecarError> {
1853 let mut stream = self
1854 .stream
1855 .lock()
1856 .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1857 stream.write_all(contents).map_err(sidecar_net_error)?;
1858 Ok(contents.len())
1859 }
1860
1861 fn shutdown_write(&self) -> Result<(), SidecarError> {
1862 let stream = self
1863 .stream
1864 .lock()
1865 .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1866 self.saw_local_shutdown.store(true, Ordering::SeqCst);
1867 stream
1868 .shutdown(Shutdown::Write)
1869 .map_err(sidecar_net_error)?;
1870 if self.saw_remote_end.load(Ordering::SeqCst)
1871 && !self.close_notified.swap(true, Ordering::SeqCst)
1872 {
1873 let _ = self
1874 .event_sender
1875 .send(JavascriptTcpSocketEvent::Close { had_error: false });
1876 }
1877 Ok(())
1878 }
1879
1880 fn close(&self) -> Result<(), SidecarError> {
1881 let stream = self
1882 .stream
1883 .lock()
1884 .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1885 stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
1886 }
1887}
1888
1889impl ActiveUnixListener {
1892 fn bind(
1893 host_path: &Path,
1894 guest_path: &str,
1895 backlog: Option<u32>,
1896 ) -> Result<Self, SidecarError> {
1897 if let Some(parent) = host_path.parent() {
1898 fs::create_dir_all(parent).map_err(sidecar_net_error)?;
1899 }
1900 let listener = UnixListener::bind(host_path).map_err(sidecar_net_error)?;
1901 listener.set_nonblocking(true).map_err(sidecar_net_error)?;
1902 Ok(Self {
1903 listener,
1904 path: guest_path.to_owned(),
1905 backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
1906 .expect("default backlog fits within usize"),
1907 active_connection_ids: BTreeSet::new(),
1908 })
1909 }
1910
1911 fn path(&self) -> &str {
1912 &self.path
1913 }
1914
1915 fn poll(
1916 &mut self,
1917 wait: Duration,
1918 ) -> Result<Option<JavascriptUnixListenerEvent>, SidecarError> {
1919 let deadline = Instant::now() + wait;
1920 loop {
1921 match self.listener.accept() {
1922 Ok((stream, remote_addr)) => {
1923 if self.active_connection_ids.len() >= self.backlog {
1924 let _ = stream.shutdown(Shutdown::Both);
1925 if wait.is_zero() || Instant::now() >= deadline {
1926 return Ok(None);
1927 }
1928 continue;
1929 }
1930
1931 let local_path = Some(self.path.clone());
1932 let remote_path = unix_socket_path(&remote_addr);
1933 return Ok(Some(JavascriptUnixListenerEvent::Connection(
1934 PendingUnixSocket {
1935 stream,
1936 local_path,
1937 remote_path,
1938 },
1939 )));
1940 }
1941 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
1942 if wait.is_zero() || Instant::now() >= deadline {
1943 return Ok(None);
1944 }
1945 thread::sleep(Duration::from_millis(10));
1946 }
1947 Err(error) => {
1948 return Ok(Some(JavascriptUnixListenerEvent::Error {
1949 code: io_error_code(&error),
1950 message: error.to_string(),
1951 }));
1952 }
1953 }
1954 }
1955 }
1956
1957 fn close(&self) -> Result<(), SidecarError> {
1958 Ok(())
1959 }
1960
1961 fn active_connection_count(&self) -> usize {
1962 self.active_connection_ids.len()
1963 }
1964
1965 fn register_connection(&mut self, socket_id: &str) {
1966 self.active_connection_ids.insert(socket_id.to_string());
1967 }
1968
1969 fn release_connection(&mut self, socket_id: &str) {
1970 self.active_connection_ids.remove(socket_id);
1971 }
1972}
1973
1974impl ActiveTcpListener {
1975 fn bind(
1976 bind_host: &str,
1977 guest_host: &str,
1978 guest_port: u16,
1979 backlog: Option<u32>,
1980 ) -> Result<Self, SidecarError> {
1981 let bind_addr = resolve_tcp_bind_addr(bind_host, 0)?;
1982 let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
1983 let listener = TcpListener::bind(bind_addr).map_err(sidecar_net_error)?;
1984 listener.set_nonblocking(true).map_err(sidecar_net_error)?;
1985 let local_addr = listener.local_addr().map_err(sidecar_net_error)?;
1986 Ok(Self {
1987 listener: Some(listener),
1988 kernel_socket_id: None,
1989 local_addr: Some(local_addr),
1990 guest_local_addr: guest_addr,
1991 backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
1992 .expect("default backlog fits within usize"),
1993 active_connection_ids: BTreeSet::new(),
1994 })
1995 }
1996
1997 fn bind_kernel(
1998 kernel: &mut SidecarKernel,
1999 kernel_pid: u32,
2000 guest_host: &str,
2001 guest_port: u16,
2002 backlog: Option<u32>,
2003 ) -> Result<Self, SidecarError> {
2004 let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
2005 let spec = match guest_addr {
2006 SocketAddr::V4(_) => SocketSpec::tcp(),
2007 SocketAddr::V6(_) => SocketSpec::new(SocketDomain::Inet6, SocketType::Stream),
2008 };
2009 let socket_id = kernel
2010 .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
2011 .map_err(kernel_error)?;
2012 kernel
2013 .socket_bind_inet(
2014 EXECUTION_DRIVER_NAME,
2015 kernel_pid,
2016 socket_id,
2017 InetSocketAddress::new(guest_addr.ip().to_string(), guest_addr.port()),
2018 )
2019 .map_err(kernel_error)?;
2020 kernel
2021 .socket_listen(
2022 EXECUTION_DRIVER_NAME,
2023 kernel_pid,
2024 socket_id,
2025 usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2026 .expect("default backlog fits within usize"),
2027 )
2028 .map_err(kernel_error)?;
2029 Ok(Self {
2030 listener: None,
2031 kernel_socket_id: Some(socket_id),
2032 local_addr: Some(guest_addr),
2033 guest_local_addr: guest_addr,
2034 backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2035 .expect("default backlog fits within usize"),
2036 active_connection_ids: BTreeSet::new(),
2037 })
2038 }
2039
2040 pub(crate) fn local_addr(&self) -> SocketAddr {
2041 self.local_addr.unwrap_or(self.guest_local_addr)
2042 }
2043
2044 fn guest_local_addr(&self) -> SocketAddr {
2045 self.guest_local_addr
2046 }
2047
2048 fn poll(
2049 &mut self,
2050 kernel: &mut SidecarKernel,
2051 kernel_pid: u32,
2052 wait: Duration,
2053 ) -> Result<Option<JavascriptTcpListenerEvent>, SidecarError> {
2054 if let Some(socket_id) = self.kernel_socket_id {
2055 let result = kernel
2056 .poll_targets(
2057 EXECUTION_DRIVER_NAME,
2058 kernel_pid,
2059 vec![PollTargetEntry::socket(socket_id, POLLIN)],
2060 i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
2061 )
2062 .map_err(kernel_error)?;
2063 let revents = result
2064 .targets
2065 .first()
2066 .map(|entry| entry.revents)
2067 .unwrap_or_else(PollEvents::empty);
2068 if revents.is_empty() {
2069 return Ok(None);
2070 }
2071 let accepted_socket_id =
2072 match kernel.socket_accept(EXECUTION_DRIVER_NAME, kernel_pid, socket_id) {
2073 Ok(accepted_socket_id) => accepted_socket_id,
2074 Err(error) if error.code() == "EAGAIN" => return Ok(None),
2075 Err(error) => {
2076 return Ok(Some(JavascriptTcpListenerEvent::Error {
2077 code: Some(error.code().to_string()),
2078 message: error.to_string(),
2079 }));
2080 }
2081 };
2082 let accepted = kernel.socket_get(accepted_socket_id).ok_or_else(|| {
2083 SidecarError::InvalidState(format!(
2084 "accepted kernel TCP socket {accepted_socket_id} is missing"
2085 ))
2086 })?;
2087 let local_addr = accepted.local_address().ok_or_else(|| {
2088 SidecarError::InvalidState(format!(
2089 "accepted kernel TCP socket {accepted_socket_id} missing local address"
2090 ))
2091 })?;
2092 let remote_addr = accepted.peer_address().ok_or_else(|| {
2093 SidecarError::InvalidState(format!(
2094 "accepted kernel TCP socket {accepted_socket_id} missing peer address"
2095 ))
2096 })?;
2097 return Ok(Some(JavascriptTcpListenerEvent::Connection(
2098 PendingTcpSocket {
2099 stream: None,
2100 kernel_socket_id: Some(accepted_socket_id),
2101 preallocated: true,
2102 guest_local_addr: resolve_tcp_bind_addr(local_addr.host(), local_addr.port())?,
2103 guest_remote_addr: resolve_tcp_bind_addr(
2104 remote_addr.host(),
2105 remote_addr.port(),
2106 )?,
2107 },
2108 )));
2109 }
2110
2111 let deadline = Instant::now() + wait;
2112 loop {
2113 match self
2114 .listener
2115 .as_ref()
2116 .ok_or_else(|| {
2117 SidecarError::InvalidState(String::from("TCP listener socket missing"))
2118 })?
2119 .accept()
2120 {
2121 Ok((stream, remote_addr)) => {
2122 if self.active_connection_ids.len() >= self.backlog {
2123 let _ = stream.shutdown(Shutdown::Both);
2124 if wait.is_zero() || Instant::now() >= deadline {
2125 return Ok(None);
2126 }
2127 continue;
2128 }
2129 return Ok(Some(JavascriptTcpListenerEvent::Connection(
2130 PendingTcpSocket {
2131 stream: Some(stream),
2132 kernel_socket_id: None,
2133 preallocated: false,
2134 guest_local_addr: self.guest_local_addr,
2135 guest_remote_addr: SocketAddr::new(
2136 remote_addr.ip(),
2137 remote_addr.port(),
2138 ),
2139 },
2140 )));
2141 }
2142 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2143 if wait.is_zero() || Instant::now() >= deadline {
2144 return Ok(None);
2145 }
2146 thread::sleep(Duration::from_millis(10));
2147 }
2148 Err(error) => {
2149 return Ok(Some(JavascriptTcpListenerEvent::Error {
2150 code: io_error_code(&error),
2151 message: error.to_string(),
2152 }));
2153 }
2154 }
2155 }
2156 }
2157
2158 fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
2159 if let Some(socket_id) = self.kernel_socket_id {
2160 kernel
2161 .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
2162 .map_err(kernel_error)?;
2163 }
2164 Ok(())
2165 }
2166
2167 fn active_connection_count(&self) -> usize {
2168 self.active_connection_ids.len()
2169 }
2170
2171 fn register_connection(&mut self, socket_id: &str) {
2172 self.active_connection_ids.insert(socket_id.to_string());
2173 }
2174
2175 fn release_connection(&mut self, socket_id: &str) {
2176 self.active_connection_ids.remove(socket_id);
2177 }
2178}
2179
2180impl ActiveUdpSocket {
2183 fn new(
2184 kernel: &mut SidecarKernel,
2185 kernel_pid: u32,
2186 family: JavascriptUdpFamily,
2187 ) -> Result<Self, SidecarError> {
2188 let spec = match family {
2189 JavascriptUdpFamily::Ipv4 => SocketSpec::udp(),
2190 JavascriptUdpFamily::Ipv6 => SocketSpec::new(SocketDomain::Inet6, SocketType::Datagram),
2191 };
2192 let socket_id = kernel
2193 .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
2194 .map_err(kernel_error)?;
2195 Ok(Self {
2196 family,
2197 socket: None,
2198 kernel_socket_id: Some(socket_id),
2199 guest_local_addr: None,
2200 recv_buffer_size: 0,
2201 send_buffer_size: 0,
2202 })
2203 }
2204
2205 fn local_addr(&self) -> Option<SocketAddr> {
2206 self.guest_local_addr
2207 }
2208
2209 fn socket(&self) -> Result<&UdpSocket, SidecarError> {
2210 self.socket
2211 .as_ref()
2212 .ok_or_else(|| SidecarError::Execution(String::from("EBADF: bad file descriptor")))
2213 }
2214
2215 fn bind(
2216 &mut self,
2217 kernel: &mut SidecarKernel,
2218 kernel_pid: u32,
2219 host: Option<&str>,
2220 port: u16,
2221 context: &JavascriptSocketPathContext,
2222 ) -> Result<SocketAddr, SidecarError> {
2223 if self.socket.is_some() || self.guest_local_addr.is_some() {
2224 return Err(SidecarError::Execution(String::from(
2225 "EINVAL: secure-exec dgram socket is already bound",
2226 )));
2227 }
2228
2229 let (bind_host, guest_host, guest_family) = normalize_udp_bind_host(host, self.family)?;
2230 let guest_port = allocate_guest_listen_port(
2231 port,
2232 guest_family,
2233 &context.used_udp_guest_ports,
2234 context.listen_policy,
2235 )?;
2236 let local_addr = resolve_udp_bind_addr(guest_host, guest_port, self.family)?;
2237 if let Some(socket_id) = self.kernel_socket_id {
2238 kernel
2239 .socket_bind_inet(
2240 EXECUTION_DRIVER_NAME,
2241 kernel_pid,
2242 socket_id,
2243 InetSocketAddress::new(local_addr.ip().to_string(), local_addr.port()),
2244 )
2245 .map_err(kernel_error)?;
2246 } else {
2247 let bind_addr = resolve_udp_bind_addr(bind_host, 0, self.family)?;
2248 let socket = UdpSocket::bind(bind_addr).map_err(sidecar_net_error)?;
2249 socket.set_nonblocking(true).map_err(sidecar_net_error)?;
2250 self.socket = Some(socket);
2251 }
2252 self.guest_local_addr = Some(local_addr);
2253 Ok(local_addr)
2254 }
2255
2256 fn ensure_bound_for_send(
2257 &mut self,
2258 kernel: &mut SidecarKernel,
2259 kernel_pid: u32,
2260 context: &JavascriptSocketPathContext,
2261 ) -> Result<SocketAddr, SidecarError> {
2262 if let Some(local_addr) = self.local_addr() {
2263 return Ok(local_addr);
2264 }
2265
2266 self.bind(kernel, kernel_pid, None, 0, context)
2267 }
2268
2269 fn send_to<B>(
2270 &mut self,
2271 request: ActiveUdpSendToRequest<'_, B>,
2272 ) -> Result<(usize, SocketAddr), SidecarError>
2273 where
2274 B: NativeSidecarBridge + Send + 'static,
2275 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
2276 {
2277 let ActiveUdpSendToRequest {
2278 bridge,
2279 kernel,
2280 kernel_pid,
2281 vm_id,
2282 dns,
2283 host,
2284 port,
2285 context,
2286 contents,
2287 } = request;
2288 let remote_addr = resolve_udp_addr(UdpRemoteAddrRequest {
2289 bridge,
2290 kernel,
2291 vm_id,
2292 dns,
2293 host,
2294 port,
2295 family: self.family,
2296 context,
2297 })?;
2298 let local_addr = self.ensure_bound_for_send(kernel, kernel_pid, context)?;
2299 let written = if let Some(socket_id) = self.kernel_socket_id {
2300 if is_loopback_ip(remote_addr.ip()) && remote_addr.port() == port {
2301 kernel
2302 .socket_send_to_inet_loopback(
2303 EXECUTION_DRIVER_NAME,
2304 kernel_pid,
2305 socket_id,
2306 InetSocketAddress::new(remote_addr.ip().to_string(), remote_addr.port()),
2307 contents,
2308 )
2309 .map_err(kernel_error)?
2310 } else {
2311 return Err(SidecarError::Execution(String::from(
2312 "ERR_NOT_IMPLEMENTED: external UDP datagrams are not yet supported by the kernel-backed V8 bridge",
2313 )));
2314 }
2315 } else {
2316 let socket = self.socket.as_ref().ok_or_else(|| {
2317 SidecarError::InvalidState(String::from("UDP socket is not initialized"))
2318 })?;
2319 socket
2320 .send_to(contents, remote_addr)
2321 .map_err(sidecar_net_error)?
2322 };
2323 Ok((written, local_addr))
2324 }
2325
2326 fn poll(
2327 &self,
2328 kernel: &mut SidecarKernel,
2329 kernel_pid: u32,
2330 wait: Duration,
2331 ) -> Result<Option<JavascriptUdpSocketEvent>, SidecarError> {
2332 if let Some(socket_id) = self.kernel_socket_id {
2333 let result = kernel
2334 .poll_targets(
2335 EXECUTION_DRIVER_NAME,
2336 kernel_pid,
2337 vec![PollTargetEntry::socket(socket_id, POLLIN)],
2338 i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
2339 )
2340 .map_err(kernel_error)?;
2341 let revents = result
2342 .targets
2343 .first()
2344 .map(|entry| entry.revents)
2345 .unwrap_or_else(PollEvents::empty);
2346 if revents.is_empty() {
2347 return Ok(None);
2348 }
2349 return match kernel.socket_recv_datagram(
2350 EXECUTION_DRIVER_NAME,
2351 kernel_pid,
2352 socket_id,
2353 64 * 1024,
2354 ) {
2355 Ok(Some(datagram)) => {
2356 let (source_address, payload) = datagram.into_parts();
2357 let remote_addr = source_address
2358 .map(|source| {
2359 resolve_udp_bind_addr(source.host(), source.port(), self.family)
2360 })
2361 .transpose()?
2362 .unwrap_or_else(|| match self.family {
2363 JavascriptUdpFamily::Ipv4 => {
2364 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)
2365 }
2366 JavascriptUdpFamily::Ipv6 => {
2367 SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0)
2368 }
2369 });
2370 Ok(Some(JavascriptUdpSocketEvent::Message {
2371 data: payload,
2372 remote_addr,
2373 }))
2374 }
2375 Ok(None) => Ok(None),
2376 Err(error) if error.code() == "EAGAIN" => Ok(None),
2377 Err(error) => Ok(Some(JavascriptUdpSocketEvent::Error {
2378 code: Some(error.code().to_string()),
2379 message: error.to_string(),
2380 })),
2381 };
2382 }
2383 let socket = self.socket()?;
2384 let deadline = Instant::now() + wait;
2385 let mut buffer = vec![0_u8; 64 * 1024];
2386
2387 loop {
2388 match socket.recv_from(&mut buffer) {
2389 Ok((bytes_read, remote_addr)) => {
2390 return Ok(Some(JavascriptUdpSocketEvent::Message {
2391 data: buffer[..bytes_read].to_vec(),
2392 remote_addr,
2393 }));
2394 }
2395 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2396 if wait.is_zero() || Instant::now() >= deadline {
2397 return Ok(None);
2398 }
2399 thread::sleep(Duration::from_millis(10));
2400 }
2401 Err(error) => {
2402 return Ok(Some(JavascriptUdpSocketEvent::Error {
2403 code: io_error_code(&error),
2404 message: error.to_string(),
2405 }));
2406 }
2407 }
2408 }
2409 }
2410
2411 fn close(&mut self, kernel: &mut SidecarKernel, kernel_pid: u32) {
2412 if let Some(socket_id) = self.kernel_socket_id {
2413 let _ = kernel.socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id);
2414 }
2415 self.socket.take();
2416 self.guest_local_addr = None;
2417 }
2418
2419 fn set_buffer_size(&mut self, which: &str, size: usize) -> Result<(), SidecarError> {
2420 match which {
2421 "recv" => self.recv_buffer_size = size,
2422 "send" => self.send_buffer_size = size,
2423 other => {
2424 return Err(SidecarError::InvalidState(format!(
2425 "unsupported UDP buffer size kind {other}"
2426 )));
2427 }
2428 }
2429 if self.kernel_socket_id.is_some() {
2430 return Ok(());
2431 }
2432 let socket = self.socket()?;
2433 let socket = SockRef::from(socket);
2434 match which {
2435 "recv" => socket.set_recv_buffer_size(size).map_err(sidecar_net_error),
2436 "send" => socket.set_send_buffer_size(size).map_err(sidecar_net_error),
2437 other => Err(SidecarError::InvalidState(format!(
2438 "unsupported UDP buffer size kind {other}"
2439 ))),
2440 }
2441 }
2442
2443 fn get_buffer_size(&self, which: &str) -> Result<usize, SidecarError> {
2444 if self.kernel_socket_id.is_some() {
2445 return Ok(match which {
2446 "recv" => self.recv_buffer_size,
2447 "send" => self.send_buffer_size,
2448 other => {
2449 return Err(SidecarError::InvalidState(format!(
2450 "unsupported UDP buffer size kind {other}"
2451 )));
2452 }
2453 });
2454 }
2455 let socket = self.socket()?;
2456 let socket = SockRef::from(socket);
2457 match which {
2458 "recv" => socket.recv_buffer_size().map_err(sidecar_net_error),
2459 "send" => socket.send_buffer_size().map_err(sidecar_net_error),
2460 other => Err(SidecarError::InvalidState(format!(
2461 "unsupported UDP buffer size kind {other}"
2462 ))),
2463 }
2464 }
2465}
2466
2467impl ActiveExecution {
2470 pub(crate) fn uses_shared_v8_runtime(&self) -> bool {
2471 match self {
2472 Self::Javascript(execution) => execution.uses_shared_v8_runtime(),
2473 Self::Python(execution) => execution.uses_shared_v8_runtime(),
2474 Self::Wasm(execution) => execution.uses_shared_v8_runtime(),
2475 Self::Tool(_) => false,
2476 }
2477 }
2478
2479 pub(crate) fn child_pid(&self) -> u32 {
2480 match self {
2481 Self::Javascript(execution) => execution.child_pid(),
2482 Self::Python(execution) => execution.child_pid(),
2483 Self::Wasm(execution) => execution.child_pid(),
2484 Self::Tool(_) => 0,
2485 }
2486 }
2487
2488 pub(crate) fn write_stdin(&mut self, chunk: &[u8]) -> Result<(), SidecarError> {
2489 match self {
2490 Self::Javascript(execution) => execution
2491 .write_stdin(chunk)
2492 .map_err(|error| SidecarError::Execution(error.to_string())),
2493 Self::Python(execution) => execution
2494 .write_stdin(chunk)
2495 .map_err(|error| SidecarError::Execution(error.to_string())),
2496 Self::Wasm(execution) => execution
2497 .write_stdin(chunk)
2498 .map_err(|error| SidecarError::Execution(error.to_string())),
2499 Self::Tool(_) => Ok(()),
2500 }
2501 }
2502
2503 pub(crate) fn close_stdin(&mut self) -> Result<(), SidecarError> {
2504 match self {
2505 Self::Javascript(execution) => execution
2506 .close_stdin()
2507 .map_err(|error| SidecarError::Execution(error.to_string())),
2508 Self::Python(execution) => execution
2509 .close_stdin()
2510 .map_err(|error| SidecarError::Execution(error.to_string())),
2511 Self::Wasm(execution) => execution
2512 .close_stdin()
2513 .map_err(|error| SidecarError::Execution(error.to_string())),
2514 Self::Tool(_) => Ok(()),
2515 }
2516 }
2517
2518 pub(crate) fn respond_python_vfs_rpc_success(
2519 &mut self,
2520 id: u64,
2521 payload: PythonVfsRpcResponsePayload,
2522 ) -> Result<(), SidecarError> {
2523 match self {
2524 Self::Python(execution) => execution
2525 .respond_vfs_rpc_success(id, payload)
2526 .map_err(|error| SidecarError::Execution(error.to_string())),
2527 _ => Err(SidecarError::InvalidState(String::from(
2528 "only Python executions can service Python VFS RPC responses",
2529 ))),
2530 }
2531 }
2532
2533 pub(crate) fn respond_python_vfs_rpc_error(
2534 &mut self,
2535 id: u64,
2536 code: impl Into<String>,
2537 message: impl Into<String>,
2538 ) -> Result<(), SidecarError> {
2539 match self {
2540 Self::Python(execution) => execution
2541 .respond_vfs_rpc_error(id, code, message)
2542 .map_err(|error| SidecarError::Execution(error.to_string())),
2543 _ => Err(SidecarError::InvalidState(String::from(
2544 "only Python executions can service Python VFS RPC responses",
2545 ))),
2546 }
2547 }
2548
2549 pub(crate) fn send_javascript_stream_event(
2550 &self,
2551 event_type: &str,
2552 payload: Value,
2553 ) -> Result<(), SidecarError> {
2554 match self {
2555 Self::Javascript(execution) => execution
2556 .send_stream_event(event_type, payload)
2557 .map_err(|error| SidecarError::Execution(error.to_string())),
2558 Self::Wasm(execution) => execution
2559 .send_stream_event(event_type, payload)
2560 .map_err(|error| SidecarError::Execution(error.to_string())),
2561 _ => Err(SidecarError::InvalidState(String::from(
2562 "only embedded V8 executions can receive JavaScript stream events",
2563 ))),
2564 }
2565 }
2566
2567 pub(crate) fn javascript_v8_session_handle(&self) -> Option<V8SessionHandle> {
2568 match self {
2569 Self::Javascript(execution) => Some(execution.v8_session_handle()),
2570 Self::Wasm(execution) => Some(execution.v8_session_handle()),
2571 _ => None,
2572 }
2573 }
2574
2575 pub(crate) fn terminate(&mut self) -> Result<(), SidecarError> {
2576 match self {
2577 Self::Javascript(execution) => execution
2578 .terminate()
2579 .map_err(|error| SidecarError::Execution(error.to_string())),
2580 Self::Python(execution) => execution
2581 .kill()
2582 .map_err(|error| SidecarError::Execution(error.to_string())),
2583 Self::Wasm(execution) => execution
2584 .terminate()
2585 .map_err(|error| SidecarError::Execution(error.to_string())),
2586 Self::Tool(_) => Ok(()),
2587 }
2588 }
2589
2590 pub(crate) fn respond_javascript_sync_rpc_success(
2591 &mut self,
2592 id: u64,
2593 result: Value,
2594 ) -> Result<(), SidecarError> {
2595 match self {
2596 Self::Javascript(execution) => execution
2597 .respond_sync_rpc_success(id, result)
2598 .map_err(|error| SidecarError::Execution(error.to_string())),
2599 Self::Python(execution) => execution
2600 .respond_javascript_sync_rpc_success(id, result)
2601 .map_err(|error| SidecarError::Execution(error.to_string())),
2602 Self::Wasm(execution) => execution
2603 .respond_sync_rpc_success(id, result)
2604 .map_err(|error| SidecarError::Execution(error.to_string())),
2605 _ => Err(SidecarError::InvalidState(String::from(
2606 "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
2607 ))),
2608 }
2609 }
2610
2611 pub(crate) fn respond_javascript_sync_rpc_error(
2612 &mut self,
2613 id: u64,
2614 code: impl Into<String>,
2615 message: impl Into<String>,
2616 ) -> Result<(), SidecarError> {
2617 match self {
2618 Self::Javascript(execution) => execution
2619 .respond_sync_rpc_error(id, code, message)
2620 .map_err(|error| SidecarError::Execution(error.to_string())),
2621 Self::Python(execution) => execution
2622 .respond_javascript_sync_rpc_error(id, code, message)
2623 .map_err(|error| SidecarError::Execution(error.to_string())),
2624 Self::Wasm(execution) => execution
2625 .respond_sync_rpc_error(id, code, message)
2626 .map_err(|error| SidecarError::Execution(error.to_string())),
2627 _ => Err(SidecarError::InvalidState(String::from(
2628 "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
2629 ))),
2630 }
2631 }
2632
2633 pub(crate) async fn poll_event(
2634 &mut self,
2635 timeout: Duration,
2636 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
2637 match self {
2638 Self::Javascript(execution) => execution
2639 .poll_event(timeout)
2640 .await
2641 .map(|event| {
2642 event.map(|event| match event {
2643 JavascriptExecutionEvent::Stdout(chunk) => {
2644 ActiveExecutionEvent::Stdout(chunk)
2645 }
2646 JavascriptExecutionEvent::Stderr(chunk) => {
2647 ActiveExecutionEvent::Stderr(chunk)
2648 }
2649 JavascriptExecutionEvent::SyncRpcRequest(request) => {
2650 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2651 }
2652 JavascriptExecutionEvent::SignalState {
2653 signal,
2654 registration,
2655 } => ActiveExecutionEvent::SignalState {
2656 signal,
2657 registration: map_node_signal_registration(registration),
2658 },
2659 JavascriptExecutionEvent::Exited(code) => {
2660 ActiveExecutionEvent::Exited(code)
2661 }
2662 })
2663 })
2664 .map_err(|error| SidecarError::Execution(error.to_string())),
2665 Self::Python(execution) => execution
2666 .poll_event(timeout)
2667 .await
2668 .map(|event| {
2669 event.map(|event| match event {
2670 PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2671 PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2672 PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
2673 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2674 }
2675 PythonExecutionEvent::VfsRpcRequest(request) => {
2676 ActiveExecutionEvent::PythonVfsRpcRequest(request)
2677 }
2678 PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2679 })
2680 })
2681 .map_err(|error| SidecarError::Execution(error.to_string())),
2682 Self::Wasm(execution) => execution
2683 .poll_event(timeout)
2684 .await
2685 .map(|event| {
2686 event.map(|event| match event {
2687 WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2688 WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2689 WasmExecutionEvent::SyncRpcRequest(request) => {
2690 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2691 }
2692 WasmExecutionEvent::SignalState {
2693 signal,
2694 registration,
2695 } => ActiveExecutionEvent::SignalState {
2696 signal,
2697 registration: map_wasm_signal_registration(registration),
2698 },
2699 WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2700 })
2701 })
2702 .map_err(|error| SidecarError::Execution(error.to_string())),
2703 Self::Tool(execution) => {
2704 let _ = timeout;
2705 poll_tool_process_event(execution)
2706 }
2707 }
2708 }
2709
2710 pub(crate) fn poll_event_blocking(
2711 &mut self,
2712 timeout: Duration,
2713 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
2714 match self {
2715 Self::Javascript(execution) => execution
2716 .poll_event_blocking(timeout)
2717 .map(|event| {
2718 event.map(|event| match event {
2719 JavascriptExecutionEvent::Stdout(chunk) => {
2720 ActiveExecutionEvent::Stdout(chunk)
2721 }
2722 JavascriptExecutionEvent::Stderr(chunk) => {
2723 ActiveExecutionEvent::Stderr(chunk)
2724 }
2725 JavascriptExecutionEvent::SyncRpcRequest(request) => {
2726 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2727 }
2728 JavascriptExecutionEvent::SignalState {
2729 signal,
2730 registration,
2731 } => ActiveExecutionEvent::SignalState {
2732 signal,
2733 registration: map_node_signal_registration(registration),
2734 },
2735 JavascriptExecutionEvent::Exited(code) => {
2736 ActiveExecutionEvent::Exited(code)
2737 }
2738 })
2739 })
2740 .map_err(|error| SidecarError::Execution(error.to_string())),
2741 Self::Python(execution) => execution
2742 .poll_event_blocking(timeout)
2743 .map(|event| {
2744 event.map(|event| match event {
2745 PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2746 PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2747 PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
2748 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2749 }
2750 PythonExecutionEvent::VfsRpcRequest(request) => {
2751 ActiveExecutionEvent::PythonVfsRpcRequest(request)
2752 }
2753 PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2754 })
2755 })
2756 .map_err(|error| SidecarError::Execution(error.to_string())),
2757 Self::Wasm(execution) => execution
2758 .poll_event_blocking(timeout)
2759 .map(|event| {
2760 event.map(|event| match event {
2761 WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2762 WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2763 WasmExecutionEvent::SyncRpcRequest(request) => {
2764 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2765 }
2766 WasmExecutionEvent::SignalState {
2767 signal,
2768 registration,
2769 } => ActiveExecutionEvent::SignalState {
2770 signal,
2771 registration: map_wasm_signal_registration(registration),
2772 },
2773 WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2774 })
2775 })
2776 .map_err(|error| SidecarError::Execution(error.to_string())),
2777 Self::Tool(execution) => {
2778 let _ = timeout;
2779 poll_tool_process_event(execution)
2780 }
2781 }
2782 }
2783}
2784
2785struct ToolProcessEventRequest {
2786 sidecar_requests: SharedSidecarRequestClient,
2787 connection_id: String,
2788 session_id: String,
2789 vm_id: String,
2790 tool_resolution: ToolCommandResolution,
2791 cancelled: Arc<AtomicBool>,
2792 pending_events: Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
2793 events_overflowed: Arc<AtomicBool>,
2794}
2795
2796pub(crate) fn send_tool_process_event(
2797 pending_events: &Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
2798 events_overflowed: &AtomicBool,
2799 event: ActiveExecutionEvent,
2800) -> bool {
2801 let mut pending_events = pending_events
2802 .lock()
2803 .unwrap_or_else(|poisoned| poisoned.into_inner());
2804 if pending_events.len() >= MAX_PROCESS_EVENT_QUEUE {
2805 events_overflowed.store(true, Ordering::Relaxed);
2806 return false;
2807 }
2808 pending_events.push_back(event);
2809 true
2810}
2811
2812fn spawn_tool_process_events(request: ToolProcessEventRequest) {
2813 let ToolProcessEventRequest {
2814 sidecar_requests,
2815 connection_id,
2816 session_id,
2817 vm_id,
2818 tool_resolution,
2819 cancelled,
2820 pending_events,
2821 events_overflowed,
2822 } = request;
2823 std::thread::spawn(move || match tool_resolution {
2824 ToolCommandResolution::Failure(message) => {
2825 if !send_tool_process_event(
2826 &pending_events,
2827 &events_overflowed,
2828 ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
2829 ) {
2830 return;
2831 }
2832 let _ = send_tool_process_event(
2833 &pending_events,
2834 &events_overflowed,
2835 ActiveExecutionEvent::Exited(1),
2836 );
2837 }
2838 ToolCommandResolution::Invoke { request, timeout } => {
2839 let response = sidecar_requests.invoke(
2840 OwnershipScope::vm(connection_id.clone(), session_id.clone(), vm_id.clone()),
2841 SidecarRequestPayload::HostCallback(request.clone()),
2842 timeout,
2843 );
2844 if cancelled.load(Ordering::Relaxed) {
2845 return;
2846 }
2847
2848 match response {
2849 Ok(crate::protocol::SidecarResponsePayload::HostCallbackResult(result)) => {
2850 if let Some(value) = result.result {
2851 let value: serde_json::Value = serde_json::from_str(&value)
2852 .unwrap_or(serde_json::Value::String(value));
2853 let stdout = serde_json::to_vec(&json!({
2854 "ok": true,
2855 "result": value,
2856 }))
2857 .unwrap_or_else(|error| {
2858 format_tool_failure_output(&format!(
2859 "failed to serialize tool result: {error}"
2860 ))
2861 });
2862 if !send_tool_process_event(
2863 &pending_events,
2864 &events_overflowed,
2865 ActiveExecutionEvent::Stdout(stdout),
2866 ) {
2867 return;
2868 }
2869 let _ = send_tool_process_event(
2870 &pending_events,
2871 &events_overflowed,
2872 ActiveExecutionEvent::Exited(0),
2873 );
2874 } else {
2875 let message = result
2876 .error
2877 .unwrap_or_else(|| String::from("tool invocation returned no result"));
2878 if !send_tool_process_event(
2879 &pending_events,
2880 &events_overflowed,
2881 ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
2882 ) {
2883 return;
2884 }
2885 let _ = send_tool_process_event(
2886 &pending_events,
2887 &events_overflowed,
2888 ActiveExecutionEvent::Exited(1),
2889 );
2890 }
2891 }
2892 Ok(_) => {
2893 if !send_tool_process_event(
2894 &pending_events,
2895 &events_overflowed,
2896 ActiveExecutionEvent::Stderr(format_tool_failure_output(
2897 "unexpected sidecar tool response",
2898 )),
2899 ) {
2900 return;
2901 }
2902 let _ = send_tool_process_event(
2903 &pending_events,
2904 &events_overflowed,
2905 ActiveExecutionEvent::Exited(1),
2906 );
2907 }
2908 Err(error) => {
2909 if !send_tool_process_event(
2910 &pending_events,
2911 &events_overflowed,
2912 ActiveExecutionEvent::Stderr(format_tool_failure_output(
2913 &error.to_string(),
2914 )),
2915 ) {
2916 return;
2917 }
2918 let _ = send_tool_process_event(
2919 &pending_events,
2920 &events_overflowed,
2921 ActiveExecutionEvent::Exited(1),
2922 );
2923 }
2924 }
2925 }
2926 });
2927}
2928
2929impl<B> NativeSidecar<B>
2930where
2931 B: NativeSidecarBridge + Send + 'static,
2932 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
2933{
2934 pub(crate) async fn execute(
2935 &mut self,
2936 request: &RequestFrame,
2937 payload: ExecuteRequest,
2938 ) -> Result<DispatchResult, SidecarError> {
2939 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
2940 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
2941
2942 let vm = self
2943 .vms
2944 .get_mut(&vm_id)
2945 .ok_or_else(|| missing_vm_error(&vm_id))?;
2946 if vm.active_processes.contains_key(&payload.process_id) {
2947 return Err(SidecarError::InvalidState(format!(
2948 "VM {vm_id} already has an active process with id {}",
2949 payload.process_id
2950 )));
2951 }
2952
2953 if let Some(command) = payload.command.as_deref() {
2954 if let Some(tool_resolution) =
2955 resolve_tool_command(vm, command, &payload.args, payload.cwd.as_deref())?
2956 {
2957 let guest_cwd = payload
2958 .cwd
2959 .as_deref()
2960 .map(normalize_path)
2961 .unwrap_or_else(|| vm.guest_cwd.clone());
2962 let kernel_handle = vm
2963 .kernel
2964 .create_virtual_process(
2965 EXECUTION_DRIVER_NAME,
2966 TOOL_DRIVER_NAME,
2967 command,
2968 std::iter::once(command.to_owned())
2969 .chain(payload.args.iter().cloned())
2970 .collect(),
2971 VirtualProcessOptions {
2972 env: vm.guest_env.clone(),
2973 cwd: Some(guest_cwd.clone()),
2974 ..VirtualProcessOptions::default()
2975 },
2976 )
2977 .map_err(kernel_error)?;
2978 let kernel_pid = kernel_handle.pid();
2979 let tool_execution = ToolExecution::default();
2980 let cancelled = tool_execution.cancelled.clone();
2981 let pending_events = tool_execution.pending_events.clone();
2982 let events_overflowed = tool_execution.events_overflowed.clone();
2983 vm.active_processes.insert(
2984 payload.process_id.clone(),
2985 ActiveProcess::new(
2986 kernel_pid,
2987 kernel_handle,
2988 GuestRuntimeKind::JavaScript,
2989 ActiveExecution::Tool(tool_execution),
2990 )
2991 .with_guest_cwd(guest_cwd.clone())
2992 .with_host_cwd(resolve_vm_guest_path_to_host(vm, &guest_cwd)),
2993 );
2994 self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
2995 spawn_tool_process_events(ToolProcessEventRequest {
2996 sidecar_requests: self.sidecar_requests.clone(),
2997 connection_id: connection_id.clone(),
2998 session_id: session_id.clone(),
2999 vm_id: vm_id.clone(),
3000 tool_resolution,
3001 cancelled,
3002 pending_events,
3003 events_overflowed,
3004 });
3005
3006 return Ok(DispatchResult {
3007 response: self.respond(
3008 request,
3009 ResponsePayload::ProcessStarted(ProcessStartedResponse {
3010 process_id: payload.process_id,
3011 pid: Some(kernel_pid),
3012 }),
3013 ),
3014 events: Vec::new(),
3015 });
3016 }
3017 }
3018
3019 let resolved = resolve_execute_request(vm, &payload)?;
3020 let mut env = resolved.env.clone();
3021 let sandbox_root = normalize_host_path(&vm.cwd);
3022 env.insert(
3023 String::from(EXECUTION_SANDBOX_ROOT_ENV),
3024 sandbox_root.to_string_lossy().into_owned(),
3025 );
3026 if resolved.runtime == GuestRuntimeKind::JavaScript {
3027 env.insert(
3028 String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
3029 String::from("1"),
3030 );
3031 } else if resolved.runtime == GuestRuntimeKind::WebAssembly {
3032 env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
3033 }
3034 let argv = std::iter::once(resolved.entrypoint.clone())
3035 .chain(resolved.execution_args.iter().cloned())
3036 .collect::<Vec<_>>();
3037 let kernel_handle = vm
3038 .kernel
3039 .spawn_process(
3040 &resolved.command,
3041 argv,
3042 SpawnOptions {
3043 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
3044 cwd: Some(resolved.guest_cwd.clone()),
3045 ..SpawnOptions::default()
3046 },
3047 )
3048 .map_err(kernel_error)?;
3049 let kernel_pid = kernel_handle.pid();
3050
3051 let (execution, process_env) = match resolved.runtime {
3052 GuestRuntimeKind::JavaScript => {
3053 let inline_code = load_javascript_entrypoint_source(
3054 vm,
3055 &resolved.host_cwd,
3056 &resolved.entrypoint,
3057 &env,
3058 );
3059 prepare_javascript_shadow(vm, &resolved)?;
3060
3061 let context =
3062 self.javascript_engine
3063 .create_context(CreateJavascriptContextRequest {
3064 vm_id: vm_id.clone(),
3065 bootstrap_module: None,
3066 compile_cache_root: Some(self.cache_root.join("node-compile-cache")),
3067 });
3068 let module_reader = build_module_reader(vm, &resolved)
3069 .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
3070 let execution = self
3071 .javascript_engine
3072 .start_execution_with_module_reader(
3073 StartJavascriptExecutionRequest {
3074 guest_runtime: guest_runtime_identity(vm, None, None),
3075 vm_id: vm_id.clone(),
3076 context_id: context.context_id,
3077 argv: std::iter::once(resolved.entrypoint.clone())
3078 .chain(resolved.execution_args.iter().cloned())
3079 .collect(),
3080 env: env.clone(),
3081 cwd: resolved.host_cwd.clone(),
3082 limits: javascript_execution_limits(vm),
3083 inline_code,
3084 },
3085 module_reader,
3086 )
3087 .map_err(javascript_error)?;
3088 (ActiveExecution::Javascript(execution), env.clone())
3089 }
3090 GuestRuntimeKind::Python => {
3091 let python_file_path = python_file_entrypoint(&resolved.entrypoint);
3092 let pyodide_dist_path = self
3093 .python_engine
3094 .bundled_pyodide_dist_path_for_vm(&vm_id)
3095 .map_err(python_error)?;
3096 let pyodide_cache_path = pyodide_dist_path
3097 .parent()
3098 .and_then(Path::parent)
3099 .unwrap_or(pyodide_dist_path.as_path())
3100 .join("pyodide-package-cache");
3101 add_runtime_guest_path_mapping(
3102 &mut env,
3103 PYTHON_PYODIDE_GUEST_ROOT,
3104 &pyodide_dist_path,
3105 );
3106 add_runtime_guest_path_mapping(
3107 &mut env,
3108 PYTHON_PYODIDE_CACHE_GUEST_ROOT,
3109 &pyodide_cache_path,
3110 );
3111 add_runtime_host_access_path(
3112 &mut env,
3113 "AGENT_OS_EXTRA_FS_READ_PATHS",
3114 &pyodide_dist_path,
3115 true,
3116 );
3117 add_runtime_host_access_path(
3118 &mut env,
3119 "AGENT_OS_EXTRA_FS_READ_PATHS",
3120 &pyodide_cache_path,
3121 true,
3122 );
3123 add_runtime_host_access_path(
3124 &mut env,
3125 "AGENT_OS_EXTRA_FS_WRITE_PATHS",
3126 &pyodide_cache_path,
3127 false,
3128 );
3129 let context = self
3130 .python_engine
3131 .create_context(CreatePythonContextRequest {
3132 vm_id: vm_id.clone(),
3133 pyodide_dist_path,
3134 });
3135 let execution = self
3136 .python_engine
3137 .start_execution(StartPythonExecutionRequest {
3138 vm_id: vm_id.clone(),
3139 context_id: context.context_id,
3140 code: resolved.entrypoint.clone(),
3141 file_path: python_file_path,
3142 env: env.clone(),
3143 cwd: resolved.host_cwd.clone(),
3144 limits: python_execution_limits(vm),
3145 guest_runtime: guest_runtime_identity(vm, None, None),
3146 })
3147 .map_err(python_error)?;
3148 (ActiveExecution::Python(execution), env.clone())
3149 }
3150 GuestRuntimeKind::WebAssembly => {
3151 let wasm_limits = wasm_execution_limits(vm);
3152 let wasm_guest_runtime =
3153 guest_runtime_identity(vm, Some(u64::from(kernel_pid)), Some(0));
3154 let wasm_permission_tier = resolved.wasm_permission_tier.unwrap_or_else(|| {
3155 resolve_wasm_permission_tier(
3156 vm,
3157 Some(&resolved.command),
3158 None,
3159 &resolved.entrypoint,
3160 )
3161 });
3162 let context = self.wasm_engine.create_context(CreateWasmContextRequest {
3163 vm_id: vm_id.clone(),
3164 module_path: Some(resolved.entrypoint.clone()),
3165 });
3166 let execution = self
3167 .wasm_engine
3168 .start_execution(StartWasmExecutionRequest {
3169 vm_id: vm_id.clone(),
3170 context_id: context.context_id,
3171 argv: resolved.process_args.clone(),
3172 env: env.clone(),
3173 cwd: resolved.host_cwd.clone(),
3174 permission_tier: execution_wasm_permission_tier(wasm_permission_tier),
3175 limits: wasm_limits,
3176 guest_runtime: wasm_guest_runtime,
3177 })
3178 .map_err(wasm_error)?;
3179 (ActiveExecution::Wasm(Box::new(execution)), env)
3180 }
3181 };
3182 let child_pid = execution.child_pid();
3183 let kernel_stdin_writer_fd = install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?;
3184 vm.active_processes.insert(
3185 payload.process_id.clone(),
3186 ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
3187 .with_kernel_stdin_writer_fd(kernel_stdin_writer_fd)
3188 .with_guest_cwd(resolved.guest_cwd.clone())
3189 .with_env(process_env)
3190 .with_host_cwd(resolved.host_cwd.clone()),
3191 );
3192 self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
3193
3194 Ok(DispatchResult {
3195 response: self.respond(
3196 request,
3197 ResponsePayload::ProcessStarted(ProcessStartedResponse {
3198 process_id: payload.process_id,
3199 pid: Some(if child_pid == 0 {
3200 kernel_pid
3201 } else {
3202 child_pid
3203 }),
3204 }),
3205 ),
3206 events: Vec::new(),
3207 })
3208 }
3209
3210 pub(crate) async fn write_stdin(
3211 &mut self,
3212 request: &RequestFrame,
3213 payload: WriteStdinRequest,
3214 ) -> Result<DispatchResult, SidecarError> {
3215 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3216 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3217
3218 let vm = self
3219 .vms
3220 .get_mut(&vm_id)
3221 .ok_or_else(|| missing_vm_error(&vm_id))?;
3222 let process = vm
3223 .active_processes
3224 .get_mut(&payload.process_id)
3225 .ok_or_else(|| {
3226 SidecarError::InvalidState(format!(
3227 "VM {vm_id} has no active process {}",
3228 payload.process_id
3229 ))
3230 })?;
3231 process.execution.write_stdin(&payload.chunk)?;
3232 write_kernel_process_stdin(&mut vm.kernel, process, &payload.chunk)?;
3233
3234 Ok(DispatchResult {
3235 response: self.respond(
3236 request,
3237 ResponsePayload::StdinWritten(StdinWrittenResponse {
3238 process_id: payload.process_id,
3239 accepted_bytes: payload.chunk.len() as u64,
3240 }),
3241 ),
3242 events: Vec::new(),
3243 })
3244 }
3245
3246 pub(crate) async fn close_stdin(
3247 &mut self,
3248 request: &RequestFrame,
3249 payload: CloseStdinRequest,
3250 ) -> Result<DispatchResult, SidecarError> {
3251 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3252 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3253
3254 let vm = self
3255 .vms
3256 .get_mut(&vm_id)
3257 .ok_or_else(|| missing_vm_error(&vm_id))?;
3258 let process = vm
3259 .active_processes
3260 .get_mut(&payload.process_id)
3261 .ok_or_else(|| {
3262 SidecarError::InvalidState(format!(
3263 "VM {vm_id} has no active process {}",
3264 payload.process_id
3265 ))
3266 })?;
3267 process.execution.close_stdin()?;
3268 close_kernel_process_stdin(&mut vm.kernel, process)?;
3269
3270 Ok(DispatchResult {
3271 response: self.respond(
3272 request,
3273 ResponsePayload::StdinClosed(StdinClosedResponse {
3274 process_id: payload.process_id,
3275 }),
3276 ),
3277 events: Vec::new(),
3278 })
3279 }
3280
3281 pub(crate) async fn kill_process(
3282 &mut self,
3283 request: &RequestFrame,
3284 payload: KillProcessRequest,
3285 ) -> Result<DispatchResult, SidecarError> {
3286 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3287 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3288 self.kill_process_internal(&vm_id, &payload.process_id, &payload.signal)?;
3289
3290 Ok(DispatchResult {
3291 response: self.respond(
3292 request,
3293 ResponsePayload::ProcessKilled(ProcessKilledResponse {
3294 process_id: payload.process_id,
3295 }),
3296 ),
3297 events: Vec::new(),
3298 })
3299 }
3300
3301 pub(crate) async fn find_listener(
3302 &mut self,
3303 request: &RequestFrame,
3304 payload: FindListenerRequest,
3305 ) -> Result<DispatchResult, SidecarError> {
3306 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3307 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3308 require_vm_inspection_permission(
3309 &self.bridge,
3310 &vm_id,
3311 "network.inspect",
3312 "network",
3313 &socket_query_resource(SocketQueryKind::TcpListener, &payload),
3314 )?;
3315
3316 let listener =
3317 find_socket_state_entry(self.vms.get(&vm_id), SocketQueryKind::TcpListener, &payload)?;
3318
3319 Ok(DispatchResult {
3320 response: self.respond(
3321 request,
3322 ResponsePayload::ListenerSnapshot(ListenerSnapshotResponse { listener }),
3323 ),
3324 events: Vec::new(),
3325 })
3326 }
3327
3328 pub(crate) async fn get_process_snapshot(
3329 &mut self,
3330 request: &RequestFrame,
3331 _payload: GetProcessSnapshotRequest,
3332 ) -> Result<DispatchResult, SidecarError> {
3333 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3334 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3335 require_vm_inspection_permission(
3336 &self.bridge,
3337 &vm_id,
3338 "process.inspect",
3339 "process",
3340 "process://snapshot",
3341 )?;
3342
3343 let processes = self
3344 .vms
3345 .get_mut(&vm_id)
3346 .map(|vm| {
3347 prune_exited_process_snapshots(vm);
3348 snapshot_vm_processes(vm)
3349 })
3350 .unwrap_or_default();
3351
3352 Ok(DispatchResult {
3353 response: self.respond(
3354 request,
3355 ResponsePayload::ProcessSnapshot(ProcessSnapshotResponse { processes }),
3356 ),
3357 events: Vec::new(),
3358 })
3359 }
3360
3361 pub(crate) async fn find_bound_udp(
3362 &mut self,
3363 request: &RequestFrame,
3364 payload: FindBoundUdpRequest,
3365 ) -> Result<DispatchResult, SidecarError> {
3366 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3367 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3368
3369 let lookup_request = FindListenerRequest {
3370 host: payload.host,
3371 port: payload.port,
3372 path: None,
3373 };
3374 require_vm_inspection_permission(
3375 &self.bridge,
3376 &vm_id,
3377 "network.inspect",
3378 "network",
3379 &socket_query_resource(SocketQueryKind::UdpBound, &lookup_request),
3380 )?;
3381 let socket = find_socket_state_entry(
3382 self.vms.get(&vm_id),
3383 SocketQueryKind::UdpBound,
3384 &lookup_request,
3385 )?;
3386
3387 Ok(DispatchResult {
3388 response: self.respond(
3389 request,
3390 ResponsePayload::BoundUdpSnapshot(BoundUdpSnapshotResponse { socket }),
3391 ),
3392 events: Vec::new(),
3393 })
3394 }
3395
3396 pub(crate) async fn vm_fetch(
3397 &mut self,
3398 request: &RequestFrame,
3399 payload: VmFetchRequest,
3400 ) -> Result<DispatchResult, SidecarError> {
3401 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3402 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3403
3404 let vm = self
3405 .vms
3406 .get_mut(&vm_id)
3407 .ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
3408 let target_path = if payload.path.starts_with('/') {
3409 payload.path.clone()
3410 } else {
3411 format!("/{}", payload.path)
3412 };
3413 let request_url = Url::parse(&format!("http://127.0.0.1:{}{target_path}", payload.port))
3414 .map_err(|error| {
3415 SidecarError::InvalidState(format!(
3416 "invalid vm.fetch target {target_path:?}: {error}"
3417 ))
3418 })?;
3419 let header_values: BTreeMap<String, Value> = serde_json::from_str(&payload.headers_json)
3420 .map_err(|error| {
3421 SidecarError::InvalidState(format!(
3422 "vm.fetch headers_json must be valid JSON: {error}"
3423 ))
3424 })?;
3425 let options = JavascriptHttpRequestOptions {
3426 method: Some(payload.method),
3427 headers: header_values,
3428 body: payload.body,
3429 reject_unauthorized: None,
3430 };
3431 let headers = parse_http_header_collection(&options.headers, "vm.fetch headers")?;
3432 let target_process_id = find_kernel_http_listener_process(vm, payload.port);
3433 if let Some(target_process_id) = target_process_id {
3434 let max_fetch_response_bytes = vm.limits.http.max_fetch_response_bytes;
3435 let response_json = match dispatch_kernel_http_fetch(
3436 &self.bridge,
3437 &vm_id,
3438 vm,
3439 &target_process_id,
3440 payload.port,
3441 &target_path,
3442 &options,
3443 &headers,
3444 max_fetch_response_bytes,
3445 ) {
3446 Ok(response_json) => response_json,
3447 Err(error) => {
3448 if let Some(exit_code) = kernel_http_fetch_target_exit_code(&error) {
3449 let _ = vm;
3450 self.finish_active_process_exit(&vm_id, &target_process_id, exit_code)?;
3451 }
3452 return Err(error);
3453 }
3454 };
3455 let response = self.respond(
3456 request,
3457 ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
3458 );
3459 ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
3460
3461 return Ok(DispatchResult {
3462 response,
3463 events: Vec::new(),
3464 });
3465 }
3466
3467 let Some((target_process_id, server_id)) =
3468 vm.active_processes
3469 .iter()
3470 .find_map(|(process_id, process)| {
3471 process
3472 .http_servers
3473 .iter()
3474 .find(|(_, server)| server.guest_local_addr.port() == payload.port)
3475 .map(|(server_id, _)| (process_id.clone(), *server_id))
3476 })
3477 else {
3478 return Err(SidecarError::Execution(format!(
3479 "vm.fetch could not find a guest HTTP listener on port {}",
3480 payload.port
3481 )));
3482 };
3483 let socket_paths = build_javascript_socket_path_context(vm)?;
3484 let resource_limits = vm.kernel.resource_limits().clone();
3485 let process = vm
3486 .active_processes
3487 .get_mut(&target_process_id)
3488 .ok_or_else(|| {
3489 SidecarError::InvalidState(format!(
3490 "vm.fetch target process disappeared: {target_process_id}"
3491 ))
3492 })?;
3493 let request_json = serialize_http_loopback_request(&request_url, &options, &headers)?;
3494 let response_json = dispatch_loopback_http_request(LoopbackHttpDispatchRequest {
3495 bridge: &self.bridge,
3496 vm_id: &vm_id,
3497 dns: &vm.dns,
3498 socket_paths: &socket_paths,
3499 kernel: &mut vm.kernel,
3500 process,
3501 resource_limits: &resource_limits,
3502 server_id,
3503 request_json: &request_json,
3504 })?;
3505
3506 let response = self.respond(
3507 request,
3508 ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
3509 );
3510 ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
3511
3512 Ok(DispatchResult {
3513 response,
3514 events: Vec::new(),
3515 })
3516 }
3517
3518 pub(crate) async fn get_signal_state(
3519 &mut self,
3520 request: &RequestFrame,
3521 payload: GetSignalStateRequest,
3522 ) -> Result<DispatchResult, SidecarError> {
3523 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3524 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3525
3526 let handlers = self
3527 .vms
3528 .get(&vm_id)
3529 .and_then(|vm| vm.signal_states.get(&payload.process_id))
3530 .cloned()
3531 .unwrap_or_default();
3532
3533 Ok(DispatchResult {
3534 response: self.respond(
3535 request,
3536 ResponsePayload::SignalState(SignalStateResponse {
3537 process_id: payload.process_id,
3538 handlers: handlers.into_iter().collect(),
3539 }),
3540 ),
3541 events: Vec::new(),
3542 })
3543 }
3544
3545 pub(crate) async fn get_zombie_timer_count(
3546 &mut self,
3547 request: &RequestFrame,
3548 _payload: GetZombieTimerCountRequest,
3549 ) -> Result<DispatchResult, SidecarError> {
3550 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3551 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3552
3553 let count = self
3554 .vms
3555 .get(&vm_id)
3556 .map(|vm| vm.kernel.zombie_timer_count() as u64)
3557 .unwrap_or_default();
3558
3559 Ok(DispatchResult {
3560 response: self.respond(
3561 request,
3562 ResponsePayload::ZombieTimerCount(ZombieTimerCountResponse { count }),
3563 ),
3564 events: Vec::new(),
3565 })
3566 }
3567
3568 pub(crate) fn kill_process_internal(
3569 &mut self,
3570 vm_id: &str,
3571 process_id: &str,
3572 signal: &str,
3573 ) -> Result<(), SidecarError> {
3574 let signal_name = signal.to_owned();
3575 let signal = parse_signal(signal)?;
3576 let vm = self
3577 .vms
3578 .get_mut(vm_id)
3579 .ok_or_else(|| SidecarError::InvalidState(format!("unknown sidecar VM {vm_id}")))?;
3580 let process = vm.active_processes.get_mut(process_id).ok_or_else(|| {
3581 SidecarError::InvalidState(format!("VM {vm_id} has no active process {process_id}"))
3582 })?;
3583 let kernel_pid = process.kernel_pid;
3584
3585 enum KillBehavior {
3586 Tool,
3587 SharedV8StateOnly,
3588 SharedV8Continue,
3589 SharedV8Terminate,
3590 SharedV8DispatchOrTerminate,
3591 Noop,
3592 HostPid(u32),
3593 }
3594
3595 let behavior = match &process.execution {
3596 ActiveExecution::Tool(_) => KillBehavior::Tool,
3597 ActiveExecution::Javascript(execution)
3598 if execution.uses_shared_v8_runtime() && matches!(signal, 0 | libc::SIGSTOP) =>
3599 {
3600 KillBehavior::SharedV8StateOnly
3601 }
3602 ActiveExecution::Javascript(execution)
3603 if execution.uses_shared_v8_runtime() && signal == libc::SIGCONT =>
3604 {
3605 KillBehavior::SharedV8Continue
3606 }
3607 ActiveExecution::Wasm(execution)
3608 if execution.uses_shared_v8_runtime()
3609 && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
3610 {
3611 KillBehavior::SharedV8StateOnly
3612 }
3613 ActiveExecution::Python(execution)
3614 if execution.uses_shared_v8_runtime()
3615 && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
3616 {
3617 KillBehavior::SharedV8StateOnly
3618 }
3619 ActiveExecution::Javascript(execution)
3620 if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
3621 {
3622 KillBehavior::SharedV8Terminate
3623 }
3624 ActiveExecution::Wasm(execution)
3625 if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
3626 {
3627 KillBehavior::SharedV8Terminate
3628 }
3629 ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime() => {
3630 KillBehavior::SharedV8DispatchOrTerminate
3631 }
3632 ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime() => {
3633 KillBehavior::SharedV8Terminate
3634 }
3635 ActiveExecution::Python(execution) if execution.uses_shared_v8_runtime() => {
3636 KillBehavior::SharedV8Terminate
3637 }
3638 ActiveExecution::Javascript(execution) if execution.child_pid() == 0 => {
3639 KillBehavior::Noop
3640 }
3641 _ => KillBehavior::HostPid(process.execution.child_pid()),
3642 };
3643
3644 match behavior {
3645 KillBehavior::Tool => {
3646 let ActiveExecution::Tool(execution) = &process.execution else {
3647 unreachable!("kill behavior must match tool execution");
3648 };
3649 if signal != 0 {
3650 execution.cancelled.store(true, Ordering::Relaxed);
3651 process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
3652 128 + signal,
3653 ))?;
3654 }
3655 }
3656 KillBehavior::SharedV8StateOnly => {
3657 if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
3658 vm.kernel
3659 .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
3660 .map_err(kernel_error)?;
3661 }
3662 }
3663 KillBehavior::SharedV8Continue => {
3664 vm.kernel
3665 .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
3666 .map_err(kernel_error)?;
3667 if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
3668 process.execution.terminate()?;
3669 }
3670 }
3671 KillBehavior::SharedV8Terminate => {
3672 if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
3673 close_kernel_process_stdin(&mut vm.kernel, process)?;
3674 }
3675 process.execution.terminate()?;
3676 let needs_synthetic_exit = matches!(process.execution, ActiveExecution::Wasm(_))
3677 || (signal == SIGKILL
3678 && matches!(process.execution, ActiveExecution::Javascript(_)));
3679 if signal != 0 && needs_synthetic_exit {
3680 process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
3681 128 + signal,
3682 ))?;
3683 }
3684 }
3685 KillBehavior::SharedV8DispatchOrTerminate => {
3686 if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
3687 process.execution.terminate()?;
3688 }
3689 }
3690 KillBehavior::Noop => {}
3691 KillBehavior::HostPid(pid) => {
3692 if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
3693 close_kernel_process_stdin(&mut vm.kernel, process)?;
3694 }
3695 signal_runtime_process(pid, signal)?;
3696 }
3697 }
3698 emit_security_audit_event(
3699 &self.bridge,
3700 vm_id,
3701 "security.process.kill",
3702 audit_fields([
3703 (String::from("source"), String::from("control_plane")),
3704 (String::from("source_pid"), String::from("0")),
3705 (String::from("target_pid"), process.kernel_pid.to_string()),
3706 (String::from("process_id"), process_id.to_owned()),
3707 (String::from("signal"), signal_name),
3708 (
3709 String::from("host_pid"),
3710 process.execution.child_pid().to_string(),
3711 ),
3712 ]),
3713 );
3714 Ok(())
3715 }
3716
3717 pub async fn pump_process_events(
3718 &mut self,
3719 ownership: &OwnershipScope,
3720 ) -> Result<bool, SidecarError> {
3721 let mut emitted_any = false;
3722
3723 let mut queued_envelopes = Vec::new();
3724 {
3725 let pending_capacity = self.pending_process_event_capacity();
3726 let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
3727 SidecarError::InvalidState(String::from("process event receiver unavailable"))
3728 })?;
3729 loop {
3730 if queued_envelopes.len() >= pending_capacity {
3731 if receiver.is_empty() {
3732 break;
3733 }
3734 return Err(process_event_queue_overflow_error());
3735 }
3736 match receiver.try_recv() {
3737 Ok(envelope) => {
3738 queued_envelopes.push(envelope);
3739 emitted_any = true;
3740 }
3741 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
3742 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
3743 }
3744 }
3745 }
3746 for envelope in queued_envelopes {
3747 self.queue_pending_process_event(envelope)?;
3748 }
3749
3750 let vm_ids = self.vm_ids_for_scope(ownership)?;
3751 for vm_id in vm_ids {
3752 while let Some(vm) = self.vms.get(&vm_id) {
3753 let connection_id = vm.connection_id.clone();
3754 let session_id = vm.session_id.clone();
3755 let process_ids = self
3756 .vms
3757 .get(&vm_id)
3758 .map(|vm| vm.active_processes.keys().cloned().collect::<Vec<_>>())
3759 .unwrap_or_default();
3760 let mut emitted_this_pass = false;
3761
3762 for process_id in process_ids {
3763 if self
3764 .vms
3765 .get(&vm_id)
3766 .is_some_and(|vm| vm.detached_child_processes.contains(&process_id))
3767 {
3768 continue;
3769 }
3770 enum ProcessPollResult {
3771 Event(Box<Option<ActiveExecutionEvent>>),
3772 RecoverClosedChannel,
3773 }
3774 let poll_result = {
3775 let Some(vm) = self.vms.get_mut(&vm_id) else {
3776 continue;
3777 };
3778 let Some(process) = vm.active_processes.get_mut(&process_id) else {
3779 continue;
3780 };
3781 if let Some(event) = process.pending_execution_events.pop_front() {
3782 ProcessPollResult::Event(Box::new(Some(event)))
3783 } else {
3784 match process.execution.poll_event(Duration::ZERO).await {
3785 Ok(event) => ProcessPollResult::Event(Box::new(event)),
3786 Err(SidecarError::Execution(message))
3787 if (process.runtime == GuestRuntimeKind::JavaScript
3788 && closed_javascript_event_channel(&message))
3789 || (process.runtime == GuestRuntimeKind::Python
3790 && closed_python_event_channel(&message))
3791 || (process.runtime == GuestRuntimeKind::WebAssembly
3792 && closed_wasm_event_channel(&message)) =>
3793 {
3794 ProcessPollResult::RecoverClosedChannel
3795 }
3796 Err(other) => return Err(other),
3797 }
3798 }
3799 };
3800 let event = match poll_result {
3801 ProcessPollResult::Event(event) => *event,
3802 ProcessPollResult::RecoverClosedChannel => {
3803 self.recover_closed_root_runtime_process_event(&vm_id, &process_id)?
3804 }
3805 };
3806
3807 let Some(event) = event else {
3808 continue;
3809 };
3810
3811 if Self::internal_execution_event(&event) {
3812 self.handle_execution_event(&vm_id, &process_id, event)?;
3817 } else {
3818 self.queue_pending_process_event(ProcessEventEnvelope {
3819 connection_id: connection_id.clone(),
3820 session_id: session_id.clone(),
3821 vm_id: vm_id.clone(),
3822 process_id: process_id.clone(),
3823 event,
3824 })?;
3825 }
3826 emitted_any = true;
3827 emitted_this_pass = true;
3828 }
3829
3830 if !emitted_this_pass {
3831 break;
3832 }
3833 }
3834
3835 if self.pump_detached_child_process_events(&vm_id)? {
3836 emitted_any = true;
3837 }
3838 }
3839
3840 Ok(emitted_any)
3841 }
3842
3843 fn internal_execution_event(event: &ActiveExecutionEvent) -> bool {
3844 matches!(
3845 event,
3846 ActiveExecutionEvent::JavascriptSyncRpcRequest(_)
3847 | ActiveExecutionEvent::PythonVfsRpcRequest(_)
3848 | ActiveExecutionEvent::SignalState { .. }
3849 )
3850 }
3851
3852 fn recover_closed_root_runtime_process_event(
3853 &mut self,
3854 vm_id: &str,
3855 process_id: &str,
3856 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
3857 let Some(vm) = self.vms.get_mut(vm_id) else {
3858 return Ok(None);
3859 };
3860 let Some(process) = vm.active_processes.get(process_id) else {
3861 return Ok(None);
3862 };
3863 if process.execution.uses_shared_v8_runtime() {
3864 return Ok(None);
3865 }
3866 if process.runtime != GuestRuntimeKind::JavaScript
3867 && process.runtime != GuestRuntimeKind::Python
3868 && process.runtime != GuestRuntimeKind::WebAssembly
3869 {
3870 return Ok(None);
3871 }
3872 let runtime_child_pid = process.execution.child_pid();
3873 if runtime_child_pid == 0 {
3874 return Ok(None);
3875 }
3876 if let Some(status) = runtime_child_exit_status(runtime_child_pid)? {
3877 return Ok(Some(ActiveExecutionEvent::Exited(status)));
3878 }
3879 if runtime_child_is_alive(runtime_child_pid)? {
3880 return Ok(None);
3881 }
3882 Ok(Some(ActiveExecutionEvent::Exited(0)))
3883 }
3884
3885 fn active_process_by_path<'a>(
3886 process: &'a ActiveProcess,
3887 child_path: &[&str],
3888 ) -> Option<&'a ActiveProcess> {
3889 let mut current = process;
3890 for child_id in child_path {
3891 current = current.child_processes.get(*child_id)?;
3892 }
3893 Some(current)
3894 }
3895
3896 fn active_process_by_path_mut<'a>(
3897 process: &'a mut ActiveProcess,
3898 child_path: &[&str],
3899 ) -> Option<&'a mut ActiveProcess> {
3900 let mut current = process;
3901 for child_id in child_path {
3902 current = current.child_processes.get_mut(*child_id)?;
3903 }
3904 Some(current)
3905 }
3906
3907 fn active_process_by_owned_path_mut<'a>(
3908 process: &'a mut ActiveProcess,
3909 child_path: &[String],
3910 ) -> Option<&'a mut ActiveProcess> {
3911 let mut current = process;
3912 for child_id in child_path {
3913 current = current.child_processes.get_mut(child_id)?;
3914 }
3915 Some(current)
3916 }
3917
3918 fn active_process_path_by_kernel_pid(
3919 process: &ActiveProcess,
3920 kernel_pid: u32,
3921 ) -> Option<Vec<String>> {
3922 if process.kernel_pid == kernel_pid {
3923 return Some(Vec::new());
3924 }
3925
3926 for (child_id, child) in &process.child_processes {
3927 let Some(mut path) = Self::active_process_path_by_kernel_pid(child, kernel_pid) else {
3928 continue;
3929 };
3930 path.insert(0, child_id.clone());
3931 return Some(path);
3932 }
3933
3934 None
3935 }
3936
3937 fn descendant_parent_process<'a>(
3938 vm: &'a VmState,
3939 process_id: &str,
3940 child_path: &[&str],
3941 ) -> Option<&'a ActiveProcess> {
3942 let root = vm.active_processes.get(process_id)?;
3943 Self::active_process_by_path(root, child_path)
3944 }
3945
3946 fn descendant_parent_process_mut<'a>(
3947 vm: &'a mut VmState,
3948 process_id: &str,
3949 child_path: &[&str],
3950 ) -> Option<&'a mut ActiveProcess> {
3951 let root = vm.active_processes.get_mut(process_id)?;
3952 Self::active_process_by_path_mut(root, child_path)
3953 }
3954
3955 fn child_process_path_label(process_id: &str, child_path: &[&str]) -> String {
3956 if child_path.is_empty() {
3957 process_id.to_owned()
3958 } else {
3959 format!("{process_id}/{}", child_path.join("/"))
3960 }
3961 }
3962
3963 fn adopt_detached_child_processes(
3964 current_process_id: &str,
3965 process: &mut ActiveProcess,
3966 ) -> Vec<(String, ActiveProcess)> {
3967 let mut adopted = Vec::new();
3968 let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
3969 for child_id in child_ids {
3970 let child_process_id = format!("{current_process_id}/{child_id}");
3971 let Some(mut child) = process.child_processes.remove(&child_id) else {
3972 continue;
3973 };
3974 if child.detached {
3975 adopted.push((child_process_id, child));
3976 continue;
3977 }
3978
3979 adopted.extend(Self::adopt_detached_child_processes(
3980 &child_process_id,
3981 &mut child,
3982 ));
3983 process.child_processes.insert(child_id, child);
3984 }
3985 adopted
3986 }
3987
3988 fn child_process_signal_key<'a>(process_id: &'a str, child_path: &[&'a str]) -> &'a str {
3989 child_path.last().copied().unwrap_or(process_id)
3990 }
3991
3992 fn resolve_detached_child_process_path(
3993 vm: &VmState,
3994 detached_process_id: &str,
3995 ) -> Option<(String, Vec<String>)> {
3996 let root_process_id = vm
3997 .active_processes
3998 .keys()
3999 .filter(|candidate| {
4000 detached_process_id == candidate.as_str()
4001 || detached_process_id
4002 .strip_prefix(candidate.as_str())
4003 .is_some_and(|remainder| remainder.starts_with('/'))
4004 })
4005 .max_by_key(|candidate| candidate.len())?
4006 .clone();
4007
4008 let remainder = detached_process_id
4009 .strip_prefix(root_process_id.as_str())
4010 .unwrap_or_default();
4011 if remainder.is_empty() {
4012 return Some((root_process_id, Vec::new()));
4013 }
4014
4015 Some((
4016 root_process_id,
4017 remainder
4018 .trim_start_matches('/')
4019 .split('/')
4020 .map(str::to_owned)
4021 .collect(),
4022 ))
4023 }
4024
4025 fn pump_detached_child_process_events(&mut self, vm_id: &str) -> Result<bool, SidecarError> {
4026 let detached_process_ids = self
4027 .vms
4028 .get(vm_id)
4029 .map(|vm| {
4030 vm.detached_child_processes
4031 .iter()
4032 .cloned()
4033 .collect::<Vec<_>>()
4034 })
4035 .unwrap_or_default();
4036 let mut emitted_any = false;
4037 for detached_process_id in detached_process_ids {
4038 let Some((root_process_id, child_path)) = self
4039 .vms
4040 .get(vm_id)
4041 .and_then(|vm| Self::resolve_detached_child_process_path(vm, &detached_process_id))
4042 else {
4043 if let Some(vm) = self.vms.get_mut(vm_id) {
4044 vm.detached_child_processes.remove(&detached_process_id);
4045 }
4046 continue;
4047 };
4048 if child_path.is_empty() {
4049 loop {
4050 enum ProcessPollResult {
4051 Event(Box<Option<ActiveExecutionEvent>>),
4052 RecoverClosedChannel,
4053 }
4054 let poll_result = {
4055 let Some(vm) = self.vms.get_mut(vm_id) else {
4056 break;
4057 };
4058 let Some(process) = vm.active_processes.get_mut(&root_process_id) else {
4059 break;
4060 };
4061 if let Some(event) = process.pending_execution_events.pop_front() {
4062 ProcessPollResult::Event(Box::new(Some(event)))
4063 } else {
4064 match process.execution.poll_event_blocking(Duration::ZERO) {
4065 Ok(event) => ProcessPollResult::Event(Box::new(event)),
4066 Err(SidecarError::Execution(message))
4067 if (process.runtime == GuestRuntimeKind::JavaScript
4068 && closed_javascript_event_channel(&message))
4069 || (process.runtime == GuestRuntimeKind::Python
4070 && closed_python_event_channel(&message))
4071 || (process.runtime == GuestRuntimeKind::WebAssembly
4072 && closed_wasm_event_channel(&message)) =>
4073 {
4074 ProcessPollResult::RecoverClosedChannel
4075 }
4076 Err(error) => return Err(error),
4077 }
4078 }
4079 };
4080 let event = match poll_result {
4081 ProcessPollResult::Event(event) => *event,
4082 ProcessPollResult::RecoverClosedChannel => {
4083 self.recover_closed_root_runtime_process_event(vm_id, &root_process_id)?
4084 }
4085 };
4086 let Some(event) = event else {
4087 break;
4088 };
4089 let Some((connection_id, session_id)) = self
4090 .vms
4091 .get(vm_id)
4092 .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
4093 else {
4094 break;
4095 };
4096 match event {
4097 ActiveExecutionEvent::Stdout(chunk) => {
4098 self.queue_pending_process_event(ProcessEventEnvelope {
4099 connection_id,
4100 session_id,
4101 vm_id: vm_id.to_owned(),
4102 process_id: detached_process_id.clone(),
4103 event: ActiveExecutionEvent::Stdout(chunk),
4104 })?;
4105 emitted_any = true;
4106 }
4107 ActiveExecutionEvent::Stderr(chunk) => {
4108 self.queue_pending_process_event(ProcessEventEnvelope {
4109 connection_id,
4110 session_id,
4111 vm_id: vm_id.to_owned(),
4112 process_id: detached_process_id.clone(),
4113 event: ActiveExecutionEvent::Stderr(chunk),
4114 })?;
4115 emitted_any = true;
4116 }
4117 ActiveExecutionEvent::Exited(exit_code) => {
4118 if let Some(vm) = self.vms.get_mut(vm_id) {
4119 vm.detached_child_processes.remove(&detached_process_id);
4120 }
4121 self.queue_pending_process_event(ProcessEventEnvelope {
4122 connection_id,
4123 session_id,
4124 vm_id: vm_id.to_owned(),
4125 process_id: detached_process_id.clone(),
4126 event: ActiveExecutionEvent::Exited(exit_code),
4127 })?;
4128 emitted_any = true;
4129 break;
4130 }
4131 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
4132 self.handle_javascript_sync_rpc_request(
4133 vm_id,
4134 &root_process_id,
4135 request,
4136 )?;
4137 }
4138 ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
4139 self.handle_python_vfs_rpc_request(vm_id, &root_process_id, *request)?;
4140 }
4141 ActiveExecutionEvent::SignalState {
4142 signal,
4143 registration,
4144 } => {
4145 if let Some(vm) = self.vms.get_mut(vm_id) {
4146 vm.signal_states
4147 .entry(root_process_id.clone())
4148 .or_default()
4149 .insert(signal, registration);
4150 }
4151 }
4152 }
4153 }
4154 continue;
4155 }
4156
4157 let parent_path = child_path[..child_path.len() - 1]
4158 .iter()
4159 .map(String::as_str)
4160 .collect::<Vec<_>>();
4161 let child_process_id = child_path.last().expect("child path cannot be empty");
4162
4163 loop {
4164 let event = match self.poll_descendant_javascript_child_process(
4165 vm_id,
4166 &root_process_id,
4167 &parent_path,
4168 child_process_id,
4169 0,
4170 ) {
4171 Ok(event) => event,
4172 Err(SidecarError::InvalidState(message))
4173 if message.contains("unknown child process")
4174 || message.contains("unknown child process path") =>
4175 {
4176 if let Some(vm) = self.vms.get_mut(vm_id) {
4177 vm.detached_child_processes.remove(&detached_process_id);
4178 }
4179 break;
4180 }
4181 Err(error) if is_javascript_child_process_gone_error(&error) => {
4182 if let Some(vm) = self.vms.get_mut(vm_id) {
4183 vm.detached_child_processes.remove(&detached_process_id);
4184 }
4185 break;
4186 }
4187 Err(error) => return Err(error),
4188 };
4189
4190 let Some(event_type) = event.get("type").and_then(Value::as_str) else {
4191 break;
4192 };
4193 let Some((connection_id, session_id)) = self
4194 .vms
4195 .get(vm_id)
4196 .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
4197 else {
4198 break;
4199 };
4200
4201 let envelope = match event_type {
4202 "stdout" => Some(ProcessEventEnvelope {
4203 connection_id: connection_id.clone(),
4204 session_id: session_id.clone(),
4205 vm_id: vm_id.to_owned(),
4206 process_id: detached_process_id.clone(),
4207 event: ActiveExecutionEvent::Stdout(javascript_sync_rpc_bytes_arg(
4208 &[event.get("data").cloned().unwrap_or(Value::Null)],
4209 0,
4210 "detached child_process stdout",
4211 )?),
4212 }),
4213 "stderr" => Some(ProcessEventEnvelope {
4214 connection_id: connection_id.clone(),
4215 session_id: session_id.clone(),
4216 vm_id: vm_id.to_owned(),
4217 process_id: detached_process_id.clone(),
4218 event: ActiveExecutionEvent::Stderr(javascript_sync_rpc_bytes_arg(
4219 &[event.get("data").cloned().unwrap_or(Value::Null)],
4220 0,
4221 "detached child_process stderr",
4222 )?),
4223 }),
4224 "exit" => {
4225 if let Some(vm) = self.vms.get_mut(vm_id) {
4226 vm.detached_child_processes.remove(&detached_process_id);
4227 }
4228 Some(ProcessEventEnvelope {
4229 connection_id,
4230 session_id,
4231 vm_id: vm_id.to_owned(),
4232 process_id: detached_process_id.clone(),
4233 event: ActiveExecutionEvent::Exited(
4234 event
4235 .get("exitCode")
4236 .and_then(Value::as_i64)
4237 .map(|value| value as i32)
4238 .unwrap_or(1),
4239 ),
4240 })
4241 }
4242 _ => None,
4243 };
4244
4245 let Some(envelope) = envelope else {
4246 break;
4247 };
4248 self.queue_pending_process_event(envelope)?;
4249 emitted_any = true;
4250
4251 if event_type == "exit" {
4252 break;
4253 }
4254 }
4255 }
4256
4257 Ok(emitted_any)
4258 }
4259 pub(crate) fn drain_queued_descendant_javascript_child_process_events(
4260 &mut self,
4261 vm_id: &str,
4262 process_id: &str,
4263 child_path: &[&str],
4264 ) -> Result<(), SidecarError> {
4265 if child_path.is_empty() {
4266 return Ok(());
4267 }
4268 let target_process_id = Self::child_process_path_label(process_id, child_path);
4269 let mut child_capacity = self
4270 .vms
4271 .get(vm_id)
4272 .and_then(|vm| vm.active_processes.get(process_id))
4273 .and_then(|root| descendant_pending_execution_event_capacity(root, child_path));
4274
4275 let mut deferred = VecDeque::new();
4276 while let Some(envelope) = self.pending_process_events.pop_front() {
4277 if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
4278 if matches!(child_capacity, Some(0)) {
4279 self.pending_process_events.push_front(envelope);
4280 while let Some(deferred_envelope) = deferred.pop_back() {
4281 self.pending_process_events.push_front(deferred_envelope);
4282 }
4283 return Err(process_event_queue_overflow_error());
4284 }
4285 if let Some(vm) = self.vms.get_mut(vm_id) {
4286 if let Some(root) = vm.active_processes.get_mut(process_id) {
4287 if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
4288 child.queue_pending_execution_event(envelope.event)?;
4289 child_capacity = child_capacity.map(|capacity| capacity - 1);
4290 continue;
4291 }
4292 }
4293 }
4294 }
4295 deferred.push_back(envelope);
4296 }
4297 self.pending_process_events = deferred;
4298
4299 let mut queued = Vec::new();
4300 {
4301 let transfer_capacity = self
4302 .pending_process_event_capacity()
4303 .min(child_capacity.unwrap_or(usize::MAX));
4304 let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
4305 SidecarError::InvalidState(String::from("process event receiver unavailable"))
4306 })?;
4307 loop {
4308 if queued.len() >= transfer_capacity {
4309 if receiver.is_empty() {
4310 break;
4311 }
4312 return Err(process_event_queue_overflow_error());
4313 }
4314 match receiver.try_recv() {
4315 Ok(envelope) => queued.push(envelope),
4316 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
4317 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
4318 }
4319 }
4320 }
4321 for envelope in queued {
4322 if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
4323 if let Some(vm) = self.vms.get_mut(vm_id) {
4324 if let Some(root) = vm.active_processes.get_mut(process_id) {
4325 if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
4326 child.queue_pending_execution_event(envelope.event)?;
4327 continue;
4328 }
4329 }
4330 }
4331 }
4332 self.queue_pending_process_event(envelope)?;
4333 }
4334
4335 Ok(())
4336 }
4337
4338 pub(crate) fn handle_execution_event(
4339 &mut self,
4340 vm_id: &str,
4341 process_id: &str,
4342 event: ActiveExecutionEvent,
4343 ) -> Result<Option<EventFrame>, SidecarError> {
4344 let Some(vm) = self.vms.get(vm_id) else {
4345 log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
4346 return Ok(None);
4347 };
4348 if !vm.active_processes.contains_key(process_id) {
4349 log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
4350 return Ok(None);
4351 }
4352 let (connection_id, session_id) = { (vm.connection_id.clone(), vm.session_id.clone()) };
4353 let ownership = OwnershipScope::vm(&connection_id, &session_id, vm_id);
4354
4355 if self.capture_extension_process_output_event(vm_id, process_id, &event) {
4356 return Ok(None);
4357 }
4358
4359 match event {
4360 ActiveExecutionEvent::Stdout(chunk) => Ok(Some(EventFrame::new(
4361 ownership,
4362 EventPayload::ProcessOutput(ProcessOutputEvent {
4363 process_id: process_id.to_owned(),
4364 channel: StreamChannel::Stdout,
4365 chunk,
4366 }),
4367 ))),
4368 ActiveExecutionEvent::Stderr(chunk) => Ok(Some(EventFrame::new(
4369 ownership,
4370 EventPayload::ProcessOutput(ProcessOutputEvent {
4371 process_id: process_id.to_owned(),
4372 channel: StreamChannel::Stderr,
4373 chunk,
4374 }),
4375 ))),
4376 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
4377 self.handle_javascript_sync_rpc_request(vm_id, process_id, request)?;
4378 Ok(None)
4379 }
4380 ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
4381 self.handle_python_vfs_rpc_request(vm_id, process_id, *request)?;
4382 Ok(None)
4383 }
4384 ActiveExecutionEvent::SignalState {
4385 signal,
4386 registration,
4387 } => {
4388 let Some(vm) = self.vms.get_mut(vm_id) else {
4389 return Ok(None);
4390 };
4391 if !vm.active_processes.contains_key(process_id) {
4392 return Ok(None);
4393 }
4394 vm.signal_states
4395 .entry(process_id.to_owned())
4396 .or_default()
4397 .insert(signal, registration);
4398 Ok(None)
4399 }
4400 ActiveExecutionEvent::Exited(exit_code) => {
4401 let became_idle = self
4402 .finish_active_process_exit(vm_id, process_id, exit_code)?
4403 .unwrap_or(false);
4404
4405 if became_idle {
4406 self.bridge.emit_lifecycle(vm_id, LifecycleState::Ready)?;
4407 }
4408
4409 Ok(Some(EventFrame::new(
4410 ownership,
4411 EventPayload::ProcessExited(ProcessExitedEvent {
4412 process_id: process_id.to_owned(),
4413 exit_code,
4414 }),
4415 )))
4416 }
4417 }
4418 }
4419
4420 pub(crate) fn finish_active_process_exit(
4421 &mut self,
4422 vm_id: &str,
4423 process_id: &str,
4424 exit_code: i32,
4425 ) -> Result<Option<bool>, SidecarError> {
4426 let Some(vm) = self.vms.get_mut(vm_id) else {
4427 log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
4428 return Ok(None);
4429 };
4430 if !vm.active_processes.contains_key(process_id) {
4431 log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
4432 return Ok(None);
4433 }
4434
4435 prune_exited_process_snapshots(vm);
4436 let process_table = vm.kernel.list_processes();
4437 let Some(mut process) = vm.active_processes.remove(process_id) else {
4438 return Ok(None);
4439 };
4440 if let Some(info) = process_table.get(&process.kernel_pid) {
4441 vm.exited_process_snapshots
4442 .push_back(ExitedProcessSnapshot {
4443 captured_at: Instant::now(),
4444 process: build_process_snapshot_entry(
4445 process_id,
4446 &process,
4447 info,
4448 Some(exit_code),
4449 ),
4450 });
4451 }
4452 let detached_children = Self::adopt_detached_child_processes(process_id, &mut process);
4453 sync_process_host_writes_to_kernel(vm, &process)?;
4454 terminate_child_process_tree(&mut vm.kernel, &mut process);
4455 process.kernel_handle.finish(exit_code);
4456 let _ = vm.kernel.wait_and_reap(process.kernel_pid);
4457 vm.signal_states.remove(process_id);
4458 for (detached_process_id, detached_child) in detached_children {
4459 vm.detached_child_processes
4460 .insert(detached_process_id.clone());
4461 vm.active_processes
4462 .insert(detached_process_id, detached_child);
4463 }
4464 let became_idle = vm.active_processes.is_empty();
4465 self.prune_extension_process_resource(process_id);
4466
4467 Ok(Some(became_idle))
4468 }
4469
4470 pub(crate) fn drain_process_events_blocking_with_limit(
4471 &mut self,
4472 vm_id: &str,
4473 process_id: &str,
4474 max_events: usize,
4475 ) -> Result<Vec<ActiveExecutionEvent>, SidecarError> {
4476 let mut events = Vec::new();
4477 if max_events == 0 {
4478 return Ok(events);
4479 }
4480 let mut deadline = Instant::now() + Duration::from_millis(150);
4481
4482 loop {
4483 if events.len() >= max_events {
4484 break;
4485 }
4486 let event = {
4487 let Some(vm) = self.vms.get_mut(vm_id) else {
4488 break;
4489 };
4490 let Some(process) = vm.active_processes.get_mut(process_id) else {
4491 break;
4492 };
4493 if let Some(event) = process.pending_execution_events.pop_front() {
4494 Some(event)
4495 } else {
4496 match process.execution.poll_event_blocking(Duration::ZERO) {
4497 Ok(event) => event,
4498 Err(SidecarError::Execution(_)) => None,
4499 Err(other) => return Err(other),
4500 }
4501 }
4502 };
4503
4504 let Some(event) = event else {
4505 if Instant::now() >= deadline {
4506 break;
4507 }
4508 let blocking_wait = deadline.saturating_duration_since(Instant::now());
4509 if blocking_wait.is_zero() {
4510 break;
4511 }
4512 if events.len() >= max_events {
4513 break;
4514 }
4515 let delayed_event = {
4516 let Some(vm) = self.vms.get_mut(vm_id) else {
4517 break;
4518 };
4519 let Some(process) = vm.active_processes.get_mut(process_id) else {
4520 break;
4521 };
4522 if let Some(event) = process.pending_execution_events.pop_front() {
4523 Some(event)
4524 } else {
4525 match process.execution.poll_event_blocking(blocking_wait) {
4526 Ok(event) => event,
4527 Err(SidecarError::Execution(_)) => None,
4528 Err(other) => return Err(other),
4529 }
4530 }
4531 };
4532 let Some(event) = delayed_event else {
4533 break;
4534 };
4535 events.push(event);
4536 deadline = Instant::now() + Duration::from_millis(150);
4537 continue;
4538 };
4539 events.push(event);
4540 deadline = Instant::now() + Duration::from_millis(150);
4541 }
4542
4543 Ok(events)
4544 }
4545
4546 pub(crate) fn handle_python_vfs_rpc_request(
4547 &mut self,
4548 vm_id: &str,
4549 process_id: &str,
4550 request: PythonVfsRpcRequest,
4551 ) -> Result<(), SidecarError> {
4552 match request.method {
4553 PythonVfsRpcMethod::Read
4554 | PythonVfsRpcMethod::Write
4555 | PythonVfsRpcMethod::Stat
4556 | PythonVfsRpcMethod::ReadDir
4557 | PythonVfsRpcMethod::Mkdir => {
4558 filesystem_handle_python_vfs_rpc_request(self, vm_id, process_id, request)
4559 }
4560 PythonVfsRpcMethod::HttpRequest => {
4561 self.handle_python_http_rpc_request(vm_id, process_id, request)
4562 }
4563 PythonVfsRpcMethod::DnsLookup => {
4564 self.handle_python_dns_rpc_request(vm_id, process_id, request)
4565 }
4566 PythonVfsRpcMethod::SubprocessRun => {
4567 self.handle_python_subprocess_rpc_request(vm_id, process_id, request)
4568 }
4569 }
4570 }
4571
4572 fn handle_python_http_rpc_request(
4573 &mut self,
4574 vm_id: &str,
4575 process_id: &str,
4576 request: PythonVfsRpcRequest,
4577 ) -> Result<(), SidecarError> {
4578 let Some(vm) = self.vms.get(vm_id) else {
4579 return Ok(());
4580 };
4581 if !vm.active_processes.contains_key(process_id) {
4582 return Ok(());
4583 }
4584 let response = (|| {
4585 let url_text = request.url.as_deref().ok_or_else(|| {
4586 SidecarError::InvalidState(String::from("python httpRequest requires a url"))
4587 })?;
4588 let url = Url::parse(url_text)
4589 .map_err(|error| SidecarError::Execution(format!("ERR_INVALID_URL: {error}")))?;
4590 let host = url.host_str().ok_or_else(|| {
4591 SidecarError::Execution(String::from("ERR_INVALID_URL: missing host"))
4592 })?;
4593 let port = url.port_or_known_default().ok_or_else(|| {
4594 SidecarError::Execution(String::from("ERR_INVALID_URL: missing port"))
4595 })?;
4596 self.bridge.require_network_access(
4597 vm_id,
4598 NetworkOperation::Http,
4599 format_tcp_resource(host, port),
4600 )?;
4601 let pinned_addresses = if let Ok(literal_ip) = host.parse::<IpAddr>() {
4608 filter_dns_safe_ip_addrs(vec![literal_ip], host)?
4609 } else {
4610 filter_dns_safe_ip_addrs(
4611 resolve_dns_ip_addrs(
4612 &self.bridge,
4613 &vm.kernel,
4614 vm_id,
4615 &vm.dns,
4616 host,
4617 DnsLookupPolicy::SkipPermissions,
4618 )?,
4619 host,
4620 )?
4621 };
4622 let mut headers = BTreeMap::new();
4623 for (name, value) in &request.headers {
4624 headers.insert(name.clone(), Value::String(value.clone()));
4625 }
4626 let options = JavascriptHttpRequestOptions {
4627 method: Some(
4628 request
4629 .http_method
4630 .clone()
4631 .unwrap_or_else(|| String::from("GET")),
4632 ),
4633 headers,
4634 body: request.body_base64.as_deref().map(|body| {
4635 String::from_utf8(
4636 base64::engine::general_purpose::STANDARD
4637 .decode(body)
4638 .unwrap_or_default(),
4639 )
4640 .unwrap_or_default()
4641 }),
4642 reject_unauthorized: None,
4643 };
4644 let headers =
4645 parse_http_header_collection(&options.headers, "python httpRequest headers")?;
4646 let response =
4647 issue_outbound_http_request(&url, &options, &headers, &pinned_addresses)?;
4648 let payload_json = response.as_str().ok_or_else(|| {
4649 SidecarError::Execution(String::from(
4650 "python httpRequest returned a non-string response payload",
4651 ))
4652 })?;
4653 let payload: Value = serde_json::from_str(payload_json).map_err(|error| {
4654 SidecarError::Execution(format!(
4655 "python httpRequest response must be valid JSON: {error}"
4656 ))
4657 })?;
4658 let header_map = payload
4659 .get("headers")
4660 .and_then(Value::as_array)
4661 .map(|entries| {
4662 let mut normalized = BTreeMap::<String, Vec<String>>::new();
4663 for entry in entries {
4664 let Some(pair) = entry.as_array() else {
4665 continue;
4666 };
4667 let Some(name) = pair.first().and_then(Value::as_str) else {
4668 continue;
4669 };
4670 let Some(value) = pair.get(1).and_then(Value::as_str) else {
4671 continue;
4672 };
4673 normalized
4674 .entry(name.to_owned())
4675 .or_default()
4676 .push(value.to_owned());
4677 }
4678 normalized
4679 })
4680 .unwrap_or_default();
4681 Ok(PythonVfsRpcResponsePayload::Http {
4682 status: payload
4683 .get("status")
4684 .and_then(Value::as_u64)
4685 .map(|value| value as u16)
4686 .unwrap_or_default(),
4687 reason: payload
4688 .get("statusText")
4689 .and_then(Value::as_str)
4690 .unwrap_or_default()
4691 .to_owned(),
4692 url: payload
4693 .get("url")
4694 .and_then(Value::as_str)
4695 .unwrap_or(url_text)
4696 .to_owned(),
4697 headers: header_map,
4698 body_base64: payload
4699 .get("body")
4700 .and_then(Value::as_str)
4701 .unwrap_or_default()
4702 .to_owned(),
4703 })
4704 })();
4705
4706 self.respond_python_rpc(vm_id, process_id, request.id, response)
4707 }
4708
4709 fn handle_python_dns_rpc_request(
4710 &mut self,
4711 vm_id: &str,
4712 process_id: &str,
4713 request: PythonVfsRpcRequest,
4714 ) -> Result<(), SidecarError> {
4715 let Some(vm) = self.vms.get(vm_id) else {
4716 return Ok(());
4717 };
4718 if !vm.active_processes.contains_key(process_id) {
4719 return Ok(());
4720 }
4721 let response = (|| {
4722 let hostname = request.hostname.as_deref().ok_or_else(|| {
4723 SidecarError::InvalidState(String::from("python dnsLookup requires a hostname"))
4724 })?;
4725 let mut addresses = filter_dns_safe_ip_addrs(
4726 resolve_dns_ip_addrs(
4727 &self.bridge,
4728 &vm.kernel,
4729 vm_id,
4730 &vm.dns,
4731 hostname,
4732 DnsLookupPolicy::CheckPermissions,
4733 )?,
4734 hostname,
4735 )?;
4736 if let Some(family) = request.family {
4737 addresses.retain(|address| {
4738 matches!((family, address), (4, IpAddr::V4(_)) | (6, IpAddr::V6(_)))
4739 });
4740 }
4741 Ok(PythonVfsRpcResponsePayload::DnsLookup {
4742 addresses: addresses
4743 .into_iter()
4744 .map(|address| address.to_string())
4745 .collect(),
4746 })
4747 })();
4748
4749 self.respond_python_rpc(vm_id, process_id, request.id, response)
4750 }
4751
4752 fn handle_python_subprocess_rpc_request(
4753 &mut self,
4754 vm_id: &str,
4755 process_id: &str,
4756 request: PythonVfsRpcRequest,
4757 ) -> Result<(), SidecarError> {
4758 let command = request.command.clone().ok_or_else(|| {
4759 SidecarError::InvalidState(String::from("python subprocessRun requires a command"))
4760 })?;
4761 let (internal_bootstrap_env, cwd) = {
4762 let Some(vm) = self.vms.get(vm_id) else {
4763 return Ok(());
4764 };
4765 let Some(process) = vm.active_processes.get(process_id) else {
4766 return Ok(());
4767 };
4768 let virtual_home = guest_virtual_home(vm);
4769 let cwd = request.cwd.clone().or_else(|| {
4770 guest_runtime_path_for_host_path(
4771 &vm.guest_env,
4772 &virtual_home,
4773 &vm.host_cwd,
4774 &process.host_cwd.to_string_lossy(),
4775 )
4776 });
4777 (
4778 sanitize_javascript_child_process_internal_bootstrap_env(&vm.guest_env),
4779 cwd,
4780 )
4781 };
4782 let response = self
4783 .spawn_javascript_child_process_sync(
4784 vm_id,
4785 process_id,
4786 JavascriptChildProcessSpawnRequest {
4787 command,
4788 args: request.args.clone(),
4789 options: JavascriptChildProcessSpawnOptions {
4790 cwd,
4791 env: request.env.clone(),
4792 input: None,
4793 internal_bootstrap_env,
4794 shell: request.shell,
4795 detached: false,
4796 stdio: vec![
4797 String::from("pipe"),
4798 String::from("pipe"),
4799 String::from("pipe"),
4800 ],
4801 timeout: None,
4802 kill_signal: None,
4803 },
4804 },
4805 request.max_buffer,
4806 )
4807 .map(|payload| PythonVfsRpcResponsePayload::SubprocessRun {
4808 exit_code: payload
4809 .get("code")
4810 .and_then(Value::as_i64)
4811 .map(|value| value as i32)
4812 .unwrap_or(1),
4813 stdout: payload
4814 .get("stdout")
4815 .and_then(Value::as_str)
4816 .unwrap_or_default()
4817 .to_owned(),
4818 stderr: payload
4819 .get("stderr")
4820 .and_then(Value::as_str)
4821 .unwrap_or_default()
4822 .to_owned(),
4823 max_buffer_exceeded: payload
4824 .get("maxBufferExceeded")
4825 .and_then(Value::as_bool)
4826 .unwrap_or(false),
4827 });
4828
4829 self.respond_python_rpc(vm_id, process_id, request.id, response)
4830 }
4831
4832 fn respond_python_rpc(
4833 &mut self,
4834 vm_id: &str,
4835 process_id: &str,
4836 request_id: u64,
4837 response: Result<PythonVfsRpcResponsePayload, SidecarError>,
4838 ) -> Result<(), SidecarError> {
4839 let Some(vm) = self.vms.get_mut(vm_id) else {
4840 return Ok(());
4841 };
4842 let Some(process) = vm.active_processes.get_mut(process_id) else {
4843 return Ok(());
4844 };
4845 let result = match response {
4846 Ok(payload) => process
4847 .execution
4848 .respond_python_vfs_rpc_success(request_id, payload),
4849 Err(error) => process.execution.respond_python_vfs_rpc_error(
4850 request_id,
4851 "ERR_AGENT_OS_PYTHON_VFS_RPC",
4852 error.to_string(),
4853 ),
4854 };
4855 match result {
4856 Ok(()) => Ok(()),
4857 Err(error) if is_broken_pipe_error(&error) => Ok(()),
4858 Err(error) => Err(error),
4859 }
4860 }
4861
4862 pub(crate) fn resolve_javascript_child_process_execution(
4863 &self,
4864 vm: &VmState,
4865 parent_env: &BTreeMap<String, String>,
4866 parent_guest_cwd: &str,
4867 parent_host_cwd: &Path,
4868 request: &JavascriptChildProcessSpawnRequest,
4869 ) -> Result<ResolvedChildProcessExecution, SidecarError> {
4870 let mut runtime_env = parent_env.clone();
4871 runtime_env.extend(request.options.internal_bootstrap_env.clone());
4872 let (guest_cwd, host_cwd_override) = request
4873 .options
4874 .cwd
4875 .as_deref()
4876 .map(|cwd| {
4877 let normalized_parent_host_cwd = normalize_host_path(parent_host_cwd);
4878 let requested_host_cwd = normalize_host_path(Path::new(cwd));
4879 if path_is_within_root(&requested_host_cwd, &normalized_parent_host_cwd) {
4880 let relative = requested_host_cwd
4881 .strip_prefix(&normalized_parent_host_cwd)
4882 .unwrap_or_else(|_| Path::new(""));
4883 let relative = relative.to_string_lossy().replace('\\', "/");
4884 let guest_cwd = if relative.is_empty() {
4885 parent_guest_cwd.to_owned()
4886 } else {
4887 normalize_path(&format!("{parent_guest_cwd}/{relative}"))
4888 };
4889 (guest_cwd, Some(requested_host_cwd))
4890 } else if Path::new(cwd).is_relative() {
4891 (
4892 normalize_path(&format!("{parent_guest_cwd}/{cwd}")),
4893 Some(normalize_host_path(&parent_host_cwd.join(cwd))),
4894 )
4895 } else {
4896 (normalize_path(cwd), None)
4897 }
4898 })
4899 .unwrap_or_else(|| (parent_guest_cwd.to_owned(), None));
4900 let inherited_host_cwd = (host_cwd_override.is_none() && guest_cwd == parent_guest_cwd)
4901 .then(|| normalize_host_path(parent_host_cwd));
4902 let host_cwd = host_cwd_override
4903 .or(inherited_host_cwd)
4904 .or_else(|| {
4905 host_runtime_path_for_guest_path_with_env(
4906 vm,
4907 &runtime_env,
4908 &guest_cwd,
4909 parent_host_cwd,
4910 )
4911 })
4912 .unwrap_or_else(|| {
4913 let candidate = PathBuf::from(&guest_cwd);
4914 if guest_cwd == parent_guest_cwd {
4915 normalize_host_path(parent_host_cwd)
4916 } else if candidate.is_absolute() {
4917 shadow_path_for_guest(vm, &guest_cwd)
4918 } else {
4919 vm.host_cwd.clone()
4920 }
4921 });
4922 let mut env = parent_env.clone();
4923 env.extend(request.options.env.clone());
4924 env.remove("AGENT_OS_GUEST_ENTRYPOINT");
4927 env.remove("AGENT_OS_NODE_EVAL");
4928
4929 let (command, process_args) = if request.options.shell {
4930 let tokens = tokenize_shell_free_command(&request.command);
4931 let requires_shell = command_requires_shell(&request.command)
4932 || tokens.first().is_some_and(|command| {
4933 is_posix_shell_builtin(command) || shell_first_token_requires_shell(command)
4934 });
4935 if requires_shell {
4936 if !vm.command_guest_paths.contains_key("sh") {
4937 return Err(SidecarError::InvalidState(format!(
4938 "shell-mode child_process command requires /bin/sh, which is not \
4939 installed in this VM (install a software package that provides sh, \
4940 for example @secure-exec/coreutils): {}",
4941 request.command
4942 )));
4943 }
4944 (
4945 String::from("sh"),
4946 vec![String::from("-c"), request.command.clone()],
4947 )
4948 } else {
4949 let Some((command, args)) = tokens.split_first() else {
4950 return Err(SidecarError::InvalidState(String::from(
4951 "child_process shell command must not be empty",
4952 )));
4953 };
4954 (command.clone(), args.to_vec())
4955 }
4956 } else {
4957 (request.command.clone(), request.args.clone())
4958 };
4959 let process_args = apply_shell_cwd_prefix(&command, process_args, &guest_cwd);
4960 if is_tool_command(vm, &command) {
4961 let command = normalized_tool_command_name(&command).unwrap_or(command);
4962 return Ok(ResolvedChildProcessExecution {
4963 command: command.clone(),
4964 process_args: std::iter::once(command.clone())
4965 .chain(process_args.iter().cloned())
4966 .collect(),
4967 runtime: GuestRuntimeKind::JavaScript,
4968 entrypoint: command,
4969 execution_args: process_args,
4970 env,
4971 guest_cwd,
4972 host_cwd,
4973 wasm_permission_tier: None,
4974 tool_command: true,
4975 });
4976 }
4977
4978 if is_path_like_specifier(&command)
4979 && matches!(
4980 Path::new(&command).extension().and_then(|ext| ext.to_str()),
4981 Some("js" | "mjs" | "cjs" | "ts" | "mts" | "cts")
4982 )
4983 {
4984 let guest_entrypoint = if command.starts_with('/') {
4985 normalize_path(&command)
4986 } else if command.starts_with("file:") {
4987 normalize_path(command.trim_start_matches("file:"))
4988 } else {
4989 normalize_path(&format!("{guest_cwd}/{command}"))
4990 };
4991 let host_entrypoint = if command.starts_with("./") || command.starts_with("../") {
4992 normalize_host_path(&host_cwd.join(&command))
4993 } else {
4994 host_runtime_path_for_guest_path_with_env(
4995 vm,
4996 &runtime_env,
4997 &guest_entrypoint,
4998 parent_host_cwd,
4999 )
5000 .unwrap_or_else(|| {
5001 let candidate = PathBuf::from(&guest_entrypoint);
5002 if candidate.is_absolute() {
5003 candidate
5004 } else {
5005 host_cwd.join(&guest_entrypoint)
5006 }
5007 })
5008 };
5009 env.insert(String::from("AGENT_OS_GUEST_ENTRYPOINT"), guest_entrypoint);
5010 let guest_entrypoint = env.get("AGENT_OS_GUEST_ENTRYPOINT").cloned();
5011 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
5012
5013 return Ok(ResolvedChildProcessExecution {
5014 command: command.clone(),
5015 process_args: std::iter::once(command)
5016 .chain(process_args.iter().cloned())
5017 .collect(),
5018 runtime: GuestRuntimeKind::JavaScript,
5019 entrypoint: host_entrypoint.to_string_lossy().into_owned(),
5020 execution_args: process_args,
5021 env,
5022 guest_cwd,
5023 host_cwd,
5024 wasm_permission_tier: None,
5025 tool_command: false,
5026 });
5027 }
5028
5029 if is_node_runtime_command(&command) {
5030 if let Some(cli) = resolve_host_node_cli_entrypoint(&command) {
5031 env.insert(
5032 String::from("AGENT_OS_NODE_EVAL"),
5033 build_host_node_cli_eval(&cli),
5034 );
5035 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5036 add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
5037 add_runtime_host_access_path(
5038 &mut env,
5039 "AGENT_OS_EXTRA_FS_READ_PATHS",
5040 &cli.package_root,
5041 true,
5042 );
5043
5044 return Ok(ResolvedChildProcessExecution {
5045 command: command.clone(),
5046 process_args: std::iter::once(command.clone())
5047 .chain(process_args.iter().cloned())
5048 .collect(),
5049 runtime: GuestRuntimeKind::JavaScript,
5050 entrypoint: String::from("-e"),
5051 execution_args: std::iter::once(cli.guest_entrypoint.clone())
5052 .chain(process_args.iter().cloned())
5053 .collect(),
5054 env,
5055 guest_cwd,
5056 host_cwd,
5057 wasm_permission_tier: None,
5058 tool_command: false,
5059 });
5060 }
5061
5062 if process_args.is_empty() {
5063 env.insert(String::from("AGENT_OS_NODE_EVAL"), String::new());
5064 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5065
5066 return Ok(ResolvedChildProcessExecution {
5067 command: command.clone(),
5068 process_args: vec![command.clone()],
5069 runtime: GuestRuntimeKind::JavaScript,
5070 entrypoint: String::from("-e"),
5071 execution_args: Vec::new(),
5072 env,
5073 guest_cwd,
5074 host_cwd,
5075 wasm_permission_tier: None,
5076 tool_command: false,
5077 });
5078 }
5079
5080 if let Some((entrypoint, execution_args)) =
5081 resolve_special_node_cli_invocation(&process_args, &mut env)
5082 {
5083 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5084
5085 return Ok(ResolvedChildProcessExecution {
5086 command: command.clone(),
5087 process_args: std::iter::once(command.clone())
5088 .chain(process_args.iter().cloned())
5089 .collect(),
5090 runtime: GuestRuntimeKind::JavaScript,
5091 entrypoint,
5092 execution_args,
5093 env,
5094 guest_cwd,
5095 host_cwd,
5096 wasm_permission_tier: None,
5097 tool_command: false,
5098 });
5099 }
5100
5101 let Some(entrypoint_specifier) = process_args.first() else {
5102 return Err(SidecarError::InvalidState(format!(
5103 "{command} child_process spawn requires an entrypoint"
5104 )));
5105 };
5106
5107 let (entrypoint, execution_args) = if is_path_like_specifier(entrypoint_specifier) {
5108 let guest_entrypoint = if entrypoint_specifier.starts_with('/') {
5109 normalize_path(entrypoint_specifier)
5110 } else if entrypoint_specifier.starts_with("file:") {
5111 normalize_path(entrypoint_specifier.trim_start_matches("file:"))
5112 } else {
5113 normalize_path(&format!("{guest_cwd}/{entrypoint_specifier}"))
5114 };
5115 let host_entrypoint = if entrypoint_specifier.starts_with("./")
5116 || entrypoint_specifier.starts_with("../")
5117 {
5118 normalize_host_path(&host_cwd.join(entrypoint_specifier))
5119 } else {
5120 host_runtime_path_for_guest_path_with_env(
5121 vm,
5122 &runtime_env,
5123 &guest_entrypoint,
5124 parent_host_cwd,
5125 )
5126 .unwrap_or_else(|| {
5127 let candidate = PathBuf::from(&guest_entrypoint);
5128 if candidate.is_absolute() {
5129 candidate
5130 } else {
5131 host_cwd.join(&guest_entrypoint)
5132 }
5133 })
5134 };
5135 env.insert(String::from("AGENT_OS_GUEST_ENTRYPOINT"), guest_entrypoint);
5136 (
5137 host_entrypoint.to_string_lossy().into_owned(),
5138 process_args.iter().skip(1).cloned().collect(),
5139 )
5140 } else {
5141 (
5142 entrypoint_specifier.clone(),
5143 process_args.iter().skip(1).cloned().collect(),
5144 )
5145 };
5146 let guest_entrypoint = env.get("AGENT_OS_GUEST_ENTRYPOINT").cloned();
5147 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
5148
5149 return Ok(ResolvedChildProcessExecution {
5150 command: command.clone(),
5151 process_args: std::iter::once(command)
5152 .chain(process_args.iter().cloned())
5153 .collect(),
5154 runtime: GuestRuntimeKind::JavaScript,
5155 entrypoint,
5156 execution_args,
5157 env,
5158 guest_cwd,
5159 host_cwd,
5160 wasm_permission_tier: None,
5161 tool_command: false,
5162 });
5163 }
5164
5165 if command == PYTHON_COMMAND {
5166 return Err(SidecarError::InvalidState(String::from(
5167 "nested python child_process execution is not supported yet",
5168 )));
5169 }
5170
5171 let guest_entrypoint = resolve_guest_command_entrypoint(
5172 vm,
5173 &guest_cwd,
5174 &command,
5175 env.get("PATH").map(String::as_str),
5176 )
5177 .ok_or_else(|| SidecarError::InvalidState(format!("command not found: {command}")))?;
5178 let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
5179 let wasm_permission_tier = vm.command_permissions.get(&command).copied().or_else(|| {
5180 Path::new(&guest_entrypoint)
5181 .file_name()
5182 .and_then(|name| name.to_str())
5183 .and_then(|name| vm.command_permissions.get(name).copied())
5184 });
5185 if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
5186 resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
5187 {
5188 prepare_guest_runtime_env(
5189 vm,
5190 &mut env,
5191 &guest_cwd,
5192 &host_cwd,
5193 Some(javascript_guest_entrypoint),
5194 )?;
5195
5196 return Ok(ResolvedChildProcessExecution {
5197 command: command.clone(),
5198 process_args: std::iter::once(command)
5199 .chain(process_args.iter().cloned())
5200 .collect(),
5201 runtime: GuestRuntimeKind::JavaScript,
5202 entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
5203 execution_args: process_args,
5204 env,
5205 guest_cwd,
5206 host_cwd,
5207 wasm_permission_tier: None,
5208 tool_command: false,
5209 });
5210 }
5211 prepare_guest_runtime_env(
5212 vm,
5213 &mut env,
5214 &guest_cwd,
5215 &host_cwd,
5216 Some(guest_entrypoint.clone()),
5217 )?;
5218
5219 Ok(ResolvedChildProcessExecution {
5220 command: command.clone(),
5221 process_args: std::iter::once(command)
5222 .chain(process_args.iter().cloned())
5223 .collect(),
5224 runtime: GuestRuntimeKind::WebAssembly,
5225 entrypoint: host_entrypoint.to_string_lossy().into_owned(),
5226 execution_args: process_args,
5227 env,
5228 guest_cwd,
5229 host_cwd,
5230 wasm_permission_tier,
5231 tool_command: false,
5232 })
5233 }
5234
5235 pub(crate) fn spawn_javascript_child_process(
5236 &mut self,
5237 vm_id: &str,
5238 process_id: &str,
5239 request: JavascriptChildProcessSpawnRequest,
5240 ) -> Result<Value, SidecarError> {
5241 let resolved = {
5242 let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
5243 let parent = vm
5244 .active_processes
5245 .get(process_id)
5246 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5247 self.resolve_javascript_child_process_execution(
5248 vm,
5249 &parent.env,
5250 &parent.guest_cwd,
5251 &parent.host_cwd,
5252 &request,
5253 )?
5254 };
5255 let (parent_kernel_pid, child_process_id) = {
5256 let vm = self
5257 .vms
5258 .get_mut(vm_id)
5259 .ok_or_else(|| missing_vm_error(vm_id))?;
5260 let process = vm
5261 .active_processes
5262 .get_mut(process_id)
5263 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5264 (process.kernel_pid, process.allocate_child_process_id())
5265 };
5266 let sidecar_requests = self.sidecar_requests.clone();
5267 let vm = self
5268 .vms
5269 .get_mut(vm_id)
5270 .ok_or_else(|| missing_vm_error(vm_id))?;
5271 let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
5272 .tool_command
5273 {
5274 let tool_resolution = resolve_tool_command(
5275 vm,
5276 &resolved.command,
5277 &resolved.execution_args,
5278 Some(&resolved.guest_cwd),
5279 )?
5280 .ok_or_else(|| {
5281 SidecarError::InvalidState(format!(
5282 "tool command no longer resolves: {}",
5283 resolved.command
5284 ))
5285 })?;
5286 let kernel_handle = vm
5287 .kernel
5288 .create_virtual_process(
5289 EXECUTION_DRIVER_NAME,
5290 TOOL_DRIVER_NAME,
5291 &resolved.command,
5292 resolved.process_args.clone(),
5293 VirtualProcessOptions {
5294 parent_pid: Some(parent_kernel_pid),
5295 env: resolved.env.clone(),
5296 cwd: Some(resolved.guest_cwd.clone()),
5297 },
5298 )
5299 .map_err(kernel_error)?;
5300 let kernel_pid = kernel_handle.pid();
5301 let tool_execution = ToolExecution::default();
5302 let cancelled = tool_execution.cancelled.clone();
5303 let pending_events = tool_execution.pending_events.clone();
5304 let events_overflowed = tool_execution.events_overflowed.clone();
5305 spawn_tool_process_events(ToolProcessEventRequest {
5306 sidecar_requests: sidecar_requests.clone(),
5307 connection_id: vm.connection_id.clone(),
5308 session_id: vm.session_id.clone(),
5309 vm_id: vm_id.to_owned(),
5310 tool_resolution,
5311 cancelled,
5312 pending_events,
5313 events_overflowed,
5314 });
5315 (
5316 kernel_pid,
5317 kernel_handle,
5318 ActiveExecution::Tool(tool_execution),
5319 None,
5320 )
5321 } else {
5322 let kernel_command = match resolved.runtime {
5323 GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
5324 GuestRuntimeKind::WebAssembly => WASM_COMMAND,
5325 GuestRuntimeKind::Python => {
5326 unreachable!("python child_process execution is rejected")
5327 }
5328 };
5329 let kernel_handle = vm
5330 .kernel
5331 .spawn_process(
5332 kernel_command,
5333 resolved.process_args.clone(),
5334 SpawnOptions {
5335 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
5336 parent_pid: Some(parent_kernel_pid),
5337 env: resolved.env.clone(),
5338 cwd: Some(resolved.guest_cwd.clone()),
5339 },
5340 )
5341 .map_err(kernel_error)?;
5342 let kernel_pid = kernel_handle.pid();
5343 if request.options.detached {
5344 vm.kernel
5345 .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
5346 .map_err(kernel_error)?;
5347 }
5348 let mut execution_env = resolved.env.clone();
5349 execution_env.insert(
5350 String::from(EXECUTION_SANDBOX_ROOT_ENV),
5351 normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
5352 );
5353
5354 let execution = match resolved.runtime {
5355 GuestRuntimeKind::JavaScript => {
5356 execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
5357 &request.options.internal_bootstrap_env,
5358 ));
5359 execution_env.insert(
5360 String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
5361 String::from("1"),
5362 );
5363 let context =
5364 self.javascript_engine
5365 .create_context(CreateJavascriptContextRequest {
5366 vm_id: vm_id.to_owned(),
5367 bootstrap_module: None,
5368 compile_cache_root: Some(
5369 self.cache_root.join("node-compile-cache"),
5370 ),
5371 });
5372 let inline_code = load_javascript_entrypoint_source(
5373 vm,
5374 &resolved.host_cwd,
5375 &resolved.entrypoint,
5376 &execution_env,
5377 );
5378 prepare_javascript_shadow(vm, &resolved)?;
5379
5380 let module_reader = build_module_reader(vm, &resolved)
5381 .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
5382 let execution = self
5383 .javascript_engine
5384 .start_execution_with_module_reader(
5385 StartJavascriptExecutionRequest {
5386 guest_runtime: guest_runtime_identity(
5387 vm,
5388 Some(u64::from(kernel_pid)),
5389 Some(u64::from(parent_kernel_pid)),
5390 ),
5391 vm_id: vm_id.to_owned(),
5392 context_id: context.context_id,
5393 argv: std::iter::once(resolved.entrypoint.clone())
5394 .chain(resolved.execution_args.clone())
5395 .collect(),
5396 env: execution_env,
5397 cwd: resolved.host_cwd.clone(),
5398 limits: javascript_execution_limits(vm),
5399 inline_code,
5400 },
5401 module_reader,
5402 )
5403 .map_err(javascript_error)?;
5404 ActiveExecution::Javascript(execution)
5405 }
5406 GuestRuntimeKind::WebAssembly => {
5407 execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
5408 let wasm_limits = wasm_execution_limits(vm);
5409 let wasm_guest_runtime = guest_runtime_identity(
5410 vm,
5411 Some(u64::from(kernel_pid)),
5412 Some(u64::from(parent_kernel_pid)),
5413 );
5414 let context = self.wasm_engine.create_context(CreateWasmContextRequest {
5415 vm_id: vm_id.to_owned(),
5416 module_path: Some(resolved.entrypoint.clone()),
5417 });
5418 let execution = self
5419 .wasm_engine
5420 .start_execution(StartWasmExecutionRequest {
5421 vm_id: vm_id.to_owned(),
5422 context_id: context.context_id,
5423 argv: resolved.process_args.clone(),
5424 env: execution_env,
5425 cwd: resolved.host_cwd.clone(),
5426 permission_tier: execution_wasm_permission_tier(
5427 resolved
5428 .wasm_permission_tier
5429 .unwrap_or(WasmPermissionTier::Full),
5430 ),
5431 limits: wasm_limits,
5432 guest_runtime: wasm_guest_runtime,
5433 })
5434 .map_err(wasm_error)?;
5435 ActiveExecution::Wasm(Box::new(execution))
5436 }
5437 GuestRuntimeKind::Python => {
5438 unreachable!("python child_process execution is rejected")
5439 }
5440 };
5441 let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
5442 "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5443 "ignore" => {
5444 vm.kernel
5445 .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
5446 .map_err(kernel_error)?;
5447 None
5448 }
5449 "inherit" => None,
5450 _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5451 };
5452 (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
5453 };
5454
5455 let process = vm
5456 .active_processes
5457 .get_mut(process_id)
5458 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5459 process.child_processes.insert(
5460 child_process_id.clone(),
5461 ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
5462 .with_detached(request.options.detached)
5463 .with_guest_cwd(resolved.guest_cwd.clone())
5464 .with_env(resolved.env.clone())
5465 .with_host_cwd(resolved.host_cwd.clone()),
5466 );
5467 if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
5468 process
5469 .child_processes
5470 .get_mut(&child_process_id)
5471 .ok_or_else(|| {
5472 SidecarError::InvalidState(format!(
5473 "child process {child_process_id} disappeared during spawn"
5474 ))
5475 })?
5476 .kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
5477 }
5478 Ok(json!({
5479 "childId": child_process_id,
5480 "pid": kernel_pid,
5481 "command": resolved.command,
5482 "args": resolved.process_args,
5483 }))
5484 }
5485
5486 pub(crate) fn spawn_javascript_child_process_sync(
5487 &mut self,
5488 vm_id: &str,
5489 process_id: &str,
5490 request: JavascriptChildProcessSpawnRequest,
5491 max_buffer: Option<usize>,
5492 ) -> Result<Value, SidecarError> {
5493 let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
5494 let timeout_deadline = request
5495 .options
5496 .timeout
5497 .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
5498 let timeout_signal = request
5499 .options
5500 .kill_signal
5501 .clone()
5502 .unwrap_or_else(|| String::from("SIGTERM"));
5503 let spawned = self.spawn_javascript_child_process(vm_id, process_id, request)?;
5504 let child_process_id = spawned
5505 .get("childId")
5506 .and_then(Value::as_str)
5507 .ok_or_else(|| {
5508 SidecarError::InvalidState(String::from(
5509 "child_process.spawn_sync response is missing childId",
5510 ))
5511 })?
5512 .to_owned();
5513
5514 if let Some(input) = sync_input.as_deref() {
5515 self.write_javascript_child_process_stdin(vm_id, process_id, &child_process_id, input)?;
5516 }
5517 self.close_javascript_child_process_stdin(vm_id, process_id, &child_process_id)?;
5518
5519 let max_buffer = max_buffer.unwrap_or(1024 * 1024);
5520 let mut stdout = Vec::new();
5521 let mut stderr = Vec::new();
5522 let mut max_buffer_exceeded = false;
5523 let mut kill_sent = false;
5524 let mut timed_out = false;
5525
5526 let exit_code = loop {
5527 let wait_ms = if let Some(deadline) = timeout_deadline {
5528 let now = Instant::now();
5529 if now >= deadline {
5530 if !kill_sent {
5531 timed_out = true;
5532 self.kill_javascript_child_process(
5533 vm_id,
5534 process_id,
5535 &child_process_id,
5536 &timeout_signal,
5537 )?;
5538 kill_sent = true;
5539 }
5540 0
5541 } else {
5542 u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
5543 .unwrap_or(50)
5544 }
5545 } else {
5546 50
5547 };
5548 let event =
5549 self.poll_javascript_child_process(vm_id, process_id, &child_process_id, wait_ms)?;
5550 if event.is_null() {
5551 continue;
5552 }
5553
5554 match event.get("type").and_then(Value::as_str) {
5555 Some("stdout") => {
5556 let chunk = javascript_sync_rpc_bytes_arg(
5557 &[event.get("data").cloned().unwrap_or(Value::Null)],
5558 0,
5559 "child_process.spawn_sync stdout",
5560 )?;
5561 stdout.extend_from_slice(&chunk);
5562 if stdout.len() > max_buffer && !kill_sent {
5563 max_buffer_exceeded = true;
5564 self.kill_javascript_child_process(
5565 vm_id,
5566 process_id,
5567 &child_process_id,
5568 "SIGTERM",
5569 )?;
5570 kill_sent = true;
5571 }
5572 }
5573 Some("stderr") => {
5574 let chunk = javascript_sync_rpc_bytes_arg(
5575 &[event.get("data").cloned().unwrap_or(Value::Null)],
5576 0,
5577 "child_process.spawn_sync stderr",
5578 )?;
5579 stderr.extend_from_slice(&chunk);
5580 if stderr.len() > max_buffer && !kill_sent {
5581 max_buffer_exceeded = true;
5582 self.kill_javascript_child_process(
5583 vm_id,
5584 process_id,
5585 &child_process_id,
5586 "SIGTERM",
5587 )?;
5588 kill_sent = true;
5589 }
5590 }
5591 Some("exit") => {
5592 break event
5593 .get("exitCode")
5594 .and_then(Value::as_i64)
5595 .map(|value| value as i32)
5596 .unwrap_or(1);
5597 }
5598 _ => {}
5599 }
5600 };
5601
5602 Ok(json!({
5603 "stdout": String::from_utf8_lossy(&stdout),
5604 "stderr": String::from_utf8_lossy(&stderr),
5605 "code": exit_code,
5606 "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
5607 "timedOut": timed_out,
5608 "maxBufferExceeded": max_buffer_exceeded,
5609 }))
5610 }
5611
5612 fn spawn_descendant_javascript_child_process(
5613 &mut self,
5614 vm_id: &str,
5615 process_id: &str,
5616 current_process_path: &[&str],
5617 request: JavascriptChildProcessSpawnRequest,
5618 ) -> Result<Value, SidecarError> {
5619 let current_process_label =
5620 Self::child_process_path_label(process_id, current_process_path);
5621 let (resolved, parent_kernel_pid) = {
5622 let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
5623 let root = vm
5624 .active_processes
5625 .get(process_id)
5626 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5627 let parent =
5628 Self::active_process_by_path(root, current_process_path).ok_or_else(|| {
5629 SidecarError::InvalidState(format!(
5630 "unknown child process path {current_process_label} during nested spawn"
5631 ))
5632 })?;
5633 (
5634 self.resolve_javascript_child_process_execution(
5635 vm,
5636 &parent.env,
5637 &parent.guest_cwd,
5638 &parent.host_cwd,
5639 &request,
5640 )?,
5641 parent.kernel_pid,
5642 )
5643 };
5644
5645 let sidecar_requests = self.sidecar_requests.clone();
5646 let vm = self
5647 .vms
5648 .get_mut(vm_id)
5649 .ok_or_else(|| missing_vm_error(vm_id))?;
5650 let child_process_id = {
5651 let root = vm
5652 .active_processes
5653 .get_mut(process_id)
5654 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5655 let parent =
5656 Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
5657 SidecarError::InvalidState(format!(
5658 "unknown child process path {current_process_label} during nested spawn"
5659 ))
5660 })?;
5661 parent.allocate_child_process_id()
5662 };
5663 let mut child_path = current_process_path.to_vec();
5664 child_path.push(child_process_id.as_str());
5665 let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
5666 .tool_command
5667 {
5668 let tool_resolution = resolve_tool_command(
5669 vm,
5670 &resolved.command,
5671 &resolved.execution_args,
5672 Some(&resolved.guest_cwd),
5673 )?
5674 .ok_or_else(|| {
5675 SidecarError::InvalidState(format!(
5676 "tool command no longer resolves: {}",
5677 resolved.command
5678 ))
5679 })?;
5680 let kernel_handle = vm
5681 .kernel
5682 .create_virtual_process(
5683 EXECUTION_DRIVER_NAME,
5684 TOOL_DRIVER_NAME,
5685 &resolved.command,
5686 resolved.process_args.clone(),
5687 VirtualProcessOptions {
5688 parent_pid: Some(parent_kernel_pid),
5689 env: resolved.env.clone(),
5690 cwd: Some(resolved.guest_cwd.clone()),
5691 },
5692 )
5693 .map_err(kernel_error)?;
5694 let kernel_pid = kernel_handle.pid();
5695 let tool_execution = ToolExecution::default();
5696 let cancelled = tool_execution.cancelled.clone();
5697 let pending_events = tool_execution.pending_events.clone();
5698 let events_overflowed = tool_execution.events_overflowed.clone();
5699 spawn_tool_process_events(ToolProcessEventRequest {
5700 sidecar_requests: sidecar_requests.clone(),
5701 connection_id: vm.connection_id.clone(),
5702 session_id: vm.session_id.clone(),
5703 vm_id: vm_id.to_owned(),
5704 tool_resolution,
5705 cancelled,
5706 pending_events,
5707 events_overflowed,
5708 });
5709 (
5710 kernel_pid,
5711 kernel_handle,
5712 ActiveExecution::Tool(tool_execution),
5713 None,
5714 )
5715 } else {
5716 let kernel_command = match resolved.runtime {
5717 GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
5718 GuestRuntimeKind::WebAssembly => WASM_COMMAND,
5719 GuestRuntimeKind::Python => {
5720 unreachable!("python child_process execution is rejected")
5721 }
5722 };
5723 let kernel_handle = vm
5724 .kernel
5725 .spawn_process(
5726 kernel_command,
5727 resolved.process_args.clone(),
5728 SpawnOptions {
5729 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
5730 parent_pid: Some(parent_kernel_pid),
5731 env: resolved.env.clone(),
5732 cwd: Some(resolved.guest_cwd.clone()),
5733 },
5734 )
5735 .map_err(kernel_error)?;
5736 let kernel_pid = kernel_handle.pid();
5737 if request.options.detached {
5738 vm.kernel
5739 .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
5740 .map_err(kernel_error)?;
5741 }
5742 let mut execution_env = resolved.env.clone();
5743 execution_env.insert(
5744 String::from(EXECUTION_SANDBOX_ROOT_ENV),
5745 normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
5746 );
5747 let execution = match resolved.runtime {
5748 GuestRuntimeKind::JavaScript => {
5749 execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
5750 &request.options.internal_bootstrap_env,
5751 ));
5752 execution_env.insert(
5753 String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
5754 String::from("1"),
5755 );
5756 let context =
5757 self.javascript_engine
5758 .create_context(CreateJavascriptContextRequest {
5759 vm_id: vm_id.to_owned(),
5760 bootstrap_module: None,
5761 compile_cache_root: Some(
5762 self.cache_root.join("node-compile-cache"),
5763 ),
5764 });
5765 let inline_code = load_javascript_entrypoint_source(
5766 vm,
5767 &resolved.host_cwd,
5768 &resolved.entrypoint,
5769 &execution_env,
5770 );
5771 prepare_javascript_shadow(vm, &resolved)?;
5772
5773 let module_reader = build_module_reader(vm, &resolved)
5774 .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
5775 let execution = self
5776 .javascript_engine
5777 .start_execution_with_module_reader(
5778 StartJavascriptExecutionRequest {
5779 guest_runtime: guest_runtime_identity(
5780 vm,
5781 Some(u64::from(kernel_pid)),
5782 Some(u64::from(parent_kernel_pid)),
5783 ),
5784 vm_id: vm_id.to_owned(),
5785 context_id: context.context_id,
5786 argv: std::iter::once(resolved.entrypoint.clone())
5787 .chain(resolved.execution_args.clone())
5788 .collect(),
5789 env: execution_env,
5790 cwd: resolved.host_cwd.clone(),
5791 limits: javascript_execution_limits(vm),
5792 inline_code,
5793 },
5794 module_reader,
5795 )
5796 .map_err(javascript_error)?;
5797 ActiveExecution::Javascript(execution)
5798 }
5799 GuestRuntimeKind::WebAssembly => {
5800 execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
5801 let wasm_limits = wasm_execution_limits(vm);
5802 let wasm_guest_runtime = guest_runtime_identity(
5803 vm,
5804 Some(u64::from(kernel_pid)),
5805 Some(u64::from(parent_kernel_pid)),
5806 );
5807 let context = self.wasm_engine.create_context(CreateWasmContextRequest {
5808 vm_id: vm_id.to_owned(),
5809 module_path: Some(resolved.entrypoint.clone()),
5810 });
5811 let execution = self
5812 .wasm_engine
5813 .start_execution(StartWasmExecutionRequest {
5814 vm_id: vm_id.to_owned(),
5815 context_id: context.context_id,
5816 argv: resolved.process_args.clone(),
5817 env: execution_env,
5818 cwd: resolved.host_cwd.clone(),
5819 permission_tier: execution_wasm_permission_tier(
5820 resolved
5821 .wasm_permission_tier
5822 .unwrap_or(WasmPermissionTier::Full),
5823 ),
5824 limits: wasm_limits,
5825 guest_runtime: wasm_guest_runtime,
5826 })
5827 .map_err(wasm_error)?;
5828 ActiveExecution::Wasm(Box::new(execution))
5829 }
5830 GuestRuntimeKind::Python => {
5831 unreachable!("python child_process execution is rejected")
5832 }
5833 };
5834 let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
5835 "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5836 "ignore" => {
5837 vm.kernel
5838 .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
5839 .map_err(kernel_error)?;
5840 None
5841 }
5842 "inherit" => None,
5843 _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5844 };
5845 (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
5846 };
5847
5848 let root = vm
5849 .active_processes
5850 .get_mut(process_id)
5851 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5852 let parent =
5853 Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
5854 SidecarError::InvalidState(format!(
5855 "unknown child process path {current_process_label} during nested spawn"
5856 ))
5857 })?;
5858 parent.child_processes.insert(
5859 child_process_id.clone(),
5860 ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
5861 .with_detached(request.options.detached)
5862 .with_guest_cwd(resolved.guest_cwd.clone())
5863 .with_env(resolved.env.clone())
5864 .with_host_cwd(resolved.host_cwd.clone()),
5865 );
5866 if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
5867 parent
5868 .child_processes
5869 .get_mut(&child_process_id)
5870 .ok_or_else(|| {
5871 SidecarError::InvalidState(format!(
5872 "child process {child_process_id} disappeared during nested spawn"
5873 ))
5874 })?
5875 .kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
5876 }
5877 Ok(json!({
5878 "childId": child_process_id,
5879 "pid": kernel_pid,
5880 "command": resolved.command,
5881 "args": resolved.process_args,
5882 }))
5883 }
5884
5885 fn spawn_descendant_javascript_child_process_sync(
5886 &mut self,
5887 vm_id: &str,
5888 process_id: &str,
5889 current_process_path: &[&str],
5890 request: JavascriptChildProcessSpawnRequest,
5891 max_buffer: Option<usize>,
5892 ) -> Result<Value, SidecarError> {
5893 let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
5894 let timeout_deadline = request
5895 .options
5896 .timeout
5897 .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
5898 let timeout_signal = request
5899 .options
5900 .kill_signal
5901 .clone()
5902 .unwrap_or_else(|| String::from("SIGTERM"));
5903 let spawned = self.spawn_descendant_javascript_child_process(
5904 vm_id,
5905 process_id,
5906 current_process_path,
5907 request,
5908 )?;
5909 let child_process_id = spawned
5910 .get("childId")
5911 .and_then(Value::as_str)
5912 .ok_or_else(|| {
5913 SidecarError::InvalidState(String::from(
5914 "child_process.spawn_sync response is missing childId",
5915 ))
5916 })?
5917 .to_owned();
5918
5919 if let Some(input) = sync_input.as_deref() {
5920 self.write_descendant_javascript_child_process_stdin(
5921 vm_id,
5922 process_id,
5923 current_process_path,
5924 &child_process_id,
5925 input,
5926 )?;
5927 }
5928 self.close_descendant_javascript_child_process_stdin(
5929 vm_id,
5930 process_id,
5931 current_process_path,
5932 &child_process_id,
5933 )?;
5934
5935 let max_buffer = max_buffer.unwrap_or(1024 * 1024);
5936 let mut stdout = Vec::new();
5937 let mut stderr = Vec::new();
5938 let mut max_buffer_exceeded = false;
5939 let mut kill_sent = false;
5940 let mut timed_out = false;
5941
5942 let exit_code = loop {
5943 let wait_ms = if let Some(deadline) = timeout_deadline {
5944 let now = Instant::now();
5945 if now >= deadline {
5946 if !kill_sent {
5947 timed_out = true;
5948 self.kill_descendant_javascript_child_process(
5949 vm_id,
5950 process_id,
5951 current_process_path,
5952 &child_process_id,
5953 &timeout_signal,
5954 )?;
5955 kill_sent = true;
5956 }
5957 0
5958 } else {
5959 u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
5960 .unwrap_or(50)
5961 }
5962 } else {
5963 50
5964 };
5965 let event = self.poll_descendant_javascript_child_process(
5966 vm_id,
5967 process_id,
5968 current_process_path,
5969 &child_process_id,
5970 wait_ms,
5971 )?;
5972 if event.is_null() {
5973 continue;
5974 }
5975
5976 match event.get("type").and_then(Value::as_str) {
5977 Some("stdout") => {
5978 let chunk = javascript_sync_rpc_bytes_arg(
5979 &[event.get("data").cloned().unwrap_or(Value::Null)],
5980 0,
5981 "child_process.spawn_sync stdout",
5982 )?;
5983 stdout.extend_from_slice(&chunk);
5984 if stdout.len() > max_buffer && !kill_sent {
5985 max_buffer_exceeded = true;
5986 self.kill_descendant_javascript_child_process(
5987 vm_id,
5988 process_id,
5989 current_process_path,
5990 &child_process_id,
5991 "SIGTERM",
5992 )?;
5993 kill_sent = true;
5994 }
5995 }
5996 Some("stderr") => {
5997 let chunk = javascript_sync_rpc_bytes_arg(
5998 &[event.get("data").cloned().unwrap_or(Value::Null)],
5999 0,
6000 "child_process.spawn_sync stderr",
6001 )?;
6002 stderr.extend_from_slice(&chunk);
6003 if stderr.len() > max_buffer && !kill_sent {
6004 max_buffer_exceeded = true;
6005 self.kill_descendant_javascript_child_process(
6006 vm_id,
6007 process_id,
6008 current_process_path,
6009 &child_process_id,
6010 "SIGTERM",
6011 )?;
6012 kill_sent = true;
6013 }
6014 }
6015 Some("exit") => {
6016 break event
6017 .get("exitCode")
6018 .and_then(Value::as_i64)
6019 .map(|value| value as i32)
6020 .unwrap_or(1);
6021 }
6022 _ => {}
6023 }
6024 };
6025
6026 Ok(json!({
6027 "stdout": String::from_utf8_lossy(&stdout),
6028 "stderr": String::from_utf8_lossy(&stderr),
6029 "code": exit_code,
6030 "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
6031 "timedOut": timed_out,
6032 "maxBufferExceeded": max_buffer_exceeded,
6033 }))
6034 }
6035
6036 fn handle_descendant_javascript_child_process_rpc(
6037 &mut self,
6038 vm_id: &str,
6039 process_id: &str,
6040 current_process_path: &[&str],
6041 request: &JavascriptSyncRpcRequest,
6042 ) -> Result<Value, SidecarError> {
6043 match request.method.as_str() {
6044 "child_process.spawn" => {
6045 let Some(vm) = self.vms.get(vm_id) else {
6046 return Ok(Value::Null);
6047 };
6048 let (payload, _) = parse_javascript_child_process_spawn_request(vm, &request.args)?;
6049 self.spawn_descendant_javascript_child_process(
6050 vm_id,
6051 process_id,
6052 current_process_path,
6053 payload,
6054 )
6055 }
6056 "child_process.spawn_sync" => {
6057 let Some(vm) = self.vms.get(vm_id) else {
6058 return Ok(Value::Null);
6059 };
6060 let (payload, max_buffer) =
6061 parse_javascript_child_process_spawn_request(vm, &request.args)?;
6062 self.spawn_descendant_javascript_child_process_sync(
6063 vm_id,
6064 process_id,
6065 current_process_path,
6066 payload,
6067 max_buffer,
6068 )
6069 }
6070 "child_process.poll" => {
6071 let child_process_id =
6072 javascript_sync_rpc_arg_str(&request.args, 0, "child_process.poll child id")?;
6073 let wait_ms = javascript_sync_rpc_arg_u64_optional(
6074 &request.args,
6075 1,
6076 "child_process.poll wait ms",
6077 )?
6078 .unwrap_or_default();
6079 self.poll_descendant_javascript_child_process(
6080 vm_id,
6081 process_id,
6082 current_process_path,
6083 child_process_id,
6084 wait_ms,
6085 )
6086 }
6087 "child_process.write_stdin" => {
6088 let child_process_id = javascript_sync_rpc_arg_str(
6089 &request.args,
6090 0,
6091 "child_process.write_stdin child id",
6092 )?;
6093 let chunk = javascript_sync_rpc_bytes_arg(
6094 &request.args,
6095 1,
6096 "child_process.write_stdin chunk",
6097 )?;
6098 self.write_descendant_javascript_child_process_stdin(
6099 vm_id,
6100 process_id,
6101 current_process_path,
6102 child_process_id,
6103 &chunk,
6104 )?;
6105 Ok(Value::Null)
6106 }
6107 "child_process.close_stdin" => {
6108 let child_process_id = javascript_sync_rpc_arg_str(
6109 &request.args,
6110 0,
6111 "child_process.close_stdin child id",
6112 )?;
6113 self.close_descendant_javascript_child_process_stdin(
6114 vm_id,
6115 process_id,
6116 current_process_path,
6117 child_process_id,
6118 )?;
6119 Ok(Value::Null)
6120 }
6121 "child_process.kill" => {
6122 let child_process_id =
6123 javascript_sync_rpc_arg_str(&request.args, 0, "child_process.kill child id")?;
6124 let signal =
6125 javascript_sync_rpc_arg_str(&request.args, 1, "child_process.kill signal")?;
6126 self.kill_descendant_javascript_child_process(
6127 vm_id,
6128 process_id,
6129 current_process_path,
6130 child_process_id,
6131 signal,
6132 )?;
6133 Ok(Value::Null)
6134 }
6135 _ => Err(SidecarError::InvalidState(format!(
6136 "unsupported nested child process RPC method {}",
6137 request.method
6138 ))),
6139 }
6140 }
6141
6142 fn poll_descendant_javascript_child_process(
6143 &mut self,
6144 vm_id: &str,
6145 process_id: &str,
6146 current_process_path: &[&str],
6147 child_process_id: &str,
6148 wait_ms: u64,
6149 ) -> Result<Value, SidecarError> {
6150 let mut child_path = current_process_path.to_vec();
6151 child_path.push(child_process_id);
6152 let child_gone_error = || javascript_child_process_gone_error(process_id, &child_path);
6153 let deadline = Instant::now() + Duration::from_millis(wait_ms);
6154 let mut polled_once = false;
6155
6156 loop {
6157 self.drain_queued_descendant_javascript_child_process_events(
6158 vm_id,
6159 process_id,
6160 &child_path,
6161 )?;
6162 enum ChildPollResult {
6163 Event(Box<Option<ActiveExecutionEvent>>),
6164 RecoverRuntimeExit,
6165 Timeout,
6166 }
6167 let wait = if wait_ms == 0 {
6168 Duration::ZERO
6169 } else {
6170 deadline.saturating_duration_since(Instant::now())
6171 };
6172 let poll_result = {
6173 let Some(vm) = self.vms.get_mut(vm_id) else {
6174 return Ok(Value::Null);
6175 };
6176 let Some(parent) =
6177 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6178 else {
6179 return Err(child_gone_error());
6180 };
6181 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6182 return Err(child_gone_error());
6183 };
6184 if let Some(event) = child.pending_execution_events.pop_front() {
6185 ChildPollResult::Event(Box::new(Some(event)))
6186 } else if polled_once && wait.is_zero() {
6187 ChildPollResult::Timeout
6188 } else {
6189 polled_once = true;
6190 match child.execution.poll_event_blocking(wait) {
6191 Ok(Some(event)) => ChildPollResult::Event(Box::new(Some(event))),
6192 Ok(None) => ChildPollResult::RecoverRuntimeExit,
6193 Err(SidecarError::Execution(message))
6194 if (child.runtime == GuestRuntimeKind::JavaScript
6195 && closed_javascript_event_channel(&message))
6196 || (child.runtime == GuestRuntimeKind::Python
6197 && closed_python_event_channel(&message))
6198 || (child.runtime == GuestRuntimeKind::WebAssembly
6199 && closed_wasm_event_channel(&message)) =>
6200 {
6201 ChildPollResult::RecoverRuntimeExit
6202 }
6203 Err(error) => return Err(error),
6204 }
6205 }
6206 };
6207 let event = match poll_result {
6208 ChildPollResult::Event(event) => *event,
6209 ChildPollResult::Timeout => return Ok(Value::Null),
6210 ChildPollResult::RecoverRuntimeExit => self
6211 .recover_descendant_runtime_child_process_event(
6212 vm_id,
6213 process_id,
6214 current_process_path,
6215 child_process_id,
6216 wait.as_millis().try_into().unwrap_or(u64::MAX),
6217 )?,
6218 };
6219
6220 let Some(event) = event else {
6221 return Ok(Value::Null);
6222 };
6223
6224 match event {
6225 ActiveExecutionEvent::Stdout(chunk) => {
6226 return Ok(json!({
6227 "type": "stdout",
6228 "data": javascript_sync_rpc_bytes_value(&chunk),
6229 }));
6230 }
6231 ActiveExecutionEvent::Stderr(chunk) => {
6232 return Ok(json!({
6233 "type": "stderr",
6234 "data": javascript_sync_rpc_bytes_value(&chunk),
6235 }));
6236 }
6237 ActiveExecutionEvent::Exited(exit_code) => {
6238 let had_trailing_events = {
6239 let Some(vm) = self.vms.get_mut(vm_id) else {
6240 return Ok(Value::Null);
6241 };
6242 let Some(parent) = Self::descendant_parent_process_mut(
6243 vm,
6244 process_id,
6245 current_process_path,
6246 ) else {
6247 return Ok(Value::Null);
6248 };
6249 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6250 return Ok(Value::Null);
6251 };
6252 let deadline = Instant::now() + Duration::from_millis(150);
6253 loop {
6254 let wait = deadline.saturating_duration_since(Instant::now());
6255 let next = poll_child_execution_after_exit(child, wait)?;
6256 let Some(next) = next else {
6257 break;
6258 };
6259 if matches!(next, ActiveExecutionEvent::Exited(_)) {
6260 continue;
6261 }
6262 child.queue_pending_execution_event(next)?;
6263 if Instant::now() >= deadline {
6264 break;
6265 }
6266 }
6267 if !child.pending_execution_events.is_empty() {
6268 child.queue_pending_execution_event(ActiveExecutionEvent::Exited(
6269 exit_code,
6270 ))?;
6271 true
6272 } else {
6273 false
6274 }
6275 };
6276 if had_trailing_events {
6277 continue;
6278 }
6279
6280 let parent_signal_key =
6281 Self::child_process_signal_key(process_id, current_process_path);
6282 let Some(vm) = self.vms.get_mut(vm_id) else {
6283 return Ok(Value::Null);
6284 };
6285 let signal_name = {
6286 let Some(parent) = Self::descendant_parent_process_mut(
6287 vm,
6288 process_id,
6289 current_process_path,
6290 ) else {
6291 return Ok(Value::Null);
6292 };
6293 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6294 return Ok(Value::Null);
6295 };
6296 child.pending_self_signal_exit.take().and_then(|signal| {
6297 if exit_code == 128 + signal {
6298 canonical_signal_name(signal).map(str::to_owned)
6299 } else {
6300 None
6301 }
6302 })
6303 };
6304 let (parent_runtime_pid, parent_v8_signal_session, should_signal_parent) = {
6305 let Some(parent) =
6306 Self::descendant_parent_process(vm, process_id, current_process_path)
6307 else {
6308 return Ok(Value::Null);
6309 };
6310 (
6311 parent.execution.child_pid(),
6312 parent.execution.javascript_v8_session_handle().filter(|_| {
6313 matches!(
6314 &parent.execution,
6315 ActiveExecution::Javascript(execution)
6316 if execution.uses_shared_v8_runtime()
6317 )
6318 }),
6319 vm.signal_states
6320 .get(parent_signal_key)
6321 .and_then(|handlers| handlers.get(&(libc::SIGCHLD as u32)))
6322 .is_some_and(|registration| {
6323 registration.action != SignalDispositionAction::Default
6324 }),
6325 )
6326 };
6327 let Some(parent) =
6328 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6329 else {
6330 return Ok(Value::Null);
6331 };
6332 let Some(mut child) = parent.child_processes.remove(child_process_id) else {
6333 return Ok(Value::Null);
6334 };
6335 let child_process_label =
6336 Self::child_process_path_label(process_id, &child_path);
6337 let detached_children =
6338 Self::adopt_detached_child_processes(&child_process_label, &mut child);
6339 sync_process_host_writes_to_kernel(vm, &child)?;
6340 terminate_child_process_tree(&mut vm.kernel, &mut child);
6341 child.kernel_handle.finish(exit_code);
6342 let _ = vm.kernel.wait_and_reap(child.kernel_pid);
6343 vm.signal_states.remove(child_process_id);
6344 for (detached_process_id, detached_child) in detached_children {
6345 vm.detached_child_processes
6346 .insert(detached_process_id.clone());
6347 vm.active_processes
6348 .insert(detached_process_id, detached_child);
6349 }
6350 if should_signal_parent {
6351 if let Some(session) = parent_v8_signal_session {
6352 dispatch_v8_session_signal_async(session, libc::SIGCHLD);
6353 } else {
6354 signal_runtime_process(parent_runtime_pid, libc::SIGCHLD)?;
6355 }
6356 }
6357 let mut payload = Map::new();
6358 payload.insert(String::from("type"), Value::String(String::from("exit")));
6359 payload.insert(String::from("exitCode"), Value::from(exit_code));
6360 if let Some(signal_name) = signal_name {
6361 payload.insert(String::from("signal"), Value::String(signal_name));
6362 }
6363 return Ok(Value::Object(payload));
6364 }
6365 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
6366 let mut current_child_path = current_process_path.to_vec();
6367 current_child_path.push(child_process_id);
6368 let response = if request.method == "process.signal_state" {
6369 let (signal, registration) =
6370 parse_process_signal_state_request(&request.args)?;
6371 let Some(vm) = self.vms.get_mut(vm_id) else {
6372 return Ok(Value::Null);
6373 };
6374 let signal_key =
6375 Self::child_process_signal_key(process_id, ¤t_child_path)
6376 .to_owned();
6377 apply_process_signal_state_update(
6378 &mut vm.signal_states,
6379 &signal_key,
6380 signal,
6381 registration,
6382 );
6383 Ok(Value::Null)
6384 } else if request.method == "process.kill" {
6385 self.handle_descendant_process_kill_rpc(
6386 vm_id,
6387 process_id,
6388 current_process_path,
6389 child_process_id,
6390 &request,
6391 )
6392 } else if request.method.starts_with("child_process.") {
6393 self.handle_descendant_javascript_child_process_rpc(
6394 vm_id,
6395 process_id,
6396 ¤t_child_path,
6397 &request,
6398 )
6399 } else {
6400 let Some(vm) = self.vms.get_mut(vm_id) else {
6401 return Ok(Value::Null);
6402 };
6403 let resource_limits = vm.kernel.resource_limits().clone();
6404 let network_counts = vm_network_resource_counts(vm);
6405 let socket_paths = build_javascript_socket_path_context(vm)?;
6406 let Some(root) = vm.active_processes.get_mut(process_id) else {
6407 return Ok(Value::Null);
6408 };
6409 let Some(parent) =
6410 Self::active_process_by_path_mut(root, current_process_path)
6411 else {
6412 return Ok(Value::Null);
6413 };
6414 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6415 return Ok(Value::Null);
6416 };
6417 service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
6418 bridge: &self.bridge,
6419 vm_id,
6420 dns: &vm.dns,
6421 socket_paths: &socket_paths,
6422 kernel: &mut vm.kernel,
6423 process: child,
6424 sync_request: &request,
6425 resource_limits: &resource_limits,
6426 network_counts,
6427 })
6428 };
6429
6430 let Some(vm) = self.vms.get_mut(vm_id) else {
6431 return Ok(Value::Null);
6432 };
6433 let Some(parent) =
6434 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6435 else {
6436 return Ok(Value::Null);
6437 };
6438 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6439 return Ok(Value::Null);
6440 };
6441 let parent_signal_event = response.as_ref().ok().and_then(|result| {
6442 let target_path_label =
6443 Self::child_process_path_label(process_id, current_process_path);
6444 if request.method != "process.kill"
6445 || result.get("action").and_then(Value::as_str) != Some("user")
6446 || result.get("targetProcessPath").and_then(Value::as_str)
6447 != Some(target_path_label.as_str())
6448 {
6449 return None;
6450 }
6451 Some(json!({
6452 "type": "signal",
6453 "signal": result.get("signal").and_then(Value::as_str).unwrap_or_default(),
6454 "number": result.get("number").and_then(Value::as_i64).unwrap_or_default(),
6455 }))
6456 });
6457 match response {
6458 Ok(result) => child
6459 .execution
6460 .respond_javascript_sync_rpc_success(request.id, result)
6461 .or_else(ignore_stale_javascript_sync_rpc_response)?,
6462 Err(error) => child
6463 .execution
6464 .respond_javascript_sync_rpc_error(
6465 request.id,
6466 javascript_sync_rpc_error_code(&error),
6467 error.to_string(),
6468 )
6469 .or_else(ignore_stale_javascript_sync_rpc_response)?,
6470 }
6471 if let Some(event) = parent_signal_event {
6472 return Ok(event);
6473 }
6474 }
6475 ActiveExecutionEvent::PythonVfsRpcRequest(_) => {
6476 return Err(SidecarError::InvalidState(String::from(
6477 "nested Python child_process execution is not supported yet",
6478 )));
6479 }
6480 ActiveExecutionEvent::SignalState {
6481 signal,
6482 registration,
6483 } => {
6484 let Some(vm) = self.vms.get_mut(vm_id) else {
6485 return Ok(Value::Null);
6486 };
6487 let signal_key =
6488 Self::child_process_signal_key(process_id, &child_path).to_owned();
6489 apply_process_signal_state_update(
6490 &mut vm.signal_states,
6491 &signal_key,
6492 signal,
6493 registration.clone(),
6494 );
6495 return Ok(json!({
6496 "type": "signal_state",
6497 "signal": signal,
6498 "registration": registration,
6499 }));
6500 }
6501 }
6502 }
6503 }
6504
6505 fn recover_descendant_runtime_child_process_event(
6506 &mut self,
6507 vm_id: &str,
6508 process_id: &str,
6509 current_process_path: &[&str],
6510 child_process_id: &str,
6511 wait_ms: u64,
6512 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
6513 let (
6514 parent_kernel_pid,
6515 child_kernel_pid,
6516 child_runtime_pid,
6517 child_runtime,
6518 child_shared_runtime,
6519 ) = {
6520 let mut child_path = current_process_path.to_vec();
6521 child_path.push(child_process_id);
6522 let Some(vm) = self.vms.get_mut(vm_id) else {
6523 return Ok(None);
6524 };
6525 let Some(parent) =
6526 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6527 else {
6528 return Err(javascript_child_process_gone_error(process_id, &child_path));
6529 };
6530 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6531 return Err(javascript_child_process_gone_error(process_id, &child_path));
6532 };
6533 (
6534 parent.kernel_pid,
6535 child.kernel_pid,
6536 child.execution.child_pid(),
6537 child.runtime.clone(),
6538 child.execution.uses_shared_v8_runtime(),
6539 )
6540 };
6541 if child_runtime != GuestRuntimeKind::JavaScript
6542 && child_runtime != GuestRuntimeKind::Python
6543 && child_runtime != GuestRuntimeKind::WebAssembly
6544 {
6545 return Ok(None);
6546 }
6547 let wait_deadline = Instant::now() + Duration::from_millis(wait_ms.min(25));
6548 loop {
6549 let Some(vm) = self.vms.get_mut(vm_id) else {
6550 return Ok(None);
6551 };
6552 if let Some(process_info) = vm.kernel.list_processes().get(&child_kernel_pid) {
6553 if process_info.status == ProcessStatus::Exited {
6554 return Ok(Some(ActiveExecutionEvent::Exited(
6555 process_info.exit_code.unwrap_or(0),
6556 )));
6557 }
6558 }
6559 if let Some(wait_result) = vm
6560 .kernel
6561 .waitpid_with_options(
6562 EXECUTION_DRIVER_NAME,
6563 parent_kernel_pid,
6564 child_kernel_pid as i32,
6565 WaitPidFlags::WNOHANG,
6566 )
6567 .map_err(kernel_error)?
6568 {
6569 return Ok(Some(ActiveExecutionEvent::Exited(wait_result.status)));
6570 }
6571
6572 if !child_shared_runtime && child_runtime_pid != 0 {
6573 if let Some(status) = runtime_child_exit_status(child_runtime_pid)? {
6574 return Ok(Some(ActiveExecutionEvent::Exited(status)));
6575 }
6576 if !runtime_child_is_alive(child_runtime_pid)? {
6577 return Ok(Some(ActiveExecutionEvent::Exited(0)));
6578 }
6579 }
6580 if Instant::now() >= wait_deadline {
6581 return Ok(None);
6582 }
6583 std::thread::sleep(Duration::from_millis(5));
6584 }
6585 }
6586
6587 fn write_descendant_javascript_child_process_stdin(
6588 &mut self,
6589 vm_id: &str,
6590 process_id: &str,
6591 current_process_path: &[&str],
6592 child_process_id: &str,
6593 chunk: &[u8],
6594 ) -> Result<(), SidecarError> {
6595 let mut child_path = current_process_path.to_vec();
6596 child_path.push(child_process_id);
6597 let Some(vm) = self.vms.get_mut(vm_id) else {
6598 return Err(javascript_child_process_gone_error(process_id, &child_path));
6599 };
6600 let Some(root) = vm.active_processes.get_mut(process_id) else {
6601 return Err(javascript_child_process_gone_error(process_id, &child_path));
6602 };
6603 let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6604 return Err(javascript_child_process_gone_error(process_id, &child_path));
6605 };
6606 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6607 return Err(javascript_child_process_gone_error(process_id, &child_path));
6608 };
6609 if let Err(error) = child.execution.write_stdin(chunk) {
6610 if is_broken_pipe_error(&error) {
6611 return Ok(());
6612 }
6613 return Err(error);
6614 }
6615 write_kernel_process_stdin(&mut vm.kernel, child, chunk)
6616 }
6617
6618 fn close_descendant_javascript_child_process_stdin(
6619 &mut self,
6620 vm_id: &str,
6621 process_id: &str,
6622 current_process_path: &[&str],
6623 child_process_id: &str,
6624 ) -> Result<(), SidecarError> {
6625 let mut child_path = current_process_path.to_vec();
6626 child_path.push(child_process_id);
6627 let Some(vm) = self.vms.get_mut(vm_id) else {
6628 return Err(javascript_child_process_gone_error(process_id, &child_path));
6629 };
6630 let Some(root) = vm.active_processes.get_mut(process_id) else {
6631 return Err(javascript_child_process_gone_error(process_id, &child_path));
6632 };
6633 let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6634 return Err(javascript_child_process_gone_error(process_id, &child_path));
6635 };
6636 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6637 return Err(javascript_child_process_gone_error(process_id, &child_path));
6638 };
6639 child.execution.close_stdin()?;
6640 close_kernel_process_stdin(&mut vm.kernel, child)
6641 }
6642
6643 fn kill_descendant_javascript_child_process(
6644 &mut self,
6645 vm_id: &str,
6646 process_id: &str,
6647 current_process_path: &[&str],
6648 child_process_id: &str,
6649 signal: &str,
6650 ) -> Result<(), SidecarError> {
6651 let signal_name = signal.to_owned();
6652 let signal = parse_signal(signal)?;
6653 let Some(vm) = self.vms.get_mut(vm_id) else {
6654 return Ok(());
6655 };
6656 let Some(root) = vm.active_processes.get_mut(process_id) else {
6657 return Ok(());
6658 };
6659 let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6660 return Ok(());
6661 };
6662 let source_pid = parent.kernel_pid;
6663 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6664 return Ok(());
6665 };
6666 terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
6667 let child_process_label = if current_process_path.is_empty() {
6668 child_process_id.to_owned()
6669 } else {
6670 format!("{}/{}", current_process_path.join("/"), child_process_id)
6671 };
6672 emit_security_audit_event(
6673 &self.bridge,
6674 vm_id,
6675 "security.process.kill",
6676 audit_fields([
6677 (String::from("source"), String::from("guest_child_process")),
6678 (String::from("source_pid"), source_pid.to_string()),
6679 (String::from("target_pid"), child.kernel_pid.to_string()),
6680 (String::from("process_id"), process_id.to_owned()),
6681 (String::from("child_process_id"), child_process_label),
6682 (String::from("signal"), signal_name),
6683 ]),
6684 );
6685 Ok(())
6686 }
6687
6688 fn handle_descendant_process_kill_rpc(
6689 &mut self,
6690 vm_id: &str,
6691 process_id: &str,
6692 current_process_path: &[&str],
6693 child_process_id: &str,
6694 request: &JavascriptSyncRpcRequest,
6695 ) -> Result<Value, SidecarError> {
6696 let target_pid = javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
6697 let signal_name = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
6698 let signal = parse_signal(signal_name)?;
6699
6700 let mut source_path = current_process_path.to_vec();
6701 source_path.push(child_process_id);
6702
6703 if signal != 0 && target_pid < 0 {
6704 let pgid = target_pid.unsigned_abs();
6705 let caller_kernel_pid = {
6706 let Some(vm) = self.vms.get(vm_id) else {
6707 return Err(SidecarError::InvalidState(String::from(
6708 "ESRCH: unknown VM during process.kill",
6709 )));
6710 };
6711 let Some(root) = vm.active_processes.get(process_id) else {
6712 return Err(SidecarError::InvalidState(format!(
6713 "ESRCH: unknown process {process_id} during process.kill",
6714 )));
6715 };
6716 let Some(source) = Self::active_process_by_path(root, &source_path) else {
6717 return Err(SidecarError::InvalidState(format!(
6718 "ESRCH: unknown child process {child_process_id} during process.kill",
6719 )));
6720 };
6721 source.kernel_pid
6722 };
6723 let caller_is_member =
6724 self.signal_vm_process_group(vm_id, caller_kernel_pid, pgid, signal_name)?;
6725 if !caller_is_member {
6726 return Ok(Value::Null);
6727 }
6728 let Some(vm) = self.vms.get_mut(vm_id) else {
6729 return Ok(Value::Null);
6730 };
6731 let Some(root) = vm.active_processes.get_mut(process_id) else {
6732 return Ok(Value::Null);
6733 };
6734 let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
6735 return Ok(Value::Null);
6736 };
6737 source.pending_self_signal_exit = None;
6738 if !matches!(
6739 canonical_signal_name(signal),
6740 Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
6741 ) {
6742 source.pending_self_signal_exit = Some(signal);
6743 }
6744 return Ok(json!({
6745 "self": true,
6746 "action": "default",
6747 }));
6748 }
6749
6750 let Some(vm) = self.vms.get_mut(vm_id) else {
6751 return Err(SidecarError::InvalidState(String::from(
6752 "ESRCH: unknown VM during process.kill",
6753 )));
6754 };
6755
6756 if signal == 0 {
6757 vm.kernel
6758 .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
6759 .map_err(kernel_error)?;
6760 return Ok(Value::Null);
6761 }
6762
6763 let target_kernel_pid = u32::try_from(target_pid).map_err(|_| {
6764 SidecarError::InvalidState(format!("EINVAL: invalid process pid {target_pid}"))
6765 })?;
6766 let (source_pid, located_target_path) = {
6767 let Some(root) = vm.active_processes.get(process_id) else {
6768 return Err(SidecarError::InvalidState(format!(
6769 "ESRCH: unknown process {process_id} during process.kill",
6770 )));
6771 };
6772 let Some(source) = Self::active_process_by_path(root, &source_path) else {
6773 return Err(SidecarError::InvalidState(format!(
6774 "ESRCH: unknown child process {child_process_id} during process.kill",
6775 )));
6776 };
6777 vm.kernel
6778 .signal_process(EXECUTION_DRIVER_NAME, target_pid, 0)
6779 .map_err(kernel_error)?;
6780 (
6781 source.kernel_pid,
6782 Self::active_process_path_by_kernel_pid(root, target_kernel_pid),
6783 )
6784 };
6785 let Some(target_path) = located_target_path else {
6786 self.signal_vm_kernel_pid(vm_id, target_kernel_pid, signal_name)?;
6790 return Ok(Value::Null);
6791 };
6792 let Some(vm) = self.vms.get_mut(vm_id) else {
6793 return Err(SidecarError::InvalidState(String::from(
6794 "ESRCH: unknown VM during process.kill",
6795 )));
6796 };
6797
6798 if source_pid == target_kernel_pid {
6799 let Some(root) = vm.active_processes.get_mut(process_id) else {
6800 return Ok(Value::Null);
6801 };
6802 let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
6803 return Ok(Value::Null);
6804 };
6805 source.pending_self_signal_exit = None;
6806 if !matches!(
6807 canonical_signal_name(signal),
6808 Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
6809 ) {
6810 source.pending_self_signal_exit = Some(signal);
6811 }
6812 return Ok(json!({
6813 "self": true,
6814 "action": "default",
6815 }));
6816 }
6817
6818 let signal_key = target_path.last().map(String::as_str).unwrap_or(process_id);
6819 let registration = vm
6820 .signal_states
6821 .get(signal_key)
6822 .and_then(|handlers| handlers.get(&(signal as u32)))
6823 .cloned();
6824
6825 let action = match registration
6826 .as_ref()
6827 .map(|registration| ®istration.action)
6828 {
6829 Some(SignalDispositionAction::Ignore) => "ignore",
6830 Some(SignalDispositionAction::User) => {
6831 let Some(root) = vm.active_processes.get_mut(process_id) else {
6832 return Ok(Value::Null);
6833 };
6834 let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
6835 else {
6836 return Err(SidecarError::InvalidState(format!(
6837 "ESRCH: unknown process pid {target_pid}"
6838 )));
6839 };
6840 if let Some(session) = target.execution.javascript_v8_session_handle().filter(
6841 |_| matches!(&target.execution, ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime())
6842 || matches!(&target.execution, ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime()),
6843 ) {
6844 dispatch_v8_session_signal_async(session, signal);
6845 } else if !dispatch_v8_process_signal(target, signal)? {
6846 return Err(SidecarError::InvalidState(format!(
6847 "unsupported guest signal delivery for pid {target_pid}"
6848 )));
6849 }
6850 "user"
6851 }
6852 Some(SignalDispositionAction::Default) | None
6853 if matches!(
6854 canonical_signal_name(signal),
6855 Some("SIGWINCH" | "SIGCHLD" | "SIGURG")
6856 ) =>
6857 {
6858 "ignore"
6859 }
6860 Some(SignalDispositionAction::Default) | None => {
6861 let Some(root) = vm.active_processes.get_mut(process_id) else {
6862 return Ok(Value::Null);
6863 };
6864 let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
6865 else {
6866 return Err(SidecarError::InvalidState(format!(
6867 "ESRCH: unknown process pid {target_pid}"
6868 )));
6869 };
6870 apply_active_process_default_signal(&mut vm.kernel, target, signal)?;
6871 "default"
6872 }
6873 };
6874
6875 let target_path_label = Self::child_process_path_label(
6876 process_id,
6877 &target_path.iter().map(String::as_str).collect::<Vec<_>>(),
6878 );
6879 emit_security_audit_event(
6880 &self.bridge,
6881 vm_id,
6882 "security.process.kill",
6883 audit_fields([
6884 (String::from("source"), String::from("guest_process")),
6885 (String::from("source_pid"), source_pid.to_string()),
6886 (String::from("target_pid"), target_pid.to_string()),
6887 (String::from("process_id"), process_id.to_owned()),
6888 (
6889 String::from("target_process_path"),
6890 target_path_label.clone(),
6891 ),
6892 (String::from("signal"), signal_name.to_owned()),
6893 ]),
6894 );
6895
6896 Ok(json!({
6897 "self": false,
6898 "action": action,
6899 "signal": signal_name,
6900 "number": signal,
6901 "targetProcessPath": target_path_label,
6902 }))
6903 }
6904
6905 pub(crate) fn poll_javascript_child_process(
6906 &mut self,
6907 vm_id: &str,
6908 process_id: &str,
6909 child_process_id: &str,
6910 wait_ms: u64,
6911 ) -> Result<Value, SidecarError> {
6912 self.poll_descendant_javascript_child_process(
6913 vm_id,
6914 process_id,
6915 &[],
6916 child_process_id,
6917 wait_ms,
6918 )
6919 }
6920
6921 pub(crate) fn write_javascript_child_process_stdin(
6922 &mut self,
6923 vm_id: &str,
6924 process_id: &str,
6925 child_process_id: &str,
6926 chunk: &[u8],
6927 ) -> Result<(), SidecarError> {
6928 let Some(vm) = self.vms.get_mut(vm_id) else {
6929 return Err(javascript_child_process_gone_error(
6930 process_id,
6931 &[child_process_id],
6932 ));
6933 };
6934 let Some(child) = vm
6935 .active_processes
6936 .get_mut(process_id)
6937 .ok_or_else(|| missing_process_error(vm_id, process_id))?
6938 .child_processes
6939 .get_mut(child_process_id)
6940 else {
6941 return Err(javascript_child_process_gone_error(
6942 process_id,
6943 &[child_process_id],
6944 ));
6945 };
6946 if let Err(error) = child.execution.write_stdin(chunk) {
6947 if is_broken_pipe_error(&error) {
6948 return Ok(());
6949 }
6950 return Err(error);
6951 }
6952 write_kernel_process_stdin(&mut vm.kernel, child, chunk)
6953 }
6954
6955 pub(crate) fn close_javascript_child_process_stdin(
6956 &mut self,
6957 vm_id: &str,
6958 process_id: &str,
6959 child_process_id: &str,
6960 ) -> Result<(), SidecarError> {
6961 let Some(vm) = self.vms.get_mut(vm_id) else {
6962 return Err(javascript_child_process_gone_error(
6963 process_id,
6964 &[child_process_id],
6965 ));
6966 };
6967 let Some(child) = vm
6968 .active_processes
6969 .get_mut(process_id)
6970 .ok_or_else(|| missing_process_error(vm_id, process_id))?
6971 .child_processes
6972 .get_mut(child_process_id)
6973 else {
6974 return Err(javascript_child_process_gone_error(
6975 process_id,
6976 &[child_process_id],
6977 ));
6978 };
6979 child.execution.close_stdin()?;
6980 close_kernel_process_stdin(&mut vm.kernel, child)
6981 }
6982
6983 pub(crate) fn kill_javascript_child_process(
6984 &mut self,
6985 vm_id: &str,
6986 process_id: &str,
6987 child_process_id: &str,
6988 signal: &str,
6989 ) -> Result<(), SidecarError> {
6990 let signal_name = signal.to_owned();
6991 let signal = parse_signal(signal)?;
6992 let Some(vm) = self.vms.get_mut(vm_id) else {
6993 return Ok(());
6994 };
6995 let process = vm
6996 .active_processes
6997 .get_mut(process_id)
6998 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
6999 let source_pid = process.kernel_pid;
7000 let child = process
7001 .child_processes
7002 .get_mut(child_process_id)
7003 .ok_or_else(|| {
7004 SidecarError::InvalidState(format!(
7005 "unknown child process {child_process_id} during kill"
7006 ))
7007 })?;
7008 terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
7009 emit_security_audit_event(
7010 &self.bridge,
7011 vm_id,
7012 "security.process.kill",
7013 audit_fields([
7014 (String::from("source"), String::from("guest_child_process")),
7015 (String::from("source_pid"), source_pid.to_string()),
7016 (String::from("target_pid"), child.kernel_pid.to_string()),
7017 (String::from("process_id"), process_id.to_owned()),
7018 (
7019 String::from("child_process_id"),
7020 child_process_id.to_owned(),
7021 ),
7022 (String::from("signal"), signal_name),
7023 ]),
7024 );
7025 Ok(())
7026 }
7027
7028 pub(crate) fn signal_vm_kernel_pid(
7034 &mut self,
7035 vm_id: &str,
7036 target_kernel_pid: u32,
7037 signal_name: &str,
7038 ) -> Result<(), SidecarError> {
7039 let signal = parse_signal(signal_name)?;
7040 let located = {
7041 let Some(vm) = self.vms.get(vm_id) else {
7042 return Err(SidecarError::InvalidState(String::from(
7043 "ESRCH: unknown VM during process.kill",
7044 )));
7045 };
7046 let alive = vm
7047 .kernel
7048 .list_processes()
7049 .get(&target_kernel_pid)
7050 .is_some_and(|info| info.status != ProcessStatus::Exited);
7051 if !alive {
7052 return Err(SidecarError::InvalidState(format!(
7053 "ESRCH: no such process {target_kernel_pid}"
7054 )));
7055 }
7056 vm.active_processes.iter().find_map(|(process_id, root)| {
7057 Self::active_process_path_by_kernel_pid(root, target_kernel_pid)
7058 .map(|path| (process_id.clone(), path))
7059 })
7060 };
7061
7062 match located {
7063 Some((process_id, path)) if path.is_empty() => {
7064 self.kill_process_internal(vm_id, &process_id, signal_name)
7065 }
7066 Some((process_id, path)) => {
7067 let Some(vm) = self.vms.get_mut(vm_id) else {
7068 return Ok(());
7069 };
7070 let Some(root) = vm.active_processes.get_mut(&process_id) else {
7071 return Ok(());
7072 };
7073 let Some(target) = Self::active_process_by_owned_path_mut(root, &path) else {
7074 return Err(SidecarError::InvalidState(format!(
7075 "ESRCH: no such process {target_kernel_pid}"
7076 )));
7077 };
7078 terminate_tracked_child_process_for_signal(&mut vm.kernel, target, signal)?;
7079 emit_security_audit_event(
7080 &self.bridge,
7081 vm_id,
7082 "security.process.kill",
7083 audit_fields([
7084 (String::from("source"), String::from("guest_process")),
7085 (String::from("target_pid"), target_kernel_pid.to_string()),
7086 (String::from("process_id"), process_id),
7087 (String::from("signal"), signal_name.to_owned()),
7088 ]),
7089 );
7090 Ok(())
7091 }
7092 None => {
7093 let Some(vm) = self.vms.get_mut(vm_id) else {
7094 return Ok(());
7095 };
7096 let target_pid = i32::try_from(target_kernel_pid).map_err(|_| {
7097 SidecarError::InvalidState(format!(
7098 "EINVAL: invalid process pid {target_kernel_pid}"
7099 ))
7100 })?;
7101 vm.kernel
7102 .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
7103 .map_err(kernel_error)?;
7104 emit_security_audit_event(
7105 &self.bridge,
7106 vm_id,
7107 "security.process.kill",
7108 audit_fields([
7109 (String::from("source"), String::from("guest_process")),
7110 (String::from("target_pid"), target_kernel_pid.to_string()),
7111 (String::from("signal"), signal_name.to_owned()),
7112 ]),
7113 );
7114 Ok(())
7115 }
7116 }
7117 }
7118
7119 pub(crate) fn signal_vm_process_group(
7124 &mut self,
7125 vm_id: &str,
7126 caller_kernel_pid: u32,
7127 pgid: u32,
7128 signal_name: &str,
7129 ) -> Result<bool, SidecarError> {
7130 parse_signal(signal_name)?;
7131 let members = {
7132 let Some(vm) = self.vms.get(vm_id) else {
7133 return Err(SidecarError::InvalidState(String::from(
7134 "ESRCH: unknown VM during process.kill",
7135 )));
7136 };
7137 vm.kernel
7138 .list_processes()
7139 .into_iter()
7140 .filter(|(_, info)| info.pgid == pgid && info.status != ProcessStatus::Exited)
7141 .map(|(pid, _)| pid)
7142 .collect::<Vec<_>>()
7143 };
7144 if members.is_empty() {
7145 return Err(SidecarError::InvalidState(format!(
7146 "ESRCH: no such process group {pgid}"
7147 )));
7148 }
7149
7150 let mut caller_is_member = false;
7151 for member_pid in members {
7152 if member_pid == caller_kernel_pid {
7153 caller_is_member = true;
7154 continue;
7155 }
7156 match self.signal_vm_kernel_pid(vm_id, member_pid, signal_name) {
7157 Ok(()) => {}
7158 Err(error) if sidecar_error_is_esrch(&error) => {}
7161 Err(error) => return Err(error),
7162 }
7163 }
7164 Ok(caller_is_member)
7165 }
7166}
7167
7168fn terminate_tracked_child_process_for_signal(
7173 kernel: &mut SidecarKernel,
7174 child: &mut ActiveProcess,
7175 signal: i32,
7176) -> Result<(), SidecarError> {
7177 let should_terminate_shared_runtime = child.execution.uses_shared_v8_runtime()
7178 && signal != 0
7179 && !matches!(
7180 signal,
7181 libc::SIGHUP
7182 | libc::SIGINT
7183 | libc::SIGTERM
7184 | libc::SIGCHLD
7185 | libc::SIGWINCH
7186 | libc::SIGSTOP
7187 | libc::SIGCONT
7188 );
7189 if should_terminate_shared_runtime {
7190 child.execution.terminate()?;
7191 child.pending_self_signal_exit = Some(signal);
7192 child.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
7193 } else {
7194 kernel
7195 .kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, signal)
7196 .map_err(kernel_error)?;
7197 }
7198 Ok(())
7199}
7200
7201fn sidecar_error_is_esrch(error: &SidecarError) -> bool {
7202 error.to_string().contains("ESRCH")
7203}
7204
7205fn apply_active_process_default_signal(
7206 kernel: &mut SidecarKernel,
7207 process: &mut ActiveProcess,
7208 signal: i32,
7209) -> Result<(), SidecarError> {
7210 if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
7211 return kernel
7212 .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
7213 .map_err(kernel_error);
7214 }
7215
7216 if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
7217 close_kernel_process_stdin(kernel, process)?;
7218 }
7219
7220 if process.execution.uses_shared_v8_runtime() {
7221 process.execution.terminate()?;
7222 if signal != 0 && matches!(process.execution, ActiveExecution::Wasm(_)) {
7223 process.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
7224 }
7225 return Ok(());
7226 }
7227
7228 kernel
7229 .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
7230 .map_err(kernel_error)
7231}
7232
7233fn map_wasm_signal_registration(
7234 registration: secure_exec_execution::wasm::WasmSignalHandlerRegistration,
7235) -> SignalHandlerRegistration {
7236 SignalHandlerRegistration {
7237 action: match registration.action {
7238 secure_exec_execution::wasm::WasmSignalDispositionAction::Default => {
7239 crate::protocol::SignalDispositionAction::Default
7240 }
7241 secure_exec_execution::wasm::WasmSignalDispositionAction::Ignore => {
7242 crate::protocol::SignalDispositionAction::Ignore
7243 }
7244 secure_exec_execution::wasm::WasmSignalDispositionAction::User => {
7245 crate::protocol::SignalDispositionAction::User
7246 }
7247 },
7248 mask: registration.mask,
7249 flags: registration.flags,
7250 }
7251}
7252
7253fn parse_process_signal_state_request(
7254 args: &[Value],
7255) -> Result<(u32, SignalHandlerRegistration), SidecarError> {
7256 let signal = javascript_sync_rpc_arg_u32(args, 0, "process.signal_state signal")?;
7257 let action = javascript_sync_rpc_arg_str(args, 1, "process.signal_state action")?;
7258 let mask_json = javascript_sync_rpc_arg_str(args, 2, "process.signal_state mask")?;
7259 let flags = javascript_sync_rpc_arg_u32(args, 3, "process.signal_state flags")?;
7260 let mask: Vec<u32> = serde_json::from_str(mask_json).map_err(|error| {
7261 SidecarError::InvalidState(format!(
7262 "process.signal_state mask must be valid JSON: {error}"
7263 ))
7264 })?;
7265 let action = match action.trim().to_ascii_lowercase().as_str() {
7266 "default" => SignalDispositionAction::Default,
7267 "ignore" => SignalDispositionAction::Ignore,
7268 "user" => SignalDispositionAction::User,
7269 other => {
7270 return Err(SidecarError::InvalidState(format!(
7271 "unsupported process.signal_state action {other}"
7272 )));
7273 }
7274 };
7275
7276 Ok((
7277 signal,
7278 SignalHandlerRegistration {
7279 action,
7280 mask,
7281 flags,
7282 },
7283 ))
7284}
7285
7286fn apply_process_signal_state_update(
7287 signal_states: &mut BTreeMap<String, BTreeMap<u32, SignalHandlerRegistration>>,
7288 process_id: &str,
7289 signal: u32,
7290 registration: SignalHandlerRegistration,
7291) {
7292 if registration.action == SignalDispositionAction::Default
7293 && registration.mask.is_empty()
7294 && registration.flags == 0
7295 {
7296 let remove_process_entry = signal_states
7297 .get_mut(process_id)
7298 .map(|handlers| {
7299 handlers.remove(&signal);
7300 handlers.is_empty()
7301 })
7302 .unwrap_or(false);
7303 if remove_process_entry {
7304 signal_states.remove(process_id);
7305 }
7306 return;
7307 }
7308
7309 signal_states
7310 .entry(process_id.to_owned())
7311 .or_default()
7312 .insert(signal, registration);
7313}
7314
7315fn map_node_signal_registration(
7316 registration: NodeSignalHandlerRegistration,
7317) -> SignalHandlerRegistration {
7318 SignalHandlerRegistration {
7319 action: match registration.action {
7320 NodeSignalDispositionAction::Default => SignalDispositionAction::Default,
7321 NodeSignalDispositionAction::Ignore => SignalDispositionAction::Ignore,
7322 NodeSignalDispositionAction::User => SignalDispositionAction::User,
7323 },
7324 mask: registration.mask,
7325 flags: registration.flags,
7326 }
7327}
7328
7329fn javascript_child_process_sync_input_bytes(
7330 value: Option<&Value>,
7331) -> Result<Option<Vec<u8>>, SidecarError> {
7332 let Some(value) = value else {
7333 return Ok(None);
7334 };
7335
7336 match value {
7337 Value::Null => Ok(None),
7338 Value::String(text) => Ok(Some(text.as_bytes().to_vec())),
7339 other => javascript_sync_rpc_bytes_arg(
7340 std::slice::from_ref(other),
7341 0,
7342 "child_process.spawn_sync input",
7343 )
7344 .map(Some),
7345 }
7346}
7347
7348fn resolve_execute_request(
7353 vm: &VmState,
7354 payload: &ExecuteRequest,
7355) -> Result<ResolvedChildProcessExecution, SidecarError> {
7356 let payload_env: BTreeMap<String, String> = payload
7357 .env
7358 .iter()
7359 .map(|(k, v)| (k.clone(), v.clone()))
7360 .collect();
7361 if let Some(command) = payload.command.as_deref() {
7362 return resolve_command_execution(
7363 vm,
7364 command,
7365 &payload.args,
7366 &payload_env,
7367 payload.cwd.as_deref(),
7368 payload.wasm_permission_tier,
7369 );
7370 }
7371
7372 let runtime = payload.runtime.clone().ok_or_else(|| {
7373 SidecarError::InvalidState(String::from("execute requires either command or runtime"))
7374 })?;
7375 let entrypoint = payload.entrypoint.clone().ok_or_else(|| {
7376 SidecarError::InvalidState(String::from(
7377 "execute requires either command or entrypoint",
7378 ))
7379 })?;
7380 let (guest_cwd, host_cwd, allow_host_path_overrides) =
7381 resolve_execution_cwds(vm, payload.cwd.as_deref());
7382 let mut env = vm.guest_env.clone();
7383 env.extend(payload_env.clone());
7384
7385 let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint);
7386 if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7387 let requested_cwd = payload.cwd.as_deref().unwrap_or(guest_cwd.as_str());
7388 return Err(SidecarError::InvalidState(format!(
7389 "execution cwd {requested_cwd} is outside sandbox root {}",
7390 vm.host_cwd.to_string_lossy()
7391 )));
7392 }
7393 let host_entrypoint_override = allow_host_path_overrides
7394 .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint))
7395 .flatten();
7396
7397 let guest_entrypoint = host_entrypoint_override
7398 .as_ref()
7399 .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7400 .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, &entrypoint));
7401 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7402
7403 Ok(ResolvedChildProcessExecution {
7404 command: match runtime {
7405 GuestRuntimeKind::JavaScript => String::from(JAVASCRIPT_COMMAND),
7406 GuestRuntimeKind::Python => String::from(PYTHON_COMMAND),
7407 GuestRuntimeKind::WebAssembly => String::from(WASM_COMMAND),
7408 },
7409 process_args: std::iter::once(entrypoint.clone())
7410 .chain(payload.args.iter().cloned())
7411 .collect(),
7412 runtime,
7413 entrypoint: host_entrypoint_override
7414 .map(|(_, host_entrypoint)| host_entrypoint)
7415 .unwrap_or(entrypoint),
7416 execution_args: payload.args.clone(),
7417 env,
7418 guest_cwd,
7419 host_cwd,
7420 wasm_permission_tier: payload.wasm_permission_tier,
7421 tool_command: false,
7422 })
7423}
7424
7425fn resolve_command_execution(
7426 vm: &VmState,
7427 command: &str,
7428 args: &[String],
7429 extra_env: &BTreeMap<String, String>,
7430 cwd: Option<&str>,
7431 explicit_wasm_permission_tier: Option<WasmPermissionTier>,
7432) -> Result<ResolvedChildProcessExecution, SidecarError> {
7433 let (guest_cwd, host_cwd, allow_host_path_overrides) = resolve_execution_cwds(vm, cwd);
7434 let mut env = vm.guest_env.clone();
7435 env.extend(extra_env.clone());
7436 let args = apply_shell_cwd_prefix(command, args.to_vec(), &guest_cwd);
7437
7438 if is_tool_command(vm, command) {
7439 let command = normalized_tool_command_name(command).unwrap_or_else(|| command.to_owned());
7440 return Ok(ResolvedChildProcessExecution {
7441 command: command.clone(),
7442 process_args: std::iter::once(command.clone())
7443 .chain(args.iter().cloned())
7444 .collect(),
7445 runtime: GuestRuntimeKind::JavaScript,
7446 entrypoint: command,
7447 execution_args: args,
7448 env,
7449 guest_cwd,
7450 host_cwd,
7451 wasm_permission_tier: None,
7452 tool_command: true,
7453 });
7454 }
7455
7456 if is_node_runtime_command(command) {
7457 if let Some(cli) = resolve_host_node_cli_entrypoint(command) {
7458 env.insert(
7459 String::from("AGENT_OS_NODE_EVAL"),
7460 build_host_node_cli_eval(&cli),
7461 );
7462 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7463 add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
7464 add_runtime_host_access_path(
7465 &mut env,
7466 "AGENT_OS_EXTRA_FS_READ_PATHS",
7467 &cli.package_root,
7468 true,
7469 );
7470
7471 return Ok(ResolvedChildProcessExecution {
7472 command: String::from(JAVASCRIPT_COMMAND),
7473 process_args: std::iter::once(command.to_owned())
7474 .chain(args.iter().cloned())
7475 .collect(),
7476 runtime: GuestRuntimeKind::JavaScript,
7477 entrypoint: String::from("-e"),
7478 execution_args: std::iter::once(cli.guest_entrypoint.clone())
7479 .chain(args.iter().cloned())
7480 .collect(),
7481 env,
7482 guest_cwd,
7483 host_cwd,
7484 wasm_permission_tier: None,
7485 tool_command: false,
7486 });
7487 }
7488
7489 if args.is_empty() {
7490 env.insert(String::from("AGENT_OS_NODE_EVAL"), String::new());
7491 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7492
7493 return Ok(ResolvedChildProcessExecution {
7494 command: String::from(JAVASCRIPT_COMMAND),
7495 process_args: vec![command.to_owned()],
7496 runtime: GuestRuntimeKind::JavaScript,
7497 entrypoint: String::from("-e"),
7498 execution_args: Vec::new(),
7499 env,
7500 guest_cwd,
7501 host_cwd,
7502 wasm_permission_tier: None,
7503 tool_command: false,
7504 });
7505 }
7506
7507 if let Some((entrypoint, execution_args)) =
7508 resolve_special_node_cli_invocation(&args, &mut env)
7509 {
7510 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7511
7512 return Ok(ResolvedChildProcessExecution {
7513 command: String::from(JAVASCRIPT_COMMAND),
7514 process_args: std::iter::once(command.to_owned())
7515 .chain(args.iter().cloned())
7516 .collect(),
7517 runtime: GuestRuntimeKind::JavaScript,
7518 entrypoint,
7519 execution_args,
7520 env,
7521 guest_cwd,
7522 host_cwd,
7523 wasm_permission_tier: None,
7524 tool_command: false,
7525 });
7526 }
7527
7528 let Some(entrypoint_specifier) = args.first() else {
7529 return Err(SidecarError::InvalidState(format!(
7530 "{command} execution requires an entrypoint"
7531 )));
7532 };
7533
7534 let (entrypoint, execution_args, guest_entrypoint) = {
7535 let requested_host_entrypoint =
7536 resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier);
7537 if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7538 let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
7539 return Err(SidecarError::InvalidState(format!(
7540 "execution cwd {requested_cwd} is outside sandbox root {}",
7541 vm.host_cwd.to_string_lossy()
7542 )));
7543 }
7544 let host_entrypoint_override = allow_host_path_overrides
7545 .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier))
7546 .flatten();
7547 let guest_entrypoint = host_entrypoint_override
7548 .as_ref()
7549 .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7550 .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, entrypoint_specifier));
7551 let entrypoint = host_entrypoint_override.map_or_else(
7552 || {
7553 guest_entrypoint.as_ref().map_or_else(
7554 || entrypoint_specifier.clone(),
7555 |guest_entrypoint| {
7556 resolve_vm_guest_path_to_host(vm, guest_entrypoint)
7557 .to_string_lossy()
7558 .into_owned()
7559 },
7560 )
7561 },
7562 |(_, host_entrypoint)| host_entrypoint,
7563 );
7564 (
7565 entrypoint,
7566 args.iter().skip(1).cloned().collect(),
7567 guest_entrypoint,
7568 )
7569 };
7570
7571 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7572
7573 return Ok(ResolvedChildProcessExecution {
7574 command: String::from(JAVASCRIPT_COMMAND),
7575 process_args: std::iter::once(command.to_owned())
7576 .chain(args.iter().cloned())
7577 .collect(),
7578 runtime: GuestRuntimeKind::JavaScript,
7579 entrypoint,
7580 execution_args,
7581 env,
7582 guest_cwd,
7583 host_cwd,
7584 wasm_permission_tier: None,
7585 tool_command: false,
7586 });
7587 }
7588
7589 if command.ends_with(".js") || command.ends_with(".mjs") || command.ends_with(".cjs") {
7590 let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, command);
7591 if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7592 let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
7593 return Err(SidecarError::InvalidState(format!(
7594 "execution cwd {requested_cwd} is outside sandbox root {}",
7595 vm.host_cwd.to_string_lossy()
7596 )));
7597 }
7598 let host_entrypoint_override = allow_host_path_overrides
7599 .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, command))
7600 .flatten();
7601 let guest_entrypoint = host_entrypoint_override
7602 .as_ref()
7603 .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7604 .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, command));
7605 let entrypoint = host_entrypoint_override.map_or_else(
7606 || {
7607 guest_entrypoint.as_ref().map_or_else(
7608 || command.to_owned(),
7609 |guest_entrypoint| {
7610 resolve_vm_guest_path_to_host(vm, guest_entrypoint)
7611 .to_string_lossy()
7612 .into_owned()
7613 },
7614 )
7615 },
7616 |(_, host_entrypoint)| host_entrypoint,
7617 );
7618 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7619
7620 return Ok(ResolvedChildProcessExecution {
7621 command: String::from(JAVASCRIPT_COMMAND),
7622 process_args: std::iter::once(command.to_owned())
7623 .chain(args.iter().cloned())
7624 .collect(),
7625 runtime: GuestRuntimeKind::JavaScript,
7626 entrypoint,
7627 execution_args: args.to_vec(),
7628 env,
7629 guest_cwd,
7630 host_cwd,
7631 wasm_permission_tier: None,
7632 tool_command: false,
7633 });
7634 }
7635
7636 let guest_entrypoint = resolve_guest_command_entrypoint(
7637 vm,
7638 &guest_cwd,
7639 command,
7640 env.get("PATH").map(String::as_str),
7641 )
7642 .ok_or_else(|| {
7643 SidecarError::InvalidState(format!(
7644 "command not found on native sidecar path: {command}"
7645 ))
7646 })?;
7647 let wasm_permission_tier = explicit_wasm_permission_tier
7648 .or_else(|| vm.command_permissions.get(command).copied())
7649 .or_else(|| {
7650 Path::new(&guest_entrypoint)
7651 .file_name()
7652 .and_then(|name| name.to_str())
7653 .and_then(|name| vm.command_permissions.get(name).copied())
7654 });
7655
7656 let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
7657 if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
7658 resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
7659 {
7660 prepare_guest_runtime_env(
7661 vm,
7662 &mut env,
7663 &guest_cwd,
7664 &host_cwd,
7665 Some(javascript_guest_entrypoint),
7666 )?;
7667
7668 return Ok(ResolvedChildProcessExecution {
7669 command: command.to_owned(),
7670 process_args: std::iter::once(command.to_owned())
7671 .chain(args.iter().cloned())
7672 .collect(),
7673 runtime: GuestRuntimeKind::JavaScript,
7674 entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
7675 execution_args: args.to_vec(),
7676 env,
7677 guest_cwd,
7678 host_cwd,
7679 wasm_permission_tier: None,
7680 tool_command: false,
7681 });
7682 }
7683 prepare_guest_runtime_env(
7684 vm,
7685 &mut env,
7686 &guest_cwd,
7687 &host_cwd,
7688 Some(guest_entrypoint.clone()),
7689 )?;
7690
7691 Ok(ResolvedChildProcessExecution {
7692 command: command.to_owned(),
7693 process_args: std::iter::once(command.to_owned())
7694 .chain(args.iter().cloned())
7695 .collect(),
7696 runtime: GuestRuntimeKind::WebAssembly,
7697 entrypoint: host_entrypoint.to_string_lossy().into_owned(),
7698 execution_args: args.to_vec(),
7699 env,
7700 guest_cwd,
7701 host_cwd,
7702 wasm_permission_tier,
7703 tool_command: false,
7704 })
7705}
7706
7707const MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH: usize = 4;
7708
7709fn resolve_javascript_command_entrypoint(
7710 vm: &VmState,
7711 guest_entrypoint: &str,
7712 host_entrypoint: &Path,
7713) -> Option<(String, PathBuf)> {
7714 resolve_javascript_command_entrypoint_inner(
7715 vm,
7716 guest_entrypoint,
7717 host_entrypoint,
7718 MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH,
7719 )
7720}
7721
7722fn resolve_javascript_command_entrypoint_inner(
7723 vm: &VmState,
7724 guest_entrypoint: &str,
7725 host_entrypoint: &Path,
7726 redirects_remaining: usize,
7727) -> Option<(String, PathBuf)> {
7728 if redirects_remaining > 0 {
7729 let symlink_target = fs::symlink_metadata(host_entrypoint)
7730 .ok()
7731 .filter(|metadata| metadata.file_type().is_symlink())
7732 .and_then(|_| fs::read_link(host_entrypoint).ok());
7733 if let Some(symlink_target) = symlink_target {
7734 let guest_parent = Path::new(guest_entrypoint)
7735 .parent()
7736 .and_then(|path| path.to_str())
7737 .unwrap_or("/");
7738 let symlink_guest_entrypoint = if symlink_target.is_absolute() {
7739 normalize_path(&symlink_target.to_string_lossy())
7740 } else {
7741 normalize_path(&format!(
7742 "{guest_parent}/{}",
7743 symlink_target.to_string_lossy().replace('\\', "/")
7744 ))
7745 };
7746 let symlink_host_entrypoint =
7747 resolve_vm_guest_path_to_host(vm, &symlink_guest_entrypoint);
7748 return resolve_javascript_command_entrypoint_inner(
7749 vm,
7750 &symlink_guest_entrypoint,
7751 &symlink_host_entrypoint,
7752 redirects_remaining - 1,
7753 );
7754 }
7755 }
7756
7757 let script = load_executable_script_preview(host_entrypoint)?;
7758 let interpreter = parse_script_interpreter_name(&script);
7759
7760 if interpreter.is_none() && is_probable_javascript_entrypoint(host_entrypoint, &script) {
7761 return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
7762 }
7763
7764 let interpreter = interpreter?;
7765 if interpreter == "node" {
7766 return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
7767 }
7768
7769 if redirects_remaining == 0 || !matches!(interpreter.as_str(), "sh" | "bash" | "dash") {
7770 return None;
7771 }
7772
7773 let shim_target = parse_node_shell_shim_target(&script)?;
7774 let guest_parent = Path::new(guest_entrypoint)
7775 .parent()
7776 .and_then(|path| path.to_str())
7777 .unwrap_or("/");
7778 let shim_guest_entrypoint = normalize_path(&format!("{guest_parent}/{shim_target}"));
7779 let shim_host_entrypoint = resolve_vm_guest_path_to_host(vm, &shim_guest_entrypoint);
7780 resolve_javascript_command_entrypoint_inner(
7781 vm,
7782 &shim_guest_entrypoint,
7783 &shim_host_entrypoint,
7784 redirects_remaining - 1,
7785 )
7786}
7787
7788fn load_executable_script_preview(path: &Path) -> Option<String> {
7789 let bytes = fs::read(path).ok()?;
7790 let preview_len = bytes.len().min(16 * 1024);
7791 Some(String::from_utf8_lossy(&bytes[..preview_len]).into_owned())
7792}
7793
7794fn parse_script_interpreter_name(script: &str) -> Option<String> {
7795 let shebang = script.lines().next()?.strip_prefix("#!")?.trim();
7796 let mut tokens = shebang.split_whitespace();
7797 let command = tokens.next()?;
7798 let command_name = Path::new(command).file_name()?.to_str()?;
7799 if command_name == "env" {
7800 for token in tokens {
7801 if token.starts_with('-') {
7802 continue;
7803 }
7804 return Path::new(token)
7805 .file_name()
7806 .and_then(|name| name.to_str())
7807 .map(ToOwned::to_owned);
7808 }
7809 return None;
7810 }
7811
7812 Some(command_name.to_owned())
7813}
7814
7815fn parse_node_shell_shim_target(script: &str) -> Option<String> {
7816 for line in script.lines() {
7817 let trimmed = line.trim();
7818 if !trimmed.starts_with("exec ") {
7819 continue;
7820 }
7821
7822 let mut remaining = trimmed;
7823 while let Some(start) = remaining.find("\"$basedir/") {
7824 let after_prefix = &remaining[start + "\"$basedir/".len()..];
7825 let end = after_prefix.find('"')?;
7826 let candidate = &after_prefix[..end];
7827 remaining = &after_prefix[end + 1..];
7828
7829 if candidate.is_empty() || candidate == "node" || candidate.ends_with("/node") {
7830 continue;
7831 }
7832
7833 return Some(candidate.to_owned());
7834 }
7835 }
7836
7837 None
7838}
7839
7840fn is_probable_javascript_entrypoint(path: &Path, script: &str) -> bool {
7841 let extension = path
7842 .extension()
7843 .and_then(|value| value.to_str())
7844 .unwrap_or_default();
7845 if matches!(extension, "js" | "cjs" | "mjs") {
7846 return true;
7847 }
7848
7849 if !path
7850 .components()
7851 .any(|component| component.as_os_str() == "node_modules")
7852 {
7853 return false;
7854 }
7855
7856 let preview = script.trim_start_matches('\u{feff}').trim_start();
7857 !preview.is_empty()
7858 && !preview.starts_with("#!")
7859 && (preview.starts_with("\"use strict\"")
7860 || preview.starts_with("'use strict'")
7861 || preview.starts_with("import ")
7862 || preview.starts_with("export ")
7863 || preview.starts_with("const ")
7864 || preview.starts_with("let ")
7865 || preview.starts_with("var ")
7866 || preview.starts_with("Object.defineProperty(exports")
7867 || preview.starts_with("module.exports")
7868 || preview.starts_with("require("))
7869}
7870
7871fn resolve_guest_execution_cwd(vm: &VmState, value: Option<&str>) -> String {
7872 value
7873 .map(normalize_path)
7874 .unwrap_or_else(|| vm.guest_cwd.clone())
7875}
7876
7877fn resolve_execution_cwds(vm: &VmState, value: Option<&str>) -> (String, PathBuf, bool) {
7878 if let Some(raw_cwd) = value {
7879 let normalized_vm_host_cwd = normalize_host_path(&vm.host_cwd);
7880 let requested_host_cwd = normalize_host_path(Path::new(raw_cwd));
7881 if path_is_within_root(&requested_host_cwd, &normalized_vm_host_cwd) {
7882 let relative = requested_host_cwd
7883 .strip_prefix(&normalized_vm_host_cwd)
7884 .unwrap_or_else(|_| Path::new(""));
7885 let relative = relative.to_string_lossy().replace('\\', "/");
7886 let guest_cwd = if relative.is_empty() {
7887 String::from("/")
7888 } else {
7889 normalize_path(&format!("/{relative}"))
7890 };
7891 return (guest_cwd, requested_host_cwd, true);
7892 }
7893 }
7894
7895 let guest_cwd = resolve_guest_execution_cwd(vm, value);
7896 let host_cwd = if value.is_none() {
7897 vm.host_cwd.clone()
7898 } else {
7899 resolve_vm_guest_path_to_host(vm, &guest_cwd)
7900 };
7901 (guest_cwd, host_cwd, value.is_none())
7902}
7903
7904fn resolve_vm_guest_path_to_host(vm: &VmState, guest_path: &str) -> PathBuf {
7905 host_mount_path_for_guest_path(vm, guest_path)
7906 .unwrap_or_else(|| shadow_path_for_guest(vm, guest_path))
7907}
7908
7909fn shadow_path_for_guest(vm: &VmState, guest_path: &str) -> PathBuf {
7910 let normalized = normalize_path(guest_path);
7911 let relative = normalized.trim_start_matches('/');
7912 if relative.is_empty() {
7913 return vm.cwd.clone();
7914 }
7915 vm.cwd.join(relative)
7916}
7917
7918fn apply_shell_cwd_prefix(command: &str, mut args: Vec<String>, guest_cwd: &str) -> Vec<String> {
7919 if guest_cwd == "/" || !is_shell_command(command) {
7920 return args;
7921 }
7922
7923 let Some(flag) = args.first() else {
7924 return args;
7925 };
7926 if !matches!(flag.as_str(), "-c" | "-lc") || args.len() < 2 {
7927 return args;
7928 }
7929
7930 let command_text = args[1].clone();
7931 let quoted_cwd = shell_single_quote(guest_cwd);
7932 args[1] = format!("cd {quoted_cwd} && {command_text}");
7933 args
7934}
7935
7936fn is_shell_command(command: &str) -> bool {
7937 Path::new(command)
7938 .file_name()
7939 .and_then(|name| name.to_str())
7940 .unwrap_or(command)
7941 .trim_end_matches(".exe")
7942 .eq("sh")
7943 || Path::new(command)
7944 .file_name()
7945 .and_then(|name| name.to_str())
7946 .unwrap_or(command)
7947 .trim_end_matches(".exe")
7948 .eq("bash")
7949}
7950
7951fn shell_single_quote(value: &str) -> String {
7952 if value.is_empty() {
7953 return String::from("''");
7954 }
7955 format!("'{}'", value.replace('\'', "'\"'\"'"))
7956}
7957
7958pub(crate) fn sync_active_process_host_writes_to_kernel(
7959 vm: &mut VmState,
7960) -> Result<(), SidecarError> {
7961 if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
7962 let shadow_root = vm.cwd.clone();
7963 sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
7964 }
7965
7966 let normalized_vm_root = normalize_host_path(&vm.cwd);
7967 let extra_roots = collect_active_process_host_sync_roots(vm, &normalized_vm_root);
7968 for (host_cwd, guest_cwd) in extra_roots {
7969 sync_host_directory_tree_to_kernel(vm, &host_cwd, &guest_cwd)?;
7970 }
7971
7972 Ok(())
7973}
7974
7975fn collect_active_process_host_sync_roots(
7976 vm: &VmState,
7977 normalized_vm_root: &Path,
7978) -> Vec<(PathBuf, String)> {
7979 let mut roots = Vec::new();
7980 let mut seen = BTreeSet::new();
7981
7982 for process in vm.active_processes.values() {
7983 collect_process_host_sync_roots(process, normalized_vm_root, &mut seen, &mut roots);
7984 }
7985
7986 roots
7987}
7988
7989fn collect_process_host_sync_roots(
7990 process: &ActiveProcess,
7991 normalized_vm_root: &Path,
7992 seen: &mut BTreeSet<(PathBuf, String)>,
7993 roots: &mut Vec<(PathBuf, String)>,
7994) {
7995 let normalized_host_cwd = normalize_host_path(&process.host_cwd);
7996 if !path_is_within_root(&normalized_host_cwd, normalized_vm_root) {
7997 let guest_cwd = normalize_path(&process.guest_cwd);
7998 if seen.insert((normalized_host_cwd.clone(), guest_cwd.clone())) {
7999 roots.push((normalized_host_cwd, guest_cwd));
8000 }
8001 }
8002
8003 for child in process.child_processes.values() {
8004 collect_process_host_sync_roots(child, normalized_vm_root, seen, roots);
8005 }
8006}
8007
8008fn sync_process_host_writes_to_kernel(
8009 vm: &mut VmState,
8010 process: &ActiveProcess,
8011) -> Result<(), SidecarError> {
8012 if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
8013 let shadow_root = vm.cwd.clone();
8014 sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
8015 }
8016
8017 if !path_is_within_root(
8018 &normalize_host_path(&process.host_cwd),
8019 &normalize_host_path(&vm.cwd),
8020 ) {
8021 sync_host_directory_tree_to_kernel(vm, &process.host_cwd, &process.guest_cwd)?;
8022 }
8023
8024 Ok(())
8025}
8026
8027fn sync_host_directory_tree_to_kernel(
8028 vm: &mut VmState,
8029 host_root: &Path,
8030 guest_root: &str,
8031) -> Result<(), SidecarError> {
8032 let normalized_host_root = normalize_host_path(host_root);
8033 let normalized_guest_root = normalize_path(guest_root);
8034 let mut synced_file_times = BTreeMap::new();
8035 sync_host_directory_tree_to_kernel_inner(
8036 vm,
8037 &normalized_host_root,
8038 &normalized_host_root,
8039 &normalized_guest_root,
8040 &mut synced_file_times,
8041 )
8042}
8043
8044fn sync_host_directory_tree_to_kernel_inner(
8045 vm: &mut VmState,
8046 host_root: &Path,
8047 current_host_dir: &Path,
8048 guest_root: &str,
8049 synced_file_times: &mut BTreeMap<(u64, u64), (u64, u64)>,
8050) -> Result<(), SidecarError> {
8051 let entries = match fs::read_dir(current_host_dir) {
8052 Ok(entries) => entries,
8053 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
8054 Err(error) => {
8055 return Err(SidecarError::Io(format!(
8056 "failed to read host shadow directory {}: {error}",
8057 current_host_dir.display()
8058 )));
8059 }
8060 };
8061
8062 for entry in entries {
8063 let entry = entry.map_err(|error| {
8064 SidecarError::Io(format!(
8065 "failed to read host shadow entry in {}: {error}",
8066 current_host_dir.display()
8067 ))
8068 })?;
8069 let host_path = entry.path();
8070 let file_type = entry.file_type().map_err(|error| {
8071 SidecarError::Io(format!(
8072 "failed to stat host shadow entry {}: {error}",
8073 host_path.display()
8074 ))
8075 })?;
8076 let relative_path = host_path
8077 .strip_prefix(host_root)
8078 .map_err(|error| {
8079 SidecarError::InvalidState(format!(
8080 "failed to relativize host shadow path {} against {}: {error}",
8081 host_path.display(),
8082 host_root.display()
8083 ))
8084 })?
8085 .to_string_lossy()
8086 .replace('\\', "/");
8087 let guest_path = if guest_root == "/" {
8088 normalize_path(&format!("/{relative_path}"))
8089 } else {
8090 normalize_path(&format!(
8091 "{}/{}",
8092 guest_root.trim_end_matches('/'),
8093 relative_path
8094 ))
8095 };
8096
8097 if should_skip_shadow_sync_path(vm, &guest_path) {
8098 continue;
8099 }
8100
8101 if file_type.is_dir() {
8102 let metadata = entry.metadata().map_err(|error| {
8103 SidecarError::Io(format!(
8104 "failed to read host shadow metadata {}: {error}",
8105 host_path.display()
8106 ))
8107 })?;
8108 if !is_shadow_bootstrap_dir(&guest_path)
8109 && !vm.kernel.exists(&guest_path).unwrap_or(false)
8110 {
8111 vm.kernel.mkdir(&guest_path, true).map_err(|error| {
8112 SidecarError::InvalidState(format!(
8113 "failed to sync host shadow directory {} to guest {}: {}",
8114 host_path.display(),
8115 guest_path,
8116 kernel_error(error)
8117 ))
8118 })?;
8119 vm.kernel
8120 .chmod(&guest_path, host_shadow_mode(&metadata))
8121 .map_err(|error| {
8122 SidecarError::InvalidState(format!(
8123 "failed to sync host shadow directory mode {} to guest {}: {}",
8124 host_path.display(),
8125 guest_path,
8126 kernel_error(error)
8127 ))
8128 })?;
8129 }
8130 sync_host_directory_tree_to_kernel_inner(
8131 vm,
8132 host_root,
8133 &host_path,
8134 guest_root,
8135 synced_file_times,
8136 )?;
8137 continue;
8138 }
8139
8140 if file_type.is_file() {
8141 let metadata = entry.metadata().map_err(|error| {
8142 SidecarError::Io(format!(
8143 "failed to read host shadow metadata {}: {error}",
8144 host_path.display()
8145 ))
8146 })?;
8147 let timestamp_key = (metadata.dev(), metadata.ino());
8148 let (atime_ms, mtime_ms) =
8149 *synced_file_times.entry(timestamp_key).or_insert_with(|| {
8150 (
8151 metadata_time_ms(metadata.atime(), metadata.atime_nsec()),
8152 metadata_time_ms(metadata.mtime(), metadata.mtime_nsec()),
8153 )
8154 });
8155 let desired_mode = host_shadow_mode(&metadata);
8156 let bytes = read_host_shadow_file(&host_path, desired_mode).map_err(|error| {
8157 SidecarError::Io(format!(
8158 "failed to read host shadow file {}: {error}",
8159 host_path.display()
8160 ))
8161 })?;
8162 vm.kernel.write_file(&guest_path, bytes).map_err(|error| {
8163 SidecarError::InvalidState(format!(
8164 "failed to sync host shadow file {} to guest {}: {}",
8165 host_path.display(),
8166 guest_path,
8167 kernel_error(error)
8168 ))
8169 })?;
8170 vm.kernel
8171 .chmod(&guest_path, desired_mode)
8172 .map_err(|error| {
8173 SidecarError::InvalidState(format!(
8174 "failed to sync host shadow file mode {} to guest {}: {}",
8175 host_path.display(),
8176 guest_path,
8177 kernel_error(error)
8178 ))
8179 })?;
8180 vm.kernel
8181 .utimes(&guest_path, atime_ms, mtime_ms)
8182 .map_err(|error| {
8183 SidecarError::InvalidState(format!(
8184 "failed to sync host shadow file times {} to guest {}: {}",
8185 host_path.display(),
8186 guest_path,
8187 kernel_error(error)
8188 ))
8189 })?;
8190 continue;
8191 }
8192
8193 if file_type.is_symlink() {
8194 let target = match fs::read_link(&host_path) {
8195 Ok(target) => target,
8196 Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
8197 Err(error) => {
8198 return Err(SidecarError::Io(format!(
8199 "failed to read host shadow symlink {}: {error}",
8200 host_path.display()
8201 )));
8202 }
8203 };
8204 replace_kernel_symlink(vm, &guest_path, &target.to_string_lossy())?;
8205 }
8206 }
8207
8208 Ok(())
8209}
8210
8211fn replace_kernel_symlink(
8212 vm: &mut VmState,
8213 guest_path: &str,
8214 target: &str,
8215) -> Result<(), SidecarError> {
8216 if vm.kernel.symlink(target, guest_path).is_ok() {
8217 return Ok(());
8218 }
8219
8220 if let Ok(existing_target) = vm.kernel.read_link(guest_path) {
8221 if existing_target == target {
8222 return Ok(());
8223 }
8224 }
8225
8226 let _ = vm.kernel.remove_file(guest_path);
8227 let _ = vm.kernel.remove_dir(guest_path);
8228 vm.kernel
8229 .symlink(target, guest_path)
8230 .map_err(kernel_error)?;
8231 Ok(())
8232}
8233
8234fn host_shadow_mode(metadata: &fs::Metadata) -> u32 {
8235 metadata.permissions().mode() & 0o7777
8236}
8237
8238fn read_host_shadow_file(host_path: &Path, mode: u32) -> std::io::Result<Vec<u8>> {
8244 match fs::read(host_path) {
8245 Ok(bytes) => Ok(bytes),
8246 Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
8247 fs::set_permissions(host_path, fs::Permissions::from_mode(mode | 0o400))?;
8248 let result = fs::read(host_path);
8249 fs::set_permissions(host_path, fs::Permissions::from_mode(mode))?;
8250 result
8251 }
8252 Err(error) => Err(error),
8253 }
8254}
8255
8256fn metadata_time_ms(seconds: i64, nanos: i64) -> u64 {
8257 let seconds = seconds.max(0) as u64;
8258 let nanos = nanos.max(0) as u64;
8259 seconds
8260 .saturating_mul(1_000)
8261 .saturating_add(nanos / 1_000_000)
8262}
8263
8264fn is_shadow_bootstrap_dir(path: &str) -> bool {
8265 matches!(
8266 path,
8267 "/dev"
8268 | "/proc"
8269 | "/tmp"
8270 | "/bin"
8271 | "/lib"
8272 | "/sbin"
8273 | "/boot"
8274 | "/etc"
8275 | "/root"
8276 | "/run"
8277 | "/srv"
8278 | "/sys"
8279 | "/opt"
8280 | "/mnt"
8281 | "/media"
8282 | "/home"
8283 | "/home/user"
8284 | "/usr"
8285 | "/usr/bin"
8286 | "/usr/games"
8287 | "/usr/include"
8288 | "/usr/lib"
8289 | "/usr/libexec"
8290 | "/usr/man"
8291 | "/usr/local"
8292 | "/usr/local/bin"
8293 | "/usr/sbin"
8294 | "/usr/share"
8295 | "/usr/share/man"
8296 | "/var"
8297 | "/var/cache"
8298 | "/var/empty"
8299 | "/var/lib"
8300 | "/var/lock"
8301 | "/var/log"
8302 | "/var/run"
8303 | "/var/spool"
8304 | "/var/tmp"
8305 | "/etc/agentos"
8306 )
8307}
8308
8309#[cfg(test)]
8310mod shadow_sync_tests {
8311 use super::{is_protected_agentos_shadow_sync_path, is_shadow_bootstrap_dir};
8312
8313 #[test]
8314 fn shadow_bootstrap_sync_skips_virtual_home_tree() {
8315 assert!(is_shadow_bootstrap_dir("/home"));
8316 assert!(is_shadow_bootstrap_dir("/home/user"));
8317 }
8318
8319 #[test]
8320 fn protected_agentos_paths_are_not_shadow_synced() {
8321 assert!(is_protected_agentos_shadow_sync_path("/etc/agentos"));
8322 assert!(is_protected_agentos_shadow_sync_path(
8323 "/etc/agentos/instructions.md"
8324 ));
8325 assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos-copy"));
8326 assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos.md"));
8327 }
8328}
8329
8330fn is_kernel_owned_shadow_sync_path(path: &str) -> bool {
8331 matches!(path, "/dev" | "/proc" | "/sys")
8332 || path.starts_with("/dev/")
8333 || path.starts_with("/proc/")
8334 || path.starts_with("/sys/")
8335}
8336
8337pub(crate) fn is_protected_agentos_shadow_sync_path(path: &str) -> bool {
8338 path == "/etc/agentos" || path.starts_with("/etc/agentos/")
8339}
8340
8341fn should_skip_shadow_sync_path(vm: &VmState, guest_path: &str) -> bool {
8342 is_kernel_owned_shadow_sync_path(guest_path)
8343 || is_protected_agentos_shadow_sync_path(guest_path)
8344 || host_mount_path_for_guest_path_from_mounts(&vm.configuration.mounts, guest_path)
8345 .is_some()
8346}
8347
8348fn resolve_path_like_guest_specifier(cwd: &str, specifier: &str) -> String {
8349 if specifier.starts_with("file://") {
8350 normalize_path(specifier.trim_start_matches("file://"))
8351 } else if specifier.starts_with("file:") {
8352 normalize_path(specifier.trim_start_matches("file:"))
8353 } else if specifier.starts_with('/') {
8354 normalize_path(specifier)
8355 } else {
8356 normalize_path(&format!("{cwd}/{specifier}"))
8357 }
8358}
8359
8360fn guest_entrypoint_for_specifier(cwd: &str, specifier: &str) -> Option<String> {
8361 is_path_like_specifier(specifier).then(|| resolve_path_like_guest_specifier(cwd, specifier))
8362}
8363
8364fn is_node_runtime_command(command: &str) -> bool {
8365 matches!(command, "node" | "npm" | "npx")
8366 || Path::new(command)
8367 .file_name()
8368 .and_then(|name| name.to_str())
8369 .is_some_and(|name| matches!(name, "node" | "npm" | "npx"))
8370}
8371
8372fn resolve_special_node_cli_invocation(
8373 args: &[String],
8374 env: &mut BTreeMap<String, String>,
8375) -> Option<(String, Vec<String>)> {
8376 let first = args.first()?;
8377 match first.as_str() {
8378 "-e" | "--eval" => {
8379 env.insert(
8380 String::from("AGENT_OS_NODE_EVAL"),
8381 args.get(1).cloned().unwrap_or_default(),
8382 );
8383 Some((first.clone(), args.iter().skip(2).cloned().collect()))
8384 }
8385 "-v" | "--version" => {
8386 env.insert(
8387 String::from("AGENT_OS_NODE_EVAL"),
8388 String::from("console.log(process.version);"),
8389 );
8390 Some((String::from("-e"), args.to_vec()))
8391 }
8392 _ => None,
8393 }
8394}
8395
8396fn node_runtime_command_name(command: &str) -> Option<&str> {
8397 let name = Path::new(command)
8398 .file_name()
8399 .and_then(|name| name.to_str())?;
8400 matches!(name, "node" | "npm" | "npx").then_some(name)
8401}
8402
8403struct ResolvedHostNodeCliEntrypoint {
8404 command_name: String,
8405 guest_root: String,
8406 guest_entrypoint: String,
8407 package_root: PathBuf,
8408}
8409
8410fn resolve_host_node_cli_entrypoint(command: &str) -> Option<ResolvedHostNodeCliEntrypoint> {
8411 let command_name = node_runtime_command_name(command)?;
8412 if !matches!(command_name, "npm" | "npx") {
8413 return None;
8414 }
8415
8416 let path = std::env::var_os("PATH")?;
8417 for root in std::env::split_paths(&path) {
8418 let candidate = root.join(command_name);
8419 if !candidate.is_file() {
8420 continue;
8421 }
8422 let entrypoint = candidate.canonicalize().ok().unwrap_or(candidate);
8423 let package_root = entrypoint.parent()?.parent()?.to_path_buf();
8424 let guest_root = format!("/__secure_exec/node-runtime/{command_name}");
8425 let relative_entrypoint = entrypoint.strip_prefix(&package_root).ok()?;
8426 let guest_entrypoint = normalize_path(&format!(
8427 "{guest_root}/{}",
8428 relative_entrypoint.to_string_lossy().replace('\\', "/")
8429 ));
8430 return Some(ResolvedHostNodeCliEntrypoint {
8431 command_name: command_name.to_owned(),
8432 guest_root,
8433 guest_entrypoint,
8434 package_root,
8435 });
8436 }
8437
8438 None
8439}
8440
8441fn build_host_node_cli_eval(cli: &ResolvedHostNodeCliEntrypoint) -> String {
8442 let guest_npm_main = normalize_path(&format!("{}/lib/npm.js", cli.guest_root));
8443 let guest_npm_cli = normalize_path(&format!("{}/bin/npm-cli.js", cli.guest_root));
8444 let guest_package_json = normalize_path(&format!("{}/package.json", cli.guest_root));
8445 let guest_display_module = normalize_path(&format!("{}/lib/utils/display.js", cli.guest_root));
8446 let guest_log_file_module =
8447 normalize_path(&format!("{}/lib/utils/log-file.js", cli.guest_root));
8448 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); } } }";
8449 let display_stub = format!(
8450 "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 }};",
8451 display_module = serde_json::to_string(&guest_display_module)
8452 .unwrap_or_else(|_| format!("\"{guest_display_module}\"")),
8453 log_file_module = serde_json::to_string(&guest_log_file_module)
8454 .unwrap_or_else(|_| format!("\"{guest_log_file_module}\"")),
8455 );
8456 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)); }";
8457 match cli.command_name.as_str() {
8458 "npx" => format!(
8459 "{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); }});",
8460 debug_preamble = debug_preamble,
8461 display_stub = display_stub,
8462 registry_fetch_stub = registry_fetch_stub.replace(
8463 "__AGENT_OS_NPM_MAIN__",
8464 &serde_json::to_string(&guest_npm_main)
8465 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8466 ),
8467 npm_main = serde_json::to_string(&guest_npm_main)
8468 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8469 npm_cli = serde_json::to_string(&guest_npm_cli)
8470 .unwrap_or_else(|_| format!("\"{guest_npm_cli}\"")),
8471 package_json = serde_json::to_string(&guest_package_json)
8472 .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
8473 ),
8474 _ => format!(
8475 "{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); }});",
8476 debug_preamble = debug_preamble,
8477 display_stub = display_stub,
8478 registry_fetch_stub = registry_fetch_stub.replace(
8479 "__AGENT_OS_NPM_MAIN__",
8480 &serde_json::to_string(&guest_npm_main)
8481 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8482 ),
8483 npm_main = serde_json::to_string(&guest_npm_main)
8484 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8485 package_json = serde_json::to_string(&guest_package_json)
8486 .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
8487 ),
8488 }
8489}
8490
8491fn resolve_guest_command_entrypoint(
8492 vm: &VmState,
8493 guest_cwd: &str,
8494 command: &str,
8495 path_env: Option<&str>,
8496) -> Option<String> {
8497 if !is_path_like_specifier(command) {
8498 if let Some(entrypoint) = vm.command_guest_paths.get(command) {
8499 return Some(entrypoint.clone());
8500 }
8501
8502 for search_dir in guest_command_search_dirs(vm, guest_cwd, path_env) {
8503 let candidate = normalize_path(&format!("{search_dir}/{command}"));
8504 if let Some(entrypoint) = resolve_guest_command_path_candidate(vm, &candidate) {
8505 return Some(entrypoint);
8506 }
8507 }
8508
8509 return None;
8510 }
8511
8512 let normalized = resolve_path_like_guest_specifier(guest_cwd, command);
8513 resolve_guest_command_path_candidate(vm, &normalized).or_else(|| {
8514 let parent_dir = Path::new(&normalized).parent()?.to_str()?;
8518 if !guest_command_search_dirs(vm, guest_cwd, path_env)
8519 .iter()
8520 .any(|search_dir| normalize_path(search_dir) == normalize_path(parent_dir))
8521 {
8522 return None;
8523 }
8524
8525 let file_name = Path::new(&normalized).file_name()?.to_str()?;
8526 vm.command_guest_paths.get(file_name).cloned()
8527 })
8528}
8529
8530fn guest_command_search_dirs(vm: &VmState, guest_cwd: &str, path_env: Option<&str>) -> Vec<String> {
8531 let mut search_dirs = Vec::new();
8532 let mut seen = BTreeSet::new();
8533
8534 if let Some(path) = path_env.or_else(|| vm.guest_env.get("PATH").map(String::as_str)) {
8535 for segment in path.split(':') {
8536 let trimmed = segment.trim();
8537 if trimmed.is_empty() {
8538 continue;
8539 }
8540 let normalized = if trimmed.starts_with('/') {
8541 normalize_path(trimmed)
8542 } else {
8543 normalize_path(&format!("{guest_cwd}/{trimmed}"))
8544 };
8545 if seen.insert(normalized.clone()) {
8546 search_dirs.push(normalized);
8547 }
8548 }
8549 }
8550
8551 for fallback in ["/bin", "/usr/bin", "/usr/local/bin"] {
8552 let normalized = String::from(fallback);
8553 if seen.insert(normalized.clone()) {
8554 search_dirs.push(normalized);
8555 }
8556 }
8557
8558 search_dirs
8559}
8560
8561fn resolve_guest_command_path_candidate(vm: &VmState, candidate: &str) -> Option<String> {
8562 if candidate.starts_with("/bin/")
8563 || candidate.starts_with("/usr/bin/")
8564 || candidate.starts_with("/usr/local/bin/")
8565 || candidate.starts_with("/__secure_exec/commands/")
8566 {
8567 if let Some(file_name) = Path::new(candidate)
8568 .file_name()
8569 .and_then(|name| name.to_str())
8570 {
8571 if let Some(guest_entrypoint) = vm.command_guest_paths.get(file_name) {
8572 return Some(guest_entrypoint.clone());
8573 }
8574 }
8575 }
8576
8577 if vm
8578 .kernel
8579 .exists(candidate)
8580 .ok()
8581 .is_some_and(|exists| exists)
8582 {
8583 return Some(normalize_path(candidate));
8584 }
8585
8586 resolve_vm_guest_path_to_host(vm, candidate)
8587 .is_file()
8588 .then(|| normalize_path(candidate))
8589}
8590
8591fn resolve_host_entrypoint_within_vm_host_cwd(
8592 vm: &VmState,
8593 specifier: &str,
8594) -> Option<(String, String)> {
8595 let candidate = Path::new(specifier);
8596 if !candidate.is_absolute() {
8597 return None;
8598 }
8599
8600 let normalized_entrypoint = normalize_host_path(candidate);
8601 let normalized_host_cwd = normalize_host_path(&vm.host_cwd);
8602 if !path_is_within_root(&normalized_entrypoint, &normalized_host_cwd) {
8603 return None;
8604 }
8605
8606 let relative = normalized_entrypoint
8607 .strip_prefix(&normalized_host_cwd)
8608 .ok()?
8609 .to_string_lossy()
8610 .replace('\\', "/");
8611 let guest_entrypoint = if relative.is_empty() {
8612 String::from("/")
8613 } else {
8614 normalize_path(&format!("/{relative}"))
8615 };
8616 Some((
8617 guest_entrypoint,
8618 normalized_entrypoint.to_string_lossy().into_owned(),
8619 ))
8620}
8621
8622fn prepare_guest_runtime_env(
8623 vm: &VmState,
8624 env: &mut BTreeMap<String, String>,
8625 guest_cwd: &str,
8626 host_cwd: &Path,
8627 guest_entrypoint: Option<String>,
8628) -> Result<(), SidecarError> {
8629 let user = vm.kernel.user_profile();
8630 let path_mappings = runtime_guest_path_mappings(vm);
8631 let read_paths = expand_host_access_paths(
8632 std::iter::once(vm.cwd.clone())
8633 .chain(
8634 path_mappings
8635 .iter()
8636 .map(|mapping| PathBuf::from(&mapping.host_path)),
8637 )
8638 .chain(std::iter::once(host_cwd.to_path_buf()))
8639 .collect::<Vec<_>>()
8640 .as_slice(),
8641 );
8642 let write_paths = dedupe_host_paths(
8643 std::iter::once(vm.cwd.clone())
8644 .chain(std::iter::once(host_cwd.to_path_buf()))
8645 .chain(runtime_guest_writable_host_paths(vm))
8646 .collect::<Vec<_>>()
8647 .as_slice(),
8648 );
8649 let allowed_node_builtins = configured_allowed_node_builtins(vm);
8650 let loopback_exempt_ports = configured_loopback_exempt_ports(vm);
8651
8652 env.insert(
8653 String::from("AGENT_OS_GUEST_PATH_MAPPINGS"),
8654 serde_json::to_string(&path_mappings).map_err(|error| {
8655 SidecarError::InvalidState(format!("failed to encode guest path mappings: {error}"))
8656 })?,
8657 );
8658 env.entry(String::from(EXECUTION_SANDBOX_ROOT_ENV))
8659 .or_insert_with(|| normalize_host_path(&vm.cwd).to_string_lossy().into_owned());
8660 env.insert(
8661 String::from("AGENT_OS_EXTRA_FS_READ_PATHS"),
8662 serde_json::to_string(
8663 &read_paths
8664 .iter()
8665 .map(|path| path.to_string_lossy().into_owned())
8666 .collect::<Vec<_>>(),
8667 )
8668 .map_err(|error| {
8669 SidecarError::InvalidState(format!("failed to encode read paths: {error}"))
8670 })?,
8671 );
8672 env.insert(
8673 String::from("AGENT_OS_EXTRA_FS_WRITE_PATHS"),
8674 serde_json::to_string(
8675 &write_paths
8676 .iter()
8677 .map(|path| path.to_string_lossy().into_owned())
8678 .collect::<Vec<_>>(),
8679 )
8680 .map_err(|error| {
8681 SidecarError::InvalidState(format!("failed to encode write paths: {error}"))
8682 })?,
8683 );
8684 env.insert(
8685 String::from("AGENT_OS_ALLOWED_NODE_BUILTINS"),
8686 serde_json::to_string(&allowed_node_builtins).map_err(|error| {
8687 SidecarError::InvalidState(format!("failed to encode allowed builtins: {error}"))
8688 })?,
8689 );
8690 env.insert(
8693 String::from("AGENT_OS_JS_PLATFORM"),
8694 js_runtime_platform_env(vm).to_owned(),
8695 );
8696 if let Some(resolution) = js_runtime_module_resolution_env(vm) {
8698 env.insert(
8699 String::from("AGENT_OS_JS_MODULE_RESOLUTION"),
8700 resolution.to_owned(),
8701 );
8702 }
8703 if let Some(allowlist) = js_runtime_enforced_builtins(vm) {
8707 env.insert(
8708 String::from("AGENT_OS_JS_BUILTIN_ALLOWLIST"),
8709 serde_json::to_string(&allowlist).map_err(|error| {
8710 SidecarError::InvalidState(format!(
8711 "failed to encode jsRuntime builtin allow-list: {error}"
8712 ))
8713 })?,
8714 );
8715 }
8716 env.entry(String::from("HOME"))
8723 .or_insert_with(|| user.homedir.clone());
8724 env.entry(String::from("USER"))
8725 .or_insert_with(|| user.username.clone());
8726 env.entry(String::from("LOGNAME"))
8727 .or_insert_with(|| user.username.clone());
8728 env.entry(String::from("SHELL"))
8729 .or_insert_with(|| user.shell.clone());
8730 env.entry(String::from("PATH")).or_insert_with(|| {
8731 vm.guest_env
8732 .get("PATH")
8733 .cloned()
8734 .unwrap_or_else(|| crate::vm::DEFAULT_GUEST_PATH_ENV.to_owned())
8735 });
8736 env.entry(String::from("TMPDIR"))
8737 .or_insert_with(|| String::from("/tmp"));
8738 env.insert(String::from("PWD"), guest_cwd.to_owned());
8739 if !loopback_exempt_ports.is_empty() {
8740 env.insert(
8741 String::from(LOOPBACK_EXEMPT_PORTS_ENV),
8742 serde_json::to_string(&loopback_exempt_ports).map_err(|error| {
8743 SidecarError::InvalidState(format!("failed to encode loopback exemptions: {error}"))
8744 })?,
8745 );
8746 }
8747 if let Some(guest_entrypoint) = guest_entrypoint {
8748 env.insert(String::from("AGENT_OS_GUEST_ENTRYPOINT"), guest_entrypoint);
8749 }
8750 Ok(())
8751}
8752
8753fn virtual_os_cpu_count(resource_limits: &ResourceLimits) -> usize {
8754 resource_limits.virtual_cpu_count.unwrap_or(1).max(1)
8755}
8756
8757fn virtual_os_totalmem_bytes(resource_limits: &ResourceLimits) -> u64 {
8758 resource_limits
8759 .max_wasm_memory_bytes
8760 .unwrap_or(1024 * 1024 * 1024)
8761}
8762
8763fn virtual_os_freemem_bytes(resource_limits: &ResourceLimits) -> u64 {
8764 resource_limits
8765 .max_wasm_memory_bytes
8766 .unwrap_or(512 * 1024 * 1024)
8767}
8768
8769fn javascript_execution_limits(vm: &VmState) -> JavascriptExecutionLimits {
8774 JavascriptExecutionLimits {
8775 v8_heap_limit_mb: vm.limits.js_runtime.v8_heap_limit_mb,
8776 sync_rpc_wait_timeout_ms: vm.limits.js_runtime.sync_rpc_wait_timeout_ms,
8777 }
8778}
8779
8780fn guest_runtime_identity(
8786 vm: &VmState,
8787 virtual_pid: Option<u64>,
8788 virtual_ppid: Option<u64>,
8789) -> GuestRuntimeConfig {
8790 let user = vm.kernel.user_profile();
8791 let resource_limits = vm.kernel.resource_limits();
8792 GuestRuntimeConfig {
8793 virtual_uid: Some(u64::from(user.uid)),
8794 virtual_gid: Some(u64::from(user.gid)),
8795 virtual_pid,
8796 virtual_ppid,
8797 virtual_exec_path: None,
8798 os_cpu_count: Some(virtual_os_cpu_count(resource_limits) as u64),
8799 os_totalmem: Some(virtual_os_totalmem_bytes(resource_limits)),
8800 os_freemem: Some(virtual_os_freemem_bytes(resource_limits)),
8801 os_homedir: Some(user.homedir.clone()),
8802 os_hostname: None,
8803 os_shell: Some(user.shell.clone()),
8804 os_user: Some(user.username.clone()),
8805 }
8806}
8807
8808fn guest_virtual_home(vm: &VmState) -> String {
8813 let homedir = vm.kernel.user_profile().homedir;
8814 if homedir.starts_with('/') {
8815 homedir
8816 } else {
8817 String::from("/root")
8818 }
8819}
8820
8821fn python_execution_limits(vm: &VmState) -> PythonExecutionLimits {
8823 PythonExecutionLimits {
8824 output_buffer_max_bytes: Some(vm.limits.python.output_buffer_max_bytes),
8825 execution_timeout_ms: Some(vm.limits.python.execution_timeout_ms),
8826 max_old_space_mb: Some(vm.limits.python.max_old_space_mb),
8827 vfs_rpc_timeout_ms: Some(vm.limits.python.vfs_rpc_timeout_ms),
8828 }
8829}
8830
8831fn wasm_execution_limits(vm: &VmState) -> WasmExecutionLimits {
8836 let resource_limits = vm.kernel.resource_limits();
8837 WasmExecutionLimits {
8838 max_fuel: resource_limits.max_wasm_fuel,
8839 max_memory_bytes: resource_limits.max_wasm_memory_bytes,
8840 max_stack_bytes: resource_limits
8841 .max_wasm_stack_bytes
8842 .map(|value| value as u64),
8843 }
8844}
8845
8846fn js_runtime_platform(vm: &VmState) -> vm_config::JsRuntimePlatform {
8849 vm.configuration
8850 .js_runtime
8851 .as_ref()
8852 .map(|cfg| cfg.platform)
8853 .unwrap_or(vm_config::JsRuntimePlatform::Node)
8854}
8855
8856fn js_runtime_platform_env(vm: &VmState) -> &'static str {
8859 match js_runtime_platform(vm) {
8860 vm_config::JsRuntimePlatform::Node => "node",
8861 vm_config::JsRuntimePlatform::Browser => "browser",
8862 vm_config::JsRuntimePlatform::Neutral => "neutral",
8863 vm_config::JsRuntimePlatform::Bare => "bare",
8864 }
8865}
8866
8867fn js_runtime_module_resolution_env(vm: &VmState) -> Option<&'static str> {
8870 let resolution = vm
8871 .configuration
8872 .js_runtime
8873 .as_ref()
8874 .map(|cfg| cfg.module_resolution)
8875 .unwrap_or(vm_config::JsModuleResolution::Node);
8876 match resolution {
8877 vm_config::JsModuleResolution::Node => None,
8878 vm_config::JsModuleResolution::Relative => Some("relative"),
8879 vm_config::JsModuleResolution::None => Some("none"),
8880 }
8881}
8882
8883fn js_runtime_enforced_builtins(vm: &VmState) -> Option<Vec<String>> {
8887 if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
8888 return Some(Vec::new());
8889 }
8890 vm.configuration
8891 .js_runtime
8892 .as_ref()
8893 .and_then(|cfg| cfg.allowed_builtins.clone())
8894}
8895
8896fn configured_allowed_node_builtins(vm: &VmState) -> Vec<String> {
8897 if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
8899 return Vec::new();
8900 }
8901 let configured = match vm
8904 .configuration
8905 .js_runtime
8906 .as_ref()
8907 .and_then(|cfg| cfg.allowed_builtins.as_ref())
8908 {
8909 Some(list) => list.clone(),
8910 None => DEFAULT_ALLOWED_NODE_BUILTINS
8911 .iter()
8912 .map(|value| (*value).to_owned())
8913 .collect::<Vec<_>>(),
8914 };
8915 dedupe_strings(&configured)
8916}
8917
8918fn configured_loopback_exempt_ports(vm: &VmState) -> Vec<String> {
8919 if !vm.configuration.loopback_exempt_ports.is_empty() {
8920 return vm
8921 .configuration
8922 .loopback_exempt_ports
8923 .iter()
8924 .map(ToString::to_string)
8925 .collect();
8926 }
8927
8928 vm.create_loopback_exempt_ports
8929 .iter()
8930 .map(ToString::to_string)
8931 .collect()
8932}
8933
8934fn mount_config_host_path(config: &str) -> Option<String> {
8936 serde_json::from_str::<Value>(config)
8937 .ok()?
8938 .get("hostPath")
8939 .and_then(Value::as_str)
8940 .map(str::to_owned)
8941}
8942
8943fn runtime_guest_writable_host_paths(vm: &VmState) -> Vec<PathBuf> {
8944 vm.configuration
8945 .mounts
8946 .iter()
8947 .filter(|mount| !mount.read_only)
8948 .filter_map(|mount| {
8949 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
8950 .then(|| mount_config_host_path(&mount.plugin.config))
8951 .flatten()
8952 .map(PathBuf::from)
8953 })
8954 .collect()
8955}
8956
8957fn runtime_guest_path_mappings(vm: &VmState) -> Vec<RuntimeGuestPathMapping> {
8958 let mut mappings = vm
8959 .configuration
8960 .mounts
8961 .iter()
8962 .filter_map(|mount| {
8963 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
8964 .then(|| {
8965 mount_config_host_path(&mount.plugin.config).map(|host_path| {
8966 RuntimeGuestPathMapping {
8967 guest_path: normalize_path(&mount.guest_path),
8968 host_path,
8969 read_only: mount.read_only,
8970 }
8971 })
8972 })
8973 .flatten()
8974 })
8975 .collect::<Vec<_>>();
8976 let mut command_root_mappings = vm
8977 .command_guest_paths
8978 .values()
8979 .filter_map(|guest_path| {
8980 Path::new(guest_path)
8981 .parent()
8982 .and_then(|parent| parent.to_str())
8983 .map(normalize_path)
8984 })
8985 .collect::<BTreeSet<_>>()
8986 .into_iter()
8987 .map(|guest_path| RuntimeGuestPathMapping {
8988 host_path: resolve_vm_guest_path_to_host(vm, &guest_path)
8989 .to_string_lossy()
8990 .into_owned(),
8991 guest_path,
8992 read_only: false,
8993 })
8994 .collect::<Vec<_>>();
8995 mappings.append(&mut command_root_mappings);
8996 let mut extra_node_modules_roots = mappings
8997 .iter()
8998 .filter(|mapping| mapping.guest_path.starts_with("/root/node_modules/"))
8999 .filter_map(|mapping| {
9000 host_node_modules_root(Path::new(&mapping.host_path)).map(|host_root| {
9001 RuntimeGuestPathMapping {
9002 guest_path: String::from("/root/node_modules"),
9003 host_path: host_root.to_string_lossy().into_owned(),
9004 read_only: mapping.read_only,
9005 }
9006 })
9007 })
9008 .collect::<Vec<_>>();
9009 mappings.append(&mut extra_node_modules_roots);
9010 mappings.push(RuntimeGuestPathMapping {
9011 guest_path: String::from("/"),
9012 host_path: vm.cwd.to_string_lossy().into_owned(),
9013 read_only: false,
9014 });
9015 mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.guest_path.len()));
9016 mappings.dedup_by(|left, right| {
9017 left.guest_path == right.guest_path && left.host_path == right.host_path
9018 });
9019 mappings
9020}
9021
9022fn build_module_reader(
9033 vm: &VmState,
9034 resolved: &ResolvedChildProcessExecution,
9035) -> Option<crate::plugins::host_dir::HostDirModuleReader> {
9036 let mut pairs: Vec<(String, PathBuf)> = vm
9037 .configuration
9038 .mounts
9039 .iter()
9040 .filter(|mount| mount.read_only)
9041 .filter(|mount| (mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
9042 .filter_map(|mount| {
9043 mount_config_host_path(&mount.plugin.config)
9044 .map(|host_path| (normalize_path(&mount.guest_path), PathBuf::from(host_path)))
9045 })
9046 .collect();
9047
9048 let guest_entrypoint = resolved
9049 .env
9050 .get("AGENT_OS_GUEST_ENTRYPOINT")
9051 .map(|path| normalize_path(path));
9052 if let Some(guest_entrypoint) = guest_entrypoint.as_deref() {
9053 let entrypoint_in_read_only_mount = pairs.iter().any(|(guest_path, _)| {
9054 guest_entrypoint == guest_path
9055 || guest_entrypoint.starts_with(&format!("{guest_path}/"))
9056 });
9057 if !entrypoint_in_read_only_mount {
9058 return None;
9059 }
9060 }
9061
9062 let extra_roots: Vec<(String, PathBuf)> = pairs
9066 .iter()
9067 .filter(|(guest_path, _)| guest_path.starts_with("/root/node_modules/"))
9068 .filter_map(|(_, host_path)| {
9069 host_node_modules_root(host_path).map(|root| (String::from("/root/node_modules"), root))
9070 })
9071 .collect();
9072 pairs.extend(extra_roots);
9073
9074 crate::plugins::host_dir::HostDirModuleReader::from_mounts(pairs)
9075}
9076
9077fn host_node_modules_root(path: &Path) -> Option<PathBuf> {
9078 if let Some(root) = path
9079 .ancestors()
9080 .filter(|candidate| {
9081 candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
9082 })
9083 .last()
9084 .map(Path::to_path_buf)
9085 {
9086 return Some(root);
9087 }
9088
9089 fs::canonicalize(path)
9090 .ok()?
9091 .ancestors()
9092 .filter(|candidate| {
9093 candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
9094 })
9095 .last()
9096 .map(Path::to_path_buf)
9097}
9098
9099#[cfg(test)]
9100mod runtime_guest_path_mapping_tests {
9101 use super::{host_node_modules_root, javascript_sync_rpc_option_bool};
9102 use serde_json::json;
9103 use std::fs;
9104 use std::time::{SystemTime, UNIX_EPOCH};
9105
9106 #[test]
9107 fn host_node_modules_root_prefers_workspace_root_over_pnpm_package_node_modules() {
9108 let unique = SystemTime::now()
9109 .duration_since(UNIX_EPOCH)
9110 .expect("clock should be monotonic")
9111 .as_nanos();
9112 let temp = std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-{unique}"));
9113 let workspace_node_modules = temp.join("node_modules");
9114 let package_root = workspace_node_modules
9115 .join(".pnpm")
9116 .join("example@1.0.0")
9117 .join("node_modules")
9118 .join("@scope")
9119 .join("pkg");
9120 fs::create_dir_all(&package_root).expect("package root should be created");
9121
9122 let resolved =
9123 host_node_modules_root(&package_root).expect("node_modules root should resolve");
9124
9125 assert_eq!(resolved, workspace_node_modules);
9126
9127 fs::remove_dir_all(&temp).expect("temp tree should be removed");
9128 }
9129
9130 #[test]
9131 fn host_node_modules_root_preserves_symlinked_workspace_node_modules_path() {
9132 let unique = SystemTime::now()
9133 .duration_since(UNIX_EPOCH)
9134 .expect("clock should be monotonic")
9135 .as_nanos();
9136 let temp =
9137 std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-symlink-{unique}"));
9138 let workspace_node_modules = temp.join("node_modules");
9139 let package_link = workspace_node_modules.join("@scope").join("pkg");
9140 let real_package = temp.join("registry").join("agent").join("pkg");
9141 fs::create_dir_all(package_link.parent().expect("package parent should exist"))
9142 .expect("scoped parent should be created");
9143 fs::create_dir_all(&real_package).expect("real package root should be created");
9144 std::os::unix::fs::symlink(&real_package, &package_link)
9145 .expect("package symlink should be created");
9146
9147 let resolved =
9148 host_node_modules_root(&package_link).expect("node_modules root should resolve");
9149
9150 assert_eq!(resolved, workspace_node_modules);
9151
9152 fs::remove_dir_all(&temp).expect("temp tree should be removed");
9153 }
9154
9155 #[test]
9156 fn javascript_sync_rpc_option_bool_accepts_boolean_recursive_argument() {
9157 assert_eq!(
9158 javascript_sync_rpc_option_bool(&[json!("/workspace"), json!(true)], 1, "recursive"),
9159 Some(true)
9160 );
9161 assert_eq!(
9162 javascript_sync_rpc_option_bool(
9163 &[json!("/workspace"), json!({ "recursive": false })],
9164 1,
9165 "recursive"
9166 ),
9167 Some(false)
9168 );
9169 }
9170}
9171
9172#[cfg(test)]
9173mod kernel_poll_sync_rpc_tests {
9174 use super::{
9175 service_javascript_kernel_poll_sync_rpc, ActiveExecution, ActiveProcess,
9176 JavascriptSyncRpcRequest, KernelPollFdResponse, SidecarKernel, ToolExecution,
9177 EXECUTION_DRIVER_NAME, JAVASCRIPT_COMMAND,
9178 };
9179 use secure_exec_kernel::command_registry::CommandDriver;
9180 use secure_exec_kernel::kernel::{KernelVmConfig, SpawnOptions};
9181 use secure_exec_kernel::mount_table::MountTable;
9182 use secure_exec_kernel::permissions::Permissions;
9183 use secure_exec_kernel::poll::{POLLHUP, POLLIN};
9184 use secure_exec_kernel::vfs::MemoryFileSystem;
9185 use serde_json::{json, Value};
9186 #[test]
9187 fn javascript_kernel_poll_sync_rpc_reports_multiple_kernel_fds() {
9188 let mut config = KernelVmConfig::new("vm-js-kernel-poll");
9189 config.permissions = Permissions::allow_all();
9190 let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config);
9191 kernel
9192 .register_driver(CommandDriver::new(
9193 EXECUTION_DRIVER_NAME,
9194 [JAVASCRIPT_COMMAND],
9195 ))
9196 .expect("register execution driver");
9197
9198 let kernel_handle = kernel
9199 .spawn_process(
9200 JAVASCRIPT_COMMAND,
9201 Vec::new(),
9202 SpawnOptions {
9203 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
9204 ..SpawnOptions::default()
9205 },
9206 )
9207 .expect("spawn javascript kernel process");
9208 let pid = kernel_handle.pid();
9209
9210 let (stdin_read_fd, stdin_write_fd) = kernel
9211 .open_pipe(EXECUTION_DRIVER_NAME, pid)
9212 .expect("open kernel stdin pipe");
9213 kernel
9214 .fd_dup2(EXECUTION_DRIVER_NAME, pid, stdin_read_fd, 0)
9215 .expect("dup stdin pipe onto fd 0");
9216 kernel
9217 .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_read_fd)
9218 .expect("close original stdin read fd");
9219
9220 let process = ActiveProcess::new(
9221 pid,
9222 kernel_handle,
9223 super::GuestRuntimeKind::JavaScript,
9224 ActiveExecution::Tool(ToolExecution::default()),
9225 );
9226
9227 kernel
9228 .fd_write(EXECUTION_DRIVER_NAME, pid, stdin_write_fd, b"poll-ready")
9229 .expect("write kernel stdin payload");
9230 kernel
9231 .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_write_fd)
9232 .expect("close kernel stdin writer");
9233
9234 let response = service_javascript_kernel_poll_sync_rpc(
9235 &mut kernel,
9236 &process,
9237 &JavascriptSyncRpcRequest {
9238 id: 1,
9239 method: String::from("__kernel_poll"),
9240 args: vec![
9241 json!([
9242 { "fd": 0, "events": POLLIN.bits() },
9243 { "fd": 1, "events": POLLIN.bits() }
9244 ]),
9245 json!(250),
9246 ],
9247 },
9248 )
9249 .expect("poll kernel fds");
9250
9251 assert_eq!(response["readyCount"], Value::from(1));
9252 let fds: Vec<KernelPollFdResponse> =
9253 serde_json::from_value(response["fds"].clone()).expect("kernel poll fd response");
9254 assert_eq!(
9255 fds,
9256 vec![
9257 KernelPollFdResponse {
9258 fd: 0,
9259 events: POLLIN.bits(),
9260 revents: (POLLIN | POLLHUP).bits(),
9261 },
9262 KernelPollFdResponse {
9263 fd: 1,
9264 events: POLLIN.bits(),
9265 revents: 0,
9266 },
9267 ]
9268 );
9269
9270 process.kernel_handle.finish(0);
9271 kernel.waitpid(pid).expect("wait javascript kernel process");
9272 }
9273}
9274
9275fn dedupe_strings(values: &[String]) -> Vec<String> {
9276 let mut seen = BTreeSet::new();
9277 let mut deduped = Vec::new();
9278 for value in values {
9279 if seen.insert(value.clone()) {
9280 deduped.push(value.clone());
9281 }
9282 }
9283 deduped
9284}
9285
9286fn dedupe_host_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
9287 let mut seen = BTreeSet::new();
9288 let mut deduped = Vec::new();
9289 for path in paths {
9290 let normalized = normalize_host_path(path);
9291 let key = normalized.to_string_lossy().into_owned();
9292 if seen.insert(key) {
9293 deduped.push(normalized);
9294 }
9295 }
9296 deduped
9297}
9298
9299fn expand_host_access_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
9300 let mut expanded = Vec::new();
9301 let mut seen = BTreeSet::new();
9302
9303 let mut add_path = |candidate: PathBuf| {
9304 let normalized = normalize_host_path(&candidate);
9305 let key = normalized.to_string_lossy().into_owned();
9306 if seen.insert(key) {
9307 expanded.push(normalized);
9308 }
9309 };
9310
9311 for host_path in paths {
9312 add_path(host_path.clone());
9313 if let Ok(realpath) = fs::canonicalize(host_path) {
9314 add_path(realpath);
9315 }
9316
9317 if host_path.file_name().and_then(|name| name.to_str()) != Some("node_modules") {
9318 continue;
9319 }
9320
9321 let mut current = host_path.parent();
9322 while let Some(parent) = current {
9323 let candidate = parent.join("node_modules");
9324 if candidate.exists() {
9325 add_path(candidate.clone());
9326 if let Ok(realpath) = fs::canonicalize(&candidate) {
9327 add_path(realpath);
9328 }
9329 }
9330 current = parent.parent();
9331 }
9332 }
9333
9334 expanded
9335}
9336
9337fn prepare_javascript_shadow(
9338 vm: &mut VmState,
9339 resolved: &ResolvedChildProcessExecution,
9340) -> Result<(), SidecarError> {
9341 let guest_entrypoint = resolved
9342 .env
9343 .get("AGENT_OS_GUEST_ENTRYPOINT")
9344 .cloned()
9345 .or_else(|| {
9353 resolve_host_entrypoint_within_vm_host_cwd(vm, &resolved.entrypoint)
9354 .map(|(guest_entrypoint, _)| guest_entrypoint)
9355 })
9356 .or_else(|| {
9357 resolved
9358 .entrypoint
9359 .starts_with('/')
9360 .then(|| normalize_path(&resolved.entrypoint))
9361 });
9362 let Some(guest_entrypoint) = guest_entrypoint else {
9363 return Ok(());
9364 };
9365 if host_mount_path_for_guest_path(vm, &guest_entrypoint).is_some() {
9366 return Ok(());
9367 }
9368 if vm.kernel.lstat(&guest_entrypoint).is_err() {
9369 let host_entrypoint = {
9370 let candidate = Path::new(&resolved.entrypoint);
9371 if candidate.is_absolute() {
9372 candidate.to_path_buf()
9373 } else {
9374 resolved.host_cwd.join(candidate)
9375 }
9376 };
9377 if host_entrypoint.exists() {
9378 materialize_host_path_to_shadow(vm, &guest_entrypoint, &host_entrypoint)?;
9379 return sync_shadow_entrypoint_into_kernel(vm, &guest_entrypoint);
9384 }
9385 }
9386 materialize_guest_path_to_shadow(vm, &guest_entrypoint)
9387}
9388
9389fn sync_shadow_entrypoint_into_kernel(
9394 vm: &mut VmState,
9395 guest_entrypoint: &str,
9396) -> Result<(), SidecarError> {
9397 if vm.kernel.exists(guest_entrypoint).unwrap_or(false) {
9398 return Ok(());
9399 }
9400 let shadow_path = shadow_path_for_guest(vm, guest_entrypoint);
9401 let bytes = match fs::read(&shadow_path) {
9402 Ok(bytes) => bytes,
9403 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
9404 Err(error) => {
9405 return Err(SidecarError::Io(format!(
9406 "failed to read staged shadow entrypoint {}: {error}",
9407 shadow_path.display()
9408 )));
9409 }
9410 };
9411 if let Some(parent) = guest_parent_path(guest_entrypoint) {
9412 if !vm.kernel.exists(&parent).unwrap_or(false) {
9413 vm.kernel.mkdir(&parent, true).map_err(kernel_error)?;
9414 }
9415 }
9416 vm.kernel
9417 .write_file(guest_entrypoint, bytes)
9418 .map_err(kernel_error)?;
9419 Ok(())
9420}
9421
9422fn guest_parent_path(guest_path: &str) -> Option<String> {
9423 let parent = Path::new(guest_path).parent()?;
9424 let parent = parent.to_string_lossy();
9425 if parent.is_empty() || parent == "/" {
9426 None
9427 } else {
9428 Some(parent.into_owned())
9429 }
9430}
9431
9432fn materialize_host_path_to_shadow(
9433 vm: &VmState,
9434 guest_path: &str,
9435 host_path: &Path,
9436) -> Result<(), SidecarError> {
9437 let shadow_path = shadow_path_for_guest(vm, guest_path);
9438 let metadata = fs::symlink_metadata(host_path)
9439 .map_err(|error| SidecarError::Io(format!("failed to stat host entrypoint: {error}")))?;
9440
9441 if metadata.file_type().is_symlink() {
9442 if let Some(parent) = shadow_path.parent() {
9443 fs::create_dir_all(parent).map_err(|error| {
9444 SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
9445 })?;
9446 }
9447 let _ = fs::remove_file(&shadow_path);
9448 let _ = fs::remove_dir_all(&shadow_path);
9449 let target = fs::read_link(host_path)
9450 .map_err(|error| SidecarError::Io(format!("failed to read host symlink: {error}")))?;
9451 std::os::unix::fs::symlink(&target, &shadow_path)
9452 .map_err(|error| SidecarError::Io(format!("failed to mirror host symlink: {error}")))?;
9453 return Ok(());
9454 }
9455
9456 if metadata.is_dir() {
9457 fs::create_dir_all(&shadow_path).map_err(|error| {
9458 SidecarError::Io(format!("failed to create shadow directory: {error}"))
9459 })?;
9460 fs::set_permissions(
9461 &shadow_path,
9462 fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
9463 )
9464 .map_err(|error| {
9465 SidecarError::Io(format!(
9466 "failed to set shadow directory mode on {}: {error}",
9467 shadow_path.display()
9468 ))
9469 })?;
9470 return Ok(());
9471 }
9472
9473 if let Some(parent) = shadow_path.parent() {
9474 fs::create_dir_all(parent).map_err(|error| {
9475 SidecarError::Io(format!("failed to create shadow parent: {error}"))
9476 })?;
9477 }
9478 let bytes = fs::read(host_path)
9479 .map_err(|error| SidecarError::Io(format!("failed to read host entrypoint: {error}")))?;
9480 fs::write(&shadow_path, bytes).map_err(|error| {
9481 SidecarError::Io(format!(
9482 "failed to mirror host file into shadow root: {error}"
9483 ))
9484 })?;
9485 fs::set_permissions(
9486 &shadow_path,
9487 fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
9488 )
9489 .map_err(|error| {
9490 SidecarError::Io(format!(
9491 "failed to set shadow file mode on {}: {error}",
9492 shadow_path.display()
9493 ))
9494 })?;
9495 Ok(())
9496}
9497
9498fn materialize_guest_path_to_shadow(
9499 vm: &mut VmState,
9500 guest_path: &str,
9501) -> Result<(), SidecarError> {
9502 let stat = vm.kernel.lstat(guest_path).map_err(kernel_error)?;
9503 let shadow_path = shadow_path_for_guest(vm, guest_path);
9504
9505 if stat.is_symbolic_link {
9506 if let Some(parent) = shadow_path.parent() {
9507 fs::create_dir_all(parent).map_err(|error| {
9508 SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
9509 })?;
9510 }
9511 let _ = fs::remove_file(&shadow_path);
9512 let _ = fs::remove_dir_all(&shadow_path);
9513 let target = vm.kernel.read_link(guest_path).map_err(kernel_error)?;
9514 std::os::unix::fs::symlink(&target, &shadow_path)
9515 .map_err(|error| SidecarError::Io(format!("failed to mirror symlink: {error}")))?;
9516 return Ok(());
9517 }
9518
9519 if stat.is_directory {
9520 fs::create_dir_all(&shadow_path).map_err(|error| {
9521 SidecarError::Io(format!("failed to create shadow directory: {error}"))
9522 })?;
9523 fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
9524 |error| {
9525 SidecarError::Io(format!(
9526 "failed to set shadow directory mode on {}: {error}",
9527 shadow_path.display()
9528 ))
9529 },
9530 )?;
9531 return Ok(());
9532 }
9533
9534 if let Some(parent) = shadow_path.parent() {
9535 fs::create_dir_all(parent).map_err(|error| {
9536 SidecarError::Io(format!("failed to create shadow parent: {error}"))
9537 })?;
9538 }
9539 let bytes = vm.kernel.read_file(guest_path).map_err(kernel_error)?;
9540 fs::write(&shadow_path, bytes).map_err(|error| {
9541 SidecarError::Io(format!(
9542 "failed to mirror guest file into shadow root: {error}"
9543 ))
9544 })?;
9545 fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
9546 |error| {
9547 SidecarError::Io(format!(
9548 "failed to set shadow file mode on {}: {error}",
9549 shadow_path.display()
9550 ))
9551 },
9552 )?;
9553 Ok(())
9554}
9555
9556fn load_javascript_entrypoint_source(
9557 vm: &mut VmState,
9558 host_cwd: &Path,
9559 entrypoint: &str,
9560 env: &BTreeMap<String, String>,
9561) -> Option<String> {
9562 let mut read_guest_file = |path: &str| {
9563 vm.kernel
9564 .read_file(path)
9565 .ok()
9566 .and_then(|bytes| String::from_utf8(bytes).ok())
9567 };
9568
9569 if let Some(source) = env
9570 .get("AGENT_OS_GUEST_ENTRYPOINT")
9571 .filter(|path| path.starts_with('/'))
9572 .and_then(|path| read_guest_file(path))
9573 {
9574 return Some(source);
9575 }
9576
9577 if entrypoint.starts_with('/') {
9578 if let Some(source) = read_guest_file(entrypoint) {
9579 return Some(source);
9580 }
9581 }
9582
9583 let host_entrypoint = if Path::new(entrypoint).is_absolute() {
9584 PathBuf::from(entrypoint)
9585 } else {
9586 host_cwd.join(entrypoint)
9587 };
9588 let normalized_entrypoint = normalize_host_path(&host_entrypoint);
9589 let sandbox_root = normalize_host_path(&vm.cwd);
9590 let host_cwd = normalize_host_path(&vm.host_cwd);
9591 if !path_is_within_root(&normalized_entrypoint, &sandbox_root)
9592 && !path_is_within_root(&normalized_entrypoint, &host_cwd)
9593 {
9594 return None;
9595 }
9596
9597 fs::read_to_string(&normalized_entrypoint).ok()
9598}
9599
9600fn emit_dns_resolution_event<B>(
9601 bridge: &SharedBridge<B>,
9602 vm_id: &str,
9603 hostname: &str,
9604 source: KernelDnsResolutionSource,
9605 addresses: &[IpAddr],
9606 dns: &VmDnsConfig,
9607) where
9608 B: NativeSidecarBridge + Send + 'static,
9609 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9610{
9611 let _ = emit_structured_event(
9612 bridge,
9613 vm_id,
9614 "network.dns.resolved",
9615 audit_fields([
9616 ("hostname", hostname.to_owned()),
9617 ("source", source.as_str().to_owned()),
9618 (
9619 "addresses",
9620 addresses
9621 .iter()
9622 .map(ToString::to_string)
9623 .collect::<Vec<_>>()
9624 .join(","),
9625 ),
9626 ("address_count", addresses.len().to_string()),
9627 ("resolver_count", dns.name_servers.len().to_string()),
9628 (
9629 "resolvers",
9630 dns.name_servers
9631 .iter()
9632 .map(ToString::to_string)
9633 .collect::<Vec<_>>()
9634 .join(","),
9635 ),
9636 ]),
9637 );
9638}
9639
9640fn emit_dns_record_resolution_event<B>(
9641 bridge: &SharedBridge<B>,
9642 vm_id: &str,
9643 hostname: &str,
9644 resolution: &DnsRecordResolution,
9645 dns: &VmDnsConfig,
9646) where
9647 B: NativeSidecarBridge + Send + 'static,
9648 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9649{
9650 if let Some(addresses) = dns_resolution_ip_addrs(resolution.records()) {
9651 emit_dns_resolution_event(
9652 bridge,
9653 vm_id,
9654 hostname,
9655 resolution.source(),
9656 &addresses,
9657 dns,
9658 );
9659 return;
9660 }
9661
9662 let _ = emit_structured_event(
9663 bridge,
9664 vm_id,
9665 "network.dns.resolved",
9666 audit_fields([
9667 ("hostname", hostname.to_owned()),
9668 ("source", resolution.source().as_str().to_owned()),
9669 (
9670 "addresses",
9671 resolution
9672 .records()
9673 .iter()
9674 .map(summarize_dns_record)
9675 .collect::<Vec<_>>()
9676 .join(","),
9677 ),
9678 ("address_count", resolution.records().len().to_string()),
9679 ("resolver_count", dns.name_servers.len().to_string()),
9680 (
9681 "resolvers",
9682 dns.name_servers
9683 .iter()
9684 .map(ToString::to_string)
9685 .collect::<Vec<_>>()
9686 .join(","),
9687 ),
9688 ]),
9689 );
9690}
9691
9692fn emit_dns_resolution_failure_event<B>(
9693 bridge: &SharedBridge<B>,
9694 vm_id: &str,
9695 hostname: &str,
9696 dns: &VmDnsConfig,
9697 error: &SidecarError,
9698) where
9699 B: NativeSidecarBridge + Send + 'static,
9700 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9701{
9702 let _ = emit_structured_event(
9703 bridge,
9704 vm_id,
9705 "network.dns.resolve_failed",
9706 audit_fields([
9707 ("hostname", hostname.to_owned()),
9708 ("reason", error.to_string()),
9709 ("resolver_count", dns.name_servers.len().to_string()),
9710 (
9711 "resolvers",
9712 dns.name_servers
9713 .iter()
9714 .map(ToString::to_string)
9715 .collect::<Vec<_>>()
9716 .join(","),
9717 ),
9718 ]),
9719 );
9720}
9721
9722fn parse_dns_record_type(rrtype: &str) -> Result<RecordType, SidecarError> {
9723 match rrtype {
9724 "A" => Ok(RecordType::A),
9725 "AAAA" => Ok(RecordType::AAAA),
9726 "MX" => Ok(RecordType::MX),
9727 "TXT" => Ok(RecordType::TXT),
9728 "SRV" => Ok(RecordType::SRV),
9729 "CNAME" => Ok(RecordType::CNAME),
9730 "PTR" => Ok(RecordType::PTR),
9731 "NS" => Ok(RecordType::NS),
9732 "SOA" => Ok(RecordType::SOA),
9733 "NAPTR" => Ok(RecordType::NAPTR),
9734 "CAA" => Ok(RecordType::CAA),
9735 "ANY" => Ok(RecordType::ANY),
9736 other => Err(SidecarError::Execution(format!(
9737 "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
9738 ))),
9739 }
9740}
9741
9742fn dns_resolution_to_node_value(
9743 resolution: &DnsRecordResolution,
9744 requested_type: &str,
9745) -> Result<Value, SidecarError> {
9746 let safe_ips = dns_resolution_safe_ip_set(resolution.records(), resolution.hostname())?;
9747 match requested_type {
9748 "A" | "AAAA" => Ok(Value::Array(
9749 resolution
9750 .records()
9751 .iter()
9752 .filter_map(|record| dns_record_ip_string(record, &safe_ips))
9753 .map(Value::String)
9754 .collect(),
9755 )),
9756 "MX" => Ok(Value::Array(
9757 resolution
9758 .records()
9759 .iter()
9760 .filter_map(|record| match record.data() {
9761 RData::MX(mx) => Some(json!({
9762 "priority": mx.preference,
9763 "exchange": normalize_dns_name_for_node(&mx.exchange),
9764 "type": "MX",
9765 })),
9766 _ => None,
9767 })
9768 .collect(),
9769 )),
9770 "TXT" => Ok(Value::Array(
9771 resolution
9772 .records()
9773 .iter()
9774 .filter_map(|record| match record.data() {
9775 RData::TXT(txt) => Some(Value::Array(
9776 txt.txt_data
9777 .iter()
9778 .map(|entry| Value::String(String::from_utf8_lossy(entry).into_owned()))
9779 .collect(),
9780 )),
9781 _ => None,
9782 })
9783 .collect(),
9784 )),
9785 "SRV" => Ok(Value::Array(
9786 resolution
9787 .records()
9788 .iter()
9789 .filter_map(|record| match record.data() {
9790 RData::SRV(srv) => Some(json!({
9791 "priority": srv.priority,
9792 "weight": srv.weight,
9793 "port": srv.port,
9794 "name": normalize_dns_name_for_node(&srv.target),
9795 "type": "SRV",
9796 })),
9797 _ => None,
9798 })
9799 .collect(),
9800 )),
9801 "CNAME" => Ok(Value::Array(
9802 resolution
9803 .records()
9804 .iter()
9805 .filter_map(|record| match record.data() {
9806 RData::CNAME(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9807 _ => None,
9808 })
9809 .collect(),
9810 )),
9811 "PTR" => Ok(Value::Array(
9812 resolution
9813 .records()
9814 .iter()
9815 .filter_map(|record| match record.data() {
9816 RData::PTR(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9817 _ => None,
9818 })
9819 .collect(),
9820 )),
9821 "NS" => Ok(Value::Array(
9822 resolution
9823 .records()
9824 .iter()
9825 .filter_map(|record| match record.data() {
9826 RData::NS(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9827 _ => None,
9828 })
9829 .collect(),
9830 )),
9831 "SOA" => resolution
9832 .records()
9833 .iter()
9834 .find_map(|record| match record.data() {
9835 RData::SOA(soa) => Some(json!({
9836 "nsname": normalize_dns_name_for_node(&soa.mname),
9837 "hostmaster": normalize_dns_name_for_node(&soa.rname),
9838 "serial": soa.serial,
9839 "refresh": soa.refresh,
9840 "retry": soa.retry,
9841 "expire": soa.expire,
9842 "minttl": soa.minimum,
9843 })),
9844 _ => None,
9845 })
9846 .ok_or_else(|| {
9847 SidecarError::Execution(String::from("failed to resolve DNS SOA record"))
9848 }),
9849 "NAPTR" => Ok(Value::Array(
9850 resolution
9851 .records()
9852 .iter()
9853 .filter_map(|record| match record.data() {
9854 RData::NAPTR(naptr) => Some(json!({
9855 "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
9856 "service": String::from_utf8_lossy(&naptr.services).into_owned(),
9857 "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
9858 "replacement": normalize_dns_name_for_node(&naptr.replacement),
9859 "order": naptr.order,
9860 "preference": naptr.preference,
9861 })),
9862 _ => None,
9863 })
9864 .collect(),
9865 )),
9866 "CAA" => Ok(Value::Array(
9867 resolution
9868 .records()
9869 .iter()
9870 .filter_map(|record| match record.data() {
9871 RData::CAA(caa) => {
9872 let mut value = serde_json::Map::new();
9873 value.insert(
9874 "critical".to_owned(),
9875 Value::from(u8::from(caa.issuer_critical)),
9876 );
9877 value.insert("type".to_owned(), Value::String(String::from("CAA")));
9878 if caa.tag.eq_ignore_ascii_case("iodef") {
9879 value.insert(
9880 "iodef".to_owned(),
9881 Value::String(
9882 caa.value_as_iodef()
9883 .map(|url| url.to_string())
9884 .unwrap_or_else(|_| {
9885 String::from_utf8_lossy(&caa.value).into_owned()
9886 }),
9887 ),
9888 );
9889 } else if let Ok((issuer, _params)) = caa.value_as_issue() {
9890 let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
9891 "issuewild"
9892 } else {
9893 "issue"
9894 };
9895 value.insert(
9896 field.to_owned(),
9897 Value::String(
9898 issuer.as_ref().map(ToString::to_string).unwrap_or_else(|| {
9899 String::from_utf8_lossy(&caa.value).into_owned()
9900 }),
9901 ),
9902 );
9903 } else {
9904 value.insert(
9905 caa.tag.to_ascii_lowercase(),
9906 Value::String(String::from_utf8_lossy(&caa.value).into_owned()),
9907 );
9908 }
9909 Some(Value::Object(value))
9910 }
9911 _ => None,
9912 })
9913 .collect(),
9914 )),
9915 "ANY" => Ok(Value::Array(
9916 resolution
9917 .records()
9918 .iter()
9919 .filter_map(|record| dns_any_record_to_value(record, &safe_ips))
9920 .collect(),
9921 )),
9922 other => Err(SidecarError::Execution(format!(
9923 "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
9924 ))),
9925 }
9926}
9927
9928fn dns_resolution_safe_ip_set(
9929 records: &[Record],
9930 hostname: &str,
9931) -> Result<BTreeSet<IpAddr>, SidecarError> {
9932 let ips = records
9933 .iter()
9934 .filter_map(dns_record_ip_addr)
9935 .collect::<Vec<_>>();
9936 if ips.is_empty() {
9937 return Ok(BTreeSet::new());
9938 }
9939 Ok(filter_dns_safe_ip_addrs(ips, hostname)?
9940 .into_iter()
9941 .collect())
9942}
9943
9944fn dns_resolution_ip_addrs(records: &[Record]) -> Option<Vec<IpAddr>> {
9945 let ips = records
9946 .iter()
9947 .filter_map(dns_record_ip_addr)
9948 .collect::<Vec<_>>();
9949 if ips.is_empty() {
9950 return None;
9951 }
9952 Some(ips)
9953}
9954
9955fn dns_record_ip_addr(record: &Record) -> Option<IpAddr> {
9956 match record.data() {
9957 RData::A(address) => Some(IpAddr::V4(**address)),
9958 RData::AAAA(address) => Some(IpAddr::V6(**address)),
9959 _ => None,
9960 }
9961}
9962
9963fn dns_record_ip_string(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<String> {
9964 let ip = dns_record_ip_addr(record)?;
9965 safe_ips.contains(&ip).then(|| ip.to_string())
9966}
9967
9968fn dns_any_record_to_value(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<Value> {
9969 let value = match record.data() {
9970 RData::A(_) | RData::AAAA(_) => json!({
9971 "address": dns_record_ip_string(record, safe_ips)?,
9972 "ttl": record.ttl(),
9973 "type": record.record_type().to_string(),
9974 }),
9975 RData::MX(mx) => json!({
9976 "exchange": normalize_dns_name_for_node(&mx.exchange),
9977 "priority": mx.preference,
9978 "type": "MX",
9979 }),
9980 RData::TXT(txt) => json!({
9981 "entries": txt
9982 .txt_data
9983 .iter()
9984 .map(|entry| String::from_utf8_lossy(entry).into_owned())
9985 .collect::<Vec<_>>(),
9986 "type": "TXT",
9987 }),
9988 RData::SRV(srv) => json!({
9989 "name": normalize_dns_name_for_node(&srv.target),
9990 "port": srv.port,
9991 "priority": srv.priority,
9992 "weight": srv.weight,
9993 "type": "SRV",
9994 }),
9995 RData::CNAME(name) => json!({
9996 "value": normalize_dns_name_for_node(&name.0),
9997 "type": "CNAME",
9998 }),
9999 RData::PTR(name) => json!({
10000 "value": normalize_dns_name_for_node(&name.0),
10001 "type": "PTR",
10002 }),
10003 RData::NS(name) => json!({
10004 "value": normalize_dns_name_for_node(&name.0),
10005 "type": "NS",
10006 }),
10007 RData::SOA(soa) => json!({
10008 "nsname": normalize_dns_name_for_node(&soa.mname),
10009 "hostmaster": normalize_dns_name_for_node(&soa.rname),
10010 "serial": soa.serial,
10011 "refresh": soa.refresh,
10012 "retry": soa.retry,
10013 "expire": soa.expire,
10014 "minttl": soa.minimum,
10015 "type": "SOA",
10016 }),
10017 RData::NAPTR(naptr) => json!({
10018 "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
10019 "service": String::from_utf8_lossy(&naptr.services).into_owned(),
10020 "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
10021 "replacement": normalize_dns_name_for_node(&naptr.replacement),
10022 "order": naptr.order,
10023 "preference": naptr.preference,
10024 "type": "NAPTR",
10025 }),
10026 RData::CAA(caa) => {
10027 let mut value = serde_json::Map::new();
10028 value.insert(
10029 "critical".to_owned(),
10030 Value::from(u8::from(caa.issuer_critical)),
10031 );
10032 value.insert("type".to_owned(), Value::String(String::from("CAA")));
10033 if caa.tag.eq_ignore_ascii_case("iodef") {
10034 value.insert(
10035 "iodef".to_owned(),
10036 Value::String(
10037 caa.value_as_iodef()
10038 .map(|url| url.to_string())
10039 .unwrap_or_else(|_| String::from_utf8_lossy(&caa.value).into_owned()),
10040 ),
10041 );
10042 } else if let Ok((issuer, _params)) = caa.value_as_issue() {
10043 let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
10044 "issuewild"
10045 } else {
10046 "issue"
10047 };
10048 value.insert(
10049 field.to_owned(),
10050 Value::String(
10051 issuer
10052 .as_ref()
10053 .map(ToString::to_string)
10054 .unwrap_or_else(|| String::from_utf8_lossy(&caa.value).into_owned()),
10055 ),
10056 );
10057 }
10058 Value::Object(value)
10059 }
10060 _ => return None,
10061 };
10062 Some(value)
10063}
10064
10065fn normalize_dns_name_for_node(name: &impl ToString) -> String {
10066 name.to_string().trim_end_matches('.').to_owned()
10067}
10068
10069fn summarize_dns_record(record: &Record) -> String {
10070 match record.data() {
10071 RData::A(_) | RData::AAAA(_) => record.data().to_string(),
10072 _ => format!("{} {}", record.record_type(), record.data()),
10073 }
10074}
10075
10076fn find_socket_state_entry(
10084 vm: Option<&VmState>,
10085 kind: SocketQueryKind,
10086 request: &FindListenerRequest,
10087) -> Result<Option<SocketStateEntry>, SidecarError> {
10088 let vm = vm.ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
10089
10090 for (process_id, process) in &vm.active_processes {
10091 if let Some(path) = request.path.as_deref() {
10092 if matches!(kind, SocketQueryKind::TcpListener) {
10093 for listener in process.unix_listeners.values() {
10094 if listener.path() != path {
10095 continue;
10096 }
10097 return Ok(Some(SocketStateEntry {
10098 process_id: process_id.to_owned(),
10099 host: None,
10100 port: None,
10101 path: Some(path.to_owned()),
10102 }));
10103 }
10104 }
10105 }
10106
10107 if request.path.is_none() {
10108 if let Some(entry) =
10109 find_kernel_socket_state_entry(&vm.kernel, process_id, process, kind, request)?
10110 {
10111 return Ok(Some(entry));
10112 }
10113
10114 match kind {
10115 SocketQueryKind::TcpListener => {
10116 for server in process.http_servers.values() {
10117 let local_addr = server.guest_local_addr;
10118 let local_host = local_addr.ip().to_string();
10119 if !socket_host_matches(request.host.as_deref(), &local_host) {
10120 continue;
10121 }
10122 if let Some(port) = request.port {
10123 if local_addr.port() != port {
10124 continue;
10125 }
10126 }
10127 return Ok(Some(SocketStateEntry {
10128 process_id: process_id.to_owned(),
10129 host: Some(local_host),
10130 port: Some(local_addr.port()),
10131 path: None,
10132 }));
10133 }
10134
10135 for listener in process.tcp_listeners.values() {
10136 if listener.kernel_socket_id.is_some() {
10137 continue;
10138 }
10139 let local_addr = listener.guest_local_addr();
10140 let local_host = local_addr.ip().to_string();
10141 if !socket_host_matches(request.host.as_deref(), &local_host) {
10142 continue;
10143 }
10144 if let Some(port) = request.port {
10145 if local_addr.port() != port {
10146 continue;
10147 }
10148 }
10149 return Ok(Some(SocketStateEntry {
10150 process_id: process_id.to_owned(),
10151 host: Some(local_host),
10152 port: Some(local_addr.port()),
10153 path: None,
10154 }));
10155 }
10156 }
10157 SocketQueryKind::UdpBound => {
10158 for socket in process.udp_sockets.values() {
10159 if socket.kernel_socket_id.is_some() {
10160 continue;
10161 }
10162 let Some(local_addr) = socket.local_addr() else {
10163 continue;
10164 };
10165 let local_host = local_addr.ip().to_string();
10166 if !socket_host_matches(request.host.as_deref(), &local_host) {
10167 continue;
10168 }
10169 if let Some(port) = request.port {
10170 if local_addr.port() != port {
10171 continue;
10172 }
10173 }
10174 return Ok(Some(SocketStateEntry {
10175 process_id: process_id.to_owned(),
10176 host: Some(local_host),
10177 port: Some(local_addr.port()),
10178 path: None,
10179 }));
10180 }
10181 }
10182 }
10183 }
10184
10185 let child_pid = process.execution.child_pid();
10186 let inodes = socket_inodes_for_pid(child_pid)?;
10187 if inodes.is_empty() {
10188 continue;
10189 }
10190
10191 if let Some(path) = request.path.as_deref() {
10192 if let Some(listener) = find_unix_socket_for_pid(child_pid, &inodes, path, process_id)?
10193 {
10194 return Ok(Some(listener));
10195 }
10196 continue;
10197 }
10198
10199 let table_paths = match kind {
10200 SocketQueryKind::TcpListener => [
10201 format!("/proc/{child_pid}/net/tcp"),
10202 format!("/proc/{child_pid}/net/tcp6"),
10203 ],
10204 SocketQueryKind::UdpBound => [
10205 format!("/proc/{child_pid}/net/udp"),
10206 format!("/proc/{child_pid}/net/udp6"),
10207 ],
10208 };
10209 for table_path in table_paths {
10210 if let Some(entry) = find_inet_socket_for_pid(
10211 &table_path,
10212 &inodes,
10213 kind,
10214 request.host.as_deref(),
10215 request.port,
10216 process_id,
10217 )? {
10218 return Ok(Some(entry));
10219 }
10220 }
10221 }
10222
10223 Ok(None)
10224}
10225
10226fn require_vm_inspection_permission<B>(
10227 bridge: &SharedBridge<B>,
10228 vm_id: &str,
10229 capability: &str,
10230 domain: &str,
10231 resource: &str,
10232) -> Result<(), SidecarError>
10233where
10234 B: NativeSidecarBridge + Send + 'static,
10235 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
10236{
10237 let decision = bridge.static_permission_decision(vm_id, capability, domain, Some(resource));
10238 if decision.as_ref().is_some_and(|decision| decision.allow) {
10239 return Ok(());
10240 }
10241
10242 let reason = decision
10243 .and_then(|decision| decision.reason)
10244 .unwrap_or_else(|| format!("{capability} permission required"));
10245 Err(SidecarError::Execution(format!(
10246 "EACCES: permission denied, {resource}: {reason}"
10247 )))
10248}
10249
10250fn socket_query_resource(kind: SocketQueryKind, request: &FindListenerRequest) -> String {
10251 if let Some(path) = request.path.as_deref() {
10252 return format!("unix://{path}");
10253 }
10254
10255 let host = request.host.as_deref().unwrap_or("*");
10256 let port = request
10257 .port
10258 .map_or_else(|| String::from("*"), |port| port.to_string());
10259 match kind {
10260 SocketQueryKind::TcpListener => format!("tcp://{host}:{port}"),
10261 SocketQueryKind::UdpBound => format!("udp://{host}:{port}"),
10262 }
10263}
10264
10265fn snapshot_vm_processes(vm: &VmState) -> Vec<ProcessSnapshotEntry> {
10266 let process_table = vm.kernel.list_processes();
10267 snapshot_vm_processes_inner(vm, &process_table)
10268}
10269
10270fn snapshot_vm_processes_inner(
10271 vm: &VmState,
10272 process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
10273) -> Vec<ProcessSnapshotEntry> {
10274 let mut entries = Vec::new();
10275
10276 for (process_id, process) in &vm.active_processes {
10277 collect_process_snapshot_entries(process_id, process, process_table, &mut entries);
10278 }
10279
10280 for exited in &vm.exited_process_snapshots {
10281 entries.push(exited.process.clone());
10282 }
10283
10284 entries
10285}
10286
10287fn prune_exited_process_snapshots(vm: &mut VmState) {
10288 let cutoff = Instant::now() - EXITED_PROCESS_SNAPSHOT_RETENTION;
10289 while vm
10290 .exited_process_snapshots
10291 .front()
10292 .is_some_and(|snapshot| snapshot.captured_at < cutoff)
10293 {
10294 vm.exited_process_snapshots.pop_front();
10295 }
10296}
10297
10298fn build_process_snapshot_entry(
10299 process_id: &str,
10300 process: &ActiveProcess,
10301 info: &secure_exec_kernel::process_table::ProcessInfo,
10302 exit_code: Option<i32>,
10303) -> ProcessSnapshotEntry {
10304 ProcessSnapshotEntry {
10305 process_id: process_id.to_owned(),
10306 pid: info.pid,
10307 ppid: info.ppid,
10308 pgid: info.pgid,
10309 sid: info.sid,
10310 driver: info.driver.clone(),
10311 command: info.command.clone(),
10312 args: Vec::new(),
10313 cwd: process.guest_cwd.clone(),
10314 status: if exit_code.is_some() {
10315 ProcessSnapshotStatus::Exited
10316 } else {
10317 match info.status {
10318 ProcessStatus::Running => ProcessSnapshotStatus::Running,
10319 ProcessStatus::Stopped => ProcessSnapshotStatus::Stopped,
10320 ProcessStatus::Exited => ProcessSnapshotStatus::Exited,
10321 }
10322 },
10323 exit_code: exit_code.or(info.exit_code),
10324 }
10325}
10326
10327fn collect_process_snapshot_entries(
10328 process_id: &str,
10329 process: &ActiveProcess,
10330 process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
10331 entries: &mut Vec<ProcessSnapshotEntry>,
10332) {
10333 if let Some(info) = process_table.get(&process.kernel_pid) {
10334 entries.push(build_process_snapshot_entry(
10335 process_id, process, info, None,
10336 ));
10337 }
10338
10339 for (child_id, child) in &process.child_processes {
10340 let child_process_id = format!("{process_id}/{child_id}");
10341 collect_process_snapshot_entries(&child_process_id, child, process_table, entries);
10342 }
10343}
10344
10345fn find_kernel_socket_state_entry(
10346 kernel: &SidecarKernel,
10347 process_id: &str,
10348 process: &ActiveProcess,
10349 kind: SocketQueryKind,
10350 request: &FindListenerRequest,
10351) -> Result<Option<SocketStateEntry>, SidecarError> {
10352 let entry = match kind {
10353 SocketQueryKind::TcpListener => process
10354 .tcp_listeners
10355 .values()
10356 .filter_map(|listener| listener.kernel_socket_id)
10357 .find_map(|socket_id| {
10358 kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
10359 }),
10360 SocketQueryKind::UdpBound => process
10361 .udp_sockets
10362 .values()
10363 .filter_map(|socket| socket.kernel_socket_id)
10364 .find_map(|socket_id| {
10365 kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
10366 }),
10367 };
10368
10369 if entry.is_some() {
10370 return Ok(entry);
10371 }
10372
10373 for child in process.child_processes.values() {
10374 if let Some(entry) =
10375 find_kernel_socket_state_entry(kernel, process_id, child, kind, request)?
10376 {
10377 return Ok(Some(entry));
10378 }
10379 }
10380
10381 Ok(None)
10382}
10383
10384fn kernel_socket_state_entry(
10385 kernel: &SidecarKernel,
10386 process_id: &str,
10387 socket_id: SocketId,
10388 kind: SocketQueryKind,
10389 request: &FindListenerRequest,
10390) -> Option<SocketStateEntry> {
10391 let record = kernel.socket_get(socket_id)?;
10392 let local_address = record.local_address()?;
10393 match kind {
10394 SocketQueryKind::TcpListener if record.state() == SocketState::Listening => {}
10395 SocketQueryKind::TcpListener => return None,
10396 SocketQueryKind::UdpBound => {}
10397 }
10398
10399 if !socket_host_matches(request.host.as_deref(), local_address.host()) {
10400 return None;
10401 }
10402 if request
10403 .port
10404 .is_some_and(|port| local_address.port() != port)
10405 {
10406 return None;
10407 }
10408
10409 Some(SocketStateEntry {
10410 process_id: process_id.to_owned(),
10411 host: Some(local_address.host().to_owned()),
10412 port: Some(local_address.port()),
10413 path: None,
10414 })
10415}
10416
10417fn socket_inodes_for_pid(pid: u32) -> Result<BTreeSet<u64>, SidecarError> {
10418 let fd_dir = PathBuf::from(format!("/proc/{pid}/fd"));
10419 let entries = match fs::read_dir(&fd_dir) {
10420 Ok(entries) => entries,
10421 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(BTreeSet::new()),
10422 Err(error) => {
10423 return Err(SidecarError::Io(format!(
10424 "failed to read socket descriptors for process {pid}: {error}"
10425 )));
10426 }
10427 };
10428
10429 let mut inodes = BTreeSet::new();
10430 for entry in entries {
10431 let entry = entry.map_err(|error| {
10432 SidecarError::Io(format!(
10433 "failed to inspect fd entry for process {pid}: {error}"
10434 ))
10435 })?;
10436 let target = match fs::read_link(entry.path()) {
10437 Ok(target) => target,
10438 Err(_) => continue,
10439 };
10440 if let Some(inode) = parse_socket_inode(&target) {
10441 inodes.insert(inode);
10442 }
10443 }
10444
10445 Ok(inodes)
10446}
10447
10448fn parse_socket_inode(target: &Path) -> Option<u64> {
10449 let value = target.to_string_lossy();
10450 let trimmed = value.strip_prefix("socket:[")?.strip_suffix(']')?;
10451 trimmed.parse().ok()
10452}
10453
10454fn unix_socket_path(addr: &UnixSocketAddr) -> Option<String> {
10455 addr.as_pathname()
10456 .map(|path| path.to_string_lossy().into_owned())
10457}
10458
10459fn find_unix_socket_for_pid(
10460 pid: u32,
10461 inodes: &BTreeSet<u64>,
10462 path: &str,
10463 process_id: &str,
10464) -> Result<Option<SocketStateEntry>, SidecarError> {
10465 let table_path = format!("/proc/{pid}/net/unix");
10466 let contents = match fs::read_to_string(&table_path) {
10467 Ok(contents) => contents,
10468 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
10469 Err(error) => {
10470 return Err(SidecarError::Io(format!(
10471 "failed to inspect unix sockets for process {pid}: {error}"
10472 )));
10473 }
10474 };
10475
10476 for line in contents.lines().skip(1) {
10477 let columns = line.split_whitespace().collect::<Vec<_>>();
10478 if columns.len() < 8 {
10479 continue;
10480 }
10481 let Ok(inode) = columns[6].parse::<u64>() else {
10482 continue;
10483 };
10484 if !inodes.contains(&inode) || columns[7] != path {
10485 continue;
10486 }
10487 return Ok(Some(SocketStateEntry {
10488 process_id: process_id.to_owned(),
10489 host: None,
10490 port: None,
10491 path: Some(path.to_owned()),
10492 }));
10493 }
10494
10495 Ok(None)
10496}
10497
10498fn find_inet_socket_for_pid(
10499 table_path: &str,
10500 inodes: &BTreeSet<u64>,
10501 kind: SocketQueryKind,
10502 requested_host: Option<&str>,
10503 requested_port: Option<u16>,
10504 process_id: &str,
10505) -> Result<Option<SocketStateEntry>, SidecarError> {
10506 for entry in parse_proc_net_entries(table_path)? {
10507 if !inodes.contains(&entry.inode) {
10508 continue;
10509 }
10510 if matches!(kind, SocketQueryKind::TcpListener) && entry.state != "0A" {
10511 continue;
10512 }
10513 if !socket_host_matches(requested_host, &entry.local_host) {
10514 continue;
10515 }
10516 if let Some(port) = requested_port {
10517 if entry.local_port != port {
10518 continue;
10519 }
10520 }
10521 return Ok(Some(SocketStateEntry {
10522 process_id: process_id.to_owned(),
10523 host: Some(entry.local_host),
10524 port: Some(entry.local_port),
10525 path: None,
10526 }));
10527 }
10528
10529 Ok(None)
10530}
10531
10532fn is_unspecified_socket_host(host: &str) -> bool {
10533 host == "0.0.0.0" || host == "::"
10534}
10535
10536fn is_loopback_socket_host(host: &str) -> bool {
10537 host == "127.0.0.1" || host == "::1" || host.eq_ignore_ascii_case("localhost")
10538}
10539
10540pub(crate) fn vm_network_resource_counts(vm: &VmState) -> NetworkResourceCounts {
10541 let snapshot = vm.kernel.resource_snapshot();
10542 let mut counts = NetworkResourceCounts {
10543 sockets: snapshot.sockets,
10544 connections: snapshot.socket_connections,
10545 };
10546 for process in vm.active_processes.values() {
10547 let process_counts = process.sidecar_only_network_resource_counts();
10548 counts.sockets += process_counts.sockets;
10549 counts.connections += process_counts.connections;
10550 }
10551 counts
10552}
10553
10554fn collect_javascript_socket_port_state(
10555 kernel: &SidecarKernel,
10556 process_id: &str,
10557 process: &ActiveProcess,
10558 tcp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10559 http_loopback_targets: &mut BTreeMap<
10560 (JavascriptSocketFamily, u16),
10561 JavascriptHttpLoopbackTarget,
10562 >,
10563 udp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10564 udp_host_to_guest: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10565 used_tcp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10566 used_udp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10567) {
10568 for (family, port) in process.tcp_port_reservations.values() {
10569 used_tcp_ports.entry(*family).or_default().insert(*port);
10570 }
10571
10572 let mut record_tcp_listener = |guest_addr: SocketAddr, host_port: u16| {
10573 let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10574 used_tcp_ports
10575 .entry(family)
10576 .or_default()
10577 .insert(guest_addr.port());
10578 tcp_guest_to_host.insert((family, guest_addr.port()), host_port);
10581 };
10582
10583 for listener in process.tcp_listeners.values() {
10584 let local_addr = listener
10585 .kernel_socket_id
10586 .and_then(|socket_id| kernel.socket_get(socket_id))
10587 .and_then(|record| record.local_address().cloned())
10588 .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
10589 .unwrap_or_else(|| listener.guest_local_addr());
10590 record_tcp_listener(local_addr, local_addr.port());
10591 }
10592
10593 for (server_id, server) in &process.http_servers {
10594 let host_port = match server.listener.local_addr() {
10595 Ok(addr) => addr.port(),
10596 Err(_) => continue,
10597 };
10598 record_tcp_listener(server.guest_local_addr, host_port);
10599 let family = JavascriptSocketFamily::from_ip(server.guest_local_addr.ip());
10600 http_loopback_targets.insert(
10601 (family, server.guest_local_addr.port()),
10602 JavascriptHttpLoopbackTarget {
10603 process_id: process_id.to_owned(),
10604 server_id: *server_id,
10605 },
10606 );
10607 }
10608
10609 if let Ok(http2) = process.http2.shared.lock() {
10610 for server in http2.servers.values() {
10611 record_tcp_listener(server.guest_local_addr, server.actual_local_addr.port());
10612 }
10613 }
10614
10615 for socket in process.tcp_sockets.values() {
10616 let guest_addr = socket
10617 .kernel_socket_id
10618 .and_then(|socket_id| kernel.socket_get(socket_id))
10619 .and_then(|record| record.local_address().cloned())
10620 .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
10621 .unwrap_or(socket.guest_local_addr);
10622 let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10623 used_tcp_ports
10624 .entry(family)
10625 .or_default()
10626 .insert(guest_addr.port());
10627 }
10628
10629 for socket in process.udp_sockets.values() {
10630 let guest_addr = socket
10631 .kernel_socket_id
10632 .and_then(|socket_id| kernel.socket_get(socket_id))
10633 .and_then(|record| record.local_address().cloned())
10634 .and_then(|address| {
10635 resolve_udp_bind_addr(address.host(), address.port(), socket.family).ok()
10636 })
10637 .or_else(|| socket.local_addr());
10638 let Some(guest_addr) = guest_addr else {
10639 continue;
10640 };
10641 let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10642 used_udp_ports
10643 .entry(family)
10644 .or_default()
10645 .insert(guest_addr.port());
10646 if let Some(host_addr) = socket
10647 .socket
10648 .as_ref()
10649 .and_then(|socket| socket.local_addr().ok())
10650 {
10651 if is_loopback_ip(guest_addr.ip()) {
10652 udp_guest_to_host.insert((family, guest_addr.port()), host_addr.port());
10653 udp_host_to_guest.insert((family, host_addr.port()), guest_addr.port());
10654 }
10655 } else if socket.kernel_socket_id.is_some() && is_loopback_ip(guest_addr.ip()) {
10656 udp_guest_to_host.insert((family, guest_addr.port()), guest_addr.port());
10657 udp_host_to_guest.insert((family, guest_addr.port()), guest_addr.port());
10658 }
10659 }
10660
10661 for (child_process_id, child) in &process.child_processes {
10662 let child_id = format!("{process_id}/{child_process_id}");
10663 collect_javascript_socket_port_state(
10664 kernel,
10665 &child_id,
10666 child,
10667 tcp_guest_to_host,
10668 http_loopback_targets,
10669 udp_guest_to_host,
10670 udp_host_to_guest,
10671 used_tcp_ports,
10672 used_udp_ports,
10673 );
10674 }
10675}
10676
10677pub(crate) fn build_javascript_socket_path_context(
10678 vm: &VmState,
10679) -> Result<JavascriptSocketPathContext, SidecarError> {
10680 let mut loopback_exempt_ports = vm.create_loopback_exempt_ports.clone();
10681 loopback_exempt_ports.extend(vm.configuration.loopback_exempt_ports.iter().copied());
10682 let mut tcp_loopback_guest_to_host_ports = BTreeMap::new();
10683 let mut http_loopback_targets = BTreeMap::new();
10684 let mut udp_loopback_guest_to_host_ports = BTreeMap::new();
10685 let mut udp_loopback_host_to_guest_ports = BTreeMap::new();
10686 let mut used_tcp_guest_ports = BTreeMap::new();
10687 let mut used_udp_guest_ports = BTreeMap::new();
10688 for (process_id, process) in &vm.active_processes {
10689 collect_javascript_socket_port_state(
10690 &vm.kernel,
10691 process_id,
10692 process,
10693 &mut tcp_loopback_guest_to_host_ports,
10694 &mut http_loopback_targets,
10695 &mut udp_loopback_guest_to_host_ports,
10696 &mut udp_loopback_host_to_guest_ports,
10697 &mut used_tcp_guest_ports,
10698 &mut used_udp_guest_ports,
10699 );
10700 }
10701 Ok(JavascriptSocketPathContext {
10702 sandbox_root: vm.cwd.clone(),
10703 mounts: vm.configuration.mounts.clone(),
10704 listen_policy: vm.listen_policy,
10705 loopback_exempt_ports,
10706 tcp_loopback_guest_to_host_ports,
10707 http_loopback_targets,
10708 udp_loopback_guest_to_host_ports,
10709 udp_loopback_host_to_guest_ports,
10710 used_tcp_guest_ports,
10711 used_udp_guest_ports,
10712 })
10713}
10714
10715fn check_network_resource_limit(
10716 limit: Option<usize>,
10717 current: usize,
10718 additional: usize,
10719 label: &str,
10720) -> Result<(), SidecarError> {
10721 if let Some(limit) = limit {
10722 if current.saturating_add(additional) > limit {
10723 return Err(SidecarError::Execution(format!(
10724 "EAGAIN: maximum {label} count reached"
10725 )));
10726 }
10727 }
10728 Ok(())
10729}
10730
10731fn normalize_tcp_listen_host(
10732 host: Option<&str>,
10733) -> Result<(JavascriptSocketFamily, &'static str, &'static str), SidecarError> {
10734 match host.unwrap_or("127.0.0.1") {
10735 "127.0.0.1" | "localhost" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "127.0.0.1")),
10736 "::1" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::1")),
10737 "0.0.0.0" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "0.0.0.0")),
10738 "::" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::")),
10739 other => Err(SidecarError::Execution(format!(
10740 "EACCES: TCP listeners must bind to loopback or unspecified addresses, got {other}"
10741 ))),
10742 }
10743}
10744
10745fn normalize_udp_bind_host(
10746 host: Option<&str>,
10747 family: JavascriptUdpFamily,
10748) -> Result<(&'static str, &'static str, JavascriptSocketFamily), SidecarError> {
10749 match (family, host) {
10750 (JavascriptUdpFamily::Ipv4, None) | (JavascriptUdpFamily::Ipv4, Some("0.0.0.0")) => {
10751 Ok(("127.0.0.1", "0.0.0.0", JavascriptSocketFamily::Ipv4))
10752 }
10753 (JavascriptUdpFamily::Ipv4, Some("127.0.0.1"))
10754 | (JavascriptUdpFamily::Ipv4, Some("localhost")) => {
10755 Ok(("127.0.0.1", "127.0.0.1", JavascriptSocketFamily::Ipv4))
10756 }
10757 (JavascriptUdpFamily::Ipv6, None) | (JavascriptUdpFamily::Ipv6, Some("::")) => {
10758 Ok(("::1", "::", JavascriptSocketFamily::Ipv6))
10759 }
10760 (JavascriptUdpFamily::Ipv6, Some("::1"))
10761 | (JavascriptUdpFamily::Ipv6, Some("localhost")) => {
10762 Ok(("::1", "::1", JavascriptSocketFamily::Ipv6))
10763 }
10764 (JavascriptUdpFamily::Ipv4, Some(other)) => Err(SidecarError::Execution(format!(
10765 "EACCES: udp4 sockets must bind to 127.0.0.1 or 0.0.0.0, got {other}"
10766 ))),
10767 (JavascriptUdpFamily::Ipv6, Some(other)) => Err(SidecarError::Execution(format!(
10768 "EACCES: udp6 sockets must bind to ::1 or ::, got {other}"
10769 ))),
10770 }
10771}
10772
10773fn allocate_guest_listen_port(
10774 requested_port: u16,
10775 family: JavascriptSocketFamily,
10776 used_ports: &BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10777 policy: VmListenPolicy,
10778) -> Result<u16, SidecarError> {
10779 let is_allowed = |port: u16| {
10780 port >= policy.port_min
10781 && port <= policy.port_max
10782 && (policy.allow_privileged || port >= 1024)
10783 };
10784 let used = used_ports.get(&family);
10785
10786 if requested_port != 0 {
10787 if !is_allowed(requested_port) {
10788 let reason = if requested_port < 1024 && !policy.allow_privileged {
10789 format!(
10790 "EACCES: privileged listen port {requested_port} requires {}=true",
10791 VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY
10792 )
10793 } else {
10794 format!(
10795 "EACCES: listen port {requested_port} is outside the allowed range {}-{}",
10796 policy.port_min, policy.port_max
10797 )
10798 };
10799 return Err(SidecarError::Execution(reason));
10800 }
10801 if used.is_some_and(|ports| ports.contains(&requested_port)) {
10802 return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
10803 libc::EADDRINUSE,
10804 )));
10805 }
10806 return Ok(requested_port);
10807 }
10808
10809 let allocation_start = policy
10810 .port_min
10811 .max(if policy.allow_privileged { 1 } else { 1024 });
10812 for candidate in allocation_start..=policy.port_max {
10813 if used.is_some_and(|ports| ports.contains(&candidate)) {
10814 continue;
10815 }
10816 return Ok(candidate);
10817 }
10818
10819 Err(sidecar_net_error(std::io::Error::from_raw_os_error(
10820 libc::EADDRINUSE,
10821 )))
10822}
10823
10824fn socket_host_matches(requested: Option<&str>, actual: &str) -> bool {
10825 match requested {
10826 None => true,
10827 Some(requested) if requested == actual => true,
10828 Some(requested)
10829 if is_unspecified_socket_host(requested) && is_unspecified_socket_host(actual) =>
10830 {
10831 true
10832 }
10833 Some(requested) if is_unspecified_socket_host(requested) => is_loopback_socket_host(actual),
10834 Some(requested) if requested.eq_ignore_ascii_case("localhost") => {
10835 is_loopback_socket_host(actual)
10836 }
10837 _ => false,
10838 }
10839}
10840
10841fn parse_proc_net_entries(table_path: &str) -> Result<Vec<ProcNetEntry>, SidecarError> {
10842 let contents = match fs::read_to_string(table_path) {
10843 Ok(contents) => contents,
10844 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
10845 Err(error) => {
10846 return Err(SidecarError::Io(format!(
10847 "failed to inspect socket table {table_path}: {error}"
10848 )));
10849 }
10850 };
10851
10852 let mut entries = Vec::new();
10853 for line in contents.lines().skip(1) {
10854 let columns = line.split_whitespace().collect::<Vec<_>>();
10855 if columns.len() < 10 {
10856 continue;
10857 }
10858 let Some((host, port)) = parse_proc_ip_port(columns[1]) else {
10859 continue;
10860 };
10861 let Ok(inode) = columns[9].parse::<u64>() else {
10862 continue;
10863 };
10864 entries.push(ProcNetEntry {
10865 local_host: host,
10866 local_port: port,
10867 state: columns[3].to_owned(),
10868 inode,
10869 });
10870 }
10871
10872 Ok(entries)
10873}
10874
10875fn parse_proc_ip_port(value: &str) -> Option<(String, u16)> {
10876 let (raw_ip, raw_port) = value.split_once(':')?;
10877 let port = u16::from_str_radix(raw_port, 16).ok()?;
10878 let host = match raw_ip.len() {
10879 8 => {
10880 let raw = u32::from_str_radix(raw_ip, 16).ok()?;
10881 Ipv4Addr::from(raw.to_le_bytes()).to_string()
10882 }
10883 32 => {
10884 let mut bytes = [0_u8; 16];
10885 for (index, chunk) in raw_ip.as_bytes().chunks(8).enumerate() {
10886 let word = u32::from_str_radix(std::str::from_utf8(chunk).ok()?, 16).ok()?;
10887 bytes[index * 4..(index + 1) * 4].copy_from_slice(&word.to_le_bytes());
10888 }
10889 Ipv6Addr::from(bytes).to_string()
10890 }
10891 _ => return None,
10892 };
10893 Some((host, port))
10894}
10895
10896fn python_file_entrypoint(entrypoint: &str) -> Option<PathBuf> {
10897 let path = Path::new(entrypoint);
10898 (path.extension().and_then(|extension| extension.to_str()) == Some("py"))
10899 .then(|| path.to_path_buf())
10900}
10901
10902fn add_runtime_guest_path_mapping(
10903 env: &mut BTreeMap<String, String>,
10904 guest_path: &str,
10905 host_path: &Path,
10906) {
10907 let mut mappings = env
10908 .get("AGENT_OS_GUEST_PATH_MAPPINGS")
10909 .and_then(|value| serde_json::from_str::<Vec<Value>>(value).ok())
10910 .unwrap_or_default();
10911 mappings.retain(|mapping| {
10912 mapping
10913 .get("guestPath")
10914 .and_then(Value::as_str)
10915 .map(|existing| normalize_path(existing) != normalize_path(guest_path))
10916 .unwrap_or(true)
10917 });
10918 mappings.push(json!({
10919 "guestPath": normalize_path(guest_path),
10920 "hostPath": host_path.display().to_string(),
10921 }));
10922 if let Ok(serialized) = serde_json::to_string(&mappings) {
10923 env.insert(String::from("AGENT_OS_GUEST_PATH_MAPPINGS"), serialized);
10924 }
10925}
10926
10927fn add_runtime_host_access_path(
10928 env: &mut BTreeMap<String, String>,
10929 key: &str,
10930 host_path: &Path,
10931 expand: bool,
10932) {
10933 let existing = env
10934 .get(key)
10935 .and_then(|value| serde_json::from_str::<Vec<String>>(value).ok())
10936 .unwrap_or_default()
10937 .into_iter()
10938 .map(PathBuf::from)
10939 .collect::<Vec<_>>();
10940 let mut paths = existing;
10941 paths.push(host_path.to_path_buf());
10942 let normalized = if expand {
10943 expand_host_access_paths(&paths)
10944 } else {
10945 dedupe_host_paths(&paths)
10946 };
10947 let serialized = normalized
10948 .iter()
10949 .map(|path| path.to_string_lossy().into_owned())
10950 .collect::<Vec<_>>();
10951 if let Ok(serialized) = serde_json::to_string(&serialized) {
10952 env.insert(key.to_owned(), serialized);
10953 }
10954}
10955
10956fn is_path_like_specifier(specifier: &str) -> bool {
10959 specifier.starts_with('/')
10960 || specifier.starts_with("./")
10961 || specifier.starts_with("../")
10962 || specifier.starts_with("file:")
10963}
10964
10965fn execution_wasm_permission_tier(tier: WasmPermissionTier) -> ExecutionWasmPermissionTier {
10966 match tier {
10967 WasmPermissionTier::Full => ExecutionWasmPermissionTier::Full,
10968 WasmPermissionTier::ReadWrite => ExecutionWasmPermissionTier::ReadWrite,
10969 WasmPermissionTier::ReadOnly => ExecutionWasmPermissionTier::ReadOnly,
10970 WasmPermissionTier::Isolated => ExecutionWasmPermissionTier::Isolated,
10971 }
10972}
10973
10974fn resolve_wasm_permission_tier(
10975 vm: &VmState,
10976 command_name: Option<&str>,
10977 explicit_tier: Option<WasmPermissionTier>,
10978 entrypoint: &str,
10979) -> WasmPermissionTier {
10980 explicit_tier
10981 .or_else(|| command_name.and_then(|command| vm.command_permissions.get(command).copied()))
10982 .or_else(|| {
10983 Path::new(entrypoint)
10984 .file_name()
10985 .and_then(|name| name.to_str())
10986 .and_then(|command| vm.command_permissions.get(command).copied())
10987 })
10988 .unwrap_or(WasmPermissionTier::Full)
10989}
10990
10991fn tokenize_shell_free_command(command: &str) -> Vec<String> {
10992 command
10993 .split_whitespace()
10994 .filter(|segment| !segment.is_empty())
10995 .map(str::to_owned)
10996 .collect()
10997}
10998
10999fn is_posix_shell_builtin(command: &str) -> bool {
11000 matches!(
11001 command,
11002 "." | ":"
11003 | "break"
11004 | "cd"
11005 | "continue"
11006 | "eval"
11007 | "exec"
11008 | "exit"
11009 | "export"
11010 | "readonly"
11011 | "return"
11012 | "set"
11013 | "shift"
11014 | "times"
11015 | "trap"
11016 | "umask"
11017 | "unset"
11018 )
11019}
11020
11021fn shell_first_token_requires_shell(token: &str) -> bool {
11027 token.contains('=') || is_shell_reserved_word(token)
11028}
11029
11030fn is_shell_reserved_word(token: &str) -> bool {
11031 matches!(
11032 token,
11033 "if" | "then"
11034 | "elif"
11035 | "else"
11036 | "fi"
11037 | "for"
11038 | "in"
11039 | "do"
11040 | "done"
11041 | "while"
11042 | "until"
11043 | "case"
11044 | "esac"
11045 | "{"
11046 | "}"
11047 | "!"
11048 )
11049}
11050
11051fn command_requires_shell(command: &str) -> bool {
11052 command.chars().any(|ch| {
11053 matches!(
11054 ch,
11055 '|' | '&'
11056 | ';'
11057 | '<'
11058 | '>'
11059 | '('
11060 | ')'
11061 | '$'
11062 | '`'
11063 | '*'
11064 | '?'
11065 | '['
11066 | ']'
11067 | '{'
11068 | '}'
11069 | '~'
11070 | '\''
11071 | '"'
11072 | '\\'
11073 | '\n'
11074 )
11075 })
11076}
11077
11078fn host_mount_path_for_guest_path(vm: &VmState, guest_path: &str) -> Option<PathBuf> {
11079 let normalized = normalize_path(guest_path);
11080
11081 let mut mounts = vm
11082 .configuration
11083 .mounts
11084 .iter()
11085 .filter_map(|mount| {
11086 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11087 .then(|| {
11088 mount_config_host_path(&mount.plugin.config)
11089 .map(|host_path| (mount.guest_path.as_str(), host_path))
11090 })
11091 .flatten()
11092 })
11093 .collect::<Vec<_>>();
11094 mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
11095
11096 for (guest_root, host_root) in mounts {
11097 if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
11098 continue;
11099 }
11100
11101 let suffix = normalized
11102 .strip_prefix(guest_root)
11103 .unwrap_or_default()
11104 .trim_start_matches('/');
11105 let mut path = PathBuf::from(host_root);
11106 if !suffix.is_empty() {
11107 path.push(suffix);
11108 }
11109 return Some(path);
11110 }
11111
11112 None
11113}
11114
11115fn host_runtime_path_for_guest_path_with_env(
11116 vm: &VmState,
11117 runtime_env: &BTreeMap<String, String>,
11118 guest_path: &str,
11119 default_host_cwd: &Path,
11120) -> Option<PathBuf> {
11121 if let Some(path) = host_mount_path_for_guest_path(vm, guest_path) {
11122 return Some(path);
11123 }
11124 if let Some(path) = host_path_from_runtime_guest_mappings(runtime_env, guest_path) {
11125 return Some(path);
11126 }
11127
11128 let normalized = normalize_path(guest_path);
11129 let virtual_home = guest_virtual_home(vm);
11130
11131 if normalized == virtual_home || normalized.starts_with(&format!("{virtual_home}/")) {
11132 let suffix = normalized
11133 .strip_prefix(&virtual_home)
11134 .unwrap_or_default()
11135 .trim_start_matches('/');
11136 let mut host_path = default_host_cwd.to_path_buf();
11137 if !suffix.is_empty() {
11138 host_path.push(suffix);
11139 }
11140 return Some(host_path);
11141 }
11142
11143 None
11144}
11145
11146#[derive(Deserialize, Serialize)]
11147struct RuntimeGuestPathMapping {
11148 #[serde(rename = "guestPath")]
11149 guest_path: String,
11150 #[serde(rename = "hostPath")]
11151 host_path: String,
11152 #[serde(rename = "readOnly", default)]
11153 read_only: bool,
11154}
11155
11156pub(crate) fn host_path_from_runtime_guest_mappings(
11157 runtime_env: &BTreeMap<String, String>,
11158 guest_path: &str,
11159) -> Option<PathBuf> {
11160 let mappings = runtime_env
11161 .get("AGENT_OS_GUEST_PATH_MAPPINGS")
11162 .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
11163 let normalized = normalize_path(guest_path);
11164
11165 let mut sorted_mappings = mappings
11166 .into_iter()
11167 .filter_map(|mapping| {
11168 (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
11169 normalize_path(&mapping.guest_path),
11170 PathBuf::from(mapping.host_path),
11171 ))
11172 })
11173 .collect::<Vec<_>>();
11174 sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.0.len()));
11175
11176 for (guest_root, mut host_root) in sorted_mappings {
11177 if guest_root != "/"
11178 && normalized != guest_root
11179 && !normalized.starts_with(&format!("{guest_root}/"))
11180 {
11181 continue;
11182 }
11183 if guest_root == "/" && !normalized.starts_with('/') {
11184 continue;
11185 }
11186
11187 if host_root.is_relative() {
11188 host_root = std::env::current_dir().ok()?.join(host_root);
11189 }
11190
11191 let suffix = if guest_root == "/" {
11192 normalized.trim_start_matches('/')
11193 } else {
11194 normalized
11195 .strip_prefix(&guest_root)
11196 .unwrap_or_default()
11197 .trim_start_matches('/')
11198 };
11199 if !suffix.is_empty() {
11200 host_root.push(suffix);
11201 }
11202 return Some(host_root);
11203 }
11204
11205 None
11206}
11207
11208fn guest_runtime_path_for_host_path(
11209 runtime_env: &BTreeMap<String, String>,
11210 virtual_home: &str,
11211 cwd: &Path,
11212 host_path: &str,
11213) -> Option<String> {
11214 let resolved = if host_path.starts_with("file://") {
11215 PathBuf::from(host_path.trim_start_matches("file://"))
11216 } else if host_path.starts_with("file:") {
11217 PathBuf::from(host_path.trim_start_matches("file:"))
11218 } else {
11219 let candidate = PathBuf::from(host_path);
11220 if candidate.is_absolute() {
11221 candidate
11222 } else if host_path.starts_with("./") || host_path.starts_with("../") {
11223 cwd.join(candidate)
11224 } else {
11225 return None;
11226 }
11227 };
11228 let normalized = normalize_host_path(&resolved);
11229
11230 if let Some(path) = guest_path_from_runtime_host_mappings(runtime_env, &normalized) {
11231 return Some(path);
11232 }
11233
11234 let normalized_cwd = normalize_host_path(cwd);
11235 if !path_is_within_root(&normalized, &normalized_cwd) {
11236 return None;
11237 }
11238
11239 let virtual_home = if virtual_home.starts_with('/') {
11240 virtual_home.to_string()
11241 } else {
11242 String::from("/root")
11243 };
11244 let suffix = normalized
11245 .strip_prefix(&normalized_cwd)
11246 .ok()?
11247 .to_string_lossy()
11248 .replace('\\', "/")
11249 .trim_start_matches('/')
11250 .to_owned();
11251
11252 Some(if suffix.is_empty() {
11253 virtual_home
11254 } else {
11255 normalize_path(&format!("{virtual_home}/{suffix}"))
11256 })
11257}
11258
11259fn guest_path_from_runtime_host_mappings(
11260 runtime_env: &BTreeMap<String, String>,
11261 host_path: &Path,
11262) -> Option<String> {
11263 let mappings = runtime_env
11264 .get("AGENT_OS_GUEST_PATH_MAPPINGS")
11265 .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
11266 let normalized = normalize_host_path(host_path);
11267
11268 let mut sorted_mappings = mappings
11269 .into_iter()
11270 .filter_map(|mapping| {
11271 (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
11272 normalize_path(&mapping.guest_path),
11273 normalize_host_path(Path::new(&mapping.host_path)),
11274 ))
11275 })
11276 .collect::<Vec<_>>();
11277 sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.1.as_os_str().len()));
11278
11279 for (guest_root, host_root) in sorted_mappings {
11280 if !path_is_within_root(&normalized, &host_root) {
11281 continue;
11282 }
11283 let suffix = normalized
11284 .strip_prefix(&host_root)
11285 .ok()?
11286 .to_string_lossy()
11287 .replace('\\', "/")
11288 .trim_start_matches('/')
11289 .to_owned();
11290
11291 return Some(if suffix.is_empty() {
11292 guest_root
11293 } else if guest_root == "/" {
11294 normalize_path(&format!("/{suffix}"))
11295 } else {
11296 normalize_path(&format!("{guest_root}/{suffix}"))
11297 });
11298 }
11299
11300 None
11301}
11302
11303fn host_mount_path_for_guest_path_from_mounts(
11304 mounts: &[crate::protocol::MountDescriptor],
11305 guest_path: &str,
11306) -> Option<PathBuf> {
11307 let normalized = normalize_path(guest_path);
11308
11309 let mut host_mounts = mounts
11310 .iter()
11311 .filter_map(|mount| {
11312 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11313 .then(|| {
11314 mount_config_host_path(&mount.plugin.config)
11315 .map(|host_path| (mount.guest_path.as_str(), host_path))
11316 })
11317 .flatten()
11318 })
11319 .collect::<Vec<_>>();
11320 host_mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
11321
11322 for (guest_root, host_root) in host_mounts {
11323 if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
11324 continue;
11325 }
11326
11327 let suffix = normalized
11328 .strip_prefix(guest_root)
11329 .unwrap_or_default()
11330 .trim_start_matches('/');
11331 let mut path = PathBuf::from(host_root);
11332 if !suffix.is_empty() {
11333 path.push(suffix);
11334 }
11335 return Some(path);
11336 }
11337
11338 None
11339}
11340
11341#[cfg(test)]
11342mod host_mount_path_for_guest_path_from_mounts_tests {
11343 use super::host_mount_path_for_guest_path_from_mounts;
11344 use crate::protocol::{MountDescriptor, MountPluginDescriptor};
11345 use serde_json::json;
11346 use std::path::PathBuf;
11347
11348 #[test]
11349 fn resolves_module_access_mount_paths() {
11350 let mounts = vec![MountDescriptor {
11351 guest_path: String::from("/root/node_modules"),
11352 read_only: true,
11353 plugin: MountPluginDescriptor {
11354 id: String::from("module_access"),
11355 config: json!({
11356 "hostPath": "/tmp/workspace/node_modules",
11357 })
11358 .to_string(),
11359 },
11360 }];
11361
11362 let resolved =
11363 host_mount_path_for_guest_path_from_mounts(&mounts, "/root/node_modules/pkg/index.js")
11364 .expect("module_access mount should resolve");
11365
11366 assert_eq!(
11367 resolved,
11368 PathBuf::from("/tmp/workspace/node_modules/pkg/index.js")
11369 );
11370 }
11371}
11372
11373fn resolve_guest_socket_host_path(
11374 context: &JavascriptSocketPathContext,
11375 guest_path: &str,
11376) -> PathBuf {
11377 if let Some(path) = host_mount_path_for_guest_path_from_mounts(&context.mounts, guest_path) {
11378 return path;
11379 }
11380
11381 let normalized = normalize_path(guest_path);
11382 let mut host_path = context.sandbox_root.clone();
11383 let suffix = normalized.trim_start_matches('/');
11384 if !suffix.is_empty() {
11385 host_path.push(suffix);
11386 }
11387 host_path
11388}
11389
11390fn ensure_kernel_parent_directories(
11391 kernel: &mut SidecarKernel,
11392 path: &str,
11393) -> Result<(), SidecarError> {
11394 let parent = dirname(path);
11395 if parent != "/" && !kernel.exists(&parent).map_err(kernel_error)? {
11396 kernel.mkdir(&parent, true).map_err(kernel_error)?;
11397 }
11398 Ok(())
11399}
11400
11401pub(crate) fn sanitize_javascript_child_process_internal_bootstrap_env(
11405 env: &BTreeMap<String, String>,
11406) -> BTreeMap<String, String> {
11407 const ALLOWED_KEYS: &[&str] = &[
11408 "AGENT_OS_ALLOWED_NODE_BUILTINS",
11409 "AGENT_OS_GUEST_PATH_MAPPINGS",
11410 "AGENT_OS_LOOPBACK_EXEMPT_PORTS",
11411 "AGENT_OS_VIRTUAL_PROCESS_EXEC_PATH",
11412 "AGENT_OS_VIRTUAL_PROCESS_UID",
11413 "AGENT_OS_VIRTUAL_PROCESS_GID",
11414 "AGENT_OS_VIRTUAL_PROCESS_VERSION",
11415 ];
11416
11417 env.iter()
11418 .filter(|(key, _)| {
11419 ALLOWED_KEYS.contains(&key.as_str()) || key.starts_with("AGENT_OS_VIRTUAL_OS_")
11420 })
11421 .map(|(key, value)| (key.clone(), value.clone()))
11422 .collect()
11423}
11424
11425fn resolve_tcp_bind_addr(host: &str, port: u16) -> Result<SocketAddr, SidecarError> {
11430 (host, port)
11431 .to_socket_addrs()
11432 .map_err(sidecar_net_error)?
11433 .next()
11434 .ok_or_else(|| {
11435 SidecarError::Execution(format!("failed to resolve TCP bind address {host}:{port}"))
11436 })
11437}
11438
11439pub(crate) fn format_dns_resource(hostname: &str) -> String {
11440 format!("dns://{hostname}")
11441}
11442
11443pub(crate) fn format_tcp_resource(host: &str, port: u16) -> String {
11444 format!("tcp://{host}:{port}")
11445}
11446
11447fn is_loopback_ip(ip: IpAddr) -> bool {
11448 match ip {
11449 IpAddr::V4(ip) => ip.is_loopback(),
11450 IpAddr::V6(ip) => {
11451 ip.is_loopback()
11452 || ip
11453 .to_ipv4_mapped()
11454 .is_some_and(|mapped| mapped.is_loopback())
11455 }
11456 }
11457}
11458
11459fn loopback_cidr(ip: IpAddr) -> &'static str {
11460 match ip {
11461 IpAddr::V4(ip) if ip.is_loopback() => "127.0.0.0/8",
11462 IpAddr::V6(ip)
11463 if ip
11464 .to_ipv4_mapped()
11465 .is_some_and(|mapped| mapped.is_loopback()) =>
11466 {
11467 "127.0.0.0/8"
11468 }
11469 IpAddr::V6(_) => "::1/128",
11470 IpAddr::V4(_) => "127.0.0.0/8",
11471 }
11472}
11473
11474fn ipv4_compatible_embedded(ip: Ipv6Addr) -> Option<Ipv4Addr> {
11480 let segments = ip.segments();
11481 if segments[0..6].iter().any(|&s| s != 0) {
11482 return None;
11483 }
11484 let embedded = (u32::from(segments[6]) << 16) | u32::from(segments[7]);
11485 if embedded == 0 || embedded == 1 {
11488 return None;
11489 }
11490 Some(Ipv4Addr::from(embedded))
11491}
11492
11493fn restricted_non_loopback_ip_range(ip: IpAddr) -> Option<(&'static str, &'static str)> {
11494 match ip {
11495 IpAddr::V4(ip) => {
11496 if ip.is_unspecified() {
11497 return Some(("0.0.0.0/32", "unspecified"));
11500 }
11501 let [first, second, ..] = ip.octets();
11502 match (first, second) {
11503 (10, _) => Some(("10.0.0.0/8", "private")),
11504 (100, 64..=127) => Some(("100.64.0.0/10", "carrier-grade-nat")),
11505 (172, 16..=31) => Some(("172.16.0.0/12", "private")),
11506 (192, 168) => Some(("192.168.0.0/16", "private")),
11507 (169, 254) => Some(("169.254.0.0/16", "link-local")),
11508 (224..=239, _) => Some(("224.0.0.0/4", "multicast")),
11513 (240..=255, _) => Some(("240.0.0.0/4", "reserved")),
11514 _ => None,
11515 }
11516 }
11517 IpAddr::V6(ip) => {
11518 if let Some(mapped) = ip.to_ipv4_mapped() {
11519 return restricted_non_loopback_ip_range(IpAddr::V4(mapped));
11520 }
11521 if let Some(compat) = ipv4_compatible_embedded(ip) {
11528 return restricted_non_loopback_ip_range(IpAddr::V4(compat));
11529 }
11530
11531 if ip.is_unspecified() {
11532 return Some(("::/128", "unspecified"));
11535 }
11536
11537 let segments = ip.segments();
11538 if (segments[0] & 0xfe00) == 0xfc00 {
11539 return Some(("fc00::/7", "unique-local"));
11540 }
11541 if (segments[0] & 0xffc0) == 0xfe80 {
11542 return Some(("fe80::/10", "link-local"));
11543 }
11544 None
11545 }
11546 }
11547}
11548
11549fn blocked_dns_resolution_error(
11550 resource: &str,
11551 ip: IpAddr,
11552 cidr: &str,
11553 label: &str,
11554) -> SidecarError {
11555 SidecarError::Execution(format!(
11556 "EACCES: blocked outbound network access to {resource}: {ip} is within restricted {label} range {cidr}"
11557 ))
11558}
11559
11560fn blocked_loopback_connect_error(resource: &str, ip: IpAddr, port: u16) -> SidecarError {
11561 SidecarError::Execution(format!(
11562 "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}",
11563 loopback_cidr(ip)
11564 ))
11565}
11566
11567fn filter_dns_safe_ip_addrs(
11568 addresses: Vec<IpAddr>,
11569 hostname: &str,
11570) -> Result<Vec<IpAddr>, SidecarError> {
11571 let resource = format_dns_resource(hostname);
11572 let mut allowed = Vec::new();
11573 let mut blocked = None;
11574
11575 for ip in addresses {
11576 if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
11577 blocked.get_or_insert((ip, cidr, label));
11578 continue;
11579 }
11580 allowed.push(ip);
11581 }
11582
11583 if allowed.is_empty() {
11584 let (ip, cidr, label) = blocked.expect("blocked DNS results should capture a reason");
11585 return Err(blocked_dns_resolution_error(&resource, ip, cidr, label));
11586 }
11587
11588 Ok(allowed)
11589}
11590
11591fn loopback_connect_allowed(context: &JavascriptSocketPathContext, port: u16) -> bool {
11592 context.loopback_port_allowed(port)
11593}
11594
11595fn filter_tcp_connect_ip_addrs(
11596 addresses: Vec<IpAddr>,
11597 host: &str,
11598 port: u16,
11599 context: &JavascriptSocketPathContext,
11600) -> Result<Vec<IpAddr>, SidecarError> {
11601 let resource = format_tcp_resource(host, port);
11602 let mut allowed = Vec::new();
11603 let mut blocked = None;
11604
11605 for ip in addresses {
11606 if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
11607 blocked.get_or_insert_with(|| blocked_dns_resolution_error(&resource, ip, cidr, label));
11608 continue;
11609 }
11610 if is_loopback_ip(ip) && !loopback_connect_allowed(context, port) {
11611 blocked.get_or_insert_with(|| blocked_loopback_connect_error(&resource, ip, port));
11612 continue;
11613 }
11614 allowed.push(ip);
11615 }
11616
11617 if allowed.is_empty() {
11618 return Err(blocked.expect("blocked TCP connect results should capture a reason"));
11619 }
11620
11621 Ok(allowed)
11622}
11623
11624fn resolve_tcp_connect_addr<B>(
11625 bridge: &SharedBridge<B>,
11626 kernel: &SidecarKernel,
11627 vm_id: &str,
11628 dns: &VmDnsConfig,
11629 host: &str,
11630 port: u16,
11631 context: &JavascriptSocketPathContext,
11632) -> Result<ResolvedTcpConnectAddr, SidecarError>
11633where
11634 B: NativeSidecarBridge + Send + 'static,
11635 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11636{
11637 let allowed = filter_tcp_connect_ip_addrs(
11638 resolve_dns_ip_addrs(
11639 bridge,
11640 kernel,
11641 vm_id,
11642 dns,
11643 host,
11644 DnsLookupPolicy::SkipPermissions,
11645 )?,
11646 host,
11647 port,
11648 context,
11649 )?;
11650 let ip = allowed
11651 .iter()
11652 .copied()
11653 .find(|candidate| {
11654 let family = JavascriptSocketFamily::from_ip(*candidate);
11655 context.translate_tcp_loopback_port(family, port).is_some()
11656 })
11657 .or_else(|| allowed.iter().copied().find(IpAddr::is_ipv4))
11660 .or_else(|| allowed.first().copied())
11661 .ok_or_else(|| {
11662 SidecarError::Execution(format!("failed to resolve TCP address {host}:{port}"))
11663 })?;
11664 let family = JavascriptSocketFamily::from_ip(ip);
11665 let translated_loopback_port = context.translate_tcp_loopback_port(family, port);
11666 let use_kernel_loopback = is_loopback_ip(ip) && translated_loopback_port == Some(port);
11667 let actual_port = if is_loopback_ip(ip) {
11668 translated_loopback_port.unwrap_or(port)
11669 } else {
11670 port
11671 };
11672 Ok(ResolvedTcpConnectAddr {
11673 actual_addr: SocketAddr::new(ip, actual_port),
11674 guest_remote_addr: SocketAddr::new(ip, port),
11675 use_kernel_loopback,
11676 })
11677}
11678
11679fn resolve_dns_ip_addrs<B>(
11680 bridge: &SharedBridge<B>,
11681 kernel: &SidecarKernel,
11682 vm_id: &str,
11683 dns: &VmDnsConfig,
11684 hostname: &str,
11685 policy: DnsLookupPolicy,
11686) -> Result<Vec<IpAddr>, SidecarError>
11687where
11688 B: NativeSidecarBridge + Send + 'static,
11689 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11690{
11691 let resolution = match kernel.resolve_dns(hostname, policy) {
11692 Ok(resolution) => resolution,
11693 Err(error) => {
11694 let sidecar_error = kernel_error(error.clone());
11695 if error.code() != "EACCES" {
11696 emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
11697 }
11698 return Err(sidecar_error);
11699 }
11700 };
11701 emit_dns_resolution_event(
11702 bridge,
11703 vm_id,
11704 hostname,
11705 resolution.source(),
11706 resolution.addresses(),
11707 dns,
11708 );
11709 Ok(resolution.addresses().to_vec())
11710}
11711
11712fn resolve_dns_records<B>(
11713 bridge: &SharedBridge<B>,
11714 kernel: &SidecarKernel,
11715 vm_id: &str,
11716 dns: &VmDnsConfig,
11717 hostname: &str,
11718 record_type: RecordType,
11719 policy: DnsLookupPolicy,
11720) -> Result<DnsRecordResolution, SidecarError>
11721where
11722 B: NativeSidecarBridge + Send + 'static,
11723 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11724{
11725 let resolution = match kernel.resolve_dns_records(hostname, record_type, policy) {
11726 Ok(resolution) => resolution,
11727 Err(error) => {
11728 let sidecar_error = kernel_error(error.clone());
11729 if error.code() != "EACCES" {
11730 emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
11731 }
11732 return Err(sidecar_error);
11733 }
11734 };
11735 emit_dns_record_resolution_event(bridge, vm_id, hostname, &resolution, dns);
11736 Ok(resolution)
11737}
11738
11739fn filter_dns_ip_addrs(
11740 addresses: Vec<IpAddr>,
11741 family: Option<u8>,
11742) -> Result<Vec<IpAddr>, SidecarError> {
11743 let filtered: Vec<_> = match family.unwrap_or(0) {
11744 0 => addresses,
11745 4 => addresses
11746 .into_iter()
11747 .filter(|ip| matches!(ip, IpAddr::V4(_)))
11748 .collect(),
11749 6 => addresses
11750 .into_iter()
11751 .filter(|ip| matches!(ip, IpAddr::V6(_)))
11752 .collect(),
11753 other => {
11754 return Err(SidecarError::InvalidState(format!(
11755 "unsupported dns family {other}"
11756 )));
11757 }
11758 };
11759
11760 if filtered.is_empty() {
11761 return Err(SidecarError::Execution(String::from(
11762 "failed to resolve DNS address for requested family",
11763 )));
11764 }
11765
11766 Ok(filtered)
11767}
11768
11769fn resolve_udp_bind_addr(
11770 host: &str,
11771 port: u16,
11772 family: JavascriptUdpFamily,
11773) -> Result<SocketAddr, SidecarError> {
11774 (host, port)
11775 .to_socket_addrs()
11776 .map_err(sidecar_net_error)?
11777 .find(|addr| family.matches_addr(addr))
11778 .ok_or_else(|| {
11779 SidecarError::Execution(format!(
11780 "failed to resolve {} UDP bind address {host}:{port}",
11781 family.socket_type()
11782 ))
11783 })
11784}
11785
11786fn resolve_udp_addr<B>(request: UdpRemoteAddrRequest<'_, B>) -> Result<SocketAddr, SidecarError>
11787where
11788 B: NativeSidecarBridge + Send + 'static,
11789 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11790{
11791 let UdpRemoteAddrRequest {
11792 bridge,
11793 kernel,
11794 vm_id,
11795 dns,
11796 host,
11797 port,
11798 family,
11799 context,
11800 } = request;
11801 resolve_dns_ip_addrs(
11802 bridge,
11803 kernel,
11804 vm_id,
11805 dns,
11806 host,
11807 DnsLookupPolicy::SkipPermissions,
11808 )?
11809 .into_iter()
11810 .map(|ip| {
11811 let family_key = JavascriptSocketFamily::from_ip(ip);
11812 let actual_port = if is_loopback_ip(ip) {
11813 context
11814 .translate_udp_loopback_port(family_key, port)
11815 .unwrap_or(port)
11816 } else {
11817 port
11818 };
11819 SocketAddr::new(ip, actual_port)
11820 })
11821 .find(|addr| family.matches_addr(addr))
11822 .ok_or_else(|| {
11823 SidecarError::Execution(format!(
11824 "failed to resolve {} UDP address {host}:{port}",
11825 family.socket_type()
11826 ))
11827 })
11828}
11829
11830fn socket_addr_family(addr: &SocketAddr) -> &'static str {
11831 match addr {
11832 SocketAddr::V4(_) => "IPv4",
11833 SocketAddr::V6(_) => "IPv6",
11834 }
11835}
11836
11837fn javascript_net_timeout_value() -> Value {
11838 Value::String(String::from(JAVASCRIPT_NET_TIMEOUT_SENTINEL))
11839}
11840
11841fn javascript_net_json_string(value: Value, label: &str) -> Result<Value, SidecarError> {
11842 serde_json::to_string(&value)
11843 .map(Value::String)
11844 .map_err(|error| {
11845 SidecarError::InvalidState(format!("failed to serialize {label} payload: {error}"))
11846 })
11847}
11848
11849fn javascript_net_read_value(
11850 event: Option<JavascriptTcpSocketEvent>,
11851) -> Result<Value, SidecarError> {
11852 match event {
11853 Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(Value::String(
11854 base64::engine::general_purpose::STANDARD.encode(chunk),
11855 )),
11856 Some(JavascriptTcpSocketEvent::End | JavascriptTcpSocketEvent::Close { .. }) => {
11857 Ok(Value::Null)
11858 }
11859 Some(JavascriptTcpSocketEvent::Error { code, message }) => {
11860 let detail = code.unwrap_or_else(|| String::from("socket read"));
11861 Err(SidecarError::Execution(format!("{detail}: {message}")))
11862 }
11863 None => Ok(javascript_net_timeout_value()),
11864 }
11865}
11866
11867fn io_error_code(error: &std::io::Error) -> Option<String> {
11868 match error.raw_os_error() {
11869 Some(libc::EADDRINUSE) => Some(String::from("EADDRINUSE")),
11870 Some(libc::EADDRNOTAVAIL) => Some(String::from("EADDRNOTAVAIL")),
11871 Some(libc::ECONNREFUSED) => Some(String::from("ECONNREFUSED")),
11872 Some(libc::ECONNRESET) => Some(String::from("ECONNRESET")),
11873 Some(libc::EINVAL) => Some(String::from("EINVAL")),
11874 Some(libc::EPIPE) => Some(String::from("EPIPE")),
11875 Some(libc::ETIMEDOUT) => Some(String::from("ETIMEDOUT")),
11876 Some(libc::EHOSTUNREACH) => Some(String::from("EHOSTUNREACH")),
11877 Some(libc::ENETUNREACH) => Some(String::from("ENETUNREACH")),
11878 _ => None,
11879 }
11880}
11881
11882fn sidecar_net_error(error: std::io::Error) -> SidecarError {
11883 let message = match io_error_code(&error) {
11884 Some(code) => format!("{code}: {error}"),
11885 None => error.to_string(),
11886 };
11887 SidecarError::Execution(message)
11888}
11889
11890fn tls_provider() -> Arc<rustls::crypto::CryptoProvider> {
11891 Arc::new(aws_lc_rs::default_provider())
11892}
11893
11894fn tls_local_certificates(
11895 options: &JavascriptTlsBridgeOptions,
11896) -> Result<Vec<Vec<u8>>, SidecarError> {
11897 let Some(certificates) = options.cert.as_ref() else {
11898 return Ok(Vec::new());
11899 };
11900 tls_material_entries(certificates)
11901}
11902
11903fn tls_material_entries(material: &JavascriptTlsMaterial) -> Result<Vec<Vec<u8>>, SidecarError> {
11904 match material {
11905 JavascriptTlsMaterial::Single(entry) => tls_data_value(entry).map(|value| vec![value]),
11906 JavascriptTlsMaterial::Many(entries) => entries.iter().map(tls_data_value).collect(),
11907 }
11908}
11909
11910fn tls_data_value(value: &JavascriptTlsDataValue) -> Result<Vec<u8>, SidecarError> {
11911 match value {
11912 JavascriptTlsDataValue::Buffer { data } => base64::engine::general_purpose::STANDARD
11913 .decode(data)
11914 .map_err(|error| {
11915 SidecarError::InvalidState(format!("TLS material contains invalid base64: {error}"))
11916 }),
11917 JavascriptTlsDataValue::String { data } => Ok(data.as_bytes().to_vec()),
11918 }
11919}
11920
11921fn tls_certificates_from_material(
11922 material: &JavascriptTlsMaterial,
11923) -> Result<Vec<CertificateDer<'static>>, SidecarError> {
11924 let mut certificates = Vec::new();
11925 for entry in tls_material_entries(material)? {
11926 let mut reader = std::io::BufReader::new(Cursor::new(entry.clone()));
11927 let parsed = rustls_pemfile::certs(&mut reader)
11928 .collect::<Result<Vec<_>, _>>()
11929 .map_err(sidecar_net_error)?;
11930 if parsed.is_empty() {
11931 certificates.push(CertificateDer::from(entry));
11932 } else {
11933 certificates.extend(parsed);
11934 }
11935 }
11936 if certificates.is_empty() {
11937 return Err(SidecarError::InvalidState(String::from(
11938 "TLS certificate material did not contain any certificates",
11939 )));
11940 }
11941 Ok(certificates)
11942}
11943
11944fn tls_private_key_from_material(
11945 material: &JavascriptTlsMaterial,
11946) -> Result<PrivateKeyDer<'static>, SidecarError> {
11947 for entry in tls_material_entries(material)? {
11948 let mut reader = std::io::BufReader::new(Cursor::new(entry));
11949 if let Some(key) = rustls_pemfile::private_key(&mut reader).map_err(sidecar_net_error)? {
11950 return Ok(key);
11951 }
11952 }
11953 Err(SidecarError::InvalidState(String::from(
11954 "TLS private key material did not contain a supported key",
11955 )))
11956}
11957
11958fn tls_root_store(options: &JavascriptTlsBridgeOptions) -> Result<RootCertStore, SidecarError> {
11959 let mut roots = RootCertStore::empty();
11960 if let Some(ca) = options.ca.as_ref() {
11961 for certificate in tls_certificates_from_material(ca)? {
11962 roots.add(certificate).map_err(|error| {
11963 SidecarError::InvalidState(format!("failed to add TLS CA certificate: {error}"))
11964 })?;
11965 }
11966 return Ok(roots);
11967 }
11968
11969 for certificate in rustls_native_certs::load_native_certs().certs {
11970 roots.add(certificate).map_err(|error| {
11971 SidecarError::InvalidState(format!(
11972 "failed to add native TLS certificate to root store: {error}"
11973 ))
11974 })?;
11975 }
11976 Ok(roots)
11977}
11978
11979fn build_client_tls_stream(
11980 stream: TcpStream,
11981 options: &JavascriptTlsBridgeOptions,
11982) -> Result<rustls::StreamOwned<ClientConnection, TcpStream>, SidecarError> {
11983 let config = build_client_tls_config(options)?;
11984 let server_name = options
11985 .servername
11986 .clone()
11987 .unwrap_or_else(|| String::from("localhost"));
11988 let server_name = ServerName::try_from(server_name)
11989 .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
11990 stream
11991 .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
11992 .map_err(sidecar_net_error)?;
11993 stream
11994 .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
11995 .map_err(sidecar_net_error)?;
11996 let mut tls_stream = rustls::StreamOwned::new(
11997 ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
11998 SidecarError::Execution(format!("failed to start TLS client: {error}"))
11999 })?,
12000 stream,
12001 );
12002 while tls_stream.conn.is_handshaking() {
12003 tls_stream
12004 .conn
12005 .complete_io(&mut tls_stream.sock)
12006 .map_err(sidecar_net_error)?;
12007 }
12008 tls_stream
12009 .sock
12010 .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
12011 .map_err(sidecar_net_error)?;
12012 tls_stream
12013 .sock
12014 .set_write_timeout(None)
12015 .map_err(sidecar_net_error)?;
12016 Ok(tls_stream)
12017}
12018
12019fn build_client_loopback_tls_stream(
12020 transport: crate::state::LoopbackTlsEndpoint,
12021 options: &JavascriptTlsBridgeOptions,
12022) -> Result<rustls::StreamOwned<ClientConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
12023{
12024 let config = build_client_tls_config(options)?;
12025 let server_name = options
12026 .servername
12027 .clone()
12028 .unwrap_or_else(|| String::from("localhost"));
12029 let server_name = ServerName::try_from(server_name)
12030 .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
12031 let mut tls_stream = rustls::StreamOwned::new(
12032 ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
12033 SidecarError::Execution(format!("failed to start TLS client: {error}"))
12034 })?,
12035 transport,
12036 );
12037 match tls_stream.conn.complete_io(&mut tls_stream.sock) {
12038 Ok(_) => {}
12039 Err(error)
12040 if matches!(
12041 error.kind(),
12042 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12043 ) => {}
12044 Err(error) => return Err(sidecar_net_error(error)),
12045 }
12046 Ok(tls_stream)
12047}
12048
12049fn build_client_tls_config(
12050 options: &JavascriptTlsBridgeOptions,
12051) -> Result<ClientConfig, SidecarError> {
12052 let provider = tls_provider();
12053 let builder = ClientConfig::builder_with_provider(provider.clone())
12054 .with_safe_default_protocol_versions()
12055 .map_err(|error| {
12056 SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
12057 })?;
12058
12059 let mut config = if options.reject_unauthorized == Some(false) {
12060 let verifier = Arc::new(InsecureTlsVerifier {
12061 supported_schemes: provider
12062 .signature_verification_algorithms
12063 .supported_schemes(),
12064 });
12065 builder
12066 .dangerous()
12067 .with_custom_certificate_verifier(verifier)
12068 .with_no_client_auth()
12069 } else {
12070 builder
12071 .with_root_certificates(tls_root_store(options)?)
12072 .with_no_client_auth()
12073 };
12074
12075 if let Some(protocols) = options.alpn_protocols.as_ref() {
12076 config.alpn_protocols = protocols
12077 .iter()
12078 .map(|protocol| protocol.as_bytes().to_vec())
12079 .collect();
12080 }
12081 Ok(config)
12082}
12083
12084fn build_server_tls_stream(
12085 stream: TcpStream,
12086 options: &JavascriptTlsBridgeOptions,
12087) -> Result<rustls::StreamOwned<ServerConnection, TcpStream>, SidecarError> {
12088 let config = build_server_tls_config(options)?;
12089 stream
12090 .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12091 .map_err(sidecar_net_error)?;
12092 stream
12093 .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12094 .map_err(sidecar_net_error)?;
12095 let mut tls_stream = rustls::StreamOwned::new(
12096 ServerConnection::new(Arc::new(config)).map_err(|error| {
12097 SidecarError::Execution(format!("failed to start TLS server: {error}"))
12098 })?,
12099 stream,
12100 );
12101 while tls_stream.conn.is_handshaking() {
12102 tls_stream
12103 .conn
12104 .complete_io(&mut tls_stream.sock)
12105 .map_err(sidecar_net_error)?;
12106 }
12107 tls_stream
12108 .sock
12109 .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
12110 .map_err(sidecar_net_error)?;
12111 tls_stream
12112 .sock
12113 .set_write_timeout(None)
12114 .map_err(sidecar_net_error)?;
12115 Ok(tls_stream)
12116}
12117
12118fn build_server_loopback_tls_stream(
12119 transport: crate::state::LoopbackTlsEndpoint,
12120 options: &JavascriptTlsBridgeOptions,
12121) -> Result<rustls::StreamOwned<ServerConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
12122{
12123 let config = build_server_tls_config(options)?;
12124 Ok(rustls::StreamOwned::new(
12125 ServerConnection::new(Arc::new(config)).map_err(|error| {
12126 SidecarError::Execution(format!("failed to start TLS server: {error}"))
12127 })?,
12128 transport,
12129 ))
12130}
12131
12132fn build_server_tls_config(
12133 options: &JavascriptTlsBridgeOptions,
12134) -> Result<ServerConfig, SidecarError> {
12135 let certificates = tls_certificates_from_material(options.cert.as_ref().ok_or_else(|| {
12136 SidecarError::InvalidState(String::from("TLS server upgrade requires a certificate"))
12137 })?)?;
12138 let key = tls_private_key_from_material(options.key.as_ref().ok_or_else(|| {
12139 SidecarError::InvalidState(String::from("TLS server upgrade requires a private key"))
12140 })?)?;
12141
12142 let mut config = ServerConfig::builder_with_provider(tls_provider())
12143 .with_safe_default_protocol_versions()
12144 .map_err(|error| {
12145 SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
12146 })?
12147 .with_no_client_auth()
12148 .with_single_cert(certificates, key)
12149 .map_err(|error| {
12150 SidecarError::InvalidState(format!("invalid TLS server config: {error}"))
12151 })?;
12152
12153 if let Some(protocols) = options.alpn_protocols.as_ref() {
12154 config.alpn_protocols = protocols
12155 .iter()
12156 .map(|protocol| protocol.as_bytes().to_vec())
12157 .collect();
12158 }
12159 Ok(config)
12160}
12161
12162fn tls_protocol_name(version: rustls::ProtocolVersion) -> String {
12163 match version {
12164 rustls::ProtocolVersion::TLSv1_2 => String::from("TLSv1.2"),
12165 rustls::ProtocolVersion::TLSv1_3 => String::from("TLSv1.3"),
12166 other => other
12167 .as_str()
12168 .map(str::to_owned)
12169 .unwrap_or_else(|| format!("{other:?}")),
12170 }
12171}
12172
12173fn tls_cipher_bridge_value(suite: rustls::SupportedCipherSuite) -> Value {
12174 tls_bridge_object(vec![
12175 (
12176 "name",
12177 suite
12178 .suite()
12179 .as_str()
12180 .map(|value| Value::String(value.to_owned()))
12181 .unwrap_or(Value::Null),
12182 ),
12183 (
12184 "standardName",
12185 suite
12186 .suite()
12187 .as_str()
12188 .map(|value| Value::String(value.to_owned()))
12189 .unwrap_or(Value::Null),
12190 ),
12191 (
12192 "version",
12193 Value::String(if suite.tls13().is_some() {
12194 String::from("TLSv1.3")
12195 } else {
12196 String::from("TLSv1.2")
12197 }),
12198 ),
12199 ])
12200}
12201
12202fn tls_certificate_bridge_value(certificate: &[u8], detailed: bool) -> Value {
12203 let mut fields = vec![("raw", tls_bridge_buffer_value(certificate))];
12204 if detailed {
12205 fields.push(("issuerCertificate", tls_bridge_undefined_value()));
12206 }
12207 tls_bridge_object(fields)
12208}
12209
12210fn tls_bridge_buffer_value(bytes: &[u8]) -> Value {
12211 json!({
12212 "type": "buffer",
12213 "data": base64::engine::general_purpose::STANDARD.encode(bytes),
12214 })
12215}
12216
12217fn tls_bridge_object(entries: Vec<(&str, Value)>) -> Value {
12218 let value = entries
12219 .into_iter()
12220 .map(|(key, value)| (key.to_owned(), value))
12221 .collect::<serde_json::Map<String, Value>>();
12222 json!({
12223 "type": "object",
12224 "id": 1,
12225 "value": value,
12226 })
12227}
12228
12229fn tls_bridge_undefined_value() -> Value {
12230 json!({
12231 "type": "undefined",
12232 })
12233}
12234
12235fn spawn_tcp_socket_reader(
12236 stream: TcpStream,
12237 sender: Sender<JavascriptTcpSocketEvent>,
12238 tls_mode: Arc<AtomicBool>,
12239 saw_local_shutdown: Arc<AtomicBool>,
12240 saw_remote_end: Arc<AtomicBool>,
12241 close_notified: Arc<AtomicBool>,
12242) {
12243 thread::spawn(move || {
12244 let mut stream = stream;
12245 let mut buffer = vec![0_u8; 64 * 1024];
12246 loop {
12247 if tls_mode.load(Ordering::SeqCst) {
12248 break;
12249 }
12250 match stream.read(&mut buffer) {
12251 Ok(0) => {
12252 saw_remote_end.store(true, Ordering::SeqCst);
12253 let _ = sender.send(JavascriptTcpSocketEvent::End);
12254 if saw_local_shutdown.load(Ordering::SeqCst)
12255 && !close_notified.swap(true, Ordering::SeqCst)
12256 {
12257 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12258 }
12259 break;
12260 }
12261 Ok(bytes_read) => {
12262 if sender
12263 .send(JavascriptTcpSocketEvent::Data(
12264 buffer[..bytes_read].to_vec(),
12265 ))
12266 .is_err()
12267 {
12268 break;
12269 }
12270 }
12271 Err(error)
12272 if matches!(
12273 error.kind(),
12274 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12275 ) =>
12276 {
12277 continue;
12278 }
12279 Err(error) => {
12280 let code = io_error_code(&error);
12281 let _ = sender.send(JavascriptTcpSocketEvent::Error {
12282 code,
12283 message: error.to_string(),
12284 });
12285 if !close_notified.swap(true, Ordering::SeqCst) {
12286 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12287 }
12288 break;
12289 }
12290 }
12291 }
12292 });
12293}
12294
12295fn spawn_tls_socket_reader(
12296 tls_stream: Arc<Mutex<Option<ActiveTlsStream>>>,
12297 sender: Sender<JavascriptTcpSocketEvent>,
12298 saw_local_shutdown: Arc<AtomicBool>,
12299 saw_remote_end: Arc<AtomicBool>,
12300 close_notified: Arc<AtomicBool>,
12301) {
12302 thread::spawn(move || {
12303 let mut buffer = vec![0_u8; 64 * 1024];
12304 loop {
12305 let read_result = {
12306 let mut guard = match tls_stream.lock() {
12307 Ok(guard) => guard,
12308 Err(_) => return,
12309 };
12310 let Some(stream) = guard.as_mut() else {
12311 return;
12312 };
12313 stream.read(&mut buffer)
12314 };
12315
12316 match read_result {
12317 Ok(0) => {
12318 saw_remote_end.store(true, Ordering::SeqCst);
12319 let _ = sender.send(JavascriptTcpSocketEvent::End);
12320 if saw_local_shutdown.load(Ordering::SeqCst)
12321 && !close_notified.swap(true, Ordering::SeqCst)
12322 {
12323 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12324 }
12325 break;
12326 }
12327 Ok(bytes_read) => {
12328 if sender
12329 .send(JavascriptTcpSocketEvent::Data(
12330 buffer[..bytes_read].to_vec(),
12331 ))
12332 .is_err()
12333 {
12334 break;
12335 }
12336 }
12337 Err(error)
12338 if matches!(
12339 error.kind(),
12340 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12341 ) =>
12342 {
12343 std::thread::sleep(Duration::from_millis(1));
12346 continue;
12347 }
12348 Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
12349 saw_remote_end.store(true, Ordering::SeqCst);
12350 let _ = sender.send(JavascriptTcpSocketEvent::End);
12351 if saw_local_shutdown.load(Ordering::SeqCst)
12352 && !close_notified.swap(true, Ordering::SeqCst)
12353 {
12354 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12355 }
12356 break;
12357 }
12358 Err(error) => {
12359 let code = io_error_code(&error);
12360 let _ = sender.send(JavascriptTcpSocketEvent::Error {
12361 code,
12362 message: error.to_string(),
12363 });
12364 if !close_notified.swap(true, Ordering::SeqCst) {
12365 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12366 }
12367 break;
12368 }
12369 }
12370 }
12371 });
12372}
12373
12374fn spawn_unix_socket_reader(
12375 stream: UnixStream,
12376 sender: Sender<JavascriptTcpSocketEvent>,
12377 saw_local_shutdown: Arc<AtomicBool>,
12378 saw_remote_end: Arc<AtomicBool>,
12379 close_notified: Arc<AtomicBool>,
12380) {
12381 thread::spawn(move || {
12382 let mut stream = stream;
12383 let mut buffer = vec![0_u8; 64 * 1024];
12384 loop {
12385 match stream.read(&mut buffer) {
12386 Ok(0) => {
12387 saw_remote_end.store(true, Ordering::SeqCst);
12388 let _ = sender.send(JavascriptTcpSocketEvent::End);
12389 if saw_local_shutdown.load(Ordering::SeqCst)
12390 && !close_notified.swap(true, Ordering::SeqCst)
12391 {
12392 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12393 }
12394 break;
12395 }
12396 Ok(bytes_read) => {
12397 if sender
12398 .send(JavascriptTcpSocketEvent::Data(
12399 buffer[..bytes_read].to_vec(),
12400 ))
12401 .is_err()
12402 {
12403 break;
12404 }
12405 }
12406 Err(error) => {
12407 let code = io_error_code(&error);
12408 let _ = sender.send(JavascriptTcpSocketEvent::Error {
12409 code,
12410 message: error.to_string(),
12411 });
12412 if !close_notified.swap(true, Ordering::SeqCst) {
12413 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12414 }
12415 break;
12416 }
12417 }
12418 }
12419 });
12420}
12421
12422fn terminate_child_process_tree(kernel: &mut SidecarKernel, process: &mut ActiveProcess) {
12423 let sqlite_database_ids = process.sqlite_databases.keys().copied().collect::<Vec<_>>();
12424 for database_id in sqlite_database_ids {
12425 let _ = close_sqlite_database(kernel, process, database_id);
12426 }
12427 process.sqlite_statements.clear();
12428 process.http_servers.clear();
12429 process.pending_http_requests.clear();
12430 if let Ok(mut http2) = process.http2.shared.lock() {
12431 let sessions = http2.sessions.values().cloned().collect::<Vec<_>>();
12432 http2.server_events.clear();
12433 http2.session_events.clear();
12434 http2.streams.clear();
12435 http2.servers.clear();
12436 http2.sessions.clear();
12437 drop(http2);
12438 for session in sessions {
12439 let (respond_to, _rx) = mpsc::channel();
12440 let _ = session.command_tx.send(Http2SessionCommand::Close {
12441 abrupt: true,
12442 respond_to,
12443 });
12444 }
12445 }
12446
12447 let listener_ids = process.tcp_listeners.keys().cloned().collect::<Vec<_>>();
12448 for listener_id in listener_ids {
12449 if let Some(listener) = process.tcp_listeners.remove(&listener_id) {
12450 let _ = listener.close(kernel, process.kernel_pid);
12451 }
12452 }
12453
12454 let sockets = process.tcp_sockets.keys().cloned().collect::<Vec<_>>();
12455 for socket_id in sockets {
12456 if let Some(socket) = process.tcp_sockets.remove(&socket_id) {
12457 let _ = socket.close(kernel, process.kernel_pid);
12458 }
12459 }
12460
12461 let unix_listener_ids = process.unix_listeners.keys().cloned().collect::<Vec<_>>();
12462 for listener_id in unix_listener_ids {
12463 if let Some(listener) = process.unix_listeners.remove(&listener_id) {
12464 let _ = listener.close();
12465 }
12466 }
12467
12468 let unix_sockets = process.unix_sockets.keys().cloned().collect::<Vec<_>>();
12469 for socket_id in unix_sockets {
12470 if let Some(socket) = process.unix_sockets.remove(&socket_id) {
12471 let _ = socket.close();
12472 }
12473 }
12474
12475 let udp_socket_ids = process.udp_sockets.keys().cloned().collect::<Vec<_>>();
12476 for socket_id in udp_socket_ids {
12477 if let Some(mut socket) = process.udp_sockets.remove(&socket_id) {
12478 socket.close(kernel, process.kernel_pid);
12479 }
12480 }
12481
12482 let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
12483 for child_id in child_ids {
12484 let Some(mut child) = process.child_processes.remove(&child_id) else {
12485 continue;
12486 };
12487 terminate_child_process_tree(kernel, &mut child);
12488 let _ = kernel.kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, SIGTERM);
12489 let _ = signal_runtime_process(child.execution.child_pid(), SIGTERM);
12490 child.kernel_handle.finish(0);
12491 let _ = kernel.wait_and_reap(child.kernel_pid);
12492 }
12493}
12494
12495fn service_javascript_sqlite_sync_rpc(
12496 kernel: &mut SidecarKernel,
12497 process: &mut ActiveProcess,
12498 request: &JavascriptSyncRpcRequest,
12499) -> Result<Value, SidecarError> {
12500 match request.method.as_str() {
12501 "sqlite.constants" => Ok(json!({})),
12502 "sqlite.open" => sqlite_open_database(kernel, process, request),
12503 "sqlite.close" => {
12504 let database_id =
12505 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.close database id")?;
12506 close_sqlite_database(kernel, process, database_id)?;
12507 Ok(Value::Null)
12508 }
12509 "sqlite.exec" => sqlite_exec_database(kernel, process, request),
12510 "sqlite.query" => sqlite_query_database(process, request),
12511 "sqlite.prepare" => sqlite_prepare_statement(process, request),
12512 "sqlite.location" => {
12513 let database_id =
12514 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.location database id")?;
12515 let database = sqlite_database(process, database_id)?;
12516 Ok(database
12517 .vm_path
12518 .as_ref()
12519 .map(|path| Value::String(path.clone()))
12520 .unwrap_or(Value::Null))
12521 }
12522 "sqlite.checkpoint" => {
12523 let database_id =
12524 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.checkpoint database id")?;
12525 let kernel_pid = process.kernel_pid;
12526 let database = sqlite_database_mut(process, database_id)?;
12527 sqlite_sync_database(kernel, kernel_pid, database)?;
12528 Ok(Value::Null)
12529 }
12530 "sqlite.statement.run" => sqlite_run_statement(kernel, process, request),
12531 "sqlite.statement.get" => sqlite_get_statement(process, request),
12532 "sqlite.statement.all" | "sqlite.statement.iterate" => {
12533 sqlite_all_statement(process, request)
12534 }
12535 "sqlite.statement.columns" => sqlite_statement_columns(process, request),
12536 "sqlite.statement.setReturnArrays" => {
12537 let statement_id = javascript_sync_rpc_arg_u64(
12538 &request.args,
12539 0,
12540 "sqlite.statement.setReturnArrays statement id",
12541 )?;
12542 let enabled = javascript_sync_rpc_arg_bool(
12543 &request.args,
12544 1,
12545 "sqlite.statement.setReturnArrays enabled",
12546 )?;
12547 sqlite_statement_mut(process, statement_id)?.return_arrays = enabled;
12548 Ok(Value::Null)
12549 }
12550 "sqlite.statement.setReadBigInts" => {
12551 let statement_id = javascript_sync_rpc_arg_u64(
12552 &request.args,
12553 0,
12554 "sqlite.statement.setReadBigInts statement id",
12555 )?;
12556 let enabled = javascript_sync_rpc_arg_bool(
12557 &request.args,
12558 1,
12559 "sqlite.statement.setReadBigInts enabled",
12560 )?;
12561 sqlite_statement_mut(process, statement_id)?.read_bigints = enabled;
12562 Ok(Value::Null)
12563 }
12564 "sqlite.statement.setAllowBareNamedParameters" => {
12565 let statement_id = javascript_sync_rpc_arg_u64(
12566 &request.args,
12567 0,
12568 "sqlite.statement.setAllowBareNamedParameters statement id",
12569 )?;
12570 let enabled = javascript_sync_rpc_arg_bool(
12571 &request.args,
12572 1,
12573 "sqlite.statement.setAllowBareNamedParameters enabled",
12574 )?;
12575 sqlite_statement_mut(process, statement_id)?.allow_bare_named_parameters = enabled;
12576 Ok(Value::Null)
12577 }
12578 "sqlite.statement.setAllowUnknownNamedParameters" => {
12579 let statement_id = javascript_sync_rpc_arg_u64(
12580 &request.args,
12581 0,
12582 "sqlite.statement.setAllowUnknownNamedParameters statement id",
12583 )?;
12584 let enabled = javascript_sync_rpc_arg_bool(
12585 &request.args,
12586 1,
12587 "sqlite.statement.setAllowUnknownNamedParameters enabled",
12588 )?;
12589 sqlite_statement_mut(process, statement_id)?.allow_unknown_named_parameters = enabled;
12590 Ok(Value::Null)
12591 }
12592 "sqlite.statement.finalize" => {
12593 let statement_id = javascript_sync_rpc_arg_u64(
12594 &request.args,
12595 0,
12596 "sqlite.statement.finalize statement id",
12597 )?;
12598 process
12599 .sqlite_statements
12600 .remove(&statement_id)
12601 .ok_or_else(|| {
12602 SidecarError::InvalidState(format!(
12603 "sqlite statement handle not found: {statement_id}"
12604 ))
12605 })?;
12606 Ok(Value::Null)
12607 }
12608 other => Err(SidecarError::InvalidState(format!(
12609 "unsupported JavaScript sqlite sync RPC method {other}"
12610 ))),
12611 }
12612}
12613
12614fn sqlite_open_database(
12615 kernel: &mut SidecarKernel,
12616 process: &mut ActiveProcess,
12617 request: &JavascriptSyncRpcRequest,
12618) -> Result<Value, SidecarError> {
12619 ensure_per_process_state_handle_capacity(process.sqlite_databases.len(), "sqlite database")?;
12620 let path = request.args.first().and_then(Value::as_str);
12621 let vm_path = path.filter(|value| !value.is_empty() && *value != ":memory:");
12622 let options = request.args.get(1);
12623 let read_only = sqlite_option_bool(options, "readOnly").unwrap_or(false);
12624 let create = sqlite_option_bool(options, "create").unwrap_or(!read_only);
12625 let timeout_ms = sqlite_option_u64(options, "timeout");
12626
12627 process.next_sqlite_database_id += 1;
12628 let database_id = process.next_sqlite_database_id;
12629
12630 let host_path = if vm_path.is_some() {
12631 Some(
12632 std::env::temp_dir()
12633 .join(format!(
12634 "secure-exec-sidecar-sqlite-{}-{database_id}",
12635 process.kernel_pid
12636 ))
12637 .join("database.sqlite"),
12638 )
12639 } else {
12640 None
12641 };
12642
12643 if let Some(host_path) = host_path.as_ref() {
12644 if let Some(parent) = host_path.parent() {
12645 fs::create_dir_all(parent).map_err(|error| {
12646 SidecarError::Io(format!(
12647 "failed to prepare sqlite temp directory {}: {error}",
12648 parent.display()
12649 ))
12650 })?;
12651 }
12652 }
12653
12654 if let (Some(vm_path), Some(host_path)) = (vm_path, host_path.as_ref()) {
12655 if kernel
12656 .exists_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
12657 .map_err(kernel_error)?
12658 {
12659 let contents = kernel
12660 .read_file_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
12661 .map_err(kernel_error)?;
12662 fs::write(host_path, contents).map_err(|error| {
12663 SidecarError::Io(format!(
12664 "failed to materialize sqlite database {}: {error}",
12665 host_path.display()
12666 ))
12667 })?;
12668 } else if read_only && !create {
12669 return Err(SidecarError::InvalidState(format!(
12670 "sqlite database does not exist: {vm_path}"
12671 )));
12672 }
12673 }
12674
12675 let target = host_path
12676 .as_ref()
12677 .map(|path| path.to_string_lossy().into_owned())
12678 .unwrap_or_else(|| String::from(":memory:"));
12679 let mut flags = if read_only {
12680 SqliteOpenFlags::SQLITE_OPEN_READ_ONLY
12681 } else {
12682 SqliteOpenFlags::SQLITE_OPEN_READ_WRITE
12683 };
12684 if create && !read_only {
12685 flags |= SqliteOpenFlags::SQLITE_OPEN_CREATE;
12686 }
12687
12688 let connection = SqliteConnection::open_with_flags(&target, flags).map_err(|error| {
12689 SidecarError::InvalidState(format!(
12690 "sqlite database open failed for {}: {error}",
12691 vm_path.unwrap_or(":memory:")
12692 ))
12693 })?;
12694 if let Some(timeout_ms) = timeout_ms {
12695 connection
12696 .busy_timeout(Duration::from_millis(timeout_ms))
12697 .map_err(sqlite_error)?;
12698 }
12699 if host_path.is_some() && !read_only {
12700 let _ = connection.pragma_update(None, "journal_mode", "WAL");
12701 }
12702
12703 process.sqlite_databases.insert(
12704 database_id,
12705 ActiveSqliteDatabase {
12706 connection,
12707 host_path,
12708 vm_path: vm_path.map(String::from),
12709 dirty: false,
12710 transaction_depth: 0,
12711 read_only,
12712 },
12713 );
12714
12715 Ok(json!(database_id))
12716}
12717
12718fn sqlite_exec_database(
12719 kernel: &mut SidecarKernel,
12720 process: &mut ActiveProcess,
12721 request: &JavascriptSyncRpcRequest,
12722) -> Result<Value, SidecarError> {
12723 let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.exec database id")?;
12724 let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.exec sql")?;
12725 let kernel_pid = process.kernel_pid;
12726 let database = sqlite_database_mut(process, database_id)?;
12727 let before = database.connection.total_changes();
12728 database
12729 .connection
12730 .execute_batch(sql)
12731 .map_err(sqlite_error)?;
12732 mark_sqlite_mutation(database, sql);
12733 sqlite_sync_database(kernel, kernel_pid, database)?;
12734 Ok(json!(database
12735 .connection
12736 .total_changes()
12737 .saturating_sub(before)))
12738}
12739
12740fn sqlite_query_database(
12741 process: &mut ActiveProcess,
12742 request: &JavascriptSyncRpcRequest,
12743) -> Result<Value, SidecarError> {
12744 let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.query database id")?;
12745 let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.query sql")?;
12746 let params = request.args.get(2);
12747 let options = request.args.get(3);
12748 let return_arrays = sqlite_option_bool(options, "returnArrays").unwrap_or(false);
12749 let read_bigints = sqlite_option_bool(options, "readBigInts").unwrap_or(false);
12750 let database = sqlite_database_mut(process, database_id)?;
12751 sqlite_query_rows(
12752 &mut database.connection,
12753 sql,
12754 params,
12755 return_arrays,
12756 read_bigints,
12757 true,
12758 false,
12759 )
12760}
12761
12762fn sqlite_prepare_statement(
12763 process: &mut ActiveProcess,
12764 request: &JavascriptSyncRpcRequest,
12765) -> Result<Value, SidecarError> {
12766 ensure_per_process_state_handle_capacity(process.sqlite_statements.len(), "sqlite statement")?;
12767 let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.prepare database id")?;
12768 let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.prepare sql")?;
12769 let _ = sqlite_database(process, database_id)?;
12770 process.next_sqlite_statement_id += 1;
12771 let statement_id = process.next_sqlite_statement_id;
12772 process.sqlite_statements.insert(
12773 statement_id,
12774 ActiveSqliteStatement {
12775 database_id,
12776 sql: sql.to_owned(),
12777 return_arrays: false,
12778 read_bigints: false,
12779 allow_bare_named_parameters: false,
12780 allow_unknown_named_parameters: false,
12781 },
12782 );
12783 Ok(json!(statement_id))
12784}
12785
12786fn sqlite_run_statement(
12787 kernel: &mut SidecarKernel,
12788 process: &mut ActiveProcess,
12789 request: &JavascriptSyncRpcRequest,
12790) -> Result<Value, SidecarError> {
12791 let statement_id =
12792 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.run statement id")?;
12793 let params = request.args.get(1);
12794 let statement_state = sqlite_statement(process, statement_id)?.clone();
12795 let kernel_pid = process.kernel_pid;
12796 let database = sqlite_database_mut(process, statement_state.database_id)?;
12797 let before = database.connection.total_changes();
12798 {
12799 let mut statement = database
12800 .connection
12801 .prepare(&statement_state.sql)
12802 .map_err(sqlite_error)?;
12803 bind_sqlite_parameters(
12804 &mut statement,
12805 params,
12806 statement_state.allow_bare_named_parameters,
12807 statement_state.allow_unknown_named_parameters,
12808 )?;
12809 statement.raw_execute().map_err(sqlite_error)?;
12810 }
12811 let changes = database.connection.total_changes().saturating_sub(before);
12812 let last_insert_rowid = database.connection.last_insert_rowid();
12813 mark_sqlite_mutation(database, &statement_state.sql);
12814 sqlite_sync_database(kernel, kernel_pid, database)?;
12815 let result = json!({
12816 "changes": changes,
12817 "lastInsertRowid": encode_sqlite_integer(last_insert_rowid, true),
12818 });
12819 Ok(result)
12820}
12821
12822fn sqlite_get_statement(
12823 process: &mut ActiveProcess,
12824 request: &JavascriptSyncRpcRequest,
12825) -> Result<Value, SidecarError> {
12826 let statement_id =
12827 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.get statement id")?;
12828 let params = request.args.get(1);
12829 let statement_state = sqlite_statement(process, statement_id)?.clone();
12830 let database = sqlite_database_mut(process, statement_state.database_id)?;
12831 let rows = sqlite_query_rows(
12832 &mut database.connection,
12833 &statement_state.sql,
12834 params,
12835 statement_state.return_arrays,
12836 statement_state.read_bigints,
12837 statement_state.allow_bare_named_parameters,
12838 statement_state.allow_unknown_named_parameters,
12839 )?;
12840 Ok(rows
12841 .as_array()
12842 .and_then(|rows| rows.first().cloned())
12843 .unwrap_or(Value::Null))
12844}
12845
12846fn sqlite_all_statement(
12847 process: &mut ActiveProcess,
12848 request: &JavascriptSyncRpcRequest,
12849) -> Result<Value, SidecarError> {
12850 let statement_id =
12851 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.all statement id")?;
12852 let params = request.args.get(1);
12853 let statement_state = sqlite_statement(process, statement_id)?.clone();
12854 let database = sqlite_database_mut(process, statement_state.database_id)?;
12855 sqlite_query_rows(
12856 &mut database.connection,
12857 &statement_state.sql,
12858 params,
12859 statement_state.return_arrays,
12860 statement_state.read_bigints,
12861 statement_state.allow_bare_named_parameters,
12862 statement_state.allow_unknown_named_parameters,
12863 )
12864}
12865
12866fn sqlite_statement_columns(
12867 process: &mut ActiveProcess,
12868 request: &JavascriptSyncRpcRequest,
12869) -> Result<Value, SidecarError> {
12870 let statement_id =
12871 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.columns statement id")?;
12872 let statement_state = sqlite_statement(process, statement_id)?.clone();
12873 let database = sqlite_database_mut(process, statement_state.database_id)?;
12874 let statement = database
12875 .connection
12876 .prepare(&statement_state.sql)
12877 .map_err(sqlite_error)?;
12878 Ok(Value::Array(
12879 statement
12880 .column_names()
12881 .iter()
12882 .map(|name| json!({ "name": name }))
12883 .collect(),
12884 ))
12885}
12886
12887fn sqlite_query_rows(
12888 connection: &mut SqliteConnection,
12889 sql: &str,
12890 params: Option<&Value>,
12891 return_arrays: bool,
12892 read_bigints: bool,
12893 allow_bare_named_parameters: bool,
12894 allow_unknown_named_parameters: bool,
12895) -> Result<Value, SidecarError> {
12896 let mut statement = connection.prepare(sql).map_err(sqlite_error)?;
12897 let column_names = statement
12898 .column_names()
12899 .iter()
12900 .map(|name| (*name).to_owned())
12901 .collect::<Vec<_>>();
12902 let column_count = statement.column_count();
12903 bind_sqlite_parameters(
12904 &mut statement,
12905 params,
12906 allow_bare_named_parameters,
12907 allow_unknown_named_parameters,
12908 )?;
12909 let mut rows = statement.raw_query();
12910 let mut encoded_rows = Vec::new();
12911 while let Some(row) = rows.next().map_err(sqlite_error)? {
12912 encoded_rows.push(encode_sqlite_row(
12913 row,
12914 &column_names,
12915 column_count,
12916 return_arrays,
12917 read_bigints,
12918 )?);
12919 }
12920 Ok(Value::Array(encoded_rows))
12921}
12922
12923fn encode_sqlite_row(
12924 row: &rusqlite::Row<'_>,
12925 column_names: &[String],
12926 column_count: usize,
12927 return_arrays: bool,
12928 read_bigints: bool,
12929) -> Result<Value, SidecarError> {
12930 if return_arrays {
12931 let mut values = Vec::with_capacity(column_count);
12932 for index in 0..column_count {
12933 values.push(encode_sqlite_value_ref(
12934 row.get_ref(index).map_err(sqlite_error)?,
12935 read_bigints,
12936 )?);
12937 }
12938 return Ok(Value::Array(values));
12939 }
12940
12941 let mut object = Map::with_capacity(column_count);
12942 for (index, name) in column_names.iter().enumerate() {
12943 object.insert(
12944 name.clone(),
12945 encode_sqlite_value_ref(row.get_ref(index).map_err(sqlite_error)?, read_bigints)?,
12946 );
12947 }
12948 Ok(Value::Object(object))
12949}
12950
12951fn encode_sqlite_value_ref(
12952 value: SqliteValueRef<'_>,
12953 read_bigints: bool,
12954) -> Result<Value, SidecarError> {
12955 Ok(match value {
12956 SqliteValueRef::Null => Value::Null,
12957 SqliteValueRef::Integer(number) => encode_sqlite_integer(number, read_bigints),
12958 SqliteValueRef::Real(number) => json!(number),
12959 SqliteValueRef::Text(text) => Value::String(String::from_utf8_lossy(text).into_owned()),
12960 SqliteValueRef::Blob(bytes) => json!({
12961 "__agentosSqliteType": "uint8array",
12962 "value": base64::engine::general_purpose::STANDARD.encode(bytes),
12963 }),
12964 })
12965}
12966
12967fn encode_sqlite_integer(number: i64, read_bigints: bool) -> Value {
12968 if read_bigints || number.abs() > SQLITE_JS_SAFE_INTEGER_MAX {
12969 json!({
12970 "__agentosSqliteType": "bigint",
12971 "value": number.to_string(),
12972 })
12973 } else {
12974 json!(number)
12975 }
12976}
12977
12978fn bind_sqlite_parameters(
12979 statement: &mut SqliteStatement<'_>,
12980 params: Option<&Value>,
12981 allow_bare_named_parameters: bool,
12982 allow_unknown_named_parameters: bool,
12983) -> Result<(), SidecarError> {
12984 let Some(params) = params else {
12985 return Ok(());
12986 };
12987 match params {
12988 Value::Null => Ok(()),
12989 Value::Array(values) => {
12990 for (index, value) in values.iter().enumerate() {
12991 statement
12992 .raw_bind_parameter(index + 1, decode_sqlite_parameter(value)?)
12993 .map_err(sqlite_error)?;
12994 }
12995 Ok(())
12996 }
12997 Value::Object(map)
12998 if map
12999 .get("__agentosSqliteType")
13000 .and_then(Value::as_str)
13001 .is_none() =>
13002 {
13003 for (key, value) in map {
13004 let index =
13005 resolve_sqlite_parameter_index(statement, key, allow_bare_named_parameters)?;
13006 let Some(index) = index else {
13007 if allow_unknown_named_parameters {
13008 continue;
13009 }
13010 return Err(SidecarError::InvalidState(format!(
13011 "sqlite named parameter not found: {key}"
13012 )));
13013 };
13014 statement
13015 .raw_bind_parameter(index, decode_sqlite_parameter(value)?)
13016 .map_err(sqlite_error)?;
13017 }
13018 Ok(())
13019 }
13020 other => statement
13021 .raw_bind_parameter(1, decode_sqlite_parameter(other)?)
13022 .map_err(sqlite_error),
13023 }
13024}
13025
13026fn resolve_sqlite_parameter_index(
13027 statement: &mut SqliteStatement<'_>,
13028 key: &str,
13029 allow_bare_named_parameters: bool,
13030) -> Result<Option<usize>, SidecarError> {
13031 let mut candidates = vec![key.to_owned()];
13032 if allow_bare_named_parameters
13033 && !key.starts_with(':')
13034 && !key.starts_with('@')
13035 && !key.starts_with('$')
13036 {
13037 candidates.push(format!(":{key}"));
13038 candidates.push(format!("@{key}"));
13039 candidates.push(format!("${key}"));
13040 }
13041 for candidate in candidates {
13042 if let Some(index) = statement
13043 .parameter_index(&candidate)
13044 .map_err(sqlite_error)?
13045 {
13046 return Ok(Some(index));
13047 }
13048 }
13049 Ok(None)
13050}
13051
13052fn decode_sqlite_parameter(value: &Value) -> Result<rusqlite::types::Value, SidecarError> {
13053 Ok(match value {
13054 Value::Null => rusqlite::types::Value::Null,
13055 Value::Bool(value) => rusqlite::types::Value::Integer(i64::from(*value)),
13056 Value::Number(value) => match (value.as_i64(), value.as_f64()) {
13057 (Some(integer), _) => rusqlite::types::Value::Integer(integer),
13058 (_, Some(real)) => rusqlite::types::Value::Real(real),
13059 _ => {
13060 return Err(SidecarError::InvalidState(String::from(
13061 "sqlite parameter number is not representable",
13062 )));
13063 }
13064 },
13065 Value::String(value) => rusqlite::types::Value::Text(value.clone()),
13066 Value::Array(_) => {
13067 return Err(SidecarError::InvalidState(String::from(
13068 "sqlite parameters do not support nested arrays",
13069 )));
13070 }
13071 Value::Object(map) => match map.get("__agentosSqliteType").and_then(Value::as_str) {
13072 Some("bigint") => rusqlite::types::Value::Integer(
13073 map.get("value")
13074 .and_then(Value::as_str)
13075 .ok_or_else(|| {
13076 SidecarError::InvalidState(String::from(
13077 "sqlite bigint parameter missing string value",
13078 ))
13079 })?
13080 .parse::<i64>()
13081 .map_err(|error| {
13082 SidecarError::InvalidState(format!(
13083 "sqlite bigint parameter is not a signed 64-bit integer: {error}"
13084 ))
13085 })?,
13086 ),
13087 Some("uint8array") => rusqlite::types::Value::Blob(
13088 base64::engine::general_purpose::STANDARD
13089 .decode(map.get("value").and_then(Value::as_str).ok_or_else(|| {
13090 SidecarError::InvalidState(String::from(
13091 "sqlite blob parameter missing base64 value",
13092 ))
13093 })?)
13094 .map_err(|error| {
13095 SidecarError::InvalidState(format!(
13096 "sqlite blob parameter contains invalid base64: {error}"
13097 ))
13098 })?,
13099 ),
13100 Some(other) => {
13101 return Err(SidecarError::InvalidState(format!(
13102 "unsupported sqlite tagged parameter type {other}"
13103 )));
13104 }
13105 None => {
13106 return Err(SidecarError::InvalidState(String::from(
13107 "sqlite named parameter objects must be passed as the top-level params object",
13108 )));
13109 }
13110 },
13111 })
13112}
13113
13114fn close_sqlite_database(
13115 kernel: &mut SidecarKernel,
13116 process: &mut ActiveProcess,
13117 database_id: u64,
13118) -> Result<(), SidecarError> {
13119 let mut database = process
13120 .sqlite_databases
13121 .remove(&database_id)
13122 .ok_or_else(|| {
13123 SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13124 })?;
13125 process
13126 .sqlite_statements
13127 .retain(|_, statement| statement.database_id != database_id);
13128 sqlite_sync_database(kernel, process.kernel_pid, &mut database)?;
13129 let host_path = database.host_path.clone();
13130 drop(database);
13131 cleanup_sqlite_host_artifacts(host_path.as_deref())?;
13132 Ok(())
13133}
13134
13135fn ensure_per_process_state_handle_capacity(len: usize, label: &str) -> Result<(), SidecarError> {
13136 if len >= MAX_PER_PROCESS_STATE_HANDLES {
13137 return Err(SidecarError::InvalidState(format!(
13138 "{label} handle limit exceeded: limit is {MAX_PER_PROCESS_STATE_HANDLES}"
13139 )));
13140 }
13141 Ok(())
13142}
13143
13144fn sqlite_sync_database(
13145 kernel: &mut SidecarKernel,
13146 kernel_pid: u32,
13147 database: &mut ActiveSqliteDatabase,
13148) -> Result<(), SidecarError> {
13149 if !database.dirty
13150 || database.transaction_depth > 0
13151 || database.read_only
13152 || database.host_path.is_none()
13153 || database.vm_path.is_none()
13154 {
13155 return Ok(());
13156 }
13157
13158 let _ = database
13159 .connection
13160 .execute_batch("PRAGMA wal_checkpoint(TRUNCATE)");
13161 let host_path = database.host_path.as_ref().expect("sqlite host path");
13162 if !host_path.exists() {
13163 return Ok(());
13164 }
13165 ensure_vm_parent_dir(
13166 kernel,
13167 kernel_pid,
13168 database.vm_path.as_deref().expect("sqlite vm path"),
13169 )?;
13170 let contents = fs::read(host_path).map_err(|error| {
13171 SidecarError::Io(format!(
13172 "failed to read sqlite temp database {}: {error}",
13173 host_path.display()
13174 ))
13175 })?;
13176 kernel
13177 .write_file_for_process(
13178 EXECUTION_DRIVER_NAME,
13179 kernel_pid,
13180 database.vm_path.as_deref().expect("sqlite vm path"),
13181 contents,
13182 None,
13183 )
13184 .map_err(kernel_error)?;
13185 database.dirty = false;
13186 Ok(())
13187}
13188
13189fn cleanup_sqlite_host_artifacts(host_path: Option<&Path>) -> Result<(), SidecarError> {
13190 let Some(host_path) = host_path else {
13191 return Ok(());
13192 };
13193 let parent = host_path.parent().map(PathBuf::from);
13194 for suffix in ["", "-wal", "-shm"] {
13195 let path = PathBuf::from(format!("{}{}", host_path.display(), suffix));
13196 if path.exists() {
13197 fs::remove_file(&path).map_err(|error| {
13198 SidecarError::Io(format!(
13199 "failed to remove sqlite temp artifact {}: {error}",
13200 path.display()
13201 ))
13202 })?;
13203 }
13204 }
13205 if let Some(parent) = parent {
13206 let _ = fs::remove_dir_all(parent);
13207 }
13208 Ok(())
13209}
13210
13211fn ensure_vm_parent_dir(
13212 kernel: &mut SidecarKernel,
13213 kernel_pid: u32,
13214 path: &str,
13215) -> Result<(), SidecarError> {
13216 let parent = dirname(path);
13217 if parent == "/" || parent == "." {
13218 return Ok(());
13219 }
13220 let mut current = String::new();
13221 for segment in parent.split('/').filter(|segment| !segment.is_empty()) {
13222 current.push('/');
13223 current.push_str(segment);
13224 if !kernel
13225 .exists_for_process(EXECUTION_DRIVER_NAME, kernel_pid, ¤t)
13226 .map_err(kernel_error)?
13227 {
13228 kernel
13229 .mkdir_for_process(EXECUTION_DRIVER_NAME, kernel_pid, ¤t, false, None)
13230 .map_err(kernel_error)?;
13231 }
13232 }
13233 Ok(())
13234}
13235
13236fn sqlite_database(
13237 process: &ActiveProcess,
13238 database_id: u64,
13239) -> Result<&ActiveSqliteDatabase, SidecarError> {
13240 process.sqlite_databases.get(&database_id).ok_or_else(|| {
13241 SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13242 })
13243}
13244
13245fn sqlite_database_mut(
13246 process: &mut ActiveProcess,
13247 database_id: u64,
13248) -> Result<&mut ActiveSqliteDatabase, SidecarError> {
13249 process
13250 .sqlite_databases
13251 .get_mut(&database_id)
13252 .ok_or_else(|| {
13253 SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13254 })
13255}
13256
13257fn sqlite_statement(
13258 process: &ActiveProcess,
13259 statement_id: u64,
13260) -> Result<&ActiveSqliteStatement, SidecarError> {
13261 process.sqlite_statements.get(&statement_id).ok_or_else(|| {
13262 SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
13263 })
13264}
13265
13266fn sqlite_statement_mut(
13267 process: &mut ActiveProcess,
13268 statement_id: u64,
13269) -> Result<&mut ActiveSqliteStatement, SidecarError> {
13270 process
13271 .sqlite_statements
13272 .get_mut(&statement_id)
13273 .ok_or_else(|| {
13274 SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
13275 })
13276}
13277
13278fn mark_sqlite_mutation(database: &mut ActiveSqliteDatabase, sql: &str) {
13279 let normalized = sql.trim_start().to_ascii_lowercase();
13280 if normalized.starts_with("begin") || normalized.starts_with("savepoint") {
13281 database.dirty = true;
13282 database.transaction_depth += 1;
13283 return;
13284 }
13285 if normalized.starts_with("commit") || normalized.starts_with("release savepoint") {
13286 database.dirty = true;
13287 database.transaction_depth = database.transaction_depth.saturating_sub(1);
13288 return;
13289 }
13290 if normalized.starts_with("rollback") && !normalized.starts_with("rollback to") {
13291 database.dirty = true;
13292 database.transaction_depth = database.transaction_depth.saturating_sub(1);
13293 return;
13294 }
13295 if normalized.starts_with("insert")
13296 || normalized.starts_with("update")
13297 || normalized.starts_with("delete")
13298 || normalized.starts_with("replace")
13299 || normalized.starts_with("create")
13300 || normalized.starts_with("alter")
13301 || normalized.starts_with("drop")
13302 || normalized.starts_with("vacuum")
13303 || normalized.starts_with("reindex")
13304 || normalized.starts_with("analyze")
13305 || normalized.starts_with("attach")
13306 || normalized.starts_with("detach")
13307 || normalized.starts_with("pragma")
13308 {
13309 database.dirty = true;
13310 }
13311}
13312
13313fn sqlite_option_bool(options: Option<&Value>, key: &str) -> Option<bool> {
13314 options
13315 .and_then(|value| value.get(key))
13316 .and_then(Value::as_bool)
13317}
13318
13319fn sqlite_option_u64(options: Option<&Value>, key: &str) -> Option<u64> {
13320 options
13321 .and_then(|value| value.get(key))
13322 .and_then(Value::as_u64)
13323}
13324
13325fn sqlite_error(error: rusqlite::Error) -> SidecarError {
13326 SidecarError::InvalidState(format!("sqlite error: {error}"))
13327}
13328
13329pub(crate) fn javascript_sync_rpc_arg_str<'a>(
13330 args: &'a [Value],
13331 index: usize,
13332 label: &str,
13333) -> Result<&'a str, SidecarError> {
13334 args.get(index)
13335 .and_then(Value::as_str)
13336 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a string argument")))
13337}
13338
13339pub(crate) fn javascript_sync_rpc_arg_bool(
13340 args: &[Value],
13341 index: usize,
13342 label: &str,
13343) -> Result<bool, SidecarError> {
13344 args.get(index)
13345 .and_then(Value::as_bool)
13346 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a boolean argument")))
13347}
13348
13349pub(crate) fn javascript_sync_rpc_encoding(args: &[Value]) -> Option<String> {
13350 args.get(1).and_then(|value| {
13351 value.as_str().map(str::to_owned).or_else(|| {
13352 value
13353 .get("encoding")
13354 .and_then(Value::as_str)
13355 .map(str::to_owned)
13356 })
13357 })
13358}
13359
13360pub(crate) fn javascript_sync_rpc_option_bool(
13361 args: &[Value],
13362 index: usize,
13363 key: &str,
13364) -> Option<bool> {
13365 let value = args.get(index)?;
13366 if key == "recursive" {
13367 if let Some(boolean) = value.as_bool() {
13368 return Some(boolean);
13369 }
13370 }
13371 value.get(key).and_then(Value::as_bool)
13372}
13373
13374pub(crate) fn javascript_sync_rpc_option_u32(
13375 args: &[Value],
13376 index: usize,
13377 key: &str,
13378) -> Result<Option<u32>, SidecarError> {
13379 let Some(value) = args.get(index).and_then(|value| {
13380 if value.is_object() {
13381 value.get(key)
13382 } else if key == "mode" && value.is_number() {
13383 Some(value)
13384 } else {
13385 None
13386 }
13387 }) else {
13388 return Ok(None);
13389 };
13390 if value.is_null() {
13391 return Ok(None);
13392 }
13393
13394 let numeric = value
13395 .as_u64()
13396 .or_else(|| {
13397 value
13398 .as_f64()
13399 .filter(|number| number.is_finite() && *number >= 0.0)
13400 .map(|number| number as u64)
13401 })
13402 .ok_or_else(|| SidecarError::InvalidState(format!("{key} must be numeric")))?;
13403
13404 u32::try_from(numeric)
13405 .map(Some)
13406 .map_err(|_| SidecarError::InvalidState(format!("{key} must fit within u32")))
13407}
13408
13409pub(crate) fn javascript_sync_rpc_arg_u32(
13410 args: &[Value],
13411 index: usize,
13412 label: &str,
13413) -> Result<u32, SidecarError> {
13414 let value = javascript_sync_rpc_arg_u64(args, index, label)?;
13415 u32::try_from(value)
13416 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
13417}
13418
13419pub(crate) fn javascript_sync_rpc_arg_i32(
13420 args: &[Value],
13421 index: usize,
13422 label: &str,
13423) -> Result<i32, SidecarError> {
13424 let Some(value) = args.get(index) else {
13425 return Err(SidecarError::InvalidState(format!("{label} is required")));
13426 };
13427
13428 let numeric = value
13429 .as_i64()
13430 .or_else(|| {
13431 value
13432 .as_f64()
13433 .filter(|number| number.is_finite())
13434 .map(|number| number as i64)
13435 })
13436 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))?;
13437
13438 i32::try_from(numeric)
13439 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within i32")))
13440}
13441
13442pub(crate) fn javascript_sync_rpc_arg_u32_optional(
13443 args: &[Value],
13444 index: usize,
13445 label: &str,
13446) -> Result<Option<u32>, SidecarError> {
13447 javascript_sync_rpc_arg_u64_optional(args, index, label)?
13448 .map(|value| {
13449 u32::try_from(value)
13450 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
13451 })
13452 .transpose()
13453}
13454
13455pub(crate) fn javascript_sync_rpc_arg_u64(
13456 args: &[Value],
13457 index: usize,
13458 label: &str,
13459) -> Result<u64, SidecarError> {
13460 let Some(value) = args.get(index) else {
13461 return Err(SidecarError::InvalidState(format!("{label} is required")));
13462 };
13463
13464 value
13465 .as_u64()
13466 .or_else(|| {
13467 value
13468 .as_f64()
13469 .filter(|number| number.is_finite() && *number >= 0.0)
13470 .map(|number| number as u64)
13471 })
13472 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))
13473}
13474
13475pub(crate) fn javascript_sync_rpc_arg_u64_optional(
13476 args: &[Value],
13477 index: usize,
13478 label: &str,
13479) -> Result<Option<u64>, SidecarError> {
13480 let Some(value) = args.get(index) else {
13481 return Ok(None);
13482 };
13483 if value.is_null() {
13484 return Ok(None);
13485 }
13486 javascript_sync_rpc_arg_u64(args, index, label).map(Some)
13487}
13488
13489pub(crate) fn javascript_sync_rpc_bytes_arg(
13490 args: &[Value],
13491 index: usize,
13492 label: &str,
13493) -> Result<Vec<u8>, SidecarError> {
13494 let Some(value) = args.get(index) else {
13495 return Err(SidecarError::InvalidState(format!("{label} is required")));
13496 };
13497
13498 if let Some(text) = value.as_str() {
13499 return Ok(text.as_bytes().to_vec());
13500 }
13501
13502 let Some(base64_value) = value
13503 .get("__agentOsType")
13504 .and_then(Value::as_str)
13505 .filter(|kind| *kind == "bytes")
13506 .and_then(|_| value.get("base64"))
13507 .and_then(Value::as_str)
13508 else {
13509 return Err(SidecarError::InvalidState(format!(
13510 "{label} must be a string or encoded bytes payload"
13511 )));
13512 };
13513
13514 base64::engine::general_purpose::STANDARD
13515 .decode(base64_value)
13516 .map_err(|error| {
13517 SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
13518 })
13519}
13520
13521pub(crate) fn javascript_sync_rpc_bytes_value(bytes: &[u8]) -> Value {
13522 json!({
13523 "__agentOsType": "bytes",
13524 "base64": base64::engine::general_purpose::STANDARD.encode(bytes),
13525 })
13526}
13527
13528#[derive(Debug, Deserialize)]
13529struct KernelPollFdRequest {
13530 fd: u32,
13531 events: u16,
13532}
13533
13534#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
13535struct KernelPollFdResponse {
13536 fd: u32,
13537 events: u16,
13538 revents: u16,
13539}
13540
13541fn javascript_sync_rpc_base64_arg(
13542 args: &[Value],
13543 index: usize,
13544 label: &str,
13545) -> Result<Vec<u8>, SidecarError> {
13546 let value = javascript_sync_rpc_arg_str(args, index, label)?;
13547 base64::engine::general_purpose::STANDARD
13548 .decode(value)
13549 .map_err(|error| {
13550 SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
13551 })
13552}
13553
13554pub(crate) fn service_javascript_sync_rpc<B>(
13555 request: JavascriptSyncRpcServiceRequest<'_, B>,
13556) -> Result<Value, SidecarError>
13557where
13558 B: NativeSidecarBridge + Send + 'static,
13559 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
13560{
13561 let JavascriptSyncRpcServiceRequest {
13562 bridge,
13563 vm_id,
13564 dns,
13565 socket_paths,
13566 kernel,
13567 process,
13568 sync_request: request,
13569 resource_limits,
13570 network_counts,
13571 } = request;
13572 match request.method.as_str() {
13573 "_resolveModule"
13576 | "_resolveModuleSync"
13577 | "__resolve_module"
13578 | "_batchResolveModules"
13579 | "__batch_resolve_modules"
13580 | "_loadFile"
13581 | "_loadFileSync"
13582 | "__load_file"
13583 | "_moduleFormat"
13584 | "__module_format" => service_javascript_module_sync_rpc(kernel, process, request),
13585 "_loadPolyfill" | "__load_polyfill" => {
13587 service_javascript_internal_bridge_sync_rpc(process, request)
13588 }
13589 "__kernel_stdin_read" => match &process.execution {
13590 ActiveExecution::Javascript(execution) => execution
13591 .read_kernel_stdin_sync_rpc(request)
13592 .map_err(|error| SidecarError::Execution(error.to_string())),
13593 ActiveExecution::Python(_) | ActiveExecution::Wasm(_) | ActiveExecution::Tool(_) => {
13594 service_javascript_kernel_stdin_sync_rpc(kernel, process, request)
13595 }
13596 },
13597 "__kernel_stdio_write" => {
13598 service_javascript_kernel_stdio_write_sync_rpc(kernel, process, request)
13599 }
13600 "__kernel_poll" => service_javascript_kernel_poll_sync_rpc(kernel, process, request),
13601 "__pty_set_raw_mode" => {
13602 service_javascript_pty_set_raw_mode_sync_rpc(kernel, process, request)
13603 }
13604 "crypto.hashDigest"
13605 | "crypto.hmacDigest"
13606 | "crypto.pbkdf2"
13607 | "crypto.scrypt"
13608 | "crypto.cipheriv"
13609 | "crypto.decipheriv"
13610 | "crypto.cipherivCreate"
13611 | "crypto.cipherivUpdate"
13612 | "crypto.cipherivFinal"
13613 | "crypto.sign"
13614 | "crypto.verify"
13615 | "crypto.asymmetricOp"
13616 | "crypto.createKeyObject"
13617 | "crypto.generateKeyPairSync"
13618 | "crypto.generateKeySync"
13619 | "crypto.generatePrimeSync"
13620 | "crypto.diffieHellman"
13621 | "crypto.diffieHellmanGroup"
13622 | "crypto.diffieHellmanSessionCreate"
13623 | "crypto.diffieHellmanSessionCall"
13624 | "crypto.diffieHellmanSessionDestroy"
13625 | "crypto.subtle" => service_javascript_crypto_sync_rpc(process, request),
13626 "dns.lookup" | "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
13627 service_javascript_dns_sync_rpc(bridge, kernel, vm_id, dns, request)
13628 }
13629 "net.http_listen" | "net.http_close" | "net.http_wait" | "net.http_respond" => {
13630 service_javascript_net_sync_rpc(JavascriptNetSyncRpcServiceRequest {
13631 bridge,
13632 vm_id,
13633 dns,
13634 socket_paths,
13635 kernel,
13636 process,
13637 sync_request: request,
13638 resource_limits,
13639 network_counts,
13640 })
13641 }
13642 "net.http2_server_listen"
13643 | "net.http2_server_poll"
13644 | "net.http2_server_close"
13645 | "net.http2_server_respond"
13646 | "net.http2_server_wait"
13647 | "net.http2_session_connect"
13648 | "net.http2_session_request"
13649 | "net.http2_session_settings"
13650 | "net.http2_session_set_local_window_size"
13651 | "net.http2_session_goaway"
13652 | "net.http2_session_close"
13653 | "net.http2_session_destroy"
13654 | "net.http2_session_poll"
13655 | "net.http2_session_wait"
13656 | "net.http2_stream_respond"
13657 | "net.http2_stream_push_stream"
13658 | "net.http2_stream_write"
13659 | "net.http2_stream_end"
13660 | "net.http2_stream_close"
13661 | "net.http2_stream_pause"
13662 | "net.http2_stream_resume"
13663 | "net.http2_stream_respond_with_file" => {
13664 service_javascript_http2_sync_rpc(JavascriptHttp2SyncRpcServiceRequest {
13665 bridge,
13666 kernel,
13667 vm_id,
13668 dns,
13669 socket_paths,
13670 process,
13671 sync_request: request,
13672 resource_limits,
13673 network_counts,
13674 })
13675 }
13676 "net.connect"
13677 | "net.reserve_tcp_port"
13678 | "net.release_tcp_port"
13679 | "net.listen"
13680 | "net.poll"
13681 | "net.socket_wait_connect"
13682 | "net.socket_read"
13683 | "net.socket_set_no_delay"
13684 | "net.socket_set_keep_alive"
13685 | "net.socket_upgrade_tls"
13686 | "net.socket_get_tls_client_hello"
13687 | "net.socket_tls_query"
13688 | "net.server_poll"
13689 | "net.server_accept"
13690 | "net.server_connections"
13691 | "net.upgrade_socket_write"
13692 | "net.upgrade_socket_end"
13693 | "net.upgrade_socket_destroy"
13694 | "net.write"
13695 | "net.shutdown"
13696 | "net.destroy"
13697 | "net.server_close"
13698 | "tls.get_ciphers" => {
13699 service_javascript_net_sync_rpc(JavascriptNetSyncRpcServiceRequest {
13700 bridge,
13701 vm_id,
13702 dns,
13703 socket_paths,
13704 kernel,
13705 process,
13706 sync_request: request,
13707 resource_limits,
13708 network_counts,
13709 })
13710 }
13711 "dgram.createSocket"
13712 | "dgram.bind"
13713 | "dgram.send"
13714 | "dgram.poll"
13715 | "dgram.close"
13716 | "dgram.address"
13717 | "dgram.setBufferSize"
13718 | "dgram.getBufferSize" => {
13719 service_javascript_dgram_sync_rpc(JavascriptDgramSyncRpcServiceRequest {
13720 bridge,
13721 kernel,
13722 vm_id,
13723 dns,
13724 socket_paths,
13725 process,
13726 sync_request: request,
13727 resource_limits,
13728 network_counts,
13729 })
13730 }
13731 "sqlite.constants"
13732 | "sqlite.open"
13733 | "sqlite.close"
13734 | "sqlite.exec"
13735 | "sqlite.query"
13736 | "sqlite.prepare"
13737 | "sqlite.location"
13738 | "sqlite.checkpoint"
13739 | "sqlite.statement.run"
13740 | "sqlite.statement.get"
13741 | "sqlite.statement.all"
13742 | "sqlite.statement.iterate"
13743 | "sqlite.statement.columns"
13744 | "sqlite.statement.setReturnArrays"
13745 | "sqlite.statement.setReadBigInts"
13746 | "sqlite.statement.setAllowBareNamedParameters"
13747 | "sqlite.statement.setAllowUnknownNamedParameters"
13748 | "sqlite.statement.finalize" => {
13749 service_javascript_sqlite_sync_rpc(kernel, process, request)
13750 }
13751 "process.kill" => {
13752 let target_pid =
13753 javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
13754 let signal = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
13755 let parsed_signal = parse_signal(signal)?;
13756 if parsed_signal == 0 {
13757 kernel
13758 .signal_process(EXECUTION_DRIVER_NAME, target_pid, parsed_signal)
13759 .map_err(kernel_error)?;
13760 return Ok(Value::Null);
13761 }
13762 let process_pid = i32::try_from(process.kernel_pid)
13763 .map_err(|_| SidecarError::InvalidState("process pid exceeds i32".into()))?;
13764 if target_pid != process_pid {
13765 return Err(SidecarError::InvalidState(format!(
13766 "unknown process pid {target_pid}"
13767 )));
13768 }
13769 process.pending_self_signal_exit = None;
13770 if parsed_signal != 0
13771 && !matches!(
13772 canonical_signal_name(parsed_signal),
13773 Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
13774 )
13775 {
13776 process.pending_self_signal_exit = Some(parsed_signal);
13777 }
13778 Ok(json!({
13779 "self": true,
13780 "action": "default",
13781 }))
13782 }
13783 "process.umask" => {
13784 let new_mask = javascript_sync_rpc_arg_u32_optional(&request.args, 0, "process umask")?;
13785 kernel
13786 .umask(EXECUTION_DRIVER_NAME, process.kernel_pid, new_mask)
13787 .map(|mask| json!(mask))
13788 .map_err(kernel_error)
13789 }
13790 "fs.chmodSync" | "fs.promises.chmod" => {
13791 let response =
13792 service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request)?;
13793 mirror_process_chmod_to_host(process, request)?;
13794 Ok(response)
13795 }
13796 _ => service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request),
13797 }
13798}
13799
13800fn service_javascript_internal_bridge_sync_rpc(
13801 process: &ActiveProcess,
13802 request: &JavascriptSyncRpcRequest,
13803) -> Result<Value, SidecarError> {
13804 let method = match request.method.as_str() {
13808 "_loadPolyfill" | "__load_polyfill" => "_loadPolyfill",
13809 other => {
13810 return Err(SidecarError::InvalidState(format!(
13811 "unsupported JavaScript internal bridge method {other}"
13812 )));
13813 }
13814 };
13815
13816 handle_internal_bridge_call_from_host_context(
13817 &process.host_cwd,
13818 &process.guest_cwd,
13819 &process.env,
13820 method,
13821 &request.args,
13822 )
13823 .ok_or_else(|| {
13824 SidecarError::InvalidState(format!(
13825 "JavaScript internal bridge method {method} returned no value"
13826 ))
13827 })
13828}
13829
13830fn mirror_process_chmod_to_host(
13831 process: &ActiveProcess,
13832 request: &JavascriptSyncRpcRequest,
13833) -> Result<(), SidecarError> {
13834 let guest_path = javascript_sync_rpc_arg_str(&request.args, 0, "filesystem chmod path")?;
13835 let mode = javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem chmod mode")? & 0o7777;
13836 let Some(host_path) = resolve_process_guest_path_to_host(process, guest_path) else {
13837 return Ok(());
13838 };
13839 if !host_path.exists() {
13840 return Ok(());
13841 }
13842 fs::set_permissions(&host_path, fs::Permissions::from_mode(mode)).map_err(|error| {
13843 SidecarError::Io(format!(
13844 "failed to mirror chmod to host path {}: {error}",
13845 host_path.display()
13846 ))
13847 })
13848}
13849
13850fn resolve_process_guest_path_to_host(
13851 process: &ActiveProcess,
13852 guest_path: &str,
13853) -> Option<PathBuf> {
13854 let normalized_guest_path = if guest_path.starts_with('/') {
13855 normalize_path(guest_path)
13856 } else {
13857 normalize_path(&format!(
13858 "{}/{}",
13859 process.guest_cwd.trim_end_matches('/'),
13860 guest_path
13861 ))
13862 };
13863 if let Some(host_path) =
13864 host_path_from_runtime_guest_mappings(&process.env, &normalized_guest_path)
13865 {
13866 return Some(host_path);
13867 }
13868 let normalized_guest_cwd = normalize_path(&process.guest_cwd);
13869 let mut host_root = normalize_host_path(&process.host_cwd);
13870 for _ in normalized_guest_cwd
13871 .trim_start_matches('/')
13872 .split('/')
13873 .filter(|segment| !segment.is_empty())
13874 {
13875 host_root = host_root.parent()?.to_path_buf();
13876 }
13877 if normalized_guest_path == "/" {
13878 Some(host_root)
13879 } else {
13880 Some(host_root.join(normalized_guest_path.trim_start_matches('/')))
13881 }
13882}
13883
13884pub(crate) fn service_javascript_crypto_sync_rpc(
13885 process: &mut ActiveProcess,
13886 request: &JavascriptSyncRpcRequest,
13887) -> Result<Value, SidecarError> {
13888 match request.method.as_str() {
13889 "crypto.hashDigest" => {
13890 let algorithm = javascript_crypto_digest_algorithm(
13891 &request.args,
13892 0,
13893 "crypto.hashDigest algorithm",
13894 )?;
13895 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hashDigest data")?;
13896 Ok(Value::String(
13897 base64::engine::general_purpose::STANDARD.encode(algorithm.digest(&data)),
13898 ))
13899 }
13900 "crypto.hmacDigest" => {
13901 let algorithm = javascript_crypto_digest_algorithm(
13902 &request.args,
13903 0,
13904 "crypto.hmacDigest algorithm",
13905 )?;
13906 let key = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hmacDigest key")?;
13907 let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.hmacDigest data")?;
13908 Ok(Value::String(
13909 base64::engine::general_purpose::STANDARD.encode(algorithm.hmac(&key, &data)?),
13910 ))
13911 }
13912 "crypto.pbkdf2" => {
13913 let password =
13914 javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.pbkdf2 password")?;
13915 let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.pbkdf2 salt")?;
13916 let iterations =
13917 javascript_sync_rpc_arg_u32(&request.args, 2, "crypto.pbkdf2 iterations")?;
13918 if iterations == 0 {
13919 return Err(SidecarError::InvalidState(String::from(
13920 "crypto.pbkdf2 iterations must be greater than zero",
13921 )));
13922 }
13923 let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
13924 &request.args,
13925 3,
13926 "crypto.pbkdf2 key length",
13927 )?)
13928 .map_err(|_| {
13929 SidecarError::InvalidState(String::from(
13930 "crypto.pbkdf2 key length must fit within usize",
13931 ))
13932 })?;
13933 let algorithm =
13934 javascript_crypto_digest_algorithm(&request.args, 4, "crypto.pbkdf2 digest")?;
13935 let mut output = vec![0u8; key_len];
13936 algorithm.pbkdf2(&password, &salt, iterations, &mut output);
13937 Ok(Value::String(
13938 base64::engine::general_purpose::STANDARD.encode(output),
13939 ))
13940 }
13941 "crypto.scrypt" => {
13942 let password =
13943 javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.scrypt password")?;
13944 let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.scrypt salt")?;
13945 let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
13946 &request.args,
13947 2,
13948 "crypto.scrypt key length",
13949 )?)
13950 .map_err(|_| {
13951 SidecarError::InvalidState(String::from(
13952 "crypto.scrypt key length must fit within usize",
13953 ))
13954 })?;
13955 let options_json =
13956 javascript_sync_rpc_arg_str(&request.args, 3, "crypto.scrypt options")?;
13957 let options: JavascriptScryptOptions =
13958 serde_json::from_str(options_json).map_err(|error| {
13959 SidecarError::InvalidState(format!(
13960 "crypto.scrypt options must be valid JSON: {error}"
13961 ))
13962 })?;
13963 let cost = options.cost.unwrap_or(DEFAULT_SCRYPT_COST);
13964 if cost == 0 || !cost.is_power_of_two() {
13965 return Err(SidecarError::InvalidState(String::from(
13966 "crypto.scrypt cost must be a positive power of two",
13967 )));
13968 }
13969 let log_n = u8::try_from(cost.ilog2()).map_err(|_| {
13970 SidecarError::InvalidState(String::from(
13971 "crypto.scrypt cost exceeds supported parameter range",
13972 ))
13973 })?;
13974 let params = ScryptParams::new(
13975 log_n,
13976 options.block_size.unwrap_or(DEFAULT_SCRYPT_BLOCK_SIZE),
13977 options
13978 .parallelization
13979 .unwrap_or(DEFAULT_SCRYPT_PARALLELIZATION),
13980 key_len,
13981 )
13982 .map_err(|error| {
13983 SidecarError::InvalidState(format!("crypto.scrypt options are invalid: {error}"))
13984 })?;
13985 let mut output = vec![0u8; key_len];
13986 scrypt(&password, &salt, ¶ms, &mut output).map_err(|error| {
13987 SidecarError::Execution(format!("crypto.scrypt failed: {error}"))
13988 })?;
13989 Ok(Value::String(
13990 base64::engine::general_purpose::STANDARD.encode(output),
13991 ))
13992 }
13993 "crypto.cipheriv" => service_javascript_crypto_cipheriv_sync_rpc(request),
13994 "crypto.decipheriv" => service_javascript_crypto_decipheriv_sync_rpc(request),
13995 "crypto.cipherivCreate" => {
13996 service_javascript_crypto_cipheriv_create_sync_rpc(process, request)
13997 }
13998 "crypto.cipherivUpdate" => {
13999 service_javascript_crypto_cipheriv_update_sync_rpc(process, request)
14000 }
14001 "crypto.cipherivFinal" => {
14002 service_javascript_crypto_cipheriv_final_sync_rpc(process, request)
14003 }
14004 "crypto.sign" => service_javascript_crypto_sign_sync_rpc(request),
14005 "crypto.verify" => service_javascript_crypto_verify_sync_rpc(request),
14006 "crypto.asymmetricOp" => service_javascript_crypto_asymmetric_op_sync_rpc(request),
14007 "crypto.createKeyObject" => service_javascript_crypto_create_key_object_sync_rpc(request),
14008 "crypto.generateKeyPairSync" => {
14009 service_javascript_crypto_generate_key_pair_sync_rpc(request)
14010 }
14011 "crypto.generateKeySync" => service_javascript_crypto_generate_key_sync_rpc(request),
14012 "crypto.generatePrimeSync" => service_javascript_crypto_generate_prime_sync_rpc(request),
14013 "crypto.diffieHellman" => service_javascript_crypto_diffie_hellman_sync_rpc(request),
14014 "crypto.diffieHellmanGroup" => {
14015 service_javascript_crypto_diffie_hellman_group_sync_rpc(request)
14016 }
14017 "crypto.diffieHellmanSessionCreate" => {
14018 service_javascript_crypto_diffie_hellman_session_create_sync_rpc(process, request)
14019 }
14020 "crypto.diffieHellmanSessionCall" => {
14021 service_javascript_crypto_diffie_hellman_session_call_sync_rpc(process, request)
14022 }
14023 "crypto.diffieHellmanSessionDestroy" => {
14024 service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(process, request)
14025 }
14026 "crypto.subtle" => service_javascript_crypto_subtle_sync_rpc(request),
14027 _ => Err(SidecarError::InvalidState(format!(
14028 "unsupported JavaScript crypto sync RPC method {}",
14029 request.method
14030 ))),
14031 }
14032}
14033
14034fn javascript_crypto_digest_algorithm(
14035 args: &[Value],
14036 index: usize,
14037 label: &str,
14038) -> Result<JavascriptCryptoDigestAlgorithm, SidecarError> {
14039 JavascriptCryptoDigestAlgorithm::parse(javascript_sync_rpc_arg_str(args, index, label)?)
14040}
14041
14042impl JavascriptCryptoDigestAlgorithm {
14043 fn parse(value: &str) -> Result<Self, SidecarError> {
14044 match value.trim().to_ascii_lowercase().replace('-', "").as_str() {
14045 "md5" => Ok(Self::Md5),
14046 "sha1" => Ok(Self::Sha1),
14047 "sha256" => Ok(Self::Sha256),
14048 "sha512" => Ok(Self::Sha512),
14049 _ => Err(SidecarError::InvalidState(format!(
14050 "unsupported crypto digest algorithm {value}"
14051 ))),
14052 }
14053 }
14054
14055 fn digest(self, data: &[u8]) -> Vec<u8> {
14056 match self {
14057 Self::Md5 => Md5::digest(data).to_vec(),
14058 Self::Sha1 => Sha1::digest(data).to_vec(),
14059 Self::Sha256 => Sha256::digest(data).to_vec(),
14060 Self::Sha512 => Sha512::digest(data).to_vec(),
14061 }
14062 }
14063
14064 fn hmac(self, key: &[u8], data: &[u8]) -> Result<Vec<u8>, SidecarError> {
14065 match self {
14066 Self::Md5 => {
14067 let mut mac = Hmac::<Md5>::new_from_slice(key).map_err(|error| {
14068 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14069 })?;
14070 mac.update(data);
14071 Ok(mac.finalize().into_bytes().to_vec())
14072 }
14073 Self::Sha1 => {
14074 let mut mac = Hmac::<Sha1>::new_from_slice(key).map_err(|error| {
14075 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14076 })?;
14077 mac.update(data);
14078 Ok(mac.finalize().into_bytes().to_vec())
14079 }
14080 Self::Sha256 => {
14081 let mut mac = Hmac::<Sha256>::new_from_slice(key).map_err(|error| {
14082 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14083 })?;
14084 mac.update(data);
14085 Ok(mac.finalize().into_bytes().to_vec())
14086 }
14087 Self::Sha512 => {
14088 let mut mac = Hmac::<Sha512>::new_from_slice(key).map_err(|error| {
14089 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14090 })?;
14091 mac.update(data);
14092 Ok(mac.finalize().into_bytes().to_vec())
14093 }
14094 }
14095 }
14096
14097 fn pbkdf2(self, password: &[u8], salt: &[u8], iterations: u32, output: &mut [u8]) {
14098 match self {
14099 Self::Md5 => pbkdf2_hmac::<Md5>(password, salt, iterations, output),
14100 Self::Sha1 => pbkdf2_hmac::<Sha1>(password, salt, iterations, output),
14101 Self::Sha256 => pbkdf2_hmac::<Sha256>(password, salt, iterations, output),
14102 Self::Sha512 => pbkdf2_hmac::<Sha512>(password, salt, iterations, output),
14103 }
14104 }
14105}
14106
14107#[derive(Debug, Clone)]
14108enum JavascriptCryptoKeyMaterial {
14109 Private(PKey<Private>),
14110 Public(PKey<Public>),
14111 Secret(Vec<u8>),
14112}
14113
14114#[derive(Debug, Clone, Deserialize, Serialize)]
14115struct JavascriptSerializedSandboxKeyObject {
14116 #[serde(rename = "type")]
14117 kind: String,
14118 #[serde(skip_serializing_if = "Option::is_none")]
14119 pem: Option<String>,
14120 #[serde(skip_serializing_if = "Option::is_none")]
14121 raw: Option<String>,
14122 #[serde(skip_serializing_if = "Option::is_none", rename = "asymmetricKeyType")]
14123 asymmetric_key_type: Option<String>,
14124 #[serde(
14125 skip_serializing_if = "Option::is_none",
14126 rename = "asymmetricKeyDetails"
14127 )]
14128 asymmetric_key_details: Option<Map<String, Value>>,
14129 #[serde(skip_serializing_if = "Option::is_none")]
14130 jwk: Option<Value>,
14131}
14132
14133#[derive(Debug, Clone)]
14134struct JavascriptDirectKeyInput {
14135 key: JavascriptCryptoKeyMaterial,
14136 padding: Option<Padding>,
14137}
14138
14139fn service_javascript_crypto_cipheriv_sync_rpc(
14140 request: &JavascriptSyncRpcRequest,
14141) -> Result<Value, SidecarError> {
14142 service_javascript_crypto_cipheriv_inner(request, false)
14143}
14144
14145fn service_javascript_crypto_decipheriv_sync_rpc(
14146 request: &JavascriptSyncRpcRequest,
14147) -> Result<Value, SidecarError> {
14148 service_javascript_crypto_cipheriv_inner(request, true)
14149}
14150
14151fn service_javascript_crypto_cipheriv_create_sync_rpc(
14152 process: &mut ActiveProcess,
14153 request: &JavascriptSyncRpcRequest,
14154) -> Result<Value, SidecarError> {
14155 ensure_per_process_state_handle_capacity(process.cipher_sessions.len(), "cipher session")?;
14156 let mode = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.cipherivCreate mode")?;
14157 let decrypt = mode == "decipher";
14158 let algorithm =
14159 javascript_sync_rpc_arg_str(&request.args, 1, "crypto.cipherivCreate algorithm")?;
14160 let key = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.cipherivCreate key")?;
14161 let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 3, "crypto.cipherivCreate iv")?;
14162 let options =
14163 javascript_sync_rpc_json_arg_optional(&request.args, 4, "crypto.cipherivCreate options")?;
14164 let auth_tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options.as_ref())?;
14165 let context = javascript_crypto_build_cipher_context(
14166 algorithm,
14167 &key,
14168 iv.as_deref(),
14169 decrypt,
14170 options.as_ref(),
14171 )?;
14172 process.next_cipher_session_id += 1;
14173 let session_id = process.next_cipher_session_id;
14174 process.cipher_sessions.insert(
14175 session_id,
14176 ActiveCipherSession {
14177 algorithm: algorithm.to_string(),
14178 auth_tag_len,
14179 context,
14180 },
14181 );
14182 Ok(json!(session_id))
14183}
14184
14185fn service_javascript_crypto_cipheriv_update_sync_rpc(
14186 process: &mut ActiveProcess,
14187 request: &JavascriptSyncRpcRequest,
14188) -> Result<Value, SidecarError> {
14189 let session_id =
14190 javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivUpdate session id")?;
14191 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.cipherivUpdate data")?;
14192 let session = process
14193 .cipher_sessions
14194 .get_mut(&session_id)
14195 .ok_or_else(|| {
14196 SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
14197 })?;
14198 let result = javascript_crypto_cipher_update(&mut session.context, &data)?;
14199 Ok(Value::String(
14200 base64::engine::general_purpose::STANDARD.encode(result),
14201 ))
14202}
14203
14204fn service_javascript_crypto_cipheriv_final_sync_rpc(
14205 process: &mut ActiveProcess,
14206 request: &JavascriptSyncRpcRequest,
14207) -> Result<Value, SidecarError> {
14208 let session_id =
14209 javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivFinal session id")?;
14210 let mut session = process.cipher_sessions.remove(&session_id).ok_or_else(|| {
14211 SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
14212 })?;
14213 let data = javascript_crypto_cipher_finalize(&mut session.context)?;
14214 let mut response = Map::new();
14215 response.insert(
14216 String::from("data"),
14217 Value::String(base64::engine::general_purpose::STANDARD.encode(data)),
14218 );
14219 if javascript_crypto_is_aead(&session.algorithm) {
14220 let mut auth_tag = vec![0_u8; session.auth_tag_len];
14221 session
14222 .context
14223 .get_tag(&mut auth_tag)
14224 .map_err(javascript_crypto_openssl_error)?;
14225 response.insert(
14226 String::from("authTag"),
14227 Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
14228 );
14229 }
14230 Ok(Value::String(serde_json::to_string(&response).map_err(
14231 |error| SidecarError::InvalidState(format!("serialize cipher final response: {error}")),
14232 )?))
14233}
14234
14235fn service_javascript_crypto_sign_sync_rpc(
14236 request: &JavascriptSyncRpcRequest,
14237) -> Result<Value, SidecarError> {
14238 let algorithm = request.args.first().and_then(Value::as_str);
14239 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.sign data")?;
14240 let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.sign key")?;
14241 let key_input =
14242 javascript_crypto_parse_direct_key_input(key_json, Some("private"), "crypto.sign key")?;
14243 let private_key = javascript_crypto_expect_private_key(key_input.key, "crypto.sign key")?;
14244 let mut signer = javascript_crypto_new_signer(algorithm, &private_key)?;
14245 if let Some(padding) = key_input.padding {
14246 signer
14247 .set_rsa_padding(padding)
14248 .map_err(javascript_crypto_openssl_error)?;
14249 }
14250 signer
14251 .update(&data)
14252 .map_err(javascript_crypto_openssl_error)?;
14253 Ok(Value::String(
14254 base64::engine::general_purpose::STANDARD.encode(
14255 signer
14256 .sign_to_vec()
14257 .map_err(javascript_crypto_openssl_error)?,
14258 ),
14259 ))
14260}
14261
14262fn service_javascript_crypto_verify_sync_rpc(
14263 request: &JavascriptSyncRpcRequest,
14264) -> Result<Value, SidecarError> {
14265 let algorithm = request.args.first().and_then(Value::as_str);
14266 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.verify data")?;
14267 let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.verify key")?;
14268 let signature = javascript_sync_rpc_base64_arg(&request.args, 3, "crypto.verify signature")?;
14269 let key_input =
14270 javascript_crypto_parse_direct_key_input(key_json, Some("public"), "crypto.verify key")?;
14271 let public_key = javascript_crypto_expect_public_key(key_input.key, "crypto.verify key")?;
14272 let mut verifier = javascript_crypto_new_verifier(algorithm, &public_key)?;
14273 if let Some(padding) = key_input.padding {
14274 verifier
14275 .set_rsa_padding(padding)
14276 .map_err(javascript_crypto_openssl_error)?;
14277 }
14278 verifier
14279 .update(&data)
14280 .map_err(javascript_crypto_openssl_error)?;
14281 Ok(json!(verifier
14282 .verify(&signature)
14283 .map_err(javascript_crypto_openssl_error)?))
14284}
14285
14286fn service_javascript_crypto_asymmetric_op_sync_rpc(
14287 request: &JavascriptSyncRpcRequest,
14288) -> Result<Value, SidecarError> {
14289 let operation = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.asymmetricOp operation")?;
14290 let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.asymmetricOp key")?;
14291 let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.asymmetricOp data")?;
14292 let expect_kind = match operation {
14293 "publicEncrypt" | "publicDecrypt" => Some("public"),
14294 "privateEncrypt" | "privateDecrypt" => Some("private"),
14295 other => {
14296 return Err(SidecarError::InvalidState(format!(
14297 "Unsupported asymmetric crypto operation: {other}"
14298 )));
14299 }
14300 };
14301 let key_input =
14302 javascript_crypto_parse_direct_key_input(key_json, expect_kind, "crypto.asymmetricOp key")?;
14303 let padding = key_input.padding.unwrap_or(Padding::PKCS1);
14304 let mut output = vec![0_u8; javascript_crypto_rsa_output_size(&key_input.key)?];
14305 let written = match (operation, key_input.key) {
14306 ("publicEncrypt", JavascriptCryptoKeyMaterial::Public(key))
14307 | ("publicDecrypt", JavascriptCryptoKeyMaterial::Public(key)) => {
14308 let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
14309 if operation == "publicEncrypt" {
14310 rsa.public_encrypt(&data, &mut output, padding)
14311 .map_err(javascript_crypto_openssl_error)?
14312 } else {
14313 rsa.public_decrypt(&data, &mut output, padding)
14314 .map_err(javascript_crypto_openssl_error)?
14315 }
14316 }
14317 ("privateEncrypt", JavascriptCryptoKeyMaterial::Private(key))
14318 | ("privateDecrypt", JavascriptCryptoKeyMaterial::Private(key)) => {
14319 let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
14320 if operation == "privateEncrypt" {
14321 rsa.private_encrypt(&data, &mut output, padding)
14322 .map_err(javascript_crypto_openssl_error)?
14323 } else {
14324 rsa.private_decrypt(&data, &mut output, padding)
14325 .map_err(javascript_crypto_openssl_error)?
14326 }
14327 }
14328 _ => {
14329 return Err(SidecarError::InvalidState(format!(
14330 "{operation} requires an RSA {} key",
14331 expect_kind.unwrap_or("asymmetric")
14332 )));
14333 }
14334 };
14335 output.truncate(written);
14336 Ok(Value::String(
14337 base64::engine::general_purpose::STANDARD.encode(output),
14338 ))
14339}
14340
14341fn service_javascript_crypto_create_key_object_sync_rpc(
14342 request: &JavascriptSyncRpcRequest,
14343) -> Result<Value, SidecarError> {
14344 let operation =
14345 javascript_sync_rpc_arg_str(&request.args, 0, "crypto.createKeyObject operation")?;
14346 let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.createKeyObject key")?;
14347 let expected = match operation {
14348 "createPrivateKey" => Some("private"),
14349 "createPublicKey" => Some("public"),
14350 other => {
14351 return Err(SidecarError::InvalidState(format!(
14352 "Unsupported key creation operation: {other}"
14353 )));
14354 }
14355 };
14356 let key_input =
14357 javascript_crypto_parse_direct_key_input(key_json, expected, "crypto.createKeyObject key")?;
14358 Ok(Value::String(
14359 serde_json::to_string(&javascript_crypto_serialize_sandbox_key_object(
14360 &key_input.key,
14361 )?)
14362 .map_err(|error| {
14363 SidecarError::InvalidState(format!("serialize crypto key object: {error}"))
14364 })?,
14365 ))
14366}
14367
14368fn service_javascript_crypto_generate_key_pair_sync_rpc(
14369 request: &JavascriptSyncRpcRequest,
14370) -> Result<Value, SidecarError> {
14371 let key_type =
14372 javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeyPairSync type")?;
14373 let options = javascript_crypto_parse_serialized_options_arg(
14374 &request.args,
14375 1,
14376 "crypto.generateKeyPairSync options",
14377 )?
14378 .unwrap_or(Value::Object(Map::new()));
14379 let public_encoding = options.get("publicKeyEncoding").cloned();
14380 let private_encoding = options.get("privateKeyEncoding").cloned();
14381
14382 let private_key = match key_type {
14383 "rsa" => {
14384 let bits = options
14385 .get("modulusLength")
14386 .and_then(Value::as_u64)
14387 .unwrap_or(2048) as u32;
14388 let exponent = options
14389 .get("publicExponent")
14390 .map(|value| javascript_crypto_u32_from_bridge_value(value, "rsa publicExponent"))
14391 .transpose()?
14392 .unwrap_or(65_537);
14393 let exponent = BigNum::from_u32(exponent).map_err(javascript_crypto_openssl_error)?;
14394 let rsa =
14395 Rsa::generate_with_e(bits, &exponent).map_err(javascript_crypto_openssl_error)?;
14396 PKey::from_rsa(rsa).map_err(javascript_crypto_openssl_error)?
14397 }
14398 "ec" => {
14399 let named_curve = options
14400 .get("namedCurve")
14401 .and_then(Value::as_str)
14402 .ok_or_else(|| {
14403 SidecarError::InvalidState(String::from(
14404 "crypto.generateKeyPairSync ec requires namedCurve",
14405 ))
14406 })?;
14407 let group = EcGroup::from_curve_name(javascript_crypto_curve_nid(named_curve)?)
14408 .map_err(javascript_crypto_openssl_error)?;
14409 let key = EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?;
14410 PKey::from_ec_key(key).map_err(javascript_crypto_openssl_error)?
14411 }
14412 "ed25519" => PKey::generate_ed25519().map_err(javascript_crypto_openssl_error)?,
14413 "x25519" => PKey::generate_x25519().map_err(javascript_crypto_openssl_error)?,
14414 other => {
14415 return Err(SidecarError::InvalidState(format!(
14416 "unsupported crypto key pair type {other}"
14417 )));
14418 }
14419 };
14420 let public_key = PKey::public_key_from_pem(
14421 &private_key
14422 .public_key_to_pem()
14423 .map_err(javascript_crypto_openssl_error)?,
14424 )
14425 .map_err(javascript_crypto_openssl_error)?;
14426 let response = if public_encoding.is_some() || private_encoding.is_some() {
14427 json!({
14428 "publicKey": javascript_crypto_serialize_encoded_key_value_public(&public_key, public_encoding.as_ref())?,
14429 "privateKey": javascript_crypto_serialize_encoded_key_value_private(&private_key, private_encoding.as_ref())?,
14430 })
14431 } else {
14432 json!({
14433 "publicKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(public_key))?,
14434 "privateKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(private_key))?,
14435 })
14436 };
14437 Ok(Value::String(serde_json::to_string(&response).map_err(
14438 |error| SidecarError::InvalidState(format!("serialize generated key pair: {error}")),
14439 )?))
14440}
14441
14442fn service_javascript_crypto_generate_key_sync_rpc(
14443 request: &JavascriptSyncRpcRequest,
14444) -> Result<Value, SidecarError> {
14445 let key_type = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeySync type")?;
14446 let options = javascript_crypto_parse_serialized_options_arg(
14447 &request.args,
14448 1,
14449 "crypto.generateKeySync options",
14450 )?
14451 .unwrap_or(Value::Object(Map::new()));
14452 let bit_length = options
14453 .get("length")
14454 .and_then(Value::as_u64)
14455 .ok_or_else(|| {
14456 SidecarError::InvalidState(String::from(
14457 "crypto.generateKeySync options.length is required",
14458 ))
14459 })? as usize;
14460 let mut raw = vec![0_u8; bit_length.div_ceil(8)];
14461 rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
14462 let serialized = match key_type {
14463 "hmac" => javascript_crypto_serialize_sandbox_key_object(
14464 &JavascriptCryptoKeyMaterial::Secret(raw),
14465 )?,
14466 "aes" => javascript_crypto_serialize_sandbox_key_object(
14467 &JavascriptCryptoKeyMaterial::Secret(raw),
14468 )?,
14469 other => {
14470 return Err(SidecarError::InvalidState(format!(
14471 "unsupported crypto.generateKeySync type {other}"
14472 )));
14473 }
14474 };
14475 Ok(Value::String(serde_json::to_string(&serialized).map_err(
14476 |error| SidecarError::InvalidState(format!("serialize generated key: {error}")),
14477 )?))
14478}
14479
14480fn service_javascript_crypto_generate_prime_sync_rpc(
14481 request: &JavascriptSyncRpcRequest,
14482) -> Result<Value, SidecarError> {
14483 let bits =
14484 javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.generatePrimeSync size")? as i32;
14485 let options = javascript_crypto_parse_serialized_options_arg(
14486 &request.args,
14487 1,
14488 "crypto.generatePrimeSync options",
14489 )?
14490 .unwrap_or(Value::Object(Map::new()));
14491 let safe = options
14492 .get("safe")
14493 .and_then(Value::as_bool)
14494 .unwrap_or(false);
14495 let add = options
14496 .get("add")
14497 .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime add"))
14498 .transpose()?;
14499 let rem = options
14500 .get("rem")
14501 .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime rem"))
14502 .transpose()?;
14503 let mut prime = BigNum::new().map_err(javascript_crypto_openssl_error)?;
14504 prime
14505 .generate_prime(bits, safe, add.as_deref(), rem.as_deref())
14506 .map_err(javascript_crypto_openssl_error)?;
14507 let payload = if options
14508 .get("bigint")
14509 .and_then(Value::as_bool)
14510 .unwrap_or(false)
14511 {
14512 json!({
14513 "__type": "bigint",
14514 "value": prime.to_dec_str().map_err(javascript_crypto_openssl_error)?.to_string(),
14515 })
14516 } else {
14517 json!({
14518 "__type": "buffer",
14519 "value": base64::engine::general_purpose::STANDARD.encode(prime.to_vec()),
14520 })
14521 };
14522 Ok(Value::String(serde_json::to_string(&payload).map_err(
14523 |error| SidecarError::InvalidState(format!("serialize generated prime: {error}")),
14524 )?))
14525}
14526
14527fn service_javascript_crypto_diffie_hellman_sync_rpc(
14528 request: &JavascriptSyncRpcRequest,
14529) -> Result<Value, SidecarError> {
14530 let options = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellman options")?;
14531 let parsed: Value = serde_json::from_str(options).map_err(|error| {
14532 SidecarError::InvalidState(format!(
14533 "crypto.diffieHellman options must be valid JSON: {error}"
14534 ))
14535 })?;
14536 let private_key = javascript_crypto_parse_key_material_value(
14537 parsed.get("privateKey").ok_or_else(|| {
14538 SidecarError::InvalidState(String::from("crypto.diffieHellman missing privateKey"))
14539 })?,
14540 Some("private"),
14541 "crypto.diffieHellman privateKey",
14542 )?;
14543 let public_key = javascript_crypto_parse_key_material_value(
14544 parsed.get("publicKey").ok_or_else(|| {
14545 SidecarError::InvalidState(String::from("crypto.diffieHellman missing publicKey"))
14546 })?,
14547 Some("public"),
14548 "crypto.diffieHellman publicKey",
14549 )?;
14550 let private_key =
14551 javascript_crypto_expect_private_key(private_key, "crypto.diffieHellman privateKey")?;
14552 let public_key =
14553 javascript_crypto_expect_public_key(public_key, "crypto.diffieHellman publicKey")?;
14554 let mut deriver = Deriver::new(&private_key).map_err(javascript_crypto_openssl_error)?;
14555 deriver
14556 .set_peer(&public_key)
14557 .map_err(javascript_crypto_openssl_error)?;
14558 let secret = deriver
14559 .derive_to_vec()
14560 .map_err(javascript_crypto_openssl_error)?;
14561 Ok(Value::String(
14562 serde_json::to_string(&json!({
14563 "__type": "buffer",
14564 "value": base64::engine::general_purpose::STANDARD.encode(secret),
14565 }))
14566 .map_err(|error| {
14567 SidecarError::InvalidState(format!("serialize derived secret: {error}"))
14568 })?,
14569 ))
14570}
14571
14572fn service_javascript_crypto_diffie_hellman_group_sync_rpc(
14573 request: &JavascriptSyncRpcRequest,
14574) -> Result<Value, SidecarError> {
14575 let name = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellmanGroup name")?;
14576 let params = javascript_crypto_named_dh_group(name)?;
14577 let response = json!({
14578 "prime": {
14579 "__type": "buffer",
14580 "value": base64::engine::general_purpose::STANDARD.encode(params.prime_p().to_vec()),
14581 },
14582 "generator": {
14583 "__type": "buffer",
14584 "value": base64::engine::general_purpose::STANDARD.encode(params.generator().to_vec()),
14585 },
14586 });
14587 Ok(Value::String(serde_json::to_string(&response).map_err(
14588 |error| {
14589 SidecarError::InvalidState(format!("serialize diffieHellmanGroup response: {error}"))
14590 },
14591 )?))
14592}
14593
14594fn service_javascript_crypto_diffie_hellman_session_create_sync_rpc(
14595 process: &mut ActiveProcess,
14596 request: &JavascriptSyncRpcRequest,
14597) -> Result<Value, SidecarError> {
14598 ensure_per_process_state_handle_capacity(
14599 process.diffie_hellman_sessions.len(),
14600 "diffie-hellman session",
14601 )?;
14602 let raw = javascript_sync_rpc_arg_str(
14603 &request.args,
14604 0,
14605 "crypto.diffieHellmanSessionCreate request",
14606 )?;
14607 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14608 SidecarError::InvalidState(format!(
14609 "crypto.diffieHellmanSessionCreate request must be valid JSON: {error}"
14610 ))
14611 })?;
14612 let session = match parsed.get("type").and_then(Value::as_str) {
14613 Some("group") => {
14614 let name = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
14615 SidecarError::InvalidState(String::from(
14616 "crypto.diffieHellmanSessionCreate group requires name",
14617 ))
14618 })?;
14619 ActiveDiffieHellmanSession::Dh(ActiveDhSession {
14620 params: javascript_crypto_named_dh_group(name)?,
14621 key_pair: None,
14622 })
14623 }
14624 Some("dh") => {
14625 let args = parsed
14626 .get("args")
14627 .and_then(Value::as_array)
14628 .ok_or_else(|| {
14629 SidecarError::InvalidState(String::from(
14630 "crypto.diffieHellmanSessionCreate dh requires args",
14631 ))
14632 })?;
14633 let params = javascript_crypto_build_dh_params(args)?;
14634 ActiveDiffieHellmanSession::Dh(ActiveDhSession {
14635 params,
14636 key_pair: None,
14637 })
14638 }
14639 Some("ecdh") => {
14640 let curve = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
14641 SidecarError::InvalidState(String::from(
14642 "crypto.diffieHellmanSessionCreate ecdh requires name",
14643 ))
14644 })?;
14645 ActiveDiffieHellmanSession::Ecdh(ActiveEcdhSession {
14646 curve: curve.to_string(),
14647 key_pair: None,
14648 })
14649 }
14650 other => {
14651 return Err(SidecarError::InvalidState(format!(
14652 "Unsupported Diffie-Hellman session type: {}",
14653 other.unwrap_or("<missing>")
14654 )));
14655 }
14656 };
14657 process.next_diffie_hellman_session_id += 1;
14658 let session_id = process.next_diffie_hellman_session_id;
14659 process.diffie_hellman_sessions.insert(session_id, session);
14660 Ok(json!(session_id))
14661}
14662
14663fn service_javascript_crypto_diffie_hellman_session_call_sync_rpc(
14664 process: &mut ActiveProcess,
14665 request: &JavascriptSyncRpcRequest,
14666) -> Result<Value, SidecarError> {
14667 let session_id = javascript_sync_rpc_arg_u64(
14668 &request.args,
14669 0,
14670 "crypto.diffieHellmanSessionCall session id",
14671 )?;
14672 let raw =
14673 javascript_sync_rpc_arg_str(&request.args, 1, "crypto.diffieHellmanSessionCall request")?;
14674 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14675 SidecarError::InvalidState(format!(
14676 "crypto.diffieHellmanSessionCall request must be valid JSON: {error}"
14677 ))
14678 })?;
14679 let method = parsed
14680 .get("method")
14681 .and_then(Value::as_str)
14682 .ok_or_else(|| {
14683 SidecarError::InvalidState(String::from(
14684 "crypto.diffieHellmanSessionCall request missing method",
14685 ))
14686 })?;
14687 let args = parsed
14688 .get("args")
14689 .and_then(Value::as_array)
14690 .cloned()
14691 .unwrap_or_default();
14692 let session = process
14693 .diffie_hellman_sessions
14694 .get_mut(&session_id)
14695 .ok_or_else(|| {
14696 SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
14697 })?;
14698 let (result, has_result) = match session {
14699 ActiveDiffieHellmanSession::Dh(session) => {
14700 javascript_crypto_call_dh_session(session, method, &args)?
14701 }
14702 ActiveDiffieHellmanSession::Ecdh(session) => {
14703 javascript_crypto_call_ecdh_session(session, method, &args)?
14704 }
14705 };
14706 Ok(Value::String(
14707 serde_json::to_string(&json!({
14708 "result": result,
14709 "hasResult": has_result,
14710 }))
14711 .map_err(|error| {
14712 SidecarError::InvalidState(format!("serialize diffie session result: {error}"))
14713 })?,
14714 ))
14715}
14716
14717fn service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(
14718 process: &mut ActiveProcess,
14719 request: &JavascriptSyncRpcRequest,
14720) -> Result<Value, SidecarError> {
14721 let session_id = javascript_sync_rpc_arg_u64(
14722 &request.args,
14723 0,
14724 "crypto.diffieHellmanSessionDestroy session id",
14725 )?;
14726 process
14727 .diffie_hellman_sessions
14728 .remove(&session_id)
14729 .ok_or_else(|| {
14730 SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
14731 })?;
14732 Ok(Value::Null)
14733}
14734
14735fn service_javascript_crypto_subtle_sync_rpc(
14736 request: &JavascriptSyncRpcRequest,
14737) -> Result<Value, SidecarError> {
14738 let raw = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.subtle request")?;
14739 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14740 SidecarError::InvalidState(format!("crypto.subtle request must be valid JSON: {error}"))
14741 })?;
14742 let op = parsed.get("op").and_then(Value::as_str).ok_or_else(|| {
14743 SidecarError::InvalidState(String::from("crypto.subtle request missing op"))
14744 })?;
14745 match op {
14746 "digest" => {
14747 let algorithm = parsed
14748 .get("algorithm")
14749 .and_then(Value::as_str)
14750 .ok_or_else(|| {
14751 SidecarError::InvalidState(String::from(
14752 "crypto.subtle.digest missing algorithm",
14753 ))
14754 })?;
14755 let data = parsed.get("data").and_then(Value::as_str).ok_or_else(|| {
14756 SidecarError::InvalidState(String::from("crypto.subtle.digest missing data"))
14757 })?;
14758 let bytes = base64::engine::general_purpose::STANDARD
14759 .decode(data)
14760 .map_err(|error| {
14761 SidecarError::InvalidState(format!("crypto.subtle.digest data base64: {error}"))
14762 })?;
14763 let digest = JavascriptCryptoDigestAlgorithm::parse(algorithm)?.digest(&bytes);
14764 Ok(Value::String(
14765 serde_json::to_string(&json!({
14766 "data": base64::engine::general_purpose::STANDARD.encode(digest),
14767 }))
14768 .map_err(|error| {
14769 SidecarError::InvalidState(format!("serialize crypto.subtle digest: {error}"))
14770 })?,
14771 ))
14772 }
14773 "generateKey" => {
14774 let algorithm = parsed.get("algorithm").ok_or_else(|| {
14775 SidecarError::InvalidState(String::from(
14776 "crypto.subtle.generateKey missing algorithm",
14777 ))
14778 })?;
14779 let name =
14780 javascript_crypto_subtle_algorithm_name(algorithm, "crypto.subtle.generateKey")?;
14781 if !matches!(name, "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW") {
14782 return Err(SidecarError::InvalidState(format!(
14783 "Unsupported key algorithm: {name}"
14784 )));
14785 }
14786 let length_bits = algorithm
14787 .get("length")
14788 .and_then(Value::as_u64)
14789 .ok_or_else(|| {
14790 SidecarError::InvalidState(String::from(
14791 "crypto.subtle.generateKey AES algorithm requires length",
14792 ))
14793 })?;
14794 if length_bits % 8 != 0 {
14795 return Err(SidecarError::InvalidState(String::from(
14796 "crypto.subtle.generateKey length must be byte-aligned",
14797 )));
14798 }
14799 let length_bytes = usize::try_from(length_bits / 8).map_err(|_| {
14800 SidecarError::InvalidState(String::from(
14801 "crypto.subtle.generateKey length is too large",
14802 ))
14803 })?;
14804 let mut raw = vec![0_u8; length_bytes];
14805 rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
14806 let key = javascript_crypto_serialize_subtle_secret_key(
14807 &raw,
14808 javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
14809 parsed
14810 .get("extractable")
14811 .and_then(Value::as_bool)
14812 .unwrap_or(false),
14813 parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
14814 )?;
14815 Ok(Value::String(
14816 serde_json::to_string(&json!({ "key": key })).map_err(|error| {
14817 SidecarError::InvalidState(format!(
14818 "serialize crypto.subtle generated key: {error}"
14819 ))
14820 })?,
14821 ))
14822 }
14823 "importKey" => {
14824 let format = parsed
14825 .get("format")
14826 .and_then(Value::as_str)
14827 .ok_or_else(|| {
14828 SidecarError::InvalidState(String::from(
14829 "crypto.subtle.importKey missing format",
14830 ))
14831 })?;
14832 if format != "raw" {
14833 return Err(SidecarError::InvalidState(format!(
14834 "Unsupported import format: {format}"
14835 )));
14836 }
14837 let key_data = parsed
14838 .get("keyData")
14839 .and_then(Value::as_str)
14840 .ok_or_else(|| {
14841 SidecarError::InvalidState(String::from(
14842 "crypto.subtle.importKey missing keyData",
14843 ))
14844 })?;
14845 let raw = base64::engine::general_purpose::STANDARD
14846 .decode(key_data)
14847 .map_err(|error| {
14848 SidecarError::InvalidState(format!(
14849 "crypto.subtle.importKey keyData base64: {error}"
14850 ))
14851 })?;
14852 let algorithm = parsed.get("algorithm").ok_or_else(|| {
14853 SidecarError::InvalidState(String::from(
14854 "crypto.subtle.importKey missing algorithm",
14855 ))
14856 })?;
14857 let key = javascript_crypto_serialize_subtle_secret_key(
14858 &raw,
14859 javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
14860 parsed
14861 .get("extractable")
14862 .and_then(Value::as_bool)
14863 .unwrap_or(false),
14864 parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
14865 )?;
14866 Ok(Value::String(
14867 serde_json::to_string(&json!({ "key": key })).map_err(|error| {
14868 SidecarError::InvalidState(format!(
14869 "serialize crypto.subtle imported key: {error}"
14870 ))
14871 })?,
14872 ))
14873 }
14874 "exportKey" => {
14875 let format = parsed
14876 .get("format")
14877 .and_then(Value::as_str)
14878 .ok_or_else(|| {
14879 SidecarError::InvalidState(String::from(
14880 "crypto.subtle.exportKey missing format",
14881 ))
14882 })?;
14883 if format != "raw" {
14884 return Err(SidecarError::InvalidState(format!(
14885 "Unsupported export format: {format}"
14886 )));
14887 }
14888 let raw = javascript_crypto_subtle_key_raw(
14889 parsed.get("key").ok_or_else(|| {
14890 SidecarError::InvalidState(String::from("crypto.subtle.exportKey missing key"))
14891 })?,
14892 "crypto.subtle.exportKey key",
14893 )?;
14894 Ok(Value::String(
14895 serde_json::to_string(&json!({
14896 "data": base64::engine::general_purpose::STANDARD.encode(raw),
14897 }))
14898 .map_err(|error| {
14899 SidecarError::InvalidState(format!("serialize crypto.subtle export: {error}"))
14900 })?,
14901 ))
14902 }
14903 "encrypt" | "decrypt" => service_javascript_crypto_subtle_aes_crypt_sync_rpc(op, &parsed),
14904 _ => Err(SidecarError::InvalidState(format!(
14905 "Unsupported subtle operation: {op}"
14906 ))),
14907 }
14908}
14909
14910fn javascript_crypto_subtle_algorithm_name<'a>(
14911 algorithm: &'a Value,
14912 label: &str,
14913) -> Result<&'a str, SidecarError> {
14914 if let Some(name) = algorithm.as_str() {
14915 return Ok(name);
14916 }
14917 algorithm
14918 .get("name")
14919 .and_then(Value::as_str)
14920 .ok_or_else(|| SidecarError::InvalidState(format!("{label} algorithm missing name")))
14921}
14922
14923fn javascript_crypto_normalize_subtle_secret_algorithm(
14924 algorithm: Value,
14925 raw: &[u8],
14926) -> Result<Value, SidecarError> {
14927 let mut object = match algorithm {
14928 Value::String(name) => {
14929 let mut object = Map::new();
14930 object.insert(String::from("name"), Value::String(name));
14931 object
14932 }
14933 Value::Object(object) => object,
14934 _ => {
14935 return Err(SidecarError::InvalidState(String::from(
14936 "crypto.subtle secret algorithm must be a string or object",
14937 )));
14938 }
14939 };
14940 let name = object
14941 .get("name")
14942 .and_then(Value::as_str)
14943 .ok_or_else(|| {
14944 SidecarError::InvalidState(String::from("crypto.subtle secret algorithm missing name"))
14945 })?
14946 .to_string();
14947 if matches!(name.as_str(), "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW")
14948 && !object.contains_key("length")
14949 {
14950 object.insert(String::from("length"), json!(raw.len() * 8));
14951 }
14952 Ok(Value::Object(object))
14953}
14954
14955fn javascript_crypto_serialize_subtle_secret_key(
14956 raw: &[u8],
14957 algorithm: Value,
14958 extractable: bool,
14959 usages: Value,
14960) -> Result<Value, SidecarError> {
14961 let raw_base64 = base64::engine::general_purpose::STANDARD.encode(raw);
14962 let source_key_object_data = javascript_crypto_serialize_sandbox_key_object(
14963 &JavascriptCryptoKeyMaterial::Secret(raw.to_vec()),
14964 )?;
14965 Ok(json!({
14966 "type": "secret",
14967 "algorithm": algorithm,
14968 "extractable": extractable,
14969 "usages": usages,
14970 "_raw": raw_base64,
14971 "_sourceKeyObjectData": source_key_object_data,
14972 }))
14973}
14974
14975fn javascript_crypto_subtle_key_raw(key: &Value, label: &str) -> Result<Vec<u8>, SidecarError> {
14976 let raw = key.get("_raw").and_then(Value::as_str).ok_or_else(|| {
14977 SidecarError::InvalidState(format!("{label} must be a raw secret CryptoKey"))
14978 })?;
14979 base64::engine::general_purpose::STANDARD
14980 .decode(raw)
14981 .map_err(|error| SidecarError::InvalidState(format!("{label} raw base64: {error}")))
14982}
14983
14984fn service_javascript_crypto_subtle_aes_crypt_sync_rpc(
14985 op: &str,
14986 parsed: &Value,
14987) -> Result<Value, SidecarError> {
14988 let algorithm = parsed.get("algorithm").ok_or_else(|| {
14989 SidecarError::InvalidState(format!("crypto.subtle.{op} missing algorithm"))
14990 })?;
14991 let name = javascript_crypto_subtle_algorithm_name(algorithm, &format!("crypto.subtle.{op}"))?;
14992 if name != "AES-GCM" {
14993 return Err(SidecarError::InvalidState(format!(
14994 "Unsupported subtle AES operation algorithm: {name}"
14995 )));
14996 }
14997 let key = javascript_crypto_subtle_key_raw(
14998 parsed
14999 .get("key")
15000 .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing key")))?,
15001 &format!("crypto.subtle.{op} key"),
15002 )?;
15003 let iv = algorithm.get("iv").and_then(Value::as_str).ok_or_else(|| {
15004 SidecarError::InvalidState(format!("crypto.subtle.{op} AES-GCM missing iv"))
15005 })?;
15006 let iv = base64::engine::general_purpose::STANDARD
15007 .decode(iv)
15008 .map_err(|error| {
15009 SidecarError::InvalidState(format!("crypto.subtle.{op} iv base64: {error}"))
15010 })?;
15011 let data = parsed
15012 .get("data")
15013 .and_then(Value::as_str)
15014 .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing data")))?;
15015 let mut data = base64::engine::general_purpose::STANDARD
15016 .decode(data)
15017 .map_err(|error| {
15018 SidecarError::InvalidState(format!("crypto.subtle.{op} data base64: {error}"))
15019 })?;
15020 let tag_len = javascript_crypto_subtle_aes_gcm_tag_len(algorithm)?;
15021 let mut options = Map::new();
15022 options.insert(String::from("authTagLength"), json!(tag_len));
15023 if let Some(additional_data) = algorithm.get("additionalData").and_then(Value::as_str) {
15024 options.insert(
15025 String::from("aad"),
15026 Value::String(additional_data.to_string()),
15027 );
15028 }
15029 let decrypt = op == "decrypt";
15030 if decrypt {
15031 if data.len() < tag_len {
15032 return Err(SidecarError::InvalidState(String::from(
15033 "crypto.subtle.decrypt AES-GCM data shorter than auth tag",
15034 )));
15035 }
15036 let auth_tag = data.split_off(data.len() - tag_len);
15037 options.insert(
15038 String::from("authTag"),
15039 Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
15040 );
15041 }
15042 let cipher_name = format!("aes-{}-gcm", key.len() * 8);
15043 let mut context = javascript_crypto_build_cipher_context(
15044 &cipher_name,
15045 &key,
15046 Some(&iv),
15047 decrypt,
15048 Some(&Value::Object(options)),
15049 )?;
15050 let mut output = javascript_crypto_cipher_update(&mut context, &data)?;
15051 output.extend(javascript_crypto_cipher_finalize(&mut context)?);
15052 if !decrypt {
15053 let mut auth_tag = vec![0_u8; tag_len];
15054 context
15055 .get_tag(&mut auth_tag)
15056 .map_err(javascript_crypto_openssl_error)?;
15057 output.extend(auth_tag);
15058 }
15059 Ok(Value::String(
15060 serde_json::to_string(&json!({
15061 "data": base64::engine::general_purpose::STANDARD.encode(output),
15062 }))
15063 .map_err(|error| {
15064 SidecarError::InvalidState(format!("serialize crypto.subtle {op}: {error}"))
15065 })?,
15066 ))
15067}
15068
15069fn javascript_crypto_subtle_aes_gcm_tag_len(algorithm: &Value) -> Result<usize, SidecarError> {
15070 let tag_bits = algorithm
15071 .get("tagLength")
15072 .and_then(Value::as_u64)
15073 .unwrap_or(128);
15074 if !tag_bits.is_multiple_of(8) {
15075 return Err(SidecarError::InvalidState(String::from(
15076 "crypto.subtle AES-GCM tagLength must be byte-aligned",
15077 )));
15078 }
15079 usize::try_from(tag_bits / 8).map_err(|_| {
15080 SidecarError::InvalidState(String::from("crypto.subtle AES-GCM tagLength too large"))
15081 })
15082}
15083
15084fn service_javascript_crypto_cipheriv_inner(
15085 request: &JavascriptSyncRpcRequest,
15086 decrypt: bool,
15087) -> Result<Value, SidecarError> {
15088 let label = if decrypt {
15089 "crypto.decipheriv"
15090 } else {
15091 "crypto.cipheriv"
15092 };
15093 let algorithm = javascript_sync_rpc_arg_str(&request.args, 0, &format!("{label} algorithm"))?;
15094 let key = javascript_sync_rpc_base64_arg(&request.args, 1, &format!("{label} key"))?;
15095 let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 2, &format!("{label} iv"))?;
15096 let data = javascript_sync_rpc_base64_arg(&request.args, 3, &format!("{label} data"))?;
15097 let options =
15098 javascript_sync_rpc_json_arg_optional(&request.args, 4, &format!("{label} options"))?;
15099 let auth_tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options.as_ref())?;
15100 let mut context = javascript_crypto_build_cipher_context(
15101 algorithm,
15102 &key,
15103 iv.as_deref(),
15104 decrypt,
15105 options.as_ref(),
15106 )?;
15107 let payload = javascript_crypto_cipher_update(&mut context, &data)?;
15108 let final_bytes = javascript_crypto_cipher_finalize(&mut context)?;
15109 if decrypt {
15110 let mut output = payload;
15111 output.extend(final_bytes);
15112 return Ok(Value::String(
15113 base64::engine::general_purpose::STANDARD.encode(output),
15114 ));
15115 }
15116
15117 let mut response = Map::new();
15118 let mut encrypted = payload;
15119 encrypted.extend(final_bytes);
15120 response.insert(
15121 String::from("data"),
15122 Value::String(base64::engine::general_purpose::STANDARD.encode(encrypted)),
15123 );
15124 if javascript_crypto_is_aead(algorithm) {
15125 let mut auth_tag = vec![0_u8; auth_tag_len];
15126 context
15127 .get_tag(&mut auth_tag)
15128 .map_err(javascript_crypto_openssl_error)?;
15129 response.insert(
15130 String::from("authTag"),
15131 Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
15132 );
15133 }
15134 Ok(Value::String(serde_json::to_string(&response).map_err(
15135 |error| SidecarError::InvalidState(format!("serialize {label} response: {error}")),
15136 )?))
15137}
15138
15139fn javascript_sync_rpc_base64_arg_optional(
15140 args: &[Value],
15141 index: usize,
15142 label: &str,
15143) -> Result<Option<Vec<u8>>, SidecarError> {
15144 if args.get(index).is_none() || args[index].is_null() {
15145 return Ok(None);
15146 }
15147 javascript_sync_rpc_base64_arg(args, index, label).map(Some)
15148}
15149
15150fn javascript_sync_rpc_json_arg_optional(
15151 args: &[Value],
15152 index: usize,
15153 label: &str,
15154) -> Result<Option<Value>, SidecarError> {
15155 if args.get(index).is_none() || args[index].is_null() {
15156 return Ok(None);
15157 }
15158 let raw = javascript_sync_rpc_arg_str(args, index, label)?;
15159 serde_json::from_str(raw)
15160 .map(Some)
15161 .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
15162}
15163
15164fn javascript_crypto_parse_direct_key_input(
15165 raw: &str,
15166 expected: Option<&str>,
15167 label: &str,
15168) -> Result<JavascriptDirectKeyInput, SidecarError> {
15169 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
15170 SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
15171 })?;
15172 let padding = match parsed.as_object().and_then(|value| value.get("padding")) {
15173 Some(value) => javascript_crypto_padding_from_value(value)?,
15174 None => None,
15175 };
15176 Ok(JavascriptDirectKeyInput {
15177 key: javascript_crypto_parse_key_material_value(&parsed, expected, label)?,
15178 padding,
15179 })
15180}
15181
15182fn javascript_crypto_parse_key_material_value(
15183 value: &Value,
15184 expected: Option<&str>,
15185 label: &str,
15186) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15187 if let Some(object) = value.as_object() {
15188 if object.get("__type").and_then(Value::as_str) == Some("keyObject") {
15189 let serialized = object.get("value").ok_or_else(|| {
15190 SidecarError::InvalidState(format!("{label} keyObject is missing a value"))
15191 })?;
15192 return javascript_crypto_parse_serialized_key_object(serialized, expected, label);
15193 }
15194 if object.contains_key("type") && (object.contains_key("pem") || object.contains_key("raw"))
15195 {
15196 return javascript_crypto_parse_serialized_key_object(value, expected, label);
15197 }
15198 if let Some(source) = object.get("key") {
15199 return javascript_crypto_parse_key_source(
15200 source,
15201 object.get("format").and_then(Value::as_str),
15202 object.get("type").and_then(Value::as_str),
15203 expected,
15204 label,
15205 );
15206 }
15207 }
15208 javascript_crypto_parse_key_source(value, None, None, expected, label)
15209}
15210
15211fn javascript_crypto_parse_key_source(
15212 source: &Value,
15213 format: Option<&str>,
15214 kind: Option<&str>,
15215 expected: Option<&str>,
15216 label: &str,
15217) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15218 match source {
15219 Value::String(pem) => javascript_crypto_parse_key_from_pem(pem.as_bytes(), expected, label),
15220 Value::Object(object) if object.get("__type").and_then(Value::as_str) == Some("buffer") => {
15221 let data = javascript_crypto_decode_bridge_buffer(source, label)?;
15222 javascript_crypto_parse_key_from_bytes(&data, format, kind, expected, label)
15223 }
15224 Value::Object(_) => {
15225 if format == Some("jwk") {
15226 return Err(SidecarError::InvalidState(format!(
15227 "{label} jwk inputs are not supported yet"
15228 )));
15229 }
15230 Err(SidecarError::InvalidState(format!(
15231 "{label} has an unsupported key shape"
15232 )))
15233 }
15234 _ => Err(SidecarError::InvalidState(format!(
15235 "{label} has an unsupported key value"
15236 ))),
15237 }
15238}
15239
15240fn javascript_crypto_parse_key_from_pem(
15241 pem: &[u8],
15242 expected: Option<&str>,
15243 label: &str,
15244) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15245 match expected {
15246 Some("private") => PKey::private_key_from_pem(pem)
15247 .map(JavascriptCryptoKeyMaterial::Private)
15248 .map_err(|error| {
15249 SidecarError::InvalidState(format!("{label} private key is invalid: {error}"))
15250 }),
15251 Some("public") => PKey::public_key_from_pem(pem)
15252 .map(JavascriptCryptoKeyMaterial::Public)
15253 .map_err(|error| {
15254 SidecarError::InvalidState(format!("{label} public key is invalid: {error}"))
15255 }),
15256 _ => PKey::private_key_from_pem(pem)
15257 .map(JavascriptCryptoKeyMaterial::Private)
15258 .or_else(|_| PKey::public_key_from_pem(pem).map(JavascriptCryptoKeyMaterial::Public))
15259 .map_err(|error| {
15260 SidecarError::InvalidState(format!("{label} PEM key is invalid: {error}"))
15261 }),
15262 }
15263}
15264
15265fn javascript_crypto_parse_key_from_bytes(
15266 der: &[u8],
15267 format: Option<&str>,
15268 kind: Option<&str>,
15269 expected: Option<&str>,
15270 label: &str,
15271) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15272 match (format.unwrap_or("der"), kind.or(expected)) {
15273 ("der", Some("pkcs8")) | ("der", Some("private")) => PKey::private_key_from_der(der)
15274 .map(JavascriptCryptoKeyMaterial::Private)
15275 .map_err(|error| {
15276 SidecarError::InvalidState(format!("{label} private key DER is invalid: {error}"))
15277 }),
15278 ("der", Some("spki")) | ("der", Some("public")) => PKey::public_key_from_der(der)
15279 .map(JavascriptCryptoKeyMaterial::Public)
15280 .map_err(|error| {
15281 SidecarError::InvalidState(format!("{label} public key DER is invalid: {error}"))
15282 }),
15283 _ => Err(SidecarError::InvalidState(format!(
15284 "{label} unsupported key bytes format"
15285 ))),
15286 }
15287}
15288
15289fn javascript_crypto_parse_serialized_key_object(
15290 value: &Value,
15291 expected: Option<&str>,
15292 label: &str,
15293) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15294 let serialized: JavascriptSerializedSandboxKeyObject = serde_json::from_value(value.clone())
15295 .map_err(|error| {
15296 SidecarError::InvalidState(format!("{label} keyObject is invalid: {error}"))
15297 })?;
15298 match serialized.kind.as_str() {
15299 "secret" => {
15300 if expected == Some("public") || expected == Some("private") {
15301 return Err(SidecarError::InvalidState(format!(
15302 "{label} expected an asymmetric key"
15303 )));
15304 }
15305 Ok(JavascriptCryptoKeyMaterial::Secret(
15306 base64::engine::general_purpose::STANDARD
15307 .decode(serialized.raw.unwrap_or_default())
15308 .map_err(|error| {
15309 SidecarError::InvalidState(format!(
15310 "{label} secret key contains invalid base64: {error}"
15311 ))
15312 })?,
15313 ))
15314 }
15315 "private" => {
15316 let pem = serialized.pem.ok_or_else(|| {
15317 SidecarError::InvalidState(format!("{label} private keyObject is missing pem"))
15318 })?;
15319 javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("private"), label)
15320 }
15321 "public" => {
15322 let pem = serialized.pem.ok_or_else(|| {
15323 SidecarError::InvalidState(format!("{label} public keyObject is missing pem"))
15324 })?;
15325 javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("public"), label)
15326 }
15327 other => Err(SidecarError::InvalidState(format!(
15328 "{label} has unsupported keyObject type {other}"
15329 ))),
15330 }
15331}
15332
15333fn javascript_crypto_expect_private_key(
15334 key: JavascriptCryptoKeyMaterial,
15335 label: &str,
15336) -> Result<PKey<Private>, SidecarError> {
15337 match key {
15338 JavascriptCryptoKeyMaterial::Private(key) => Ok(key),
15339 _ => Err(SidecarError::InvalidState(format!(
15340 "{label} requires a private key"
15341 ))),
15342 }
15343}
15344
15345fn javascript_crypto_expect_public_key(
15346 key: JavascriptCryptoKeyMaterial,
15347 label: &str,
15348) -> Result<PKey<Public>, SidecarError> {
15349 match key {
15350 JavascriptCryptoKeyMaterial::Public(key) => Ok(key),
15351 JavascriptCryptoKeyMaterial::Private(key) => {
15352 let pem = key
15353 .public_key_to_pem()
15354 .map_err(javascript_crypto_openssl_error)?;
15355 PKey::public_key_from_pem(&pem).map_err(javascript_crypto_openssl_error)
15356 }
15357 _ => Err(SidecarError::InvalidState(format!(
15358 "{label} requires a public key"
15359 ))),
15360 }
15361}
15362
15363fn javascript_crypto_new_signer<'a>(
15364 algorithm: Option<&'a str>,
15365 key: &'a PKey<Private>,
15366) -> Result<Signer<'a>, SidecarError> {
15367 if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
15368 return Signer::new_without_digest(key).map_err(javascript_crypto_openssl_error);
15369 }
15370 Signer::new(
15371 javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
15372 SidecarError::InvalidState(String::from("crypto.sign requires a digest algorithm"))
15373 })?)?,
15374 key,
15375 )
15376 .map_err(javascript_crypto_openssl_error)
15377}
15378
15379fn javascript_crypto_new_verifier<'a>(
15380 algorithm: Option<&'a str>,
15381 key: &'a PKey<Public>,
15382) -> Result<Verifier<'a>, SidecarError> {
15383 if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
15384 return Verifier::new_without_digest(key).map_err(javascript_crypto_openssl_error);
15385 }
15386 Verifier::new(
15387 javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
15388 SidecarError::InvalidState(String::from("crypto.verify requires a digest algorithm"))
15389 })?)?,
15390 key,
15391 )
15392 .map_err(javascript_crypto_openssl_error)
15393}
15394
15395fn javascript_crypto_message_digest_from_name(name: &str) -> Result<MessageDigest, SidecarError> {
15396 match name.trim().to_ascii_lowercase().replace('-', "").as_str() {
15397 "md5" => Ok(MessageDigest::md5()),
15398 "sha1" => Ok(MessageDigest::sha1()),
15399 "sha256" => Ok(MessageDigest::sha256()),
15400 "sha384" => Ok(MessageDigest::sha384()),
15401 "sha512" => Ok(MessageDigest::sha512()),
15402 other => Err(SidecarError::InvalidState(format!(
15403 "unsupported crypto digest algorithm {other}"
15404 ))),
15405 }
15406}
15407
15408fn javascript_crypto_padding_from_value(value: &Value) -> Result<Option<Padding>, SidecarError> {
15409 let Some(number) = value.as_i64() else {
15410 return Ok(None);
15411 };
15412 let padding = match number {
15413 1 => Padding::PKCS1,
15414 3 => Padding::NONE,
15415 4 => Padding::PKCS1_OAEP,
15416 6 => Padding::PKCS1_PSS,
15417 other => {
15418 return Err(SidecarError::InvalidState(format!(
15419 "unsupported RSA padding constant {other}"
15420 )));
15421 }
15422 };
15423 Ok(Some(padding))
15424}
15425
15426fn javascript_crypto_decode_bridge_buffer(
15427 value: &Value,
15428 label: &str,
15429) -> Result<Vec<u8>, SidecarError> {
15430 let base64_value = value
15431 .as_object()
15432 .filter(|object| object.get("__type").and_then(Value::as_str) == Some("buffer"))
15433 .and_then(|object| object.get("value"))
15434 .and_then(Value::as_str)
15435 .ok_or_else(|| {
15436 SidecarError::InvalidState(format!("{label} must be a serialized bridge buffer"))
15437 })?;
15438 base64::engine::general_purpose::STANDARD
15439 .decode(base64_value)
15440 .map_err(|error| {
15441 SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
15442 })
15443}
15444
15445fn javascript_crypto_serialize_sandbox_key_object(
15446 key: &JavascriptCryptoKeyMaterial,
15447) -> Result<Value, SidecarError> {
15448 let serialized = match key {
15449 JavascriptCryptoKeyMaterial::Private(key) => JavascriptSerializedSandboxKeyObject {
15450 kind: String::from("private"),
15451 pem: Some(
15452 String::from_utf8(
15453 key.private_key_to_pem_pkcs8()
15454 .map_err(javascript_crypto_openssl_error)?,
15455 )
15456 .map_err(|error| {
15457 SidecarError::InvalidState(format!("private key PEM is not utf8: {error}"))
15458 })?,
15459 ),
15460 raw: None,
15461 asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
15462 asymmetric_key_details: None,
15463 jwk: None,
15464 },
15465 JavascriptCryptoKeyMaterial::Public(key) => JavascriptSerializedSandboxKeyObject {
15466 kind: String::from("public"),
15467 pem: Some(
15468 String::from_utf8(
15469 key.public_key_to_pem()
15470 .map_err(javascript_crypto_openssl_error)?,
15471 )
15472 .map_err(|error| {
15473 SidecarError::InvalidState(format!("public key PEM is not utf8: {error}"))
15474 })?,
15475 ),
15476 raw: None,
15477 asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
15478 asymmetric_key_details: None,
15479 jwk: None,
15480 },
15481 JavascriptCryptoKeyMaterial::Secret(raw) => JavascriptSerializedSandboxKeyObject {
15482 kind: String::from("secret"),
15483 pem: None,
15484 raw: Some(base64::engine::general_purpose::STANDARD.encode(raw)),
15485 asymmetric_key_type: None,
15486 asymmetric_key_details: None,
15487 jwk: None,
15488 },
15489 };
15490 serde_json::to_value(serialized)
15491 .map_err(|error| SidecarError::InvalidState(format!("serialize key object: {error}")))
15492}
15493
15494fn javascript_crypto_pkey_type_name(id: PKeyId) -> Option<String> {
15495 match id {
15496 PKeyId::RSA => Some(String::from("rsa")),
15497 PKeyId::EC => Some(String::from("ec")),
15498 PKeyId::ED25519 => Some(String::from("ed25519")),
15499 PKeyId::ED448 => Some(String::from("ed448")),
15500 PKeyId::X25519 => Some(String::from("x25519")),
15501 PKeyId::X448 => Some(String::from("x448")),
15502 PKeyId::DH => Some(String::from("dh")),
15503 _ => None,
15504 }
15505}
15506
15507fn javascript_crypto_rsa_output_size(
15508 key: &JavascriptCryptoKeyMaterial,
15509) -> Result<usize, SidecarError> {
15510 match key {
15511 JavascriptCryptoKeyMaterial::Private(key) => key
15512 .rsa()
15513 .map(|rsa| rsa.size() as usize)
15514 .map_err(javascript_crypto_openssl_error),
15515 JavascriptCryptoKeyMaterial::Public(key) => key
15516 .rsa()
15517 .map(|rsa| rsa.size() as usize)
15518 .map_err(javascript_crypto_openssl_error),
15519 JavascriptCryptoKeyMaterial::Secret(_) => Err(SidecarError::InvalidState(String::from(
15520 "RSA operations require an asymmetric key",
15521 ))),
15522 }
15523}
15524
15525fn javascript_crypto_parse_serialized_options_arg(
15526 args: &[Value],
15527 index: usize,
15528 label: &str,
15529) -> Result<Option<Value>, SidecarError> {
15530 let Some(raw) = args.get(index).and_then(Value::as_str) else {
15531 return Ok(None);
15532 };
15533 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
15534 SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
15535 })?;
15536 if parsed.get("hasOptions").and_then(Value::as_bool) == Some(true) {
15537 Ok(parsed.get("options").cloned())
15538 } else {
15539 Ok(None)
15540 }
15541}
15542
15543fn javascript_crypto_u32_from_bridge_value(
15544 value: &Value,
15545 label: &str,
15546) -> Result<u32, SidecarError> {
15547 if let Some(number) = value.as_u64() {
15548 return u32::try_from(number)
15549 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")));
15550 }
15551 let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
15552 if bytes.len() > 4 {
15553 return Err(SidecarError::InvalidState(format!(
15554 "{label} buffer is too large for u32"
15555 )));
15556 }
15557 Ok(bytes
15558 .into_iter()
15559 .fold(0_u32, |acc, byte| (acc << 8) | u32::from(byte)))
15560}
15561
15562fn javascript_crypto_bignum_from_bridge_value(
15563 value: &Value,
15564 label: &str,
15565) -> Result<BigNum, SidecarError> {
15566 if let Some(object) = value.as_object() {
15567 if object.get("__type").and_then(Value::as_str) == Some("bigint") {
15568 let decimal = object.get("value").and_then(Value::as_str).ok_or_else(|| {
15569 SidecarError::InvalidState(format!("{label} bigint is missing a value"))
15570 })?;
15571 return BigNum::from_dec_str(decimal).map_err(javascript_crypto_openssl_error);
15572 }
15573 }
15574 let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
15575 BigNum::from_slice(&bytes).map_err(javascript_crypto_openssl_error)
15576}
15577
15578fn javascript_crypto_curve_nid(name: &str) -> Result<Nid, SidecarError> {
15579 match name {
15580 "prime256v1" | "P-256" => Ok(Nid::X9_62_PRIME256V1),
15581 "secp384r1" | "P-384" => Ok(Nid::SECP384R1),
15582 "secp521r1" | "P-521" => Ok(Nid::SECP521R1),
15583 "secp256k1" => Ok(Nid::SECP256K1),
15584 other => Err(SidecarError::InvalidState(format!(
15585 "unsupported EC curve {other}"
15586 ))),
15587 }
15588}
15589
15590fn javascript_crypto_named_dh_group(name: &str) -> Result<Dh<Params>, SidecarError> {
15591 match name {
15592 "modp2" => Dh::get_1024_160().map_err(javascript_crypto_openssl_error),
15593 "modp14" | "modp15" | "modp16" | "modp17" | "modp18" => {
15594 Dh::get_2048_256().map_err(javascript_crypto_openssl_error)
15595 }
15596 other => Err(SidecarError::InvalidState(format!(
15597 "unsupported Diffie-Hellman group {other}"
15598 ))),
15599 }
15600}
15601
15602fn javascript_crypto_clone_dh_params(params: &Dh<Params>) -> Result<Dh<Params>, SidecarError> {
15603 Dh::from_pqg(
15604 params
15605 .prime_p()
15606 .to_owned()
15607 .map_err(javascript_crypto_openssl_error)?,
15608 params
15609 .prime_q()
15610 .map(|value| value.to_owned().map_err(javascript_crypto_openssl_error))
15611 .transpose()?,
15612 params
15613 .generator()
15614 .to_owned()
15615 .map_err(javascript_crypto_openssl_error)?,
15616 )
15617 .map_err(javascript_crypto_openssl_error)
15618}
15619
15620fn javascript_crypto_build_dh_params(args: &[Value]) -> Result<Dh<Params>, SidecarError> {
15621 let Some(first) = args.first() else {
15622 return Err(SidecarError::InvalidState(String::from(
15623 "Diffie-Hellman session args are required",
15624 )));
15625 };
15626 if let Some(bits) = first.as_u64() {
15627 let generator = args
15628 .get(1)
15629 .map(|value| javascript_crypto_u32_from_bridge_value(value, "Diffie-Hellman generator"))
15630 .transpose()?
15631 .unwrap_or(2);
15632 return Dh::generate_params(bits as u32, generator)
15633 .map_err(javascript_crypto_openssl_error);
15634 }
15635 let prime = javascript_crypto_bignum_from_bridge_value(first, "Diffie-Hellman prime")?;
15636 let generator = args
15637 .get(1)
15638 .map(|value| javascript_crypto_bignum_from_bridge_value(value, "Diffie-Hellman generator"))
15639 .transpose()?
15640 .unwrap_or(BigNum::from_u32(2).map_err(javascript_crypto_openssl_error)?);
15641 Dh::from_pqg(prime, None, generator).map_err(javascript_crypto_openssl_error)
15642}
15643
15644fn javascript_crypto_call_dh_session(
15645 session: &mut ActiveDhSession,
15646 method: &str,
15647 args: &[Value],
15648) -> Result<(Value, bool), SidecarError> {
15649 match method {
15650 "verifyError" => Ok((Value::Null, false)),
15651 "generateKeys" => {
15652 if session.key_pair.is_none() {
15653 session.key_pair = Some(
15654 javascript_crypto_clone_dh_params(&session.params)?
15655 .generate_key()
15656 .map_err(javascript_crypto_openssl_error)?,
15657 );
15658 }
15659 let public = session
15660 .key_pair
15661 .as_ref()
15662 .expect("dh key pair")
15663 .public_key()
15664 .to_vec();
15665 Ok((javascript_crypto_bridge_buffer_value(&public), true))
15666 }
15667 "computeSecret" => {
15668 if session.key_pair.is_none() {
15669 session.key_pair = Some(
15670 javascript_crypto_clone_dh_params(&session.params)?
15671 .generate_key()
15672 .map_err(javascript_crypto_openssl_error)?,
15673 );
15674 }
15675 let peer = javascript_crypto_bignum_from_bridge_value(
15676 args.first().ok_or_else(|| {
15677 SidecarError::InvalidState(String::from(
15678 "computeSecret requires peer public key",
15679 ))
15680 })?,
15681 "Diffie-Hellman peer public key",
15682 )?;
15683 let secret = session
15684 .key_pair
15685 .as_ref()
15686 .expect("dh key pair")
15687 .compute_key(&peer)
15688 .map_err(javascript_crypto_openssl_error)?;
15689 Ok((javascript_crypto_bridge_buffer_value(&secret), true))
15690 }
15691 "getPrime" => Ok((
15692 javascript_crypto_bridge_buffer_value(&session.params.prime_p().to_vec()),
15693 true,
15694 )),
15695 "getGenerator" => Ok((
15696 javascript_crypto_bridge_buffer_value(&session.params.generator().to_vec()),
15697 true,
15698 )),
15699 "getPublicKey" => {
15700 if session.key_pair.is_none() {
15701 session.key_pair = Some(
15702 javascript_crypto_clone_dh_params(&session.params)?
15703 .generate_key()
15704 .map_err(javascript_crypto_openssl_error)?,
15705 );
15706 }
15707 Ok((
15708 javascript_crypto_bridge_buffer_value(
15709 &session
15710 .key_pair
15711 .as_ref()
15712 .expect("dh key pair")
15713 .public_key()
15714 .to_vec(),
15715 ),
15716 true,
15717 ))
15718 }
15719 "getPrivateKey" => {
15720 if session.key_pair.is_none() {
15721 session.key_pair = Some(
15722 javascript_crypto_clone_dh_params(&session.params)?
15723 .generate_key()
15724 .map_err(javascript_crypto_openssl_error)?,
15725 );
15726 }
15727 Ok((
15728 javascript_crypto_bridge_buffer_value(
15729 &session
15730 .key_pair
15731 .as_ref()
15732 .expect("dh key pair")
15733 .private_key()
15734 .to_vec(),
15735 ),
15736 true,
15737 ))
15738 }
15739 other => Err(SidecarError::InvalidState(format!(
15740 "Unsupported Diffie-Hellman method: {other}"
15741 ))),
15742 }
15743}
15744
15745fn javascript_crypto_call_ecdh_session(
15746 session: &mut ActiveEcdhSession,
15747 method: &str,
15748 args: &[Value],
15749) -> Result<(Value, bool), SidecarError> {
15750 let nid = javascript_crypto_curve_nid(&session.curve)?;
15751 let group = EcGroup::from_curve_name(nid).map_err(javascript_crypto_openssl_error)?;
15752 match method {
15753 "verifyError" => Ok((Value::Null, false)),
15754 "generateKeys" => {
15755 if session.key_pair.is_none() {
15756 session.key_pair =
15757 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15758 }
15759 let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15760 let bytes = session
15761 .key_pair
15762 .as_ref()
15763 .expect("ecdh key pair")
15764 .public_key()
15765 .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
15766 .map_err(javascript_crypto_openssl_error)?;
15767 Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
15768 }
15769 "computeSecret" => {
15770 if session.key_pair.is_none() {
15771 session.key_pair =
15772 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15773 }
15774 let peer_bytes = javascript_crypto_decode_bridge_buffer(
15775 args.first().ok_or_else(|| {
15776 SidecarError::InvalidState(String::from(
15777 "computeSecret requires peer public key",
15778 ))
15779 })?,
15780 "ECDH peer public key",
15781 )?;
15782 let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15783 let peer_point = EcPoint::from_bytes(&group, &peer_bytes, &mut ctx)
15784 .map_err(javascript_crypto_openssl_error)?;
15785 let peer_key = EcKey::from_public_key(&group, &peer_point)
15786 .map_err(javascript_crypto_openssl_error)?;
15787 let private =
15788 PKey::from_ec_key(session.key_pair.as_ref().expect("ecdh key pair").to_owned())
15789 .map_err(javascript_crypto_openssl_error)?;
15790 let peer = PKey::from_ec_key(peer_key).map_err(javascript_crypto_openssl_error)?;
15791 let mut deriver = Deriver::new(&private).map_err(javascript_crypto_openssl_error)?;
15792 deriver
15793 .set_peer(&peer)
15794 .map_err(javascript_crypto_openssl_error)?;
15795 let secret = deriver
15796 .derive_to_vec()
15797 .map_err(javascript_crypto_openssl_error)?;
15798 Ok((javascript_crypto_bridge_buffer_value(&secret), true))
15799 }
15800 "getPublicKey" => {
15801 if session.key_pair.is_none() {
15802 session.key_pair =
15803 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15804 }
15805 let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15806 let bytes = session
15807 .key_pair
15808 .as_ref()
15809 .expect("ecdh key pair")
15810 .public_key()
15811 .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
15812 .map_err(javascript_crypto_openssl_error)?;
15813 Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
15814 }
15815 "getPrivateKey" => {
15816 if session.key_pair.is_none() {
15817 session.key_pair =
15818 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15819 }
15820 Ok((
15821 javascript_crypto_bridge_buffer_value(
15822 &session
15823 .key_pair
15824 .as_ref()
15825 .expect("ecdh key pair")
15826 .private_key()
15827 .to_vec(),
15828 ),
15829 true,
15830 ))
15831 }
15832 other => Err(SidecarError::InvalidState(format!(
15833 "Unsupported Diffie-Hellman method: {other}"
15834 ))),
15835 }
15836}
15837
15838fn javascript_crypto_serialize_encoded_key_value_public(
15839 key: &PKey<Public>,
15840 encoding: Option<&Value>,
15841) -> Result<Value, SidecarError> {
15842 if let Some(encoding) = encoding {
15843 let format = encoding
15844 .get("format")
15845 .and_then(Value::as_str)
15846 .unwrap_or("pem");
15847 return Ok(match format {
15848 "der" => json!({
15849 "kind": "buffer",
15850 "value": base64::engine::general_purpose::STANDARD
15851 .encode(key.public_key_to_der().map_err(javascript_crypto_openssl_error)?),
15852 }),
15853 _ => json!({
15854 "kind": "string",
15855 "value": String::from_utf8(
15856 key.public_key_to_pem().map_err(javascript_crypto_openssl_error)?,
15857 )
15858 .map_err(|error| SidecarError::InvalidState(format!("public key PEM utf8: {error}")))?,
15859 }),
15860 });
15861 }
15862 javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(
15863 key.to_owned(),
15864 ))
15865}
15866
15867fn javascript_crypto_serialize_encoded_key_value_private(
15868 key: &PKey<Private>,
15869 encoding: Option<&Value>,
15870) -> Result<Value, SidecarError> {
15871 if let Some(encoding) = encoding {
15872 let format = encoding
15873 .get("format")
15874 .and_then(Value::as_str)
15875 .unwrap_or("pem");
15876 return Ok(match format {
15877 "der" => json!({
15878 "kind": "buffer",
15879 "value": base64::engine::general_purpose::STANDARD
15880 .encode(key.private_key_to_der().map_err(javascript_crypto_openssl_error)?),
15881 }),
15882 _ => json!({
15883 "kind": "string",
15884 "value": String::from_utf8(
15885 key.private_key_to_pem_pkcs8().map_err(javascript_crypto_openssl_error)?,
15886 )
15887 .map_err(|error| SidecarError::InvalidState(format!("private key PEM utf8: {error}")))?,
15888 }),
15889 });
15890 }
15891 javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(
15892 key.to_owned(),
15893 ))
15894}
15895
15896fn javascript_crypto_bridge_buffer_value(bytes: &[u8]) -> Value {
15897 json!({
15898 "__type": "buffer",
15899 "value": base64::engine::general_purpose::STANDARD.encode(bytes),
15900 })
15901}
15902
15903fn javascript_crypto_build_cipher_context(
15904 algorithm: &str,
15905 key: &[u8],
15906 iv: Option<&[u8]>,
15907 decrypt: bool,
15908 options: Option<&Value>,
15909) -> Result<Crypter, SidecarError> {
15910 let cipher = javascript_crypto_cipher_from_name(algorithm)?;
15911 let mode = if decrypt {
15912 Mode::Decrypt
15913 } else {
15914 Mode::Encrypt
15915 };
15916 let mut context =
15917 Crypter::new(cipher, mode, key, iv).map_err(javascript_crypto_openssl_error)?;
15918 if let Some(auto_padding) = options
15919 .and_then(|value| value.get("autoPadding"))
15920 .and_then(Value::as_bool)
15921 {
15922 context.pad(auto_padding);
15923 }
15924 if javascript_crypto_is_aead(algorithm) {
15925 if let Some(aad) = options
15926 .and_then(|value| value.get("aad"))
15927 .and_then(Value::as_str)
15928 {
15929 context
15930 .aad_update(
15931 &base64::engine::general_purpose::STANDARD
15932 .decode(aad)
15933 .map_err(|error| {
15934 SidecarError::InvalidState(format!(
15935 "cipher aad contains invalid base64: {error}"
15936 ))
15937 })?,
15938 )
15939 .map_err(javascript_crypto_openssl_error)?;
15940 }
15941 if decrypt {
15942 if let Some(auth_tag) = options
15943 .and_then(|value| value.get("authTag"))
15944 .and_then(Value::as_str)
15945 {
15946 let decoded = base64::engine::general_purpose::STANDARD
15947 .decode(auth_tag)
15948 .map_err(|error| {
15949 SidecarError::InvalidState(format!(
15950 "cipher authTag contains invalid base64: {error}"
15951 ))
15952 })?;
15953 context
15954 .set_tag(&decoded)
15955 .map_err(javascript_crypto_openssl_error)?;
15956 }
15957 }
15958 }
15959 Ok(context)
15960}
15961
15962fn javascript_crypto_requested_aead_tag_len(
15963 algorithm: &str,
15964 options: Option<&Value>,
15965) -> Result<usize, SidecarError> {
15966 if !javascript_crypto_is_aead(algorithm) {
15967 return Ok(0);
15968 }
15969 let requested = options
15970 .and_then(|value| value.get("authTagLength"))
15971 .and_then(Value::as_u64)
15972 .unwrap_or(javascript_crypto_aead_tag_len(algorithm) as u64);
15973 usize::try_from(requested).map_err(|_| {
15974 SidecarError::InvalidState(String::from("cipher authTagLength must fit within usize"))
15975 })
15976}
15977
15978fn javascript_crypto_cipher_update(
15979 context: &mut Crypter,
15980 data: &[u8],
15981) -> Result<Vec<u8>, SidecarError> {
15982 let mut output = vec![0_u8; data.len() + 32];
15983 let written = context
15984 .update(data, &mut output)
15985 .map_err(javascript_crypto_openssl_error)?;
15986 output.truncate(written);
15987 Ok(output)
15988}
15989
15990fn javascript_crypto_cipher_finalize(context: &mut Crypter) -> Result<Vec<u8>, SidecarError> {
15991 let mut output = vec![0_u8; 32];
15992 let written = context
15993 .finalize(&mut output)
15994 .map_err(javascript_crypto_openssl_error)?;
15995 output.truncate(written);
15996 Ok(output)
15997}
15998
15999fn javascript_crypto_cipher_from_name(name: &str) -> Result<Cipher, SidecarError> {
16000 match name.to_ascii_lowercase().as_str() {
16001 "aes-128-cbc" => Ok(Cipher::aes_128_cbc()),
16002 "aes-192-cbc" => Ok(Cipher::aes_192_cbc()),
16003 "aes-256-cbc" => Ok(Cipher::aes_256_cbc()),
16004 "aes-128-ctr" => Ok(Cipher::aes_128_ctr()),
16005 "aes-192-ctr" => Ok(Cipher::aes_192_ctr()),
16006 "aes-256-ctr" => Ok(Cipher::aes_256_ctr()),
16007 "aes-128-gcm" => Ok(Cipher::aes_128_gcm()),
16008 "aes-192-gcm" => Ok(Cipher::aes_192_gcm()),
16009 "aes-256-gcm" => Ok(Cipher::aes_256_gcm()),
16010 other => Err(SidecarError::InvalidState(format!(
16011 "unsupported crypto cipher algorithm {other}"
16012 ))),
16013 }
16014}
16015
16016fn javascript_crypto_is_aead(algorithm: &str) -> bool {
16017 algorithm.to_ascii_lowercase().ends_with("-gcm")
16018}
16019
16020fn javascript_crypto_aead_tag_len(_algorithm: &str) -> usize {
16021 16
16022}
16023
16024fn javascript_crypto_openssl_error(error: openssl::error::ErrorStack) -> SidecarError {
16025 SidecarError::Execution(format!("crypto operation failed: {error}"))
16026}
16027
16028fn service_javascript_kernel_stdin_sync_rpc(
16029 kernel: &mut SidecarKernel,
16030 process: &mut ActiveProcess,
16031 request: &JavascriptSyncRpcRequest,
16032) -> Result<Value, SidecarError> {
16033 let max_bytes =
16034 javascript_sync_rpc_arg_u64_optional(&request.args, 0, "__kernel_stdin_read max bytes")?
16035 .map(|value| value.clamp(1, DEFAULT_KERNEL_STDIN_READ_MAX_BYTES as u64) as usize)
16036 .unwrap_or(DEFAULT_KERNEL_STDIN_READ_MAX_BYTES);
16037 let timeout_ms =
16038 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_stdin_read timeout ms")?
16039 .unwrap_or(DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS);
16040
16041 match kernel
16042 .fd_read_with_timeout_result(
16043 EXECUTION_DRIVER_NAME,
16044 process.kernel_pid,
16045 0,
16046 max_bytes,
16047 Some(Duration::from_millis(timeout_ms)),
16048 )
16049 .map_err(kernel_error)
16050 {
16051 Ok(Some(chunk)) if !chunk.is_empty() => Ok(json!({
16052 "dataBase64": base64::engine::general_purpose::STANDARD.encode(chunk),
16053 })),
16054 Ok(Some(_)) => Ok(Value::Null),
16055 Ok(None) => Ok(json!({
16056 "done": true,
16057 })),
16058 Err(SidecarError::Kernel(error)) if error.starts_with("EAGAIN:") => Ok(Value::Null),
16059 Err(error) => Err(error),
16060 }
16061}
16062
16063fn service_javascript_pty_set_raw_mode_sync_rpc(
16064 kernel: &mut SidecarKernel,
16065 process: &mut ActiveProcess,
16066 request: &JavascriptSyncRpcRequest,
16067) -> Result<Value, SidecarError> {
16068 let enabled = javascript_sync_rpc_arg_bool(&request.args, 0, "__pty_set_raw_mode enabled")?;
16069 kernel
16070 .pty_set_discipline(
16071 EXECUTION_DRIVER_NAME,
16072 process.kernel_pid,
16073 0,
16074 LineDisciplineConfig {
16075 canonical: Some(!enabled),
16076 echo: Some(!enabled),
16077 isig: Some(!enabled),
16078 },
16079 )
16080 .map_err(kernel_error)?;
16081 Ok(Value::Null)
16082}
16083
16084fn service_javascript_kernel_stdio_write_sync_rpc(
16085 kernel: &mut SidecarKernel,
16086 process: &mut ActiveProcess,
16087 request: &JavascriptSyncRpcRequest,
16088) -> Result<Value, SidecarError> {
16089 let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_stdio_write fd")?;
16090 let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "__kernel_stdio_write chunk")?;
16091
16092 let written = match fd {
16093 1 => kernel
16094 .write_process_stdout(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
16095 .map_err(kernel_error)?,
16096 2 => kernel
16097 .write_process_stderr(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
16098 .map_err(kernel_error)?,
16099 other => {
16100 return Err(SidecarError::InvalidState(format!(
16101 "__kernel_stdio_write only supports fd 1/2, got {other}"
16102 )));
16103 }
16104 };
16105
16106 let event = if fd == 1 {
16107 ActiveExecutionEvent::Stdout(chunk)
16108 } else {
16109 ActiveExecutionEvent::Stderr(chunk)
16110 };
16111 process.queue_pending_execution_event(event)?;
16112
16113 Ok(json!(written))
16114}
16115
16116fn service_javascript_kernel_poll_sync_rpc(
16117 kernel: &mut SidecarKernel,
16118 process: &ActiveProcess,
16119 request: &JavascriptSyncRpcRequest,
16120) -> Result<Value, SidecarError> {
16121 let fd_requests: Vec<KernelPollFdRequest> = serde_json::from_value(
16122 request
16123 .args
16124 .first()
16125 .cloned()
16126 .unwrap_or_else(|| Value::Array(Vec::new())),
16127 )
16128 .map_err(|error| {
16129 SidecarError::InvalidState(format!(
16130 "__kernel_poll fd list must be a JSON array of {{ fd, events }} objects: {error}"
16131 ))
16132 })?;
16133 let timeout_ms =
16134 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_poll timeout ms")?
16135 .unwrap_or_default();
16136 let timeout_ms = i32::try_from(timeout_ms).map_err(|_| {
16137 SidecarError::InvalidState(String::from("__kernel_poll timeout ms must fit within i32"))
16138 })?;
16139
16140 let poll_fds = fd_requests
16141 .iter()
16142 .map(|entry| PollFd {
16143 fd: entry.fd,
16144 events: PollEvents::from_bits(entry.events),
16145 revents: PollEvents::empty(),
16146 })
16147 .collect::<Vec<_>>();
16148 let result = kernel
16149 .poll_fds(
16150 EXECUTION_DRIVER_NAME,
16151 process.kernel_pid,
16152 poll_fds,
16153 timeout_ms,
16154 )
16155 .map_err(kernel_error)?;
16156
16157 Ok(json!({
16158 "readyCount": result.ready_count,
16159 "fds": result
16160 .fds
16161 .into_iter()
16162 .map(|entry| KernelPollFdResponse {
16163 fd: entry.fd,
16164 events: entry.events.bits(),
16165 revents: entry.revents.bits(),
16166 })
16167 .collect::<Vec<_>>(),
16168 }))
16169}
16170
16171fn install_kernel_stdin_pipe(kernel: &mut SidecarKernel, pid: u32) -> Result<u32, SidecarError> {
16172 let (read_fd, write_fd) = kernel
16173 .open_pipe(EXECUTION_DRIVER_NAME, pid)
16174 .map_err(kernel_error)?;
16175 kernel
16176 .fd_dup2(EXECUTION_DRIVER_NAME, pid, read_fd, 0)
16177 .map_err(kernel_error)?;
16178 kernel
16179 .fd_close(EXECUTION_DRIVER_NAME, pid, read_fd)
16180 .map_err(kernel_error)?;
16181 Ok(write_fd)
16182}
16183
16184fn javascript_child_process_stdin_mode(request: &JavascriptChildProcessSpawnRequest) -> &str {
16185 request
16186 .options
16187 .stdio
16188 .first()
16189 .map(String::as_str)
16190 .unwrap_or("pipe")
16191}
16192
16193pub(crate) fn write_kernel_process_stdin(
16194 kernel: &mut SidecarKernel,
16195 process: &mut ActiveProcess,
16196 chunk: &[u8],
16197) -> Result<(), SidecarError> {
16198 if process.runtime == GuestRuntimeKind::JavaScript {
16199 return Ok(());
16200 }
16201 let Some(writer_fd) = process.kernel_stdin_writer_fd else {
16202 return Ok(());
16203 };
16204 kernel
16205 .fd_write(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd, chunk)
16206 .map(|_| ())
16207 .map_err(kernel_error)
16208}
16209
16210pub(crate) fn close_kernel_process_stdin(
16211 kernel: &mut SidecarKernel,
16212 process: &mut ActiveProcess,
16213) -> Result<(), SidecarError> {
16214 let Some(writer_fd) = process.kernel_stdin_writer_fd.take() else {
16215 return Ok(());
16216 };
16217 kernel
16218 .fd_close(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd)
16219 .map_err(kernel_error)
16220}
16221
16222fn parse_http_header_collection(
16223 headers: &BTreeMap<String, Value>,
16224 label: &str,
16225) -> Result<HttpHeaderCollection, SidecarError> {
16226 let mut normalized = BTreeMap::<String, Vec<String>>::new();
16227 let mut raw_pairs = Vec::new();
16228
16229 for (raw_name, value) in headers {
16230 let normalized_name = raw_name.to_ascii_lowercase();
16231 let values = match value {
16232 Value::String(text) => vec![text.clone()],
16233 Value::Array(values) => values
16234 .iter()
16235 .map(|entry| {
16236 entry.as_str().map(str::to_owned).ok_or_else(|| {
16237 SidecarError::InvalidState(format!(
16238 "{label} header {raw_name} must contain only strings"
16239 ))
16240 })
16241 })
16242 .collect::<Result<Vec<_>, _>>()?,
16243 other => {
16244 return Err(SidecarError::InvalidState(format!(
16245 "{label} header {raw_name} must be a string or string array, received {other}"
16246 )));
16247 }
16248 };
16249 raw_pairs.extend(
16250 values
16251 .iter()
16252 .cloned()
16253 .map(|entry| (raw_name.clone(), entry)),
16254 );
16255 normalized
16256 .entry(normalized_name)
16257 .or_default()
16258 .extend(values);
16259 }
16260
16261 Ok(HttpHeaderCollection {
16262 normalized,
16263 raw_pairs,
16264 })
16265}
16266
16267fn http_headers_json(headers: &HttpHeaderCollection) -> Value {
16268 let map = headers
16269 .normalized
16270 .iter()
16271 .map(|(name, values)| {
16272 let value = if values.len() == 1 {
16273 Value::String(values[0].clone())
16274 } else {
16275 Value::Array(values.iter().cloned().map(Value::String).collect())
16276 };
16277 (name.clone(), value)
16278 })
16279 .collect::<Map<String, Value>>();
16280 Value::Object(map)
16281}
16282
16283fn http_raw_headers_json(headers: &HttpHeaderCollection) -> Value {
16284 Value::Array(
16285 headers
16286 .raw_pairs
16287 .iter()
16288 .flat_map(|(name, value)| [Value::String(name.clone()), Value::String(value.clone())])
16289 .collect(),
16290 )
16291}
16292
16293fn is_loopback_request_host(host: &str) -> bool {
16294 let bare = host
16295 .strip_prefix('[')
16296 .and_then(|value| value.strip_suffix(']'))
16297 .unwrap_or(host);
16298 matches!(bare, "localhost" | "127.0.0.1" | "::1")
16299}
16300
16301fn serialize_http_loopback_request(
16302 url: &Url,
16303 options: &JavascriptHttpRequestOptions,
16304 headers: &HttpHeaderCollection,
16305) -> Result<String, SidecarError> {
16306 let body_base64 = options
16307 .body
16308 .as_ref()
16309 .map(|body| base64::engine::general_purpose::STANDARD.encode(body.as_bytes()));
16310 serde_json::to_string(&json!({
16311 "method": options.method.clone().unwrap_or_else(|| String::from("GET")),
16312 "url": http_request_target(url),
16313 "headers": http_headers_json(headers),
16314 "rawHeaders": http_raw_headers_json(headers),
16315 "bodyBase64": body_base64,
16316 }))
16317 .map_err(|error| SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}")))
16318}
16319
16320fn http_request_target(url: &Url) -> String {
16321 let path = if url.path().is_empty() {
16322 "/"
16323 } else {
16324 url.path()
16325 };
16326 format!(
16327 "{path}{}",
16328 url.query()
16329 .map(|query| format!("?{query}"))
16330 .unwrap_or_default()
16331 )
16332}
16333
16334fn find_kernel_http_listener_process(vm: &VmState, port: u16) -> Option<String> {
16335 vm.active_processes
16336 .iter()
16337 .find_map(|(process_id, process)| {
16338 process.tcp_listeners.values().find_map(|listener| {
16339 let socket_id = listener.kernel_socket_id?;
16340 let record = vm.kernel.socket_get(socket_id)?;
16341 let local_addr = record
16342 .local_address()
16343 .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
16344 .unwrap_or_else(|| listener.guest_local_addr());
16345 if local_addr.port() == port && is_vm_local_http_listener_addr(local_addr.ip()) {
16346 Some(process_id.to_owned())
16347 } else {
16348 None
16349 }
16350 })
16351 })
16352}
16353
16354fn is_vm_local_http_listener_addr(ip: IpAddr) -> bool {
16355 ip.is_loopback() || ip.is_unspecified()
16356}
16357
16358fn serialize_kernel_http_fetch_request(
16359 port: u16,
16360 path: &str,
16361 options: &JavascriptHttpRequestOptions,
16362 headers: &HttpHeaderCollection,
16363) -> Vec<u8> {
16364 let method = options.method.as_deref().unwrap_or("GET");
16365 let mut lines = vec![format!("{method} {path} HTTP/1.1")];
16366 let mut has_host = false;
16367 let mut has_connection = false;
16368 let mut has_content_length = false;
16369 for (name, values) in &headers.normalized {
16370 match name.as_str() {
16371 "host" => has_host = true,
16372 "connection" => has_connection = true,
16373 "content-length" => has_content_length = true,
16374 _ => {}
16375 }
16376 lines.push(format!("{name}: {}", values.join(", ")));
16377 }
16378 if !has_host {
16379 lines.push(format!("Host: 127.0.0.1:{port}"));
16380 }
16381 if !has_connection {
16382 lines.push(String::from("Connection: close"));
16383 }
16384 let body = options.body.as_deref().unwrap_or("").as_bytes();
16385 if !has_content_length && !body.is_empty() {
16386 lines.push(format!("Content-Length: {}", body.len()));
16387 }
16388 lines.push(String::new());
16389 lines.push(String::new());
16390
16391 let mut request = lines.join("\r\n").into_bytes();
16392 request.extend_from_slice(body);
16393 request
16394}
16395
16396fn parse_kernel_http_fetch_response(
16397 buffer: &[u8],
16398 peer_closed: bool,
16399 url: &str,
16400) -> Result<Option<String>, SidecarError> {
16401 let Some(header_end) = find_http_header_end(buffer) else {
16402 return Ok(None);
16403 };
16404 let header_bytes = &buffer[..header_end];
16405 let head = String::from_utf8_lossy(header_bytes);
16406 let mut lines = head.split("\r\n");
16407 let status_line = lines.next().unwrap_or_default();
16408 let mut status_parts = status_line.splitn(3, ' ');
16409 let version = status_parts.next().unwrap_or_default();
16410 if !version.starts_with("HTTP/") {
16411 return Err(SidecarError::Execution(format!(
16412 "invalid vm.fetch HTTP response status line: {status_line}"
16413 )));
16414 }
16415 let status = status_parts
16416 .next()
16417 .ok_or_else(|| {
16418 SidecarError::Execution(format!(
16419 "invalid vm.fetch HTTP response status line: {status_line}"
16420 ))
16421 })?
16422 .parse::<u16>()
16423 .map_err(|error| {
16424 SidecarError::Execution(format!(
16425 "invalid vm.fetch HTTP response status code in {status_line:?}: {error}"
16426 ))
16427 })?;
16428 let status_text = status_parts.next().unwrap_or_default();
16429 let mut headers = Vec::new();
16430 let mut raw_headers = Vec::new();
16431 let mut content_length = None;
16432 let mut transfer_encoding_values = Vec::new();
16433 for line in lines {
16434 if line.is_empty() {
16435 continue;
16436 }
16437 let Some((name, value)) = line.split_once(':') else {
16438 return Err(SidecarError::Execution(format!(
16439 "invalid vm.fetch HTTP response header line: {line}"
16440 )));
16441 };
16442 let value = value.trim().to_owned();
16443 let normalized = name.to_ascii_lowercase();
16444 if normalized == "content-length" {
16445 content_length = Some(value.parse::<usize>().map_err(|error| {
16446 SidecarError::Execution(format!(
16447 "invalid vm.fetch Content-Length header {value:?}: {error}"
16448 ))
16449 })?);
16450 } else if normalized == "transfer-encoding" {
16451 transfer_encoding_values.push(value.clone());
16452 }
16453 headers.push(json!([normalized, value.clone()]));
16454 raw_headers.push(Value::String(name.to_owned()));
16455 raw_headers.push(Value::String(value));
16456 }
16457
16458 let body_start = header_end + 4;
16459 let transfer_encoding = transfer_encoding_tokens(&transfer_encoding_values);
16460 let is_chunked = transfer_encoding.iter().any(|token| token == "chunked");
16461 let body = if is_chunked {
16462 if content_length.is_some() {
16463 return Err(SidecarError::Execution(String::from(
16464 "vm.fetch HTTP response cannot include both Transfer-Encoding: chunked and Content-Length",
16465 )));
16466 }
16467 if transfer_encoding.len() != 1 {
16468 return Err(SidecarError::Execution(format!(
16469 "unsupported vm.fetch Transfer-Encoding: {}",
16470 transfer_encoding.join(", ")
16471 )));
16472 }
16473 let Some(decoded) = decode_kernel_http_chunked_body(&buffer[body_start..])? else {
16474 return Ok(None);
16475 };
16476 decoded
16477 } else if !transfer_encoding.is_empty() {
16478 return Err(SidecarError::Execution(format!(
16479 "unsupported vm.fetch Transfer-Encoding: {}",
16480 transfer_encoding.join(", ")
16481 )));
16482 } else if let Some(content_length) = content_length {
16483 let body_end = body_start.saturating_add(content_length);
16484 if buffer.len() < body_end {
16485 return Ok(None);
16486 }
16487 buffer[body_start..body_end].to_vec()
16488 } else if peer_closed {
16489 buffer[body_start..].to_vec()
16490 } else {
16491 return Ok(None);
16492 };
16493
16494 serde_json::to_string(&json!({
16495 "status": status,
16496 "statusText": status_text,
16497 "headers": headers,
16498 "rawHeaders": raw_headers,
16499 "body": base64::engine::general_purpose::STANDARD.encode(&body),
16500 "bodyEncoding": "base64",
16501 "url": url,
16502 }))
16503 .map(Some)
16504 .map_err(|error| SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}")))
16505}
16506
16507fn find_http_header_end(buffer: &[u8]) -> Option<usize> {
16508 buffer.windows(4).position(|window| window == b"\r\n\r\n")
16509}
16510
16511fn find_crlf(buffer: &[u8], start: usize) -> Option<usize> {
16512 buffer
16513 .get(start..)?
16514 .windows(2)
16515 .position(|window| window == b"\r\n")
16516 .map(|offset| start + offset)
16517}
16518
16519fn transfer_encoding_tokens(values: &[String]) -> Vec<String> {
16520 values
16521 .iter()
16522 .flat_map(|value| value.split(','))
16523 .map(|token| token.trim().to_ascii_lowercase())
16524 .filter(|token| !token.is_empty())
16525 .collect()
16526}
16527
16528fn decode_kernel_http_chunked_body(buffer: &[u8]) -> Result<Option<Vec<u8>>, SidecarError> {
16529 let mut offset = 0;
16530 let mut body = Vec::new();
16531 loop {
16532 let Some(line_end) = find_crlf(buffer, offset) else {
16533 return Ok(None);
16534 };
16535 let size_line = std::str::from_utf8(&buffer[offset..line_end]).map_err(|error| {
16536 SidecarError::Execution(format!(
16537 "invalid vm.fetch chunk size line encoding: {error}"
16538 ))
16539 })?;
16540 let size_part = size_line.split(';').next().unwrap_or_default();
16541 if size_part.is_empty() || !size_part.bytes().all(|byte| byte.is_ascii_hexdigit()) {
16542 return Err(SidecarError::Execution(format!(
16543 "invalid vm.fetch chunk size line: {size_line:?}"
16544 )));
16545 }
16546 let chunk_size = usize::from_str_radix(size_part, 16).map_err(|error| {
16547 SidecarError::Execution(format!(
16548 "invalid vm.fetch chunk size {size_part:?}: {error}"
16549 ))
16550 })?;
16551 let chunk_start = line_end + 2;
16552 let chunk_end = chunk_start
16553 .checked_add(chunk_size)
16554 .ok_or_else(|| SidecarError::Execution(String::from("vm.fetch chunk size overflow")))?;
16555 if chunk_size > 0 {
16556 let chunk_terminator_end = chunk_end.checked_add(2).ok_or_else(|| {
16557 SidecarError::Execution(String::from("vm.fetch chunk terminator overflow"))
16558 })?;
16559 if chunk_terminator_end > buffer.len() {
16560 return Ok(None);
16561 }
16562 if buffer.get(chunk_end..chunk_terminator_end) != Some(b"\r\n") {
16563 return Err(SidecarError::Execution(String::from(
16564 "invalid vm.fetch chunk terminator",
16565 )));
16566 }
16567 body.extend_from_slice(&buffer[chunk_start..chunk_end]);
16568 offset = chunk_terminator_end;
16569 continue;
16570 }
16571
16572 if buffer.get(chunk_start..chunk_start + 2) == Some(b"\r\n") {
16573 return Ok(Some(body));
16574 }
16575 let Some(trailer_end) = find_http_header_end(&buffer[chunk_start..]) else {
16576 return Ok(None);
16577 };
16578 let trailer_bytes = &buffer[chunk_start..chunk_start + trailer_end];
16579 let trailers = String::from_utf8_lossy(trailer_bytes);
16580 for line in trailers.split("\r\n") {
16581 if line.is_empty() {
16582 continue;
16583 }
16584 if line.starts_with(' ') || line.starts_with('\t') || !line.contains(':') {
16585 return Err(SidecarError::Execution(format!(
16586 "invalid vm.fetch chunk trailer line: {line}"
16587 )));
16588 }
16589 }
16590 return Ok(Some(body));
16591 }
16592}
16593
16594fn kernel_http_fetch_target_exit_code(error: &SidecarError) -> Option<i32> {
16595 let SidecarError::Execution(message) = error else {
16596 return None;
16597 };
16598 message
16599 .strip_prefix("vm.fetch target exited before responding (exit code ")?
16600 .strip_suffix(')')?
16601 .parse()
16602 .ok()
16603}
16604
16605fn service_host_fetch_target_event<B>(
16606 bridge: &SharedBridge<B>,
16607 vm_id: &str,
16608 dns: &VmDnsConfig,
16609 socket_paths: &JavascriptSocketPathContext,
16610 kernel: &mut SidecarKernel,
16611 process: &mut ActiveProcess,
16612 resource_limits: &ResourceLimits,
16613 wait: Duration,
16614) -> Result<bool, SidecarError>
16615where
16616 B: NativeSidecarBridge + Send + 'static,
16617 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16618{
16619 let Some(event) = process
16620 .execution
16621 .poll_event_blocking(wait)
16622 .map_err(|error| SidecarError::Execution(error.to_string()))?
16623 else {
16624 return Ok(false);
16625 };
16626
16627 match event {
16628 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
16629 let network_counts = process.network_resource_counts();
16630 let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
16631 bridge,
16632 vm_id,
16633 dns,
16634 socket_paths,
16635 kernel,
16636 process,
16637 sync_request: &request,
16638 resource_limits,
16639 network_counts,
16640 });
16641 match response {
16642 Ok(result) => process
16643 .execution
16644 .respond_javascript_sync_rpc_success(request.id, result)
16645 .or_else(ignore_stale_javascript_sync_rpc_response)?,
16646 Err(error) => process
16647 .execution
16648 .respond_javascript_sync_rpc_error(
16649 request.id,
16650 javascript_sync_rpc_error_code(&error),
16651 error.to_string(),
16652 )
16653 .or_else(ignore_stale_javascript_sync_rpc_response)?,
16654 }
16655 }
16656 ActiveExecutionEvent::Exited(code) => {
16657 return Err(SidecarError::Execution(format!(
16658 "vm.fetch target exited before responding (exit code {code})"
16659 )));
16660 }
16661 other => {
16662 process.queue_pending_execution_event(other)?;
16663 }
16664 }
16665 Ok(true)
16666}
16667
16668fn drain_host_fetch_target_events<B>(
16669 bridge: &SharedBridge<B>,
16670 vm_id: &str,
16671 vm: &mut VmState,
16672 target_process_id: &str,
16673 socket_paths: &JavascriptSocketPathContext,
16674 resource_limits: &ResourceLimits,
16675) -> Result<(), SidecarError>
16676where
16677 B: NativeSidecarBridge + Send + 'static,
16678 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16679{
16680 for _ in 0..32 {
16681 let dns = vm.dns.clone();
16682 let Some(process) = vm.active_processes.get_mut(target_process_id) else {
16683 break;
16684 };
16685 let serviced = service_host_fetch_target_event(
16686 bridge,
16687 vm_id,
16688 &dns,
16689 socket_paths,
16690 &mut vm.kernel,
16691 process,
16692 resource_limits,
16693 Duration::from_millis(1),
16694 )?;
16695 if !serviced {
16696 break;
16697 }
16698 }
16699 Ok(())
16700}
16701
16702fn dispatch_kernel_http_fetch<B>(
16703 bridge: &SharedBridge<B>,
16704 vm_id: &str,
16705 vm: &mut VmState,
16706 target_process_id: &str,
16707 port: u16,
16708 path: &str,
16709 options: &JavascriptHttpRequestOptions,
16710 headers: &HttpHeaderCollection,
16711 max_fetch_response_bytes: usize,
16712) -> Result<String, SidecarError>
16713where
16714 B: NativeSidecarBridge + Send + 'static,
16715 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16716{
16717 let socket_paths = build_javascript_socket_path_context(vm)?;
16718 let family = JavascriptSocketFamily::Ipv4;
16719 let local_port = allocate_guest_listen_port(
16720 0,
16721 family,
16722 &socket_paths.used_tcp_guest_ports,
16723 socket_paths.listen_policy,
16724 )?;
16725 let resource_limits = vm.kernel.resource_limits().clone();
16726 let network_counts = vm_network_resource_counts(vm);
16727 check_network_resource_limit(
16728 resource_limits.max_sockets,
16729 network_counts.sockets,
16730 2,
16731 "socket",
16732 )?;
16733 check_network_resource_limit(
16734 resource_limits.max_connections,
16735 network_counts.connections,
16736 2,
16737 "connection",
16738 )?;
16739
16740 let kernel_pid = vm
16741 .active_processes
16742 .get(target_process_id)
16743 .ok_or_else(|| {
16744 SidecarError::InvalidState(format!(
16745 "vm.fetch target process disappeared: {target_process_id}"
16746 ))
16747 })?
16748 .kernel_pid;
16749 let socket_id = vm
16750 .kernel
16751 .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, SocketSpec::tcp())
16752 .map_err(kernel_error)?;
16753
16754 let result = dispatch_kernel_http_fetch_with_socket(
16755 bridge,
16756 vm_id,
16757 vm,
16758 target_process_id,
16759 kernel_pid,
16760 socket_id,
16761 local_port,
16762 port,
16763 path,
16764 options,
16765 headers,
16766 &socket_paths,
16767 &resource_limits,
16768 max_fetch_response_bytes,
16769 );
16770 let close_result = vm
16771 .kernel
16772 .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
16773 .map_err(kernel_error);
16774 let cleanup_result = if result.is_err() {
16775 drain_host_fetch_target_events(
16776 bridge,
16777 vm_id,
16778 vm,
16779 target_process_id,
16780 &socket_paths,
16781 &resource_limits,
16782 )
16783 } else {
16784 Ok(())
16785 };
16786 match (result, close_result) {
16787 (Ok(response), Ok(())) => cleanup_result.map(|()| response),
16788 (Err(error), _) => Err(error),
16789 (Ok(_), Err(error)) => Err(error),
16790 }
16791}
16792
16793#[allow(clippy::too_many_arguments)]
16794fn dispatch_kernel_http_fetch_with_socket<B>(
16795 bridge: &SharedBridge<B>,
16796 vm_id: &str,
16797 vm: &mut VmState,
16798 target_process_id: &str,
16799 kernel_pid: u32,
16800 socket_id: SocketId,
16801 local_port: u16,
16802 port: u16,
16803 path: &str,
16804 options: &JavascriptHttpRequestOptions,
16805 headers: &HttpHeaderCollection,
16806 socket_paths: &JavascriptSocketPathContext,
16807 resource_limits: &ResourceLimits,
16808 max_fetch_response_bytes: usize,
16809) -> Result<String, SidecarError>
16810where
16811 B: NativeSidecarBridge + Send + 'static,
16812 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16813{
16814 vm.kernel
16815 .socket_bind_inet(
16816 EXECUTION_DRIVER_NAME,
16817 kernel_pid,
16818 socket_id,
16819 InetSocketAddress::new("127.0.0.1", local_port),
16820 )
16821 .map_err(kernel_error)?;
16822 vm.kernel
16823 .socket_connect_inet_loopback(
16824 EXECUTION_DRIVER_NAME,
16825 kernel_pid,
16826 socket_id,
16827 InetSocketAddress::new("127.0.0.1", port),
16828 )
16829 .map_err(kernel_error)?;
16830
16831 let request_bytes = serialize_kernel_http_fetch_request(port, path, options, headers);
16832 vm.kernel
16833 .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, &request_bytes)
16834 .map_err(kernel_error)?;
16835
16836 let mut response_buffer = Vec::new();
16837 let mut peer_closed = false;
16838 let url = format!("http://127.0.0.1:{port}{path}");
16839 let deadline = Instant::now() + http_loopback_request_timeout();
16840 loop {
16841 if let Some(response) =
16842 parse_kernel_http_fetch_response(&response_buffer, peer_closed, &url)?
16843 {
16844 ensure_vm_fetch_response_within_limit(&response, "vm.fetch", max_fetch_response_bytes)?;
16845 return Ok(response);
16846 }
16847 if Instant::now() >= deadline {
16848 let preview = String::from_utf8_lossy(&response_buffer);
16849 return Err(SidecarError::Execution(format!(
16850 "vm.fetch timed out waiting for kernel TCP HTTP response ({} buffered bytes: {:?})",
16851 response_buffer.len(),
16852 preview.chars().take(200).collect::<String>()
16853 )));
16854 }
16855
16856 {
16857 let dns = vm.dns.clone();
16858 let process = vm
16859 .active_processes
16860 .get_mut(target_process_id)
16861 .ok_or_else(|| {
16862 SidecarError::InvalidState(format!(
16863 "vm.fetch target process disappeared: {target_process_id}"
16864 ))
16865 })?;
16866 service_host_fetch_target_event(
16867 bridge,
16868 vm_id,
16869 &dns,
16870 socket_paths,
16871 &mut vm.kernel,
16872 process,
16873 resource_limits,
16874 Duration::from_millis(5),
16875 )?;
16876 }
16877
16878 let poll = vm
16879 .kernel
16880 .poll_targets(
16881 EXECUTION_DRIVER_NAME,
16882 kernel_pid,
16883 vec![PollTargetEntry::socket(
16884 socket_id,
16885 POLLIN | POLLHUP | POLLERR,
16886 )],
16887 5,
16888 )
16889 .map_err(kernel_error)?;
16890 let revents = poll
16891 .targets
16892 .first()
16893 .map(|entry| entry.revents)
16894 .unwrap_or_else(PollEvents::empty);
16895 if revents.intersects(POLLERR) {
16896 return Err(SidecarError::Execution(String::from(
16897 "vm.fetch kernel TCP socket reported POLLERR",
16898 )));
16899 }
16900 if revents.intersects(POLLIN) {
16901 match vm
16902 .kernel
16903 .socket_read(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, 64 * 1024)
16904 {
16905 Ok(Some(bytes)) if !bytes.is_empty() => {
16906 response_buffer.extend(bytes);
16907 ensure_vm_fetch_raw_response_buffer_within_limit(
16908 response_buffer.len(),
16909 "vm.fetch",
16910 )?;
16911 }
16912 Ok(Some(_)) => {}
16913 Ok(None) => peer_closed = true,
16914 Err(error) if error.code() == "EAGAIN" => {}
16915 Err(error) => return Err(kernel_error(error)),
16916 }
16917 }
16918 if revents.intersects(POLLHUP) {
16919 peer_closed = true;
16920 }
16921 }
16922}
16923
16924fn outbound_http_response_json(url: &Url, response: ureq::Response) -> Result<Value, SidecarError> {
16925 let status = response.status();
16926 let status_text = response.status_text().to_owned();
16927 let mut header_pairs = Vec::new();
16928 let mut raw_headers = Vec::new();
16929 for raw_name in response.headers_names() {
16930 for value in response.all(&raw_name) {
16931 header_pairs.push(json!([raw_name.to_ascii_lowercase(), value]));
16932 raw_headers.push(Value::String(raw_name.clone()));
16933 raw_headers.push(Value::String(value.to_owned()));
16934 }
16935 }
16936 let mut reader = response.into_reader();
16937 let mut body = Vec::new();
16938 reader.read_to_end(&mut body).map_err(|error| {
16939 SidecarError::Execution(format!("failed to read HTTP response: {error}"))
16940 })?;
16941 serde_json::to_string(&json!({
16942 "status": status,
16943 "statusText": status_text,
16944 "headers": header_pairs,
16945 "rawHeaders": raw_headers,
16946 "body": base64::engine::general_purpose::STANDARD.encode(body),
16947 "bodyEncoding": "base64",
16948 "url": url.as_str(),
16949 }))
16950 .map(Value::String)
16951 .map_err(|error| SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}")))
16952}
16953
16954fn split_netloc(netloc: &str) -> Option<(&str, u16)> {
16958 let (host, port) = netloc.rsplit_once(':')?;
16959 let port: u16 = port.parse().ok()?;
16960 let host = host
16961 .strip_prefix('[')
16962 .and_then(|rest| rest.strip_suffix(']'))
16963 .unwrap_or(host);
16964 Some((host, port))
16965}
16966
16967fn issue_outbound_http_request(
16968 url: &Url,
16969 options: &JavascriptHttpRequestOptions,
16970 headers: &HttpHeaderCollection,
16971 pinned_addresses: &[IpAddr],
16972) -> Result<Value, SidecarError> {
16973 let method = options.method.as_deref().unwrap_or("GET");
16974 let pinned_host = url.host_str().map(str::to_owned);
16983 let pinned: Vec<IpAddr> = pinned_addresses.to_vec();
16984 let resolver = move |netloc: &str| -> std::io::Result<Vec<SocketAddr>> {
16985 let (host, port) = split_netloc(netloc).ok_or_else(|| {
16986 std::io::Error::new(
16987 std::io::ErrorKind::InvalidInput,
16988 format!("invalid network location: {netloc}"),
16989 )
16990 })?;
16991 let expected_host = pinned_host.as_deref();
16992 if expected_host != Some(host) {
16993 return Err(std::io::Error::new(
16994 std::io::ErrorKind::PermissionDenied,
16995 format!(
16996 "EACCES: outbound HTTP resolver pinned to {expected_host:?}, refusing {host}"
16997 ),
16998 ));
16999 }
17000 if pinned.is_empty() {
17001 return Err(std::io::Error::new(
17002 std::io::ErrorKind::PermissionDenied,
17003 "EACCES: no egress-vetted address available for outbound HTTP request",
17004 ));
17005 }
17006 Ok(pinned.iter().map(|ip| SocketAddr::new(*ip, port)).collect())
17007 };
17008 let mut agent_builder = ureq::AgentBuilder::new()
17009 .resolver(resolver)
17010 .timeout_connect(Duration::from_secs(5))
17011 .timeout_read(Duration::from_secs(15))
17012 .timeout_write(Duration::from_secs(15));
17013 if url.scheme() == "https" {
17014 let tls_options = JavascriptTlsBridgeOptions {
17015 is_server: false,
17016 servername: url.host_str().map(str::to_owned),
17017 alpn_protocols: Some(vec![String::from("http/1.1")]),
17018 reject_unauthorized: options.reject_unauthorized,
17019 ..JavascriptTlsBridgeOptions::default()
17020 };
17021 agent_builder = agent_builder.tls_config(Arc::new(build_client_tls_config(&tls_options)?));
17022 }
17023 let agent = agent_builder.build();
17024 let mut request = agent.request_url(method, url);
17025 for (name, values) in &headers.normalized {
17026 if name == "host" {
17027 continue;
17028 }
17029 let header_value = values.join(", ");
17030 request = request.set(name, &header_value);
17031 }
17032 let response = match options.body.as_deref() {
17033 Some(body) => request.send_string(body),
17034 None => request.call(),
17035 };
17036
17037 match response {
17038 Ok(response) => outbound_http_response_json(url, response),
17039 Err(ureq::Error::Status(_, response)) => outbound_http_response_json(url, response),
17040 Err(ureq::Error::Transport(error)) => Err(SidecarError::Execution(format!(
17041 "ERR_HTTP_REQUEST_FAILED: {error}"
17042 ))),
17043 }
17044}
17045
17046fn wait_for_loopback_http_response<B>(
17047 request: LoopbackHttpResponseWaitRequest<'_, B>,
17048) -> Result<String, SidecarError>
17049where
17050 B: NativeSidecarBridge + Send + 'static,
17051 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17052{
17053 let LoopbackHttpResponseWaitRequest {
17054 bridge,
17055 vm_id,
17056 dns,
17057 socket_paths,
17058 kernel,
17059 process,
17060 resource_limits,
17061 request_key,
17062 } = request;
17063 let deadline = Instant::now() + http_loopback_request_timeout();
17064 loop {
17065 if let Some(response) = process
17066 .pending_http_requests
17067 .get(&request_key)
17068 .and_then(|response| response.clone())
17069 {
17070 process.pending_http_requests.remove(&request_key);
17071 return Ok(response);
17072 }
17073
17074 if Instant::now() >= deadline {
17075 process.pending_http_requests.remove(&request_key);
17076 return Err(SidecarError::Execution(String::from(
17077 "HTTP loopback request timed out waiting for net.http_respond",
17078 )));
17079 }
17080
17081 let Some(event) = process
17082 .execution
17083 .poll_event_blocking(Duration::from_millis(10))
17084 .map_err(|error| SidecarError::Execution(error.to_string()))?
17085 else {
17086 continue;
17087 };
17088
17089 match event {
17090 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
17091 let network_counts = process.network_resource_counts();
17092 let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
17093 bridge,
17094 vm_id,
17095 dns,
17096 socket_paths,
17097 kernel,
17098 process,
17099 sync_request: &request,
17100 resource_limits,
17101 network_counts,
17102 });
17103 match response {
17104 Ok(result) => process
17105 .execution
17106 .respond_javascript_sync_rpc_success(request.id, result)
17107 .or_else(ignore_stale_javascript_sync_rpc_response)?,
17108 Err(error) => process
17109 .execution
17110 .respond_javascript_sync_rpc_error(
17111 request.id,
17112 javascript_sync_rpc_error_code(&error),
17113 error.to_string(),
17114 )
17115 .or_else(ignore_stale_javascript_sync_rpc_response)?,
17116 }
17117 }
17118 ActiveExecutionEvent::Exited(code) => {
17119 process.pending_http_requests.remove(&request_key);
17120 return Err(SidecarError::Execution(format!(
17121 "HTTP loopback server exited before responding (exit code {code})"
17122 )));
17123 }
17124 ActiveExecutionEvent::Stdout(_)
17125 | ActiveExecutionEvent::Stderr(_)
17126 | ActiveExecutionEvent::PythonVfsRpcRequest(_)
17127 | ActiveExecutionEvent::SignalState { .. } => {}
17128 }
17129 }
17130}
17131
17132pub(crate) fn dispatch_loopback_http_request<B>(
17133 request: LoopbackHttpDispatchRequest<'_, B>,
17134) -> Result<String, SidecarError>
17135where
17136 B: NativeSidecarBridge + Send + 'static,
17137 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17138{
17139 let LoopbackHttpDispatchRequest {
17140 bridge,
17141 vm_id,
17142 dns,
17143 socket_paths,
17144 kernel,
17145 process,
17146 resource_limits,
17147 server_id,
17148 request_json,
17149 } = request;
17150 let request_id = {
17151 let server = process.http_servers.get_mut(&server_id).ok_or_else(|| {
17152 SidecarError::InvalidState(format!("HTTP target server disappeared: {server_id}"))
17153 })?;
17154 server.next_request_id += 1;
17155 server.next_request_id
17156 };
17157 process
17158 .pending_http_requests
17159 .insert((server_id, request_id), None);
17160 process.execution.send_javascript_stream_event(
17161 "http_request",
17162 json!({
17163 "serverId": server_id,
17164 "requestId": request_id,
17165 "request": request_json,
17166 }),
17167 )?;
17168 wait_for_loopback_http_response(LoopbackHttpResponseWaitRequest {
17169 bridge,
17170 vm_id,
17171 dns,
17172 socket_paths,
17173 kernel,
17174 process,
17175 resource_limits,
17176 request_key: (server_id, request_id),
17177 })
17178}
17179
17180fn ensure_vm_fetch_response_within_limit(
17181 response_json: &str,
17182 operation: &str,
17183 limit: usize,
17184) -> Result<(), SidecarError> {
17185 let size = response_json.len();
17186 if size > limit {
17187 return Err(SidecarError::Execution(format!(
17188 "{operation} payload is {size} bytes, limit is {limit}"
17189 )));
17190 }
17191 Ok(())
17192}
17193
17194fn ensure_vm_fetch_raw_response_buffer_within_limit(
17195 size: usize,
17196 operation: &str,
17197) -> Result<(), SidecarError> {
17198 if size > VM_FETCH_BUFFER_LIMIT_BYTES {
17199 return Err(SidecarError::Execution(format!(
17200 "{operation} raw response buffer is {size} bytes, limit is {VM_FETCH_BUFFER_LIMIT_BYTES}"
17201 )));
17202 }
17203 Ok(())
17204}
17205
17206pub(crate) fn ensure_vm_fetch_response_frame_within_limit(
17207 response: &ResponseFrame,
17208 max_frame_bytes: usize,
17209) -> Result<(), SidecarError> {
17210 let max_frame_bytes = max_frame_bytes.min(VM_FETCH_BUFFER_LIMIT_BYTES);
17211 let frame = crate::protocol::to_generated_protocol_frame(
17212 &crate::protocol::ProtocolFrame::Response(response.clone()),
17213 )
17214 .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))?;
17215 let WireProtocolFrame::ResponseFrame(_) = &frame else {
17216 return Err(SidecarError::FrameTooLarge(String::from(
17217 "vm fetch response converted to non-response wire frame",
17218 )));
17219 };
17220 WireFrameCodec::new(max_frame_bytes)
17221 .encode(&frame)
17222 .map(|_| ())
17223 .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))
17224}
17225
17226fn service_javascript_dns_sync_rpc<B>(
17227 bridge: &SharedBridge<B>,
17228 kernel: &SidecarKernel,
17229 vm_id: &str,
17230 dns: &VmDnsConfig,
17231 request: &JavascriptSyncRpcRequest,
17232) -> Result<Value, SidecarError>
17233where
17234 B: NativeSidecarBridge + Send + 'static,
17235 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17236{
17237 match request.method.as_str() {
17238 "dns.lookup" => {
17239 let payload = request
17240 .args
17241 .first()
17242 .cloned()
17243 .ok_or_else(|| {
17244 SidecarError::InvalidState(String::from(
17245 "dns.lookup requires a request payload",
17246 ))
17247 })
17248 .and_then(|value| {
17249 serde_json::from_value::<JavascriptDnsLookupRequest>(value).map_err(|error| {
17250 SidecarError::InvalidState(format!("invalid dns.lookup payload: {error}"))
17251 })
17252 })?;
17253 let addresses = filter_dns_ip_addrs(
17254 resolve_dns_ip_addrs(
17255 bridge,
17256 kernel,
17257 vm_id,
17258 dns,
17259 &payload.hostname,
17260 DnsLookupPolicy::CheckPermissions,
17261 )?,
17262 payload.family,
17263 )?;
17264 let addresses = filter_dns_safe_ip_addrs(addresses, &payload.hostname)?;
17265 Ok(Value::Array(
17266 addresses
17267 .into_iter()
17268 .map(|ip| {
17269 json!({
17270 "address": ip.to_string(),
17271 "family": if ip.is_ipv6() { 6 } else { 4 },
17272 })
17273 })
17274 .collect(),
17275 ))
17276 }
17277 "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
17278 let payload = request
17279 .args
17280 .first()
17281 .cloned()
17282 .ok_or_else(|| {
17283 SidecarError::InvalidState(String::from(
17284 "dns.resolve requires a request payload",
17285 ))
17286 })
17287 .and_then(|value| {
17288 serde_json::from_value::<JavascriptDnsResolveRequest>(value).map_err(|error| {
17289 SidecarError::InvalidState(format!("invalid dns.resolve payload: {error}"))
17290 })
17291 })?;
17292 let requested_type = match request.method.as_str() {
17293 "dns.resolve4" => String::from("A"),
17294 "dns.resolve6" => String::from("AAAA"),
17295 _ => payload
17296 .rrtype
17297 .as_deref()
17298 .unwrap_or("A")
17299 .to_ascii_uppercase(),
17300 };
17301 let record_type = parse_dns_record_type(&requested_type)?;
17302 let resolution = resolve_dns_records(
17303 bridge,
17304 kernel,
17305 vm_id,
17306 dns,
17307 &payload.hostname,
17308 record_type,
17309 DnsLookupPolicy::CheckPermissions,
17310 )?;
17311 dns_resolution_to_node_value(&resolution, &requested_type)
17312 }
17313 other => Err(SidecarError::InvalidState(format!(
17314 "unsupported JavaScript dns sync RPC method {other}"
17315 ))),
17316 }
17317}
17318
17319fn service_javascript_dgram_sync_rpc<B>(
17320 request: JavascriptDgramSyncRpcServiceRequest<'_, B>,
17321) -> Result<Value, SidecarError>
17322where
17323 B: NativeSidecarBridge + Send + 'static,
17324 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17325{
17326 let JavascriptDgramSyncRpcServiceRequest {
17327 bridge,
17328 kernel,
17329 vm_id,
17330 dns,
17331 socket_paths,
17332 process,
17333 sync_request: request,
17334 resource_limits,
17335 network_counts,
17336 } = request;
17337 match request.method.as_str() {
17338 "dgram.createSocket" => {
17339 check_network_resource_limit(
17340 resource_limits.max_sockets,
17341 network_counts.sockets,
17342 1,
17343 "socket",
17344 )?;
17345 let payload = request
17346 .args
17347 .first()
17348 .cloned()
17349 .ok_or_else(|| {
17350 SidecarError::InvalidState(String::from(
17351 "dgram.createSocket requires a request payload",
17352 ))
17353 })
17354 .and_then(|value| {
17355 serde_json::from_value::<JavascriptDgramCreateSocketRequest>(value).map_err(
17356 |error| {
17357 SidecarError::InvalidState(format!(
17358 "invalid dgram.createSocket payload: {error}"
17359 ))
17360 },
17361 )
17362 })?;
17363 let family = JavascriptUdpFamily::from_socket_type(&payload.socket_type)?;
17364 let socket_id = process.allocate_udp_socket_id();
17365 process.udp_sockets.insert(
17366 socket_id.clone(),
17367 ActiveUdpSocket::new(kernel, process.kernel_pid, family)?,
17368 );
17369 Ok(json!({
17370 "socketId": socket_id,
17371 "type": family.socket_type(),
17372 }))
17373 }
17374 "dgram.bind" => {
17375 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.bind socket id")?;
17376 let payload = request
17377 .args
17378 .get(1)
17379 .cloned()
17380 .ok_or_else(|| {
17381 SidecarError::InvalidState(String::from(
17382 "dgram.bind requires a request payload",
17383 ))
17384 })
17385 .and_then(|value| {
17386 serde_json::from_value::<JavascriptDgramBindRequest>(value).map_err(|error| {
17387 SidecarError::InvalidState(format!("invalid dgram.bind payload: {error}"))
17388 })
17389 })?;
17390 let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17391 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17392 })?;
17393 let local_addr = socket.bind(
17394 kernel,
17395 process.kernel_pid,
17396 payload.address.as_deref(),
17397 payload.port,
17398 socket_paths,
17399 )?;
17400 Ok(json!({
17401 "localAddress": local_addr.ip().to_string(),
17402 "localPort": local_addr.port(),
17403 "family": socket_addr_family(&local_addr),
17404 }))
17405 }
17406 "dgram.send" => {
17407 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.send socket id")?;
17408 let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "dgram.send payload")?;
17409 let payload = request
17410 .args
17411 .get(2)
17412 .cloned()
17413 .ok_or_else(|| {
17414 SidecarError::InvalidState(String::from(
17415 "dgram.send requires a request payload",
17416 ))
17417 })
17418 .and_then(|value| {
17419 serde_json::from_value::<JavascriptDgramSendRequest>(value).map_err(|error| {
17420 SidecarError::InvalidState(format!("invalid dgram.send payload: {error}"))
17421 })
17422 })?;
17423 let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17424 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17425 })?;
17426 let (written, local_addr) = socket.send_to(ActiveUdpSendToRequest {
17427 bridge,
17428 kernel,
17429 kernel_pid: process.kernel_pid,
17430 vm_id,
17431 dns,
17432 host: payload.address.as_deref().unwrap_or("localhost"),
17433 port: payload.port,
17434 context: socket_paths,
17435 contents: &chunk,
17436 })?;
17437 Ok(json!({
17438 "bytes": written,
17439 "localAddress": local_addr.ip().to_string(),
17440 "localPort": local_addr.port(),
17441 "family": socket_addr_family(&local_addr),
17442 }))
17443 }
17444 "dgram.poll" => {
17445 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.poll socket id")?;
17446 let wait_ms =
17447 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "dgram.poll wait ms")?
17448 .unwrap_or_default();
17449 let event = {
17450 let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17451 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17452 })?;
17453 socket.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?
17454 };
17455
17456 match event {
17457 Some(JavascriptUdpSocketEvent::Message { data, remote_addr }) => {
17458 let family = JavascriptSocketFamily::from_ip(remote_addr.ip());
17459 let guest_remote_port = if is_loopback_ip(remote_addr.ip()) {
17460 socket_paths
17461 .guest_udp_port_for_host_port(family, remote_addr.port())
17462 .unwrap_or(remote_addr.port())
17463 } else {
17464 remote_addr.port()
17465 };
17466 Ok(json!({
17467 "type": "message",
17468 "data": javascript_sync_rpc_bytes_value(&data),
17469 "remoteAddress": remote_addr.ip().to_string(),
17470 "remotePort": guest_remote_port,
17471 "remoteFamily": socket_addr_family(&remote_addr),
17472 }))
17473 }
17474 Some(JavascriptUdpSocketEvent::Error { code, message }) => Ok(json!({
17475 "type": "error",
17476 "code": code,
17477 "message": message,
17478 })),
17479 None => Ok(Value::Null),
17480 }
17481 }
17482 "dgram.close" => {
17483 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.close socket id")?;
17484 let mut socket = process.udp_sockets.remove(socket_id).ok_or_else(|| {
17485 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17486 })?;
17487 socket.close(kernel, process.kernel_pid);
17488 Ok(Value::Null)
17489 }
17490 "dgram.address" => {
17491 let socket_id =
17492 javascript_sync_rpc_arg_str(&request.args, 0, "dgram.address socket id")?;
17493 let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17494 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17495 })?;
17496 let local_addr = socket.local_addr().ok_or_else(|| {
17497 SidecarError::Execution(String::from("EBADF: bad file descriptor"))
17498 })?;
17499 javascript_net_json_string(
17500 json!({
17501 "address": local_addr.ip().to_string(),
17502 "port": local_addr.port(),
17503 "family": socket_addr_family(&local_addr),
17504 }),
17505 "dgram.address",
17506 )
17507 }
17508 "dgram.setBufferSize" => {
17509 let socket_id =
17510 javascript_sync_rpc_arg_str(&request.args, 0, "dgram.setBufferSize socket id")?;
17511 let which =
17512 javascript_sync_rpc_arg_str(&request.args, 1, "dgram.setBufferSize buffer kind")?;
17513 let size = javascript_sync_rpc_arg_u64(&request.args, 2, "dgram.setBufferSize size")?;
17514 let size = usize::try_from(size).map_err(|_| {
17515 SidecarError::InvalidState(String::from(
17516 "dgram.setBufferSize size must fit within usize",
17517 ))
17518 })?;
17519 let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17520 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17521 })?;
17522 socket.set_buffer_size(which, size)?;
17523 Ok(Value::Null)
17524 }
17525 "dgram.getBufferSize" => {
17526 let socket_id =
17527 javascript_sync_rpc_arg_str(&request.args, 0, "dgram.getBufferSize socket id")?;
17528 let which =
17529 javascript_sync_rpc_arg_str(&request.args, 1, "dgram.getBufferSize buffer kind")?;
17530 let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17531 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17532 })?;
17533 let size = socket.get_buffer_size(which)?;
17534 Ok(json!(size))
17535 }
17536 other => Err(SidecarError::InvalidState(format!(
17537 "unsupported JavaScript dgram sync RPC method {other}"
17538 ))),
17539 }
17540}
17541
17542#[derive(Debug)]
17543struct ClientHttp2StreamState {
17544 send_stream: Option<h2::SendStream<Bytes>>,
17545}
17546
17547#[derive(Debug)]
17548struct ServerHttp2StreamState {
17549 send_response: Option<ServerHttp2Responder>,
17550 send_stream: Option<h2::SendStream<Bytes>>,
17551}
17552
17553#[derive(Debug)]
17554enum ServerHttp2Responder {
17555 Regular(server::SendResponse<Bytes>),
17556 Pushed(server::SendPushedResponse<Bytes>),
17557}
17558
17559const HTTP2_DEFAULT_WINDOW_SIZE: u32 = 65_535;
17560const HTTP2_POLL_DELAY: Duration = Duration::from_millis(10);
17561
17562fn http2_runtime_snapshot() -> Http2RuntimeSnapshot {
17563 Http2RuntimeSnapshot {
17564 effective_local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17565 local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17566 remote_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17567 next_stream_id: 1,
17568 outbound_queue_size: 1,
17569 deflate_dynamic_table_size: 0,
17570 inflate_dynamic_table_size: 0,
17571 }
17572}
17573
17574fn http2_snapshot_json(snapshot: &Http2SessionSnapshot) -> Result<String, SidecarError> {
17575 serde_json::to_string(snapshot)
17576 .map_err(|error| SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}")))
17577}
17578
17579fn http2_event_value(event: &Http2BridgeEvent) -> Result<Value, SidecarError> {
17580 serde_json::to_string(event)
17581 .map(Value::String)
17582 .map_err(|error| SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}")))
17583}
17584
17585fn push_http2_server_event(
17586 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17587 server_id: u64,
17588 event: Http2BridgeEvent,
17589) {
17590 if let Ok(mut state) = shared.lock() {
17591 state
17592 .server_events
17593 .entry(server_id)
17594 .or_default()
17595 .push_back(event);
17596 }
17597}
17598
17599fn push_http2_session_event(
17600 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17601 session_id: u64,
17602 event: Http2BridgeEvent,
17603) {
17604 if let Ok(mut state) = shared.lock() {
17605 state
17606 .session_events
17607 .entry(session_id)
17608 .or_default()
17609 .push_back(event);
17610 }
17611}
17612
17613fn pop_http2_event(
17614 queue: &mut BTreeMap<u64, VecDeque<Http2BridgeEvent>>,
17615 id: u64,
17616) -> Option<Http2BridgeEvent> {
17617 queue.get_mut(&id).and_then(VecDeque::pop_front)
17618}
17619
17620fn wait_for_http2_event(
17621 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17622 id: u64,
17623 is_server: bool,
17624 wait_ms: u64,
17625) -> Option<Http2BridgeEvent> {
17626 let deadline = Instant::now() + Duration::from_millis(wait_ms);
17627 loop {
17628 if let Ok(mut state) = shared.lock() {
17629 let queue = if is_server {
17630 &mut state.server_events
17631 } else {
17632 &mut state.session_events
17633 };
17634 if let Some(event) = pop_http2_event(queue, id) {
17635 return Some(event);
17636 }
17637 }
17638 if wait_ms == 0 || Instant::now() >= deadline {
17639 return None;
17640 }
17641 thread::sleep(HTTP2_POLL_DELAY);
17642 }
17643}
17644
17645fn next_http2_session_id(shared: &mut crate::state::Http2SharedState) -> u64 {
17646 shared.next_session_id += 1;
17647 shared.next_session_id
17648}
17649
17650fn next_http2_stream_id(shared: &mut crate::state::Http2SharedState) -> u64 {
17651 shared.next_stream_id += 1;
17652 shared.next_stream_id
17653}
17654
17655fn http2_reason(code: Option<u32>) -> Reason {
17656 code.unwrap_or(Reason::NO_ERROR.into()).into()
17657}
17658
17659fn http2_error_payload(message: impl Into<String>) -> String {
17660 serde_json::to_string(&json!({
17661 "name": "Error",
17662 "code": "ERR_HTTP2_ERROR",
17663 "message": message.into(),
17664 }))
17665 .unwrap_or_else(|_| {
17666 String::from(
17667 "{\"name\":\"Error\",\"code\":\"ERR_HTTP2_ERROR\",\"message\":\"HTTP/2 bridge error\"}",
17668 )
17669 })
17670}
17671
17672fn http2_socket_snapshot(local_addr: SocketAddr, remote_addr: SocketAddr) -> Http2SocketSnapshot {
17673 Http2SocketSnapshot {
17674 encrypted: false,
17675 allow_half_open: false,
17676 local_address: Some(local_addr.ip().to_string()),
17677 local_port: Some(local_addr.port()),
17678 local_family: Some(socket_addr_family(&local_addr).to_string()),
17679 remote_address: Some(remote_addr.ip().to_string()),
17680 remote_port: Some(remote_addr.port()),
17681 remote_family: Some(socket_addr_family(&remote_addr).to_string()),
17682 servername: None,
17683 alpn_protocol: Some(String::from("h2c")),
17684 }
17685}
17686
17687fn http2_wait_result(kind: &str, id: u64) -> Value {
17688 json!({
17689 "kind": kind,
17690 "id": id,
17691 })
17692}
17693
17694fn is_http2_terminal_event(event: &Http2BridgeEvent, is_server: bool, id: u64) -> bool {
17695 if is_server {
17696 event.kind == "serverClose" && event.id == id
17697 } else {
17698 event.kind == "sessionClose" && event.id == id
17699 }
17700}
17701
17702fn dispatch_http2_wait_loop(
17703 process: &ActiveProcess,
17704 id: u64,
17705 is_server: bool,
17706) -> Result<Value, SidecarError> {
17707 loop {
17708 if let Some(event) = wait_for_http2_event(&process.http2.shared, id, is_server, 50) {
17709 let payload = serde_json::to_value(&event).map_err(|error| {
17710 SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}"))
17711 })?;
17712 process
17713 .execution
17714 .send_javascript_stream_event("http2", payload.clone())?;
17715 if is_http2_terminal_event(&event, is_server, id) {
17716 return Ok(payload);
17717 }
17718 continue;
17719 }
17720
17721 let exists = process
17722 .http2
17723 .shared
17724 .lock()
17725 .map(|state| {
17726 if is_server {
17727 state.servers.contains_key(&id)
17728 } else {
17729 state.sessions.contains_key(&id)
17730 }
17731 })
17732 .unwrap_or(false);
17733 if !exists {
17734 return Ok(if is_server {
17735 http2_wait_result("serverClose", id)
17736 } else {
17737 http2_wait_result("sessionClose", id)
17738 });
17739 }
17740 }
17741}
17742
17743fn dispatch_http_wait_loop(process: &ActiveProcess, server_id: u64) -> Result<Value, SidecarError> {
17744 loop {
17745 if !process.http_servers.contains_key(&server_id) {
17746 return Ok(json!({
17747 "kind": "serverClose",
17748 "id": server_id,
17749 }));
17750 }
17751 thread::sleep(Duration::from_millis(25));
17752 }
17753}
17754
17755fn http2_settings_from_value(settings: &BTreeMap<String, Value>) -> BTreeMap<String, Value> {
17756 settings.clone()
17757}
17758
17759fn parse_http2_headers_json(
17760 headers_json: &str,
17761 label: &str,
17762) -> Result<BTreeMap<String, Value>, SidecarError> {
17763 serde_json::from_str::<BTreeMap<String, Value>>(headers_json)
17764 .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
17765}
17766
17767fn apply_http2_header_values(
17768 header_map: &mut HeaderMap,
17769 name: &str,
17770 value: &Value,
17771) -> Result<(), SidecarError> {
17772 let header_name = HeaderName::from_bytes(name.as_bytes()).map_err(|error| {
17773 SidecarError::InvalidState(format!("invalid HTTP/2 header name {name:?}: {error}"))
17774 })?;
17775 match value {
17776 Value::Array(values) => {
17777 for value in values {
17778 apply_http2_header_values(header_map, name, value)?;
17779 }
17780 }
17781 Value::String(text) => {
17782 let value = HeaderValue::from_str(text).map_err(|error| {
17783 SidecarError::InvalidState(format!(
17784 "invalid HTTP/2 header value for {name}: {error}"
17785 ))
17786 })?;
17787 header_map.append(header_name.clone(), value);
17788 }
17789 Value::Number(number) => {
17790 let value = HeaderValue::from_str(&number.to_string()).map_err(|error| {
17791 SidecarError::InvalidState(format!(
17792 "invalid HTTP/2 numeric header value for {name}: {error}"
17793 ))
17794 })?;
17795 header_map.append(header_name.clone(), value);
17796 }
17797 Value::Bool(boolean) => {
17798 let value = HeaderValue::from_str(if *boolean { "true" } else { "false" }).map_err(
17799 |error| {
17800 SidecarError::InvalidState(format!(
17801 "invalid HTTP/2 boolean header value for {name}: {error}"
17802 ))
17803 },
17804 )?;
17805 header_map.append(header_name.clone(), value);
17806 }
17807 Value::Null => {}
17808 Value::Object(_) => {
17809 return Err(SidecarError::InvalidState(format!(
17810 "unsupported HTTP/2 header object value for {name}"
17811 )));
17812 }
17813 }
17814 Ok(())
17815}
17816
17817fn build_http2_request(headers_json: &str) -> Result<Request<()>, SidecarError> {
17818 let headers = parse_http2_headers_json(headers_json, "HTTP/2 request headers")?;
17819 let method = headers
17820 .get(":method")
17821 .and_then(Value::as_str)
17822 .unwrap_or("GET");
17823 let path = headers.get(":path").and_then(Value::as_str).unwrap_or("/");
17824 let mut builder = Request::builder()
17825 .method(Method::from_bytes(method.as_bytes()).map_err(|error| {
17826 SidecarError::InvalidState(format!("invalid HTTP/2 method {method:?}: {error}"))
17827 })?)
17828 .uri(path.parse::<Uri>().map_err(|error| {
17829 SidecarError::InvalidState(format!("invalid HTTP/2 path {path:?}: {error}"))
17830 })?);
17831 {
17832 let header_map = builder.headers_mut().expect("request header map");
17833 for (name, value) in &headers {
17834 if name.starts_with(':') {
17835 continue;
17836 }
17837 apply_http2_header_values(header_map, name, value)?;
17838 }
17839 }
17840 builder
17841 .body(())
17842 .map_err(|error| SidecarError::InvalidState(format!("invalid HTTP/2 request: {error}")))
17843}
17844
17845fn build_http2_response(headers_json: &str) -> Result<Response<()>, SidecarError> {
17846 let headers = parse_http2_headers_json(headers_json, "HTTP/2 response headers")?;
17847 let status = headers
17848 .get(":status")
17849 .and_then(Value::as_u64)
17850 .or_else(|| {
17851 headers
17852 .get(":status")
17853 .and_then(Value::as_str)
17854 .and_then(|value| value.parse::<u16>().ok().map(u64::from))
17855 })
17856 .unwrap_or(200);
17857 let mut builder = Response::builder().status(status as u16);
17858 {
17859 let header_map = builder.headers_mut().expect("response header map");
17860 for (name, value) in &headers {
17861 if name.starts_with(':') {
17862 continue;
17863 }
17864 apply_http2_header_values(header_map, name, value)?;
17865 }
17866 }
17867 builder.body(()).map_err(|error| {
17868 SidecarError::InvalidState(format!("invalid HTTP/2 response headers: {error}"))
17869 })
17870}
17871
17872fn serialize_http2_headers_map(
17873 pseudo: BTreeMap<String, Value>,
17874 headers: &HeaderMap,
17875) -> Result<String, SidecarError> {
17876 let mut serialized = pseudo;
17877 for (name, value) in headers {
17878 let name = name.as_str().to_string();
17879 let value = Value::String(
17880 value
17881 .to_str()
17882 .map_err(|error| {
17883 SidecarError::Execution(format!("invalid HTTP/2 header value: {error}"))
17884 })?
17885 .to_owned(),
17886 );
17887 match serialized.get_mut(&name) {
17888 Some(Value::Array(values)) => values.push(value),
17889 Some(existing) => {
17890 let first = existing.clone();
17891 *existing = Value::Array(vec![first, value]);
17892 }
17893 None => {
17894 serialized.insert(name, value);
17895 }
17896 }
17897 }
17898 serde_json::to_string(&serialized)
17899 .map_err(|error| SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}")))
17900}
17901
17902fn serialize_http2_request_headers(
17903 request: &Request<h2::RecvStream>,
17904) -> Result<String, SidecarError> {
17905 let mut pseudo = BTreeMap::new();
17906 pseudo.insert(
17907 String::from(":method"),
17908 Value::String(request.method().as_str().to_string()),
17909 );
17910 pseudo.insert(
17911 String::from(":path"),
17912 Value::String(
17913 request
17914 .uri()
17915 .path_and_query()
17916 .map(|value| value.as_str().to_string())
17917 .unwrap_or_else(|| String::from("/")),
17918 ),
17919 );
17920 serialize_http2_headers_map(pseudo, request.headers())
17921}
17922
17923fn serialize_http2_response_headers(
17924 response: &Response<h2::RecvStream>,
17925) -> Result<String, SidecarError> {
17926 let mut pseudo = BTreeMap::new();
17927 pseudo.insert(
17928 String::from(":status"),
17929 Value::Number(serde_json::Number::from(response.status().as_u16())),
17930 );
17931 serialize_http2_headers_map(pseudo, response.headers())
17932}
17933
17934fn remove_http2_session_resources(
17935 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17936 session_id: u64,
17937) {
17938 if let Ok(mut state) = shared.lock() {
17939 state.sessions.remove(&session_id);
17940 state.session_events.remove(&session_id);
17941 let stream_ids = state
17942 .streams
17943 .iter()
17944 .filter_map(|(stream_id, stream)| {
17945 (stream.session_id == session_id).then_some(*stream_id)
17946 })
17947 .collect::<Vec<_>>();
17948 for stream_id in stream_ids {
17949 state.streams.remove(&stream_id);
17950 }
17951 }
17952}
17953
17954fn spawn_http2_client_session(
17955 shared: Arc<Mutex<crate::state::Http2SharedState>>,
17956 session_id: u64,
17957 remote_addr: SocketAddr,
17958 tls: Option<JavascriptTlsBridgeOptions>,
17959 snapshot: Arc<Mutex<Http2SessionSnapshot>>,
17960 mut command_rx: UnboundedReceiver<Http2SessionCommand>,
17961) {
17962 thread::spawn(move || {
17963 let runtime = match TokioRuntimeBuilder::new_current_thread()
17964 .enable_all()
17965 .build()
17966 {
17967 Ok(runtime) => runtime,
17968 Err(error) => {
17969 push_http2_session_event(
17970 &shared,
17971 session_id,
17972 Http2BridgeEvent {
17973 kind: String::from("sessionError"),
17974 id: session_id,
17975 data: Some(http2_error_payload(error.to_string())),
17976 ..Http2BridgeEvent::default()
17977 },
17978 );
17979 remove_http2_session_resources(&shared, session_id);
17980 return;
17981 }
17982 };
17983
17984 runtime.block_on(async move {
17985 let stream = match tokio::net::TcpStream::connect(remote_addr).await {
17986 Ok(stream) => stream,
17987 Err(error) => {
17988 push_http2_session_event(
17989 &shared,
17990 session_id,
17991 Http2BridgeEvent {
17992 kind: String::from("sessionError"),
17993 id: session_id,
17994 data: Some(http2_error_payload(error.to_string())),
17995 ..Http2BridgeEvent::default()
17996 },
17997 );
17998 remove_http2_session_resources(&shared, session_id);
17999 return;
18000 }
18001 };
18002
18003 let local_addr = match stream.local_addr() {
18004 Ok(addr) => addr,
18005 Err(error) => {
18006 push_http2_session_event(
18007 &shared,
18008 session_id,
18009 Http2BridgeEvent {
18010 kind: String::from("sessionError"),
18011 id: session_id,
18012 data: Some(http2_error_payload(error.to_string())),
18013 ..Http2BridgeEvent::default()
18014 },
18015 );
18016 remove_http2_session_resources(&shared, session_id);
18017 return;
18018 }
18019 };
18020
18021 {
18022 let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
18023 snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
18024 if let Some(options) = tls.as_ref() {
18025 snapshot_guard.encrypted = true;
18026 snapshot_guard.alpn_protocol = Some(String::from("h2"));
18027 snapshot_guard.socket.encrypted = true;
18028 snapshot_guard.socket.servername = options.servername.clone();
18029 snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
18030 }
18031 snapshot_guard.state = http2_runtime_snapshot();
18032 }
18033 if let Ok(snapshot_json) =
18034 http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
18035 {
18036 push_http2_session_event(
18037 &shared,
18038 session_id,
18039 Http2BridgeEvent {
18040 kind: String::from("sessionConnect"),
18041 id: session_id,
18042 data: Some(snapshot_json),
18043 ..Http2BridgeEvent::default()
18044 },
18045 );
18046 }
18047
18048 let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
18049 let server_name = match ServerName::try_from(
18050 options
18051 .servername
18052 .clone()
18053 .unwrap_or_else(|| String::from("localhost")),
18054 ) {
18055 Ok(server_name) => server_name,
18056 Err(_) => {
18057 push_http2_session_event(
18058 &shared,
18059 session_id,
18060 Http2BridgeEvent {
18061 kind: String::from("sessionError"),
18062 id: session_id,
18063 data: Some(http2_error_payload("invalid TLS servername")),
18064 ..Http2BridgeEvent::default()
18065 },
18066 );
18067 remove_http2_session_resources(&shared, session_id);
18068 return;
18069 }
18070 };
18071 let connector = match build_client_tls_config(options) {
18072 Ok(config) => TlsConnector::from(Arc::new(config)),
18073 Err(error) => {
18074 push_http2_session_event(
18075 &shared,
18076 session_id,
18077 Http2BridgeEvent {
18078 kind: String::from("sessionError"),
18079 id: session_id,
18080 data: Some(http2_error_payload(error.to_string())),
18081 ..Http2BridgeEvent::default()
18082 },
18083 );
18084 remove_http2_session_resources(&shared, session_id);
18085 return;
18086 }
18087 };
18088 match connector.connect(server_name, stream).await {
18089 Ok(tls_stream) => Box::pin(tls_stream),
18090 Err(error) => {
18091 push_http2_session_event(
18092 &shared,
18093 session_id,
18094 Http2BridgeEvent {
18095 kind: String::from("sessionError"),
18096 id: session_id,
18097 data: Some(http2_error_payload(error.to_string())),
18098 ..Http2BridgeEvent::default()
18099 },
18100 );
18101 remove_http2_session_resources(&shared, session_id);
18102 return;
18103 }
18104 }
18105 } else {
18106 Box::pin(stream)
18107 };
18108
18109 let (mut sender, connection) = match client::handshake(io).await {
18110 Ok(parts) => parts,
18111 Err(error) => {
18112 push_http2_session_event(
18113 &shared,
18114 session_id,
18115 Http2BridgeEvent {
18116 kind: String::from("sessionError"),
18117 id: session_id,
18118 data: Some(http2_error_payload(error.to_string())),
18119 ..Http2BridgeEvent::default()
18120 },
18121 );
18122 remove_http2_session_resources(&shared, session_id);
18123 return;
18124 }
18125 };
18126
18127 let (status_tx, mut status_rx) = unbounded_channel::<Result<(), String>>();
18128 tokio::spawn(async move {
18129 let _ = status_tx.send(connection.await.map_err(|error| error.to_string()));
18130 });
18131
18132 let streams: Arc<Mutex<BTreeMap<u64, ClientHttp2StreamState>>> =
18133 Arc::new(Mutex::new(BTreeMap::new()));
18134
18135 loop {
18136 tokio::select! {
18137 Some(result) = status_rx.recv() => {
18138 if let Err(message) = result {
18139 push_http2_session_event(
18140 &shared,
18141 session_id,
18142 Http2BridgeEvent {
18143 kind: String::from("sessionError"),
18144 id: session_id,
18145 data: Some(http2_error_payload(message)),
18146 ..Http2BridgeEvent::default()
18147 },
18148 );
18149 }
18150 push_http2_session_event(
18151 &shared,
18152 session_id,
18153 Http2BridgeEvent {
18154 kind: String::from("sessionClose"),
18155 id: session_id,
18156 ..Http2BridgeEvent::default()
18157 },
18158 );
18159 remove_http2_session_resources(&shared, session_id);
18160 break;
18161 }
18162 Some(command) = command_rx.recv() => {
18163 match command {
18164 Http2SessionCommand::Request { headers_json, options_json, respond_to } => {
18165 let request = match build_http2_request(&headers_json) {
18166 Ok(request) => request,
18167 Err(error) => {
18168 let _ = respond_to.send(Err(error.to_string()));
18169 continue;
18170 }
18171 };
18172 let options: JavascriptHttp2RequestOptions =
18173 serde_json::from_str(&options_json).unwrap_or_default();
18174 let stream_id = {
18175 let mut state = shared.lock().expect("http2 shared state");
18176 let stream_id = next_http2_stream_id(&mut state);
18177 state.streams.insert(
18178 stream_id,
18179 ActiveHttp2Stream {
18180 session_id,
18181 paused: Arc::new(AtomicBool::new(false)),
18182 },
18183 );
18184 stream_id
18185 };
18186 match sender.send_request(request, options.end_stream) {
18187 Ok((response_future, send_stream)) => {
18188 if !options.end_stream {
18189 streams
18190 .lock()
18191 .expect("http2 client streams")
18192 .insert(stream_id, ClientHttp2StreamState { send_stream: Some(send_stream) });
18193 }
18194 let shared_clone = Arc::clone(&shared);
18195 let snapshot_clone = Arc::clone(&snapshot);
18196 tokio::spawn(async move {
18197 match response_future.await {
18198 Ok(response) => {
18199 if let Ok(headers_json) = serialize_http2_response_headers(&response) {
18200 push_http2_session_event(
18201 &shared_clone,
18202 session_id,
18203 Http2BridgeEvent {
18204 kind: String::from("clientResponseHeaders"),
18205 id: stream_id,
18206 data: Some(headers_json),
18207 ..Http2BridgeEvent::default()
18208 },
18209 );
18210 }
18211 let mut body = response.into_body();
18212 while let Some(chunk) = body.data().await {
18213 match chunk {
18214 Ok(bytes) => {
18215 let paused = {
18216 let state = shared_clone.lock().expect("http2 shared state");
18217 state.streams.get(&stream_id).map(|stream| Arc::clone(&stream.paused))
18218 };
18219 if let Some(paused) = paused {
18220 while paused.load(Ordering::SeqCst) {
18221 tokio::time::sleep(HTTP2_POLL_DELAY).await;
18222 }
18223 }
18224 let _ = body.flow_control().release_capacity(bytes.len());
18225 push_http2_session_event(
18226 &shared_clone,
18227 session_id,
18228 Http2BridgeEvent {
18229 kind: String::from("clientData"),
18230 id: stream_id,
18231 data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
18232 ..Http2BridgeEvent::default()
18233 },
18234 );
18235 }
18236 Err(error) => {
18237 push_http2_session_event(
18238 &shared_clone,
18239 session_id,
18240 Http2BridgeEvent {
18241 kind: String::from("clientError"),
18242 id: stream_id,
18243 data: Some(http2_error_payload(error.to_string())),
18244 ..Http2BridgeEvent::default()
18245 },
18246 );
18247 break;
18248 }
18249 }
18250 }
18251 {
18252 let mut snapshot = snapshot_clone.lock().expect("http2 snapshot lock");
18253 snapshot.state.next_stream_id =
18254 snapshot.state.next_stream_id.saturating_add(2);
18255 }
18256 push_http2_session_event(
18257 &shared_clone,
18258 session_id,
18259 Http2BridgeEvent {
18260 kind: String::from("clientEnd"),
18261 id: stream_id,
18262 ..Http2BridgeEvent::default()
18263 },
18264 );
18265 push_http2_session_event(
18266 &shared_clone,
18267 session_id,
18268 Http2BridgeEvent {
18269 kind: String::from("clientClose"),
18270 id: stream_id,
18271 extra_number: Some(0),
18272 ..Http2BridgeEvent::default()
18273 },
18274 );
18275 if let Ok(mut state) = shared_clone.lock() {
18276 state.streams.remove(&stream_id);
18277 }
18278 }
18279 Err(error) => {
18280 push_http2_session_event(
18281 &shared_clone,
18282 session_id,
18283 Http2BridgeEvent {
18284 kind: String::from("clientError"),
18285 id: stream_id,
18286 data: Some(http2_error_payload(error.to_string())),
18287 ..Http2BridgeEvent::default()
18288 },
18289 );
18290 push_http2_session_event(
18291 &shared_clone,
18292 session_id,
18293 Http2BridgeEvent {
18294 kind: String::from("clientClose"),
18295 id: stream_id,
18296 extra_number: Some(u32::from(Reason::INTERNAL_ERROR) as u64),
18297 ..Http2BridgeEvent::default()
18298 },
18299 );
18300 if let Ok(mut state) = shared_clone.lock() {
18301 state.streams.remove(&stream_id);
18302 }
18303 }
18304 }
18305 });
18306 let _ = respond_to.send(Ok(json!(stream_id)));
18307 }
18308 Err(error) => {
18309 if let Ok(mut state) = shared.lock() {
18310 state.streams.remove(&stream_id);
18311 }
18312 let _ = respond_to.send(Err(error.to_string()));
18313 }
18314 }
18315 }
18316 Http2SessionCommand::Settings { settings_json, respond_to } => {
18317 let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
18318 .unwrap_or_default();
18319 {
18320 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18321 snapshot.local_settings = http2_settings_from_value(&settings);
18322 }
18323 if let Ok(headers_json) = serde_json::to_string(&settings) {
18324 push_http2_session_event(
18325 &shared,
18326 session_id,
18327 Http2BridgeEvent {
18328 kind: String::from("sessionLocalSettings"),
18329 id: session_id,
18330 data: Some(headers_json.clone()),
18331 ..Http2BridgeEvent::default()
18332 },
18333 );
18334 push_http2_session_event(
18335 &shared,
18336 session_id,
18337 Http2BridgeEvent {
18338 kind: String::from("sessionSettingsAck"),
18339 id: session_id,
18340 ..Http2BridgeEvent::default()
18341 },
18342 );
18343 }
18344 let _ = respond_to.send(Ok(Value::Null));
18345 }
18346 Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
18347 {
18348 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18349 snapshot.state.local_window_size = size;
18350 snapshot.state.effective_local_window_size = size;
18351 }
18352 let value = snapshot
18353 .lock()
18354 .ok()
18355 .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
18356 .map(Value::String)
18357 .unwrap_or(Value::Null);
18358 let _ = respond_to.send(Ok(value));
18359 }
18360 Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
18361 push_http2_session_event(
18362 &shared,
18363 session_id,
18364 Http2BridgeEvent {
18365 kind: String::from("sessionGoaway"),
18366 id: session_id,
18367 data: opaque_data.map(|value| {
18368 base64::engine::general_purpose::STANDARD.encode(value)
18369 }),
18370 extra_number: Some(error_code as u64),
18371 flags: Some(last_stream_id as u64),
18372 ..Http2BridgeEvent::default()
18373 },
18374 );
18375 let _ = respond_to.send(Ok(Value::Null));
18376 }
18377 Http2SessionCommand::Close { respond_to, .. } => {
18378 let _ = respond_to.send(Ok(Value::Null));
18379 push_http2_session_event(
18380 &shared,
18381 session_id,
18382 Http2BridgeEvent {
18383 kind: String::from("sessionClose"),
18384 id: session_id,
18385 ..Http2BridgeEvent::default()
18386 },
18387 );
18388 remove_http2_session_resources(&shared, session_id);
18389 break;
18390 }
18391 Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
18392 let result = streams
18393 .lock()
18394 .expect("http2 client streams")
18395 .get_mut(&stream_id)
18396 .and_then(|stream| stream.send_stream.as_mut())
18397 .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 client stream {stream_id}")))
18398 .and_then(|stream| stream.send_data(Bytes::from(chunk), end_stream).map_err(|error| SidecarError::Execution(error.to_string())));
18399 match result {
18400 Ok(()) => {
18401 if end_stream {
18402 streams.lock().expect("http2 client streams").remove(&stream_id);
18403 }
18404 let _ = respond_to.send(Ok(Value::Bool(true)));
18405 }
18406 Err(error) => {
18407 let _ = respond_to.send(Err(error.to_string()));
18408 }
18409 }
18410 }
18411 Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
18412 let mut streams = streams.lock().expect("http2 client streams");
18413 let Some(mut state) = streams.remove(&stream_id) else {
18414 let _ = respond_to.send(Err(format!("unknown HTTP/2 client stream {stream_id}")));
18415 continue;
18416 };
18417 if let Some(stream) = state.send_stream.as_mut() {
18418 stream.send_reset(http2_reason(error_code));
18419 }
18420 if let Ok(mut state) = shared.lock() {
18421 state.streams.remove(&stream_id);
18422 }
18423 push_http2_session_event(
18424 &shared,
18425 session_id,
18426 Http2BridgeEvent {
18427 kind: String::from("clientClose"),
18428 id: stream_id,
18429 extra_number: Some(u32::from(http2_reason(error_code)) as u64),
18430 ..Http2BridgeEvent::default()
18431 },
18432 );
18433 let _ = respond_to.send(Ok(Value::Null));
18434 }
18435 Http2SessionCommand::StreamRespond { respond_to, .. }
18436 | Http2SessionCommand::StreamPush { respond_to, .. }
18437 | Http2SessionCommand::StreamRespondWithFile { respond_to, .. } => {
18438 let _ = respond_to.send(Err(String::from("HTTP/2 client streams cannot send server responses")));
18439 }
18440 }
18441 }
18442 else => break,
18443 }
18444 }
18445 });
18446 });
18447}
18448
18449fn spawn_http2_server_session(
18450 shared: Arc<Mutex<crate::state::Http2SharedState>>,
18451 server_id: u64,
18452 session_id: u64,
18453 stream: TcpStream,
18454 tls: Option<JavascriptTlsBridgeOptions>,
18455 snapshot: Arc<Mutex<Http2SessionSnapshot>>,
18456 mut command_rx: UnboundedReceiver<Http2SessionCommand>,
18457) {
18458 thread::spawn(move || {
18459 let runtime = match TokioRuntimeBuilder::new_current_thread()
18460 .enable_all()
18461 .build()
18462 {
18463 Ok(runtime) => runtime,
18464 Err(error) => {
18465 push_http2_server_event(
18466 &shared,
18467 server_id,
18468 Http2BridgeEvent {
18469 kind: String::from("serverStreamError"),
18470 id: session_id,
18471 data: Some(http2_error_payload(error.to_string())),
18472 ..Http2BridgeEvent::default()
18473 },
18474 );
18475 remove_http2_session_resources(&shared, session_id);
18476 return;
18477 }
18478 };
18479
18480 runtime.block_on(async move {
18481 if let Err(error) = stream.set_nonblocking(true) {
18482 push_http2_server_event(
18483 &shared,
18484 server_id,
18485 Http2BridgeEvent {
18486 kind: String::from("serverStreamError"),
18487 id: session_id,
18488 data: Some(http2_error_payload(error.to_string())),
18489 ..Http2BridgeEvent::default()
18490 },
18491 );
18492 remove_http2_session_resources(&shared, session_id);
18493 return;
18494 }
18495 let stream = match tokio::net::TcpStream::from_std(stream) {
18496 Ok(stream) => stream,
18497 Err(error) => {
18498 push_http2_server_event(
18499 &shared,
18500 server_id,
18501 Http2BridgeEvent {
18502 kind: String::from("serverStreamError"),
18503 id: session_id,
18504 data: Some(http2_error_payload(error.to_string())),
18505 ..Http2BridgeEvent::default()
18506 },
18507 );
18508 remove_http2_session_resources(&shared, session_id);
18509 return;
18510 }
18511 };
18512 let local_addr = match stream.local_addr() {
18513 Ok(addr) => addr,
18514 Err(error) => {
18515 push_http2_server_event(
18516 &shared,
18517 server_id,
18518 Http2BridgeEvent {
18519 kind: String::from("serverStreamError"),
18520 id: session_id,
18521 data: Some(http2_error_payload(error.to_string())),
18522 ..Http2BridgeEvent::default()
18523 },
18524 );
18525 remove_http2_session_resources(&shared, session_id);
18526 return;
18527 }
18528 };
18529 let remote_addr = match stream.peer_addr() {
18530 Ok(addr) => addr,
18531 Err(error) => {
18532 push_http2_server_event(
18533 &shared,
18534 server_id,
18535 Http2BridgeEvent {
18536 kind: String::from("serverStreamError"),
18537 id: session_id,
18538 data: Some(http2_error_payload(error.to_string())),
18539 ..Http2BridgeEvent::default()
18540 },
18541 );
18542 remove_http2_session_resources(&shared, session_id);
18543 return;
18544 }
18545 };
18546 {
18547 let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
18548 snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
18549 if tls.is_some() {
18550 snapshot_guard.encrypted = true;
18551 snapshot_guard.alpn_protocol = Some(String::from("h2"));
18552 snapshot_guard.socket.encrypted = true;
18553 snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
18554 }
18555 snapshot_guard.state = http2_runtime_snapshot();
18556 }
18557 if let Ok(snapshot_json) =
18558 http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
18559 {
18560 push_http2_server_event(
18561 &shared,
18562 server_id,
18563 Http2BridgeEvent {
18564 kind: String::from(if tls.is_some() {
18565 "serverSecureConnection"
18566 } else {
18567 "serverConnection"
18568 }),
18569 id: server_id,
18570 data: Some(serde_json::to_string(&http2_socket_snapshot(local_addr, remote_addr)).unwrap_or_default()),
18571 ..Http2BridgeEvent::default()
18572 },
18573 );
18574 push_http2_server_event(
18575 &shared,
18576 server_id,
18577 Http2BridgeEvent {
18578 kind: String::from("serverSession"),
18579 id: server_id,
18580 data: Some(snapshot_json),
18581 extra_number: Some(session_id),
18582 ..Http2BridgeEvent::default()
18583 },
18584 );
18585 }
18586
18587 let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
18588 let acceptor = match build_server_tls_config(options) {
18589 Ok(config) => TlsAcceptor::from(Arc::new(config)),
18590 Err(error) => {
18591 push_http2_server_event(
18592 &shared,
18593 server_id,
18594 Http2BridgeEvent {
18595 kind: String::from("serverStreamError"),
18596 id: session_id,
18597 data: Some(http2_error_payload(error.to_string())),
18598 ..Http2BridgeEvent::default()
18599 },
18600 );
18601 remove_http2_session_resources(&shared, session_id);
18602 return;
18603 }
18604 };
18605 match acceptor.accept(stream).await {
18606 Ok(tls_stream) => Box::pin(tls_stream),
18607 Err(error) => {
18608 push_http2_server_event(
18609 &shared,
18610 server_id,
18611 Http2BridgeEvent {
18612 kind: String::from("serverStreamError"),
18613 id: session_id,
18614 data: Some(http2_error_payload(error.to_string())),
18615 ..Http2BridgeEvent::default()
18616 },
18617 );
18618 remove_http2_session_resources(&shared, session_id);
18619 return;
18620 }
18621 }
18622 } else {
18623 Box::pin(stream)
18624 };
18625
18626 let mut connection = match server::handshake(io).await {
18627 Ok(connection) => connection,
18628 Err(error) => {
18629 push_http2_server_event(
18630 &shared,
18631 server_id,
18632 Http2BridgeEvent {
18633 kind: String::from("serverStreamError"),
18634 id: session_id,
18635 data: Some(http2_error_payload(error.to_string())),
18636 ..Http2BridgeEvent::default()
18637 },
18638 );
18639 remove_http2_session_resources(&shared, session_id);
18640 return;
18641 }
18642 };
18643
18644 let streams: Arc<Mutex<BTreeMap<u64, ServerHttp2StreamState>>> =
18645 Arc::new(Mutex::new(BTreeMap::new()));
18646
18647 loop {
18648 tokio::select! {
18649 incoming = connection.accept() => {
18650 match incoming {
18651 Some(Ok((request, respond))) => {
18652 let headers_json = match serialize_http2_request_headers(&request) {
18653 Ok(headers) => headers,
18654 Err(error) => {
18655 push_http2_server_event(
18656 &shared,
18657 server_id,
18658 Http2BridgeEvent {
18659 kind: String::from("serverStreamError"),
18660 id: server_id,
18661 data: Some(http2_error_payload(error.to_string())),
18662 ..Http2BridgeEvent::default()
18663 },
18664 );
18665 continue;
18666 }
18667 };
18668 let stream_id = {
18669 let mut state = shared.lock().expect("http2 shared state");
18670 let stream_id = next_http2_stream_id(&mut state);
18671 state.streams.insert(
18672 stream_id,
18673 ActiveHttp2Stream {
18674 session_id,
18675 paused: Arc::new(AtomicBool::new(false)),
18676 },
18677 );
18678 stream_id
18679 };
18680 streams.lock().expect("http2 server streams").insert(
18681 stream_id,
18682 ServerHttp2StreamState {
18683 send_response: Some(ServerHttp2Responder::Regular(respond)),
18684 send_stream: None,
18685 },
18686 );
18687 let snapshot_json = snapshot
18688 .lock()
18689 .ok()
18690 .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok());
18691 push_http2_server_event(
18692 &shared,
18693 server_id,
18694 Http2BridgeEvent {
18695 kind: String::from("serverStream"),
18696 id: server_id,
18697 data: Some(stream_id.to_string()),
18698 extra: snapshot_json,
18699 extra_number: Some(session_id),
18700 extra_headers: Some(headers_json),
18701 flags: Some(0),
18702 },
18703 );
18704 let shared_clone = Arc::clone(&shared);
18705 tokio::spawn(async move {
18706 let mut body = request.into_body();
18707 while let Some(chunk) = body.data().await {
18708 match chunk {
18709 Ok(bytes) => {
18710 let paused = {
18711 let state = shared_clone.lock().expect("http2 shared state");
18712 state.streams.get(&stream_id).map(|stream| Arc::clone(&stream.paused))
18713 };
18714 if let Some(paused) = paused {
18715 while paused.load(Ordering::SeqCst) {
18716 tokio::time::sleep(HTTP2_POLL_DELAY).await;
18717 }
18718 }
18719 let _ = body.flow_control().release_capacity(bytes.len());
18720 push_http2_server_event(
18721 &shared_clone,
18722 server_id,
18723 Http2BridgeEvent {
18724 kind: String::from("serverStreamData"),
18725 id: stream_id,
18726 data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
18727 ..Http2BridgeEvent::default()
18728 },
18729 );
18730 }
18731 Err(error) => {
18732 push_http2_server_event(
18733 &shared_clone,
18734 server_id,
18735 Http2BridgeEvent {
18736 kind: String::from("serverStreamError"),
18737 id: stream_id,
18738 data: Some(http2_error_payload(error.to_string())),
18739 ..Http2BridgeEvent::default()
18740 },
18741 );
18742 break;
18743 }
18744 }
18745 }
18746 push_http2_server_event(
18747 &shared_clone,
18748 server_id,
18749 Http2BridgeEvent {
18750 kind: String::from("serverStreamEnd"),
18751 id: stream_id,
18752 ..Http2BridgeEvent::default()
18753 },
18754 );
18755 });
18756 }
18757 Some(Err(error)) => {
18758 push_http2_server_event(
18759 &shared,
18760 server_id,
18761 Http2BridgeEvent {
18762 kind: String::from("serverStreamError"),
18763 id: server_id,
18764 data: Some(http2_error_payload(error.to_string())),
18765 ..Http2BridgeEvent::default()
18766 },
18767 );
18768 break;
18769 }
18770 None => {
18771 push_http2_server_event(
18772 &shared,
18773 server_id,
18774 Http2BridgeEvent {
18775 kind: String::from("sessionClose"),
18776 id: session_id,
18777 ..Http2BridgeEvent::default()
18778 },
18779 );
18780 remove_http2_session_resources(&shared, session_id);
18781 break;
18782 }
18783 }
18784 }
18785 Some(command) = command_rx.recv() => {
18786 match command {
18787 Http2SessionCommand::Settings { settings_json, respond_to } => {
18788 let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
18789 .unwrap_or_default();
18790 if let Some(initial_window_size) = settings
18791 .get("initialWindowSize")
18792 .and_then(Value::as_u64)
18793 {
18794 let _ = connection.set_initial_window_size(initial_window_size as u32);
18795 }
18796 {
18797 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18798 snapshot.local_settings = http2_settings_from_value(&settings);
18799 }
18800 if let Ok(headers_json) = serde_json::to_string(&settings) {
18801 push_http2_session_event(
18802 &shared,
18803 session_id,
18804 Http2BridgeEvent {
18805 kind: String::from("sessionLocalSettings"),
18806 id: session_id,
18807 data: Some(headers_json),
18808 ..Http2BridgeEvent::default()
18809 },
18810 );
18811 }
18812 let _ = respond_to.send(Ok(Value::Null));
18813 }
18814 Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
18815 connection.set_target_window_size(size);
18816 {
18817 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18818 snapshot.state.local_window_size = size;
18819 snapshot.state.effective_local_window_size = size;
18820 }
18821 let value = snapshot
18822 .lock()
18823 .ok()
18824 .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
18825 .map(Value::String)
18826 .unwrap_or(Value::Null);
18827 let _ = respond_to.send(Ok(value));
18828 }
18829 Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
18830 connection.abrupt_shutdown(http2_reason(Some(error_code)));
18831 push_http2_session_event(
18832 &shared,
18833 session_id,
18834 Http2BridgeEvent {
18835 kind: String::from("sessionGoaway"),
18836 id: session_id,
18837 data: opaque_data.map(|value| {
18838 base64::engine::general_purpose::STANDARD.encode(value)
18839 }),
18840 extra_number: Some(error_code as u64),
18841 flags: Some(last_stream_id as u64),
18842 ..Http2BridgeEvent::default()
18843 },
18844 );
18845 let _ = respond_to.send(Ok(Value::Null));
18846 }
18847 Http2SessionCommand::Close { abrupt, respond_to } => {
18848 if abrupt {
18849 connection.abrupt_shutdown(Reason::NO_ERROR);
18850 } else {
18851 connection.graceful_shutdown();
18852 }
18853 let _ = respond_to.send(Ok(Value::Null));
18854 push_http2_session_event(
18855 &shared,
18856 session_id,
18857 Http2BridgeEvent {
18858 kind: String::from("sessionClose"),
18859 id: session_id,
18860 ..Http2BridgeEvent::default()
18861 },
18862 );
18863 remove_http2_session_resources(&shared, session_id);
18864 break;
18865 }
18866 Http2SessionCommand::StreamRespond { stream_id, headers_json, respond_to } => {
18867 let response = match build_http2_response(&headers_json) {
18868 Ok(response) => response,
18869 Err(error) => {
18870 let _ = respond_to.send(Err(error.to_string()));
18871 continue;
18872 }
18873 };
18874 let mut streams = streams.lock().expect("http2 server streams");
18875 let Some(state) = streams.get_mut(&stream_id) else {
18876 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18877 continue;
18878 };
18879 let Some(send_response) = state.send_response.as_mut() else {
18880 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
18881 continue;
18882 };
18883 match match send_response {
18884 ServerHttp2Responder::Regular(send_response) => {
18885 send_response.send_response(response, false)
18886 }
18887 ServerHttp2Responder::Pushed(send_response) => {
18888 send_response.send_response(response, false)
18889 }
18890 } {
18891 Ok(send_stream) => {
18892 state.send_stream = Some(send_stream);
18893 state.send_response = None;
18894 let _ = respond_to.send(Ok(Value::Null));
18895 }
18896 Err(error) => {
18897 let _ = respond_to.send(Err(error.to_string()));
18898 }
18899 }
18900 }
18901 Http2SessionCommand::StreamPush { stream_id, headers_json, respond_to } => {
18902 let request = match build_http2_request(&headers_json) {
18903 Ok(request) => request,
18904 Err(error) => {
18905 let _ = respond_to.send(Err(error.to_string()));
18906 continue;
18907 }
18908 };
18909 let mut streams_guard = streams.lock().expect("http2 server streams");
18910 let Some(state) = streams_guard.get_mut(&stream_id) else {
18911 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18912 continue;
18913 };
18914 let Some(send_response) = state.send_response.as_mut() else {
18915 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} cannot push after responding")));
18916 continue;
18917 };
18918 let ServerHttp2Responder::Regular(send_response) = send_response else {
18919 let _ = respond_to.send(Err(format!("HTTP/2 pushed stream {stream_id} cannot create nested push promises")));
18920 continue;
18921 };
18922 match send_response.push_request(request) {
18923 Ok(pushed) => {
18924 let pushed_stream_id = {
18925 let mut state = shared.lock().expect("http2 shared state");
18926 let pushed_stream_id = next_http2_stream_id(&mut state);
18927 state.streams.insert(
18928 pushed_stream_id,
18929 ActiveHttp2Stream {
18930 session_id,
18931 paused: Arc::new(AtomicBool::new(false)),
18932 },
18933 );
18934 pushed_stream_id
18935 };
18936 streams_guard.insert(
18937 pushed_stream_id,
18938 ServerHttp2StreamState {
18939 send_response: Some(ServerHttp2Responder::Pushed(pushed)),
18940 send_stream: None,
18941 },
18942 );
18943 let _ = respond_to.send(Ok(json!({
18944 "streamId": pushed_stream_id,
18945 "headers": headers_json,
18946 }).to_string().into()));
18947 }
18948 Err(error) => {
18949 let _ = respond_to.send(Err(error.to_string()));
18950 }
18951 }
18952 }
18953 Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
18954 let mut streams = streams.lock().expect("http2 server streams");
18955 let Some(state) = streams.get_mut(&stream_id) else {
18956 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18957 continue;
18958 };
18959 let Some(send_stream) = state.send_stream.as_mut() else {
18960 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} has not sent response headers")));
18961 continue;
18962 };
18963 match send_stream.send_data(Bytes::from(chunk), end_stream) {
18964 Ok(()) => {
18965 if end_stream {
18966 streams.remove(&stream_id);
18967 if let Ok(mut state) = shared.lock() {
18968 state.streams.remove(&stream_id);
18969 }
18970 push_http2_server_event(
18971 &shared,
18972 server_id,
18973 Http2BridgeEvent {
18974 kind: String::from("serverStreamClose"),
18975 id: stream_id,
18976 extra_number: Some(0),
18977 ..Http2BridgeEvent::default()
18978 },
18979 );
18980 }
18981 let _ = respond_to.send(Ok(Value::Bool(true)));
18982 }
18983 Err(error) => {
18984 let _ = respond_to.send(Err(error.to_string()));
18985 }
18986 }
18987 }
18988 Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
18989 let mut streams_guard = streams.lock().expect("http2 server streams");
18990 let Some(mut state) = streams_guard.remove(&stream_id) else {
18991 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18992 continue;
18993 };
18994 let reason = http2_reason(error_code);
18995 if let Some(send_stream) = state.send_stream.as_mut() {
18996 send_stream.send_reset(reason);
18997 }
18998 if let Some(send_response) = state.send_response.as_mut() {
18999 match send_response {
19000 ServerHttp2Responder::Regular(send_response) => {
19001 send_response.send_reset(reason)
19002 }
19003 ServerHttp2Responder::Pushed(send_response) => {
19004 send_response.send_reset(reason)
19005 }
19006 }
19007 }
19008 if let Ok(mut shared_guard) = shared.lock() {
19009 shared_guard.streams.remove(&stream_id);
19010 }
19011 push_http2_server_event(
19012 &shared,
19013 server_id,
19014 Http2BridgeEvent {
19015 kind: String::from("serverStreamClose"),
19016 id: stream_id,
19017 extra_number: Some(u32::from(reason) as u64),
19018 ..Http2BridgeEvent::default()
19019 },
19020 );
19021 let _ = respond_to.send(Ok(Value::Null));
19022 }
19023 Http2SessionCommand::StreamRespondWithFile { stream_id, body, headers_json, options_json, respond_to } => {
19024 let options: JavascriptHttp2FileResponseOptions =
19025 serde_json::from_str(&options_json).unwrap_or_default();
19026 let response = match build_http2_response(&headers_json) {
19027 Ok(response) => response,
19028 Err(error) => {
19029 let _ = respond_to.send(Err(error.to_string()));
19030 continue;
19031 }
19032 };
19033 let offset = usize::try_from(options.offset.unwrap_or_default()).unwrap_or(0);
19034 let body = if offset >= body.len() {
19035 Vec::new()
19036 } else {
19037 let body = &body[offset..];
19038 match options.length {
19039 Some(length) if length >= 0 => {
19040 body[..body.len().min(length as usize)].to_vec()
19041 }
19042 _ => body.to_vec(),
19043 }
19044 };
19045 let mut streams_guard = streams.lock().expect("http2 server streams");
19046 let Some(state) = streams_guard.get_mut(&stream_id) else {
19047 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19048 continue;
19049 };
19050 let Some(send_response) = state.send_response.as_mut() else {
19051 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
19052 continue;
19053 };
19054 match match send_response {
19055 ServerHttp2Responder::Regular(send_response) => {
19056 send_response.send_response(response, body.is_empty())
19057 }
19058 ServerHttp2Responder::Pushed(send_response) => {
19059 send_response.send_response(response, body.is_empty())
19060 }
19061 } {
19062 Ok(mut send_stream) => {
19063 state.send_response = None;
19064 if body.is_empty() {
19065 streams_guard.remove(&stream_id);
19066 if let Ok(mut shared_guard) = shared.lock() {
19067 shared_guard.streams.remove(&stream_id);
19068 }
19069 } else {
19070 if let Err(error) = send_stream.send_data(Bytes::from(body), true) {
19071 let _ = respond_to.send(Err(error.to_string()));
19072 continue;
19073 }
19074 streams_guard.remove(&stream_id);
19075 if let Ok(mut shared_guard) = shared.lock() {
19076 shared_guard.streams.remove(&stream_id);
19077 }
19078 }
19079 push_http2_server_event(
19080 &shared,
19081 server_id,
19082 Http2BridgeEvent {
19083 kind: String::from("serverStreamClose"),
19084 id: stream_id,
19085 extra_number: Some(0),
19086 ..Http2BridgeEvent::default()
19087 },
19088 );
19089 let _ = respond_to.send(Ok(Value::Null));
19090 }
19091 Err(error) => {
19092 let _ = respond_to.send(Err(error.to_string()));
19093 }
19094 }
19095 }
19096 Http2SessionCommand::Request { respond_to, .. } => {
19097 let _ = respond_to.send(Err(String::from("HTTP/2 server sessions cannot initiate client requests")));
19098 }
19099 }
19100 }
19101 else => break,
19102 }
19103 }
19104 });
19105 });
19106}
19107
19108fn spawn_http2_server_accept_loop(
19109 shared: Arc<Mutex<crate::state::Http2SharedState>>,
19110 server_id: u64,
19111 listener: TcpListener,
19112) {
19113 thread::spawn(move || {
19114 let listener = listener;
19115 loop {
19116 let closed = shared
19117 .lock()
19118 .ok()
19119 .and_then(|state| {
19120 state
19121 .servers
19122 .get(&server_id)
19123 .map(|server| server.closed.load(Ordering::SeqCst))
19124 })
19125 .unwrap_or(true);
19126 if closed {
19127 break;
19128 }
19129 match listener.accept() {
19130 Ok((stream, _)) => {
19131 let (command_tx, command_rx) = unbounded_channel();
19132 let (guest_local_addr, secure, tls) = {
19133 let state = shared.lock().expect("http2 shared state");
19134 let server = state.servers.get(&server_id).expect("http2 server state");
19135 (server.guest_local_addr, server.secure, server.tls.clone())
19136 };
19137 let (local_addr, remote_addr) = match (stream.local_addr(), stream.peer_addr())
19138 {
19139 (Ok(local_addr), Ok(remote_addr)) => (local_addr, remote_addr),
19140 _ => continue,
19141 };
19142 let session_snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
19143 encrypted: secure,
19144 alpn_protocol: Some(if secure {
19145 String::from("h2")
19146 } else {
19147 String::from("h2c")
19148 }),
19149 local_settings: BTreeMap::new(),
19150 remote_settings: BTreeMap::new(),
19151 state: http2_runtime_snapshot(),
19152 socket: Http2SocketSnapshot {
19153 local_address: Some(guest_local_addr.ip().to_string()),
19154 local_port: Some(guest_local_addr.port()),
19155 local_family: Some(socket_addr_family(&guest_local_addr).to_string()),
19156 remote_address: Some(remote_addr.ip().to_string()),
19157 remote_port: Some(remote_addr.port()),
19158 remote_family: Some(socket_addr_family(&remote_addr).to_string()),
19159 ..http2_socket_snapshot(local_addr, remote_addr)
19160 },
19161 ..Http2SessionSnapshot::default()
19162 }));
19163 let session_id = {
19164 let mut state = shared.lock().expect("http2 shared state");
19165 let session_id = next_http2_session_id(&mut state);
19166 state
19167 .sessions
19168 .insert(session_id, ActiveHttp2Session { command_tx });
19169 session_id
19170 };
19171 spawn_http2_server_session(
19172 Arc::clone(&shared),
19173 server_id,
19174 session_id,
19175 stream,
19176 tls,
19177 session_snapshot,
19178 command_rx,
19179 );
19180 }
19181 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
19182 thread::sleep(HTTP2_POLL_DELAY);
19183 }
19184 Err(error) => {
19185 push_http2_server_event(
19186 &shared,
19187 server_id,
19188 Http2BridgeEvent {
19189 kind: String::from("serverStreamError"),
19190 id: server_id,
19191 data: Some(http2_error_payload(error.to_string())),
19192 ..Http2BridgeEvent::default()
19193 },
19194 );
19195 thread::sleep(HTTP2_POLL_DELAY);
19196 }
19197 }
19198 }
19199 });
19200}
19201
19202fn send_http2_command(
19203 session: &ActiveHttp2Session,
19204 command: impl FnOnce(Sender<Result<Value, String>>) -> Http2SessionCommand,
19205) -> Result<Value, SidecarError> {
19206 let (respond_to, response_rx) = mpsc::channel();
19207 session.command_tx.send(command(respond_to)).map_err(|_| {
19208 SidecarError::InvalidState(String::from("HTTP/2 session command channel closed"))
19209 })?;
19210 response_rx
19211 .recv_timeout(Duration::from_secs(30))
19212 .map_err(|_| {
19213 SidecarError::Execution(String::from("timed out waiting for HTTP/2 session command"))
19214 })?
19215 .map_err(SidecarError::Execution)
19216}
19217
19218fn parse_http2_server_listen_payload(
19219 request: &JavascriptSyncRpcRequest,
19220) -> Result<JavascriptHttp2ServerListenRequest, SidecarError> {
19221 let payload_json =
19222 javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_server_listen payload")?;
19223 serde_json::from_str(payload_json).map_err(|error| {
19224 SidecarError::InvalidState(format!(
19225 "net.http2_server_listen payload must be valid JSON: {error}"
19226 ))
19227 })
19228}
19229
19230fn parse_http2_connect_payload(
19231 request: &JavascriptSyncRpcRequest,
19232) -> Result<JavascriptHttp2SessionConnectRequest, SidecarError> {
19233 let payload_json =
19234 javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_session_connect payload")?;
19235 serde_json::from_str(payload_json).map_err(|error| {
19236 SidecarError::InvalidState(format!(
19237 "net.http2_session_connect payload must be valid JSON: {error}"
19238 ))
19239 })
19240}
19241
19242fn http2_session_for_id(
19243 process: &ActiveProcess,
19244 session_id: u64,
19245) -> Result<ActiveHttp2Session, SidecarError> {
19246 let shared = process
19247 .http2
19248 .shared
19249 .lock()
19250 .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
19251 shared
19252 .sessions
19253 .get(&session_id)
19254 .cloned()
19255 .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 session {session_id}")))
19256}
19257
19258fn http2_stream_for_id(
19259 process: &ActiveProcess,
19260 stream_id: u64,
19261) -> Result<ActiveHttp2Stream, SidecarError> {
19262 let shared = process
19263 .http2
19264 .shared
19265 .lock()
19266 .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
19267 shared
19268 .streams
19269 .get(&stream_id)
19270 .cloned()
19271 .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 stream {stream_id}")))
19272}
19273
19274fn service_javascript_http2_sync_rpc<B>(
19275 request: JavascriptHttp2SyncRpcServiceRequest<'_, B>,
19276) -> Result<Value, SidecarError>
19277where
19278 B: NativeSidecarBridge + Send + 'static,
19279 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19280{
19281 let JavascriptHttp2SyncRpcServiceRequest {
19282 bridge,
19283 kernel,
19284 vm_id,
19285 dns,
19286 socket_paths,
19287 process,
19288 sync_request: request,
19289 resource_limits,
19290 network_counts,
19291 } = request;
19292 match request.method.as_str() {
19293 "net.http2_server_listen" => {
19294 check_network_resource_limit(
19295 resource_limits.max_sockets,
19296 network_counts.sockets,
19297 1,
19298 "socket",
19299 )?;
19300 let payload = parse_http2_server_listen_payload(request)?;
19301 let (family, bind_host, guest_host) =
19302 normalize_tcp_listen_host(payload.host.as_deref())?;
19303 let requested_port = payload.port.unwrap_or(0);
19304 bridge.require_network_access(
19305 vm_id,
19306 NetworkOperation::Listen,
19307 format_tcp_resource(bind_host, requested_port),
19308 )?;
19309 let port = allocate_guest_listen_port(
19310 requested_port,
19311 family,
19312 &socket_paths.used_tcp_guest_ports,
19313 socket_paths.listen_policy,
19314 )?;
19315 let mut listener =
19316 ActiveTcpListener::bind(bind_host, guest_host, port, payload.backlog)?;
19317 let guest_local_addr = listener.guest_local_addr();
19318 let closed = Arc::new(AtomicBool::new(false));
19319 {
19320 let mut state = process.http2.shared.lock().map_err(|_| {
19321 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19322 })?;
19323 state.servers.insert(
19324 payload.server_id,
19325 ActiveHttp2Server {
19326 actual_local_addr: listener.local_addr(),
19327 guest_local_addr,
19328 secure: payload.secure,
19329 tls: payload.tls.clone().map(|mut tls| {
19330 tls.is_server = payload.secure;
19331 if payload.secure && tls.alpn_protocols.is_none() {
19332 tls.alpn_protocols = Some(vec![String::from("h2")]);
19333 }
19334 tls
19335 }),
19336 closed: Arc::clone(&closed),
19337 },
19338 );
19339 state.server_events.entry(payload.server_id).or_default();
19340 }
19341 spawn_http2_server_accept_loop(
19342 Arc::clone(&process.http2.shared),
19343 payload.server_id,
19344 listener.listener.take().ok_or_else(|| {
19345 SidecarError::InvalidState(String::from(
19346 "HTTP/2 listener missing host TCP socket",
19347 ))
19348 })?,
19349 );
19350 javascript_net_json_string(
19351 json!({
19352 "address": {
19353 "address": guest_local_addr.ip().to_string(),
19354 "family": socket_addr_family(&guest_local_addr),
19355 "port": guest_local_addr.port(),
19356 }
19357 }),
19358 "net.http2_server_listen",
19359 )
19360 }
19361 "net.http2_server_poll" => {
19362 let server_id =
19363 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_poll server id")?;
19364 let wait_ms = javascript_sync_rpc_arg_u64_optional(
19365 &request.args,
19366 1,
19367 "net.http2_server_poll wait ms",
19368 )?
19369 .unwrap_or_default();
19370 match wait_for_http2_event(&process.http2.shared, server_id, true, wait_ms) {
19371 Some(event) => http2_event_value(&event),
19372 None => Ok(Value::Null),
19373 }
19374 }
19375 "net.http2_server_wait" => {
19376 let server_id =
19377 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_wait server id")?;
19378 dispatch_http2_wait_loop(process, server_id, true)
19379 }
19380 "net.http2_server_close" => {
19381 let server_id =
19382 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_close server id")?;
19383 let server = {
19384 let mut state = process.http2.shared.lock().map_err(|_| {
19385 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19386 })?;
19387 state.servers.remove(&server_id)
19388 }
19389 .ok_or_else(|| {
19390 SidecarError::InvalidState(format!("unknown HTTP/2 server {server_id}"))
19391 })?;
19392 server.closed.store(true, Ordering::SeqCst);
19393 push_http2_server_event(
19394 &process.http2.shared,
19395 server_id,
19396 Http2BridgeEvent {
19397 kind: String::from("serverClose"),
19398 id: server_id,
19399 ..Http2BridgeEvent::default()
19400 },
19401 );
19402 Ok(Value::Null)
19403 }
19404 "net.http2_server_respond" => {
19405 let server_id = javascript_sync_rpc_arg_u64(
19406 &request.args,
19407 0,
19408 "net.http2_server_respond server id",
19409 )?;
19410 let request_id = javascript_sync_rpc_arg_u64(
19411 &request.args,
19412 1,
19413 "net.http2_server_respond request id",
19414 )?;
19415 let response_json =
19416 javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_server_respond payload")?;
19417 ensure_vm_fetch_response_within_limit(
19418 response_json,
19419 "net.http2_server_respond",
19420 VM_FETCH_BUFFER_LIMIT_BYTES,
19421 )?;
19422 serde_json::from_str::<Value>(response_json).map_err(|error| {
19423 SidecarError::Execution(format!(
19424 "net.http2_server_respond payload must be valid JSON: {error}"
19425 ))
19426 })?;
19427 let Some(pending) = process
19428 .pending_http_requests
19429 .get_mut(&(server_id, request_id))
19430 else {
19431 return Err(SidecarError::InvalidState(format!(
19432 "unknown pending HTTP/2 request {request_id} for server {server_id}"
19433 )));
19434 };
19435 *pending = Some(response_json.to_owned());
19436 Ok(Value::Bool(true))
19437 }
19438 "net.http2_session_connect" => {
19439 check_network_resource_limit(
19440 resource_limits.max_sockets,
19441 network_counts.sockets,
19442 1,
19443 "socket",
19444 )?;
19445 check_network_resource_limit(
19446 resource_limits.max_connections,
19447 network_counts.connections,
19448 1,
19449 "connection",
19450 )?;
19451 let payload = parse_http2_connect_payload(request)?;
19452 let authority = payload.authority.clone().unwrap_or_else(|| {
19453 format!(
19454 "{}://{}:{}",
19455 payload.protocol.as_deref().unwrap_or("http"),
19456 payload.host.as_deref().unwrap_or("localhost"),
19457 payload.port.unwrap_or(80)
19458 )
19459 });
19460 let url = Url::parse(&authority).map_err(|error| {
19461 SidecarError::InvalidState(format!(
19462 "invalid HTTP/2 authority {authority:?}: {error}"
19463 ))
19464 })?;
19465 let secure = url.scheme() == "https" || payload.protocol.as_deref() == Some("https:");
19466 let host = payload
19467 .host
19468 .as_deref()
19469 .or_else(|| url.host_str())
19470 .unwrap_or("localhost");
19471 let port = payload.port.or_else(|| url.port()).unwrap_or(80);
19472 bridge.require_network_access(
19473 vm_id,
19474 NetworkOperation::Http,
19475 format_tcp_resource(host, port),
19476 )?;
19477 let resolved = {
19478 let shared = process.http2.shared.lock().map_err(|_| {
19479 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19480 })?;
19481 shared
19482 .servers
19483 .values()
19484 .find(|server| {
19485 is_loopback_request_host(host) && server.guest_local_addr.port() == port
19486 })
19487 .map(|server| ResolvedTcpConnectAddr {
19488 actual_addr: server.actual_local_addr,
19489 guest_remote_addr: server.guest_local_addr,
19490 use_kernel_loopback: false,
19491 })
19492 };
19493 let resolved = match resolved {
19494 Some(resolved) => resolved,
19495 None => {
19496 resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, socket_paths)?
19497 }
19498 };
19499 let (command_tx, command_rx) = unbounded_channel();
19500 let snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
19501 encrypted: secure,
19502 alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
19503 local_settings: http2_settings_from_value(&payload.settings),
19504 remote_settings: BTreeMap::new(),
19505 state: http2_runtime_snapshot(),
19506 socket: Http2SocketSnapshot {
19507 encrypted: secure,
19508 remote_address: Some(resolved.guest_remote_addr.ip().to_string()),
19509 remote_port: Some(resolved.guest_remote_addr.port()),
19510 remote_family: Some(
19511 socket_addr_family(&resolved.guest_remote_addr).to_string(),
19512 ),
19513 servername: if secure {
19514 payload
19515 .tls
19516 .as_ref()
19517 .and_then(|tls| tls.servername.clone())
19518 .or_else(|| Some(host.to_string()))
19519 } else {
19520 None
19521 },
19522 alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
19523 ..Http2SocketSnapshot::default()
19524 },
19525 ..Http2SessionSnapshot::default()
19526 }));
19527 let session_id = {
19528 let mut state = process.http2.shared.lock().map_err(|_| {
19529 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19530 })?;
19531 let session_id = next_http2_session_id(&mut state);
19532 state
19533 .sessions
19534 .insert(session_id, ActiveHttp2Session { command_tx });
19535 state.session_events.entry(session_id).or_default();
19536 session_id
19537 };
19538 spawn_http2_client_session(
19539 Arc::clone(&process.http2.shared),
19540 session_id,
19541 resolved.actual_addr,
19542 if secure {
19543 Some(payload.tls.unwrap_or(JavascriptTlsBridgeOptions {
19544 is_server: false,
19545 servername: Some(host.to_string()),
19546 alpn_protocols: Some(vec![String::from("h2")]),
19547 ..JavascriptTlsBridgeOptions::default()
19548 }))
19549 } else {
19550 None
19551 },
19552 Arc::clone(&snapshot),
19553 command_rx,
19554 );
19555 let snapshot_json =
19556 http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())?;
19557 javascript_net_json_string(
19558 json!({
19559 "sessionId": session_id,
19560 "state": snapshot_json,
19561 }),
19562 "net.http2_session_connect",
19563 )
19564 }
19565 "net.http2_session_request" => {
19566 let session_id = javascript_sync_rpc_arg_u64(
19567 &request.args,
19568 0,
19569 "net.http2_session_request session id",
19570 )?;
19571 let headers_json =
19572 javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_session_request headers")?;
19573 let options_json =
19574 javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_session_request options")?;
19575 let session = http2_session_for_id(process, session_id)?;
19576 send_http2_command(&session, |respond_to| Http2SessionCommand::Request {
19577 headers_json: headers_json.to_owned(),
19578 options_json: options_json.to_owned(),
19579 respond_to,
19580 })
19581 }
19582 "net.http2_session_settings" => {
19583 let session_id = javascript_sync_rpc_arg_u64(
19584 &request.args,
19585 0,
19586 "net.http2_session_settings session id",
19587 )?;
19588 let settings_json = javascript_sync_rpc_arg_str(
19589 &request.args,
19590 1,
19591 "net.http2_session_settings settings",
19592 )?;
19593 let session = http2_session_for_id(process, session_id)?;
19594 send_http2_command(&session, |respond_to| Http2SessionCommand::Settings {
19595 settings_json: settings_json.to_owned(),
19596 respond_to,
19597 })
19598 }
19599 "net.http2_session_set_local_window_size" => {
19600 let session_id = javascript_sync_rpc_arg_u64(
19601 &request.args,
19602 0,
19603 "net.http2_session_set_local_window_size session id",
19604 )?;
19605 let window_size = javascript_sync_rpc_arg_u64(
19606 &request.args,
19607 1,
19608 "net.http2_session_set_local_window_size window size",
19609 )?;
19610 let session = http2_session_for_id(process, session_id)?;
19611 send_http2_command(&session, |respond_to| {
19612 Http2SessionCommand::SetLocalWindowSize {
19613 size: window_size as u32,
19614 respond_to,
19615 }
19616 })
19617 }
19618 "net.http2_session_goaway" => {
19619 let session_id = javascript_sync_rpc_arg_u64(
19620 &request.args,
19621 0,
19622 "net.http2_session_goaway session id",
19623 )?;
19624 let error_code = javascript_sync_rpc_arg_u64(
19625 &request.args,
19626 1,
19627 "net.http2_session_goaway error code",
19628 )?;
19629 let last_stream_id = javascript_sync_rpc_arg_u64(
19630 &request.args,
19631 2,
19632 "net.http2_session_goaway last stream id",
19633 )?;
19634 let opaque_data = request
19635 .args
19636 .get(3)
19637 .and_then(Value::as_str)
19638 .map(|value| {
19639 base64::engine::general_purpose::STANDARD
19640 .decode(value)
19641 .map_err(|error| {
19642 SidecarError::InvalidState(format!("invalid GOAWAY payload: {error}"))
19643 })
19644 })
19645 .transpose()?;
19646 let session = http2_session_for_id(process, session_id)?;
19647 send_http2_command(&session, |respond_to| Http2SessionCommand::Goaway {
19648 error_code: error_code as u32,
19649 last_stream_id: last_stream_id as u32,
19650 opaque_data,
19651 respond_to,
19652 })
19653 }
19654 "net.http2_session_close" | "net.http2_session_destroy" => {
19655 let session_id = javascript_sync_rpc_arg_u64(
19656 &request.args,
19657 0,
19658 "net.http2_session_close session id",
19659 )?;
19660 let session = http2_session_for_id(process, session_id)?;
19661 send_http2_command(&session, |respond_to| Http2SessionCommand::Close {
19662 abrupt: request.method == "net.http2_session_destroy",
19663 respond_to,
19664 })
19665 }
19666 "net.http2_session_poll" => {
19667 let session_id =
19668 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_poll session id")?;
19669 let wait_ms = javascript_sync_rpc_arg_u64_optional(
19670 &request.args,
19671 1,
19672 "net.http2_session_poll wait ms",
19673 )?
19674 .unwrap_or_default();
19675 match wait_for_http2_event(&process.http2.shared, session_id, false, wait_ms) {
19676 Some(event) => http2_event_value(&event),
19677 None => Ok(Value::Null),
19678 }
19679 }
19680 "net.http2_session_wait" => {
19681 let session_id =
19682 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_wait session id")?;
19683 dispatch_http2_wait_loop(process, session_id, false)
19684 }
19685 "net.http2_stream_respond" => {
19686 let stream_id = javascript_sync_rpc_arg_u64(
19687 &request.args,
19688 0,
19689 "net.http2_stream_respond stream id",
19690 )?;
19691 let headers_json =
19692 javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_stream_respond headers")?;
19693 let stream = http2_stream_for_id(process, stream_id)?;
19694 let session = http2_session_for_id(process, stream.session_id)?;
19695 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamRespond {
19696 stream_id,
19697 headers_json: headers_json.to_owned(),
19698 respond_to,
19699 })
19700 }
19701 "net.http2_stream_push_stream" => {
19702 let stream_id = javascript_sync_rpc_arg_u64(
19703 &request.args,
19704 0,
19705 "net.http2_stream_push_stream stream id",
19706 )?;
19707 let headers_json = javascript_sync_rpc_arg_str(
19708 &request.args,
19709 1,
19710 "net.http2_stream_push_stream headers",
19711 )?;
19712 let _options_json = javascript_sync_rpc_arg_str(
19713 &request.args,
19714 2,
19715 "net.http2_stream_push_stream options",
19716 )?;
19717 let stream = http2_stream_for_id(process, stream_id)?;
19718 let session = http2_session_for_id(process, stream.session_id)?;
19719 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamPush {
19720 stream_id,
19721 headers_json: headers_json.to_owned(),
19722 respond_to,
19723 })
19724 }
19725 "net.http2_stream_write" => {
19726 let stream_id =
19727 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_write stream id")?;
19728 let chunk =
19729 javascript_sync_rpc_base64_arg(&request.args, 1, "net.http2_stream_write data")?;
19730 let stream = http2_stream_for_id(process, stream_id)?;
19731 let session = http2_session_for_id(process, stream.session_id)?;
19732 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
19733 stream_id,
19734 chunk,
19735 end_stream: false,
19736 respond_to,
19737 })
19738 }
19739 "net.http2_stream_end" => {
19740 let stream_id =
19741 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_end stream id")?;
19742 let chunk = request
19743 .args
19744 .get(1)
19745 .and_then(Value::as_str)
19746 .map(|value| {
19747 base64::engine::general_purpose::STANDARD
19748 .decode(value)
19749 .map_err(|error| {
19750 SidecarError::InvalidState(format!(
19751 "invalid HTTP/2 stream payload: {error}"
19752 ))
19753 })
19754 })
19755 .transpose()?
19756 .unwrap_or_default();
19757 let stream = http2_stream_for_id(process, stream_id)?;
19758 let session = http2_session_for_id(process, stream.session_id)?;
19759 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
19760 stream_id,
19761 chunk,
19762 end_stream: true,
19763 respond_to,
19764 })
19765 }
19766 "net.http2_stream_close" => {
19767 let stream_id =
19768 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_close stream id")?;
19769 let code = javascript_sync_rpc_arg_u64_optional(
19770 &request.args,
19771 1,
19772 "net.http2_stream_close error code",
19773 )?
19774 .map(|value| value as u32);
19775 let stream = http2_stream_for_id(process, stream_id)?;
19776 let session = http2_session_for_id(process, stream.session_id)?;
19777 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamClose {
19778 stream_id,
19779 error_code: code,
19780 respond_to,
19781 })
19782 }
19783 "net.http2_stream_pause" => {
19784 let stream_id =
19785 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_pause stream id")?;
19786 let stream = http2_stream_for_id(process, stream_id)?;
19787 stream.paused.store(true, Ordering::SeqCst);
19788 Ok(Value::Null)
19789 }
19790 "net.http2_stream_resume" => {
19791 let stream_id =
19792 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_resume stream id")?;
19793 let stream = http2_stream_for_id(process, stream_id)?;
19794 stream.paused.store(false, Ordering::SeqCst);
19795 Ok(Value::Null)
19796 }
19797 "net.http2_stream_respond_with_file" => {
19798 let stream_id = javascript_sync_rpc_arg_u64(
19799 &request.args,
19800 0,
19801 "net.http2_stream_respond_with_file stream id",
19802 )?;
19803 let path = javascript_sync_rpc_arg_str(
19804 &request.args,
19805 1,
19806 "net.http2_stream_respond_with_file path",
19807 )?;
19808 let headers_json = javascript_sync_rpc_arg_str(
19809 &request.args,
19810 2,
19811 "net.http2_stream_respond_with_file headers",
19812 )?;
19813 let options_json = javascript_sync_rpc_arg_str(
19814 &request.args,
19815 3,
19816 "net.http2_stream_respond_with_file options",
19817 )?;
19818 let stream = http2_stream_for_id(process, stream_id)?;
19819 let session = http2_session_for_id(process, stream.session_id)?;
19820 let guest_path = resolve_http2_file_response_guest_path(process, path);
19821 let body = kernel.read_file(&guest_path).map_err(kernel_error)?;
19822 send_http2_command(&session, |respond_to| {
19823 Http2SessionCommand::StreamRespondWithFile {
19824 stream_id,
19825 body,
19826 headers_json: headers_json.to_owned(),
19827 options_json: options_json.to_owned(),
19828 respond_to,
19829 }
19830 })
19831 }
19832 other => Err(SidecarError::InvalidState(format!(
19833 "unsupported JavaScript HTTP/2 sync RPC method {other}"
19834 ))),
19835 }
19836}
19837
19838const JAVASCRIPT_NET_POLL_MAX_WAIT: Duration = Duration::from_millis(50);
19839const EXITED_PROCESS_SNAPSHOT_RETENTION: Duration = Duration::from_secs(2);
19840
19841fn resolve_http2_file_response_guest_path(process: &ActiveProcess, path: &str) -> String {
19842 if Path::new(path).is_absolute() {
19843 normalize_path(path)
19844 } else {
19845 normalize_path(&format!("{}/{}", process.guest_cwd, path))
19846 }
19847}
19848
19849pub(crate) fn clamp_javascript_net_poll_wait(wait_ms: u64) -> Duration {
19850 if wait_ms == 0 {
19853 Duration::ZERO
19854 } else {
19855 Duration::from_millis(wait_ms).min(JAVASCRIPT_NET_POLL_MAX_WAIT)
19856 }
19857}
19858
19859pub(crate) fn service_javascript_net_sync_rpc<B>(
19860 request: JavascriptNetSyncRpcServiceRequest<'_, B>,
19861) -> Result<Value, SidecarError>
19862where
19863 B: NativeSidecarBridge + Send + 'static,
19864 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19865{
19866 let JavascriptNetSyncRpcServiceRequest {
19867 bridge,
19868 vm_id,
19869 dns,
19870 socket_paths,
19871 kernel,
19872 process,
19873 sync_request: request,
19874 resource_limits,
19875 network_counts,
19876 } = request;
19877 match request.method.as_str() {
19878 "net.http_listen" => {
19879 check_network_resource_limit(
19880 resource_limits.max_sockets,
19881 network_counts.sockets,
19882 1,
19883 "socket",
19884 )?;
19885 let payload_json =
19886 javascript_sync_rpc_arg_str(&request.args, 0, "net.http_listen payload")?;
19887 let payload: JavascriptHttpListenRequest =
19888 serde_json::from_str(payload_json).map_err(|error| {
19889 SidecarError::InvalidState(format!(
19890 "net.http_listen payload must be valid JSON: {error}"
19891 ))
19892 })?;
19893 let (family, bind_host, guest_host) =
19894 normalize_tcp_listen_host(payload.hostname.as_deref())?;
19895 let requested_port = payload.port.unwrap_or(0);
19896 bridge.require_network_access(
19897 vm_id,
19898 NetworkOperation::Listen,
19899 format_tcp_resource(bind_host, requested_port),
19900 )?;
19901 let port = allocate_guest_listen_port(
19902 requested_port,
19903 family,
19904 &socket_paths.used_tcp_guest_ports,
19905 socket_paths.listen_policy,
19906 )?;
19907 let mut listener = ActiveTcpListener::bind(
19908 bind_host,
19909 guest_host,
19910 port,
19911 Some(DEFAULT_JAVASCRIPT_NET_BACKLOG),
19912 )?;
19913 let guest_local_addr = listener.guest_local_addr();
19914 process.http_servers.insert(
19915 payload.server_id,
19916 ActiveHttpServer {
19917 listener: listener.listener.take().ok_or_else(|| {
19918 SidecarError::InvalidState(String::from(
19919 "HTTP listener missing host TCP socket",
19920 ))
19921 })?,
19922 guest_local_addr,
19923 next_request_id: 0,
19924 },
19925 );
19926 serde_json::to_string(&json!({
19927 "address": {
19928 "address": guest_local_addr.ip().to_string(),
19929 "family": socket_addr_family(&guest_local_addr),
19930 "port": guest_local_addr.port(),
19931 }
19932 }))
19933 .map(Value::String)
19934 .map_err(|error| {
19935 SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}"))
19936 })
19937 }
19938 "net.http_close" => {
19939 let server_id =
19940 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_close server id")?;
19941 let server = process.http_servers.remove(&server_id).ok_or_else(|| {
19942 SidecarError::InvalidState(format!("unknown HTTP server {server_id}"))
19943 })?;
19944 drop(server.listener);
19945 process
19946 .pending_http_requests
19947 .retain(|(pending_server_id, _), _| *pending_server_id != server_id);
19948 Ok(Value::Null)
19949 }
19950 "net.http_wait" => {
19951 let server_id =
19952 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_wait server id")?;
19953 dispatch_http_wait_loop(process, server_id)
19954 }
19955 "net.http_respond" => {
19956 let server_id =
19957 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_respond server id")?;
19958 let request_id =
19959 javascript_sync_rpc_arg_u64(&request.args, 1, "net.http_respond request id")?;
19960 let response_json =
19961 javascript_sync_rpc_arg_str(&request.args, 2, "net.http_respond payload")?;
19962 ensure_vm_fetch_response_within_limit(
19963 response_json,
19964 "net.http_respond",
19965 VM_FETCH_BUFFER_LIMIT_BYTES,
19966 )?;
19967 serde_json::from_str::<Value>(response_json).map_err(|error| {
19968 SidecarError::Execution(format!(
19969 "net.http_respond payload must be valid JSON: {error}"
19970 ))
19971 })?;
19972 let Some(pending) = process
19973 .pending_http_requests
19974 .get_mut(&(server_id, request_id))
19975 else {
19976 return Err(SidecarError::InvalidState(format!(
19977 "unknown pending HTTP request {request_id} for server {server_id}"
19978 )));
19979 };
19980 *pending = Some(response_json.to_owned());
19981 Ok(Value::Null)
19982 }
19983 "net.reserve_tcp_port" => {
19984 let payload = request
19985 .args
19986 .first()
19987 .cloned()
19988 .ok_or_else(|| {
19989 SidecarError::InvalidState(String::from(
19990 "net.reserve_tcp_port requires a request payload",
19991 ))
19992 })
19993 .and_then(|value| {
19994 serde_json::from_value::<JavascriptNetReserveTcpPortRequest>(value).map_err(
19995 |error| {
19996 SidecarError::InvalidState(format!(
19997 "invalid net.reserve_tcp_port payload: {error}"
19998 ))
19999 },
20000 )
20001 })?;
20002 let (family, _bind_host, guest_host) =
20003 normalize_tcp_listen_host(payload.host.as_deref())?;
20004 let requested_port = payload.port.unwrap_or(0);
20005 let port = allocate_guest_listen_port(
20006 requested_port,
20007 family,
20008 &socket_paths.used_tcp_guest_ports,
20009 socket_paths.listen_policy,
20010 )?;
20011 let reservation_id = process.allocate_tcp_port_reservation_id();
20012 process
20013 .tcp_port_reservations
20014 .insert(reservation_id.clone(), (family, port));
20015 Ok(json!({
20016 "reservationId": reservation_id,
20017 "localAddress": guest_host,
20018 "localPort": port,
20019 "family": match family {
20020 JavascriptSocketFamily::Ipv4 => "IPv4",
20021 JavascriptSocketFamily::Ipv6 => "IPv6",
20022 },
20023 }))
20024 }
20025 "net.release_tcp_port" => {
20026 let reservation_id =
20027 javascript_sync_rpc_arg_str(&request.args, 0, "net.release_tcp_port reservation")?;
20028 process.tcp_port_reservations.remove(reservation_id);
20029 Ok(Value::Null)
20030 }
20031 "net.connect" => {
20032 check_network_resource_limit(
20033 resource_limits.max_sockets,
20034 network_counts.sockets,
20035 1,
20036 "socket",
20037 )?;
20038 check_network_resource_limit(
20039 resource_limits.max_connections,
20040 network_counts.connections,
20041 1,
20042 "connection",
20043 )?;
20044 let payload = request
20045 .args
20046 .first()
20047 .cloned()
20048 .ok_or_else(|| {
20049 SidecarError::InvalidState(String::from(
20050 "net.connect requires a request payload",
20051 ))
20052 })
20053 .and_then(|value| {
20054 serde_json::from_value::<JavascriptNetConnectRequest>(value).map_err(|error| {
20055 SidecarError::InvalidState(format!("invalid net.connect payload: {error}"))
20056 })
20057 })?;
20058 if let Some(path) = payload.path.as_deref() {
20059 let guest_path = normalize_path(path);
20060 let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
20061 let socket = ActiveUnixSocket::connect(&host_path, &guest_path)?;
20062 let socket_id = process.allocate_unix_socket_id();
20063 process.unix_sockets.insert(socket_id.clone(), socket);
20064 Ok(json!({
20065 "socketId": socket_id,
20066 "remotePath": guest_path,
20067 }))
20068 } else {
20069 let port = payload.port.ok_or_else(|| {
20070 SidecarError::InvalidState(String::from(
20071 "net.connect requires either a path or port",
20072 ))
20073 })?;
20074 let host = payload.host.as_deref().unwrap_or("localhost");
20075 let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
20076 process
20077 .tcp_port_reservations
20078 .remove(id)
20079 .map(|reservation| (id.to_owned(), reservation))
20080 });
20081 bridge.require_network_access(
20082 vm_id,
20083 NetworkOperation::Http,
20084 format_tcp_resource(host, port),
20085 )?;
20086 if is_loopback_socket_host(host) {
20087 let families = [JavascriptSocketFamily::Ipv4, JavascriptSocketFamily::Ipv6];
20088 if let Some((family, target)) = families.iter().find_map(|family| {
20089 socket_paths
20090 .http_loopback_target(*family, port)
20091 .map(|target| (*family, target))
20092 }) {
20093 if let Some((reservation_id, reservation)) = local_reservation {
20094 process
20095 .tcp_port_reservations
20096 .insert(reservation_id, reservation);
20097 }
20098 let remote_address = match family {
20099 JavascriptSocketFamily::Ipv4 => "127.0.0.1",
20100 JavascriptSocketFamily::Ipv6 => "::1",
20101 };
20102 return Ok(json!({
20103 "loopbackHttpTarget": {
20104 "processId": target.process_id.clone(),
20105 "serverId": target.server_id,
20106 "host": remote_address,
20107 "port": port,
20108 },
20109 "localAddress": match family {
20110 JavascriptSocketFamily::Ipv4 => "127.0.0.1",
20111 JavascriptSocketFamily::Ipv6 => "::1",
20112 },
20113 "localPort": payload.local_port.unwrap_or(0),
20114 "remoteAddress": remote_address,
20115 "remotePort": port,
20116 "remoteFamily": match family {
20117 JavascriptSocketFamily::Ipv4 => "IPv4",
20118 JavascriptSocketFamily::Ipv6 => "IPv6",
20119 },
20120 }));
20121 }
20122 }
20123 let connect_result = ActiveTcpSocket::connect(ActiveTcpConnectRequest {
20124 bridge,
20125 kernel,
20126 kernel_pid: process.kernel_pid,
20127 vm_id,
20128 dns,
20129 host,
20130 port,
20131 local_address: payload.local_address.as_deref(),
20132 local_port: payload.local_port,
20133 local_reservation: local_reservation
20134 .as_ref()
20135 .map(|(_, reservation)| *reservation),
20136 context: socket_paths,
20137 });
20138 if let Err(error) = connect_result {
20139 if let Some((reservation_id, reservation)) = local_reservation {
20140 process
20141 .tcp_port_reservations
20142 .insert(reservation_id, reservation);
20143 }
20144 return Err(error);
20145 }
20146 let socket = connect_result?;
20147 let socket_id = process.allocate_tcp_socket_id();
20148 let local_addr = socket.guest_local_addr;
20149 let remote_addr = socket.guest_remote_addr;
20150 process.tcp_sockets.insert(socket_id.clone(), socket);
20151 Ok(json!({
20152 "socketId": socket_id,
20153 "localAddress": local_addr.ip().to_string(),
20154 "localPort": local_addr.port(),
20155 "remoteAddress": remote_addr.ip().to_string(),
20156 "remotePort": remote_addr.port(),
20157 "remoteFamily": socket_addr_family(&remote_addr),
20158 }))
20159 }
20160 }
20161 "net.listen" => {
20162 check_network_resource_limit(
20163 resource_limits.max_sockets,
20164 network_counts.sockets,
20165 1,
20166 "socket",
20167 )?;
20168 let payload = request
20169 .args
20170 .first()
20171 .cloned()
20172 .ok_or_else(|| {
20173 SidecarError::InvalidState(String::from(
20174 "net.listen requires a request payload",
20175 ))
20176 })
20177 .and_then(|value| match value {
20178 Value::String(json) => {
20179 serde_json::from_str::<JavascriptNetListenRequest>(&json).map_err(|error| {
20180 SidecarError::InvalidState(format!(
20181 "invalid net.listen payload: {error}"
20182 ))
20183 })
20184 }
20185 other => serde_json::from_value::<JavascriptNetListenRequest>(other).map_err(
20186 |error| {
20187 SidecarError::InvalidState(format!(
20188 "invalid net.listen payload: {error}"
20189 ))
20190 },
20191 ),
20192 })?;
20193 if let Some(path) = payload.path.as_deref() {
20194 let guest_path = normalize_path(path);
20195 if kernel.exists(&guest_path).map_err(kernel_error)? {
20196 return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
20197 libc::EADDRINUSE,
20198 )));
20199 }
20200
20201 let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
20202 let on_host_mount =
20203 host_mount_path_for_guest_path_from_mounts(&socket_paths.mounts, &guest_path)
20204 .is_some();
20205 let listener = ActiveUnixListener::bind(&host_path, &guest_path, payload.backlog)?;
20206 if !on_host_mount {
20207 ensure_kernel_parent_directories(kernel, &guest_path)?;
20208 kernel
20209 .write_file(&guest_path, Vec::new())
20210 .map_err(kernel_error)?;
20211 }
20212 let listener_id = process.allocate_unix_listener_id();
20213 process.unix_listeners.insert(listener_id.clone(), listener);
20214 Ok(json!({
20215 "serverId": listener_id,
20216 "path": guest_path,
20217 }))
20218 } else {
20219 let (family, bind_host, guest_host) =
20220 normalize_tcp_listen_host(payload.host.as_deref())?;
20221 let requested_port = payload.port.unwrap_or(0);
20222 bridge.require_network_access(
20223 vm_id,
20224 NetworkOperation::Listen,
20225 format_tcp_resource(bind_host, requested_port),
20226 )?;
20227 let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
20228 process
20229 .tcp_port_reservations
20230 .remove(id)
20231 .map(|reservation| (id.to_owned(), reservation))
20232 });
20233 let port = if requested_port != 0
20234 && local_reservation
20235 .as_ref()
20236 .map(|(_, reservation)| *reservation)
20237 == Some((family, requested_port))
20238 {
20239 requested_port
20240 } else {
20241 allocate_guest_listen_port(
20242 requested_port,
20243 family,
20244 &socket_paths.used_tcp_guest_ports,
20245 socket_paths.listen_policy,
20246 )?
20247 };
20248 let listener_result = ActiveTcpListener::bind_kernel(
20249 kernel,
20250 process.kernel_pid,
20251 guest_host,
20252 port,
20253 payload.backlog,
20254 );
20255 if let Err(error) = listener_result {
20256 if let Some((reservation_id, reservation)) = local_reservation {
20257 process
20258 .tcp_port_reservations
20259 .insert(reservation_id, reservation);
20260 }
20261 return Err(error);
20262 }
20263 let listener = listener_result?;
20264 let listener_id = process.allocate_tcp_listener_id();
20265 let local_addr = listener.guest_local_addr();
20266 process.tcp_listeners.insert(listener_id.clone(), listener);
20267 Ok(json!({
20268 "serverId": listener_id,
20269 "localAddress": local_addr.ip().to_string(),
20270 "localPort": local_addr.port(),
20271 "family": socket_addr_family(&local_addr),
20272 }))
20273 }
20274 }
20275 "net.poll" => {
20276 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.poll socket id")?;
20277 let wait_ms =
20278 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.poll wait ms")?
20279 .unwrap_or_default();
20280 let wait = clamp_javascript_net_poll_wait(wait_ms);
20281 let event = if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20282 socket.poll(kernel, process.kernel_pid, wait)?
20283 } else if let Some(socket) = process.unix_sockets.get_mut(socket_id) {
20284 socket.poll(wait)?
20285 } else {
20286 return Err(SidecarError::InvalidState(format!(
20287 "unknown net socket {socket_id}"
20288 )));
20289 };
20290
20291 match event {
20292 Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(json!({
20293 "type": "data",
20294 "data": javascript_sync_rpc_bytes_value(&chunk),
20295 })),
20296 Some(JavascriptTcpSocketEvent::End) => Ok(json!({
20297 "type": "end",
20298 })),
20299 Some(JavascriptTcpSocketEvent::Error { code, message }) => Ok(json!({
20300 "type": "error",
20301 "code": code,
20302 "message": message,
20303 })),
20304 Some(JavascriptTcpSocketEvent::Close { had_error }) => {
20305 if let Some(socket) = process.tcp_sockets.remove(socket_id) {
20306 if let Some(listener_id) = socket.listener_id.as_deref() {
20307 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20308 listener.release_connection(socket_id);
20309 }
20310 }
20311 } else if let Some(socket) = process.unix_sockets.remove(socket_id) {
20312 if let Some(listener_id) = socket.listener_id.as_deref() {
20313 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20314 listener.release_connection(socket_id);
20315 }
20316 }
20317 }
20318 Ok(json!({
20319 "type": "close",
20320 "hadError": had_error,
20321 }))
20322 }
20323 None => Ok(Value::Null),
20324 }
20325 }
20326 "net.socket_wait_connect" => {
20327 let socket_id =
20328 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_wait_connect socket id")?;
20329 if let Some(socket) = process.tcp_sockets.get(socket_id) {
20330 javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
20331 } else {
20332 let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20333 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20334 })?;
20335 javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
20336 }
20337 }
20338 "net.socket_read" => {
20339 let socket_id =
20340 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_read socket id")?;
20341 if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20342 javascript_net_read_value(socket.poll(
20343 kernel,
20344 process.kernel_pid,
20345 Duration::ZERO,
20346 )?)
20347 } else {
20348 let socket = process.unix_sockets.get_mut(socket_id).ok_or_else(|| {
20349 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20350 })?;
20351 javascript_net_read_value(socket.poll(Duration::ZERO)?)
20352 }
20353 }
20354 "net.socket_set_no_delay" => {
20355 let socket_id =
20356 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_set_no_delay socket id")?;
20357 let enable =
20358 javascript_sync_rpc_arg_bool(&request.args, 1, "net.socket_set_no_delay enabled")?;
20359 if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20360 socket.set_no_delay(enable)?;
20361 } else if !process.unix_sockets.contains_key(socket_id) {
20362 return Err(SidecarError::InvalidState(format!(
20363 "unknown net socket {socket_id}"
20364 )));
20365 }
20366 Ok(Value::Null)
20367 }
20368 "net.socket_set_keep_alive" => {
20369 let socket_id = javascript_sync_rpc_arg_str(
20370 &request.args,
20371 0,
20372 "net.socket_set_keep_alive socket id",
20373 )?;
20374 let enable = javascript_sync_rpc_arg_bool(
20375 &request.args,
20376 1,
20377 "net.socket_set_keep_alive enabled",
20378 )?;
20379 let initial_delay_secs = javascript_sync_rpc_arg_u64_optional(
20380 &request.args,
20381 2,
20382 "net.socket_set_keep_alive initial delay seconds",
20383 )?;
20384 if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20385 socket.set_keep_alive(enable, initial_delay_secs)?;
20386 } else if !process.unix_sockets.contains_key(socket_id) {
20387 return Err(SidecarError::InvalidState(format!(
20388 "unknown net socket {socket_id}"
20389 )));
20390 }
20391 Ok(Value::Null)
20392 }
20393 "net.socket_upgrade_tls" => {
20394 let socket_id =
20395 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_upgrade_tls socket id")?;
20396 let options_json =
20397 javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_upgrade_tls options")?;
20398 let options: JavascriptTlsBridgeOptions =
20399 serde_json::from_str(options_json).map_err(|error| {
20400 SidecarError::InvalidState(format!(
20401 "net.socket_upgrade_tls options must be valid JSON: {error}"
20402 ))
20403 })?;
20404 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20405 SidecarError::InvalidState(format!(
20406 "unknown TCP socket {socket_id} for TLS upgrade"
20407 ))
20408 })?;
20409 socket.upgrade_tls(vm_id, kernel, options)?;
20410 Ok(Value::Null)
20411 }
20412 "net.socket_get_tls_client_hello" => {
20413 let socket_id = javascript_sync_rpc_arg_str(
20414 &request.args,
20415 0,
20416 "net.socket_get_tls_client_hello socket id",
20417 )?;
20418 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20419 SidecarError::InvalidState(format!(
20420 "unknown TCP socket {socket_id} for TLS client hello query"
20421 ))
20422 })?;
20423 socket.tls_client_hello_json(vm_id, kernel)
20424 }
20425 "net.socket_tls_query" => {
20426 let socket_id =
20427 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_tls_query socket id")?;
20428 let query =
20429 javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_tls_query query")?;
20430 let detailed = request
20431 .args
20432 .get(2)
20433 .and_then(Value::as_bool)
20434 .unwrap_or(false);
20435 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20436 SidecarError::InvalidState(format!("unknown TCP socket {socket_id} for TLS query"))
20437 })?;
20438 socket.tls_query(query, detailed)
20439 }
20440 "net.server_poll" => {
20441 let listener_id =
20442 javascript_sync_rpc_arg_str(&request.args, 0, "net.server_poll listener id")?;
20443 let wait_ms =
20444 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.server_poll wait ms")?
20445 .unwrap_or_default();
20446 let tcp_event = if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20447 Some(listener.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?)
20448 } else {
20449 None
20450 };
20451
20452 if let Some(event) = tcp_event {
20453 return match event {
20454 Some(JavascriptTcpListenerEvent::Connection(pending)) => {
20455 let PendingTcpSocket {
20456 stream,
20457 kernel_socket_id,
20458 preallocated,
20459 guest_local_addr,
20460 guest_remote_addr,
20461 } = pending;
20462 if !preallocated {
20463 if let Err(error) = check_network_resource_limit(
20464 resource_limits.max_sockets,
20465 network_counts.sockets,
20466 1,
20467 "socket",
20468 )
20469 .and_then(|()| {
20470 check_network_resource_limit(
20471 resource_limits.max_connections,
20472 network_counts.connections,
20473 1,
20474 "connection",
20475 )
20476 }) {
20477 if let Some(stream) = stream {
20478 let _ = stream.shutdown(Shutdown::Both);
20479 }
20480 return Ok(json!({
20481 "type": "error",
20482 "code": "EAGAIN",
20483 "message": error.to_string(),
20484 }));
20485 }
20486 }
20487 let socket = if let Some(stream) = stream {
20488 ActiveTcpSocket::from_stream(
20489 stream,
20490 Some(listener_id.to_string()),
20491 guest_local_addr,
20492 guest_remote_addr,
20493 )?
20494 } else {
20495 ActiveTcpSocket::from_kernel(
20496 kernel_socket_id.ok_or_else(|| {
20497 SidecarError::InvalidState(String::from(
20498 "kernel TCP accept missing socket id",
20499 ))
20500 })?,
20501 Some(listener_id.to_string()),
20502 guest_local_addr,
20503 guest_remote_addr,
20504 )
20505 };
20506 let socket_id = process.allocate_tcp_socket_id();
20507 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20508 listener.register_connection(&socket_id);
20509 }
20510 process.tcp_sockets.insert(socket_id.clone(), socket);
20511 Ok(json!({
20512 "type": "connection",
20513 "socketId": socket_id,
20514 "localAddress": guest_local_addr.ip().to_string(),
20515 "localPort": guest_local_addr.port(),
20516 "remoteAddress": guest_remote_addr.ip().to_string(),
20517 "remotePort": guest_remote_addr.port(),
20518 "remoteFamily": socket_addr_family(&guest_remote_addr),
20519 }))
20520 }
20521 Some(JavascriptTcpListenerEvent::Error { code, message }) => Ok(json!({
20522 "type": "error",
20523 "code": code,
20524 "message": message,
20525 })),
20526 None => Ok(Value::Null),
20527 };
20528 }
20529
20530 let event = {
20531 let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
20532 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20533 })?;
20534 listener.poll(Duration::from_millis(wait_ms))?
20535 };
20536
20537 match event {
20538 Some(JavascriptUnixListenerEvent::Connection(pending)) => {
20539 if let Err(error) = check_network_resource_limit(
20540 resource_limits.max_sockets,
20541 network_counts.sockets,
20542 1,
20543 "socket",
20544 )
20545 .and_then(|()| {
20546 check_network_resource_limit(
20547 resource_limits.max_connections,
20548 network_counts.connections,
20549 1,
20550 "connection",
20551 )
20552 }) {
20553 let _ = pending.stream.shutdown(Shutdown::Both);
20554 return Ok(json!({
20555 "type": "error",
20556 "code": "EAGAIN",
20557 "message": error.to_string(),
20558 }));
20559 }
20560 let socket = ActiveUnixSocket::from_stream(
20561 pending.stream,
20562 Some(listener_id.to_string()),
20563 pending.local_path.clone(),
20564 pending.remote_path.clone(),
20565 )?;
20566 let socket_id = process.allocate_unix_socket_id();
20567 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20568 listener.register_connection(&socket_id);
20569 }
20570 process.unix_sockets.insert(socket_id.clone(), socket);
20571 Ok(json!({
20572 "type": "connection",
20573 "socketId": socket_id,
20574 "localPath": pending.local_path,
20575 "remotePath": pending.remote_path,
20576 }))
20577 }
20578 Some(JavascriptUnixListenerEvent::Error { code, message }) => Ok(json!({
20579 "type": "error",
20580 "code": code,
20581 "message": message,
20582 })),
20583 None => Ok(Value::Null),
20584 }
20585 }
20586 "net.server_accept" => {
20587 let listener_id =
20588 javascript_sync_rpc_arg_str(&request.args, 0, "net.server_accept listener id")?;
20589 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20590 return match listener.poll(kernel, process.kernel_pid, Duration::ZERO)? {
20591 Some(JavascriptTcpListenerEvent::Connection(pending)) => {
20592 let PendingTcpSocket {
20593 stream,
20594 kernel_socket_id,
20595 preallocated,
20596 guest_local_addr,
20597 guest_remote_addr,
20598 } = pending;
20599 if !preallocated {
20600 check_network_resource_limit(
20601 resource_limits.max_sockets,
20602 network_counts.sockets,
20603 1,
20604 "socket",
20605 )?;
20606 check_network_resource_limit(
20607 resource_limits.max_connections,
20608 network_counts.connections,
20609 1,
20610 "connection",
20611 )?;
20612 }
20613 let info = json!({
20614 "localAddress": guest_local_addr.ip().to_string(),
20615 "localPort": guest_local_addr.port(),
20616 "localFamily": socket_addr_family(&guest_local_addr),
20617 "remoteAddress": guest_remote_addr.ip().to_string(),
20618 "remotePort": guest_remote_addr.port(),
20619 "remoteFamily": socket_addr_family(&guest_remote_addr),
20620 });
20621 let socket = if let Some(stream) = stream {
20622 ActiveTcpSocket::from_stream(
20623 stream,
20624 Some(listener_id.to_string()),
20625 guest_local_addr,
20626 guest_remote_addr,
20627 )?
20628 } else {
20629 ActiveTcpSocket::from_kernel(
20630 kernel_socket_id.ok_or_else(|| {
20631 SidecarError::InvalidState(String::from(
20632 "kernel TCP accept missing socket id",
20633 ))
20634 })?,
20635 Some(listener_id.to_string()),
20636 guest_local_addr,
20637 guest_remote_addr,
20638 )
20639 };
20640 let socket_id = process.allocate_tcp_socket_id();
20641 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20642 listener.register_connection(&socket_id);
20643 }
20644 process.tcp_sockets.insert(socket_id.clone(), socket);
20645 javascript_net_json_string(
20646 json!({
20647 "socketId": socket_id,
20648 "info": info,
20649 }),
20650 "net.server_accept",
20651 )
20652 }
20653 Some(JavascriptTcpListenerEvent::Error { code, message }) => {
20654 let detail = code.unwrap_or_else(|| String::from("server accept"));
20655 Err(SidecarError::Execution(format!("{detail}: {message}")))
20656 }
20657 None => Ok(javascript_net_timeout_value()),
20658 };
20659 }
20660
20661 let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
20662 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20663 })?;
20664 match listener.poll(Duration::ZERO)? {
20665 Some(JavascriptUnixListenerEvent::Connection(pending)) => {
20666 check_network_resource_limit(
20667 resource_limits.max_sockets,
20668 network_counts.sockets,
20669 1,
20670 "socket",
20671 )?;
20672 check_network_resource_limit(
20673 resource_limits.max_connections,
20674 network_counts.connections,
20675 1,
20676 "connection",
20677 )?;
20678 let info = json!({
20679 "localPath": pending.local_path.clone(),
20680 "remotePath": pending.remote_path.clone(),
20681 });
20682 let socket = ActiveUnixSocket::from_stream(
20683 pending.stream,
20684 Some(listener_id.to_string()),
20685 pending.local_path,
20686 pending.remote_path,
20687 )?;
20688 let socket_id = process.allocate_unix_socket_id();
20689 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20690 listener.register_connection(&socket_id);
20691 }
20692 process.unix_sockets.insert(socket_id.clone(), socket);
20693 javascript_net_json_string(
20694 json!({
20695 "socketId": socket_id,
20696 "info": info,
20697 }),
20698 "net.server_accept",
20699 )
20700 }
20701 Some(JavascriptUnixListenerEvent::Error { code, message }) => {
20702 let detail = code.unwrap_or_else(|| String::from("server accept"));
20703 Err(SidecarError::Execution(format!("{detail}: {message}")))
20704 }
20705 None => Ok(javascript_net_timeout_value()),
20706 }
20707 }
20708 "net.server_connections" => {
20709 let listener_id = javascript_sync_rpc_arg_str(
20710 &request.args,
20711 0,
20712 "net.server_connections listener id",
20713 )?;
20714 if let Some(listener) = process.tcp_listeners.get(listener_id) {
20715 Ok(json!(listener.active_connection_count()))
20716 } else {
20717 let listener = process.unix_listeners.get(listener_id).ok_or_else(|| {
20718 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20719 })?;
20720 Ok(json!(listener.active_connection_count()))
20721 }
20722 }
20723 "net.upgrade_socket_write" => {
20724 let socket_id = javascript_sync_rpc_arg_str(
20725 &request.args,
20726 0,
20727 "net.upgrade_socket_write socket id",
20728 )?;
20729 let chunk =
20730 javascript_sync_rpc_base64_arg(&request.args, 1, "net.upgrade_socket_write chunk")?;
20731 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20732 SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20733 })?;
20734 socket
20735 .write_all(kernel, process.kernel_pid, &chunk)
20736 .map(|written| json!(written))
20737 }
20738 "net.upgrade_socket_end" => {
20739 let socket_id =
20740 javascript_sync_rpc_arg_str(&request.args, 0, "net.upgrade_socket_end socket id")?;
20741 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20742 SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20743 })?;
20744 socket.shutdown_write(kernel, process.kernel_pid)?;
20745 Ok(Value::Null)
20746 }
20747 "net.upgrade_socket_destroy" => {
20748 let socket_id = javascript_sync_rpc_arg_str(
20749 &request.args,
20750 0,
20751 "net.upgrade_socket_destroy socket id",
20752 )?;
20753 let socket = process.tcp_sockets.remove(socket_id).ok_or_else(|| {
20754 SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20755 })?;
20756 if let Some(listener_id) = socket.listener_id.as_deref() {
20757 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20758 listener.release_connection(socket_id);
20759 }
20760 }
20761 let _ = socket.close(kernel, process.kernel_pid);
20762 Ok(Value::Null)
20763 }
20764 "net.write" => {
20765 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.write socket id")?;
20766 let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "net.write chunk")?;
20767 if let Some(socket) = process.tcp_sockets.get(socket_id) {
20768 socket
20769 .write_all(kernel, process.kernel_pid, &chunk)
20770 .map(|written| json!(written))
20771 } else {
20772 let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20773 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20774 })?;
20775 socket.write_all(&chunk).map(|written| json!(written))
20776 }
20777 }
20778 "net.shutdown" => {
20779 let socket_id =
20780 javascript_sync_rpc_arg_str(&request.args, 0, "net.shutdown socket id")?;
20781 if let Some(socket) = process.tcp_sockets.get(socket_id) {
20782 socket.shutdown_write(kernel, process.kernel_pid)?;
20783 } else {
20784 let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20785 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20786 })?;
20787 socket.shutdown_write()?;
20788 }
20789 Ok(Value::Null)
20790 }
20791 "net.destroy" => {
20792 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.destroy socket id")?;
20793 if let Some(socket) = process.tcp_sockets.remove(socket_id) {
20794 if let Some(listener_id) = socket.listener_id.as_deref() {
20795 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20796 listener.release_connection(socket_id);
20797 }
20798 }
20799 let _ = socket.close(kernel, process.kernel_pid);
20800 Ok(Value::Null)
20801 } else {
20802 let socket = process.unix_sockets.remove(socket_id).ok_or_else(|| {
20803 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20804 })?;
20805 if let Some(listener_id) = socket.listener_id.as_deref() {
20806 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20807 listener.release_connection(socket_id);
20808 }
20809 }
20810 let _ = socket.close();
20811 Ok(Value::Null)
20812 }
20813 }
20814 "net.server_close" => {
20815 let listener_id =
20816 javascript_sync_rpc_arg_str(&request.args, 0, "net.server_close listener id")?;
20817 if let Some(listener) = process.tcp_listeners.remove(listener_id) {
20818 listener.close(kernel, process.kernel_pid)?;
20819 Ok(Value::Null)
20820 } else {
20821 let listener = process.unix_listeners.remove(listener_id).ok_or_else(|| {
20822 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20823 })?;
20824 listener.close()?;
20825 Ok(Value::Null)
20826 }
20827 }
20828 "tls.get_ciphers" => javascript_net_json_string(
20829 Value::Array(
20830 tls_provider()
20831 .cipher_suites
20832 .iter()
20833 .filter_map(|suite| {
20834 suite
20835 .suite()
20836 .as_str()
20837 .map(|value| Value::String(value.to_owned()))
20838 })
20839 .collect(),
20840 ),
20841 "tls.get_ciphers",
20842 ),
20843 _ => Err(SidecarError::InvalidState(format!(
20844 "unsupported JavaScript net sync RPC method {}",
20845 request.method
20846 ))),
20847 }
20848}
20849
20850fn signal_name_for_stream_event(signal: i32) -> Option<&'static str> {
20851 match signal {
20852 libc::SIGHUP => Some("SIGHUP"),
20853 libc::SIGINT => Some("SIGINT"),
20854 libc::SIGUSR1 => Some("SIGUSR1"),
20855 libc::SIGALRM => Some("SIGALRM"),
20856 libc::SIGCONT => Some("SIGCONT"),
20857 libc::SIGTERM => Some("SIGTERM"),
20858 libc::SIGCHLD => Some("SIGCHLD"),
20859 libc::SIGWINCH => Some("SIGWINCH"),
20860 _ => None,
20861 }
20862}
20863
20864pub(crate) fn canonical_signal_name(signal: i32) -> Option<&'static str> {
20865 match signal {
20866 1 => Some("SIGHUP"),
20867 2 => Some("SIGINT"),
20868 3 => Some("SIGQUIT"),
20869 4 => Some("SIGILL"),
20870 5 => Some("SIGTRAP"),
20871 6 => Some("SIGABRT"),
20872 7 => Some("SIGBUS"),
20873 8 => Some("SIGFPE"),
20874 9 => Some("SIGKILL"),
20875 10 => Some("SIGUSR1"),
20876 11 => Some("SIGSEGV"),
20877 12 => Some("SIGUSR2"),
20878 13 => Some("SIGPIPE"),
20879 14 => Some("SIGALRM"),
20880 15 => Some("SIGTERM"),
20881 17 => Some("SIGCHLD"),
20882 18 => Some("SIGCONT"),
20883 19 => Some("SIGSTOP"),
20884 20 => Some("SIGTSTP"),
20885 21 => Some("SIGTTIN"),
20886 22 => Some("SIGTTOU"),
20887 23 => Some("SIGURG"),
20888 24 => Some("SIGXCPU"),
20889 25 => Some("SIGXFSZ"),
20890 26 => Some("SIGVTALRM"),
20891 27 => Some("SIGPROF"),
20892 28 => Some("SIGWINCH"),
20893 29 => Some("SIGIO"),
20894 30 => Some("SIGPWR"),
20895 31 => Some("SIGSYS"),
20896 _ => None,
20897 }
20898}
20899
20900fn dispatch_v8_process_signal(process: &ActiveProcess, signal: i32) -> Result<bool, SidecarError> {
20901 let Some(signal_name) = signal_name_for_stream_event(signal) else {
20902 return Ok(false);
20903 };
20904 process.execution.send_javascript_stream_event(
20905 "signal",
20906 json!({
20907 "signal": signal_name,
20908 "number": signal,
20909 "action": "default",
20910 }),
20911 )?;
20912 Ok(true)
20913}
20914
20915fn dispatch_v8_session_signal_async(session: V8SessionHandle, signal: i32) {
20916 let Some(signal_name) = signal_name_for_stream_event(signal).map(str::to_owned) else {
20917 return;
20918 };
20919 thread::spawn(move || {
20920 thread::sleep(Duration::from_millis(1));
20921 let payload = v8_runtime::json_to_cbor_payload(&json!({
20922 "signal": signal_name,
20923 "number": signal,
20924 "action": "default",
20925 }))
20926 .unwrap_or_default();
20927 let _ = session.send_stream_event("signal", payload);
20928 });
20929}
20930
20931pub(crate) fn parse_signal(signal: &str) -> Result<i32, SidecarError> {
20932 let trimmed = signal.trim();
20933 if trimmed.is_empty() {
20934 return Err(SidecarError::InvalidState(String::from(
20935 "kill_process requires a non-empty signal",
20936 )));
20937 }
20938
20939 if let Ok(value) = trimmed.parse::<i32>() {
20940 return match value {
20941 0..=31 => Ok(value),
20942 _ => Err(SidecarError::InvalidState(format!(
20943 "unsupported kill_process signal {signal}"
20944 ))),
20945 };
20946 }
20947
20948 let upper = trimmed.to_ascii_uppercase();
20949 let normalized = upper.strip_prefix("SIG").unwrap_or(&upper);
20950
20951 signal_number_from_name(normalized).ok_or_else(|| {
20952 SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
20953 })
20954}
20955
20956fn signal_number_from_name(signal: &str) -> Option<i32> {
20957 match signal {
20958 "0" => Some(0),
20959 "HUP" => Some(1),
20960 "INT" => Some(2),
20961 "QUIT" => Some(3),
20962 "ILL" => Some(4),
20963 "TRAP" => Some(5),
20964 "ABRT" | "IOT" => Some(6),
20965 "BUS" => Some(7),
20966 "FPE" => Some(8),
20967 "KILL" => Some(9),
20968 "USR1" => Some(10),
20969 "SEGV" => Some(11),
20970 "USR2" => Some(12),
20971 "PIPE" => Some(13),
20972 "ALRM" => Some(14),
20973 "TERM" => Some(15),
20974 "STKFLT" => Some(16),
20975 "CHLD" => Some(17),
20976 "CONT" => Some(18),
20977 "STOP" => Some(19),
20978 "TSTP" => Some(20),
20979 "TTIN" => Some(21),
20980 "TTOU" => Some(22),
20981 "URG" => Some(23),
20982 "XCPU" => Some(24),
20983 "XFSZ" => Some(25),
20984 "VTALRM" => Some(26),
20985 "PROF" => Some(27),
20986 "WINCH" => Some(28),
20987 "IO" | "POLL" => Some(29),
20988 "PWR" => Some(30),
20989 "SYS" => Some(31),
20990 _ => None,
20991 }
20992}
20993
20994pub(crate) fn runtime_child_is_alive(child_pid: u32) -> Result<bool, SidecarError> {
20995 Ok(runtime_child_exit_status(child_pid)?.is_none())
20996}
20997
20998#[cfg(not(target_os = "macos"))]
20999fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
21000 if child_pid == 0 {
21001 return Ok(Some(0));
21002 }
21003
21004 let wait_flags = WaitPidFlag::WNOHANG
21005 | WaitPidFlag::WNOWAIT
21006 | WaitPidFlag::WEXITED
21007 | WaitPidFlag::WUNTRACED
21008 | WaitPidFlag::WCONTINUED;
21009 match wait_on_child(WaitId::Pid(Pid::from_raw(child_pid as i32)), wait_flags) {
21010 Ok(WaitStatus::StillAlive)
21011 | Ok(WaitStatus::Stopped(_, _))
21012 | Ok(WaitStatus::Continued(_)) => Ok(None),
21013 Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
21014 Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
21015 #[cfg(any(target_os = "linux", target_os = "android"))]
21016 Ok(WaitStatus::PtraceEvent(_, _, _) | WaitStatus::PtraceSyscall(_)) => Ok(None),
21017 Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
21018 Err(error) => Err(SidecarError::Execution(format!(
21019 "failed to inspect guest runtime process {child_pid}: {error}"
21020 ))),
21021 }
21022}
21023
21024#[cfg(target_os = "macos")]
21030fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
21031 if child_pid == 0 {
21032 return Ok(Some(0));
21033 }
21034
21035 match waitpid(Pid::from_raw(child_pid as i32), Some(WaitPidFlag::WNOHANG)) {
21036 Ok(WaitStatus::StillAlive)
21037 | Ok(WaitStatus::Stopped(_, _))
21038 | Ok(WaitStatus::Continued(_)) => Ok(None),
21039 Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
21040 Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
21041 Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
21042 Err(error) => Err(SidecarError::Execution(format!(
21043 "failed to inspect guest runtime process {child_pid}: {error}"
21044 ))),
21045 }
21046}
21047
21048pub(crate) fn signal_runtime_process(child_pid: u32, signal: i32) -> Result<(), SidecarError> {
21049 if child_pid == 0 {
21050 return Ok(());
21051 }
21052
21053 if !runtime_child_is_alive(child_pid)? {
21054 return Ok(());
21055 }
21056
21057 if signal == 0 {
21058 return Ok(());
21059 }
21060
21061 let parsed = Signal::try_from(signal).map_err(|_| {
21062 SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
21063 })?;
21064 let result = send_signal(Pid::from_raw(child_pid as i32), Some(parsed));
21065
21066 match result {
21067 Ok(()) => Ok(()),
21068 Err(nix::errno::Errno::ESRCH) => Ok(()),
21069 Err(error) => Err(SidecarError::Execution(format!(
21070 "failed to signal guest runtime process {child_pid}: {error}"
21071 ))),
21072 }
21073}
21074
21075pub(crate) fn error_code(error: &SidecarError) -> &'static str {
21076 match error {
21077 SidecarError::InvalidState(_) => "invalid_state",
21078 SidecarError::ProtocolVersionMismatch(_) => "protocol_version_mismatch",
21079 SidecarError::BridgeVersionMismatch(_) => "bridge_version_mismatch",
21080 SidecarError::Conflict(_) => "conflict",
21081 SidecarError::Unauthorized(_) => "unauthorized",
21082 SidecarError::Unsupported(_) => "unsupported",
21083 SidecarError::FrameTooLarge(_) => "frame_too_large",
21084 SidecarError::Kernel(_) => "kernel_error",
21085 SidecarError::Plugin(_) => "plugin_error",
21086 SidecarError::Execution(_) => "execution_error",
21087 SidecarError::Bridge(_) => "bridge_error",
21088 SidecarError::Io(_) => "io_error",
21089 }
21090}
21091
21092fn guest_errno_code(message: &str) -> Option<&str> {
21093 const TRUSTED_PREFIXES: &[&str] = &[
21094 "ERR_AGENT_OS_NODE_SYNC_RPC",
21095 "ERR_AGENT_OS_PYTHON_VFS_RPC",
21096 "ERR_AGENT_OS_BRIDGE",
21097 ];
21098
21099 let mut segments = message.split(':').map(str::trim);
21100 let first = segments.next()?;
21101 if is_guest_errno_segment(first) {
21102 return Some(first);
21103 }
21104
21105 if TRUSTED_PREFIXES.contains(&first) {
21106 let second = segments.next()?;
21107 if is_guest_errno_segment(second) {
21108 return Some(second);
21109 }
21110 }
21111
21112 None
21113}
21114
21115fn is_guest_errno_segment(segment: &str) -> bool {
21116 segment.len() >= 2
21117 && segment.starts_with('E')
21118 && !segment.starts_with("ERR_")
21119 && segment[1..]
21120 .bytes()
21121 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
21122}
21123
21124pub(crate) fn javascript_sync_rpc_error_code(error: &SidecarError) -> String {
21125 let message = error.to_string();
21126 if let Some(code) = guest_errno_code(&message) {
21127 return code.to_owned();
21128 }
21129 if message.starts_with("ERR_NATIVE_BINARY_NOT_SUPPORTED:") {
21130 return String::from("ERR_NATIVE_BINARY_NOT_SUPPORTED");
21131 }
21132
21133 let lower = message.to_ascii_lowercase();
21134 if lower.contains("no such file or directory")
21135 || lower.contains("entry not found")
21136 || lower.contains("not found")
21137 {
21138 return String::from("ENOENT");
21139 }
21140 if lower.contains("permission denied") {
21141 return String::from("EACCES");
21142 }
21143 if lower.contains("already exists")
21144 || lower.contains("already registered")
21145 || lower.contains("file exists")
21146 {
21147 return String::from("EEXIST");
21148 }
21149 if lower.contains("invalid argument") {
21150 return String::from("EINVAL");
21151 }
21152
21153 String::from("ERR_AGENT_OS_NODE_SYNC_RPC")
21154}
21155
21156pub(crate) fn ignore_stale_javascript_sync_rpc_response(
21157 error: SidecarError,
21158) -> Result<(), SidecarError> {
21159 match error {
21160 SidecarError::Execution(message)
21161 if message.ends_with("is no longer pending")
21162 && message.starts_with("sync RPC request ") =>
21163 {
21164 Ok(())
21165 }
21166 SidecarError::Execution(message) => {
21167 let lower = message.to_ascii_lowercase();
21168 if lower.contains("sync rpc response")
21169 && (lower.contains("broken pipe") || lower.contains("channel closed unexpectedly"))
21170 {
21171 Ok(())
21172 } else {
21173 Err(SidecarError::Execution(message))
21174 }
21175 }
21176 other => Err(other),
21177 }
21178}
21179
21180#[cfg(test)]
21181mod error_code_tests {
21182 use super::{guest_errno_code, javascript_sync_rpc_error_code, SidecarError};
21183
21184 #[test]
21185 fn guest_errno_code_rejects_guest_controlled_errno_segments() {
21186 assert_eq!(guest_errno_code("user said 'EACCES: denied'"), None);
21187 assert_eq!(
21188 guest_errno_code("prefix: user said 'EPERM': more text"),
21189 None
21190 );
21191 assert_eq!(guest_errno_code("ERR_AGENT_OS_FAKE: EACCES: denied"), None);
21192 }
21193
21194 #[test]
21195 fn guest_errno_code_accepts_trusted_secure_exec_prefixes() {
21196 assert_eq!(
21197 guest_errno_code("ERR_AGENT_OS_NODE_SYNC_RPC: EACCES: permission denied on /foo"),
21198 Some("EACCES")
21199 );
21200 assert_eq!(
21201 guest_errno_code("ERR_AGENT_OS_PYTHON_VFS_RPC: ENOENT: missing file"),
21202 Some("ENOENT")
21203 );
21204 assert_eq!(guest_errno_code("EEXIST: already exists"), Some("EEXIST"));
21205 }
21206
21207 #[test]
21208 fn javascript_sync_rpc_error_code_ignores_spoofed_errnos() {
21209 let error = SidecarError::Execution(String::from("user said 'EACCES: denied'"));
21210 assert_eq!(
21211 javascript_sync_rpc_error_code(&error),
21212 "ERR_AGENT_OS_NODE_SYNC_RPC"
21213 );
21214 }
21215
21216 #[test]
21217 fn javascript_sync_rpc_error_code_preserves_real_sidecar_errnos() {
21218 let error = SidecarError::Execution(String::from(
21219 "ERR_AGENT_OS_NODE_SYNC_RPC: EACCES: permission denied on /foo",
21220 ));
21221 assert_eq!(javascript_sync_rpc_error_code(&error), "EACCES");
21222 }
21223
21224 #[test]
21225 fn javascript_sync_rpc_error_code_maps_file_exists_messages() {
21226 let error = SidecarError::Io(String::from(
21227 "failed to create mapped guest directory /.next/server: File exists (os error 17)",
21228 ));
21229 assert_eq!(javascript_sync_rpc_error_code(&error), "EEXIST");
21230 }
21231
21232 #[test]
21233 fn javascript_sync_rpc_error_code_preserves_native_binary_rejections() {
21234 let error = SidecarError::Execution(String::from(
21235 "ERR_NATIVE_BINARY_NOT_SUPPORTED: refused to execute native ELF guest binary at /tmp/fake-rg inside the VM",
21236 ));
21237 assert_eq!(
21238 javascript_sync_rpc_error_code(&error),
21239 "ERR_NATIVE_BINARY_NOT_SUPPORTED"
21240 );
21241 }
21242}
21243#[cfg(test)]
21244mod ssrf_egress_classifier_tests {
21245 use super::{
21255 filter_dns_safe_ip_addrs, is_loopback_ip, restricted_non_loopback_ip_range, SidecarError,
21256 };
21257 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
21258
21259 fn assert_restricted(ip: IpAddr, expected_label: &str) {
21260 let classification = restricted_non_loopback_ip_range(ip);
21261 assert!(
21262 classification.is_some(),
21263 "{ip} must be classified as a restricted egress target"
21264 );
21265 let (_cidr, label) = classification.unwrap();
21266 assert_eq!(
21267 label, expected_label,
21268 "{ip} should be labelled {expected_label}, got {label}"
21269 );
21270 }
21271
21272 fn assert_dns_denied(ip: IpAddr, label: &str) {
21273 match filter_dns_safe_ip_addrs(vec![ip], "attacker.example") {
21274 Err(SidecarError::Execution(message)) => assert!(
21275 message.starts_with("EACCES:"),
21276 "{label}: egress filter must deny with EACCES, got: {message}"
21277 ),
21278 other => panic!("{label}: expected EACCES denial, got {other:?}"),
21279 }
21280 }
21281
21282 #[test]
21284 fn classifier_denies_unspecified_and_cgnat_targets() {
21285 assert_restricted(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "unspecified");
21287 assert_restricted(IpAddr::V6(Ipv6Addr::UNSPECIFIED), "unspecified");
21289
21290 assert_restricted(
21292 IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
21293 "carrier-grade-nat",
21294 );
21295 assert_restricted(
21296 IpAddr::V4(Ipv4Addr::new(100, 127, 255, 254)),
21297 "carrier-grade-nat",
21298 );
21299
21300 assert!(
21302 restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 63, 255, 255)))
21303 .is_none(),
21304 "100.63.255.255 is outside CGNAT and must remain allowed"
21305 );
21306 assert!(
21307 restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 128, 0, 0))).is_none(),
21308 "100.128.0.0 is outside CGNAT and must remain allowed"
21309 );
21310
21311 assert_dns_denied(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "0.0.0.0 (unspecified)");
21313 assert_dns_denied(IpAddr::V6(Ipv6Addr::UNSPECIFIED), ":: (unspecified)");
21314 assert_dns_denied(
21315 IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
21316 "100.64.0.1 (CGNAT)",
21317 );
21318 }
21319
21320 #[test]
21322 fn classifier_denies_ipv6_spelled_metadata_addresses() {
21323 let mapped = "::ffff:169.254.169.254".parse::<Ipv6Addr>().unwrap();
21326 assert_restricted(IpAddr::V6(mapped), "link-local");
21327
21328 let compat = "::169.254.169.254".parse::<Ipv6Addr>().unwrap();
21329 assert_restricted(IpAddr::V6(compat), "link-local");
21330
21331 assert_restricted(
21333 IpAddr::V6("::10.0.0.1".parse::<Ipv6Addr>().unwrap()),
21334 "private",
21335 );
21336 assert_restricted(
21337 IpAddr::V6("::100.64.0.1".parse::<Ipv6Addr>().unwrap()),
21338 "carrier-grade-nat",
21339 );
21340
21341 assert_eq!(
21345 restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::UNSPECIFIED)),
21346 Some(("::/128", "unspecified")),
21347 ":: must classify as unspecified, not via the IPv4-compat path"
21348 );
21349 assert!(
21350 restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::LOCALHOST)).is_none()
21351 || is_loopback_ip(IpAddr::V6(Ipv6Addr::LOCALHOST)),
21352 "::1 must not be classified as a restricted IPv4-compatible target"
21353 );
21354 assert!(
21355 restricted_non_loopback_ip_range(IpAddr::V6("::8.8.8.8".parse::<Ipv6Addr>().unwrap()))
21356 .is_none(),
21357 "::8.8.8.8 (public IPv4-compatible) must remain allowed"
21358 );
21359
21360 assert_dns_denied(
21362 IpAddr::V6("::169.254.169.254".parse::<Ipv6Addr>().unwrap()),
21363 "::169.254.169.254 (IPv4-compat metadata)",
21364 );
21365 }
21366
21367 #[test]
21369 fn classifier_denies_reserved_and_multicast_targets() {
21370 assert_restricted(IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)), "multicast");
21374 assert_restricted(IpAddr::V4(Ipv4Addr::new(239, 255, 255, 255)), "multicast");
21375 assert_restricted(IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)), "reserved");
21376 assert_restricted(IpAddr::V4(Ipv4Addr::BROADCAST), "reserved");
21378
21379 assert_restricted(
21381 IpAddr::V6("::224.0.0.1".parse::<Ipv6Addr>().unwrap()),
21382 "multicast",
21383 );
21384 assert_restricted(
21385 IpAddr::V6("::240.0.0.1".parse::<Ipv6Addr>().unwrap()),
21386 "reserved",
21387 );
21388
21389 assert!(
21391 restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(223, 255, 255, 255)))
21392 .is_none(),
21393 "223.255.255.255 is outside 224/4 and must remain allowed"
21394 );
21395
21396 assert_dns_denied(
21398 IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)),
21399 "240.0.0.1 (reserved)",
21400 );
21401 assert_dns_denied(
21402 IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)),
21403 "224.0.0.1 (multicast)",
21404 );
21405 }
21406}
21407
21408#[cfg(test)]
21417mod dns_rebinding_pin_tests {
21418 use super::{issue_outbound_http_request, split_netloc, JavascriptHttpRequestOptions};
21419 use std::collections::BTreeMap;
21420 use std::io::{Read, Write};
21421 use std::net::{IpAddr, Ipv4Addr, TcpListener};
21422 use std::thread;
21423 use url::Url;
21424
21425 fn empty_headers() -> super::HttpHeaderCollection {
21426 super::parse_http_header_collection(&BTreeMap::new(), "test headers")
21427 .expect("empty header collection")
21428 }
21429
21430 fn options() -> JavascriptHttpRequestOptions {
21431 JavascriptHttpRequestOptions {
21432 method: Some(String::from("GET")),
21433 headers: BTreeMap::new(),
21434 body: None,
21435 reject_unauthorized: None,
21436 }
21437 }
21438
21439 #[test]
21440 fn split_netloc_handles_hostnames_and_bracketed_ipv6() {
21441 assert_eq!(
21442 split_netloc("attacker.example:80"),
21443 Some(("attacker.example", 80))
21444 );
21445 assert_eq!(split_netloc("[::1]:443"), Some(("::1", 443)));
21446 assert_eq!(split_netloc("10.0.0.1:8080"), Some(("10.0.0.1", 8080)));
21447 assert_eq!(split_netloc("no-port"), None);
21448 assert_eq!(split_netloc("host:notaport"), None);
21449 }
21450
21451 #[test]
21457 fn outbound_http_connect_is_pinned_to_vetted_ip() {
21458 let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind loopback server");
21459 let port = listener.local_addr().expect("local addr").port();
21460 let server = thread::spawn(move || {
21461 let (mut stream, _) = listener.accept().expect("accept");
21462 let mut buf = [0u8; 1024];
21463 let _ = stream.read(&mut buf);
21464 stream
21465 .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi")
21466 .expect("write response");
21467 let _ = stream.flush();
21468 });
21469
21470 let url = Url::parse(&format!("http://attacker.example:{port}/")).expect("url");
21471 let pinned = vec![IpAddr::V4(Ipv4Addr::LOCALHOST)];
21472 let result = issue_outbound_http_request(&url, &options(), &empty_headers(), &pinned)
21473 .expect("pinned request should reach the vetted loopback target");
21474 let payload = result.as_str().expect("string payload");
21475 assert!(
21476 payload.contains("\"status\":200"),
21477 "expected 200 from pinned target, got: {payload}"
21478 );
21479 server.join().expect("server thread");
21480 }
21481
21482 #[test]
21486 fn outbound_http_refuses_when_no_vetted_address() {
21487 let url = Url::parse("https://attacker.example/").expect("url");
21488 let error = issue_outbound_http_request(&url, &options(), &empty_headers(), &[])
21489 .expect_err("empty pinned set must be refused");
21490 let message = error.to_string();
21491 assert!(
21492 message.contains("EACCES") || message.contains("ERR_HTTP_REQUEST_FAILED"),
21493 "expected an egress refusal, got: {message}"
21494 );
21495 }
21496}