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_read_sync_rpc, service_javascript_fs_readdir_raw_sync_rpc,
8 service_javascript_fs_sync_rpc, service_javascript_module_sync_rpc,
9};
10use crate::protocol::{
11 CloseStdinRequest, EventFrame, EventPayload, ExecuteRequest, FindBoundUdpRequest,
12 FindListenerRequest, GetProcessSnapshotRequest, GetResourceSnapshotRequest,
13 GetSignalStateRequest, GetZombieTimerCountRequest, GuestKernelCallRequest,
14 GuestKernelResultResponse, GuestRuntimeKind, JavascriptChildProcessSpawnOptions,
15 JavascriptChildProcessSpawnRequest, JavascriptDgramBindRequest,
16 JavascriptDgramCreateSocketRequest, JavascriptDgramSendRequest, JavascriptDnsLookupRequest,
17 JavascriptDnsResolveRequest, JavascriptNetConnectRequest, JavascriptNetListenRequest,
18 JavascriptNetReserveTcpPortRequest, KillProcessRequest, OwnershipScope, ProcessExitedEvent,
19 ProcessOutputEvent, ProcessSnapshotEntry, ProcessSnapshotStatus, PtyResizedResponse,
20 QueueSnapshotEntry, RequestFrame, ResizePtyRequest, ResourceSnapshotResponse, ResponseFrame,
21 ResponsePayload, SidecarRequestPayload, SignalDispositionAction, SignalHandlerRegistration,
22 SocketStateEntry, StreamChannel, VmFetchRequest, VmFetchResponse, WasmPermissionTier,
23 WriteStdinRequest,
24};
25use crate::service::{
26 audit_fields, dirname, emit_security_audit_event, emit_structured_event, javascript_error,
27 kernel_error, log_stale_process_event, normalize_host_path, normalize_path,
28 parse_javascript_child_process_spawn_request, path_is_within_root,
29 process_event_queue_overflow_error, python_error, wasm_error, MAX_PROCESS_EVENT_QUEUE,
30};
31use crate::state::{
32 ActiveCipherSession, ActiveDhSession, ActiveDiffieHellmanSession, ActiveEcdhSession,
33 ActiveExecution, ActiveExecutionEvent, ActiveHttp2Server, ActiveHttp2Session,
34 ActiveHttp2Stream, ActiveHttpServer, ActiveMappedHostFd, ActiveProcess, ActiveSqliteDatabase,
35 ActiveSqliteStatement, ActiveTcpListener, ActiveTcpSocket, ActiveTlsState, ActiveTlsStream,
36 ActiveUdpSocket, ActiveUnixListener, ActiveUnixSocket, BridgeError, ExitedProcessSnapshot,
37 Http2BridgeEvent, Http2RuntimeSnapshot, Http2SessionCommand, Http2SessionSnapshot,
38 Http2SocketSnapshot, JavascriptHttpLoopbackTarget, JavascriptSocketEventPusher,
39 JavascriptSocketFamily, JavascriptSocketPathContext, JavascriptTcpListenerEvent,
40 JavascriptTcpSocketEvent, JavascriptTlsBridgeOptions, JavascriptTlsClientHello,
41 JavascriptTlsDataValue, JavascriptTlsMaterial, JavascriptUdpFamily, JavascriptUdpSocketEvent,
42 JavascriptUnixListenerEvent, KernelSocketReadinessEvent, KernelSocketReadinessRegistry,
43 KernelSocketReadinessTarget, LoopbackTlsPendingWriteHandle, LoopbackTlsPendingWriteState,
44 NetworkResourceCounts, PendingTcpSocket, PendingUnixSocket, ProcNetEntry, ProcessEventEnvelope,
45 PythonHostSocket, ResolvedChildProcessExecution, ResolvedTcpConnectAddr, SharedBridge,
46 SharedSidecarRequestClient, SidecarKernel, SocketQueryKind, ToolExecution, VmDnsConfig,
47 VmListenPolicy, VmState, DEFAULT_JAVASCRIPT_NET_BACKLOG, EXECUTION_DRIVER_NAME,
48 EXECUTION_SANDBOX_ROOT_ENV, JAVASCRIPT_COMMAND, LOOPBACK_EXEMPT_PORTS_ENV,
49 MAPPED_HOST_FD_START, PYTHON_COMMAND, TOOL_DRIVER_NAME,
50 VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY, WASM_COMMAND, WASM_STDIO_SYNC_RPC_ENV,
51};
52use crate::tools::{
53 format_tool_failure_output, is_tool_command, normalized_tool_command_name,
54 resolve_tool_command, ToolCommandResolution,
55};
56use crate::wire::{ProtocolFrame as WireProtocolFrame, WireFrameCodec};
57use crate::{DispatchResult, NativeSidecar, NativeSidecarBridge, SidecarError};
58
59use base64::Engine;
60use bytes::Bytes;
61use h2::{client, server, Reason};
62use hickory_resolver::proto::rr::{RData, Record, RecordType};
63use hmac::{Hmac, Mac};
64use http::{HeaderMap, HeaderName, HeaderValue, Method, Request, Response, Uri};
65use md5::Md5;
66use nix::libc;
67use nix::poll::{poll, PollFd as NixPollFd, PollFlags, PollTimeout};
68use nix::sys::signal::{kill as send_signal, Signal};
69use nix::sys::wait::WaitStatus;
70#[cfg(not(target_os = "macos"))]
71use nix::sys::wait::{waitid as wait_on_child, Id as WaitId, WaitPidFlag};
72#[cfg(target_os = "macos")]
73use nix::sys::wait::{waitpid, WaitPidFlag};
74use nix::unistd::Pid;
75use openssl::bn::{BigNum, BigNumContext};
76use openssl::derive::Deriver;
77use openssl::dh::Dh;
78use openssl::ec::{EcGroup, EcKey, EcPoint, PointConversionForm};
79use openssl::hash::MessageDigest;
80use openssl::nid::Nid;
81use openssl::pkey::{Id as PKeyId, PKey, Params, Private, Public};
82use openssl::rand::rand_bytes;
83use openssl::rsa::{Padding, Rsa};
84use openssl::sign::{Signer, Verifier};
85use pbkdf2::pbkdf2_hmac;
86
87use crate::crypto_cipher::{CipherError as AesCipherError, StreamCipherSession};
88use rusqlite::types::ValueRef as SqliteValueRef;
89use rusqlite::{
90 Connection as SqliteConnection, OpenFlags as SqliteOpenFlags, Statement as SqliteStatement,
91};
92use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
93use rustls::crypto::aws_lc_rs;
94use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
95use rustls::{
96 ClientConfig, ClientConnection, DigitallySignedStruct, RootCertStore, ServerConfig,
97 ServerConnection, SignatureScheme,
98};
99use scrypt::{scrypt, Params as ScryptParams};
100use secure_exec_bridge::{queue_tracker, LifecycleState};
101use secure_exec_execution::wasm::WasmExecutionError;
102use secure_exec_execution::{
103 javascript::handle_internal_bridge_call_from_host_context, v8_host::V8SessionHandle,
104 v8_runtime, CreateJavascriptContextRequest, CreatePythonContextRequest,
105 CreateWasmContextRequest, GuestModuleReader, GuestRuntimeConfig, JavascriptExecutionEvent,
106 JavascriptExecutionLimits, JavascriptSyncRpcRequest, ModuleFsReader,
107 NodeSignalDispositionAction, NodeSignalHandlerRegistration, PythonExecutionEvent,
108 PythonExecutionLimits, PythonVfsRpcMethod, PythonVfsRpcRequest, PythonVfsRpcResponsePayload,
109 StartJavascriptExecutionRequest, StartPythonExecutionRequest, StartWasmExecutionRequest,
110 WasmExecutionEvent, WasmExecutionLimits, WasmPermissionTier as ExecutionWasmPermissionTier,
111};
112use secure_exec_kernel::dns::{
113 DnsLookupPolicy, DnsRecordResolution, DnsResolutionSource as KernelDnsResolutionSource,
114};
115use secure_exec_kernel::kernel::{KernelProcessHandle, SpawnOptions, VirtualProcessOptions};
116pub(crate) use secure_exec_kernel::network_policy::format_tcp_resource;
117use secure_exec_kernel::network_policy::{
118 is_loopback_ip, loopback_cidr, restricted_non_loopback_ip_range,
119};
120use secure_exec_kernel::permissions::NetworkOperation;
121use secure_exec_kernel::poll::{PollEvents, PollFd, PollTargetEntry, POLLERR, POLLHUP, POLLIN};
122use secure_exec_kernel::process_table::{ProcessStatus, WaitPidFlags, SIGKILL, SIGTERM};
123use secure_exec_kernel::pty::{LineDisciplineConfig, MAX_PTY_BUFFER_BYTES};
124use secure_exec_kernel::resource_accounting::ResourceLimits;
125use secure_exec_kernel::root_fs::RootFilesystemMode;
126use secure_exec_kernel::socket_table::{
127 reset_socket_read_trace, set_socket_read_trace_enabled, socket_read_trace_snapshot,
128 InetSocketAddress, SocketDomain, SocketId, SocketShutdown as KernelSocketShutdown, SocketSpec,
129 SocketState, SocketType,
130};
131use secure_exec_sidecar_core::{
132 apply_process_signal_state_update, bound_udp_snapshot_response, bridge_buffer_value,
133 decode_base64, decode_bridge_buffer_value, decode_encoded_bytes_value, encoded_bytes_value,
134 ensure_vm_fetch_raw_response_buffer_within_limit, ensure_vm_fetch_response_within_limit,
135 listener_snapshot_response, local_endpoint_value, parse_kernel_http_fetch_response,
136 parse_process_signal_state_request, process_killed_response,
137 process_snapshot_entry_from_kernel, process_snapshot_response, process_started_response,
138 remote_endpoint_value, shared_guest_runtime_identity, signal_state_response,
139 socket_addr_family, socket_address_value, stdin_closed_response, stdin_written_response,
140 tcp_socket_info_value, unix_socket_info_value, zombie_timer_count_response,
141 SharedProcessSnapshotEntry, SharedProcessSnapshotStatus, SidecarCoreError,
142 VM_FETCH_BUFFER_LIMIT_BYTES,
143};
144use serde::{Deserialize, Serialize};
145use serde_json::{json, Map, Value};
146use sha1::Sha1;
147use sha2::{digest::Digest, Sha224, Sha256, Sha384, Sha512};
148use socket2::{SockRef, TcpKeepalive};
149use std::collections::VecDeque;
150use std::collections::{BTreeMap, BTreeSet};
151use std::fmt;
152use std::fs;
153use std::io::{Cursor, Read, Write};
154use std::net::{
155 IpAddr, Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr, TcpListener, TcpStream, ToSocketAddrs,
156 UdpSocket,
157};
158use std::os::fd::{AsFd, BorrowedFd};
159use std::os::unix::fs::{MetadataExt, PermissionsExt};
160use std::os::unix::net::{SocketAddr as UnixSocketAddr, UnixListener, UnixStream};
161use std::path::{Path, PathBuf};
162use std::pin::Pin;
163use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
164use std::sync::mpsc::{self, RecvTimeoutError, Sender};
165use std::sync::{Arc, Mutex, OnceLock, Weak};
166use std::thread;
167use std::time::{Duration, Instant};
168use tokio::io::{AsyncRead, AsyncWrite};
169use tokio::runtime::Builder as TokioRuntimeBuilder;
170use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
171use tokio_rustls::{TlsAcceptor, TlsConnector};
172use url::Url;
173
174const DEFAULT_KERNEL_STDIN_READ_MAX_BYTES: usize = 64 * 1024;
175const DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS: u64 = 100;
176const JAVASCRIPT_NET_TIMEOUT_SENTINEL: &str = "__secure_exec_net_timeout__";
177const PYTHON_PYODIDE_GUEST_ROOT: &str = "/__agentos_pyodide";
178const PYTHON_PYODIDE_CACHE_GUEST_ROOT: &str = "/__agentos_pyodide_cache";
179const TCP_SOCKET_POLL_TIMEOUT: Duration = Duration::from_millis(100);
180const TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5);
181const LOOPBACK_TLS_HANDSHAKE_POLL_TIMEOUT: Duration = Duration::from_millis(1);
182const LOOPBACK_TLS_PENDING_WRITE_BUFFER_BYTES: usize = 4 * 1024 * 1024;
183const LOOPBACK_TLS_PENDING_WRITE_WARNING_BYTES: usize =
184 LOOPBACK_TLS_PENDING_WRITE_BUFFER_BYTES * 4 / 5;
185const HTTP_LOOPBACK_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
186const PROCESS_EXIT_DRAIN_INITIAL_QUIET: Duration = Duration::from_millis(1);
187const PROCESS_EXIT_DRAIN_TRAILING_QUIET: Duration = Duration::from_millis(25);
188
189struct NetTcpTraceCounters {
190 socket_read_calls: AtomicU64,
191 socket_read_zero_wait_calls: AtomicU64,
192 socket_read_data_events: AtomicU64,
193 socket_read_bytes: AtomicU64,
194 socket_read_kernel_us: AtomicU64,
195 socket_read_end_events: AtomicU64,
196 socket_read_eagain: AtomicU64,
197 socket_read_errors: AtomicU64,
198 socket_read_push_attempts: AtomicU64,
199 socket_read_push_sent: AtomicU64,
200 socket_read_push_missing: AtomicU64,
201 socket_read_push_errors: AtomicU64,
202 socket_write_calls: AtomicU64,
203 socket_write_bytes: AtomicU64,
204 socket_write_kernel_us: AtomicU64,
205 socket_write_errors: AtomicU64,
206 server_accept_calls: AtomicU64,
207 server_accept_zero_wait_calls: AtomicU64,
208 server_accept_connections: AtomicU64,
209 server_accept_eagain: AtomicU64,
210 server_accept_errors: AtomicU64,
211 kernel_poll_targets: AtomicU64,
212 kernel_poll_zero_wait_calls: AtomicU64,
213 kernel_poll_wait_us: AtomicU64,
214 kernel_poll_elapsed_us: AtomicU64,
215 kernel_poll_empty: AtomicU64,
216 kernel_poll_ready: AtomicU64,
217 kernel_poll_revents_read: AtomicU64,
218 kernel_poll_revents_hup: AtomicU64,
219 kernel_poll_revents_err: AtomicU64,
220 kernel_poll_revents_bits_or: AtomicU64,
221}
222
223impl NetTcpTraceCounters {
224 const fn new() -> Self {
225 Self {
226 socket_read_calls: AtomicU64::new(0),
227 socket_read_zero_wait_calls: AtomicU64::new(0),
228 socket_read_data_events: AtomicU64::new(0),
229 socket_read_bytes: AtomicU64::new(0),
230 socket_read_kernel_us: AtomicU64::new(0),
231 socket_read_end_events: AtomicU64::new(0),
232 socket_read_eagain: AtomicU64::new(0),
233 socket_read_errors: AtomicU64::new(0),
234 socket_read_push_attempts: AtomicU64::new(0),
235 socket_read_push_sent: AtomicU64::new(0),
236 socket_read_push_missing: AtomicU64::new(0),
237 socket_read_push_errors: AtomicU64::new(0),
238 socket_write_calls: AtomicU64::new(0),
239 socket_write_bytes: AtomicU64::new(0),
240 socket_write_kernel_us: AtomicU64::new(0),
241 socket_write_errors: AtomicU64::new(0),
242 server_accept_calls: AtomicU64::new(0),
243 server_accept_zero_wait_calls: AtomicU64::new(0),
244 server_accept_connections: AtomicU64::new(0),
245 server_accept_eagain: AtomicU64::new(0),
246 server_accept_errors: AtomicU64::new(0),
247 kernel_poll_targets: AtomicU64::new(0),
248 kernel_poll_zero_wait_calls: AtomicU64::new(0),
249 kernel_poll_wait_us: AtomicU64::new(0),
250 kernel_poll_elapsed_us: AtomicU64::new(0),
251 kernel_poll_empty: AtomicU64::new(0),
252 kernel_poll_ready: AtomicU64::new(0),
253 kernel_poll_revents_read: AtomicU64::new(0),
254 kernel_poll_revents_hup: AtomicU64::new(0),
255 kernel_poll_revents_err: AtomicU64::new(0),
256 kernel_poll_revents_bits_or: AtomicU64::new(0),
257 }
258 }
259}
260
261static NET_TCP_TRACE_COUNTERS: NetTcpTraceCounters = NetTcpTraceCounters::new();
262pub(crate) const MAX_PER_PROCESS_STATE_HANDLES: usize = 1024;
263const DEFAULT_SCRYPT_COST: u64 = 16_384;
264const DEFAULT_SCRYPT_BLOCK_SIZE: u32 = 8;
265const DEFAULT_SCRYPT_PARALLELIZATION: u32 = 1;
266const SQLITE_JS_SAFE_INTEGER_MAX: i64 = 9_007_199_254_740_991;
267const HTTP_LOOPBACK_REQUEST_TIMEOUT_MS_ENV: &str =
268 "SECURE_EXEC_TEST_HTTP_LOOPBACK_REQUEST_TIMEOUT_MS";
269
270trait Http2AsyncIo: AsyncRead + AsyncWrite + Unpin + Send {}
271
272impl<T> Http2AsyncIo for T where T: AsyncRead + AsyncWrite + Unpin + Send {}
273
274fn http_loopback_request_timeout() -> Duration {
275 std::env::var(HTTP_LOOPBACK_REQUEST_TIMEOUT_MS_ENV)
276 .ok()
277 .and_then(|value| value.parse::<u64>().ok())
278 .map(Duration::from_millis)
279 .unwrap_or(HTTP_LOOPBACK_REQUEST_TIMEOUT)
280}
281
282fn wait_fd_readable_until(fd: BorrowedFd<'_>, deadline: Instant) -> bool {
291 let remaining = deadline.saturating_duration_since(Instant::now());
292 if remaining.is_zero() {
293 return false;
294 }
295
296 let timeout_ms = remaining.as_millis().saturating_add(u128::from(
297 !remaining.subsec_nanos().is_multiple_of(1_000_000),
298 ));
299 let timeout =
300 PollTimeout::try_from(timeout_ms.min(i32::MAX as u128)).unwrap_or(PollTimeout::MAX);
301 let mut fds = [NixPollFd::new(fd, PollFlags::POLLIN)];
302 match poll(&mut fds, timeout) {
303 Ok(0) => false,
304 Ok(_) => fds[0]
305 .revents()
306 .unwrap_or_else(PollFlags::empty)
307 .intersects(PollFlags::POLLIN | PollFlags::POLLHUP | PollFlags::POLLERR),
308 Err(_) => true,
309 }
310}
311
312const DEFAULT_ALLOWED_NODE_BUILTINS: &[&str] = &[
313 "assert",
314 "buffer",
315 "console",
316 "child_process",
317 "crypto",
318 "dns",
319 "events",
320 "fs",
321 "http",
322 "http2",
323 "https",
324 "module",
325 "os",
326 "path",
327 "perf_hooks",
328 "querystring",
329 "sqlite",
330 "stream",
331 "string_decoder",
332 "timers",
333 "tls",
334 "tty",
335 "url",
336 "util",
337 "zlib",
338];
339const EXECUTION_REQUEST_TTY_ENV: &str = "AGENTOS_EXEC_TTY";
340
341#[derive(Debug, Clone, Copy, PartialEq, Eq)]
342enum JavascriptCryptoDigestAlgorithm {
343 Md5,
344 Sha1,
345 Sha224,
346 Sha256,
347 Sha384,
348 Sha512,
349}
350
351#[derive(Debug, Default, Deserialize)]
352#[serde(default, rename_all = "camelCase")]
353struct JavascriptScryptOptions {
354 #[serde(alias = "N")]
355 cost: Option<u64>,
356 #[serde(alias = "r")]
357 block_size: Option<u32>,
358 #[serde(alias = "p")]
359 parallelization: Option<u32>,
360}
361
362#[derive(Debug, Deserialize)]
363#[serde(rename_all = "camelCase")]
364struct JavascriptHttpListenRequest {
365 server_id: u64,
366 #[serde(default)]
367 port: Option<u16>,
368 #[serde(default)]
369 hostname: Option<String>,
370}
371
372#[derive(Debug, Default, Deserialize)]
373#[serde(default, rename_all = "camelCase")]
374struct JavascriptHttpRequestOptions {
375 method: Option<String>,
376 headers: BTreeMap<String, Value>,
377 body: Option<String>,
378 reject_unauthorized: Option<bool>,
379}
380
381#[derive(Debug, Default, Deserialize)]
382#[serde(default, rename_all = "camelCase")]
383struct JavascriptHttp2ServerListenRequest {
384 server_id: u64,
385 secure: bool,
386 port: Option<u16>,
387 host: Option<String>,
388 backlog: Option<u32>,
389 timeout: Option<u64>,
390 settings: BTreeMap<String, Value>,
391 tls: Option<JavascriptTlsBridgeOptions>,
392}
393
394#[derive(Debug, Default, Deserialize)]
395#[serde(default, rename_all = "camelCase")]
396struct JavascriptHttp2SessionConnectRequest {
397 authority: Option<String>,
398 protocol: Option<String>,
399 host: Option<String>,
400 port: Option<u16>,
401 settings: BTreeMap<String, Value>,
402 tls: Option<JavascriptTlsBridgeOptions>,
403}
404
405#[derive(Debug, Default, Deserialize)]
406#[serde(default, rename_all = "camelCase")]
407struct JavascriptHttp2RequestOptions {
408 end_stream: bool,
409}
410
411#[derive(Debug, Default, Deserialize)]
412#[serde(default, rename_all = "camelCase")]
413struct JavascriptHttp2FileResponseOptions {
414 offset: Option<u64>,
415 length: Option<i64>,
416}
417
418#[derive(Debug, Clone)]
419struct HttpHeaderCollection {
420 normalized: BTreeMap<String, Vec<String>>,
421 raw_pairs: Vec<(String, String)>,
422}
423
424#[derive(Debug)]
425struct InsecureTlsVerifier {
426 supported_schemes: Vec<SignatureScheme>,
427}
428
429impl ServerCertVerifier for InsecureTlsVerifier {
430 fn verify_server_cert(
431 &self,
432 _end_entity: &CertificateDer<'_>,
433 _intermediates: &[CertificateDer<'_>],
434 _server_name: &ServerName<'_>,
435 _ocsp_response: &[u8],
436 _now: rustls::pki_types::UnixTime,
437 ) -> Result<ServerCertVerified, rustls::Error> {
438 Ok(ServerCertVerified::assertion())
439 }
440
441 fn verify_tls12_signature(
442 &self,
443 _message: &[u8],
444 _cert: &CertificateDer<'_>,
445 _dss: &DigitallySignedStruct,
446 ) -> Result<HandshakeSignatureValid, rustls::Error> {
447 Ok(HandshakeSignatureValid::assertion())
448 }
449
450 fn verify_tls13_signature(
451 &self,
452 _message: &[u8],
453 _cert: &CertificateDer<'_>,
454 _dss: &DigitallySignedStruct,
455 ) -> Result<HandshakeSignatureValid, rustls::Error> {
456 Ok(HandshakeSignatureValid::assertion())
457 }
458
459 fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
460 self.supported_schemes.clone()
461 }
462}
463
464impl ActiveProcess {
465 pub(crate) fn new(
466 kernel_pid: u32,
467 kernel_handle: KernelProcessHandle,
468 runtime: GuestRuntimeKind,
469 execution: ActiveExecution,
470 ) -> Self {
471 Self {
472 kernel_pid,
473 kernel_handle,
474 kernel_stdin_writer_fd: None,
475 tty_master_fd: None,
476 runtime,
477 detached: false,
478 execution,
479 guest_cwd: String::from("/"),
480 env: BTreeMap::new(),
481 host_cwd: PathBuf::from("/"),
482 host_write_dirty: false,
483 mapped_host_fds: BTreeMap::new(),
484 next_mapped_host_fd: MAPPED_HOST_FD_START,
485 pending_execution_events: VecDeque::new(),
486 pending_self_signal_exit: None,
487 child_processes: BTreeMap::new(),
488 next_child_process_id: 0,
489 http_servers: BTreeMap::new(),
490 pending_http_requests: BTreeMap::new(),
491 http2: Default::default(),
492 tcp_listeners: BTreeMap::new(),
493 next_tcp_listener_id: 0,
494 tcp_sockets: BTreeMap::new(),
495 next_tcp_socket_id: 0,
496 tcp_port_reservations: BTreeMap::new(),
497 next_tcp_port_reservation_id: 0,
498 unix_listeners: BTreeMap::new(),
499 next_unix_listener_id: 0,
500 unix_sockets: BTreeMap::new(),
501 next_unix_socket_id: 0,
502 udp_sockets: BTreeMap::new(),
503 next_udp_socket_id: 0,
504 python_sockets: BTreeMap::new(),
505 next_python_socket_id: 0,
506 cipher_sessions: BTreeMap::new(),
507 next_cipher_session_id: 0,
508 diffie_hellman_sessions: BTreeMap::new(),
509 next_diffie_hellman_session_id: 0,
510 sqlite_databases: BTreeMap::new(),
511 next_sqlite_database_id: 0,
512 sqlite_statements: BTreeMap::new(),
513 next_sqlite_statement_id: 0,
514 tty_master_owner: None,
515 deferred_kernel_wait_rpc: None,
516 module_resolution_cache: secure_exec_execution::LocalModuleResolutionCache::default(),
517 }
518 }
519
520 pub(crate) fn queue_pending_execution_event(
521 &mut self,
522 event: ActiveExecutionEvent,
523 ) -> Result<(), SidecarError> {
524 if self.pending_execution_events.len() >= MAX_PROCESS_EVENT_QUEUE {
525 return Err(process_event_queue_overflow_error());
526 }
527 self.pending_execution_events.push_back(event);
528 Ok(())
529 }
530
531 pub(crate) fn with_host_cwd(mut self, host_cwd: PathBuf) -> Self {
532 self.host_cwd = host_cwd;
533 self
534 }
535
536 pub(crate) fn mark_host_write_dirty(&mut self) {
537 self.host_write_dirty = true;
538 }
539
540 pub(crate) fn host_write_dirty_recursive(&self) -> bool {
541 self.host_write_dirty
542 || self
543 .child_processes
544 .values()
545 .any(ActiveProcess::host_write_dirty_recursive)
546 }
547
548 pub(crate) fn clean_host_writes_are_observable_recursive(&self) -> bool {
549 matches!(
550 self.execution,
551 ActiveExecution::Javascript(_) | ActiveExecution::Python(_) | ActiveExecution::Wasm(_)
552 ) && self
553 .child_processes
554 .values()
555 .all(ActiveProcess::clean_host_writes_are_observable_recursive)
556 }
557
558 pub(crate) fn with_guest_cwd(mut self, guest_cwd: String) -> Self {
559 self.guest_cwd = guest_cwd;
560 self
561 }
562
563 pub(crate) fn with_env(mut self, env: BTreeMap<String, String>) -> Self {
564 self.env = env;
565 self
566 }
567
568 pub(crate) fn with_kernel_stdin_writer_fd(mut self, fd: u32) -> Self {
569 self.kernel_stdin_writer_fd = Some(fd);
570 self
571 }
572
573 pub(crate) fn with_tty_master_fd(mut self, fd: Option<u32>) -> Self {
574 self.tty_master_fd = fd;
575 self
576 }
577
578 pub(crate) fn with_detached(mut self, detached: bool) -> Self {
579 self.detached = detached;
580 self
581 }
582
583 pub(crate) fn allocate_mapped_host_fd(&mut self, fd: ActiveMappedHostFd) -> u32 {
584 let handle = self.next_mapped_host_fd;
585 self.next_mapped_host_fd = self
586 .next_mapped_host_fd
587 .checked_add(1)
588 .unwrap_or(MAPPED_HOST_FD_START);
589 self.mapped_host_fds.insert(handle, fd);
590 handle
591 }
592
593 pub(crate) fn mapped_host_fd(&self, fd: u32) -> Option<&ActiveMappedHostFd> {
594 self.mapped_host_fds.get(&fd)
595 }
596
597 pub(crate) fn mapped_host_fd_mut(&mut self, fd: u32) -> Option<&mut ActiveMappedHostFd> {
598 self.mapped_host_fds.get_mut(&fd)
599 }
600
601 pub(crate) fn close_mapped_host_fd(&mut self, fd: u32) -> bool {
602 self.mapped_host_fds.remove(&fd).is_some()
603 }
604
605 pub(crate) fn allocate_child_process_id(&mut self) -> String {
606 self.next_child_process_id += 1;
607 format!("child-{}", self.next_child_process_id)
608 }
609
610 fn allocate_tcp_listener_id(&mut self) -> String {
611 self.next_tcp_listener_id += 1;
612 format!("listener-{}", self.next_tcp_listener_id)
613 }
614
615 fn allocate_tcp_socket_id(&mut self) -> String {
616 self.next_tcp_socket_id += 1;
617 format!("socket-{}", self.next_tcp_socket_id)
618 }
619
620 fn allocate_tcp_port_reservation_id(&mut self) -> String {
621 self.next_tcp_port_reservation_id += 1;
622 format!("tcp-port-reservation-{}", self.next_tcp_port_reservation_id)
623 }
624
625 fn allocate_unix_listener_id(&mut self) -> String {
626 self.next_unix_listener_id += 1;
627 format!("unix-listener-{}", self.next_unix_listener_id)
628 }
629
630 fn allocate_unix_socket_id(&mut self) -> String {
631 self.next_unix_socket_id += 1;
632 format!("unix-socket-{}", self.next_unix_socket_id)
633 }
634
635 fn allocate_udp_socket_id(&mut self) -> String {
636 self.next_udp_socket_id += 1;
637 format!("udp-socket-{}", self.next_udp_socket_id)
638 }
639
640 pub(crate) fn network_resource_counts(&self) -> NetworkResourceCounts {
641 let mut counts = NetworkResourceCounts {
642 sockets: self.http_servers.len()
643 + self.tcp_listeners.len()
644 + self.tcp_sockets.len()
645 + self.unix_listeners.len()
646 + self.unix_sockets.len()
647 + self.udp_sockets.len()
648 + self.python_sockets.len(),
649 connections: self.tcp_sockets.len() + self.unix_sockets.len(),
650 };
651 if let Ok(http2) = self.http2.shared.lock() {
652 counts.sockets += http2.servers.len() + http2.sessions.len();
653 counts.connections += http2.sessions.len();
654 }
655
656 for child in self.child_processes.values() {
657 let child_counts = child.network_resource_counts();
658 counts.sockets += child_counts.sockets;
659 counts.connections += child_counts.connections;
660 }
661
662 counts
663 }
664
665 fn sidecar_only_network_resource_counts(&self) -> NetworkResourceCounts {
666 let mut counts = NetworkResourceCounts {
667 sockets: self.http_servers.len()
668 + self
669 .tcp_listeners
670 .values()
671 .filter(|listener| listener.kernel_socket_id.is_none())
672 .count()
673 + self
674 .tcp_sockets
675 .values()
676 .filter(|socket| socket.kernel_socket_id.is_none())
677 .count()
678 + self.unix_listeners.len()
679 + self.unix_sockets.len()
680 + self
681 .udp_sockets
682 .values()
683 .filter(|socket| socket.kernel_socket_id.is_none())
684 .count()
685 + self.python_sockets.len(),
686 connections: self
687 .tcp_sockets
688 .values()
689 .filter(|socket| socket.kernel_socket_id.is_none())
690 .count()
691 + self.unix_sockets.len(),
692 };
693 if let Ok(http2) = self.http2.shared.lock() {
694 counts.sockets += http2.servers.len() + http2.sessions.len();
695 counts.connections += http2.sessions.len();
696 }
697
698 for child in self.child_processes.values() {
699 let child_counts = child.sidecar_only_network_resource_counts();
700 counts.sockets += child_counts.sockets;
701 counts.connections += child_counts.connections;
702 }
703
704 counts
705 }
706}
707
708fn poll_tool_process_event(
709 execution: &ToolExecution,
710) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
711 let event = execution
712 .pending_events
713 .lock()
714 .unwrap_or_else(|poisoned| poisoned.into_inner())
715 .pop_front();
716 if event.is_some() {
717 return Ok(event);
718 }
719 if execution.events_overflowed.load(Ordering::Relaxed) {
720 return Err(process_event_queue_overflow_error());
721 }
722 Ok(None)
723}
724
725fn descendant_pending_execution_event_capacity(
726 root: &ActiveProcess,
727 child_path: &[&str],
728) -> Option<usize> {
729 let mut child = root;
730 for child_process_id in child_path {
731 child = child.child_processes.get(*child_process_id)?;
732 }
733 Some(MAX_PROCESS_EVENT_QUEUE.saturating_sub(child.pending_execution_events.len()))
734}
735
736fn poll_child_execution_after_exit(
737 child: &mut ActiveProcess,
738 wait: Duration,
739) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
740 match child.execution.poll_event_blocking(wait) {
741 Ok(event) => Ok(event),
742 Err(SidecarError::Execution(message))
743 if child.runtime == GuestRuntimeKind::WebAssembly
744 && message == WasmExecutionError::EventChannelClosed.to_string() =>
745 {
746 Ok(None)
747 }
748 Err(error) => Err(error),
749 }
750}
751
752fn closed_javascript_event_channel(message: &str) -> bool {
753 message == "guest JavaScript event channel closed unexpectedly"
754}
755
756fn closed_python_event_channel(message: &str) -> bool {
757 message == "guest Python event channel closed unexpectedly"
758}
759
760fn closed_wasm_event_channel(message: &str) -> bool {
761 message == WasmExecutionError::EventChannelClosed.to_string()
762}
763
764fn missing_vm_error(vm_id: &str) -> SidecarError {
765 SidecarError::InvalidState(format!("VM {vm_id} is no longer active"))
766}
767
768fn missing_process_error(vm_id: &str, process_id: &str) -> SidecarError {
769 SidecarError::InvalidState(format!(
770 "VM {vm_id} no longer has active process {process_id}"
771 ))
772}
773
774fn guest_kernel_core_error(error: secure_exec_sidecar_core::SidecarCoreError) -> SidecarError {
778 let message = error.to_string();
779 let is_errno = message.split_once(':').is_some_and(|(code, _)| {
780 code.len() >= 2
781 && code.starts_with('E')
782 && code[1..]
783 .bytes()
784 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
785 });
786 if is_errno {
787 SidecarError::Kernel(message)
788 } else {
789 SidecarError::InvalidState(message)
790 }
791}
792
793fn is_broken_pipe_error(error: &SidecarError) -> bool {
794 matches!(error, SidecarError::Execution(message) if message.contains("Broken pipe") || message.contains("os error 32") || message.contains("EPIPE"))
795}
796
797fn javascript_child_process_gone_error(process_id: &str, child_path: &[&str]) -> SidecarError {
798 let child_label = if child_path.is_empty() {
799 process_id.to_owned()
800 } else {
801 format!("{process_id}/{}", child_path.join("/"))
802 };
803 SidecarError::Execution(format!(
804 "ECHILD: child_process {child_label} is no longer available"
805 ))
806}
807
808fn is_javascript_child_process_gone_error(error: &SidecarError) -> bool {
809 matches!(
810 error,
811 SidecarError::Execution(message) if guest_errno_code(message) == Some("ECHILD")
812 )
813}
814
815fn loopback_tls_transport_registry(
816) -> &'static Mutex<BTreeMap<String, Weak<crate::state::LoopbackTlsTransportPair>>> {
817 static REGISTRY: OnceLock<
818 Mutex<BTreeMap<String, Weak<crate::state::LoopbackTlsTransportPair>>>,
819 > = OnceLock::new();
820 REGISTRY.get_or_init(|| Mutex::new(BTreeMap::new()))
821}
822
823#[cfg(test)]
824#[allow(dead_code)]
825pub(crate) fn loopback_tls_registry_len() -> usize {
826 loopback_tls_transport_registry()
827 .lock()
828 .expect("loopback TLS transport registry lock poisoned")
829 .len()
830}
831
832#[cfg(test)]
833pub(crate) fn loopback_tls_registry_contains(key: &str) -> bool {
834 loopback_tls_transport_registry()
835 .lock()
836 .expect("loopback TLS transport registry lock poisoned")
837 .contains_key(key)
838}
839
840fn loopback_tls_transport_key(
841 vm_id: &str,
842 socket_id: SocketId,
843 peer_socket_id: SocketId,
844) -> String {
845 let (lower, higher) = if socket_id <= peer_socket_id {
846 (socket_id, peer_socket_id)
847 } else {
848 (peer_socket_id, socket_id)
849 };
850 format!("{vm_id}:{lower}:{higher}")
851}
852
853fn loopback_tls_endpoint(
854 vm_id: &str,
855 socket_id: SocketId,
856 peer_socket_id: SocketId,
857) -> Result<crate::state::LoopbackTlsEndpoint, SidecarError> {
858 let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
859 let registry = loopback_tls_transport_registry();
860 let mut transports = registry.lock().map_err(|_| {
861 SidecarError::InvalidState(String::from(
862 "loopback TLS transport registry lock poisoned",
863 ))
864 })?;
865 transports.retain(|_, pair| pair.strong_count() > 0);
866 let pair = transports
867 .get(&key)
868 .and_then(Weak::upgrade)
869 .unwrap_or_else(|| {
870 let pair = Arc::new(crate::state::LoopbackTlsTransportPair {
871 state: Mutex::new(crate::state::LoopbackTlsTransportPairState::default()),
872 ready: std::sync::Condvar::new(),
873 });
874 transports.insert(key.clone(), Arc::downgrade(&pair));
875 pair
876 });
877 Ok(crate::state::LoopbackTlsEndpoint {
878 pair,
879 is_lower_socket: socket_id <= peer_socket_id,
880 poll_timeout: TCP_SOCKET_POLL_TIMEOUT,
881 registry_key: Some(key),
882 })
883}
884
885impl crate::state::LoopbackTlsEndpoint {
886 fn set_poll_timeout(&mut self, timeout: Duration) {
887 self.poll_timeout = timeout;
888 }
889
890 fn interrupt_reader(pair: &Arc<crate::state::LoopbackTlsTransportPair>, is_lower_socket: bool) {
891 if let Ok(mut state) = pair.state.lock() {
892 if is_lower_socket {
893 state.lower_read_interrupt = true;
894 } else {
895 state.higher_read_interrupt = true;
896 }
897 pair.ready.notify_all();
898 }
899 }
900
901 fn shutdown_write(&self) -> Result<(), SidecarError> {
902 let mut state = self.pair.state.lock().map_err(|_| {
903 SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
904 })?;
905 if self.is_lower_socket {
906 state.lower_write_closed = true;
907 } else {
908 state.higher_write_closed = true;
909 }
910 self.pair.ready.notify_all();
911 Ok(())
912 }
913
914 fn close_endpoint(&self) -> Result<(), SidecarError> {
915 let mut state = self.pair.state.lock().map_err(|_| {
916 SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
917 })?;
918 if self.is_lower_socket {
919 state.lower_write_closed = true;
920 state.lower_closed = true;
921 } else {
922 state.higher_write_closed = true;
923 state.higher_closed = true;
924 }
925 self.pair.ready.notify_all();
926 Ok(())
927 }
928}
929
930impl LoopbackTlsPendingWriteHandle {
931 pub(crate) fn new(endpoint: &crate::state::LoopbackTlsEndpoint) -> Self {
932 Self {
933 state: Arc::new(Mutex::new(LoopbackTlsPendingWriteState {
934 buffer: Vec::new(),
935 warned_near_cap: false,
936 flushing: false,
937 defer_shutdown_write: false,
938 failure_message: None,
939 })),
940 tls_handshake_complete: Arc::new(AtomicBool::new(false)),
941 failed: Arc::new(AtomicBool::new(false)),
942 pair: Arc::clone(&endpoint.pair),
943 is_lower_socket: endpoint.is_lower_socket,
944 handshake_started_at: Instant::now(),
945 }
946 }
947
948 fn failure_error(&self) -> SidecarError {
949 let message = self
950 .state
951 .lock()
952 .ok()
953 .and_then(|state| state.failure_message.clone())
954 .unwrap_or_else(|| String::from("loopback TLS pending write failed"));
955 SidecarError::Execution(message)
956 }
957
958 fn mark_failed(&self, message: impl Into<String>) {
959 self.failed.store(true, Ordering::SeqCst);
960 if let Ok(mut state) = self.state.lock() {
961 state.buffer.clear();
962 state.flushing = false;
963 state.defer_shutdown_write = false;
964 state.failure_message = Some(message.into());
965 }
966 }
967
968 fn clear_for_close(&self) {
969 self.mark_failed("loopback TLS socket closed before buffered writes flushed");
970 }
971
972 fn fail_if_pending(&self, message: impl Into<String>) {
973 let has_pending = self
974 .state
975 .lock()
976 .map(|state| !state.buffer.is_empty() || state.flushing || state.defer_shutdown_write)
977 .unwrap_or(true);
978 if has_pending {
979 self.mark_failed(message);
980 }
981 }
982
983 fn should_buffer_write(&self) -> Result<bool, SidecarError> {
984 if self.failed.load(Ordering::SeqCst) {
985 return Err(self.failure_error());
986 }
987 let state = self.state.lock().map_err(|_| {
988 SidecarError::InvalidState(String::from("loopback TLS pending write lock poisoned"))
989 })?;
990 Ok(!self.tls_handshake_complete.load(Ordering::SeqCst)
991 || !state.buffer.is_empty()
992 || state.flushing)
993 }
994
995 pub(crate) fn append_write(&self, contents: &[u8]) -> Result<(), SidecarError> {
996 if self.failed.load(Ordering::SeqCst) {
997 return Err(self.failure_error());
998 }
999 let mut state = self.state.lock().map_err(|_| {
1000 SidecarError::InvalidState(String::from("loopback TLS pending write lock poisoned"))
1001 })?;
1002 let observed = state.buffer.len().saturating_add(contents.len());
1003 if observed > LOOPBACK_TLS_PENDING_WRITE_BUFFER_BYTES {
1004 return Err(SidecarError::Execution(format!(
1005 "loopback TLS pending write buffer exceeded: {observed} bytes > {cap} bytes (limit: loopback TLS pending write buffer)",
1006 cap = LOOPBACK_TLS_PENDING_WRITE_BUFFER_BYTES
1007 )));
1008 }
1009 if !state.warned_near_cap && observed >= LOOPBACK_TLS_PENDING_WRITE_WARNING_BYTES {
1010 state.warned_near_cap = true;
1011 tracing::warn!(
1012 limit = "loopback_tls_pending_write_buffer",
1013 observed_bytes = observed,
1014 cap_bytes = LOOPBACK_TLS_PENDING_WRITE_BUFFER_BYTES,
1015 fill_percent =
1016 (observed as f64 / LOOPBACK_TLS_PENDING_WRITE_BUFFER_BYTES as f64) * 100.0,
1017 wired = "invariant.loopbackTlsPendingWriteBufferBytes",
1018 "loopback TLS pending write buffer is near capacity"
1019 );
1020 }
1021 state.buffer.extend_from_slice(contents);
1022 Ok(())
1023 }
1024
1025 fn defer_shutdown_write(&self) -> Result<(), SidecarError> {
1026 if self.failed.load(Ordering::SeqCst) {
1027 return Err(self.failure_error());
1028 }
1029 let mut state = self.state.lock().map_err(|_| {
1030 SidecarError::InvalidState(String::from("loopback TLS pending write lock poisoned"))
1031 })?;
1032 state.defer_shutdown_write = true;
1033 Ok(())
1034 }
1035
1036 fn take_buffer_for_flush(&self) -> Result<Option<Vec<u8>>, SidecarError> {
1037 let mut state = self.state.lock().map_err(|_| {
1038 SidecarError::InvalidState(String::from("loopback TLS pending write lock poisoned"))
1039 })?;
1040 if state.buffer.is_empty() {
1041 return Ok(None);
1042 }
1043 state.flushing = true;
1044 Ok(Some(std::mem::take(&mut state.buffer)))
1045 }
1046
1047 fn finish_flush(&self) {
1048 if let Ok(mut state) = self.state.lock() {
1049 state.flushing = false;
1050 }
1051 }
1052
1053 fn take_deferred_shutdown_write(&self) -> Result<bool, SidecarError> {
1054 let mut state = self.state.lock().map_err(|_| {
1055 SidecarError::InvalidState(String::from("loopback TLS pending write lock poisoned"))
1056 })?;
1057 if state.buffer.is_empty() && !state.flushing && state.defer_shutdown_write {
1058 state.defer_shutdown_write = false;
1059 return Ok(true);
1060 }
1061 Ok(false)
1062 }
1063
1064 fn interrupt_own_reader(&self) {
1065 crate::state::LoopbackTlsEndpoint::interrupt_reader(&self.pair, self.is_lower_socket);
1066 }
1067}
1068
1069fn parse_tls_client_hello_from_bytes(
1070 buffer: &[u8],
1071) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
1072 if buffer.is_empty() {
1073 return Ok(None);
1074 }
1075
1076 let mut acceptor = rustls::server::Acceptor::default();
1077 let mut cursor = Cursor::new(buffer);
1078 acceptor.read_tls(&mut cursor).map_err(sidecar_net_error)?;
1079 let Some(accepted) = acceptor.accept().map_err(|(error, _)| {
1080 SidecarError::Execution(format!("failed to parse TLS client hello: {error}"))
1081 })?
1082 else {
1083 return Ok(None);
1084 };
1085 let client_hello = accepted.client_hello();
1086 let alpn_protocols = client_hello.alpn().map(|protocols| {
1087 protocols
1088 .filter_map(|protocol| String::from_utf8(protocol.to_vec()).ok())
1089 .collect::<Vec<_>>()
1090 });
1091 Ok(Some(JavascriptTlsClientHello {
1092 servername: client_hello.server_name().map(str::to_owned),
1093 alpn_protocols,
1094 }))
1095}
1096
1097fn peek_loopback_tls_client_hello(
1098 vm_id: &str,
1099 socket_id: SocketId,
1100 peer_socket_id: SocketId,
1101) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
1102 let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
1103 let registry = loopback_tls_transport_registry();
1104 let pair = registry
1105 .lock()
1106 .map_err(|_| {
1107 SidecarError::InvalidState(String::from(
1108 "loopback TLS transport registry lock poisoned",
1109 ))
1110 })?
1111 .get(&key)
1112 .and_then(Weak::upgrade);
1113 let Some(pair) = pair else {
1114 return Ok(None);
1115 };
1116 let is_lower_socket = socket_id <= peer_socket_id;
1117 let state = pair.state.lock().map_err(|_| {
1118 SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
1119 })?;
1120 let buffered = if is_lower_socket {
1121 state.higher_to_lower.iter().copied().collect::<Vec<_>>()
1122 } else {
1123 state.lower_to_higher.iter().copied().collect::<Vec<_>>()
1124 };
1125 drop(state);
1126 parse_tls_client_hello_from_bytes(&buffered)
1127}
1128
1129fn wait_for_loopback_peer_socket_id(
1130 kernel: &SidecarKernel,
1131 socket_id: SocketId,
1132) -> Option<SocketId> {
1133 let deadline = Instant::now() + Duration::from_millis(500);
1135 let mut backoff = Duration::from_micros(100);
1136 loop {
1137 if let Some(peer_socket_id) = kernel
1138 .socket_get(socket_id)
1139 .and_then(|record| record.peer_socket_id())
1140 {
1141 return Some(peer_socket_id);
1142 }
1143 let now = Instant::now();
1144 if now >= deadline {
1145 break;
1146 }
1147 std::thread::sleep(backoff.min(deadline.saturating_duration_since(now)));
1148 backoff = (backoff * 2).min(Duration::from_millis(1));
1149 }
1150 None
1151}
1152
1153impl Drop for crate::state::LoopbackTlsEndpoint {
1154 fn drop(&mut self) {
1155 let _ = self.close_endpoint();
1156
1157 let Some(key) = self.registry_key.take() else {
1165 return;
1166 };
1167 let Ok(mut transports) = loopback_tls_transport_registry().lock() else {
1168 return;
1170 };
1171 let should_remove = match transports.get(&key) {
1172 Some(weak) => match weak.upgrade() {
1177 Some(existing) => {
1178 let same_pair = Arc::ptr_eq(&existing, &self.pair);
1179 drop(existing);
1180 same_pair && Arc::strong_count(&self.pair) <= 1
1181 }
1182 None => true,
1183 },
1184 None => false,
1185 };
1186 if should_remove {
1187 transports.remove(&key);
1188 }
1189 }
1190}
1191
1192impl Read for crate::state::LoopbackTlsEndpoint {
1193 fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
1194 let mut state = self
1195 .pair
1196 .state
1197 .lock()
1198 .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
1199
1200 loop {
1201 let (peer_write_closed, peer_closed) = if self.is_lower_socket {
1202 (state.higher_write_closed, state.higher_closed)
1203 } else {
1204 (state.lower_write_closed, state.lower_closed)
1205 };
1206
1207 let incoming = if self.is_lower_socket {
1208 &mut state.higher_to_lower
1209 } else {
1210 &mut state.lower_to_higher
1211 };
1212
1213 if !incoming.is_empty() {
1214 let count = incoming.len().min(buffer.len());
1215 let (head, tail) = incoming.as_slices();
1216 let head_count = head.len().min(count);
1217 buffer[..head_count].copy_from_slice(&head[..head_count]);
1218 if head_count < count {
1219 buffer[head_count..count].copy_from_slice(&tail[..count - head_count]);
1220 }
1221 incoming.drain(..count);
1222 return Ok(count);
1223 }
1224
1225 if peer_write_closed || peer_closed {
1226 return Ok(0);
1227 }
1228
1229 let read_interrupted = if self.is_lower_socket {
1230 std::mem::take(&mut state.lower_read_interrupt)
1231 } else {
1232 std::mem::take(&mut state.higher_read_interrupt)
1233 };
1234 if read_interrupted {
1235 return Err(std::io::Error::new(
1236 std::io::ErrorKind::WouldBlock,
1237 "loopback TLS transport read interrupted",
1238 ));
1239 }
1240
1241 let (next_state, wait_result) = self
1242 .pair
1243 .ready
1244 .wait_timeout(state, self.poll_timeout)
1245 .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
1246 state = next_state;
1247 if wait_result.timed_out() {
1248 return Err(std::io::Error::new(
1249 std::io::ErrorKind::WouldBlock,
1250 "loopback TLS transport read timed out",
1251 ));
1252 }
1253 }
1254 }
1255}
1256
1257impl Write for crate::state::LoopbackTlsEndpoint {
1258 fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
1259 let mut state = self
1260 .pair
1261 .state
1262 .lock()
1263 .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
1264
1265 let peer_closed = if self.is_lower_socket {
1266 state.higher_closed
1267 } else {
1268 state.lower_closed
1269 };
1270 let outgoing = if self.is_lower_socket {
1271 &mut state.lower_to_higher
1272 } else {
1273 &mut state.higher_to_lower
1274 };
1275 if peer_closed {
1276 return Err(std::io::Error::new(
1277 std::io::ErrorKind::BrokenPipe,
1278 "loopback TLS peer is closed",
1279 ));
1280 }
1281
1282 outgoing.extend(buffer.iter().copied());
1283 self.pair.ready.notify_all();
1284 Ok(buffer.len())
1285 }
1286
1287 fn flush(&mut self) -> std::io::Result<()> {
1288 Ok(())
1289 }
1290}
1291
1292#[cfg(test)]
1293mod loopback_tls_registry_tests {
1294 use super::{
1295 loopback_tls_endpoint, loopback_tls_registry_contains, loopback_tls_transport_key,
1296 };
1297
1298 #[test]
1302 fn dropping_endpoint_removes_its_registry_entry() {
1303 let vm_id = "loopback-tls-drop-removes-entry";
1304 let key = loopback_tls_transport_key(vm_id, 1, 2);
1305
1306 let endpoint = loopback_tls_endpoint(vm_id, 1, 2).expect("create endpoint");
1307 assert!(
1308 loopback_tls_registry_contains(&key),
1309 "registry should contain the key while the endpoint is alive"
1310 );
1311
1312 drop(endpoint);
1313 assert!(
1314 !loopback_tls_registry_contains(&key),
1315 "registry entry must be pruned in the endpoint's Drop, not left for the lazy retain()"
1316 );
1317 }
1318
1319 #[test]
1320 fn registry_entry_survives_until_last_peer_endpoint_drops() {
1321 let vm_id = "loopback-tls-shared-pair";
1322 let key = loopback_tls_transport_key(vm_id, 3, 4);
1323
1324 let lower = loopback_tls_endpoint(vm_id, 3, 4).expect("create lower endpoint");
1326 let higher = loopback_tls_endpoint(vm_id, 4, 3).expect("create higher endpoint");
1327 assert!(loopback_tls_registry_contains(&key));
1328
1329 drop(lower);
1332 assert!(
1333 loopback_tls_registry_contains(&key),
1334 "entry must survive while a peer endpoint still shares the pair"
1335 );
1336
1337 drop(higher);
1338 assert!(
1339 !loopback_tls_registry_contains(&key),
1340 "entry must be pruned once the last peer endpoint drops"
1341 );
1342 }
1343}
1344
1345struct ActiveTcpConnectRequest<'a, B> {
1348 bridge: &'a SharedBridge<B>,
1349 kernel: &'a mut SidecarKernel,
1350 kernel_pid: u32,
1351 vm_id: &'a str,
1352 dns: &'a VmDnsConfig,
1353 host: &'a str,
1354 port: u16,
1355 local_address: Option<&'a str>,
1356 local_port: Option<u16>,
1357 local_reservation: Option<(JavascriptSocketFamily, u16)>,
1358 context: &'a JavascriptSocketPathContext,
1359}
1360
1361struct ActiveUdpSendToRequest<'a, B> {
1362 bridge: &'a SharedBridge<B>,
1363 kernel: &'a mut SidecarKernel,
1364 kernel_pid: u32,
1365 vm_id: &'a str,
1366 dns: &'a VmDnsConfig,
1367 host: &'a str,
1368 port: u16,
1369 context: &'a JavascriptSocketPathContext,
1370 contents: &'a [u8],
1371}
1372
1373struct UdpRemoteAddrRequest<'a, B> {
1374 bridge: &'a SharedBridge<B>,
1375 kernel: &'a SidecarKernel,
1376 vm_id: &'a str,
1377 dns: &'a VmDnsConfig,
1378 host: &'a str,
1379 port: u16,
1380 family: JavascriptUdpFamily,
1381 context: &'a JavascriptSocketPathContext,
1382}
1383
1384pub(crate) struct JavascriptSyncRpcServiceRequest<'a, B> {
1385 pub(crate) bridge: &'a SharedBridge<B>,
1386 pub(crate) vm_id: &'a str,
1387 pub(crate) dns: &'a VmDnsConfig,
1388 pub(crate) socket_paths: &'a JavascriptSocketPathContext,
1389 pub(crate) kernel: &'a mut SidecarKernel,
1390 pub(crate) kernel_readiness: KernelSocketReadinessRegistry,
1391 pub(crate) process: &'a mut ActiveProcess,
1392 pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
1393 pub(crate) resource_limits: &'a ResourceLimits,
1394 pub(crate) network_counts: NetworkResourceCounts,
1395}
1396
1397pub(crate) enum JavascriptSyncRpcServiceResponse {
1398 Json(Value),
1399 Raw(Vec<u8>),
1400}
1401
1402impl From<Value> for JavascriptSyncRpcServiceResponse {
1403 fn from(value: Value) -> Self {
1404 Self::Json(value)
1405 }
1406}
1407
1408impl JavascriptSyncRpcServiceResponse {
1409 fn as_json(&self) -> Option<&Value> {
1410 match self {
1411 Self::Json(value) => Some(value),
1412 Self::Raw(_) => None,
1413 }
1414 }
1415}
1416
1417pub(crate) struct JavascriptNetSyncRpcServiceRequest<'a, B> {
1418 pub(crate) bridge: &'a SharedBridge<B>,
1419 pub(crate) vm_id: &'a str,
1420 pub(crate) dns: &'a VmDnsConfig,
1421 pub(crate) socket_paths: &'a JavascriptSocketPathContext,
1422 pub(crate) kernel: &'a mut SidecarKernel,
1423 pub(crate) kernel_readiness: KernelSocketReadinessRegistry,
1424 pub(crate) process: &'a mut ActiveProcess,
1425 pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
1426 pub(crate) resource_limits: &'a ResourceLimits,
1427 pub(crate) network_counts: NetworkResourceCounts,
1428}
1429
1430struct LoopbackHttpResponseWaitRequest<'a, B> {
1431 bridge: &'a SharedBridge<B>,
1432 vm_id: &'a str,
1433 dns: &'a VmDnsConfig,
1434 socket_paths: &'a JavascriptSocketPathContext,
1435 kernel: &'a mut SidecarKernel,
1436 kernel_readiness: KernelSocketReadinessRegistry,
1437 process: &'a mut ActiveProcess,
1438 resource_limits: &'a ResourceLimits,
1439 request_key: (u64, u64),
1440}
1441
1442pub(crate) struct LoopbackHttpDispatchRequest<'a, B> {
1443 pub(crate) bridge: &'a SharedBridge<B>,
1444 pub(crate) vm_id: &'a str,
1445 pub(crate) dns: &'a VmDnsConfig,
1446 pub(crate) socket_paths: &'a JavascriptSocketPathContext,
1447 pub(crate) kernel: &'a mut SidecarKernel,
1448 pub(crate) kernel_readiness: KernelSocketReadinessRegistry,
1449 pub(crate) process: &'a mut ActiveProcess,
1450 pub(crate) resource_limits: &'a ResourceLimits,
1451 pub(crate) server_id: u64,
1452 pub(crate) request_json: &'a str,
1453}
1454
1455struct JavascriptDgramSyncRpcServiceRequest<'a, B> {
1456 bridge: &'a SharedBridge<B>,
1457 kernel: &'a mut SidecarKernel,
1458 vm_id: &'a str,
1459 dns: &'a VmDnsConfig,
1460 socket_paths: &'a JavascriptSocketPathContext,
1461 process: &'a mut ActiveProcess,
1462 kernel_readiness: KernelSocketReadinessRegistry,
1463 sync_request: &'a JavascriptSyncRpcRequest,
1464 resource_limits: &'a ResourceLimits,
1465 network_counts: NetworkResourceCounts,
1466}
1467
1468struct JavascriptHttp2SyncRpcServiceRequest<'a, B> {
1469 bridge: &'a SharedBridge<B>,
1470 kernel: &'a mut SidecarKernel,
1471 vm_id: &'a str,
1472 dns: &'a VmDnsConfig,
1473 socket_paths: &'a JavascriptSocketPathContext,
1474 process: &'a mut ActiveProcess,
1475 sync_request: &'a JavascriptSyncRpcRequest,
1476 resource_limits: &'a ResourceLimits,
1477 network_counts: NetworkResourceCounts,
1478}
1479
1480impl ActiveTcpSocket {
1481 fn connect<B>(request: ActiveTcpConnectRequest<'_, B>) -> Result<Self, SidecarError>
1482 where
1483 B: NativeSidecarBridge + Send + 'static,
1484 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
1485 {
1486 let ActiveTcpConnectRequest {
1487 bridge,
1488 kernel,
1489 kernel_pid,
1490 vm_id,
1491 dns,
1492 host,
1493 port,
1494 local_address,
1495 local_port,
1496 local_reservation,
1497 context,
1498 } = request;
1499 let resolved = resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, context)?;
1500 if resolved.use_kernel_loopback {
1501 let family = JavascriptSocketFamily::from_ip(resolved.guest_remote_addr.ip());
1502 let requested_local_port = local_port.unwrap_or(0);
1503 let local_port = if requested_local_port != 0
1504 && local_reservation == Some((family, requested_local_port))
1505 {
1506 requested_local_port
1507 } else {
1508 allocate_guest_listen_port(
1509 requested_local_port,
1510 family,
1511 &context.used_tcp_guest_ports,
1512 context.listen_policy,
1513 )?
1514 };
1515 let local_ip = match (family, local_address) {
1516 (JavascriptSocketFamily::Ipv4, Some("0.0.0.0")) => {
1517 IpAddr::V4(Ipv4Addr::UNSPECIFIED)
1518 }
1519 (JavascriptSocketFamily::Ipv4, Some("127.0.0.1") | Some("localhost") | None) => {
1520 IpAddr::V4(Ipv4Addr::LOCALHOST)
1521 }
1522 (JavascriptSocketFamily::Ipv6, Some("::")) => IpAddr::V6(Ipv6Addr::UNSPECIFIED),
1523 (JavascriptSocketFamily::Ipv6, Some("::1") | Some("localhost") | None) => {
1524 IpAddr::V6(Ipv6Addr::LOCALHOST)
1525 }
1526 (JavascriptSocketFamily::Ipv4, Some(other)) => {
1527 return Err(SidecarError::Execution(format!(
1528 "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1529 )));
1530 }
1531 (JavascriptSocketFamily::Ipv6, Some(other)) => {
1532 return Err(SidecarError::Execution(format!(
1533 "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1534 )));
1535 }
1536 };
1537 let local_addr = SocketAddr::new(local_ip, local_port);
1538 let spec = match family {
1539 JavascriptSocketFamily::Ipv4 => SocketSpec::tcp(),
1540 JavascriptSocketFamily::Ipv6 => {
1541 SocketSpec::new(SocketDomain::Inet6, SocketType::Stream)
1542 }
1543 };
1544 let socket_id = kernel
1545 .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
1546 .map_err(kernel_error)?;
1547 kernel
1548 .socket_bind_inet(
1549 EXECUTION_DRIVER_NAME,
1550 kernel_pid,
1551 socket_id,
1552 InetSocketAddress::new(local_ip.to_string(), local_port),
1553 )
1554 .map_err(kernel_error)?;
1555 kernel
1556 .socket_connect_inet_loopback(
1557 EXECUTION_DRIVER_NAME,
1558 kernel_pid,
1559 socket_id,
1560 InetSocketAddress::new(
1561 resolved.guest_remote_addr.ip().to_string(),
1562 resolved.guest_remote_addr.port(),
1563 ),
1564 )
1565 .map_err(kernel_error)?;
1566 return Ok(Self::from_kernel(
1567 socket_id,
1568 None,
1569 local_addr,
1570 resolved.guest_remote_addr,
1571 ));
1572 }
1573
1574 let stream = TcpStream::connect_timeout(&resolved.actual_addr, Duration::from_secs(30))
1575 .map_err(sidecar_net_error)?;
1576 let guest_local_addr = stream.local_addr().map_err(sidecar_net_error)?;
1577 Self::from_stream(stream, None, guest_local_addr, resolved.guest_remote_addr)
1578 }
1579
1580 fn from_stream(
1581 stream: TcpStream,
1582 listener_id: Option<String>,
1583 guest_local_addr: SocketAddr,
1584 guest_remote_addr: SocketAddr,
1585 ) -> Result<Self, SidecarError> {
1586 let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
1587 read_stream
1588 .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
1589 .map_err(sidecar_net_error)?;
1590 let stream = Arc::new(Mutex::new(stream));
1591 let pending_read_stream = Arc::new(Mutex::new(Some(read_stream)));
1592 let (sender, events) = mpsc::channel();
1593 let tls_mode = Arc::new(AtomicBool::new(false));
1594 let tls_stream = Arc::new(Mutex::new(None));
1595 let tls_state = Arc::new(Mutex::new(None));
1596 let loopback_tls_pending_write = Arc::new(Mutex::new(None));
1597 let saw_local_shutdown = Arc::new(AtomicBool::new(false));
1598 let saw_remote_end = Arc::new(AtomicBool::new(false));
1599 let close_notified = Arc::new(AtomicBool::new(false));
1600
1601 Ok(Self {
1602 stream: Some(stream),
1603 pending_read_stream: Some(pending_read_stream),
1604 events: Some(events),
1605 event_sender: Some(sender),
1606 event_pusher: Arc::new(Mutex::new(None)),
1607 kernel_socket_id: None,
1608 no_delay: false,
1609 keep_alive: false,
1610 keep_alive_initial_delay_secs: None,
1611 guest_local_addr,
1612 guest_remote_addr,
1613 listener_id,
1614 tls_mode,
1615 tls_stream,
1616 tls_state,
1617 loopback_tls_pending_write,
1618 saw_local_shutdown,
1619 saw_remote_end,
1620 close_notified,
1621 })
1622 }
1623
1624 fn from_kernel(
1625 socket_id: SocketId,
1626 listener_id: Option<String>,
1627 guest_local_addr: SocketAddr,
1628 guest_remote_addr: SocketAddr,
1629 ) -> Self {
1630 let (sender, events) = mpsc::channel();
1631 Self {
1632 stream: None,
1633 pending_read_stream: None,
1634 events: Some(events),
1635 event_sender: Some(sender),
1636 event_pusher: Arc::new(Mutex::new(None)),
1637 kernel_socket_id: Some(socket_id),
1638 no_delay: false,
1639 keep_alive: false,
1640 keep_alive_initial_delay_secs: None,
1641 guest_local_addr,
1642 guest_remote_addr,
1643 listener_id,
1644 tls_mode: Arc::new(AtomicBool::new(false)),
1645 tls_stream: Arc::new(Mutex::new(None)),
1646 tls_state: Arc::new(Mutex::new(None)),
1647 loopback_tls_pending_write: Arc::new(Mutex::new(None)),
1648 saw_local_shutdown: Arc::new(AtomicBool::new(false)),
1649 saw_remote_end: Arc::new(AtomicBool::new(false)),
1650 close_notified: Arc::new(AtomicBool::new(false)),
1651 }
1652 }
1653
1654 fn set_event_pusher(&self, session: Option<V8SessionHandle>, socket_id: String) {
1655 let Some(session) = session else {
1656 return;
1657 };
1658 if let Ok(mut pusher) = self.event_pusher.lock() {
1659 *pusher = Some(JavascriptSocketEventPusher { session, socket_id });
1660 }
1661 }
1662
1663 fn poll(
1664 &mut self,
1665 kernel: &mut SidecarKernel,
1666 kernel_pid: u32,
1667 wait: Duration,
1668 trace_enabled: bool,
1669 ) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
1670 if self.tls_mode.load(Ordering::SeqCst) {
1671 self.ensure_tcp_reader()?;
1672 return match self
1673 .events
1674 .as_ref()
1675 .ok_or_else(|| {
1676 SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1677 })?
1678 .recv_timeout(wait)
1679 {
1680 Ok(event) => Ok(Some(event)),
1681 Err(RecvTimeoutError::Timeout) => Ok(None),
1682 Err(RecvTimeoutError::Disconnected) => Ok(None),
1683 };
1684 }
1685
1686 if let Some(socket_id) = self.kernel_socket_id {
1687 let poll_started = Instant::now();
1688 let result = kernel
1689 .poll_targets(
1690 EXECUTION_DRIVER_NAME,
1691 kernel_pid,
1692 vec![PollTargetEntry::socket(
1693 socket_id,
1694 POLLIN | POLLHUP | POLLERR,
1695 )],
1696 i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
1697 )
1698 .map_err(kernel_error)?;
1699 let poll_elapsed = poll_started.elapsed();
1700 let revents = result
1701 .targets
1702 .first()
1703 .map(|entry| entry.revents)
1704 .unwrap_or_else(PollEvents::empty);
1705 record_net_tcp_kernel_poll(trace_enabled, wait, poll_elapsed, revents);
1706 if revents.is_empty() {
1707 return Ok(None);
1708 }
1709 if revents.intersects(POLLIN) {
1710 let read_started = Instant::now();
1711 let read_result =
1712 kernel.socket_read(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, 64 * 1024);
1713 if trace_enabled {
1714 NET_TCP_TRACE_COUNTERS.socket_read_kernel_us.fetch_add(
1715 duration_micros_u64(read_started.elapsed()),
1716 Ordering::Relaxed,
1717 );
1718 }
1719 return match read_result {
1720 Ok(Some(bytes)) if !bytes.is_empty() => {
1721 if trace_enabled {
1722 NET_TCP_TRACE_COUNTERS
1723 .socket_read_data_events
1724 .fetch_add(1, Ordering::Relaxed);
1725 NET_TCP_TRACE_COUNTERS.socket_read_bytes.fetch_add(
1726 u64::try_from(bytes.len()).unwrap_or(u64::MAX),
1727 Ordering::Relaxed,
1728 );
1729 }
1730 Ok(Some(JavascriptTcpSocketEvent::Data(bytes)))
1731 }
1732 Ok(Some(_)) => {
1733 if trace_enabled {
1734 NET_TCP_TRACE_COUNTERS
1735 .socket_read_data_events
1736 .fetch_add(1, Ordering::Relaxed);
1737 }
1738 Ok(Some(JavascriptTcpSocketEvent::Data(Vec::new())))
1739 }
1740 Ok(None) => {
1741 if trace_enabled {
1742 NET_TCP_TRACE_COUNTERS
1743 .socket_read_end_events
1744 .fetch_add(1, Ordering::Relaxed);
1745 }
1746 self.saw_remote_end.store(true, Ordering::SeqCst);
1747 Ok(Some(JavascriptTcpSocketEvent::End))
1748 }
1749 Err(error) if error.code() == "EAGAIN" => {
1750 if trace_enabled {
1751 NET_TCP_TRACE_COUNTERS
1752 .socket_read_eagain
1753 .fetch_add(1, Ordering::Relaxed);
1754 }
1755 Ok(None)
1756 }
1757 Err(error) => {
1758 if trace_enabled {
1759 NET_TCP_TRACE_COUNTERS
1760 .socket_read_errors
1761 .fetch_add(1, Ordering::Relaxed);
1762 }
1763 Ok(Some(JavascriptTcpSocketEvent::Error {
1764 code: Some(error.code().to_string()),
1765 message: error.to_string(),
1766 }))
1767 }
1768 };
1769 }
1770 if revents.intersects(POLLHUP) {
1771 self.saw_remote_end.store(true, Ordering::SeqCst);
1772 return Ok(Some(JavascriptTcpSocketEvent::End));
1773 }
1774 if revents.intersects(POLLERR) {
1775 return Ok(Some(JavascriptTcpSocketEvent::Error {
1776 code: Some(String::from("EPIPE")),
1777 message: String::from("kernel TCP socket reported POLLERR"),
1778 }));
1779 }
1780 return Ok(None);
1781 }
1782
1783 self.ensure_tcp_reader()?;
1784 match self
1785 .events
1786 .as_ref()
1787 .ok_or_else(|| {
1788 SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1789 })?
1790 .recv_timeout(wait)
1791 {
1792 Ok(event) => Ok(Some(event)),
1793 Err(RecvTimeoutError::Timeout) => Ok(None),
1794 Err(RecvTimeoutError::Disconnected) => Ok(None),
1795 }
1796 }
1797
1798 fn ensure_tcp_reader(&self) -> Result<(), SidecarError> {
1799 if self.kernel_socket_id.is_some() {
1800 return Ok(());
1801 }
1802 if self.tls_mode.load(Ordering::SeqCst) {
1803 return Ok(());
1804 }
1805 let read_stream = self
1806 .pending_read_stream
1807 .as_ref()
1808 .ok_or_else(|| {
1809 SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1810 })?
1811 .lock()
1812 .map_err(|_| {
1813 SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1814 })?
1815 .take();
1816 if let Some(read_stream) = read_stream {
1817 spawn_tcp_socket_reader(
1818 read_stream,
1819 self.event_sender
1820 .as_ref()
1821 .ok_or_else(|| {
1822 SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1823 })?
1824 .clone(),
1825 Arc::clone(&self.event_pusher),
1826 Arc::clone(&self.tls_mode),
1827 Arc::clone(&self.saw_local_shutdown),
1828 Arc::clone(&self.saw_remote_end),
1829 Arc::clone(&self.close_notified),
1830 );
1831 }
1832 Ok(())
1833 }
1834
1835 fn socket_info(&self) -> Value {
1836 tcp_socket_info_value(&self.guest_local_addr, &self.guest_remote_addr)
1837 }
1838
1839 fn set_no_delay(&mut self, enable: bool) -> Result<(), SidecarError> {
1840 self.no_delay = enable;
1841 if self.kernel_socket_id.is_some() {
1842 return Ok(());
1843 }
1844 let stream = self
1845 .stream
1846 .as_ref()
1847 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1848 .lock()
1849 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1850 stream.set_nodelay(enable).map_err(sidecar_net_error)
1851 }
1852
1853 fn set_keep_alive(
1854 &mut self,
1855 enable: bool,
1856 initial_delay_secs: Option<u64>,
1857 ) -> Result<(), SidecarError> {
1858 self.keep_alive = enable;
1859 self.keep_alive_initial_delay_secs = initial_delay_secs;
1860 if self.kernel_socket_id.is_some() {
1861 return Ok(());
1862 }
1863 let stream = self
1864 .stream
1865 .as_ref()
1866 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1867 .lock()
1868 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1869 let socket = SockRef::from(&*stream);
1870 socket.set_keepalive(enable).map_err(sidecar_net_error)?;
1871 if enable {
1872 if let Some(delay_secs) = initial_delay_secs.filter(|delay_secs| *delay_secs > 0) {
1873 socket
1874 .set_tcp_keepalive(
1875 &TcpKeepalive::new().with_time(Duration::from_secs(delay_secs)),
1876 )
1877 .map_err(sidecar_net_error)?;
1878 }
1879 }
1880 Ok(())
1881 }
1882
1883 fn upgrade_tls(
1884 &self,
1885 vm_id: &str,
1886 kernel: &SidecarKernel,
1887 options: JavascriptTlsBridgeOptions,
1888 ) -> Result<(), SidecarError> {
1889 if self.tls_mode.load(Ordering::SeqCst) {
1890 return Ok(());
1891 }
1892
1893 let client_hello = if options.is_server {
1894 self.peek_tls_client_hello(vm_id, kernel)?
1895 } else {
1896 None
1897 };
1898
1899 let (tls_stream, loopback_pending_write) = if let Some(socket_id) = self.kernel_socket_id {
1900 let peer_socket_id = wait_for_loopback_peer_socket_id(kernel, socket_id)
1901 .ok_or_else(|| {
1902 SidecarError::Execution(format!(
1903 "ERR_NOT_IMPLEMENTED: kernel-backed loopback socket {socket_id} has no peer for TLS upgrade"
1904 ))
1905 })?;
1906 let endpoint = loopback_tls_endpoint(vm_id, socket_id, peer_socket_id)?;
1907 let pending_write = LoopbackTlsPendingWriteHandle::new(&endpoint);
1908 let tls_stream = if options.is_server {
1909 ActiveTlsStream::LoopbackServer(build_server_loopback_tls_stream(
1910 endpoint, &options,
1911 )?)
1912 } else {
1913 ActiveTlsStream::LoopbackClient(build_client_loopback_tls_stream(
1914 endpoint, &options,
1915 )?)
1916 };
1917 (tls_stream, Some(pending_write))
1918 } else {
1919 self.pending_read_stream
1920 .as_ref()
1921 .ok_or_else(|| {
1922 SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1923 })?
1924 .lock()
1925 .map_err(|_| {
1926 SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1927 })?
1928 .take();
1929 let stream = self
1930 .stream
1931 .as_ref()
1932 .ok_or_else(|| {
1933 SidecarError::InvalidState(String::from("TCP socket stream missing"))
1934 })?
1935 .lock()
1936 .map_err(|_| {
1937 SidecarError::InvalidState(String::from("TCP socket lock poisoned"))
1938 })?;
1939 let cloned = stream.try_clone().map_err(sidecar_net_error)?;
1940 drop(stream);
1941
1942 if options.is_server {
1943 (
1944 ActiveTlsStream::Server(build_server_tls_stream(cloned, &options)?),
1945 None,
1946 )
1947 } else {
1948 (
1949 ActiveTlsStream::Client(build_client_tls_stream(cloned, &options)?),
1950 None,
1951 )
1952 }
1953 };
1954
1955 let tls_state = ActiveTlsState {
1956 client_hello,
1957 local_certificates: tls_local_certificates(&options)?,
1958 session_reused: false,
1959 };
1960
1961 self.tls_mode.store(true, Ordering::SeqCst);
1962 {
1963 let mut state = self
1964 .tls_state
1965 .lock()
1966 .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?;
1967 *state = Some(tls_state);
1968 }
1969 {
1970 let mut stream = self.tls_stream.lock().map_err(|_| {
1971 SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
1972 })?;
1973 *stream = Some(tls_stream);
1974 }
1975 {
1976 let mut pending = self.loopback_tls_pending_write.lock().map_err(|_| {
1977 SidecarError::InvalidState(String::from(
1978 "loopback TLS pending write handle lock poisoned",
1979 ))
1980 })?;
1981 *pending = loopback_pending_write.clone();
1982 }
1983
1984 spawn_tls_socket_reader(
1985 Arc::clone(&self.tls_stream),
1986 loopback_pending_write,
1987 self.event_sender
1988 .as_ref()
1989 .ok_or_else(|| {
1990 SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1991 })?
1992 .clone(),
1993 Arc::clone(&self.event_pusher),
1994 Arc::clone(&self.saw_local_shutdown),
1995 Arc::clone(&self.saw_remote_end),
1996 Arc::clone(&self.close_notified),
1997 );
1998 Ok(())
1999 }
2000
2001 fn peek_tls_client_hello(
2002 &self,
2003 vm_id: &str,
2004 kernel: &SidecarKernel,
2005 ) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
2006 if let Some(socket_id) = self.kernel_socket_id {
2007 let Some(peer_socket_id) = kernel
2008 .socket_get(socket_id)
2009 .and_then(|record| record.peer_socket_id())
2010 else {
2011 return Ok(None);
2012 };
2013 return peek_loopback_tls_client_hello(vm_id, socket_id, peer_socket_id);
2014 }
2015
2016 let stream = self
2017 .stream
2018 .as_ref()
2019 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
2020 .lock()
2021 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
2022 let mut buffer = vec![0_u8; 16 * 1024];
2023 let bytes = match stream.peek(&mut buffer) {
2024 Ok(0) => return Ok(None),
2025 Ok(bytes) => bytes,
2026 Err(error)
2027 if matches!(
2028 error.kind(),
2029 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
2030 ) =>
2031 {
2032 return Ok(None);
2033 }
2034 Err(error) => return Err(sidecar_net_error(error)),
2035 };
2036 parse_tls_client_hello_from_bytes(&buffer[..bytes])
2037 }
2038
2039 fn tls_client_hello_json(
2040 &self,
2041 vm_id: &str,
2042 kernel: &SidecarKernel,
2043 ) -> Result<Value, SidecarError> {
2044 if let Some(client_hello) = self
2045 .tls_state
2046 .lock()
2047 .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
2048 .as_ref()
2049 .and_then(|state| state.client_hello.clone())
2050 {
2051 return javascript_net_json_string(
2052 serde_json::to_value(client_hello).map_err(|error| {
2053 SidecarError::InvalidState(format!(
2054 "failed to serialize TLS client hello: {error}"
2055 ))
2056 })?,
2057 "net.socket_get_tls_client_hello",
2058 );
2059 }
2060
2061 javascript_net_json_string(
2062 serde_json::to_value(
2063 self.peek_tls_client_hello(vm_id, kernel)?
2064 .unwrap_or_default(),
2065 )
2066 .map_err(|error| {
2067 SidecarError::InvalidState(format!("failed to serialize TLS client hello: {error}"))
2068 })?,
2069 "net.socket_get_tls_client_hello",
2070 )
2071 }
2072
2073 fn tls_query(&self, query: &str, detailed: bool) -> Result<Value, SidecarError> {
2074 let state = self
2075 .tls_state
2076 .lock()
2077 .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
2078 .clone();
2079 let mut tls_stream = self
2080 .tls_stream
2081 .lock()
2082 .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?;
2083 let Some(stream) = tls_stream.as_mut() else {
2084 return javascript_net_json_string(
2085 tls_bridge_undefined_value(),
2086 "net.socket_tls_query",
2087 );
2088 };
2089
2090 let payload = match query {
2091 "getSession" => tls_bridge_undefined_value(),
2092 "isSessionReused" => Value::Bool(
2093 state
2094 .as_ref()
2095 .is_some_and(|tls_state| tls_state.session_reused),
2096 ),
2097 "getPeerCertificate" => {
2098 let certificate = stream
2099 .peer_certificates()
2100 .and_then(|certificates| certificates.first())
2101 .map(|certificate| {
2102 tls_certificate_bridge_value(certificate.as_ref(), detailed)
2103 });
2104 certificate.unwrap_or_else(tls_bridge_undefined_value)
2105 }
2106 "getCertificate" => state
2107 .as_ref()
2108 .and_then(|tls_state| tls_state.local_certificates.first())
2109 .map(|certificate| tls_certificate_bridge_value(certificate, detailed))
2110 .unwrap_or_else(tls_bridge_undefined_value),
2111 "getProtocol" => stream
2112 .protocol_version()
2113 .map(tls_protocol_name)
2114 .map(Value::String)
2115 .unwrap_or(Value::Null),
2116 "getCipher" => stream
2117 .negotiated_cipher_suite()
2118 .map(tls_cipher_bridge_value)
2119 .unwrap_or_else(tls_bridge_undefined_value),
2120 other => {
2121 return Err(SidecarError::InvalidState(format!(
2122 "unsupported TLS query {other}"
2123 )));
2124 }
2125 };
2126 javascript_net_json_string(payload, "net.socket_tls_query")
2127 }
2128
2129 fn write_all(
2130 &self,
2131 kernel: &mut SidecarKernel,
2132 kernel_pid: u32,
2133 contents: &[u8],
2134 ) -> Result<usize, SidecarError> {
2135 if self.tls_mode.load(Ordering::SeqCst) {
2136 let loopback_pending_write = self
2137 .loopback_tls_pending_write
2138 .lock()
2139 .map_err(|_| {
2140 SidecarError::InvalidState(String::from(
2141 "loopback TLS pending write handle lock poisoned",
2142 ))
2143 })?
2144 .clone();
2145 if let Some(pending_write) = loopback_pending_write.as_ref() {
2146 if pending_write.should_buffer_write()? {
2147 pending_write.append_write(contents)?;
2148 return Ok(contents.len());
2149 }
2150 pending_write.interrupt_own_reader();
2151 }
2152 let mut tls_stream = self.tls_stream.lock().map_err(|_| {
2153 SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
2154 })?;
2155 let stream = tls_stream.as_mut().ok_or_else(|| {
2156 SidecarError::InvalidState(String::from("TLS stream missing for upgraded socket"))
2157 })?;
2158 stream.write_all(contents)?;
2159 return Ok(contents.len());
2160 }
2161 if let Some(socket_id) = self.kernel_socket_id {
2162 return kernel
2163 .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, contents)
2164 .map_err(kernel_error);
2165 }
2166
2167 let mut stream = self
2168 .stream
2169 .as_ref()
2170 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
2171 .lock()
2172 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
2173 stream.write_all(contents).map_err(sidecar_net_error)?;
2174 Ok(contents.len())
2175 }
2176
2177 fn shutdown_write(
2178 &self,
2179 kernel: &mut SidecarKernel,
2180 kernel_pid: u32,
2181 ) -> Result<(), SidecarError> {
2182 if self.tls_mode.load(Ordering::SeqCst) {
2183 let loopback_pending_write = self
2184 .loopback_tls_pending_write
2185 .lock()
2186 .map_err(|_| {
2187 SidecarError::InvalidState(String::from(
2188 "loopback TLS pending write handle lock poisoned",
2189 ))
2190 })?
2191 .clone();
2192 if let Some(pending_write) = loopback_pending_write.as_ref() {
2193 if pending_write.should_buffer_write()? {
2194 pending_write.defer_shutdown_write()?;
2195 self.saw_local_shutdown.store(true, Ordering::SeqCst);
2196 return Ok(());
2197 }
2198 pending_write.interrupt_own_reader();
2199 }
2200 if let Some(stream) = self
2201 .tls_stream
2202 .lock()
2203 .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
2204 .as_mut()
2205 {
2206 let _ = stream.send_close_notify();
2207 let _ = stream.shutdown_write();
2208 }
2209 if self.kernel_socket_id.is_some() {
2210 self.saw_local_shutdown.store(true, Ordering::SeqCst);
2211 return Ok(());
2212 }
2213 }
2214 if let Some(socket_id) = self.kernel_socket_id {
2215 self.saw_local_shutdown.store(true, Ordering::SeqCst);
2216 match kernel.socket_shutdown(
2217 EXECUTION_DRIVER_NAME,
2218 kernel_pid,
2219 socket_id,
2220 KernelSocketShutdown::Write,
2221 ) {
2222 Ok(()) => {}
2223 Err(error) if error.code() == "ENOENT" => {}
2224 Err(error) => return Err(kernel_error(error)),
2225 }
2226 return Ok(());
2227 }
2228 let stream = self
2229 .stream
2230 .as_ref()
2231 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
2232 .lock()
2233 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
2234 self.saw_local_shutdown.store(true, Ordering::SeqCst);
2235 match stream.shutdown(Shutdown::Write) {
2236 Ok(()) => {}
2237 Err(error) if error.kind() == std::io::ErrorKind::NotConnected => {}
2238 Err(error) => return Err(sidecar_net_error(error)),
2239 }
2240 if self.saw_remote_end.load(Ordering::SeqCst)
2241 && !self.close_notified.swap(true, Ordering::SeqCst)
2242 {
2243 let _ = self
2244 .event_sender
2245 .as_ref()
2246 .ok_or_else(|| {
2247 SidecarError::InvalidState(String::from("TCP socket event sender missing"))
2248 })?
2249 .send(JavascriptTcpSocketEvent::Close { had_error: false });
2250 }
2251 Ok(())
2252 }
2253
2254 fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
2255 if self.tls_mode.load(Ordering::SeqCst) {
2256 if let Some(pending_write) = self
2257 .loopback_tls_pending_write
2258 .lock()
2259 .map_err(|_| {
2260 SidecarError::InvalidState(String::from(
2261 "loopback TLS pending write handle lock poisoned",
2262 ))
2263 })?
2264 .take()
2265 {
2266 pending_write.clear_for_close();
2267 pending_write.interrupt_own_reader();
2268 }
2269 if let Some(stream) = self
2270 .tls_stream
2271 .lock()
2272 .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
2273 .as_mut()
2274 {
2275 let _ = stream.send_close_notify();
2276 let _ = stream.close();
2277 }
2278 }
2279 if let Some(socket_id) = self.kernel_socket_id {
2280 return close_kernel_socket_idempotent(kernel, kernel_pid, socket_id);
2281 }
2282 let stream = self
2283 .stream
2284 .as_ref()
2285 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
2286 .lock()
2287 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
2288 stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
2289 }
2290}
2291
2292fn close_kernel_socket_idempotent(
2293 kernel: &mut SidecarKernel,
2294 kernel_pid: u32,
2295 socket_id: SocketId,
2296) -> Result<(), SidecarError> {
2297 match kernel.socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id) {
2298 Ok(()) => Ok(()),
2299 Err(error) if error.code() == "ENOENT" => Ok(()),
2300 Err(error) => Err(kernel_error(error)),
2301 }
2302}
2303
2304fn register_kernel_readiness_target(
2305 registry: &KernelSocketReadinessRegistry,
2306 kernel_socket_id: Option<SocketId>,
2307 session: Option<V8SessionHandle>,
2308 target_id: String,
2309 event: KernelSocketReadinessEvent,
2310) {
2311 let (Some(kernel_socket_id), Some(session)) = (kernel_socket_id, session) else {
2312 return;
2313 };
2314 if let Ok(mut targets) = registry.lock() {
2315 targets.insert(
2316 kernel_socket_id,
2317 KernelSocketReadinessTarget {
2318 session,
2319 target_id,
2320 event,
2321 },
2322 );
2323 }
2324}
2325
2326fn unregister_kernel_readiness_target(
2327 registry: &KernelSocketReadinessRegistry,
2328 kernel_socket_id: Option<SocketId>,
2329) {
2330 let Some(kernel_socket_id) = kernel_socket_id else {
2331 return;
2332 };
2333 if let Ok(mut targets) = registry.lock() {
2334 targets.remove(&kernel_socket_id);
2335 }
2336}
2337
2338fn release_tcp_socket_handle(
2339 process: &mut ActiveProcess,
2340 socket_id: &str,
2341 socket: ActiveTcpSocket,
2342 kernel: &mut SidecarKernel,
2343 kernel_readiness: &KernelSocketReadinessRegistry,
2344) {
2345 unregister_kernel_readiness_target(kernel_readiness, socket.kernel_socket_id);
2346 if let Some(listener_id) = socket.listener_id.as_deref() {
2347 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
2348 listener.release_connection(socket_id);
2349 }
2350 }
2351 let _ = socket.close(kernel, process.kernel_pid);
2352}
2353
2354fn release_unix_socket_handle(
2355 process: &mut ActiveProcess,
2356 socket_id: &str,
2357 socket: ActiveUnixSocket,
2358) {
2359 if let Some(listener_id) = socket.listener_id.as_deref() {
2360 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
2361 listener.release_connection(socket_id);
2362 }
2363 }
2364 let _ = socket.close();
2365}
2366
2367impl ActiveTlsStream {
2368 fn is_loopback(&self) -> bool {
2369 matches!(self, Self::LoopbackClient(_) | Self::LoopbackServer(_))
2370 }
2371
2372 fn is_handshaking(&self) -> bool {
2373 match self {
2374 Self::Client(stream) => stream.conn.is_handshaking(),
2375 Self::Server(stream) => stream.conn.is_handshaking(),
2376 Self::LoopbackClient(stream) => stream.conn.is_handshaking(),
2377 Self::LoopbackServer(stream) => stream.conn.is_handshaking(),
2378 }
2379 }
2380
2381 fn set_loopback_poll_timeout(&mut self, timeout: Duration) {
2382 match self {
2383 Self::LoopbackClient(stream) => stream.sock.set_poll_timeout(timeout),
2384 Self::LoopbackServer(stream) => stream.sock.set_poll_timeout(timeout),
2385 Self::Client(_) | Self::Server(_) => {}
2386 }
2387 }
2388
2389 fn write_all(&mut self, contents: &[u8]) -> Result<(), SidecarError> {
2390 match self {
2391 Self::Client(stream) => {
2392 stream.write_all(contents).map_err(sidecar_net_error)?;
2393 stream.flush().map_err(sidecar_net_error)
2394 }
2395 Self::Server(stream) => {
2396 stream.write_all(contents).map_err(sidecar_net_error)?;
2397 stream.flush().map_err(sidecar_net_error)
2398 }
2399 Self::LoopbackClient(stream) => {
2400 stream.write_all(contents).map_err(sidecar_net_error)?;
2401 stream.flush().map_err(sidecar_net_error)
2402 }
2403 Self::LoopbackServer(stream) => {
2404 stream.write_all(contents).map_err(sidecar_net_error)?;
2405 stream.flush().map_err(sidecar_net_error)
2406 }
2407 }
2408 }
2409
2410 fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
2411 match self {
2412 Self::Client(stream) => stream.read(buffer),
2413 Self::Server(stream) => stream.read(buffer),
2414 Self::LoopbackClient(stream) => stream.read(buffer),
2415 Self::LoopbackServer(stream) => stream.read(buffer),
2416 }
2417 }
2418
2419 fn send_close_notify(&mut self) -> Result<(), SidecarError> {
2420 match self {
2421 Self::Client(stream) => {
2422 stream.conn.send_close_notify();
2423 let _ = stream.conn.complete_io(&mut stream.sock);
2424 }
2425 Self::Server(stream) => {
2426 stream.conn.send_close_notify();
2427 let _ = stream.conn.complete_io(&mut stream.sock);
2428 }
2429 Self::LoopbackClient(stream) => {
2430 stream.conn.send_close_notify();
2431 let _ = stream.conn.complete_io(&mut stream.sock);
2432 }
2433 Self::LoopbackServer(stream) => {
2434 stream.conn.send_close_notify();
2435 let _ = stream.conn.complete_io(&mut stream.sock);
2436 }
2437 }
2438 Ok(())
2439 }
2440
2441 fn shutdown_write(&mut self) -> Result<(), SidecarError> {
2442 match self {
2443 Self::Client(stream) => stream
2444 .sock
2445 .shutdown(Shutdown::Write)
2446 .map_err(sidecar_net_error),
2447 Self::Server(stream) => stream
2448 .sock
2449 .shutdown(Shutdown::Write)
2450 .map_err(sidecar_net_error),
2451 Self::LoopbackClient(stream) => stream.sock.shutdown_write(),
2452 Self::LoopbackServer(stream) => stream.sock.shutdown_write(),
2453 }
2454 }
2455
2456 fn close(&mut self) -> Result<(), SidecarError> {
2457 match self {
2458 Self::Client(stream) => stream
2459 .sock
2460 .shutdown(Shutdown::Both)
2461 .map_err(sidecar_net_error),
2462 Self::Server(stream) => stream
2463 .sock
2464 .shutdown(Shutdown::Both)
2465 .map_err(sidecar_net_error),
2466 Self::LoopbackClient(stream) => stream.sock.close_endpoint(),
2467 Self::LoopbackServer(stream) => stream.sock.close_endpoint(),
2468 }
2469 }
2470
2471 fn peer_certificates(&self) -> Option<&[CertificateDer<'static>]> {
2472 match self {
2473 Self::Client(stream) => stream.conn.peer_certificates(),
2474 Self::Server(stream) => stream.conn.peer_certificates(),
2475 Self::LoopbackClient(stream) => stream.conn.peer_certificates(),
2476 Self::LoopbackServer(stream) => stream.conn.peer_certificates(),
2477 }
2478 }
2479
2480 fn negotiated_cipher_suite(&self) -> Option<rustls::SupportedCipherSuite> {
2481 match self {
2482 Self::Client(stream) => stream.conn.negotiated_cipher_suite(),
2483 Self::Server(stream) => stream.conn.negotiated_cipher_suite(),
2484 Self::LoopbackClient(stream) => stream.conn.negotiated_cipher_suite(),
2485 Self::LoopbackServer(stream) => stream.conn.negotiated_cipher_suite(),
2486 }
2487 }
2488
2489 fn protocol_version(&self) -> Option<rustls::ProtocolVersion> {
2490 match self {
2491 Self::Client(stream) => stream.conn.protocol_version(),
2492 Self::Server(stream) => stream.conn.protocol_version(),
2493 Self::LoopbackClient(stream) => stream.conn.protocol_version(),
2494 Self::LoopbackServer(stream) => stream.conn.protocol_version(),
2495 }
2496 }
2497}
2498
2499impl ActiveUnixSocket {
2504 fn connect(host_path: &Path, guest_path: &str) -> Result<Self, SidecarError> {
2505 let stream = UnixStream::connect(host_path).map_err(sidecar_net_error)?;
2506 Self::from_stream(stream, None, None, Some(guest_path.to_owned()))
2507 }
2508
2509 fn from_stream(
2510 stream: UnixStream,
2511 listener_id: Option<String>,
2512 local_path: Option<String>,
2513 remote_path: Option<String>,
2514 ) -> Result<Self, SidecarError> {
2515 let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
2516 let stream = Arc::new(Mutex::new(stream));
2517 let (sender, events) = mpsc::channel();
2518 let event_pusher = Arc::new(Mutex::new(None));
2519 let saw_local_shutdown = Arc::new(AtomicBool::new(false));
2520 let saw_remote_end = Arc::new(AtomicBool::new(false));
2521 let close_notified = Arc::new(AtomicBool::new(false));
2522 spawn_unix_socket_reader(
2523 read_stream,
2524 sender.clone(),
2525 Arc::clone(&event_pusher),
2526 Arc::clone(&saw_local_shutdown),
2527 Arc::clone(&saw_remote_end),
2528 Arc::clone(&close_notified),
2529 );
2530
2531 Ok(Self {
2532 stream,
2533 events,
2534 event_sender: sender,
2535 event_pusher,
2536 listener_id,
2537 local_path,
2538 remote_path,
2539 saw_local_shutdown,
2540 saw_remote_end,
2541 close_notified,
2542 })
2543 }
2544
2545 fn set_event_pusher(&self, session: Option<V8SessionHandle>, socket_id: String) {
2546 let Some(session) = session else {
2547 return;
2548 };
2549 if let Ok(mut pusher) = self.event_pusher.lock() {
2550 *pusher = Some(JavascriptSocketEventPusher { session, socket_id });
2551 }
2552 }
2553
2554 fn poll(&mut self, wait: Duration) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
2555 match self.events.recv_timeout(wait) {
2556 Ok(event) => Ok(Some(event)),
2557 Err(RecvTimeoutError::Timeout) => Ok(None),
2558 Err(RecvTimeoutError::Disconnected) => Ok(None),
2559 }
2560 }
2561
2562 fn socket_info(&self) -> Value {
2563 unix_socket_info_value(self.local_path.as_deref(), self.remote_path.as_deref())
2564 }
2565
2566 fn write_all(&self, contents: &[u8]) -> Result<usize, SidecarError> {
2567 let mut stream = self
2568 .stream
2569 .lock()
2570 .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
2571 stream.write_all(contents).map_err(sidecar_net_error)?;
2572 Ok(contents.len())
2573 }
2574
2575 fn shutdown_write(&self) -> Result<(), SidecarError> {
2576 let stream = self
2577 .stream
2578 .lock()
2579 .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
2580 self.saw_local_shutdown.store(true, Ordering::SeqCst);
2581 stream
2582 .shutdown(Shutdown::Write)
2583 .map_err(sidecar_net_error)?;
2584 if self.saw_remote_end.load(Ordering::SeqCst)
2585 && !self.close_notified.swap(true, Ordering::SeqCst)
2586 {
2587 let _ = self
2588 .event_sender
2589 .send(JavascriptTcpSocketEvent::Close { had_error: false });
2590 }
2591 Ok(())
2592 }
2593
2594 fn close(&self) -> Result<(), SidecarError> {
2595 let stream = self
2596 .stream
2597 .lock()
2598 .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
2599 stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
2600 }
2601}
2602
2603impl ActiveUnixListener {
2606 fn bind(
2607 host_path: &Path,
2608 guest_path: &str,
2609 backlog: Option<u32>,
2610 ) -> Result<Self, SidecarError> {
2611 if let Some(parent) = host_path.parent() {
2612 fs::create_dir_all(parent).map_err(sidecar_net_error)?;
2613 }
2614 let listener = UnixListener::bind(host_path).map_err(sidecar_net_error)?;
2615 listener.set_nonblocking(true).map_err(sidecar_net_error)?;
2616 Ok(Self {
2617 listener,
2618 path: guest_path.to_owned(),
2619 backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2620 .expect("default backlog fits within usize"),
2621 active_connection_ids: BTreeSet::new(),
2622 })
2623 }
2624
2625 fn path(&self) -> &str {
2626 &self.path
2627 }
2628
2629 fn poll(
2630 &mut self,
2631 wait: Duration,
2632 ) -> Result<Option<JavascriptUnixListenerEvent>, SidecarError> {
2633 let deadline = Instant::now() + wait;
2634 loop {
2635 match self.listener.accept() {
2636 Ok((stream, remote_addr)) => {
2637 if self.active_connection_ids.len() >= self.backlog {
2638 let _ = stream.shutdown(Shutdown::Both);
2639 if wait.is_zero() || Instant::now() >= deadline {
2640 return Ok(None);
2641 }
2642 continue;
2643 }
2644
2645 let local_path = Some(self.path.clone());
2646 let remote_path = unix_socket_path(&remote_addr);
2647 return Ok(Some(JavascriptUnixListenerEvent::Connection(
2648 PendingUnixSocket {
2649 stream,
2650 local_path,
2651 remote_path,
2652 },
2653 )));
2654 }
2655 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2656 if wait.is_zero() || Instant::now() >= deadline {
2657 return Ok(None);
2658 }
2659 if !wait_fd_readable_until(self.listener.as_fd(), deadline) {
2660 return Ok(None);
2661 }
2662 }
2663 Err(error) => {
2664 return Ok(Some(JavascriptUnixListenerEvent::Error {
2665 code: io_error_code(&error),
2666 message: error.to_string(),
2667 }));
2668 }
2669 }
2670 }
2671 }
2672
2673 fn close(&self) -> Result<(), SidecarError> {
2674 Ok(())
2675 }
2676
2677 fn active_connection_count(&self) -> usize {
2678 self.active_connection_ids.len()
2679 }
2680
2681 fn register_connection(&mut self, socket_id: &str) {
2682 self.active_connection_ids.insert(socket_id.to_string());
2683 }
2684
2685 fn release_connection(&mut self, socket_id: &str) {
2686 self.active_connection_ids.remove(socket_id);
2687 }
2688}
2689
2690impl ActiveTcpListener {
2691 fn bind(
2692 bind_host: &str,
2693 guest_host: &str,
2694 guest_port: u16,
2695 backlog: Option<u32>,
2696 ) -> Result<Self, SidecarError> {
2697 let bind_addr = resolve_tcp_bind_addr(bind_host, 0)?;
2698 let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
2699 let listener = TcpListener::bind(bind_addr).map_err(sidecar_net_error)?;
2700 listener.set_nonblocking(true).map_err(sidecar_net_error)?;
2701 let local_addr = listener.local_addr().map_err(sidecar_net_error)?;
2702 Ok(Self {
2703 listener: Some(listener),
2704 kernel_socket_id: None,
2705 local_addr: Some(local_addr),
2706 guest_local_addr: guest_addr,
2707 backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2708 .expect("default backlog fits within usize"),
2709 active_connection_ids: BTreeSet::new(),
2710 })
2711 }
2712
2713 fn bind_kernel(
2714 kernel: &mut SidecarKernel,
2715 kernel_pid: u32,
2716 guest_host: &str,
2717 guest_port: u16,
2718 backlog: Option<u32>,
2719 ) -> Result<Self, SidecarError> {
2720 let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
2721 let spec = match guest_addr {
2722 SocketAddr::V4(_) => SocketSpec::tcp(),
2723 SocketAddr::V6(_) => SocketSpec::new(SocketDomain::Inet6, SocketType::Stream),
2724 };
2725 let socket_id = kernel
2726 .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
2727 .map_err(kernel_error)?;
2728 kernel
2729 .socket_bind_inet(
2730 EXECUTION_DRIVER_NAME,
2731 kernel_pid,
2732 socket_id,
2733 InetSocketAddress::new(guest_addr.ip().to_string(), guest_addr.port()),
2734 )
2735 .map_err(kernel_error)?;
2736 kernel
2737 .socket_listen(
2738 EXECUTION_DRIVER_NAME,
2739 kernel_pid,
2740 socket_id,
2741 usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2742 .expect("default backlog fits within usize"),
2743 )
2744 .map_err(kernel_error)?;
2745 Ok(Self {
2746 listener: None,
2747 kernel_socket_id: Some(socket_id),
2748 local_addr: Some(guest_addr),
2749 guest_local_addr: guest_addr,
2750 backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2751 .expect("default backlog fits within usize"),
2752 active_connection_ids: BTreeSet::new(),
2753 })
2754 }
2755
2756 pub(crate) fn local_addr(&self) -> SocketAddr {
2757 self.local_addr.unwrap_or(self.guest_local_addr)
2758 }
2759
2760 fn guest_local_addr(&self) -> SocketAddr {
2761 self.guest_local_addr
2762 }
2763
2764 fn poll(
2765 &mut self,
2766 kernel: &mut SidecarKernel,
2767 kernel_pid: u32,
2768 wait: Duration,
2769 trace_enabled: bool,
2770 ) -> Result<Option<JavascriptTcpListenerEvent>, SidecarError> {
2771 if let Some(socket_id) = self.kernel_socket_id {
2772 let poll_started = Instant::now();
2773 let result = kernel
2774 .poll_targets(
2775 EXECUTION_DRIVER_NAME,
2776 kernel_pid,
2777 vec![PollTargetEntry::socket(socket_id, POLLIN)],
2778 i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
2779 )
2780 .map_err(kernel_error)?;
2781 let poll_elapsed = poll_started.elapsed();
2782 let revents = result
2783 .targets
2784 .first()
2785 .map(|entry| entry.revents)
2786 .unwrap_or_else(PollEvents::empty);
2787 record_net_tcp_kernel_poll(trace_enabled, wait, poll_elapsed, revents);
2788 if revents.is_empty() {
2789 return Ok(None);
2790 }
2791 let accepted_socket_id =
2792 match kernel.socket_accept(EXECUTION_DRIVER_NAME, kernel_pid, socket_id) {
2793 Ok(accepted_socket_id) => accepted_socket_id,
2794 Err(error) if error.code() == "EAGAIN" => {
2795 if trace_enabled {
2796 NET_TCP_TRACE_COUNTERS
2797 .server_accept_eagain
2798 .fetch_add(1, Ordering::Relaxed);
2799 }
2800 return Ok(None);
2801 }
2802 Err(error) => {
2803 if trace_enabled {
2804 NET_TCP_TRACE_COUNTERS
2805 .server_accept_errors
2806 .fetch_add(1, Ordering::Relaxed);
2807 }
2808 return Ok(Some(JavascriptTcpListenerEvent::Error {
2809 code: Some(error.code().to_string()),
2810 message: error.to_string(),
2811 }));
2812 }
2813 };
2814 let accepted = kernel.socket_get(accepted_socket_id).ok_or_else(|| {
2815 SidecarError::InvalidState(format!(
2816 "accepted kernel TCP socket {accepted_socket_id} is missing"
2817 ))
2818 })?;
2819 let local_addr = accepted.local_address().ok_or_else(|| {
2820 SidecarError::InvalidState(format!(
2821 "accepted kernel TCP socket {accepted_socket_id} missing local address"
2822 ))
2823 })?;
2824 let remote_addr = accepted.peer_address().ok_or_else(|| {
2825 SidecarError::InvalidState(format!(
2826 "accepted kernel TCP socket {accepted_socket_id} missing peer address"
2827 ))
2828 })?;
2829 if trace_enabled {
2830 NET_TCP_TRACE_COUNTERS
2831 .server_accept_connections
2832 .fetch_add(1, Ordering::Relaxed);
2833 }
2834 return Ok(Some(JavascriptTcpListenerEvent::Connection(
2835 PendingTcpSocket {
2836 stream: None,
2837 kernel_socket_id: Some(accepted_socket_id),
2838 preallocated: true,
2839 guest_local_addr: resolve_tcp_bind_addr(local_addr.host(), local_addr.port())?,
2840 guest_remote_addr: resolve_tcp_bind_addr(
2841 remote_addr.host(),
2842 remote_addr.port(),
2843 )?,
2844 },
2845 )));
2846 }
2847
2848 let deadline = Instant::now() + wait;
2849 loop {
2850 match self
2851 .listener
2852 .as_ref()
2853 .ok_or_else(|| {
2854 SidecarError::InvalidState(String::from("TCP listener socket missing"))
2855 })?
2856 .accept()
2857 {
2858 Ok((stream, remote_addr)) => {
2859 if self.active_connection_ids.len() >= self.backlog {
2860 let _ = stream.shutdown(Shutdown::Both);
2861 if wait.is_zero() || Instant::now() >= deadline {
2862 return Ok(None);
2863 }
2864 continue;
2865 }
2866 return Ok(Some(JavascriptTcpListenerEvent::Connection(
2867 PendingTcpSocket {
2868 stream: Some(stream),
2869 kernel_socket_id: None,
2870 preallocated: false,
2871 guest_local_addr: self.guest_local_addr,
2872 guest_remote_addr: SocketAddr::new(
2873 remote_addr.ip(),
2874 remote_addr.port(),
2875 ),
2876 },
2877 )));
2878 }
2879 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2880 if wait.is_zero() || Instant::now() >= deadline {
2881 return Ok(None);
2882 }
2883 if !wait_fd_readable_until(
2884 self.listener
2885 .as_ref()
2886 .expect("TCP listener checked before accept")
2887 .as_fd(),
2888 deadline,
2889 ) {
2890 return Ok(None);
2891 }
2892 }
2893 Err(error) => {
2894 return Ok(Some(JavascriptTcpListenerEvent::Error {
2895 code: io_error_code(&error),
2896 message: error.to_string(),
2897 }));
2898 }
2899 }
2900 }
2901 }
2902
2903 fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
2904 if let Some(socket_id) = self.kernel_socket_id {
2905 close_kernel_socket_idempotent(kernel, kernel_pid, socket_id)?;
2906 }
2907 Ok(())
2908 }
2909
2910 fn active_connection_count(&self) -> usize {
2911 self.active_connection_ids.len()
2912 }
2913
2914 fn register_connection(&mut self, socket_id: &str) {
2915 self.active_connection_ids.insert(socket_id.to_string());
2916 }
2917
2918 fn release_connection(&mut self, socket_id: &str) {
2919 self.active_connection_ids.remove(socket_id);
2920 }
2921}
2922
2923impl ActiveUdpSocket {
2926 fn new(
2927 kernel: &mut SidecarKernel,
2928 kernel_pid: u32,
2929 family: JavascriptUdpFamily,
2930 ) -> Result<Self, SidecarError> {
2931 let spec = match family {
2932 JavascriptUdpFamily::Ipv4 => SocketSpec::udp(),
2933 JavascriptUdpFamily::Ipv6 => SocketSpec::new(SocketDomain::Inet6, SocketType::Datagram),
2934 };
2935 let socket_id = kernel
2936 .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
2937 .map_err(kernel_error)?;
2938 Ok(Self {
2939 family,
2940 socket: None,
2941 kernel_socket_id: Some(socket_id),
2942 guest_local_addr: None,
2943 recv_buffer_size: 0,
2944 send_buffer_size: 0,
2945 })
2946 }
2947
2948 fn local_addr(&self) -> Option<SocketAddr> {
2949 self.guest_local_addr
2950 }
2951
2952 fn socket(&self) -> Result<&UdpSocket, SidecarError> {
2953 self.socket
2954 .as_ref()
2955 .ok_or_else(|| SidecarError::Execution(String::from("EBADF: bad file descriptor")))
2956 }
2957
2958 fn bind(
2959 &mut self,
2960 kernel: &mut SidecarKernel,
2961 kernel_pid: u32,
2962 host: Option<&str>,
2963 port: u16,
2964 context: &JavascriptSocketPathContext,
2965 ) -> Result<SocketAddr, SidecarError> {
2966 if self.socket.is_some() || self.guest_local_addr.is_some() {
2967 return Err(SidecarError::Execution(String::from(
2968 "EINVAL: secure-exec dgram socket is already bound",
2969 )));
2970 }
2971
2972 let (bind_host, guest_host, guest_family) = normalize_udp_bind_host(host, self.family)?;
2973 let guest_port = allocate_guest_listen_port(
2974 port,
2975 guest_family,
2976 &context.used_udp_guest_ports,
2977 context.listen_policy,
2978 )?;
2979 let local_addr = resolve_udp_bind_addr(guest_host, guest_port, self.family)?;
2980 if let Some(socket_id) = self.kernel_socket_id {
2981 kernel
2982 .socket_bind_inet(
2983 EXECUTION_DRIVER_NAME,
2984 kernel_pid,
2985 socket_id,
2986 InetSocketAddress::new(local_addr.ip().to_string(), local_addr.port()),
2987 )
2988 .map_err(kernel_error)?;
2989 } else {
2990 let bind_addr = resolve_udp_bind_addr(bind_host, 0, self.family)?;
2991 let socket = UdpSocket::bind(bind_addr).map_err(sidecar_net_error)?;
2992 socket.set_nonblocking(true).map_err(sidecar_net_error)?;
2993 self.socket = Some(socket);
2994 }
2995 self.guest_local_addr = Some(local_addr);
2996 Ok(local_addr)
2997 }
2998
2999 fn ensure_bound_for_send(
3000 &mut self,
3001 kernel: &mut SidecarKernel,
3002 kernel_pid: u32,
3003 context: &JavascriptSocketPathContext,
3004 ) -> Result<SocketAddr, SidecarError> {
3005 if let Some(local_addr) = self.local_addr() {
3006 return Ok(local_addr);
3007 }
3008
3009 self.bind(kernel, kernel_pid, None, 0, context)
3010 }
3011
3012 fn send_to<B>(
3013 &mut self,
3014 request: ActiveUdpSendToRequest<'_, B>,
3015 ) -> Result<(usize, SocketAddr), SidecarError>
3016 where
3017 B: NativeSidecarBridge + Send + 'static,
3018 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
3019 {
3020 let ActiveUdpSendToRequest {
3021 bridge,
3022 kernel,
3023 kernel_pid,
3024 vm_id,
3025 dns,
3026 host,
3027 port,
3028 context,
3029 contents,
3030 } = request;
3031 let remote_addr = resolve_udp_addr(UdpRemoteAddrRequest {
3032 bridge,
3033 kernel,
3034 vm_id,
3035 dns,
3036 host,
3037 port,
3038 family: self.family,
3039 context,
3040 })?;
3041 let local_addr = self.ensure_bound_for_send(kernel, kernel_pid, context)?;
3042 let written = if let Some(socket_id) = self.kernel_socket_id {
3043 if is_loopback_ip(remote_addr.ip()) && remote_addr.port() == port {
3044 kernel
3045 .socket_send_to_inet_loopback(
3046 EXECUTION_DRIVER_NAME,
3047 kernel_pid,
3048 socket_id,
3049 InetSocketAddress::new(remote_addr.ip().to_string(), remote_addr.port()),
3050 contents,
3051 )
3052 .map_err(kernel_error)?
3053 } else {
3054 return Err(SidecarError::Execution(String::from(
3055 "ERR_NOT_IMPLEMENTED: external UDP datagrams are not yet supported by the kernel-backed V8 bridge",
3056 )));
3057 }
3058 } else {
3059 let socket = self.socket.as_ref().ok_or_else(|| {
3060 SidecarError::InvalidState(String::from("UDP socket is not initialized"))
3061 })?;
3062 socket
3063 .send_to(contents, remote_addr)
3064 .map_err(sidecar_net_error)?
3065 };
3066 Ok((written, local_addr))
3067 }
3068
3069 fn poll(
3070 &self,
3071 kernel: &mut SidecarKernel,
3072 kernel_pid: u32,
3073 wait: Duration,
3074 ) -> Result<Option<JavascriptUdpSocketEvent>, SidecarError> {
3075 if let Some(socket_id) = self.kernel_socket_id {
3076 let result = kernel
3077 .poll_targets(
3078 EXECUTION_DRIVER_NAME,
3079 kernel_pid,
3080 vec![PollTargetEntry::socket(socket_id, POLLIN)],
3081 i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
3082 )
3083 .map_err(kernel_error)?;
3084 let revents = result
3085 .targets
3086 .first()
3087 .map(|entry| entry.revents)
3088 .unwrap_or_else(PollEvents::empty);
3089 if revents.is_empty() {
3090 return Ok(None);
3091 }
3092 return match kernel.socket_recv_datagram(
3093 EXECUTION_DRIVER_NAME,
3094 kernel_pid,
3095 socket_id,
3096 64 * 1024,
3097 ) {
3098 Ok(Some(datagram)) => {
3099 let (source_address, payload) = datagram.into_parts();
3100 let remote_addr = source_address
3101 .map(|source| {
3102 resolve_udp_bind_addr(source.host(), source.port(), self.family)
3103 })
3104 .transpose()?
3105 .unwrap_or_else(|| match self.family {
3106 JavascriptUdpFamily::Ipv4 => {
3107 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)
3108 }
3109 JavascriptUdpFamily::Ipv6 => {
3110 SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0)
3111 }
3112 });
3113 Ok(Some(JavascriptUdpSocketEvent::Message {
3114 data: payload,
3115 remote_addr,
3116 }))
3117 }
3118 Ok(None) => Ok(None),
3119 Err(error) if error.code() == "EAGAIN" => Ok(None),
3120 Err(error) => Ok(Some(JavascriptUdpSocketEvent::Error {
3121 code: Some(error.code().to_string()),
3122 message: error.to_string(),
3123 })),
3124 };
3125 }
3126 let socket = self.socket()?;
3127 let deadline = Instant::now() + wait;
3128 let mut buffer = vec![0_u8; 64 * 1024];
3129
3130 loop {
3131 match socket.recv_from(&mut buffer) {
3132 Ok((bytes_read, remote_addr)) => {
3133 return Ok(Some(JavascriptUdpSocketEvent::Message {
3134 data: buffer[..bytes_read].to_vec(),
3135 remote_addr,
3136 }));
3137 }
3138 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
3139 if wait.is_zero() || Instant::now() >= deadline {
3140 return Ok(None);
3141 }
3142 if !wait_fd_readable_until(socket.as_fd(), deadline) {
3143 return Ok(None);
3144 }
3145 }
3146 Err(error) => {
3147 return Ok(Some(JavascriptUdpSocketEvent::Error {
3148 code: io_error_code(&error),
3149 message: error.to_string(),
3150 }));
3151 }
3152 }
3153 }
3154 }
3155
3156 fn close(&mut self, kernel: &mut SidecarKernel, kernel_pid: u32) {
3157 if let Some(socket_id) = self.kernel_socket_id {
3158 let _ = close_kernel_socket_idempotent(kernel, kernel_pid, socket_id);
3159 }
3160 self.socket.take();
3161 self.guest_local_addr = None;
3162 }
3163
3164 fn set_buffer_size(&mut self, which: &str, size: usize) -> Result<(), SidecarError> {
3165 match which {
3166 "recv" => self.recv_buffer_size = size,
3167 "send" => self.send_buffer_size = size,
3168 other => {
3169 return Err(SidecarError::InvalidState(format!(
3170 "unsupported UDP buffer size kind {other}"
3171 )));
3172 }
3173 }
3174 if self.kernel_socket_id.is_some() {
3175 return Ok(());
3176 }
3177 let socket = self.socket()?;
3178 let socket = SockRef::from(socket);
3179 match which {
3180 "recv" => socket.set_recv_buffer_size(size).map_err(sidecar_net_error),
3181 "send" => socket.set_send_buffer_size(size).map_err(sidecar_net_error),
3182 other => Err(SidecarError::InvalidState(format!(
3183 "unsupported UDP buffer size kind {other}"
3184 ))),
3185 }
3186 }
3187
3188 fn get_buffer_size(&self, which: &str) -> Result<usize, SidecarError> {
3189 if self.kernel_socket_id.is_some() {
3190 return Ok(match which {
3191 "recv" => self.recv_buffer_size,
3192 "send" => self.send_buffer_size,
3193 other => {
3194 return Err(SidecarError::InvalidState(format!(
3195 "unsupported UDP buffer size kind {other}"
3196 )));
3197 }
3198 });
3199 }
3200 let socket = self.socket()?;
3201 let socket = SockRef::from(socket);
3202 match which {
3203 "recv" => socket.recv_buffer_size().map_err(sidecar_net_error),
3204 "send" => socket.send_buffer_size().map_err(sidecar_net_error),
3205 other => Err(SidecarError::InvalidState(format!(
3206 "unsupported UDP buffer size kind {other}"
3207 ))),
3208 }
3209 }
3210}
3211
3212impl ActiveExecution {
3215 pub(crate) fn uses_shared_v8_runtime(&self) -> bool {
3216 match self {
3217 Self::Javascript(execution) => execution.uses_shared_v8_runtime(),
3218 Self::Python(execution) => execution.uses_shared_v8_runtime(),
3219 Self::Wasm(execution) => execution.uses_shared_v8_runtime(),
3220 Self::Tool(_) => false,
3221 }
3222 }
3223
3224 pub(crate) fn child_pid(&self) -> u32 {
3225 match self {
3226 Self::Javascript(execution) => execution.child_pid(),
3227 Self::Python(execution) => execution.child_pid(),
3228 Self::Wasm(execution) => execution.child_pid(),
3229 Self::Tool(_) => 0,
3230 }
3231 }
3232
3233 pub(crate) fn write_stdin(&mut self, chunk: &[u8]) -> Result<(), SidecarError> {
3234 match self {
3235 Self::Javascript(execution) => execution
3236 .write_stdin(chunk)
3237 .map_err(|error| SidecarError::Execution(error.to_string())),
3238 Self::Python(execution) => execution
3239 .write_stdin(chunk)
3240 .map_err(|error| SidecarError::Execution(error.to_string())),
3241 Self::Wasm(execution) => execution
3247 .write_stdin_kernel_only(chunk)
3248 .map_err(|error| SidecarError::Execution(error.to_string())),
3249 Self::Tool(_) => Ok(()),
3250 }
3251 }
3252
3253 pub(crate) fn close_stdin(&mut self) -> Result<(), SidecarError> {
3254 match self {
3255 Self::Javascript(execution) => execution
3256 .close_stdin()
3257 .map_err(|error| SidecarError::Execution(error.to_string())),
3258 Self::Python(execution) => execution
3259 .close_stdin()
3260 .map_err(|error| SidecarError::Execution(error.to_string())),
3261 Self::Wasm(execution) => execution
3262 .close_stdin()
3263 .map_err(|error| SidecarError::Execution(error.to_string())),
3264 Self::Tool(_) => Ok(()),
3265 }
3266 }
3267
3268 pub(crate) fn respond_python_vfs_rpc_success(
3269 &mut self,
3270 id: u64,
3271 payload: PythonVfsRpcResponsePayload,
3272 ) -> Result<(), SidecarError> {
3273 match self {
3274 Self::Python(execution) => execution
3275 .respond_vfs_rpc_success(id, payload)
3276 .map_err(|error| SidecarError::Execution(error.to_string())),
3277 _ => Err(SidecarError::InvalidState(String::from(
3278 "only Python executions can service Python VFS RPC responses",
3279 ))),
3280 }
3281 }
3282
3283 pub(crate) fn respond_python_vfs_rpc_error(
3284 &mut self,
3285 id: u64,
3286 code: impl Into<String>,
3287 message: impl Into<String>,
3288 ) -> Result<(), SidecarError> {
3289 match self {
3290 Self::Python(execution) => execution
3291 .respond_vfs_rpc_error(id, code, message)
3292 .map_err(|error| SidecarError::Execution(error.to_string())),
3293 _ => Err(SidecarError::InvalidState(String::from(
3294 "only Python executions can service Python VFS RPC responses",
3295 ))),
3296 }
3297 }
3298
3299 pub(crate) fn send_javascript_stream_event(
3300 &self,
3301 event_type: &str,
3302 payload: Value,
3303 ) -> Result<(), SidecarError> {
3304 match self {
3305 Self::Javascript(execution) => execution
3306 .send_stream_event(event_type, payload)
3307 .map_err(|error| SidecarError::Execution(error.to_string())),
3308 Self::Wasm(execution) => execution
3309 .send_stream_event(event_type, payload)
3310 .map_err(|error| SidecarError::Execution(error.to_string())),
3311 _ => Err(SidecarError::InvalidState(String::from(
3312 "only embedded V8 executions can receive JavaScript stream events",
3313 ))),
3314 }
3315 }
3316
3317 pub(crate) fn javascript_v8_session_handle(&self) -> Option<V8SessionHandle> {
3318 match self {
3319 Self::Javascript(execution) => Some(execution.v8_session_handle()),
3320 Self::Wasm(execution) => Some(execution.v8_session_handle()),
3321 _ => None,
3322 }
3323 }
3324
3325 pub(crate) fn terminate(&mut self) -> Result<(), SidecarError> {
3326 match self {
3327 Self::Javascript(execution) => execution
3328 .terminate()
3329 .map_err(|error| SidecarError::Execution(error.to_string())),
3330 Self::Python(execution) => execution
3331 .kill()
3332 .map_err(|error| SidecarError::Execution(error.to_string())),
3333 Self::Wasm(execution) => execution
3334 .terminate()
3335 .map_err(|error| SidecarError::Execution(error.to_string())),
3336 Self::Tool(_) => Ok(()),
3337 }
3338 }
3339
3340 pub(crate) fn respond_javascript_sync_rpc_success(
3341 &mut self,
3342 id: u64,
3343 result: Value,
3344 ) -> Result<(), SidecarError> {
3345 match self {
3346 Self::Javascript(execution) => execution
3347 .respond_sync_rpc_success(id, result)
3348 .map_err(|error| SidecarError::Execution(error.to_string())),
3349 Self::Python(execution) => execution
3350 .respond_javascript_sync_rpc_success(id, result)
3351 .map_err(|error| SidecarError::Execution(error.to_string())),
3352 Self::Wasm(execution) => execution
3353 .respond_sync_rpc_success(id, result)
3354 .map_err(|error| SidecarError::Execution(error.to_string())),
3355 _ => Err(SidecarError::InvalidState(String::from(
3356 "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
3357 ))),
3358 }
3359 }
3360
3361 pub(crate) fn respond_javascript_sync_rpc_raw_success(
3362 &mut self,
3363 id: u64,
3364 payload: Vec<u8>,
3365 ) -> Result<(), SidecarError> {
3366 match self {
3367 Self::Javascript(execution) => execution
3368 .respond_sync_rpc_raw_success(id, payload)
3369 .map_err(|error| SidecarError::Execution(error.to_string())),
3370 Self::Wasm(execution) => execution
3371 .respond_sync_rpc_raw_success(id, payload)
3372 .map_err(|error| SidecarError::Execution(error.to_string())),
3373 _ => Err(SidecarError::InvalidState(String::from(
3374 "only embedded V8 executions can service raw JavaScript sync RPC responses",
3375 ))),
3376 }
3377 }
3378
3379 pub(crate) fn respond_javascript_sync_rpc_response(
3380 &mut self,
3381 id: u64,
3382 response: JavascriptSyncRpcServiceResponse,
3383 ) -> Result<(), SidecarError> {
3384 match response {
3385 JavascriptSyncRpcServiceResponse::Json(result) => {
3386 self.respond_javascript_sync_rpc_success(id, result)
3387 }
3388 JavascriptSyncRpcServiceResponse::Raw(payload) => {
3389 self.respond_javascript_sync_rpc_raw_success(id, payload)
3390 }
3391 }
3392 }
3393
3394 pub(crate) fn respond_javascript_sync_rpc_error(
3395 &mut self,
3396 id: u64,
3397 code: impl Into<String>,
3398 message: impl Into<String>,
3399 ) -> Result<(), SidecarError> {
3400 match self {
3401 Self::Javascript(execution) => execution
3402 .respond_sync_rpc_error(id, code, message)
3403 .map_err(|error| SidecarError::Execution(error.to_string())),
3404 Self::Python(execution) => execution
3405 .respond_javascript_sync_rpc_error(id, code, message)
3406 .map_err(|error| SidecarError::Execution(error.to_string())),
3407 Self::Wasm(execution) => execution
3408 .respond_sync_rpc_error(id, code, message)
3409 .map_err(|error| SidecarError::Execution(error.to_string())),
3410 _ => Err(SidecarError::InvalidState(String::from(
3411 "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
3412 ))),
3413 }
3414 }
3415
3416 pub(crate) async fn poll_event(
3417 &mut self,
3418 timeout: Duration,
3419 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
3420 match self {
3421 Self::Javascript(execution) => execution
3422 .poll_event(timeout)
3423 .await
3424 .map(|event| {
3425 event.map(|event| match event {
3426 JavascriptExecutionEvent::Stdout(chunk) => {
3427 ActiveExecutionEvent::Stdout(chunk)
3428 }
3429 JavascriptExecutionEvent::Stderr(chunk) => {
3430 ActiveExecutionEvent::Stderr(chunk)
3431 }
3432 JavascriptExecutionEvent::SyncRpcRequest(request) => {
3433 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
3434 }
3435 JavascriptExecutionEvent::SignalState {
3436 signal,
3437 registration,
3438 } => ActiveExecutionEvent::SignalState {
3439 signal,
3440 registration: map_node_signal_registration(registration),
3441 },
3442 JavascriptExecutionEvent::Exited(code) => {
3443 ActiveExecutionEvent::Exited(code)
3444 }
3445 })
3446 })
3447 .map_err(|error| SidecarError::Execution(error.to_string())),
3448 Self::Python(execution) => execution
3449 .poll_event(timeout)
3450 .await
3451 .map(|event| {
3452 event.map(|event| match event {
3453 PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
3454 PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
3455 PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
3456 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
3457 }
3458 PythonExecutionEvent::VfsRpcRequest(request) => {
3459 ActiveExecutionEvent::PythonVfsRpcRequest(request)
3460 }
3461 PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
3462 })
3463 })
3464 .map_err(|error| SidecarError::Execution(error.to_string())),
3465 Self::Wasm(execution) => execution
3466 .poll_event(timeout)
3467 .await
3468 .map(|event| {
3469 event.map(|event| match event {
3470 WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
3471 WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
3472 WasmExecutionEvent::SyncRpcRequest(request) => {
3473 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
3474 }
3475 WasmExecutionEvent::SignalState {
3476 signal,
3477 registration,
3478 } => ActiveExecutionEvent::SignalState {
3479 signal,
3480 registration: map_wasm_signal_registration(registration),
3481 },
3482 WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
3483 })
3484 })
3485 .map_err(|error| SidecarError::Execution(error.to_string())),
3486 Self::Tool(execution) => {
3487 let _ = timeout;
3488 poll_tool_process_event(execution)
3489 }
3490 }
3491 }
3492
3493 pub(crate) fn poll_event_blocking(
3494 &mut self,
3495 timeout: Duration,
3496 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
3497 match self {
3498 Self::Javascript(execution) => execution
3499 .poll_event_blocking(timeout)
3500 .map(|event| {
3501 event.map(|event| match event {
3502 JavascriptExecutionEvent::Stdout(chunk) => {
3503 ActiveExecutionEvent::Stdout(chunk)
3504 }
3505 JavascriptExecutionEvent::Stderr(chunk) => {
3506 ActiveExecutionEvent::Stderr(chunk)
3507 }
3508 JavascriptExecutionEvent::SyncRpcRequest(request) => {
3509 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
3510 }
3511 JavascriptExecutionEvent::SignalState {
3512 signal,
3513 registration,
3514 } => ActiveExecutionEvent::SignalState {
3515 signal,
3516 registration: map_node_signal_registration(registration),
3517 },
3518 JavascriptExecutionEvent::Exited(code) => {
3519 ActiveExecutionEvent::Exited(code)
3520 }
3521 })
3522 })
3523 .map_err(|error| SidecarError::Execution(error.to_string())),
3524 Self::Python(execution) => execution
3525 .poll_event_blocking(timeout)
3526 .map(|event| {
3527 event.map(|event| match event {
3528 PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
3529 PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
3530 PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
3531 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
3532 }
3533 PythonExecutionEvent::VfsRpcRequest(request) => {
3534 ActiveExecutionEvent::PythonVfsRpcRequest(request)
3535 }
3536 PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
3537 })
3538 })
3539 .map_err(|error| SidecarError::Execution(error.to_string())),
3540 Self::Wasm(execution) => execution
3541 .poll_event_blocking(timeout)
3542 .map(|event| {
3543 event.map(|event| match event {
3544 WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
3545 WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
3546 WasmExecutionEvent::SyncRpcRequest(request) => {
3547 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
3548 }
3549 WasmExecutionEvent::SignalState {
3550 signal,
3551 registration,
3552 } => ActiveExecutionEvent::SignalState {
3553 signal,
3554 registration: map_wasm_signal_registration(registration),
3555 },
3556 WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
3557 })
3558 })
3559 .map_err(|error| SidecarError::Execution(error.to_string())),
3560 Self::Tool(execution) => {
3561 let _ = timeout;
3562 poll_tool_process_event(execution)
3563 }
3564 }
3565 }
3566}
3567
3568struct ToolProcessEventRequest {
3569 sidecar_requests: SharedSidecarRequestClient,
3570 connection_id: String,
3571 session_id: String,
3572 vm_id: String,
3573 tool_resolution: ToolCommandResolution,
3574 cancelled: Arc<AtomicBool>,
3575 pending_events: Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
3576 events_overflowed: Arc<AtomicBool>,
3577}
3578
3579pub(crate) fn send_tool_process_event(
3580 pending_events: &Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
3581 events_overflowed: &AtomicBool,
3582 event: ActiveExecutionEvent,
3583) -> bool {
3584 let mut pending_events = pending_events
3585 .lock()
3586 .unwrap_or_else(|poisoned| poisoned.into_inner());
3587 if pending_events.len() >= MAX_PROCESS_EVENT_QUEUE {
3588 events_overflowed.store(true, Ordering::Relaxed);
3589 return false;
3590 }
3591 pending_events.push_back(event);
3592 true
3593}
3594
3595fn spawn_tool_process_events(request: ToolProcessEventRequest) {
3596 let ToolProcessEventRequest {
3597 sidecar_requests,
3598 connection_id,
3599 session_id,
3600 vm_id,
3601 tool_resolution,
3602 cancelled,
3603 pending_events,
3604 events_overflowed,
3605 } = request;
3606 std::thread::spawn(move || match tool_resolution {
3607 ToolCommandResolution::Failure(message) => {
3608 if !send_tool_process_event(
3609 &pending_events,
3610 &events_overflowed,
3611 ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
3612 ) {
3613 return;
3614 }
3615 let _ = send_tool_process_event(
3616 &pending_events,
3617 &events_overflowed,
3618 ActiveExecutionEvent::Exited(1),
3619 );
3620 }
3621 ToolCommandResolution::Invoke { request, timeout } => {
3622 let response = sidecar_requests.invoke(
3623 OwnershipScope::vm(connection_id.clone(), session_id.clone(), vm_id.clone()),
3624 SidecarRequestPayload::HostCallback(request.clone()),
3625 timeout,
3626 );
3627 if cancelled.load(Ordering::Relaxed) {
3628 return;
3629 }
3630
3631 match response {
3632 Ok(crate::protocol::SidecarResponsePayload::HostCallbackResult(result)) => {
3633 if let Some(value) = result.result {
3634 let value: serde_json::Value = serde_json::from_str(&value)
3635 .unwrap_or(serde_json::Value::String(value));
3636 let stdout = serde_json::to_vec(&json!({
3637 "ok": true,
3638 "result": value,
3639 }))
3640 .unwrap_or_else(|error| {
3641 format_tool_failure_output(&format!(
3642 "failed to serialize tool result: {error}"
3643 ))
3644 });
3645 if !send_tool_process_event(
3646 &pending_events,
3647 &events_overflowed,
3648 ActiveExecutionEvent::Stdout(stdout),
3649 ) {
3650 return;
3651 }
3652 let _ = send_tool_process_event(
3653 &pending_events,
3654 &events_overflowed,
3655 ActiveExecutionEvent::Exited(0),
3656 );
3657 } else {
3658 let message = result
3659 .error
3660 .unwrap_or_else(|| String::from("tool invocation returned no result"));
3661 if !send_tool_process_event(
3662 &pending_events,
3663 &events_overflowed,
3664 ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
3665 ) {
3666 return;
3667 }
3668 let _ = send_tool_process_event(
3669 &pending_events,
3670 &events_overflowed,
3671 ActiveExecutionEvent::Exited(1),
3672 );
3673 }
3674 }
3675 Ok(_) => {
3676 if !send_tool_process_event(
3677 &pending_events,
3678 &events_overflowed,
3679 ActiveExecutionEvent::Stderr(format_tool_failure_output(
3680 "unexpected sidecar tool response",
3681 )),
3682 ) {
3683 return;
3684 }
3685 let _ = send_tool_process_event(
3686 &pending_events,
3687 &events_overflowed,
3688 ActiveExecutionEvent::Exited(1),
3689 );
3690 }
3691 Err(error) => {
3692 if !send_tool_process_event(
3693 &pending_events,
3694 &events_overflowed,
3695 ActiveExecutionEvent::Stderr(format_tool_failure_output(
3696 &error.to_string(),
3697 )),
3698 ) {
3699 return;
3700 }
3701 let _ = send_tool_process_event(
3702 &pending_events,
3703 &events_overflowed,
3704 ActiveExecutionEvent::Exited(1),
3705 );
3706 }
3707 }
3708 }
3709 });
3710}
3711
3712impl<B> NativeSidecar<B>
3713where
3714 B: NativeSidecarBridge + Send + 'static,
3715 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
3716{
3717 pub(crate) async fn execute(
3718 &mut self,
3719 request: &RequestFrame,
3720 payload: ExecuteRequest,
3721 ) -> Result<DispatchResult, SidecarError> {
3722 let execute_total_start = Instant::now();
3723 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3724 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3725
3726 let vm = self
3727 .vms
3728 .get_mut(&vm_id)
3729 .ok_or_else(|| missing_vm_error(&vm_id))?;
3730 if vm.active_processes.contains_key(&payload.process_id) {
3731 return Err(SidecarError::InvalidState(format!(
3732 "VM {vm_id} already has an active process with id {}",
3733 payload.process_id
3734 )));
3735 }
3736
3737 if let Some(command) = payload.command.as_deref() {
3738 if let Some(tool_resolution) =
3739 resolve_tool_command(vm, command, &payload.args, payload.cwd.as_deref())?
3740 {
3741 let guest_cwd = payload
3742 .cwd
3743 .as_deref()
3744 .map(normalize_path)
3745 .unwrap_or_else(|| vm.guest_cwd.clone());
3746 let kernel_handle = vm
3747 .kernel
3748 .create_virtual_process(
3749 EXECUTION_DRIVER_NAME,
3750 TOOL_DRIVER_NAME,
3751 command,
3752 std::iter::once(command.to_owned())
3753 .chain(payload.args.iter().cloned())
3754 .collect(),
3755 VirtualProcessOptions {
3756 env: vm.guest_env.clone(),
3757 cwd: Some(guest_cwd.clone()),
3758 ..VirtualProcessOptions::default()
3759 },
3760 )
3761 .map_err(kernel_error)?;
3762 let kernel_pid = kernel_handle.pid();
3763 let tool_execution = ToolExecution::default();
3764 let cancelled = tool_execution.cancelled.clone();
3765 let pending_events = tool_execution.pending_events.clone();
3766 let events_overflowed = tool_execution.events_overflowed.clone();
3767 vm.active_processes.insert(
3768 payload.process_id.clone(),
3769 ActiveProcess::new(
3770 kernel_pid,
3771 kernel_handle,
3772 GuestRuntimeKind::JavaScript,
3773 ActiveExecution::Tool(tool_execution),
3774 )
3775 .with_guest_cwd(guest_cwd.clone())
3776 .with_host_cwd(resolve_vm_guest_path_to_host(vm, &guest_cwd)),
3777 );
3778 self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
3779 spawn_tool_process_events(ToolProcessEventRequest {
3780 sidecar_requests: self.sidecar_requests.clone(),
3781 connection_id: connection_id.clone(),
3782 session_id: session_id.clone(),
3783 vm_id: vm_id.clone(),
3784 tool_resolution,
3785 cancelled,
3786 pending_events,
3787 events_overflowed,
3788 });
3789
3790 return Ok(DispatchResult {
3791 response: process_started_response(
3792 request,
3793 payload.process_id,
3794 Some(kernel_pid),
3795 ),
3796 events: Vec::new(),
3797 });
3798 }
3799 }
3800
3801 let requested_tty = payload
3802 .env
3803 .get(EXECUTION_REQUEST_TTY_ENV)
3804 .is_some_and(|value| value == "1" || value.eq_ignore_ascii_case("true"));
3805 let phase_start = Instant::now();
3806 let resolved = resolve_execute_request(vm, &payload)?;
3807 record_execute_phase("resolve_execute_request", phase_start.elapsed());
3808 let phase_start = Instant::now();
3809 let mut env = resolved.env.clone();
3810 env.remove(EXECUTION_REQUEST_TTY_ENV);
3811 let sandbox_root = normalize_host_path(&vm.cwd);
3812 env.insert(
3813 String::from(EXECUTION_SANDBOX_ROOT_ENV),
3814 sandbox_root.to_string_lossy().into_owned(),
3815 );
3816 if resolved.runtime == GuestRuntimeKind::JavaScript {
3817 env.insert(
3818 String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
3819 String::from("1"),
3820 );
3821 } else if resolved.runtime == GuestRuntimeKind::WebAssembly {
3830 env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
3831 }
3832 let argv = std::iter::once(resolved.entrypoint.clone())
3833 .chain(resolved.execution_args.iter().cloned())
3834 .collect::<Vec<_>>();
3835 record_execute_phase("env_argv_setup", phase_start.elapsed());
3836 let phase_start = Instant::now();
3837 let kernel_handle = vm
3838 .kernel
3839 .spawn_process(
3840 &resolved.command,
3841 argv,
3842 SpawnOptions {
3843 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
3844 cwd: Some(resolved.guest_cwd.clone()),
3845 ..SpawnOptions::default()
3846 },
3847 )
3848 .map_err(kernel_error)?;
3849 let kernel_pid = kernel_handle.pid();
3850 record_execute_phase("kernel_spawn_process", phase_start.elapsed());
3851 let tty_master_fd = if requested_tty {
3852 let (master_fd, slave_fd, _) = vm
3853 .kernel
3854 .open_pty(EXECUTION_DRIVER_NAME, kernel_pid)
3855 .map_err(kernel_error)?;
3856 vm.kernel
3857 .fd_dup2(EXECUTION_DRIVER_NAME, kernel_pid, slave_fd, 0)
3858 .map_err(kernel_error)?;
3859 vm.kernel
3860 .fd_dup2(EXECUTION_DRIVER_NAME, kernel_pid, slave_fd, 1)
3861 .map_err(kernel_error)?;
3862 vm.kernel
3863 .fd_dup2(EXECUTION_DRIVER_NAME, kernel_pid, slave_fd, 2)
3864 .map_err(kernel_error)?;
3865 vm.kernel
3866 .pty_set_foreground_pgid(EXECUTION_DRIVER_NAME, kernel_pid, master_fd, kernel_pid)
3867 .map_err(kernel_error)?;
3868 if let Some((cols, rows)) = requested_pty_window_size(&env) {
3869 vm.kernel
3870 .pty_resize(EXECUTION_DRIVER_NAME, kernel_pid, master_fd, cols, rows)
3871 .map_err(kernel_error)?;
3872 }
3873 Some(master_fd)
3874 } else {
3875 None
3876 };
3877
3878 let (execution, process_env) = match resolved.runtime {
3879 GuestRuntimeKind::JavaScript => {
3880 let phase_start = Instant::now();
3881 let inline_code = load_javascript_entrypoint_source(
3882 vm,
3883 &resolved.host_cwd,
3884 &resolved.entrypoint,
3885 &env,
3886 );
3887 record_execute_phase("js_load_entrypoint_source", phase_start.elapsed());
3888 let phase_start = Instant::now();
3889 prepare_javascript_shadow(vm, &resolved)?;
3890 record_execute_phase("js_prepare_shadow", phase_start.elapsed());
3891
3892 let phase_start = Instant::now();
3893 let context =
3894 self.javascript_engine
3895 .create_context(CreateJavascriptContextRequest {
3896 vm_id: vm_id.clone(),
3897 bootstrap_module: None,
3898 compile_cache_root: Some(self.cache_root.join("node-compile-cache")),
3899 });
3900 record_execute_phase("js_create_context", phase_start.elapsed());
3901 let phase_start = Instant::now();
3902 let built_reader = build_module_reader(vm, &resolved);
3903 let guest_reader = built_reader.clone().map(|reader| {
3904 Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
3905 as Box<dyn GuestModuleReader>
3906 });
3907 let module_reader =
3908 built_reader.map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
3909 record_execute_phase("js_build_module_reader", phase_start.elapsed());
3910 let phase_start = Instant::now();
3911 let execution = self
3912 .javascript_engine
3913 .start_execution_with_module_reader(
3914 StartJavascriptExecutionRequest {
3915 guest_runtime: guest_runtime_identity(vm, None, None),
3916 vm_id: vm_id.clone(),
3917 context_id: context.context_id,
3918 argv: std::iter::once(resolved.entrypoint.clone())
3919 .chain(resolved.execution_args.iter().cloned())
3920 .collect(),
3921 env: env.clone(),
3922 cwd: resolved.host_cwd.clone(),
3923 limits: javascript_execution_limits(vm),
3924 inline_code,
3925 wasm_module_bytes: None,
3926 },
3927 module_reader,
3928 guest_reader,
3929 )
3930 .map_err(javascript_error)?;
3931 record_execute_phase("js_start_execution", phase_start.elapsed());
3932 (ActiveExecution::Javascript(execution), env.clone())
3933 }
3934 GuestRuntimeKind::Python => {
3935 let python_file_path = if resolved.env.contains_key("AGENTOS_PYTHON_ARGV") {
3940 resolved.env.get("AGENTOS_PYTHON_FILE").map(PathBuf::from)
3941 } else {
3942 python_file_entrypoint(&resolved.entrypoint)
3943 };
3944 let pyodide_dist_path = self
3945 .python_engine
3946 .bundled_pyodide_dist_path_for_vm(&vm_id)
3947 .map_err(python_error)?;
3948 let pyodide_cache_path = pyodide_dist_path
3949 .parent()
3950 .and_then(Path::parent)
3951 .unwrap_or(pyodide_dist_path.as_path())
3952 .join("pyodide-package-cache");
3953 add_runtime_guest_path_mapping(
3954 &mut env,
3955 PYTHON_PYODIDE_GUEST_ROOT,
3956 &pyodide_dist_path,
3957 );
3958 add_runtime_guest_path_mapping(
3959 &mut env,
3960 PYTHON_PYODIDE_CACHE_GUEST_ROOT,
3961 &pyodide_cache_path,
3962 );
3963 add_runtime_host_access_path(
3964 &mut env,
3965 "AGENTOS_EXTRA_FS_READ_PATHS",
3966 &pyodide_dist_path,
3967 true,
3968 );
3969 add_runtime_host_access_path(
3970 &mut env,
3971 "AGENTOS_EXTRA_FS_READ_PATHS",
3972 &pyodide_cache_path,
3973 true,
3974 );
3975 add_runtime_host_access_path(
3976 &mut env,
3977 "AGENTOS_EXTRA_FS_WRITE_PATHS",
3978 &pyodide_cache_path,
3979 false,
3980 );
3981 let context = self
3982 .python_engine
3983 .create_context(CreatePythonContextRequest {
3984 vm_id: vm_id.clone(),
3985 pyodide_dist_path,
3986 });
3987 let execution = self
3988 .python_engine
3989 .start_execution(StartPythonExecutionRequest {
3990 vm_id: vm_id.clone(),
3991 context_id: context.context_id,
3992 code: resolved.entrypoint.clone(),
3993 file_path: python_file_path,
3994 env: env.clone(),
3995 cwd: resolved.host_cwd.clone(),
3996 limits: python_execution_limits(vm),
3997 guest_runtime: guest_runtime_identity(vm, None, None),
3998 })
3999 .map_err(python_error)?;
4000 (ActiveExecution::Python(execution), env.clone())
4001 }
4002 GuestRuntimeKind::WebAssembly => {
4003 let wasm_limits = wasm_execution_limits(vm);
4004 let wasm_guest_runtime =
4005 guest_runtime_identity(vm, Some(u64::from(kernel_pid)), Some(0));
4006 let wasm_permission_tier = resolved.wasm_permission_tier.unwrap_or_else(|| {
4007 resolve_wasm_permission_tier(
4008 vm,
4009 Some(&resolved.command),
4010 None,
4011 &resolved.entrypoint,
4012 )
4013 });
4014 let context = self.wasm_engine.create_context(CreateWasmContextRequest {
4015 vm_id: vm_id.clone(),
4016 module_path: Some(resolved.entrypoint.clone()),
4017 });
4018 let execution = self
4019 .wasm_engine
4020 .start_execution(StartWasmExecutionRequest {
4021 vm_id: vm_id.clone(),
4022 context_id: context.context_id,
4023 argv: resolved.process_args.clone(),
4024 env: env.clone(),
4025 cwd: resolved.host_cwd.clone(),
4026 permission_tier: execution_wasm_permission_tier(wasm_permission_tier),
4027 limits: wasm_limits,
4028 guest_runtime: wasm_guest_runtime,
4029 })
4030 .map_err(wasm_error)?;
4031 (ActiveExecution::Wasm(Box::new(execution)), env)
4032 }
4033 };
4034 let child_pid = execution.child_pid();
4035 let phase_start = Instant::now();
4036 let kernel_stdin_writer_fd = if let Some(master_fd) = tty_master_fd {
4037 master_fd
4038 } else {
4039 install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?
4040 };
4041 vm.active_processes.insert(
4042 payload.process_id.clone(),
4043 ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
4044 .with_kernel_stdin_writer_fd(kernel_stdin_writer_fd)
4045 .with_tty_master_fd(tty_master_fd)
4046 .with_guest_cwd(resolved.guest_cwd.clone())
4047 .with_env(process_env)
4048 .with_host_cwd(resolved.host_cwd.clone()),
4049 );
4050 self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
4051 mark_execute_response_ready(&vm_id, &payload.process_id);
4052 record_execute_phase("process_register_and_lifecycle", phase_start.elapsed());
4053 record_execute_phase("execute_total", execute_total_start.elapsed());
4054
4055 Ok(DispatchResult {
4056 response: process_started_response(
4057 request,
4058 payload.process_id,
4059 Some(if child_pid == 0 {
4060 kernel_pid
4061 } else {
4062 child_pid
4063 }),
4064 ),
4065 events: Vec::new(),
4066 })
4067 }
4068
4069 pub(crate) async fn resize_pty(
4070 &mut self,
4071 request: &RequestFrame,
4072 payload: ResizePtyRequest,
4073 ) -> Result<DispatchResult, SidecarError> {
4074 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4075 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4076
4077 let vm = self
4078 .vms
4079 .get_mut(&vm_id)
4080 .ok_or_else(|| missing_vm_error(&vm_id))?;
4081 let process = vm
4082 .active_processes
4083 .get_mut(&payload.process_id)
4084 .ok_or_else(|| {
4085 SidecarError::InvalidState(format!(
4086 "VM {vm_id} has no active process {}",
4087 payload.process_id
4088 ))
4089 })?;
4090 let Some(writer_fd) = process.kernel_stdin_writer_fd else {
4091 return Err(SidecarError::InvalidState(format!(
4092 "process {} does not have a PTY",
4093 payload.process_id
4094 )));
4095 };
4096 vm.kernel
4097 .pty_resize(
4098 EXECUTION_DRIVER_NAME,
4099 process.kernel_pid,
4100 writer_fd,
4101 payload.cols,
4102 payload.rows,
4103 )
4104 .map_err(kernel_error)?;
4105
4106 Ok(DispatchResult {
4107 response: self.respond(
4108 request,
4109 ResponsePayload::PtyResized(PtyResizedResponse {
4110 process_id: payload.process_id,
4111 cols: payload.cols,
4112 rows: payload.rows,
4113 }),
4114 ),
4115 events: Vec::new(),
4116 })
4117 }
4118
4119 pub(crate) async fn write_stdin(
4120 &mut self,
4121 request: &RequestFrame,
4122 payload: WriteStdinRequest,
4123 ) -> Result<DispatchResult, SidecarError> {
4124 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4125 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4126
4127 let vm = self
4128 .vms
4129 .get_mut(&vm_id)
4130 .ok_or_else(|| missing_vm_error(&vm_id))?;
4131 let process = vm
4132 .active_processes
4133 .get_mut(&payload.process_id)
4134 .ok_or_else(|| {
4135 SidecarError::InvalidState(format!(
4136 "VM {vm_id} has no active process {}",
4137 payload.process_id
4138 ))
4139 })?;
4140 let tty_js =
4146 process.runtime == GuestRuntimeKind::JavaScript && process.tty_master_fd.is_some();
4147 if !tty_js {
4148 process.execution.write_stdin(&payload.chunk)?;
4149 }
4150 write_kernel_process_stdin(&mut vm.kernel, process, &payload.chunk)?;
4151
4152 Ok(DispatchResult {
4153 response: stdin_written_response(
4154 request,
4155 payload.process_id,
4156 payload.chunk.len() as u64,
4157 ),
4158 events: Vec::new(),
4159 })
4160 }
4161
4162 pub(crate) async fn close_stdin(
4163 &mut self,
4164 request: &RequestFrame,
4165 payload: CloseStdinRequest,
4166 ) -> Result<DispatchResult, SidecarError> {
4167 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4168 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4169
4170 let vm = self
4171 .vms
4172 .get_mut(&vm_id)
4173 .ok_or_else(|| missing_vm_error(&vm_id))?;
4174 let process = vm
4175 .active_processes
4176 .get_mut(&payload.process_id)
4177 .ok_or_else(|| {
4178 SidecarError::InvalidState(format!(
4179 "VM {vm_id} has no active process {}",
4180 payload.process_id
4181 ))
4182 })?;
4183 process.execution.close_stdin()?;
4184 close_kernel_process_stdin(&mut vm.kernel, process)?;
4185
4186 Ok(DispatchResult {
4187 response: stdin_closed_response(request, payload.process_id),
4188 events: Vec::new(),
4189 })
4190 }
4191
4192 pub(crate) async fn kill_process(
4193 &mut self,
4194 request: &RequestFrame,
4195 payload: KillProcessRequest,
4196 ) -> Result<DispatchResult, SidecarError> {
4197 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4198 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4199 self.kill_process_internal(&vm_id, &payload.process_id, &payload.signal)?;
4200
4201 Ok(DispatchResult {
4202 response: process_killed_response(request, payload.process_id),
4203 events: Vec::new(),
4204 })
4205 }
4206
4207 pub(crate) async fn find_listener(
4208 &mut self,
4209 request: &RequestFrame,
4210 payload: FindListenerRequest,
4211 ) -> Result<DispatchResult, SidecarError> {
4212 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4213 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4214 require_vm_inspection_permission(
4215 &self.bridge,
4216 &vm_id,
4217 "network.inspect",
4218 "network",
4219 &socket_query_resource(SocketQueryKind::TcpListener, &payload),
4220 )?;
4221
4222 let listener =
4223 find_socket_state_entry(self.vms.get(&vm_id), SocketQueryKind::TcpListener, &payload)?;
4224
4225 Ok(DispatchResult {
4226 response: listener_snapshot_response(request, listener),
4227 events: Vec::new(),
4228 })
4229 }
4230
4231 pub(crate) async fn get_process_snapshot(
4232 &mut self,
4233 request: &RequestFrame,
4234 _payload: GetProcessSnapshotRequest,
4235 ) -> Result<DispatchResult, SidecarError> {
4236 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4237 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4238 require_vm_inspection_permission(
4239 &self.bridge,
4240 &vm_id,
4241 "process.inspect",
4242 "process",
4243 "process://snapshot",
4244 )?;
4245
4246 let processes = self
4247 .vms
4248 .get_mut(&vm_id)
4249 .map(|vm| {
4250 prune_exited_process_snapshots(vm);
4251 snapshot_vm_processes(vm)
4252 })
4253 .unwrap_or_default();
4254
4255 Ok(DispatchResult {
4256 response: process_snapshot_response(request, processes),
4257 events: Vec::new(),
4258 })
4259 }
4260
4261 pub(crate) async fn guest_kernel_call(
4262 &mut self,
4263 request: &RequestFrame,
4264 payload: GuestKernelCallRequest,
4265 ) -> Result<DispatchResult, SidecarError> {
4266 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4267 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4268
4269 let vm = self.vms.get_mut(&vm_id).ok_or_else(|| {
4270 SidecarError::InvalidState(format!("VM {vm_id} no longer exists for guest kernel call"))
4271 })?;
4272 let kernel_pid = vm
4273 .active_processes
4274 .get(&payload.execution_id)
4275 .map(|process| process.kernel_pid)
4276 .ok_or_else(|| {
4277 SidecarError::InvalidState(format!(
4278 "VM {vm_id} has no active process {} for guest kernel call",
4279 payload.execution_id
4280 ))
4281 })?;
4282
4283 let response = secure_exec_sidecar_core::handle_guest_kernel_call(
4284 &mut vm.kernel,
4285 kernel_pid,
4286 EXECUTION_DRIVER_NAME,
4287 &payload.operation,
4288 &payload.payload,
4289 )
4290 .map_err(guest_kernel_core_error)?;
4291
4292 Ok(DispatchResult {
4293 response: self.respond(
4294 request,
4295 ResponsePayload::GuestKernelResult(GuestKernelResultResponse { payload: response }),
4296 ),
4297 events: Vec::new(),
4298 })
4299 }
4300
4301 pub(crate) async fn get_resource_snapshot(
4302 &mut self,
4303 request: &RequestFrame,
4304 _payload: GetResourceSnapshotRequest,
4305 ) -> Result<DispatchResult, SidecarError> {
4306 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4307 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4308 require_vm_inspection_permission(
4309 &self.bridge,
4310 &vm_id,
4311 "process.inspect",
4312 "process",
4313 "process://resources",
4314 )?;
4315
4316 let snapshot = self
4317 .vms
4318 .get(&vm_id)
4319 .map(|vm| vm.kernel.resource_snapshot())
4320 .unwrap_or_default();
4321 let queue_snapshots = queue_tracker::queue_snapshot()
4322 .into_iter()
4323 .map(|queue| QueueSnapshotEntry {
4324 name: queue.name.as_str().to_owned(),
4325 category: queue.category.as_str().to_owned(),
4326 depth: queue.depth as u64,
4327 high_water: queue.high_water as u64,
4328 capacity: queue.capacity as u64,
4329 fill_percent: queue.fill_percent as u64,
4330 })
4331 .collect();
4332
4333 Ok(DispatchResult {
4334 response: self.respond(
4335 request,
4336 ResponsePayload::ResourceSnapshot(ResourceSnapshotResponse {
4337 running_processes: snapshot.running_processes as u64,
4338 exited_processes: snapshot.exited_processes as u64,
4339 fd_tables: snapshot.fd_tables as u64,
4340 open_fds: snapshot.open_fds as u64,
4341 pipes: snapshot.pipes as u64,
4342 pipe_buffered_bytes: snapshot.pipe_buffered_bytes as u64,
4343 ptys: snapshot.ptys as u64,
4344 pty_buffered_input_bytes: snapshot.pty_buffered_input_bytes as u64,
4345 pty_buffered_output_bytes: snapshot.pty_buffered_output_bytes as u64,
4346 sockets: snapshot.sockets as u64,
4347 socket_listeners: snapshot.socket_listeners as u64,
4348 socket_connections: snapshot.socket_connections as u64,
4349 socket_buffered_bytes: snapshot.socket_buffered_bytes as u64,
4350 socket_datagram_queue_len: snapshot.socket_datagram_queue_len as u64,
4351 queue_snapshots,
4352 }),
4353 ),
4354 events: Vec::new(),
4355 })
4356 }
4357
4358 pub(crate) async fn find_bound_udp(
4359 &mut self,
4360 request: &RequestFrame,
4361 payload: FindBoundUdpRequest,
4362 ) -> Result<DispatchResult, SidecarError> {
4363 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4364 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4365
4366 let lookup_request = FindListenerRequest {
4367 host: payload.host,
4368 port: payload.port,
4369 path: None,
4370 };
4371 require_vm_inspection_permission(
4372 &self.bridge,
4373 &vm_id,
4374 "network.inspect",
4375 "network",
4376 &socket_query_resource(SocketQueryKind::UdpBound, &lookup_request),
4377 )?;
4378 let socket = find_socket_state_entry(
4379 self.vms.get(&vm_id),
4380 SocketQueryKind::UdpBound,
4381 &lookup_request,
4382 )?;
4383
4384 Ok(DispatchResult {
4385 response: bound_udp_snapshot_response(request, socket),
4386 events: Vec::new(),
4387 })
4388 }
4389
4390 pub(crate) async fn vm_fetch(
4391 &mut self,
4392 request: &RequestFrame,
4393 payload: VmFetchRequest,
4394 ) -> Result<DispatchResult, SidecarError> {
4395 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4396 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4397
4398 let vm = self
4399 .vms
4400 .get_mut(&vm_id)
4401 .ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
4402 let target_path = if payload.path.starts_with('/') {
4403 payload.path.clone()
4404 } else {
4405 format!("/{}", payload.path)
4406 };
4407 let request_url = Url::parse(&format!("http://127.0.0.1:{}{target_path}", payload.port))
4408 .map_err(|error| {
4409 SidecarError::InvalidState(format!(
4410 "invalid vm.fetch target {target_path:?}: {error}"
4411 ))
4412 })?;
4413 let header_values: BTreeMap<String, Value> = serde_json::from_str(&payload.headers_json)
4414 .map_err(|error| {
4415 SidecarError::InvalidState(format!(
4416 "vm.fetch headers_json must be valid JSON: {error}"
4417 ))
4418 })?;
4419 let options = JavascriptHttpRequestOptions {
4420 method: Some(payload.method),
4421 headers: header_values,
4422 body: payload.body,
4423 reject_unauthorized: None,
4424 };
4425 let headers = parse_http_header_collection(&options.headers, "vm.fetch headers")?;
4426 let target_process_id = find_kernel_http_listener_process(vm, payload.port);
4427 if let Some(target_process_id) = target_process_id {
4428 let max_fetch_response_bytes = vm.limits.http.max_fetch_response_bytes;
4429 let response_json = match dispatch_kernel_http_fetch(
4430 &self.bridge,
4431 &vm_id,
4432 vm,
4433 &target_process_id,
4434 payload.port,
4435 &target_path,
4436 &options,
4437 &headers,
4438 max_fetch_response_bytes,
4439 ) {
4440 Ok(response_json) => response_json,
4441 Err(error) => {
4442 if let Some(exit_code) = kernel_http_fetch_target_exit_code(&error) {
4443 let _ = vm;
4444 self.finish_active_process_exit(&vm_id, &target_process_id, exit_code)?;
4445 }
4446 return Err(error);
4447 }
4448 };
4449 let response = self.respond(
4450 request,
4451 ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
4452 );
4453 ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
4454
4455 return Ok(DispatchResult {
4456 response,
4457 events: Vec::new(),
4458 });
4459 }
4460
4461 let Some((target_process_id, server_id)) =
4462 vm.active_processes
4463 .iter()
4464 .find_map(|(process_id, process)| {
4465 process
4466 .http_servers
4467 .iter()
4468 .find(|(_, server)| server.guest_local_addr.port() == payload.port)
4469 .map(|(server_id, _)| (process_id.clone(), *server_id))
4470 })
4471 else {
4472 return Err(SidecarError::Execution(format!(
4473 "vm.fetch could not find a guest HTTP listener on port {}",
4474 payload.port
4475 )));
4476 };
4477 let socket_paths = build_javascript_socket_path_context(vm)?;
4478 let resource_limits = vm.kernel.resource_limits().clone();
4479 let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness);
4480 let process = vm
4481 .active_processes
4482 .get_mut(&target_process_id)
4483 .ok_or_else(|| {
4484 SidecarError::InvalidState(format!(
4485 "vm.fetch target process disappeared: {target_process_id}"
4486 ))
4487 })?;
4488 let request_json = serialize_http_loopback_request(&request_url, &options, &headers)?;
4489 let response_json = dispatch_loopback_http_request(LoopbackHttpDispatchRequest {
4490 bridge: &self.bridge,
4491 vm_id: &vm_id,
4492 dns: &vm.dns,
4493 socket_paths: &socket_paths,
4494 kernel: &mut vm.kernel,
4495 kernel_readiness,
4496 process,
4497 resource_limits: &resource_limits,
4498 server_id,
4499 request_json: &request_json,
4500 })?;
4501
4502 let response = self.respond(
4503 request,
4504 ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
4505 );
4506 ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
4507
4508 Ok(DispatchResult {
4509 response,
4510 events: Vec::new(),
4511 })
4512 }
4513
4514 pub(crate) async fn get_signal_state(
4515 &mut self,
4516 request: &RequestFrame,
4517 payload: GetSignalStateRequest,
4518 ) -> Result<DispatchResult, SidecarError> {
4519 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4520 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4521
4522 let handlers = self
4523 .vms
4524 .get(&vm_id)
4525 .and_then(|vm| vm.signal_states.get(&payload.process_id))
4526 .cloned()
4527 .unwrap_or_default();
4528
4529 Ok(DispatchResult {
4530 response: signal_state_response(request, payload.process_id, handlers),
4531 events: Vec::new(),
4532 })
4533 }
4534
4535 pub(crate) async fn get_zombie_timer_count(
4536 &mut self,
4537 request: &RequestFrame,
4538 _payload: GetZombieTimerCountRequest,
4539 ) -> Result<DispatchResult, SidecarError> {
4540 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4541 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4542
4543 let count = self
4544 .vms
4545 .get(&vm_id)
4546 .map(|vm| vm.kernel.zombie_timer_count() as u64)
4547 .unwrap_or_default();
4548
4549 Ok(DispatchResult {
4550 response: zombie_timer_count_response(request, count),
4551 events: Vec::new(),
4552 })
4553 }
4554
4555 pub(crate) fn kill_process_internal(
4556 &mut self,
4557 vm_id: &str,
4558 process_id: &str,
4559 signal: &str,
4560 ) -> Result<(), SidecarError> {
4561 let signal_name = signal.to_owned();
4562 let signal = parse_signal(signal)?;
4563 let vm = self
4564 .vms
4565 .get_mut(vm_id)
4566 .ok_or_else(|| SidecarError::InvalidState(format!("unknown sidecar VM {vm_id}")))?;
4567 let process = vm.active_processes.get_mut(process_id).ok_or_else(|| {
4568 SidecarError::InvalidState(format!("VM {vm_id} has no active process {process_id}"))
4569 })?;
4570 let kernel_pid = process.kernel_pid;
4571 if !matches!(signal, 0 | libc::SIGCONT) {
4572 flush_parked_kernel_wait_rpc(process);
4576 }
4577
4578 enum KillBehavior {
4579 Tool,
4580 SharedV8StateOnly,
4581 SharedV8Continue,
4582 SharedV8Terminate,
4583 SharedV8DispatchOrTerminate,
4584 Noop,
4585 HostPid(u32),
4586 }
4587
4588 let behavior = match &process.execution {
4589 ActiveExecution::Tool(_) => KillBehavior::Tool,
4590 ActiveExecution::Javascript(execution)
4591 if execution.uses_shared_v8_runtime() && matches!(signal, 0 | libc::SIGSTOP) =>
4592 {
4593 KillBehavior::SharedV8StateOnly
4594 }
4595 ActiveExecution::Javascript(execution)
4596 if execution.uses_shared_v8_runtime() && signal == libc::SIGCONT =>
4597 {
4598 KillBehavior::SharedV8Continue
4599 }
4600 ActiveExecution::Wasm(execution)
4601 if execution.uses_shared_v8_runtime()
4602 && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
4603 {
4604 KillBehavior::SharedV8StateOnly
4605 }
4606 ActiveExecution::Python(execution)
4607 if execution.uses_shared_v8_runtime()
4608 && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
4609 {
4610 KillBehavior::SharedV8StateOnly
4611 }
4612 ActiveExecution::Javascript(execution)
4613 if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
4614 {
4615 KillBehavior::SharedV8Terminate
4616 }
4617 ActiveExecution::Wasm(execution)
4618 if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
4619 {
4620 KillBehavior::SharedV8Terminate
4621 }
4622 ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime() => {
4623 KillBehavior::SharedV8DispatchOrTerminate
4624 }
4625 ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime() => {
4626 KillBehavior::SharedV8Terminate
4627 }
4628 ActiveExecution::Python(execution) if execution.uses_shared_v8_runtime() => {
4629 KillBehavior::SharedV8Terminate
4630 }
4631 ActiveExecution::Javascript(execution) if execution.child_pid() == 0 => {
4632 KillBehavior::Noop
4633 }
4634 _ => KillBehavior::HostPid(process.execution.child_pid()),
4635 };
4636
4637 match behavior {
4638 KillBehavior::Tool => {
4639 let ActiveExecution::Tool(execution) = &process.execution else {
4640 unreachable!("kill behavior must match tool execution");
4641 };
4642 if signal != 0 {
4643 execution.cancelled.store(true, Ordering::Relaxed);
4644 process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
4645 128 + signal,
4646 ))?;
4647 }
4648 }
4649 KillBehavior::SharedV8StateOnly => {
4650 if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
4651 vm.kernel
4652 .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
4653 .map_err(kernel_error)?;
4654 }
4655 }
4656 KillBehavior::SharedV8Continue => {
4657 vm.kernel
4658 .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
4659 .map_err(kernel_error)?;
4660 if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
4661 process.execution.terminate()?;
4662 }
4663 }
4664 KillBehavior::SharedV8Terminate => {
4665 if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
4666 close_kernel_process_stdin(&mut vm.kernel, process)?;
4667 }
4668 process.execution.terminate()?;
4669 let needs_synthetic_exit = matches!(process.execution, ActiveExecution::Wasm(_))
4670 || (signal == SIGKILL
4671 && matches!(process.execution, ActiveExecution::Javascript(_)));
4672 if signal != 0 && needs_synthetic_exit {
4673 process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
4674 128 + signal,
4675 ))?;
4676 }
4677 }
4678 KillBehavior::SharedV8DispatchOrTerminate => {
4679 if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
4680 process.execution.terminate()?;
4681 }
4682 }
4683 KillBehavior::Noop => {}
4684 KillBehavior::HostPid(pid) => {
4685 if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
4686 close_kernel_process_stdin(&mut vm.kernel, process)?;
4687 }
4688 signal_runtime_process(pid, signal)?;
4689 }
4690 }
4691 emit_security_audit_event(
4692 &self.bridge,
4693 vm_id,
4694 "security.process.kill",
4695 audit_fields([
4696 (String::from("source"), String::from("control_plane")),
4697 (String::from("source_pid"), String::from("0")),
4698 (String::from("target_pid"), process.kernel_pid.to_string()),
4699 (String::from("process_id"), process_id.to_owned()),
4700 (String::from("signal"), signal_name),
4701 (
4702 String::from("host_pid"),
4703 process.execution.child_pid().to_string(),
4704 ),
4705 ]),
4706 );
4707 Ok(())
4708 }
4709
4710 pub async fn pump_process_events(
4711 &mut self,
4712 ownership: &OwnershipScope,
4713 ) -> Result<bool, SidecarError> {
4714 let mut emitted_any = false;
4715
4716 let mut queued_envelopes = Vec::new();
4717 {
4718 let pending_capacity = self.pending_process_event_capacity();
4719 let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
4720 SidecarError::InvalidState(String::from("process event receiver unavailable"))
4721 })?;
4722 loop {
4723 if queued_envelopes.len() >= pending_capacity {
4724 if receiver.is_empty() {
4725 break;
4726 }
4727 return Err(process_event_queue_overflow_error());
4728 }
4729 match receiver.try_recv() {
4730 Ok(envelope) => {
4731 queued_envelopes.push(envelope);
4732 emitted_any = true;
4733 }
4734 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
4735 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
4736 }
4737 }
4738 }
4739 for envelope in queued_envelopes {
4740 self.queue_pending_process_event(envelope)?;
4741 }
4742
4743 let vm_ids = self.vm_ids_for_scope(ownership)?;
4744 for vm_id in vm_ids {
4745 while let Some(vm) = self.vms.get(&vm_id) {
4746 let connection_id = vm.connection_id.clone();
4747 let session_id = vm.session_id.clone();
4748 let process_ids = self
4749 .vms
4750 .get(&vm_id)
4751 .map(|vm| vm.active_processes.keys().cloned().collect::<Vec<_>>())
4752 .unwrap_or_default();
4753 let mut emitted_this_pass = false;
4754
4755 for process_id in process_ids {
4756 if self
4757 .vms
4758 .get(&vm_id)
4759 .is_some_and(|vm| vm.detached_child_processes.contains(&process_id))
4760 {
4761 continue;
4762 }
4763 enum ProcessPollResult {
4764 Event(Box<Option<ActiveExecutionEvent>>),
4765 RecoverClosedChannel,
4766 }
4767 let poll_result = {
4768 let Some(vm) = self.vms.get_mut(&vm_id) else {
4769 continue;
4770 };
4771 let Some(process) = vm.active_processes.get_mut(&process_id) else {
4772 continue;
4773 };
4774 if let Some(event) = process.pending_execution_events.pop_front() {
4775 ProcessPollResult::Event(Box::new(Some(event)))
4776 } else {
4777 match process.execution.poll_event(Duration::ZERO).await {
4778 Ok(event) => ProcessPollResult::Event(Box::new(event)),
4779 Err(SidecarError::Execution(message))
4780 if (process.runtime == GuestRuntimeKind::JavaScript
4781 && closed_javascript_event_channel(&message))
4782 || (process.runtime == GuestRuntimeKind::Python
4783 && closed_python_event_channel(&message))
4784 || (process.runtime == GuestRuntimeKind::WebAssembly
4785 && closed_wasm_event_channel(&message)) =>
4786 {
4787 ProcessPollResult::RecoverClosedChannel
4788 }
4789 Err(other) => return Err(other),
4790 }
4791 }
4792 };
4793 let event = match poll_result {
4794 ProcessPollResult::Event(event) => *event,
4795 ProcessPollResult::RecoverClosedChannel => {
4796 self.recover_closed_root_runtime_process_event(&vm_id, &process_id)?
4797 }
4798 };
4799
4800 let Some(event) = event else {
4801 continue;
4802 };
4803 if matches!(&event, ActiveExecutionEvent::Exited(_)) {
4804 record_execute_response_to_exit_milestone(
4805 "execute_response_to_exit_event_polled",
4806 &vm_id,
4807 &process_id,
4808 );
4809 }
4810
4811 if Self::internal_execution_event(&event) {
4812 self.handle_execution_event(&vm_id, &process_id, event)?;
4817 } else {
4818 self.queue_pending_process_event(ProcessEventEnvelope {
4819 connection_id: connection_id.clone(),
4820 session_id: session_id.clone(),
4821 vm_id: vm_id.clone(),
4822 process_id: process_id.clone(),
4823 event,
4824 })?;
4825 }
4826 emitted_any = true;
4827 emitted_this_pass = true;
4828 }
4829
4830 if !emitted_this_pass {
4831 break;
4832 }
4833 }
4834
4835 if self.pump_detached_child_process_events(&vm_id)? {
4836 emitted_any = true;
4837 }
4838 }
4839
4840 Ok(emitted_any)
4841 }
4842
4843 fn internal_execution_event(event: &ActiveExecutionEvent) -> bool {
4844 matches!(
4845 event,
4846 ActiveExecutionEvent::JavascriptSyncRpcRequest(_)
4847 | ActiveExecutionEvent::PythonVfsRpcRequest(_)
4848 | ActiveExecutionEvent::SignalState { .. }
4849 )
4850 }
4851
4852 fn recover_closed_root_runtime_process_event(
4853 &mut self,
4854 vm_id: &str,
4855 process_id: &str,
4856 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
4857 let Some(vm) = self.vms.get_mut(vm_id) else {
4858 return Ok(None);
4859 };
4860 let Some(process) = vm.active_processes.get(process_id) else {
4861 return Ok(None);
4862 };
4863 if process.execution.uses_shared_v8_runtime() {
4864 return Ok(None);
4865 }
4866 if process.runtime != GuestRuntimeKind::JavaScript
4867 && process.runtime != GuestRuntimeKind::Python
4868 && process.runtime != GuestRuntimeKind::WebAssembly
4869 {
4870 return Ok(None);
4871 }
4872 let runtime_child_pid = process.execution.child_pid();
4873 if runtime_child_pid == 0 {
4874 return Ok(None);
4875 }
4876 if let Some(status) = runtime_child_exit_status(runtime_child_pid)? {
4877 return Ok(Some(ActiveExecutionEvent::Exited(status)));
4878 }
4879 if runtime_child_is_alive(runtime_child_pid)? {
4880 return Ok(None);
4881 }
4882 Ok(Some(ActiveExecutionEvent::Exited(0)))
4883 }
4884
4885 fn active_process_by_path<'a>(
4886 process: &'a ActiveProcess,
4887 child_path: &[&str],
4888 ) -> Option<&'a ActiveProcess> {
4889 let mut current = process;
4890 for child_id in child_path {
4891 current = current.child_processes.get(*child_id)?;
4892 }
4893 Some(current)
4894 }
4895
4896 fn active_process_by_path_mut<'a>(
4897 process: &'a mut ActiveProcess,
4898 child_path: &[&str],
4899 ) -> Option<&'a mut ActiveProcess> {
4900 let mut current = process;
4901 for child_id in child_path {
4902 current = current.child_processes.get_mut(*child_id)?;
4903 }
4904 Some(current)
4905 }
4906
4907 fn active_process_by_owned_path_mut<'a>(
4908 process: &'a mut ActiveProcess,
4909 child_path: &[String],
4910 ) -> Option<&'a mut ActiveProcess> {
4911 let mut current = process;
4912 for child_id in child_path {
4913 current = current.child_processes.get_mut(child_id)?;
4914 }
4915 Some(current)
4916 }
4917
4918 fn active_process_path_by_kernel_pid(
4919 process: &ActiveProcess,
4920 kernel_pid: u32,
4921 ) -> Option<Vec<String>> {
4922 if process.kernel_pid == kernel_pid {
4923 return Some(Vec::new());
4924 }
4925
4926 for (child_id, child) in &process.child_processes {
4927 let Some(mut path) = Self::active_process_path_by_kernel_pid(child, kernel_pid) else {
4928 continue;
4929 };
4930 path.insert(0, child_id.clone());
4931 return Some(path);
4932 }
4933
4934 None
4935 }
4936
4937 fn descendant_parent_process<'a>(
4938 vm: &'a VmState,
4939 process_id: &str,
4940 child_path: &[&str],
4941 ) -> Option<&'a ActiveProcess> {
4942 let root = vm.active_processes.get(process_id)?;
4943 Self::active_process_by_path(root, child_path)
4944 }
4945
4946 fn descendant_parent_process_mut<'a>(
4947 vm: &'a mut VmState,
4948 process_id: &str,
4949 child_path: &[&str],
4950 ) -> Option<&'a mut ActiveProcess> {
4951 let root = vm.active_processes.get_mut(process_id)?;
4952 Self::active_process_by_path_mut(root, child_path)
4953 }
4954
4955 fn child_process_path_label(process_id: &str, child_path: &[&str]) -> String {
4956 if child_path.is_empty() {
4957 process_id.to_owned()
4958 } else {
4959 format!("{process_id}/{}", child_path.join("/"))
4960 }
4961 }
4962
4963 fn adopt_detached_child_processes(
4964 current_process_id: &str,
4965 process: &mut ActiveProcess,
4966 ) -> Vec<(String, ActiveProcess)> {
4967 let mut adopted = Vec::new();
4968 let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
4969 for child_id in child_ids {
4970 let child_process_id = format!("{current_process_id}/{child_id}");
4971 let Some(mut child) = process.child_processes.remove(&child_id) else {
4972 continue;
4973 };
4974 if child.detached {
4975 adopted.push((child_process_id, child));
4976 continue;
4977 }
4978
4979 adopted.extend(Self::adopt_detached_child_processes(
4980 &child_process_id,
4981 &mut child,
4982 ));
4983 process.child_processes.insert(child_id, child);
4984 }
4985 adopted
4986 }
4987
4988 fn child_process_signal_key<'a>(process_id: &'a str, child_path: &[&'a str]) -> &'a str {
4989 child_path.last().copied().unwrap_or(process_id)
4990 }
4991
4992 fn resolve_detached_child_process_path(
4993 vm: &VmState,
4994 detached_process_id: &str,
4995 ) -> Option<(String, Vec<String>)> {
4996 let root_process_id = vm
4997 .active_processes
4998 .keys()
4999 .filter(|candidate| {
5000 detached_process_id == candidate.as_str()
5001 || detached_process_id
5002 .strip_prefix(candidate.as_str())
5003 .is_some_and(|remainder| remainder.starts_with('/'))
5004 })
5005 .max_by_key(|candidate| candidate.len())?
5006 .clone();
5007
5008 let remainder = detached_process_id
5009 .strip_prefix(root_process_id.as_str())
5010 .unwrap_or_default();
5011 if remainder.is_empty() {
5012 return Some((root_process_id, Vec::new()));
5013 }
5014
5015 Some((
5016 root_process_id,
5017 remainder
5018 .trim_start_matches('/')
5019 .split('/')
5020 .map(str::to_owned)
5021 .collect(),
5022 ))
5023 }
5024
5025 fn pump_detached_child_process_events(&mut self, vm_id: &str) -> Result<bool, SidecarError> {
5026 let detached_process_ids = self
5027 .vms
5028 .get(vm_id)
5029 .map(|vm| {
5030 vm.detached_child_processes
5031 .iter()
5032 .cloned()
5033 .collect::<Vec<_>>()
5034 })
5035 .unwrap_or_default();
5036 let mut emitted_any = false;
5037 for detached_process_id in detached_process_ids {
5038 let Some((root_process_id, child_path)) = self
5039 .vms
5040 .get(vm_id)
5041 .and_then(|vm| Self::resolve_detached_child_process_path(vm, &detached_process_id))
5042 else {
5043 if let Some(vm) = self.vms.get_mut(vm_id) {
5044 vm.detached_child_processes.remove(&detached_process_id);
5045 }
5046 continue;
5047 };
5048 if child_path.is_empty() {
5049 loop {
5050 enum ProcessPollResult {
5051 Event(Box<Option<ActiveExecutionEvent>>),
5052 RecoverClosedChannel,
5053 }
5054 let poll_result = {
5055 let Some(vm) = self.vms.get_mut(vm_id) else {
5056 break;
5057 };
5058 let Some(process) = vm.active_processes.get_mut(&root_process_id) else {
5059 break;
5060 };
5061 if let Some(event) = process.pending_execution_events.pop_front() {
5062 ProcessPollResult::Event(Box::new(Some(event)))
5063 } else {
5064 match process.execution.poll_event_blocking(Duration::ZERO) {
5065 Ok(event) => ProcessPollResult::Event(Box::new(event)),
5066 Err(SidecarError::Execution(message))
5067 if (process.runtime == GuestRuntimeKind::JavaScript
5068 && closed_javascript_event_channel(&message))
5069 || (process.runtime == GuestRuntimeKind::Python
5070 && closed_python_event_channel(&message))
5071 || (process.runtime == GuestRuntimeKind::WebAssembly
5072 && closed_wasm_event_channel(&message)) =>
5073 {
5074 ProcessPollResult::RecoverClosedChannel
5075 }
5076 Err(error) => return Err(error),
5077 }
5078 }
5079 };
5080 let event = match poll_result {
5081 ProcessPollResult::Event(event) => *event,
5082 ProcessPollResult::RecoverClosedChannel => {
5083 self.recover_closed_root_runtime_process_event(vm_id, &root_process_id)?
5084 }
5085 };
5086 let Some(event) = event else {
5087 break;
5088 };
5089 if matches!(&event, ActiveExecutionEvent::Exited(_)) {
5090 record_execute_response_to_exit_milestone(
5091 "execute_response_to_detached_exit_event_polled",
5092 vm_id,
5093 &detached_process_id,
5094 );
5095 }
5096 let Some((connection_id, session_id)) = self
5097 .vms
5098 .get(vm_id)
5099 .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
5100 else {
5101 break;
5102 };
5103 match event {
5104 ActiveExecutionEvent::Stdout(chunk) => {
5105 self.queue_pending_process_event(ProcessEventEnvelope {
5106 connection_id,
5107 session_id,
5108 vm_id: vm_id.to_owned(),
5109 process_id: detached_process_id.clone(),
5110 event: ActiveExecutionEvent::Stdout(chunk),
5111 })?;
5112 emitted_any = true;
5113 }
5114 ActiveExecutionEvent::Stderr(chunk) => {
5115 self.queue_pending_process_event(ProcessEventEnvelope {
5116 connection_id,
5117 session_id,
5118 vm_id: vm_id.to_owned(),
5119 process_id: detached_process_id.clone(),
5120 event: ActiveExecutionEvent::Stderr(chunk),
5121 })?;
5122 emitted_any = true;
5123 }
5124 ActiveExecutionEvent::Exited(exit_code) => {
5125 if let Some(vm) = self.vms.get_mut(vm_id) {
5126 vm.detached_child_processes.remove(&detached_process_id);
5127 }
5128 self.queue_pending_process_event(ProcessEventEnvelope {
5129 connection_id,
5130 session_id,
5131 vm_id: vm_id.to_owned(),
5132 process_id: detached_process_id.clone(),
5133 event: ActiveExecutionEvent::Exited(exit_code),
5134 })?;
5135 emitted_any = true;
5136 break;
5137 }
5138 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
5139 self.handle_javascript_sync_rpc_request(
5140 vm_id,
5141 &root_process_id,
5142 request,
5143 )?;
5144 }
5145 ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
5146 self.handle_python_vfs_rpc_request(vm_id, &root_process_id, *request)?;
5147 }
5148 ActiveExecutionEvent::SignalState {
5149 signal,
5150 registration,
5151 } => {
5152 if let Some(vm) = self.vms.get_mut(vm_id) {
5153 vm.signal_states
5154 .entry(root_process_id.clone())
5155 .or_default()
5156 .insert(signal, registration);
5157 }
5158 }
5159 }
5160 }
5161 continue;
5162 }
5163
5164 let parent_path = child_path[..child_path.len() - 1]
5165 .iter()
5166 .map(String::as_str)
5167 .collect::<Vec<_>>();
5168 let child_process_id = child_path.last().expect("child path cannot be empty");
5169
5170 loop {
5171 let event = match self.poll_descendant_javascript_child_process(
5172 vm_id,
5173 &root_process_id,
5174 &parent_path,
5175 child_process_id,
5176 0,
5177 ) {
5178 Ok(event) => event,
5179 Err(SidecarError::InvalidState(message))
5180 if message.contains("unknown child process")
5181 || message.contains("unknown child process path") =>
5182 {
5183 if let Some(vm) = self.vms.get_mut(vm_id) {
5184 vm.detached_child_processes.remove(&detached_process_id);
5185 }
5186 break;
5187 }
5188 Err(error) if is_javascript_child_process_gone_error(&error) => {
5189 if let Some(vm) = self.vms.get_mut(vm_id) {
5190 vm.detached_child_processes.remove(&detached_process_id);
5191 }
5192 break;
5193 }
5194 Err(error) => return Err(error),
5195 };
5196
5197 let Some(event_type) = event.get("type").and_then(Value::as_str) else {
5198 break;
5199 };
5200 let Some((connection_id, session_id)) = self
5201 .vms
5202 .get(vm_id)
5203 .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
5204 else {
5205 break;
5206 };
5207
5208 let envelope = match event_type {
5209 "stdout" => Some(ProcessEventEnvelope {
5210 connection_id: connection_id.clone(),
5211 session_id: session_id.clone(),
5212 vm_id: vm_id.to_owned(),
5213 process_id: detached_process_id.clone(),
5214 event: ActiveExecutionEvent::Stdout(javascript_sync_rpc_bytes_arg(
5215 &[event.get("data").cloned().unwrap_or(Value::Null)],
5216 0,
5217 "detached child_process stdout",
5218 )?),
5219 }),
5220 "stderr" => Some(ProcessEventEnvelope {
5221 connection_id: connection_id.clone(),
5222 session_id: session_id.clone(),
5223 vm_id: vm_id.to_owned(),
5224 process_id: detached_process_id.clone(),
5225 event: ActiveExecutionEvent::Stderr(javascript_sync_rpc_bytes_arg(
5226 &[event.get("data").cloned().unwrap_or(Value::Null)],
5227 0,
5228 "detached child_process stderr",
5229 )?),
5230 }),
5231 "exit" => {
5232 if let Some(vm) = self.vms.get_mut(vm_id) {
5233 vm.detached_child_processes.remove(&detached_process_id);
5234 }
5235 Some(ProcessEventEnvelope {
5236 connection_id,
5237 session_id,
5238 vm_id: vm_id.to_owned(),
5239 process_id: detached_process_id.clone(),
5240 event: ActiveExecutionEvent::Exited(
5241 event
5242 .get("exitCode")
5243 .and_then(Value::as_i64)
5244 .map(|value| value as i32)
5245 .unwrap_or(1),
5246 ),
5247 })
5248 }
5249 _ => None,
5250 };
5251
5252 let Some(envelope) = envelope else {
5253 break;
5254 };
5255 self.queue_pending_process_event(envelope)?;
5256 emitted_any = true;
5257
5258 if event_type == "exit" {
5259 break;
5260 }
5261 }
5262 }
5263
5264 Ok(emitted_any)
5265 }
5266 pub(crate) fn drain_queued_descendant_javascript_child_process_events(
5267 &mut self,
5268 vm_id: &str,
5269 process_id: &str,
5270 child_path: &[&str],
5271 ) -> Result<(), SidecarError> {
5272 if child_path.is_empty() {
5273 return Ok(());
5274 }
5275 let target_process_id = Self::child_process_path_label(process_id, child_path);
5276 let mut child_capacity = self
5277 .vms
5278 .get(vm_id)
5279 .and_then(|vm| vm.active_processes.get(process_id))
5280 .and_then(|root| descendant_pending_execution_event_capacity(root, child_path));
5281
5282 let mut deferred = VecDeque::new();
5283 while let Some(envelope) = self.pending_process_events.pop_front() {
5284 if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
5285 if matches!(child_capacity, Some(0)) {
5286 self.pending_process_events.push_front(envelope);
5287 while let Some(deferred_envelope) = deferred.pop_back() {
5288 self.pending_process_events.push_front(deferred_envelope);
5289 }
5290 return Err(process_event_queue_overflow_error());
5291 }
5292 if let Some(vm) = self.vms.get_mut(vm_id) {
5293 if let Some(root) = vm.active_processes.get_mut(process_id) {
5294 if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
5295 child.queue_pending_execution_event(envelope.event)?;
5296 child_capacity = child_capacity.map(|capacity| capacity - 1);
5297 continue;
5298 }
5299 }
5300 }
5301 }
5302 deferred.push_back(envelope);
5303 }
5304 self.pending_process_events = deferred;
5305
5306 let mut queued = Vec::new();
5307 {
5308 let transfer_capacity = self
5309 .pending_process_event_capacity()
5310 .min(child_capacity.unwrap_or(usize::MAX));
5311 let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
5312 SidecarError::InvalidState(String::from("process event receiver unavailable"))
5313 })?;
5314 loop {
5315 if queued.len() >= transfer_capacity {
5316 if receiver.is_empty() {
5317 break;
5318 }
5319 return Err(process_event_queue_overflow_error());
5320 }
5321 match receiver.try_recv() {
5322 Ok(envelope) => queued.push(envelope),
5323 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
5324 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
5325 }
5326 }
5327 }
5328 for envelope in queued {
5329 if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
5330 if let Some(vm) = self.vms.get_mut(vm_id) {
5331 if let Some(root) = vm.active_processes.get_mut(process_id) {
5332 if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
5333 child.queue_pending_execution_event(envelope.event)?;
5334 continue;
5335 }
5336 }
5337 }
5338 }
5339 self.queue_pending_process_event(envelope)?;
5340 }
5341
5342 Ok(())
5343 }
5344
5345 pub(crate) fn handle_execution_event(
5346 &mut self,
5347 vm_id: &str,
5348 process_id: &str,
5349 event: ActiveExecutionEvent,
5350 ) -> Result<Option<EventFrame>, SidecarError> {
5351 let Some(vm) = self.vms.get(vm_id) else {
5352 log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
5353 return Ok(None);
5354 };
5355 if !vm.active_processes.contains_key(process_id) {
5356 log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
5357 return Ok(None);
5358 }
5359 let (connection_id, session_id) = { (vm.connection_id.clone(), vm.session_id.clone()) };
5360 let ownership = OwnershipScope::vm(&connection_id, &session_id, vm_id);
5361
5362 if self.capture_extension_process_output_event(vm_id, process_id, &event) {
5363 return Ok(None);
5364 }
5365
5366 match event {
5367 ActiveExecutionEvent::Stdout(chunk) => Ok(Some(EventFrame::new(
5368 ownership,
5369 EventPayload::ProcessOutput(ProcessOutputEvent {
5370 process_id: process_id.to_owned(),
5371 channel: StreamChannel::Stdout,
5372 chunk,
5373 }),
5374 ))),
5375 ActiveExecutionEvent::Stderr(chunk) => Ok(Some(EventFrame::new(
5376 ownership,
5377 EventPayload::ProcessOutput(ProcessOutputEvent {
5378 process_id: process_id.to_owned(),
5379 channel: StreamChannel::Stderr,
5380 chunk,
5381 }),
5382 ))),
5383 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
5384 self.handle_javascript_sync_rpc_request(vm_id, process_id, request)?;
5385 Ok(None)
5386 }
5387 ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
5388 self.handle_python_vfs_rpc_request(vm_id, process_id, *request)?;
5389 Ok(None)
5390 }
5391 ActiveExecutionEvent::SignalState {
5392 signal,
5393 registration,
5394 } => {
5395 let Some(vm) = self.vms.get_mut(vm_id) else {
5396 return Ok(None);
5397 };
5398 if !vm.active_processes.contains_key(process_id) {
5399 return Ok(None);
5400 }
5401 vm.signal_states
5402 .entry(process_id.to_owned())
5403 .or_default()
5404 .insert(signal, registration);
5405 Ok(None)
5406 }
5407 ActiveExecutionEvent::Exited(exit_code) => {
5408 record_execute_response_to_exit_milestone(
5409 "execute_response_to_exit_event_handle",
5410 vm_id,
5411 process_id,
5412 );
5413 record_execute_response_to_exit(vm_id, process_id);
5414 let phase_start = Instant::now();
5415 let became_idle = self
5416 .finish_active_process_exit(vm_id, process_id, exit_code)?
5417 .unwrap_or(false);
5418 record_execute_phase("process_exit_cleanup", phase_start.elapsed());
5419
5420 let phase_start = Instant::now();
5421 if became_idle {
5422 self.bridge.emit_lifecycle(vm_id, LifecycleState::Ready)?;
5423 }
5424 record_execute_phase("process_exit_lifecycle_emit", phase_start.elapsed());
5425
5426 Ok(Some(EventFrame::new(
5427 ownership,
5428 EventPayload::ProcessExited(ProcessExitedEvent {
5429 process_id: process_id.to_owned(),
5430 exit_code,
5431 }),
5432 )))
5433 }
5434 }
5435 }
5436
5437 pub(crate) fn finish_active_process_exit(
5438 &mut self,
5439 vm_id: &str,
5440 process_id: &str,
5441 exit_code: i32,
5442 ) -> Result<Option<bool>, SidecarError> {
5443 let Some(vm) = self.vms.get_mut(vm_id) else {
5444 log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
5445 return Ok(None);
5446 };
5447 if !vm.active_processes.contains_key(process_id) {
5448 log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
5449 return Ok(None);
5450 }
5451
5452 let phase_start = Instant::now();
5453 prune_exited_process_snapshots(vm);
5454 record_execute_phase(
5455 "process_exit_cleanup_prune_snapshots",
5456 phase_start.elapsed(),
5457 );
5458 let phase_start = Instant::now();
5459 let process_table = vm.kernel.list_processes();
5460 record_execute_phase("process_exit_cleanup_list_processes", phase_start.elapsed());
5461 let phase_start = Instant::now();
5462 let Some(mut process) = vm.active_processes.remove(process_id) else {
5463 return Ok(None);
5464 };
5465 record_execute_phase("process_exit_cleanup_remove_active", phase_start.elapsed());
5466 let phase_start = Instant::now();
5467 if let Some(info) = process_table.get(&process.kernel_pid) {
5468 vm.exited_process_snapshots
5469 .push_back(ExitedProcessSnapshot {
5470 captured_at: Instant::now(),
5471 process: build_process_snapshot_entry(
5472 process_id,
5473 &process,
5474 info,
5475 Some(exit_code),
5476 ),
5477 });
5478 }
5479 record_execute_phase("process_exit_cleanup_build_snapshot", phase_start.elapsed());
5480 let phase_start = Instant::now();
5481 let detached_children = Self::adopt_detached_child_processes(process_id, &mut process);
5482 record_execute_phase("process_exit_cleanup_adopt_detached", phase_start.elapsed());
5483 let phase_start = Instant::now();
5484 let should_sync_host_writes = process.host_write_dirty_recursive()
5485 || !process.clean_host_writes_are_observable_recursive();
5486 if should_sync_host_writes {
5487 sync_process_host_writes_to_kernel(vm, &process)?;
5488 } else {
5489 record_execute_phase(
5490 "process_exit_cleanup_sync_host_writes_clean_skip",
5491 Duration::ZERO,
5492 );
5493 }
5494 record_execute_phase(
5495 "process_exit_cleanup_sync_host_writes",
5496 phase_start.elapsed(),
5497 );
5498 let phase_start = Instant::now();
5499 let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness);
5500 terminate_child_process_tree(&mut vm.kernel, &mut process, &kernel_readiness);
5501 record_execute_phase(
5502 "process_exit_cleanup_terminate_child_tree",
5503 phase_start.elapsed(),
5504 );
5505 let phase_start = Instant::now();
5506 process.kernel_handle.finish(exit_code);
5507 record_execute_phase("process_exit_cleanup_kernel_finish", phase_start.elapsed());
5508 let phase_start = Instant::now();
5509 let _ = vm.kernel.wait_and_reap(process.kernel_pid);
5510 record_execute_phase("process_exit_cleanup_wait_and_reap", phase_start.elapsed());
5511 let phase_start = Instant::now();
5512 vm.signal_states.remove(process_id);
5513 record_execute_phase(
5514 "process_exit_cleanup_signal_state_remove",
5515 phase_start.elapsed(),
5516 );
5517 let phase_start = Instant::now();
5518 for (detached_process_id, detached_child) in detached_children {
5519 vm.detached_child_processes
5520 .insert(detached_process_id.clone());
5521 vm.active_processes
5522 .insert(detached_process_id, detached_child);
5523 }
5524 record_execute_phase(
5525 "process_exit_cleanup_reinsert_detached",
5526 phase_start.elapsed(),
5527 );
5528 let phase_start = Instant::now();
5529 let became_idle = vm.active_processes.is_empty();
5530 record_execute_phase("process_exit_cleanup_became_idle", phase_start.elapsed());
5531 let phase_start = Instant::now();
5532 self.prune_extension_process_resource(process_id);
5533 record_execute_phase("process_exit_cleanup_prune_resource", phase_start.elapsed());
5534
5535 Ok(Some(became_idle))
5536 }
5537
5538 pub(crate) fn drain_process_events_blocking_with_limit(
5539 &mut self,
5540 vm_id: &str,
5541 process_id: &str,
5542 max_events: usize,
5543 ) -> Result<Vec<ActiveExecutionEvent>, SidecarError> {
5544 let mut events = Vec::new();
5545 if max_events == 0 {
5546 return Ok(events);
5547 }
5548 let mut deadline = Instant::now() + PROCESS_EXIT_DRAIN_INITIAL_QUIET;
5549
5550 loop {
5551 if events.len() >= max_events {
5552 break;
5553 }
5554 let event = {
5555 let Some(vm) = self.vms.get_mut(vm_id) else {
5556 break;
5557 };
5558 let Some(process) = vm.active_processes.get_mut(process_id) else {
5559 break;
5560 };
5561 if let Some(event) = process.pending_execution_events.pop_front() {
5562 Some(event)
5563 } else {
5564 match process.execution.poll_event_blocking(Duration::ZERO) {
5565 Ok(event) => event,
5566 Err(SidecarError::Execution(_)) => None,
5567 Err(other) => return Err(other),
5568 }
5569 }
5570 };
5571
5572 let Some(event) = event else {
5573 if Instant::now() >= deadline {
5574 break;
5575 }
5576 let blocking_wait = deadline.saturating_duration_since(Instant::now());
5577 if blocking_wait.is_zero() {
5578 break;
5579 }
5580 if events.len() >= max_events {
5581 break;
5582 }
5583 let delayed_event = {
5584 let Some(vm) = self.vms.get_mut(vm_id) else {
5585 break;
5586 };
5587 let Some(process) = vm.active_processes.get_mut(process_id) else {
5588 break;
5589 };
5590 if let Some(event) = process.pending_execution_events.pop_front() {
5591 Some(event)
5592 } else {
5593 match process.execution.poll_event_blocking(blocking_wait) {
5594 Ok(event) => event,
5595 Err(SidecarError::Execution(_)) => None,
5596 Err(other) => return Err(other),
5597 }
5598 }
5599 };
5600 let Some(event) = delayed_event else {
5601 break;
5602 };
5603 events.push(event);
5604 deadline = Instant::now() + PROCESS_EXIT_DRAIN_TRAILING_QUIET;
5605 continue;
5606 };
5607 events.push(event);
5608 deadline = Instant::now() + PROCESS_EXIT_DRAIN_TRAILING_QUIET;
5609 }
5610
5611 Ok(events)
5612 }
5613
5614 pub(crate) fn handle_python_vfs_rpc_request(
5615 &mut self,
5616 vm_id: &str,
5617 process_id: &str,
5618 request: PythonVfsRpcRequest,
5619 ) -> Result<(), SidecarError> {
5620 match request.method {
5621 PythonVfsRpcMethod::Read
5622 | PythonVfsRpcMethod::Write
5623 | PythonVfsRpcMethod::Stat
5624 | PythonVfsRpcMethod::Lstat
5625 | PythonVfsRpcMethod::ReadDir
5626 | PythonVfsRpcMethod::Mkdir
5627 | PythonVfsRpcMethod::Unlink
5628 | PythonVfsRpcMethod::Rmdir
5629 | PythonVfsRpcMethod::Rename
5630 | PythonVfsRpcMethod::Symlink
5631 | PythonVfsRpcMethod::ReadLink
5632 | PythonVfsRpcMethod::Setattr => {
5633 filesystem_handle_python_vfs_rpc_request(self, vm_id, process_id, request)
5634 }
5635 PythonVfsRpcMethod::HttpRequest => {
5636 self.handle_python_http_rpc_request(vm_id, process_id, request)
5637 }
5638 PythonVfsRpcMethod::DnsLookup => {
5639 self.handle_python_dns_rpc_request(vm_id, process_id, request)
5640 }
5641 PythonVfsRpcMethod::SubprocessRun => {
5642 self.handle_python_subprocess_rpc_request(vm_id, process_id, request)
5643 }
5644 PythonVfsRpcMethod::SocketConnect
5645 | PythonVfsRpcMethod::SocketSend
5646 | PythonVfsRpcMethod::SocketRecv
5647 | PythonVfsRpcMethod::SocketClose
5648 | PythonVfsRpcMethod::UdpCreate
5649 | PythonVfsRpcMethod::UdpSendto
5650 | PythonVfsRpcMethod::UdpRecvfrom => {
5651 self.handle_python_socket_rpc_request(vm_id, process_id, request)
5652 }
5653 }
5654 }
5655
5656 fn handle_python_http_rpc_request(
5657 &mut self,
5658 vm_id: &str,
5659 process_id: &str,
5660 request: PythonVfsRpcRequest,
5661 ) -> Result<(), SidecarError> {
5662 let Some(vm) = self.vms.get(vm_id) else {
5663 return Ok(());
5664 };
5665 if !vm.active_processes.contains_key(process_id) {
5666 return Ok(());
5667 }
5668 let response = (|| {
5669 let url_text = request.url.as_deref().ok_or_else(|| {
5670 SidecarError::InvalidState(String::from("python httpRequest requires a url"))
5671 })?;
5672 let url = Url::parse(url_text)
5673 .map_err(|error| SidecarError::Execution(format!("ERR_INVALID_URL: {error}")))?;
5674 let host = url.host_str().ok_or_else(|| {
5675 SidecarError::Execution(String::from("ERR_INVALID_URL: missing host"))
5676 })?;
5677 let port = url.port_or_known_default().ok_or_else(|| {
5678 SidecarError::Execution(String::from("ERR_INVALID_URL: missing port"))
5679 })?;
5680 self.bridge.require_network_access(
5681 vm_id,
5682 NetworkOperation::Http,
5683 format_tcp_resource(host, port),
5684 )?;
5685 let pinned_addresses = if let Ok(literal_ip) = host.parse::<IpAddr>() {
5692 filter_dns_safe_ip_addrs(vec![literal_ip], host)?
5693 } else {
5694 filter_dns_safe_ip_addrs(
5695 resolve_dns_ip_addrs(
5696 &self.bridge,
5697 &vm.kernel,
5698 vm_id,
5699 &vm.dns,
5700 host,
5701 DnsLookupPolicy::SkipPermissions,
5702 )?,
5703 host,
5704 )?
5705 };
5706 let mut headers = BTreeMap::new();
5707 for (name, value) in &request.headers {
5708 headers.insert(name.clone(), Value::String(value.clone()));
5709 }
5710 let options = JavascriptHttpRequestOptions {
5711 method: Some(
5712 request
5713 .http_method
5714 .clone()
5715 .unwrap_or_else(|| String::from("GET")),
5716 ),
5717 headers,
5718 body: request.body_base64.as_deref().map(|body| {
5719 String::from_utf8(
5720 base64::engine::general_purpose::STANDARD
5721 .decode(body)
5722 .unwrap_or_default(),
5723 )
5724 .unwrap_or_default()
5725 }),
5726 reject_unauthorized: None,
5727 };
5728 let headers =
5729 parse_http_header_collection(&options.headers, "python httpRequest headers")?;
5730 let response =
5731 issue_outbound_http_request(&url, &options, &headers, &pinned_addresses)?;
5732 let payload_json = response.as_str().ok_or_else(|| {
5733 SidecarError::Execution(String::from(
5734 "python httpRequest returned a non-string response payload",
5735 ))
5736 })?;
5737 let payload: Value = serde_json::from_str(payload_json).map_err(|error| {
5738 SidecarError::Execution(format!(
5739 "python httpRequest response must be valid JSON: {error}"
5740 ))
5741 })?;
5742 let header_map = payload
5743 .get("headers")
5744 .and_then(Value::as_array)
5745 .map(|entries| {
5746 let mut normalized = BTreeMap::<String, Vec<String>>::new();
5747 for entry in entries {
5748 let Some(pair) = entry.as_array() else {
5749 continue;
5750 };
5751 let Some(name) = pair.first().and_then(Value::as_str) else {
5752 continue;
5753 };
5754 let Some(value) = pair.get(1).and_then(Value::as_str) else {
5755 continue;
5756 };
5757 normalized
5758 .entry(name.to_owned())
5759 .or_default()
5760 .push(value.to_owned());
5761 }
5762 normalized
5763 })
5764 .unwrap_or_default();
5765 Ok(PythonVfsRpcResponsePayload::Http {
5766 status: payload
5767 .get("status")
5768 .and_then(Value::as_u64)
5769 .map(|value| value as u16)
5770 .unwrap_or_default(),
5771 reason: payload
5772 .get("statusText")
5773 .and_then(Value::as_str)
5774 .unwrap_or_default()
5775 .to_owned(),
5776 url: payload
5777 .get("url")
5778 .and_then(Value::as_str)
5779 .unwrap_or(url_text)
5780 .to_owned(),
5781 headers: header_map,
5782 body_base64: payload
5783 .get("body")
5784 .and_then(Value::as_str)
5785 .unwrap_or_default()
5786 .to_owned(),
5787 })
5788 })();
5789
5790 self.respond_python_rpc(vm_id, process_id, request.id, response)
5791 }
5792
5793 fn handle_python_dns_rpc_request(
5794 &mut self,
5795 vm_id: &str,
5796 process_id: &str,
5797 request: PythonVfsRpcRequest,
5798 ) -> Result<(), SidecarError> {
5799 let Some(vm) = self.vms.get(vm_id) else {
5800 return Ok(());
5801 };
5802 if !vm.active_processes.contains_key(process_id) {
5803 return Ok(());
5804 }
5805 let response = (|| {
5806 let hostname = request.hostname.as_deref().ok_or_else(|| {
5807 SidecarError::InvalidState(String::from("python dnsLookup requires a hostname"))
5808 })?;
5809 let mut addresses = filter_dns_safe_ip_addrs(
5810 resolve_dns_ip_addrs(
5811 &self.bridge,
5812 &vm.kernel,
5813 vm_id,
5814 &vm.dns,
5815 hostname,
5816 DnsLookupPolicy::CheckPermissions,
5817 )?,
5818 hostname,
5819 )?;
5820 if let Some(family) = request.family {
5821 addresses.retain(|address| {
5822 matches!((family, address), (4, IpAddr::V4(_)) | (6, IpAddr::V6(_)))
5823 });
5824 }
5825 Ok(PythonVfsRpcResponsePayload::DnsLookup {
5826 addresses: addresses
5827 .into_iter()
5828 .map(|address| address.to_string())
5829 .collect(),
5830 })
5831 })();
5832
5833 self.respond_python_rpc(vm_id, process_id, request.id, response)
5834 }
5835
5836 fn handle_python_subprocess_rpc_request(
5837 &mut self,
5838 vm_id: &str,
5839 process_id: &str,
5840 request: PythonVfsRpcRequest,
5841 ) -> Result<(), SidecarError> {
5842 let command = request.command.clone().ok_or_else(|| {
5843 SidecarError::InvalidState(String::from("python subprocessRun requires a command"))
5844 })?;
5845 let (internal_bootstrap_env, cwd) = {
5846 let Some(vm) = self.vms.get(vm_id) else {
5847 return Ok(());
5848 };
5849 let Some(process) = vm.active_processes.get(process_id) else {
5850 return Ok(());
5851 };
5852 let virtual_home = guest_virtual_home(vm);
5853 let cwd = request.cwd.clone().or_else(|| {
5854 guest_runtime_path_for_host_path(
5855 &vm.guest_env,
5856 &virtual_home,
5857 &vm.host_cwd,
5858 &process.host_cwd.to_string_lossy(),
5859 )
5860 });
5861 (
5862 sanitize_javascript_child_process_internal_bootstrap_env(&vm.guest_env),
5863 cwd,
5864 )
5865 };
5866 let response = self
5867 .spawn_javascript_child_process_sync(
5868 vm_id,
5869 process_id,
5870 JavascriptChildProcessSpawnRequest {
5871 command,
5872 args: request.args.clone(),
5873 options: JavascriptChildProcessSpawnOptions {
5874 cwd,
5875 env: request.env.clone(),
5876 input: None,
5877 internal_bootstrap_env,
5878 shell: request.shell,
5879 detached: false,
5880 stdio: vec![
5881 String::from("pipe"),
5882 String::from("pipe"),
5883 String::from("pipe"),
5884 ],
5885 timeout: None,
5886 kill_signal: None,
5887 },
5888 },
5889 request.max_buffer,
5890 )
5891 .map(|payload| PythonVfsRpcResponsePayload::SubprocessRun {
5892 exit_code: payload
5893 .get("code")
5894 .and_then(Value::as_i64)
5895 .map(|value| value as i32)
5896 .unwrap_or(1),
5897 stdout: payload
5898 .get("stdout")
5899 .and_then(Value::as_str)
5900 .unwrap_or_default()
5901 .to_owned(),
5902 stderr: payload
5903 .get("stderr")
5904 .and_then(Value::as_str)
5905 .unwrap_or_default()
5906 .to_owned(),
5907 max_buffer_exceeded: payload
5908 .get("maxBufferExceeded")
5909 .and_then(Value::as_bool)
5910 .unwrap_or(false),
5911 });
5912
5913 self.respond_python_rpc(vm_id, process_id, request.id, response)
5914 }
5915
5916 fn handle_python_socket_rpc_request(
5917 &mut self,
5918 vm_id: &str,
5919 process_id: &str,
5920 request: PythonVfsRpcRequest,
5921 ) -> Result<(), SidecarError> {
5922 if !self.vms.contains_key(vm_id) {
5923 return Ok(());
5924 }
5925 let response = self.python_socket_op(vm_id, process_id, &request);
5926 self.respond_python_rpc(vm_id, process_id, request.id, response)
5927 }
5928
5929 fn python_socket_op(
5930 &mut self,
5931 vm_id: &str,
5932 process_id: &str,
5933 request: &PythonVfsRpcRequest,
5934 ) -> Result<PythonVfsRpcResponsePayload, SidecarError> {
5935 match request.method {
5936 PythonVfsRpcMethod::SocketConnect => {
5937 let host = python_socket_host(request)?;
5938 let port = python_socket_port(request)?;
5939 self.check_python_socket_limit(vm_id)?;
5940 self.bridge.require_network_access(
5941 vm_id,
5942 NetworkOperation::Http,
5943 format_tcp_resource(&host, port),
5944 )?;
5945 let pinned = self.python_socket_pinned_addrs(vm_id, &host, port)?;
5946 let stream = python_connect_tcp(&pinned, port)?;
5947 stream
5948 .set_read_timeout(Some(PYTHON_SOCKET_READ_POLL))
5949 .map_err(python_socket_io_error)?;
5950 stream
5953 .set_write_timeout(Some(PYTHON_SOCKET_WRITE_TIMEOUT))
5954 .map_err(python_socket_io_error)?;
5955 let socket_id =
5956 self.store_python_socket(vm_id, process_id, PythonHostSocket::Tcp(stream))?;
5957 Ok(PythonVfsRpcResponsePayload::SocketCreated { socket_id })
5958 }
5959 PythonVfsRpcMethod::SocketSend => {
5960 let data = python_socket_payload(request)?;
5961 let socket = self.python_socket_mut(vm_id, process_id, request)?;
5962 let PythonHostSocket::Tcp(stream) = socket else {
5963 return Err(python_socket_kind_error("send", "TCP"));
5964 };
5965 stream.write_all(&data).map_err(python_socket_io_error)?;
5966 Ok(PythonVfsRpcResponsePayload::SocketSent {
5967 bytes_sent: data.len(),
5968 })
5969 }
5970 PythonVfsRpcMethod::SocketRecv => {
5971 let max = python_socket_recv_len(request);
5972 let socket = self.python_socket_mut(vm_id, process_id, request)?;
5973 let PythonHostSocket::Tcp(stream) = socket else {
5974 return Err(python_socket_kind_error("recv", "TCP"));
5975 };
5976 let mut buf = vec![0u8; max];
5977 match stream.read(&mut buf) {
5978 Ok(0) => Ok(PythonVfsRpcResponsePayload::SocketReceived {
5979 data_base64: String::new(),
5980 closed: true,
5981 timed_out: false,
5982 }),
5983 Ok(n) => Ok(PythonVfsRpcResponsePayload::SocketReceived {
5984 data_base64: base64::engine::general_purpose::STANDARD.encode(&buf[..n]),
5985 closed: false,
5986 timed_out: false,
5987 }),
5988 Err(error) if python_socket_would_block(&error) => {
5989 Ok(PythonVfsRpcResponsePayload::SocketReceived {
5990 data_base64: String::new(),
5991 closed: false,
5992 timed_out: true,
5993 })
5994 }
5995 Err(error) => Err(python_socket_io_error(error)),
5996 }
5997 }
5998 PythonVfsRpcMethod::SocketClose => {
5999 self.remove_python_socket(vm_id, process_id, request);
6000 Ok(PythonVfsRpcResponsePayload::Empty)
6001 }
6002 PythonVfsRpcMethod::UdpCreate => {
6003 self.check_python_socket_limit(vm_id)?;
6004 let socket = UdpSocket::bind("0.0.0.0:0").map_err(python_socket_io_error)?;
6005 socket
6006 .set_read_timeout(Some(PYTHON_SOCKET_READ_POLL))
6007 .map_err(python_socket_io_error)?;
6008 let socket_id =
6009 self.store_python_socket(vm_id, process_id, PythonHostSocket::Udp(socket))?;
6010 Ok(PythonVfsRpcResponsePayload::SocketCreated { socket_id })
6011 }
6012 PythonVfsRpcMethod::UdpSendto => {
6013 let host = python_socket_host(request)?;
6014 let port = python_socket_port(request)?;
6015 let data = python_socket_payload(request)?;
6016 self.bridge.require_network_access(
6017 vm_id,
6018 NetworkOperation::Http,
6019 format_tcp_resource(&host, port),
6020 )?;
6021 let pinned = self.python_socket_pinned_addrs(vm_id, &host, port)?;
6022 let target = pinned
6023 .first()
6024 .map(|ip| SocketAddr::new(*ip, port))
6025 .ok_or_else(|| {
6026 SidecarError::Execution(format!("EAI_NONAME: cannot resolve {host}"))
6027 })?;
6028 let socket = self.python_socket_mut(vm_id, process_id, request)?;
6029 let PythonHostSocket::Udp(udp) = socket else {
6030 return Err(python_socket_kind_error("sendto", "UDP"));
6031 };
6032 let sent = udp.send_to(&data, target).map_err(python_socket_io_error)?;
6033 Ok(PythonVfsRpcResponsePayload::SocketSent { bytes_sent: sent })
6034 }
6035 PythonVfsRpcMethod::UdpRecvfrom => {
6036 let max = python_socket_recv_len(request);
6037 let socket = self.python_socket_mut(vm_id, process_id, request)?;
6038 let PythonHostSocket::Udp(udp) = socket else {
6039 return Err(python_socket_kind_error("recvfrom", "UDP"));
6040 };
6041 let mut buf = vec![0u8; max];
6042 match udp.recv_from(&mut buf) {
6043 Ok((n, addr)) => Ok(PythonVfsRpcResponsePayload::UdpReceived {
6044 data_base64: base64::engine::general_purpose::STANDARD.encode(&buf[..n]),
6045 host: addr.ip().to_string(),
6046 port: addr.port(),
6047 timed_out: false,
6048 }),
6049 Err(error) if python_socket_would_block(&error) => {
6050 Ok(PythonVfsRpcResponsePayload::UdpReceived {
6051 data_base64: String::new(),
6052 host: String::new(),
6053 port: 0,
6054 timed_out: true,
6055 })
6056 }
6057 Err(error) => Err(python_socket_io_error(error)),
6058 }
6059 }
6060 _ => Err(SidecarError::InvalidState(String::from(
6061 "non-socket python RPC reached the socket dispatcher unexpectedly",
6062 ))),
6063 }
6064 }
6065
6066 fn python_socket_pinned_addrs(
6071 &self,
6072 vm_id: &str,
6073 host: &str,
6074 port: u16,
6075 ) -> Result<Vec<IpAddr>, SidecarError> {
6076 let Some(vm) = self.vms.get(vm_id) else {
6077 return Err(SidecarError::InvalidState(String::from(
6078 "python socket op for unknown vm",
6079 )));
6080 };
6081 let context = build_javascript_socket_path_context(vm)?;
6082 if let Ok(literal_ip) = host.parse::<IpAddr>() {
6083 filter_tcp_connect_ip_addrs(vec![literal_ip], host, port, &context)
6084 } else {
6085 filter_tcp_connect_ip_addrs(
6086 resolve_dns_ip_addrs(
6087 &self.bridge,
6088 &vm.kernel,
6089 vm_id,
6090 &vm.dns,
6091 host,
6092 DnsLookupPolicy::SkipPermissions,
6093 )?,
6094 host,
6095 port,
6096 &context,
6097 )
6098 }
6099 }
6100
6101 fn check_python_socket_limit(&self, vm_id: &str) -> Result<(), SidecarError> {
6105 let Some(vm) = self.vms.get(vm_id) else {
6106 return Ok(());
6107 };
6108 let limit = vm.kernel.resource_limits().max_sockets;
6109 let current = vm_network_resource_counts(vm).sockets;
6110 check_network_resource_limit(limit, current, 1, "socket")
6111 }
6112
6113 fn store_python_socket(
6114 &mut self,
6115 vm_id: &str,
6116 process_id: &str,
6117 socket: PythonHostSocket,
6118 ) -> Result<u64, SidecarError> {
6119 let process = self
6120 .vms
6121 .get_mut(vm_id)
6122 .and_then(|vm| vm.active_processes.get_mut(process_id))
6123 .ok_or_else(|| {
6124 SidecarError::InvalidState(String::from("python socket op for reaped vm/process"))
6125 })?;
6126 let socket_id = process.next_python_socket_id;
6127 process.next_python_socket_id = process.next_python_socket_id.wrapping_add(1);
6128 process.python_sockets.insert(socket_id, socket);
6129 Ok(socket_id)
6130 }
6131
6132 fn python_socket_mut(
6133 &mut self,
6134 vm_id: &str,
6135 process_id: &str,
6136 request: &PythonVfsRpcRequest,
6137 ) -> Result<&mut PythonHostSocket, SidecarError> {
6138 let socket_id = request.socket_id.ok_or_else(|| {
6139 SidecarError::InvalidState(String::from("python socket op requires socketId"))
6140 })?;
6141 self.vms
6142 .get_mut(vm_id)
6143 .and_then(|vm| vm.active_processes.get_mut(process_id))
6144 .and_then(|process| process.python_sockets.get_mut(&socket_id))
6145 .ok_or_else(|| {
6146 SidecarError::Execution(format!("EBADF: unknown python socket {socket_id}"))
6147 })
6148 }
6149
6150 fn remove_python_socket(
6151 &mut self,
6152 vm_id: &str,
6153 process_id: &str,
6154 request: &PythonVfsRpcRequest,
6155 ) {
6156 let Some(socket_id) = request.socket_id else {
6157 return;
6158 };
6159 if let Some(process) = self
6160 .vms
6161 .get_mut(vm_id)
6162 .and_then(|vm| vm.active_processes.get_mut(process_id))
6163 {
6164 process.python_sockets.remove(&socket_id);
6165 }
6166 }
6167
6168 fn respond_python_rpc(
6169 &mut self,
6170 vm_id: &str,
6171 process_id: &str,
6172 request_id: u64,
6173 response: Result<PythonVfsRpcResponsePayload, SidecarError>,
6174 ) -> Result<(), SidecarError> {
6175 let Some(vm) = self.vms.get_mut(vm_id) else {
6176 return Ok(());
6177 };
6178 let Some(process) = vm.active_processes.get_mut(process_id) else {
6179 return Ok(());
6180 };
6181 let result = match response {
6182 Ok(payload) => process
6183 .execution
6184 .respond_python_vfs_rpc_success(request_id, payload),
6185 Err(error) => process.execution.respond_python_vfs_rpc_error(
6186 request_id,
6187 "ERR_AGENTOS_PYTHON_VFS_RPC",
6188 error.to_string(),
6189 ),
6190 };
6191 match result {
6192 Ok(()) => Ok(()),
6193 Err(error) if is_broken_pipe_error(&error) => Ok(()),
6194 Err(error) => Err(error),
6195 }
6196 }
6197
6198 pub(crate) fn resolve_javascript_child_process_execution(
6199 &self,
6200 vm: &VmState,
6201 parent_env: &BTreeMap<String, String>,
6202 parent_guest_cwd: &str,
6203 parent_host_cwd: &Path,
6204 request: &JavascriptChildProcessSpawnRequest,
6205 ) -> Result<ResolvedChildProcessExecution, SidecarError> {
6206 let mut runtime_env = parent_env.clone();
6207 runtime_env.extend(request.options.internal_bootstrap_env.clone());
6208 let (guest_cwd, host_cwd_override) = request
6209 .options
6210 .cwd
6211 .as_deref()
6212 .map(|cwd| {
6213 let normalized_parent_host_cwd = normalize_host_path(parent_host_cwd);
6214 let requested_host_cwd = normalize_host_path(Path::new(cwd));
6215 if path_is_within_root(&requested_host_cwd, &normalized_parent_host_cwd) {
6216 let relative = requested_host_cwd
6217 .strip_prefix(&normalized_parent_host_cwd)
6218 .unwrap_or_else(|_| Path::new(""));
6219 let relative = relative.to_string_lossy().replace('\\', "/");
6220 let guest_cwd = if relative.is_empty() {
6221 parent_guest_cwd.to_owned()
6222 } else {
6223 normalize_path(&format!("{parent_guest_cwd}/{relative}"))
6224 };
6225 (guest_cwd, Some(requested_host_cwd))
6226 } else if Path::new(cwd).is_relative() {
6227 (
6228 normalize_path(&format!("{parent_guest_cwd}/{cwd}")),
6229 Some(normalize_host_path(&parent_host_cwd.join(cwd))),
6230 )
6231 } else {
6232 (normalize_path(cwd), None)
6233 }
6234 })
6235 .unwrap_or_else(|| (parent_guest_cwd.to_owned(), None));
6236 let inherited_host_cwd = (host_cwd_override.is_none() && guest_cwd == parent_guest_cwd)
6237 .then(|| normalize_host_path(parent_host_cwd));
6238 let host_cwd = host_cwd_override
6239 .or(inherited_host_cwd)
6240 .or_else(|| {
6241 host_runtime_path_for_guest_path_with_env(
6242 vm,
6243 &runtime_env,
6244 &guest_cwd,
6245 parent_host_cwd,
6246 )
6247 })
6248 .unwrap_or_else(|| {
6249 let candidate = PathBuf::from(&guest_cwd);
6250 if guest_cwd == parent_guest_cwd {
6251 normalize_host_path(parent_host_cwd)
6252 } else if candidate.is_absolute() {
6253 shadow_path_for_guest(vm, &guest_cwd)
6254 } else {
6255 vm.host_cwd.clone()
6256 }
6257 });
6258 let mut env = parent_env.clone();
6259 env.extend(request.options.env.clone());
6260 env.remove("AGENTOS_GUEST_ENTRYPOINT");
6263 env.remove("AGENTOS_NODE_EVAL");
6264
6265 let (command, process_args) = if request.options.shell {
6266 let tokens = tokenize_shell_free_command(&request.command);
6267 let requires_shell = command_requires_shell(&request.command)
6268 || tokens.first().is_some_and(|command| {
6269 is_posix_shell_builtin(command) || shell_first_token_requires_shell(command)
6270 });
6271 if requires_shell {
6272 if !vm.command_guest_paths.contains_key("sh") {
6273 return Err(SidecarError::InvalidState(format!(
6274 "shell-mode child_process command requires /bin/sh, which is not \
6275 installed in this VM (install a software package that provides sh, \
6276 for example @secure-exec/coreutils): {}",
6277 request.command
6278 )));
6279 }
6280 (
6281 String::from("sh"),
6282 vec![String::from("-c"), request.command.clone()],
6283 )
6284 } else {
6285 let Some((command, args)) = tokens.split_first() else {
6286 return Err(SidecarError::InvalidState(String::from(
6287 "child_process shell command must not be empty",
6288 )));
6289 };
6290 (command.clone(), args.to_vec())
6291 }
6292 } else {
6293 (request.command.clone(), request.args.clone())
6294 };
6295 let process_args = apply_shell_cwd_prefix(&command, process_args, &guest_cwd);
6296 if is_tool_command(vm, &command) {
6297 let command = normalized_tool_command_name(&command).unwrap_or(command);
6298 return Ok(ResolvedChildProcessExecution {
6299 command: command.clone(),
6300 process_args: std::iter::once(command.clone())
6301 .chain(process_args.iter().cloned())
6302 .collect(),
6303 runtime: GuestRuntimeKind::JavaScript,
6304 entrypoint: command,
6305 execution_args: process_args,
6306 env,
6307 guest_cwd,
6308 host_cwd,
6309 wasm_permission_tier: None,
6310 tool_command: true,
6311 });
6312 }
6313
6314 if is_path_like_specifier(&command)
6315 && matches!(
6316 Path::new(&command).extension().and_then(|ext| ext.to_str()),
6317 Some("js" | "mjs" | "cjs" | "ts" | "mts" | "cts")
6318 )
6319 {
6320 let guest_entrypoint = if command.starts_with('/') {
6321 normalize_path(&command)
6322 } else if command.starts_with("file:") {
6323 normalize_path(command.trim_start_matches("file:"))
6324 } else {
6325 normalize_path(&format!("{guest_cwd}/{command}"))
6326 };
6327 let host_entrypoint = if command.starts_with("./") || command.starts_with("../") {
6328 normalize_host_path(&host_cwd.join(&command))
6329 } else {
6330 host_runtime_path_for_guest_path_with_env(
6331 vm,
6332 &runtime_env,
6333 &guest_entrypoint,
6334 parent_host_cwd,
6335 )
6336 .unwrap_or_else(|| {
6337 let candidate = PathBuf::from(&guest_entrypoint);
6338 if candidate.is_absolute() {
6339 candidate
6340 } else {
6341 host_cwd.join(&guest_entrypoint)
6342 }
6343 })
6344 };
6345 env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
6346 let guest_entrypoint = env.get("AGENTOS_GUEST_ENTRYPOINT").cloned();
6347 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
6348
6349 return Ok(ResolvedChildProcessExecution {
6350 command: command.clone(),
6351 process_args: std::iter::once(command)
6352 .chain(process_args.iter().cloned())
6353 .collect(),
6354 runtime: GuestRuntimeKind::JavaScript,
6355 entrypoint: host_entrypoint.to_string_lossy().into_owned(),
6356 execution_args: process_args,
6357 env,
6358 guest_cwd,
6359 host_cwd,
6360 wasm_permission_tier: None,
6361 tool_command: false,
6362 });
6363 }
6364
6365 if is_node_runtime_command(&command) {
6366 if let Some(cli) = resolve_host_node_cli_entrypoint(&command) {
6367 env.insert(
6368 String::from("AGENTOS_NODE_EVAL"),
6369 build_host_node_cli_eval(&cli),
6370 );
6371 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
6372 add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
6373 add_runtime_host_access_path(
6374 &mut env,
6375 "AGENTOS_EXTRA_FS_READ_PATHS",
6376 &cli.package_root,
6377 true,
6378 );
6379
6380 return Ok(ResolvedChildProcessExecution {
6381 command: command.clone(),
6382 process_args: std::iter::once(command.clone())
6383 .chain(process_args.iter().cloned())
6384 .collect(),
6385 runtime: GuestRuntimeKind::JavaScript,
6386 entrypoint: String::from("-e"),
6387 execution_args: std::iter::once(cli.guest_entrypoint.clone())
6388 .chain(process_args.iter().cloned())
6389 .collect(),
6390 env,
6391 guest_cwd,
6392 host_cwd,
6393 wasm_permission_tier: None,
6394 tool_command: false,
6395 });
6396 }
6397
6398 if process_args.is_empty() {
6399 env.insert(String::from("AGENTOS_NODE_EVAL"), String::new());
6400 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
6401
6402 return Ok(ResolvedChildProcessExecution {
6403 command: command.clone(),
6404 process_args: vec![command.clone()],
6405 runtime: GuestRuntimeKind::JavaScript,
6406 entrypoint: String::from("-e"),
6407 execution_args: Vec::new(),
6408 env,
6409 guest_cwd,
6410 host_cwd,
6411 wasm_permission_tier: None,
6412 tool_command: false,
6413 });
6414 }
6415
6416 if let Some((entrypoint, execution_args)) =
6417 resolve_special_node_cli_invocation(&process_args, &mut env)
6418 {
6419 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
6420
6421 return Ok(ResolvedChildProcessExecution {
6422 command: command.clone(),
6423 process_args: std::iter::once(command.clone())
6424 .chain(process_args.iter().cloned())
6425 .collect(),
6426 runtime: GuestRuntimeKind::JavaScript,
6427 entrypoint,
6428 execution_args,
6429 env,
6430 guest_cwd,
6431 host_cwd,
6432 wasm_permission_tier: None,
6433 tool_command: false,
6434 });
6435 }
6436
6437 let Some(entrypoint_specifier) = process_args.first() else {
6438 return Err(SidecarError::InvalidState(format!(
6439 "{command} child_process spawn requires an entrypoint"
6440 )));
6441 };
6442
6443 let (entrypoint, execution_args) = if is_path_like_specifier(entrypoint_specifier) {
6444 let guest_entrypoint = if entrypoint_specifier.starts_with('/') {
6445 normalize_path(entrypoint_specifier)
6446 } else if entrypoint_specifier.starts_with("file:") {
6447 normalize_path(entrypoint_specifier.trim_start_matches("file:"))
6448 } else {
6449 normalize_path(&format!("{guest_cwd}/{entrypoint_specifier}"))
6450 };
6451 let host_entrypoint = if entrypoint_specifier.starts_with("./")
6452 || entrypoint_specifier.starts_with("../")
6453 {
6454 normalize_host_path(&host_cwd.join(entrypoint_specifier))
6455 } else {
6456 host_runtime_path_for_guest_path_with_env(
6457 vm,
6458 &runtime_env,
6459 &guest_entrypoint,
6460 parent_host_cwd,
6461 )
6462 .unwrap_or_else(|| {
6463 let candidate = PathBuf::from(&guest_entrypoint);
6464 if candidate.is_absolute() {
6465 candidate
6466 } else {
6467 host_cwd.join(&guest_entrypoint)
6468 }
6469 })
6470 };
6471 env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
6472 (
6473 host_entrypoint.to_string_lossy().into_owned(),
6474 process_args.iter().skip(1).cloned().collect(),
6475 )
6476 } else {
6477 (
6478 entrypoint_specifier.clone(),
6479 process_args.iter().skip(1).cloned().collect(),
6480 )
6481 };
6482 let guest_entrypoint = env.get("AGENTOS_GUEST_ENTRYPOINT").cloned();
6483 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
6484
6485 return Ok(ResolvedChildProcessExecution {
6486 command: command.clone(),
6487 process_args: std::iter::once(command)
6488 .chain(process_args.iter().cloned())
6489 .collect(),
6490 runtime: GuestRuntimeKind::JavaScript,
6491 entrypoint,
6492 execution_args,
6493 env,
6494 guest_cwd,
6495 host_cwd,
6496 wasm_permission_tier: None,
6497 tool_command: false,
6498 });
6499 }
6500
6501 if is_python_runtime_command(&command) {
6502 return resolve_python_command_execution(
6503 vm,
6504 &command,
6505 &process_args,
6506 env,
6507 guest_cwd,
6508 host_cwd,
6509 );
6510 }
6511
6512 let guest_entrypoint = resolve_guest_command_entrypoint(
6513 vm,
6514 &guest_cwd,
6515 &command,
6516 env.get("PATH").map(String::as_str),
6517 )
6518 .ok_or_else(|| SidecarError::InvalidState(format!("command not found: {command}")))?;
6519 let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
6520 let wasm_permission_tier = vm.command_permissions.get(&command).copied().or_else(|| {
6521 Path::new(&guest_entrypoint)
6522 .file_name()
6523 .and_then(|name| name.to_str())
6524 .and_then(|name| vm.command_permissions.get(name).copied())
6525 });
6526 if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
6527 resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
6528 {
6529 prepare_guest_runtime_env(
6530 vm,
6531 &mut env,
6532 &guest_cwd,
6533 &host_cwd,
6534 Some(javascript_guest_entrypoint),
6535 )?;
6536
6537 return Ok(ResolvedChildProcessExecution {
6538 command: command.clone(),
6539 process_args: std::iter::once(command)
6540 .chain(process_args.iter().cloned())
6541 .collect(),
6542 runtime: GuestRuntimeKind::JavaScript,
6543 entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
6544 execution_args: process_args,
6545 env,
6546 guest_cwd,
6547 host_cwd,
6548 wasm_permission_tier: None,
6549 tool_command: false,
6550 });
6551 }
6552 prepare_guest_runtime_env(
6553 vm,
6554 &mut env,
6555 &guest_cwd,
6556 &host_cwd,
6557 Some(guest_entrypoint.clone()),
6558 )?;
6559
6560 Ok(ResolvedChildProcessExecution {
6561 command: command.clone(),
6562 process_args: std::iter::once(command)
6563 .chain(process_args.iter().cloned())
6564 .collect(),
6565 runtime: GuestRuntimeKind::WebAssembly,
6566 entrypoint: host_entrypoint.to_string_lossy().into_owned(),
6567 execution_args: process_args,
6568 env,
6569 guest_cwd,
6570 host_cwd,
6571 wasm_permission_tier,
6572 tool_command: false,
6573 })
6574 }
6575
6576 pub(crate) fn spawn_javascript_child_process(
6577 &mut self,
6578 vm_id: &str,
6579 process_id: &str,
6580 request: JavascriptChildProcessSpawnRequest,
6581 ) -> Result<Value, SidecarError> {
6582 let total_start = Instant::now();
6583 let phase_start = Instant::now();
6584 let resolved = {
6585 let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
6586 let parent = vm
6587 .active_processes
6588 .get(process_id)
6589 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
6590 self.resolve_javascript_child_process_execution(
6591 vm,
6592 &parent.env,
6593 &parent.guest_cwd,
6594 &parent.host_cwd,
6595 &request,
6596 )?
6597 };
6598 record_execute_phase("child_process_resolve_execution", phase_start.elapsed());
6599 let (parent_kernel_pid, child_process_id) = {
6600 let vm = self
6601 .vms
6602 .get_mut(vm_id)
6603 .ok_or_else(|| missing_vm_error(vm_id))?;
6604 let process = vm
6605 .active_processes
6606 .get_mut(process_id)
6607 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
6608 (process.kernel_pid, process.allocate_child_process_id())
6609 };
6610 let sidecar_requests = self.sidecar_requests.clone();
6611 let vm = self
6612 .vms
6613 .get_mut(vm_id)
6614 .ok_or_else(|| missing_vm_error(vm_id))?;
6615 let phase_start = Instant::now();
6616 let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
6617 .tool_command
6618 {
6619 let tool_resolution = resolve_tool_command(
6620 vm,
6621 &resolved.command,
6622 &resolved.execution_args,
6623 Some(&resolved.guest_cwd),
6624 )?
6625 .ok_or_else(|| {
6626 SidecarError::InvalidState(format!(
6627 "tool command no longer resolves: {}",
6628 resolved.command
6629 ))
6630 })?;
6631 let kernel_handle = vm
6632 .kernel
6633 .create_virtual_process(
6634 EXECUTION_DRIVER_NAME,
6635 TOOL_DRIVER_NAME,
6636 &resolved.command,
6637 resolved.process_args.clone(),
6638 VirtualProcessOptions {
6639 parent_pid: Some(parent_kernel_pid),
6640 env: resolved.env.clone(),
6641 cwd: Some(resolved.guest_cwd.clone()),
6642 },
6643 )
6644 .map_err(kernel_error)?;
6645 let kernel_pid = kernel_handle.pid();
6646 let tool_execution = ToolExecution::default();
6647 let cancelled = tool_execution.cancelled.clone();
6648 let pending_events = tool_execution.pending_events.clone();
6649 let events_overflowed = tool_execution.events_overflowed.clone();
6650 spawn_tool_process_events(ToolProcessEventRequest {
6651 sidecar_requests: sidecar_requests.clone(),
6652 connection_id: vm.connection_id.clone(),
6653 session_id: vm.session_id.clone(),
6654 vm_id: vm_id.to_owned(),
6655 tool_resolution,
6656 cancelled,
6657 pending_events,
6658 events_overflowed,
6659 });
6660 (
6661 kernel_pid,
6662 kernel_handle,
6663 ActiveExecution::Tool(tool_execution),
6664 None,
6665 )
6666 } else {
6667 let kernel_command = match resolved.runtime {
6668 GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
6669 GuestRuntimeKind::WebAssembly => WASM_COMMAND,
6670 GuestRuntimeKind::Python => PYTHON_COMMAND,
6671 };
6672 let kernel_handle = vm
6673 .kernel
6674 .spawn_process(
6675 kernel_command,
6676 resolved.process_args.clone(),
6677 SpawnOptions {
6678 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
6679 parent_pid: Some(parent_kernel_pid),
6680 env: resolved.env.clone(),
6681 cwd: Some(resolved.guest_cwd.clone()),
6682 },
6683 )
6684 .map_err(kernel_error)?;
6685 let kernel_pid = kernel_handle.pid();
6686 if request.options.detached {
6687 vm.kernel
6688 .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
6689 .map_err(kernel_error)?;
6690 }
6691 let mut execution_env = resolved.env.clone();
6692 execution_env.insert(
6693 String::from(EXECUTION_SANDBOX_ROOT_ENV),
6694 normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
6695 );
6696
6697 let execution = match resolved.runtime {
6698 GuestRuntimeKind::JavaScript => {
6699 execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
6700 &request.options.internal_bootstrap_env,
6701 ));
6702 execution_env.insert(
6703 String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
6704 String::from("1"),
6705 );
6706 let context =
6707 self.javascript_engine
6708 .create_context(CreateJavascriptContextRequest {
6709 vm_id: vm_id.to_owned(),
6710 bootstrap_module: None,
6711 compile_cache_root: Some(
6712 self.cache_root.join("node-compile-cache"),
6713 ),
6714 });
6715 let inline_code = load_javascript_entrypoint_source(
6716 vm,
6717 &resolved.host_cwd,
6718 &resolved.entrypoint,
6719 &execution_env,
6720 );
6721 prepare_javascript_shadow(vm, &resolved)?;
6722
6723 let built_reader = build_module_reader(vm, &resolved);
6724 let guest_reader = built_reader.clone().map(|reader| {
6725 Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
6726 as Box<dyn GuestModuleReader>
6727 });
6728 let module_reader = built_reader
6729 .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
6730 let execution = self
6731 .javascript_engine
6732 .start_execution_with_module_reader(
6733 StartJavascriptExecutionRequest {
6734 guest_runtime: guest_runtime_identity(
6735 vm,
6736 Some(u64::from(kernel_pid)),
6737 Some(u64::from(parent_kernel_pid)),
6738 ),
6739 vm_id: vm_id.to_owned(),
6740 context_id: context.context_id,
6741 argv: std::iter::once(resolved.entrypoint.clone())
6742 .chain(resolved.execution_args.clone())
6743 .collect(),
6744 env: execution_env,
6745 cwd: resolved.host_cwd.clone(),
6746 limits: javascript_execution_limits(vm),
6747 inline_code,
6748 wasm_module_bytes: None,
6749 },
6750 module_reader,
6751 guest_reader,
6752 )
6753 .map_err(javascript_error)?;
6754 ActiveExecution::Javascript(execution)
6755 }
6756 GuestRuntimeKind::WebAssembly => {
6757 execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
6758 let wasm_limits = wasm_execution_limits(vm);
6759 let wasm_guest_runtime = guest_runtime_identity(
6760 vm,
6761 Some(u64::from(kernel_pid)),
6762 Some(u64::from(parent_kernel_pid)),
6763 );
6764 let context = self.wasm_engine.create_context(CreateWasmContextRequest {
6765 vm_id: vm_id.to_owned(),
6766 module_path: Some(resolved.entrypoint.clone()),
6767 });
6768 let execution = self
6769 .wasm_engine
6770 .start_execution(StartWasmExecutionRequest {
6771 vm_id: vm_id.to_owned(),
6772 context_id: context.context_id,
6773 argv: resolved.process_args.clone(),
6774 env: execution_env,
6775 cwd: resolved.host_cwd.clone(),
6776 permission_tier: execution_wasm_permission_tier(
6777 resolved
6778 .wasm_permission_tier
6779 .unwrap_or(WasmPermissionTier::Full),
6780 ),
6781 limits: wasm_limits,
6782 guest_runtime: wasm_guest_runtime,
6783 })
6784 .map_err(wasm_error)?;
6785 ActiveExecution::Wasm(Box::new(execution))
6786 }
6787 GuestRuntimeKind::Python => {
6788 let python_file_path = if execution_env.contains_key("AGENTOS_PYTHON_ARGV") {
6792 execution_env.get("AGENTOS_PYTHON_FILE").map(PathBuf::from)
6793 } else {
6794 python_file_entrypoint(&resolved.entrypoint)
6795 };
6796 let pyodide_dist_path = self
6797 .python_engine
6798 .bundled_pyodide_dist_path_for_vm(vm_id)
6799 .map_err(python_error)?;
6800 let pyodide_cache_path = pyodide_dist_path
6801 .parent()
6802 .and_then(Path::parent)
6803 .unwrap_or(pyodide_dist_path.as_path())
6804 .join("pyodide-package-cache");
6805 add_runtime_guest_path_mapping(
6806 &mut execution_env,
6807 PYTHON_PYODIDE_GUEST_ROOT,
6808 &pyodide_dist_path,
6809 );
6810 add_runtime_guest_path_mapping(
6811 &mut execution_env,
6812 PYTHON_PYODIDE_CACHE_GUEST_ROOT,
6813 &pyodide_cache_path,
6814 );
6815 add_runtime_host_access_path(
6816 &mut execution_env,
6817 "AGENTOS_EXTRA_FS_READ_PATHS",
6818 &pyodide_dist_path,
6819 true,
6820 );
6821 add_runtime_host_access_path(
6822 &mut execution_env,
6823 "AGENTOS_EXTRA_FS_READ_PATHS",
6824 &pyodide_cache_path,
6825 true,
6826 );
6827 add_runtime_host_access_path(
6828 &mut execution_env,
6829 "AGENTOS_EXTRA_FS_WRITE_PATHS",
6830 &pyodide_cache_path,
6831 false,
6832 );
6833 let context = self
6834 .python_engine
6835 .create_context(CreatePythonContextRequest {
6836 vm_id: vm_id.to_owned(),
6837 pyodide_dist_path,
6838 });
6839 let execution = self
6840 .python_engine
6841 .start_execution(StartPythonExecutionRequest {
6842 vm_id: vm_id.to_owned(),
6843 context_id: context.context_id,
6844 code: resolved.entrypoint.clone(),
6845 file_path: python_file_path,
6846 env: execution_env,
6847 cwd: resolved.host_cwd.clone(),
6848 limits: python_execution_limits(vm),
6849 guest_runtime: guest_runtime_identity(
6850 vm,
6851 Some(u64::from(kernel_pid)),
6852 Some(u64::from(parent_kernel_pid)),
6853 ),
6854 })
6855 .map_err(python_error)?;
6856 ActiveExecution::Python(execution)
6857 }
6858 };
6859 let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
6860 "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
6861 "ignore" => {
6862 vm.kernel
6863 .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
6864 .map_err(kernel_error)?;
6865 None
6866 }
6867 "inherit" => None,
6868 _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
6869 };
6870 (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
6871 };
6872 record_execute_phase(
6873 "child_process_spawn_and_start_execution",
6874 phase_start.elapsed(),
6875 );
6876
6877 let phase_start = Instant::now();
6878 let child_fd1_is_tty = vm
6883 .kernel
6884 .isatty(EXECUTION_DRIVER_NAME, kernel_pid, 1)
6885 .unwrap_or(false);
6886 let process = vm
6887 .active_processes
6888 .get_mut(process_id)
6889 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
6890 let inherited_tty_master_owner = if child_fd1_is_tty {
6891 process
6892 .tty_master_fd
6893 .map(|master_fd| (process.kernel_pid, master_fd))
6894 .or(process.tty_master_owner)
6895 } else {
6896 None
6897 };
6898 process.child_processes.insert(
6899 child_process_id.clone(),
6900 ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
6901 .with_detached(request.options.detached)
6902 .with_guest_cwd(resolved.guest_cwd.clone())
6903 .with_env(resolved.env.clone())
6904 .with_host_cwd(resolved.host_cwd.clone()),
6905 );
6906 {
6907 let child = process
6908 .child_processes
6909 .get_mut(&child_process_id)
6910 .ok_or_else(|| {
6911 SidecarError::InvalidState(format!(
6912 "child process {child_process_id} disappeared during spawn"
6913 ))
6914 })?;
6915 child.tty_master_owner = inherited_tty_master_owner;
6916 if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
6917 child.kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
6918 }
6919 }
6920 record_execute_phase("child_process_register", phase_start.elapsed());
6921 record_execute_phase("child_process_spawn_total", total_start.elapsed());
6922 Ok(json!({
6923 "childId": child_process_id,
6924 "pid": kernel_pid,
6925 "command": resolved.command,
6926 "args": resolved.process_args,
6927 }))
6928 }
6929
6930 pub(crate) fn spawn_javascript_child_process_sync(
6931 &mut self,
6932 vm_id: &str,
6933 process_id: &str,
6934 request: JavascriptChildProcessSpawnRequest,
6935 max_buffer: Option<usize>,
6936 ) -> Result<Value, SidecarError> {
6937 let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
6938 let timeout_deadline = request
6939 .options
6940 .timeout
6941 .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
6942 let timeout_signal = request
6943 .options
6944 .kill_signal
6945 .clone()
6946 .unwrap_or_else(|| String::from("SIGTERM"));
6947 let spawned = self.spawn_javascript_child_process(vm_id, process_id, request)?;
6948 let child_process_id = spawned
6949 .get("childId")
6950 .and_then(Value::as_str)
6951 .ok_or_else(|| {
6952 SidecarError::InvalidState(String::from(
6953 "child_process.spawn_sync response is missing childId",
6954 ))
6955 })?
6956 .to_owned();
6957
6958 if let Some(input) = sync_input.as_deref() {
6959 self.write_javascript_child_process_stdin(vm_id, process_id, &child_process_id, input)?;
6960 }
6961 self.close_javascript_child_process_stdin(vm_id, process_id, &child_process_id)?;
6962
6963 let max_buffer = max_buffer.unwrap_or(1024 * 1024);
6964 let mut stdout = Vec::new();
6965 let mut stderr = Vec::new();
6966 let mut max_buffer_exceeded = false;
6967 let mut kill_sent = false;
6968 let mut timed_out = false;
6969
6970 let exit_code = loop {
6971 let wait_ms = if let Some(deadline) = timeout_deadline {
6972 let now = Instant::now();
6973 if now >= deadline {
6974 if !kill_sent {
6975 timed_out = true;
6976 self.kill_javascript_child_process(
6977 vm_id,
6978 process_id,
6979 &child_process_id,
6980 &timeout_signal,
6981 )?;
6982 kill_sent = true;
6983 }
6984 0
6985 } else {
6986 u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
6987 .unwrap_or(50)
6988 }
6989 } else {
6990 50
6991 };
6992 let event =
6993 self.poll_javascript_child_process(vm_id, process_id, &child_process_id, wait_ms)?;
6994 if event.is_null() {
6995 continue;
6996 }
6997
6998 match event.get("type").and_then(Value::as_str) {
6999 Some("stdout") => {
7000 let chunk = javascript_sync_rpc_bytes_arg(
7001 &[event.get("data").cloned().unwrap_or(Value::Null)],
7002 0,
7003 "child_process.spawn_sync stdout",
7004 )?;
7005 stdout.extend_from_slice(&chunk);
7006 if stdout.len() > max_buffer && !kill_sent {
7007 max_buffer_exceeded = true;
7008 self.kill_javascript_child_process(
7009 vm_id,
7010 process_id,
7011 &child_process_id,
7012 "SIGTERM",
7013 )?;
7014 kill_sent = true;
7015 }
7016 }
7017 Some("stderr") => {
7018 let chunk = javascript_sync_rpc_bytes_arg(
7019 &[event.get("data").cloned().unwrap_or(Value::Null)],
7020 0,
7021 "child_process.spawn_sync stderr",
7022 )?;
7023 stderr.extend_from_slice(&chunk);
7024 if stderr.len() > max_buffer && !kill_sent {
7025 max_buffer_exceeded = true;
7026 self.kill_javascript_child_process(
7027 vm_id,
7028 process_id,
7029 &child_process_id,
7030 "SIGTERM",
7031 )?;
7032 kill_sent = true;
7033 }
7034 }
7035 Some("exit") => {
7036 break event
7037 .get("exitCode")
7038 .and_then(Value::as_i64)
7039 .map(|value| value as i32)
7040 .unwrap_or(1);
7041 }
7042 _ => {}
7043 }
7044 };
7045
7046 Ok(json!({
7047 "stdout": String::from_utf8_lossy(&stdout),
7048 "stderr": String::from_utf8_lossy(&stderr),
7049 "code": exit_code,
7050 "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
7051 "timedOut": timed_out,
7052 "maxBufferExceeded": max_buffer_exceeded,
7053 }))
7054 }
7055
7056 fn spawn_descendant_javascript_child_process(
7057 &mut self,
7058 vm_id: &str,
7059 process_id: &str,
7060 current_process_path: &[&str],
7061 request: JavascriptChildProcessSpawnRequest,
7062 ) -> Result<Value, SidecarError> {
7063 let total_start = Instant::now();
7064 let current_process_label =
7065 Self::child_process_path_label(process_id, current_process_path);
7066 let phase_start = Instant::now();
7067 let (resolved, parent_kernel_pid) = {
7068 let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
7069 let root = vm
7070 .active_processes
7071 .get(process_id)
7072 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
7073 let parent =
7074 Self::active_process_by_path(root, current_process_path).ok_or_else(|| {
7075 SidecarError::InvalidState(format!(
7076 "unknown child process path {current_process_label} during nested spawn"
7077 ))
7078 })?;
7079 (
7080 self.resolve_javascript_child_process_execution(
7081 vm,
7082 &parent.env,
7083 &parent.guest_cwd,
7084 &parent.host_cwd,
7085 &request,
7086 )?,
7087 parent.kernel_pid,
7088 )
7089 };
7090 record_execute_phase("child_process_resolve_execution", phase_start.elapsed());
7091
7092 let sidecar_requests = self.sidecar_requests.clone();
7093 let vm = self
7094 .vms
7095 .get_mut(vm_id)
7096 .ok_or_else(|| missing_vm_error(vm_id))?;
7097 let phase_start = Instant::now();
7098 let child_process_id = {
7099 let root = vm
7100 .active_processes
7101 .get_mut(process_id)
7102 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
7103 let parent =
7104 Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
7105 SidecarError::InvalidState(format!(
7106 "unknown child process path {current_process_label} during nested spawn"
7107 ))
7108 })?;
7109 parent.allocate_child_process_id()
7110 };
7111 let mut child_path = current_process_path.to_vec();
7112 child_path.push(child_process_id.as_str());
7113 let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
7114 .tool_command
7115 {
7116 let tool_resolution = resolve_tool_command(
7117 vm,
7118 &resolved.command,
7119 &resolved.execution_args,
7120 Some(&resolved.guest_cwd),
7121 )?
7122 .ok_or_else(|| {
7123 SidecarError::InvalidState(format!(
7124 "tool command no longer resolves: {}",
7125 resolved.command
7126 ))
7127 })?;
7128 let kernel_handle = vm
7129 .kernel
7130 .create_virtual_process(
7131 EXECUTION_DRIVER_NAME,
7132 TOOL_DRIVER_NAME,
7133 &resolved.command,
7134 resolved.process_args.clone(),
7135 VirtualProcessOptions {
7136 parent_pid: Some(parent_kernel_pid),
7137 env: resolved.env.clone(),
7138 cwd: Some(resolved.guest_cwd.clone()),
7139 },
7140 )
7141 .map_err(kernel_error)?;
7142 let kernel_pid = kernel_handle.pid();
7143 let tool_execution = ToolExecution::default();
7144 let cancelled = tool_execution.cancelled.clone();
7145 let pending_events = tool_execution.pending_events.clone();
7146 let events_overflowed = tool_execution.events_overflowed.clone();
7147 spawn_tool_process_events(ToolProcessEventRequest {
7148 sidecar_requests: sidecar_requests.clone(),
7149 connection_id: vm.connection_id.clone(),
7150 session_id: vm.session_id.clone(),
7151 vm_id: vm_id.to_owned(),
7152 tool_resolution,
7153 cancelled,
7154 pending_events,
7155 events_overflowed,
7156 });
7157 (
7158 kernel_pid,
7159 kernel_handle,
7160 ActiveExecution::Tool(tool_execution),
7161 None,
7162 )
7163 } else {
7164 let kernel_command = match resolved.runtime {
7165 GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
7166 GuestRuntimeKind::WebAssembly => WASM_COMMAND,
7167 GuestRuntimeKind::Python => PYTHON_COMMAND,
7168 };
7169 let kernel_handle = vm
7170 .kernel
7171 .spawn_process(
7172 kernel_command,
7173 resolved.process_args.clone(),
7174 SpawnOptions {
7175 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
7176 parent_pid: Some(parent_kernel_pid),
7177 env: resolved.env.clone(),
7178 cwd: Some(resolved.guest_cwd.clone()),
7179 },
7180 )
7181 .map_err(kernel_error)?;
7182 let kernel_pid = kernel_handle.pid();
7183 if request.options.detached {
7184 vm.kernel
7185 .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
7186 .map_err(kernel_error)?;
7187 }
7188 let mut execution_env = resolved.env.clone();
7189 execution_env.insert(
7190 String::from(EXECUTION_SANDBOX_ROOT_ENV),
7191 normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
7192 );
7193 let execution = match resolved.runtime {
7194 GuestRuntimeKind::JavaScript => {
7195 execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
7196 &request.options.internal_bootstrap_env,
7197 ));
7198 execution_env.insert(
7199 String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
7200 String::from("1"),
7201 );
7202 let context =
7203 self.javascript_engine
7204 .create_context(CreateJavascriptContextRequest {
7205 vm_id: vm_id.to_owned(),
7206 bootstrap_module: None,
7207 compile_cache_root: Some(
7208 self.cache_root.join("node-compile-cache"),
7209 ),
7210 });
7211 let inline_code = load_javascript_entrypoint_source(
7212 vm,
7213 &resolved.host_cwd,
7214 &resolved.entrypoint,
7215 &execution_env,
7216 );
7217 prepare_javascript_shadow(vm, &resolved)?;
7218
7219 let built_reader = build_module_reader(vm, &resolved);
7220 let guest_reader = built_reader.clone().map(|reader| {
7221 Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
7222 as Box<dyn GuestModuleReader>
7223 });
7224 let module_reader = built_reader
7225 .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
7226 let execution = self
7227 .javascript_engine
7228 .start_execution_with_module_reader(
7229 StartJavascriptExecutionRequest {
7230 guest_runtime: guest_runtime_identity(
7231 vm,
7232 Some(u64::from(kernel_pid)),
7233 Some(u64::from(parent_kernel_pid)),
7234 ),
7235 vm_id: vm_id.to_owned(),
7236 context_id: context.context_id,
7237 argv: std::iter::once(resolved.entrypoint.clone())
7238 .chain(resolved.execution_args.clone())
7239 .collect(),
7240 env: execution_env,
7241 cwd: resolved.host_cwd.clone(),
7242 limits: javascript_execution_limits(vm),
7243 inline_code,
7244 wasm_module_bytes: None,
7245 },
7246 module_reader,
7247 guest_reader,
7248 )
7249 .map_err(javascript_error)?;
7250 ActiveExecution::Javascript(execution)
7251 }
7252 GuestRuntimeKind::WebAssembly => {
7253 execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
7254 let wasm_limits = wasm_execution_limits(vm);
7255 let wasm_guest_runtime = guest_runtime_identity(
7256 vm,
7257 Some(u64::from(kernel_pid)),
7258 Some(u64::from(parent_kernel_pid)),
7259 );
7260 let context = self.wasm_engine.create_context(CreateWasmContextRequest {
7261 vm_id: vm_id.to_owned(),
7262 module_path: Some(resolved.entrypoint.clone()),
7263 });
7264 let execution = self
7265 .wasm_engine
7266 .start_execution(StartWasmExecutionRequest {
7267 vm_id: vm_id.to_owned(),
7268 context_id: context.context_id,
7269 argv: resolved.process_args.clone(),
7270 env: execution_env,
7271 cwd: resolved.host_cwd.clone(),
7272 permission_tier: execution_wasm_permission_tier(
7273 resolved
7274 .wasm_permission_tier
7275 .unwrap_or(WasmPermissionTier::Full),
7276 ),
7277 limits: wasm_limits,
7278 guest_runtime: wasm_guest_runtime,
7279 })
7280 .map_err(wasm_error)?;
7281 ActiveExecution::Wasm(Box::new(execution))
7282 }
7283 GuestRuntimeKind::Python => {
7284 let python_file_path = if execution_env.contains_key("AGENTOS_PYTHON_ARGV") {
7288 execution_env.get("AGENTOS_PYTHON_FILE").map(PathBuf::from)
7289 } else {
7290 python_file_entrypoint(&resolved.entrypoint)
7291 };
7292 let pyodide_dist_path = self
7293 .python_engine
7294 .bundled_pyodide_dist_path_for_vm(vm_id)
7295 .map_err(python_error)?;
7296 let pyodide_cache_path = pyodide_dist_path
7297 .parent()
7298 .and_then(Path::parent)
7299 .unwrap_or(pyodide_dist_path.as_path())
7300 .join("pyodide-package-cache");
7301 add_runtime_guest_path_mapping(
7302 &mut execution_env,
7303 PYTHON_PYODIDE_GUEST_ROOT,
7304 &pyodide_dist_path,
7305 );
7306 add_runtime_guest_path_mapping(
7307 &mut execution_env,
7308 PYTHON_PYODIDE_CACHE_GUEST_ROOT,
7309 &pyodide_cache_path,
7310 );
7311 add_runtime_host_access_path(
7312 &mut execution_env,
7313 "AGENTOS_EXTRA_FS_READ_PATHS",
7314 &pyodide_dist_path,
7315 true,
7316 );
7317 add_runtime_host_access_path(
7318 &mut execution_env,
7319 "AGENTOS_EXTRA_FS_READ_PATHS",
7320 &pyodide_cache_path,
7321 true,
7322 );
7323 add_runtime_host_access_path(
7324 &mut execution_env,
7325 "AGENTOS_EXTRA_FS_WRITE_PATHS",
7326 &pyodide_cache_path,
7327 false,
7328 );
7329 let context = self
7330 .python_engine
7331 .create_context(CreatePythonContextRequest {
7332 vm_id: vm_id.to_owned(),
7333 pyodide_dist_path,
7334 });
7335 let execution = self
7336 .python_engine
7337 .start_execution(StartPythonExecutionRequest {
7338 vm_id: vm_id.to_owned(),
7339 context_id: context.context_id,
7340 code: resolved.entrypoint.clone(),
7341 file_path: python_file_path,
7342 env: execution_env,
7343 cwd: resolved.host_cwd.clone(),
7344 limits: python_execution_limits(vm),
7345 guest_runtime: guest_runtime_identity(
7346 vm,
7347 Some(u64::from(kernel_pid)),
7348 Some(u64::from(parent_kernel_pid)),
7349 ),
7350 })
7351 .map_err(python_error)?;
7352 ActiveExecution::Python(execution)
7353 }
7354 };
7355 let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
7356 "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
7357 "ignore" => {
7358 vm.kernel
7359 .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
7360 .map_err(kernel_error)?;
7361 None
7362 }
7363 "inherit" => None,
7364 _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
7365 };
7366 (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
7367 };
7368 record_execute_phase(
7369 "child_process_spawn_and_start_execution",
7370 phase_start.elapsed(),
7371 );
7372
7373 let phase_start = Instant::now();
7374 let child_fd1_is_tty = vm
7375 .kernel
7376 .isatty(EXECUTION_DRIVER_NAME, kernel_pid, 1)
7377 .unwrap_or(false);
7378 let root = vm
7379 .active_processes
7380 .get_mut(process_id)
7381 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
7382 let parent =
7383 Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
7384 SidecarError::InvalidState(format!(
7385 "unknown child process path {current_process_label} during nested spawn"
7386 ))
7387 })?;
7388 let inherited_tty_master_owner = if child_fd1_is_tty {
7389 parent
7390 .tty_master_fd
7391 .map(|master_fd| (parent.kernel_pid, master_fd))
7392 .or(parent.tty_master_owner)
7393 } else {
7394 None
7395 };
7396 parent.child_processes.insert(
7397 child_process_id.clone(),
7398 ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
7399 .with_detached(request.options.detached)
7400 .with_guest_cwd(resolved.guest_cwd.clone())
7401 .with_env(resolved.env.clone())
7402 .with_host_cwd(resolved.host_cwd.clone()),
7403 );
7404 {
7405 let child = parent
7406 .child_processes
7407 .get_mut(&child_process_id)
7408 .ok_or_else(|| {
7409 SidecarError::InvalidState(format!(
7410 "child process {child_process_id} disappeared during nested spawn"
7411 ))
7412 })?;
7413 child.tty_master_owner = inherited_tty_master_owner;
7414 if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
7415 child.kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
7416 }
7417 }
7418 record_execute_phase("child_process_register", phase_start.elapsed());
7419 record_execute_phase("child_process_spawn_total", total_start.elapsed());
7420 Ok(json!({
7421 "childId": child_process_id,
7422 "pid": kernel_pid,
7423 "command": resolved.command,
7424 "args": resolved.process_args,
7425 }))
7426 }
7427
7428 fn spawn_descendant_javascript_child_process_sync(
7429 &mut self,
7430 vm_id: &str,
7431 process_id: &str,
7432 current_process_path: &[&str],
7433 request: JavascriptChildProcessSpawnRequest,
7434 max_buffer: Option<usize>,
7435 ) -> Result<Value, SidecarError> {
7436 let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
7437 let timeout_deadline = request
7438 .options
7439 .timeout
7440 .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
7441 let timeout_signal = request
7442 .options
7443 .kill_signal
7444 .clone()
7445 .unwrap_or_else(|| String::from("SIGTERM"));
7446 let spawned = self.spawn_descendant_javascript_child_process(
7447 vm_id,
7448 process_id,
7449 current_process_path,
7450 request,
7451 )?;
7452 let child_process_id = spawned
7453 .get("childId")
7454 .and_then(Value::as_str)
7455 .ok_or_else(|| {
7456 SidecarError::InvalidState(String::from(
7457 "child_process.spawn_sync response is missing childId",
7458 ))
7459 })?
7460 .to_owned();
7461
7462 if let Some(input) = sync_input.as_deref() {
7463 self.write_descendant_javascript_child_process_stdin(
7464 vm_id,
7465 process_id,
7466 current_process_path,
7467 &child_process_id,
7468 input,
7469 )?;
7470 }
7471 self.close_descendant_javascript_child_process_stdin(
7472 vm_id,
7473 process_id,
7474 current_process_path,
7475 &child_process_id,
7476 )?;
7477
7478 let max_buffer = max_buffer.unwrap_or(1024 * 1024);
7479 let mut stdout = Vec::new();
7480 let mut stderr = Vec::new();
7481 let mut max_buffer_exceeded = false;
7482 let mut kill_sent = false;
7483 let mut timed_out = false;
7484
7485 let exit_code = loop {
7486 let wait_ms = if let Some(deadline) = timeout_deadline {
7487 let now = Instant::now();
7488 if now >= deadline {
7489 if !kill_sent {
7490 timed_out = true;
7491 self.kill_descendant_javascript_child_process(
7492 vm_id,
7493 process_id,
7494 current_process_path,
7495 &child_process_id,
7496 &timeout_signal,
7497 )?;
7498 kill_sent = true;
7499 }
7500 0
7501 } else {
7502 u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
7503 .unwrap_or(50)
7504 }
7505 } else {
7506 50
7507 };
7508 let event = self.poll_descendant_javascript_child_process(
7509 vm_id,
7510 process_id,
7511 current_process_path,
7512 &child_process_id,
7513 wait_ms,
7514 )?;
7515 if event.is_null() {
7516 continue;
7517 }
7518
7519 match event.get("type").and_then(Value::as_str) {
7520 Some("stdout") => {
7521 let chunk = javascript_sync_rpc_bytes_arg(
7522 &[event.get("data").cloned().unwrap_or(Value::Null)],
7523 0,
7524 "child_process.spawn_sync stdout",
7525 )?;
7526 stdout.extend_from_slice(&chunk);
7527 if stdout.len() > max_buffer && !kill_sent {
7528 max_buffer_exceeded = true;
7529 self.kill_descendant_javascript_child_process(
7530 vm_id,
7531 process_id,
7532 current_process_path,
7533 &child_process_id,
7534 "SIGTERM",
7535 )?;
7536 kill_sent = true;
7537 }
7538 }
7539 Some("stderr") => {
7540 let chunk = javascript_sync_rpc_bytes_arg(
7541 &[event.get("data").cloned().unwrap_or(Value::Null)],
7542 0,
7543 "child_process.spawn_sync stderr",
7544 )?;
7545 stderr.extend_from_slice(&chunk);
7546 if stderr.len() > max_buffer && !kill_sent {
7547 max_buffer_exceeded = true;
7548 self.kill_descendant_javascript_child_process(
7549 vm_id,
7550 process_id,
7551 current_process_path,
7552 &child_process_id,
7553 "SIGTERM",
7554 )?;
7555 kill_sent = true;
7556 }
7557 }
7558 Some("exit") => {
7559 break event
7560 .get("exitCode")
7561 .and_then(Value::as_i64)
7562 .map(|value| value as i32)
7563 .unwrap_or(1);
7564 }
7565 _ => {}
7566 }
7567 };
7568
7569 Ok(json!({
7570 "stdout": String::from_utf8_lossy(&stdout),
7571 "stderr": String::from_utf8_lossy(&stderr),
7572 "code": exit_code,
7573 "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
7574 "timedOut": timed_out,
7575 "maxBufferExceeded": max_buffer_exceeded,
7576 }))
7577 }
7578
7579 fn handle_descendant_javascript_child_process_rpc(
7580 &mut self,
7581 vm_id: &str,
7582 process_id: &str,
7583 current_process_path: &[&str],
7584 request: &JavascriptSyncRpcRequest,
7585 ) -> Result<Value, SidecarError> {
7586 match request.method.as_str() {
7587 "child_process.spawn" => {
7588 let Some(vm) = self.vms.get(vm_id) else {
7589 return Ok(Value::Null);
7590 };
7591 let (payload, _) = parse_javascript_child_process_spawn_request(vm, &request.args)?;
7592 self.spawn_descendant_javascript_child_process(
7593 vm_id,
7594 process_id,
7595 current_process_path,
7596 payload,
7597 )
7598 }
7599 "child_process.spawn_sync" => {
7600 let Some(vm) = self.vms.get(vm_id) else {
7601 return Ok(Value::Null);
7602 };
7603 let (payload, max_buffer) =
7604 parse_javascript_child_process_spawn_request(vm, &request.args)?;
7605 self.spawn_descendant_javascript_child_process_sync(
7606 vm_id,
7607 process_id,
7608 current_process_path,
7609 payload,
7610 max_buffer,
7611 )
7612 }
7613 "child_process.poll" => {
7614 let child_process_id =
7615 javascript_sync_rpc_arg_str(&request.args, 0, "child_process.poll child id")?;
7616 let wait_ms = javascript_sync_rpc_arg_u64_optional(
7617 &request.args,
7618 1,
7619 "child_process.poll wait ms",
7620 )?
7621 .unwrap_or_default();
7622 self.poll_descendant_javascript_child_process(
7623 vm_id,
7624 process_id,
7625 current_process_path,
7626 child_process_id,
7627 wait_ms,
7628 )
7629 }
7630 "child_process.write_stdin" => {
7631 let child_process_id = javascript_sync_rpc_arg_str(
7632 &request.args,
7633 0,
7634 "child_process.write_stdin child id",
7635 )?;
7636 let chunk = javascript_sync_rpc_bytes_arg(
7637 &request.args,
7638 1,
7639 "child_process.write_stdin chunk",
7640 )?;
7641 self.write_descendant_javascript_child_process_stdin(
7642 vm_id,
7643 process_id,
7644 current_process_path,
7645 child_process_id,
7646 &chunk,
7647 )?;
7648 Ok(Value::Null)
7649 }
7650 "child_process.close_stdin" => {
7651 let child_process_id = javascript_sync_rpc_arg_str(
7652 &request.args,
7653 0,
7654 "child_process.close_stdin child id",
7655 )?;
7656 self.close_descendant_javascript_child_process_stdin(
7657 vm_id,
7658 process_id,
7659 current_process_path,
7660 child_process_id,
7661 )?;
7662 Ok(Value::Null)
7663 }
7664 "child_process.kill" => {
7665 let child_process_id =
7666 javascript_sync_rpc_arg_str(&request.args, 0, "child_process.kill child id")?;
7667 let signal =
7668 javascript_sync_rpc_arg_str(&request.args, 1, "child_process.kill signal")?;
7669 self.kill_descendant_javascript_child_process(
7670 vm_id,
7671 process_id,
7672 current_process_path,
7673 child_process_id,
7674 signal,
7675 )?;
7676 Ok(Value::Null)
7677 }
7678 _ => Err(SidecarError::InvalidState(format!(
7679 "unsupported nested child process RPC method {}",
7680 request.method
7681 ))),
7682 }
7683 }
7684
7685 fn service_child_kernel_wait_rpc(
7693 &mut self,
7694 vm_id: &str,
7695 process_id: &str,
7696 current_process_path: &[&str],
7697 child_process_id: &str,
7698 request: &JavascriptSyncRpcRequest,
7699 ) -> Result<bool, SidecarError> {
7700 let Some(vm) = self.vms.get_mut(vm_id) else {
7701 return Ok(true);
7702 };
7703 let kernel = &mut vm.kernel;
7704 let Some(root) = vm.active_processes.get_mut(process_id) else {
7705 return Ok(true);
7706 };
7707 let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
7708 return Ok(true);
7709 };
7710 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
7711 return Ok(true);
7712 };
7713 if request.method == "__kernel_stdin_read"
7714 && matches!(child.execution, ActiveExecution::Javascript(_))
7715 && child.tty_master_fd.is_none()
7716 {
7717 return Ok(false);
7718 }
7719 let now = Instant::now();
7720 let requested_timeout_ms = match request.method.as_str() {
7721 "__kernel_stdin_read" => parse_kernel_stdin_read_args(request)?.1,
7722 _ => u64::try_from(parse_kernel_poll_args(request)?.1).unwrap_or(0),
7723 };
7724 let deadline = match &child.deferred_kernel_wait_rpc {
7725 Some((parked, parked_deadline)) if parked.id == request.id => *parked_deadline,
7726 _ => now + Duration::from_millis(requested_timeout_ms),
7727 };
7728 let kernel_pid = child.kernel_pid;
7729 let probe = match request.method.as_str() {
7730 "__kernel_stdin_read" => {
7731 let (max_bytes, _) = parse_kernel_stdin_read_args(request)?;
7732 kernel_stdin_read_response(kernel, kernel_pid, max_bytes, Duration::ZERO)
7733 }
7734 _ => {
7735 let (fd_requests, _) = parse_kernel_poll_args(request)?;
7736 kernel_poll_response(kernel, kernel_pid, &fd_requests, 0)
7737 }
7738 };
7739 let probe = match probe {
7740 Ok(value) => value,
7741 Err(error) => {
7742 child.deferred_kernel_wait_rpc = None;
7743 child
7744 .execution
7745 .respond_javascript_sync_rpc_error(
7746 request.id,
7747 javascript_sync_rpc_error_code(&error),
7748 error.to_string(),
7749 )
7750 .or_else(ignore_stale_javascript_sync_rpc_response)?;
7751 return Ok(true);
7752 }
7753 };
7754 let ready = match request.method.as_str() {
7755 "__kernel_stdin_read" => !probe.is_null(),
7756 _ => probe.get("readyCount").and_then(Value::as_u64).unwrap_or(0) > 0,
7757 };
7758 if ready || requested_timeout_ms == 0 || now >= deadline {
7759 child.deferred_kernel_wait_rpc = None;
7760 child
7761 .execution
7762 .respond_javascript_sync_rpc_response(request.id, probe.into())
7763 .or_else(ignore_stale_javascript_sync_rpc_response)?;
7764 return Ok(true);
7765 }
7766 child.deferred_kernel_wait_rpc = Some((request.clone(), deadline));
7767 Ok(true)
7768 }
7769
7770 pub(crate) fn service_shared_tty_stdio_write(
7777 &mut self,
7778 vm_id: &str,
7779 writer_kernel_pid: u32,
7780 owner: (u32, u32),
7781 request: &JavascriptSyncRpcRequest,
7782 ) -> Result<Value, SidecarError> {
7783 let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_stdio_write fd")?;
7784 let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "__kernel_stdio_write chunk")?;
7785 if fd != 1 && fd != 2 {
7786 return Err(SidecarError::InvalidState(format!(
7787 "__kernel_stdio_write only supports fd 1/2, got {fd}"
7788 )));
7789 }
7790 let Some(vm) = self.vms.get_mut(vm_id) else {
7791 return Ok(json!(chunk.len()));
7792 };
7793 let written = if fd == 1 {
7794 vm.kernel
7795 .write_process_stdout(EXECUTION_DRIVER_NAME, writer_kernel_pid, &chunk)
7796 .map_err(kernel_error)?
7797 } else {
7798 vm.kernel
7799 .write_process_stderr(EXECUTION_DRIVER_NAME, writer_kernel_pid, &chunk)
7800 .map_err(kernel_error)?
7801 };
7802 let (owner_pid, master_fd) = owner;
7803 let mut drained: Vec<u8> = Vec::new();
7804 loop {
7805 match vm.kernel.fd_read_with_timeout_result(
7806 EXECUTION_DRIVER_NAME,
7807 owner_pid,
7808 master_fd,
7809 MAX_PTY_BUFFER_BYTES,
7810 Some(Duration::ZERO),
7811 ) {
7812 Ok(Some(bytes)) if !bytes.is_empty() => drained.extend(bytes),
7813 Ok(_) => break,
7814 Err(error) if error.code() == "EAGAIN" => break,
7815 Err(error) => return Err(kernel_error(error)),
7816 }
7817 }
7818 if !drained.is_empty() {
7819 if let Some(owner_process) = vm
7820 .active_processes
7821 .values_mut()
7822 .find(|process| process.kernel_pid == owner_pid)
7823 {
7824 owner_process
7825 .queue_pending_execution_event(ActiveExecutionEvent::Stdout(drained))?;
7826 }
7827 }
7828 Ok(json!(written))
7829 }
7830
7831 fn recheck_child_deferred_kernel_wait_rpc(
7834 &mut self,
7835 vm_id: &str,
7836 process_id: &str,
7837 current_process_path: &[&str],
7838 child_process_id: &str,
7839 ) -> Result<(), SidecarError> {
7840 let parked = {
7841 let Some(vm) = self.vms.get_mut(vm_id) else {
7842 return Ok(());
7843 };
7844 let Some(root) = vm.active_processes.get_mut(process_id) else {
7845 return Ok(());
7846 };
7847 let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
7848 return Ok(());
7849 };
7850 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
7851 return Ok(());
7852 };
7853 child
7854 .deferred_kernel_wait_rpc
7855 .as_ref()
7856 .map(|(request, _)| request.clone())
7857 };
7858 if let Some(request) = parked {
7859 let _ = self.service_child_kernel_wait_rpc(
7860 vm_id,
7861 process_id,
7862 current_process_path,
7863 child_process_id,
7864 &request,
7865 )?;
7866 }
7867 Ok(())
7868 }
7869
7870 fn poll_descendant_javascript_child_process(
7871 &mut self,
7872 vm_id: &str,
7873 process_id: &str,
7874 current_process_path: &[&str],
7875 child_process_id: &str,
7876 wait_ms: u64,
7877 ) -> Result<Value, SidecarError> {
7878 let mut child_path = current_process_path.to_vec();
7879 child_path.push(child_process_id);
7880 let child_gone_error = || javascript_child_process_gone_error(process_id, &child_path);
7881 let deadline = Instant::now() + Duration::from_millis(wait_ms);
7882 let mut polled_once = false;
7883
7884 loop {
7885 self.drain_queued_descendant_javascript_child_process_events(
7886 vm_id,
7887 process_id,
7888 &child_path,
7889 )?;
7890 self.recheck_child_deferred_kernel_wait_rpc(
7891 vm_id,
7892 process_id,
7893 current_process_path,
7894 child_process_id,
7895 )?;
7896 enum ChildPollResult {
7897 Event(Box<Option<ActiveExecutionEvent>>),
7898 RecoverRuntimeExit,
7899 Timeout,
7900 }
7901 let wait = if wait_ms == 0 {
7902 Duration::ZERO
7903 } else {
7904 deadline.saturating_duration_since(Instant::now())
7905 };
7906 let poll_result = {
7907 let Some(vm) = self.vms.get_mut(vm_id) else {
7908 return Ok(Value::Null);
7909 };
7910 let Some(parent) =
7911 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
7912 else {
7913 return Err(child_gone_error());
7914 };
7915 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
7916 return Err(child_gone_error());
7917 };
7918 if let Some(event) = child.pending_execution_events.pop_front() {
7919 ChildPollResult::Event(Box::new(Some(event)))
7920 } else if polled_once && wait.is_zero() {
7921 ChildPollResult::Timeout
7922 } else {
7923 polled_once = true;
7924 match child.execution.poll_event_blocking(wait) {
7925 Ok(Some(event)) => ChildPollResult::Event(Box::new(Some(event))),
7926 Ok(None) => ChildPollResult::RecoverRuntimeExit,
7927 Err(SidecarError::Execution(message))
7928 if (child.runtime == GuestRuntimeKind::JavaScript
7929 && closed_javascript_event_channel(&message))
7930 || (child.runtime == GuestRuntimeKind::Python
7931 && closed_python_event_channel(&message))
7932 || (child.runtime == GuestRuntimeKind::WebAssembly
7933 && closed_wasm_event_channel(&message)) =>
7934 {
7935 ChildPollResult::RecoverRuntimeExit
7936 }
7937 Err(error) => return Err(error),
7938 }
7939 }
7940 };
7941 let event = match poll_result {
7942 ChildPollResult::Event(event) => *event,
7943 ChildPollResult::Timeout => return Ok(Value::Null),
7944 ChildPollResult::RecoverRuntimeExit => self
7945 .recover_descendant_runtime_child_process_event(
7946 vm_id,
7947 process_id,
7948 current_process_path,
7949 child_process_id,
7950 wait.as_millis().try_into().unwrap_or(u64::MAX),
7951 )?,
7952 };
7953
7954 let Some(event) = event else {
7955 return Ok(Value::Null);
7956 };
7957
7958 match event {
7959 ActiveExecutionEvent::Stdout(chunk) => {
7960 return Ok(json!({
7961 "type": "stdout",
7962 "data": javascript_sync_rpc_bytes_value(&chunk),
7963 }));
7964 }
7965 ActiveExecutionEvent::Stderr(chunk) => {
7966 return Ok(json!({
7967 "type": "stderr",
7968 "data": javascript_sync_rpc_bytes_value(&chunk),
7969 }));
7970 }
7971 ActiveExecutionEvent::Exited(exit_code) => {
7972 let cleanup_start = Instant::now();
7973 let had_trailing_events = {
7974 let Some(vm) = self.vms.get_mut(vm_id) else {
7975 return Ok(Value::Null);
7976 };
7977 let Some(parent) = Self::descendant_parent_process_mut(
7978 vm,
7979 process_id,
7980 current_process_path,
7981 ) else {
7982 return Ok(Value::Null);
7983 };
7984 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
7985 return Ok(Value::Null);
7986 };
7987 let mut quiet_deadline = Instant::now() + PROCESS_EXIT_DRAIN_INITIAL_QUIET;
7988 loop {
7989 let wait = quiet_deadline.saturating_duration_since(Instant::now());
7990 let next = poll_child_execution_after_exit(child, wait)?;
7991 let Some(next) = next else {
7992 break;
7993 };
7994 if matches!(next, ActiveExecutionEvent::Exited(_)) {
7995 continue;
7996 }
7997 child.queue_pending_execution_event(next)?;
7998 quiet_deadline = Instant::now() + PROCESS_EXIT_DRAIN_TRAILING_QUIET;
7999 }
8000 if !child.pending_execution_events.is_empty() {
8001 child.queue_pending_execution_event(ActiveExecutionEvent::Exited(
8002 exit_code,
8003 ))?;
8004 true
8005 } else {
8006 false
8007 }
8008 };
8009 if had_trailing_events {
8010 continue;
8011 }
8012
8013 let parent_signal_key =
8014 Self::child_process_signal_key(process_id, current_process_path);
8015 let Some(vm) = self.vms.get_mut(vm_id) else {
8016 return Ok(Value::Null);
8017 };
8018 let signal_name = {
8019 let Some(parent) = Self::descendant_parent_process_mut(
8020 vm,
8021 process_id,
8022 current_process_path,
8023 ) else {
8024 return Ok(Value::Null);
8025 };
8026 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8027 return Ok(Value::Null);
8028 };
8029 child.pending_self_signal_exit.take().and_then(|signal| {
8030 if exit_code == 128 + signal {
8031 canonical_signal_name(signal).map(str::to_owned)
8032 } else {
8033 None
8034 }
8035 })
8036 };
8037 let (parent_runtime_pid, parent_v8_signal_session, should_signal_parent) = {
8038 let Some(parent) =
8039 Self::descendant_parent_process(vm, process_id, current_process_path)
8040 else {
8041 return Ok(Value::Null);
8042 };
8043 (
8044 parent.execution.child_pid(),
8045 parent.execution.javascript_v8_session_handle().filter(|_| {
8046 matches!(
8047 &parent.execution,
8048 ActiveExecution::Javascript(execution)
8049 if execution.uses_shared_v8_runtime()
8050 )
8051 }),
8052 vm.signal_states
8053 .get(parent_signal_key)
8054 .and_then(|handlers| handlers.get(&(libc::SIGCHLD as u32)))
8055 .is_some_and(|registration| {
8056 registration.action != SignalDispositionAction::Default
8057 }),
8058 )
8059 };
8060 let Some(parent) =
8061 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
8062 else {
8063 return Ok(Value::Null);
8064 };
8065 let Some(mut child) = parent.child_processes.remove(child_process_id) else {
8066 return Ok(Value::Null);
8067 };
8068 let child_process_label =
8069 Self::child_process_path_label(process_id, &child_path);
8070 let detached_children =
8071 Self::adopt_detached_child_processes(&child_process_label, &mut child);
8072 sync_process_host_writes_to_kernel(vm, &child)?;
8073 let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness);
8074 terminate_child_process_tree(&mut vm.kernel, &mut child, &kernel_readiness);
8075 child.kernel_handle.finish(exit_code);
8076 let _ = vm.kernel.wait_and_reap(child.kernel_pid);
8077 vm.signal_states.remove(child_process_id);
8078 for (detached_process_id, detached_child) in detached_children {
8079 vm.detached_child_processes
8080 .insert(detached_process_id.clone());
8081 vm.active_processes
8082 .insert(detached_process_id, detached_child);
8083 }
8084 if should_signal_parent {
8085 if let Some(session) = parent_v8_signal_session {
8086 dispatch_v8_session_signal_async(session, libc::SIGCHLD);
8087 } else {
8088 signal_runtime_process(parent_runtime_pid, libc::SIGCHLD)?;
8089 }
8090 }
8091 let mut payload = Map::new();
8092 payload.insert(String::from("type"), Value::String(String::from("exit")));
8093 payload.insert(String::from("exitCode"), Value::from(exit_code));
8094 if let Some(signal_name) = signal_name {
8095 payload.insert(String::from("signal"), Value::String(signal_name));
8096 }
8097 record_execute_phase("child_process_exit_cleanup", cleanup_start.elapsed());
8098 return Ok(Value::Object(payload));
8099 }
8100 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
8101 let mut current_child_path = current_process_path.to_vec();
8102 current_child_path.push(child_process_id);
8103 if matches!(
8104 request.method.as_str(),
8105 "__kernel_stdin_read" | "__kernel_poll"
8106 ) && self.service_child_kernel_wait_rpc(
8107 vm_id,
8108 process_id,
8109 current_process_path,
8110 child_process_id,
8111 &request,
8112 )? {
8113 continue;
8116 }
8117 if request.method == "__kernel_stdio_write" {
8118 let shared_tty = {
8119 let Some(vm) = self.vms.get_mut(vm_id) else {
8120 return Ok(Value::Null);
8121 };
8122 let Some(root) = vm.active_processes.get_mut(process_id) else {
8123 return Ok(Value::Null);
8124 };
8125 let Some(parent) =
8126 Self::active_process_by_path_mut(root, current_process_path)
8127 else {
8128 return Ok(Value::Null);
8129 };
8130 parent
8131 .child_processes
8132 .get(child_process_id)
8133 .and_then(|child| {
8134 child
8135 .tty_master_owner
8136 .map(|owner| (child.kernel_pid, owner))
8137 })
8138 };
8139 if let Some((child_kernel_pid, owner)) = shared_tty {
8140 let response = self.service_shared_tty_stdio_write(
8141 vm_id,
8142 child_kernel_pid,
8143 owner,
8144 &request,
8145 );
8146 let Some(vm) = self.vms.get_mut(vm_id) else {
8147 return Ok(Value::Null);
8148 };
8149 let Some(root) = vm.active_processes.get_mut(process_id) else {
8150 return Ok(Value::Null);
8151 };
8152 let Some(parent) =
8153 Self::active_process_by_path_mut(root, current_process_path)
8154 else {
8155 return Ok(Value::Null);
8156 };
8157 let Some(child) = parent.child_processes.get_mut(child_process_id)
8158 else {
8159 return Ok(Value::Null);
8160 };
8161 match response {
8162 Ok(result) => child
8163 .execution
8164 .respond_javascript_sync_rpc_response(request.id, result.into())
8165 .or_else(ignore_stale_javascript_sync_rpc_response)?,
8166 Err(error) => child
8167 .execution
8168 .respond_javascript_sync_rpc_error(
8169 request.id,
8170 javascript_sync_rpc_error_code(&error),
8171 error.to_string(),
8172 )
8173 .or_else(ignore_stale_javascript_sync_rpc_response)?,
8174 }
8175 continue;
8176 }
8177 }
8178 let response = if request.method == "process.signal_state" {
8179 let (signal, registration) =
8180 parse_process_signal_state_request(&request.args)
8181 .map_err(|error| SidecarError::InvalidState(error.to_string()))?;
8182 let Some(vm) = self.vms.get_mut(vm_id) else {
8183 return Ok(Value::Null);
8184 };
8185 let signal_key =
8186 Self::child_process_signal_key(process_id, ¤t_child_path)
8187 .to_owned();
8188 apply_process_signal_state_update(
8189 &mut vm.signal_states,
8190 &signal_key,
8191 signal,
8192 registration,
8193 );
8194 Ok(Value::Null.into())
8195 } else if request.method == "process.kill" {
8196 self.handle_descendant_process_kill_rpc(
8197 vm_id,
8198 process_id,
8199 current_process_path,
8200 child_process_id,
8201 &request,
8202 )
8203 .map(Into::into)
8204 } else if request.method.starts_with("child_process.") {
8205 self.handle_descendant_javascript_child_process_rpc(
8206 vm_id,
8207 process_id,
8208 ¤t_child_path,
8209 &request,
8210 )
8211 .map(Into::into)
8212 } else {
8213 let Some(vm) = self.vms.get_mut(vm_id) else {
8214 return Ok(Value::Null);
8215 };
8216 let resource_limits = vm.kernel.resource_limits().clone();
8217 let network_counts = vm_network_resource_counts(vm);
8218 let socket_paths = build_javascript_socket_path_context(vm)?;
8219 let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness);
8220 let Some(root) = vm.active_processes.get_mut(process_id) else {
8221 return Ok(Value::Null);
8222 };
8223 let Some(parent) =
8224 Self::active_process_by_path_mut(root, current_process_path)
8225 else {
8226 return Ok(Value::Null);
8227 };
8228 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8229 return Ok(Value::Null);
8230 };
8231 service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
8232 bridge: &self.bridge,
8233 vm_id,
8234 dns: &vm.dns,
8235 socket_paths: &socket_paths,
8236 kernel: &mut vm.kernel,
8237 kernel_readiness,
8238 process: child,
8239 sync_request: &request,
8240 resource_limits: &resource_limits,
8241 network_counts,
8242 })
8243 };
8244
8245 let Some(vm) = self.vms.get_mut(vm_id) else {
8246 return Ok(Value::Null);
8247 };
8248 let Some(parent) =
8249 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
8250 else {
8251 return Ok(Value::Null);
8252 };
8253 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8254 return Ok(Value::Null);
8255 };
8256 let parent_signal_event = response
8257 .as_ref()
8258 .ok()
8259 .and_then(JavascriptSyncRpcServiceResponse::as_json)
8260 .and_then(|result| {
8261 let target_path_label =
8262 Self::child_process_path_label(process_id, current_process_path);
8263 if request.method != "process.kill"
8264 || result.get("action").and_then(Value::as_str) != Some("user")
8265 || result.get("targetProcessPath").and_then(Value::as_str)
8266 != Some(target_path_label.as_str())
8267 {
8268 return None;
8269 }
8270 Some(json!({
8271 "type": "signal",
8272 "signal": result.get("signal").and_then(Value::as_str).unwrap_or_default(),
8273 "number": result.get("number").and_then(Value::as_i64).unwrap_or_default(),
8274 }))
8275 });
8276 match response {
8277 Ok(result) => child
8278 .execution
8279 .respond_javascript_sync_rpc_response(request.id, result)
8280 .or_else(ignore_stale_javascript_sync_rpc_response)?,
8281 Err(error) => child
8282 .execution
8283 .respond_javascript_sync_rpc_error(
8284 request.id,
8285 javascript_sync_rpc_error_code(&error),
8286 error.to_string(),
8287 )
8288 .or_else(ignore_stale_javascript_sync_rpc_response)?,
8289 }
8290 if let Some(event) = parent_signal_event {
8291 return Ok(event);
8292 }
8293 }
8294 ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
8295 let Some(vm) = self.vms.get_mut(vm_id) else {
8303 return Ok(Value::Null);
8304 };
8305 let Some(parent) =
8306 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
8307 else {
8308 return Ok(Value::Null);
8309 };
8310 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8311 return Ok(Value::Null);
8312 };
8313 let _ = child.execution.respond_python_vfs_rpc_error(
8319 request.id,
8320 "ERR_AGENTOS_PYTHON_VFS_UNAVAILABLE",
8321 "python VFS is not available for nested child processes",
8322 );
8323 }
8324 ActiveExecutionEvent::SignalState {
8325 signal,
8326 registration,
8327 } => {
8328 let Some(vm) = self.vms.get_mut(vm_id) else {
8329 return Ok(Value::Null);
8330 };
8331 let signal_key =
8332 Self::child_process_signal_key(process_id, &child_path).to_owned();
8333 apply_process_signal_state_update(
8334 &mut vm.signal_states,
8335 &signal_key,
8336 signal,
8337 registration.clone(),
8338 );
8339 return Ok(json!({
8340 "type": "signal_state",
8341 "signal": signal,
8342 "registration": registration,
8343 }));
8344 }
8345 }
8346 }
8347 }
8348
8349 fn recover_descendant_runtime_child_process_event(
8350 &mut self,
8351 vm_id: &str,
8352 process_id: &str,
8353 current_process_path: &[&str],
8354 child_process_id: &str,
8355 wait_ms: u64,
8356 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
8357 let (
8358 parent_kernel_pid,
8359 child_kernel_pid,
8360 child_runtime_pid,
8361 child_runtime,
8362 child_shared_runtime,
8363 ) = {
8364 let mut child_path = current_process_path.to_vec();
8365 child_path.push(child_process_id);
8366 let Some(vm) = self.vms.get_mut(vm_id) else {
8367 return Ok(None);
8368 };
8369 let Some(parent) =
8370 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
8371 else {
8372 return Err(javascript_child_process_gone_error(process_id, &child_path));
8373 };
8374 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8375 return Err(javascript_child_process_gone_error(process_id, &child_path));
8376 };
8377 (
8378 parent.kernel_pid,
8379 child.kernel_pid,
8380 child.execution.child_pid(),
8381 child.runtime.clone(),
8382 child.execution.uses_shared_v8_runtime(),
8383 )
8384 };
8385 if child_runtime != GuestRuntimeKind::JavaScript
8386 && child_runtime != GuestRuntimeKind::Python
8387 && child_runtime != GuestRuntimeKind::WebAssembly
8388 {
8389 return Ok(None);
8390 }
8391 let wait_deadline = Instant::now() + Duration::from_millis(wait_ms.min(25));
8392 loop {
8393 let Some(vm) = self.vms.get_mut(vm_id) else {
8394 return Ok(None);
8395 };
8396 if let Some(process_info) = vm.kernel.list_processes().get(&child_kernel_pid) {
8397 if process_info.status == ProcessStatus::Exited {
8398 return Ok(Some(ActiveExecutionEvent::Exited(
8399 process_info.exit_code.unwrap_or(0),
8400 )));
8401 }
8402 }
8403 if let Some(wait_result) = vm
8404 .kernel
8405 .waitpid_with_options(
8406 EXECUTION_DRIVER_NAME,
8407 parent_kernel_pid,
8408 child_kernel_pid as i32,
8409 WaitPidFlags::WNOHANG,
8410 )
8411 .map_err(kernel_error)?
8412 {
8413 return Ok(Some(ActiveExecutionEvent::Exited(wait_result.status)));
8414 }
8415
8416 if !child_shared_runtime && child_runtime_pid != 0 {
8417 if let Some(status) = runtime_child_exit_status(child_runtime_pid)? {
8418 return Ok(Some(ActiveExecutionEvent::Exited(status)));
8419 }
8420 if !runtime_child_is_alive(child_runtime_pid)? {
8421 return Ok(Some(ActiveExecutionEvent::Exited(0)));
8422 }
8423 }
8424 if Instant::now() >= wait_deadline {
8425 return Ok(None);
8426 }
8427 std::thread::sleep(Duration::from_millis(5));
8428 }
8429 }
8430
8431 fn write_descendant_javascript_child_process_stdin(
8432 &mut self,
8433 vm_id: &str,
8434 process_id: &str,
8435 current_process_path: &[&str],
8436 child_process_id: &str,
8437 chunk: &[u8],
8438 ) -> Result<(), SidecarError> {
8439 let mut child_path = current_process_path.to_vec();
8440 child_path.push(child_process_id);
8441 let Some(vm) = self.vms.get_mut(vm_id) else {
8442 return Err(javascript_child_process_gone_error(process_id, &child_path));
8443 };
8444 let Some(root) = vm.active_processes.get_mut(process_id) else {
8445 return Err(javascript_child_process_gone_error(process_id, &child_path));
8446 };
8447 let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
8448 return Err(javascript_child_process_gone_error(process_id, &child_path));
8449 };
8450 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8451 return Err(javascript_child_process_gone_error(process_id, &child_path));
8452 };
8453 if let Err(error) = child.execution.write_stdin(chunk) {
8454 if is_broken_pipe_error(&error) {
8455 return Ok(());
8456 }
8457 return Err(error);
8458 }
8459 write_kernel_process_stdin(&mut vm.kernel, child, chunk)
8460 }
8461
8462 fn close_descendant_javascript_child_process_stdin(
8463 &mut self,
8464 vm_id: &str,
8465 process_id: &str,
8466 current_process_path: &[&str],
8467 child_process_id: &str,
8468 ) -> Result<(), SidecarError> {
8469 let mut child_path = current_process_path.to_vec();
8470 child_path.push(child_process_id);
8471 let Some(vm) = self.vms.get_mut(vm_id) else {
8472 return Err(javascript_child_process_gone_error(process_id, &child_path));
8473 };
8474 let Some(root) = vm.active_processes.get_mut(process_id) else {
8475 return Err(javascript_child_process_gone_error(process_id, &child_path));
8476 };
8477 let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
8478 return Err(javascript_child_process_gone_error(process_id, &child_path));
8479 };
8480 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8481 return Err(javascript_child_process_gone_error(process_id, &child_path));
8482 };
8483 child.execution.close_stdin()?;
8484 close_kernel_process_stdin(&mut vm.kernel, child)
8485 }
8486
8487 fn kill_descendant_javascript_child_process(
8488 &mut self,
8489 vm_id: &str,
8490 process_id: &str,
8491 current_process_path: &[&str],
8492 child_process_id: &str,
8493 signal: &str,
8494 ) -> Result<(), SidecarError> {
8495 let signal_name = signal.to_owned();
8496 let signal = parse_signal(signal)?;
8497 let Some(vm) = self.vms.get_mut(vm_id) else {
8498 return Ok(());
8499 };
8500 let Some(root) = vm.active_processes.get_mut(process_id) else {
8501 return Ok(());
8502 };
8503 let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
8504 return Ok(());
8505 };
8506 let source_pid = parent.kernel_pid;
8507 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8508 return Ok(());
8509 };
8510 terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
8511 let child_process_label = if current_process_path.is_empty() {
8512 child_process_id.to_owned()
8513 } else {
8514 format!("{}/{}", current_process_path.join("/"), child_process_id)
8515 };
8516 emit_security_audit_event(
8517 &self.bridge,
8518 vm_id,
8519 "security.process.kill",
8520 audit_fields([
8521 (String::from("source"), String::from("guest_child_process")),
8522 (String::from("source_pid"), source_pid.to_string()),
8523 (String::from("target_pid"), child.kernel_pid.to_string()),
8524 (String::from("process_id"), process_id.to_owned()),
8525 (String::from("child_process_id"), child_process_label),
8526 (String::from("signal"), signal_name),
8527 ]),
8528 );
8529 Ok(())
8530 }
8531
8532 fn handle_descendant_process_kill_rpc(
8533 &mut self,
8534 vm_id: &str,
8535 process_id: &str,
8536 current_process_path: &[&str],
8537 child_process_id: &str,
8538 request: &JavascriptSyncRpcRequest,
8539 ) -> Result<Value, SidecarError> {
8540 let target_pid = javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
8541 let signal_name = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
8542 let signal = parse_signal(signal_name)?;
8543
8544 let mut source_path = current_process_path.to_vec();
8545 source_path.push(child_process_id);
8546
8547 if signal != 0 && target_pid < 0 {
8548 let pgid = target_pid.unsigned_abs();
8549 let caller_kernel_pid = {
8550 let Some(vm) = self.vms.get(vm_id) else {
8551 return Err(SidecarError::InvalidState(String::from(
8552 "ESRCH: unknown VM during process.kill",
8553 )));
8554 };
8555 let Some(root) = vm.active_processes.get(process_id) else {
8556 return Err(SidecarError::InvalidState(format!(
8557 "ESRCH: unknown process {process_id} during process.kill",
8558 )));
8559 };
8560 let Some(source) = Self::active_process_by_path(root, &source_path) else {
8561 return Err(SidecarError::InvalidState(format!(
8562 "ESRCH: unknown child process {child_process_id} during process.kill",
8563 )));
8564 };
8565 source.kernel_pid
8566 };
8567 let caller_is_member =
8568 self.signal_vm_process_group(vm_id, caller_kernel_pid, pgid, signal_name)?;
8569 if !caller_is_member {
8570 return Ok(Value::Null);
8571 }
8572 let Some(vm) = self.vms.get_mut(vm_id) else {
8573 return Ok(Value::Null);
8574 };
8575 let Some(root) = vm.active_processes.get_mut(process_id) else {
8576 return Ok(Value::Null);
8577 };
8578 let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
8579 return Ok(Value::Null);
8580 };
8581 source.pending_self_signal_exit = None;
8582 if !matches!(
8583 canonical_signal_name(signal),
8584 Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
8585 ) {
8586 source.pending_self_signal_exit = Some(signal);
8587 }
8588 return Ok(json!({
8589 "self": true,
8590 "action": "default",
8591 }));
8592 }
8593
8594 let Some(vm) = self.vms.get_mut(vm_id) else {
8595 return Err(SidecarError::InvalidState(String::from(
8596 "ESRCH: unknown VM during process.kill",
8597 )));
8598 };
8599
8600 if signal == 0 {
8601 vm.kernel
8602 .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
8603 .map_err(kernel_error)?;
8604 return Ok(Value::Null);
8605 }
8606
8607 let target_kernel_pid = u32::try_from(target_pid).map_err(|_| {
8608 SidecarError::InvalidState(format!("EINVAL: invalid process pid {target_pid}"))
8609 })?;
8610 let (source_pid, located_target_path) = {
8611 let Some(root) = vm.active_processes.get(process_id) else {
8612 return Err(SidecarError::InvalidState(format!(
8613 "ESRCH: unknown process {process_id} during process.kill",
8614 )));
8615 };
8616 let Some(source) = Self::active_process_by_path(root, &source_path) else {
8617 return Err(SidecarError::InvalidState(format!(
8618 "ESRCH: unknown child process {child_process_id} during process.kill",
8619 )));
8620 };
8621 vm.kernel
8622 .signal_process(EXECUTION_DRIVER_NAME, target_pid, 0)
8623 .map_err(kernel_error)?;
8624 (
8625 source.kernel_pid,
8626 Self::active_process_path_by_kernel_pid(root, target_kernel_pid),
8627 )
8628 };
8629 let Some(target_path) = located_target_path else {
8630 self.signal_vm_kernel_pid(vm_id, target_kernel_pid, signal_name)?;
8634 return Ok(Value::Null);
8635 };
8636 let Some(vm) = self.vms.get_mut(vm_id) else {
8637 return Err(SidecarError::InvalidState(String::from(
8638 "ESRCH: unknown VM during process.kill",
8639 )));
8640 };
8641
8642 if source_pid == target_kernel_pid {
8643 let Some(root) = vm.active_processes.get_mut(process_id) else {
8644 return Ok(Value::Null);
8645 };
8646 let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
8647 return Ok(Value::Null);
8648 };
8649 source.pending_self_signal_exit = None;
8650 if !matches!(
8651 canonical_signal_name(signal),
8652 Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
8653 ) {
8654 source.pending_self_signal_exit = Some(signal);
8655 }
8656 return Ok(json!({
8657 "self": true,
8658 "action": "default",
8659 }));
8660 }
8661
8662 let signal_key = target_path.last().map(String::as_str).unwrap_or(process_id);
8663 let registration = vm
8664 .signal_states
8665 .get(signal_key)
8666 .and_then(|handlers| handlers.get(&(signal as u32)))
8667 .cloned();
8668
8669 let action = match registration
8670 .as_ref()
8671 .map(|registration| ®istration.action)
8672 {
8673 Some(SignalDispositionAction::Ignore) => "ignore",
8674 Some(SignalDispositionAction::User) => {
8675 let Some(root) = vm.active_processes.get_mut(process_id) else {
8676 return Ok(Value::Null);
8677 };
8678 let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
8679 else {
8680 return Err(SidecarError::InvalidState(format!(
8681 "ESRCH: unknown process pid {target_pid}"
8682 )));
8683 };
8684 if let Some(session) = target.execution.javascript_v8_session_handle().filter(
8685 |_| matches!(&target.execution, ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime())
8686 || matches!(&target.execution, ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime()),
8687 ) {
8688 dispatch_v8_session_signal_async(session, signal);
8689 } else if !dispatch_v8_process_signal(target, signal)? {
8690 return Err(SidecarError::InvalidState(format!(
8691 "unsupported guest signal delivery for pid {target_pid}"
8692 )));
8693 }
8694 "user"
8695 }
8696 Some(SignalDispositionAction::Default) | None
8697 if matches!(
8698 canonical_signal_name(signal),
8699 Some("SIGWINCH" | "SIGCHLD" | "SIGURG")
8700 ) =>
8701 {
8702 "ignore"
8703 }
8704 Some(SignalDispositionAction::Default) | None => {
8705 let Some(root) = vm.active_processes.get_mut(process_id) else {
8706 return Ok(Value::Null);
8707 };
8708 let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
8709 else {
8710 return Err(SidecarError::InvalidState(format!(
8711 "ESRCH: unknown process pid {target_pid}"
8712 )));
8713 };
8714 apply_active_process_default_signal(&mut vm.kernel, target, signal)?;
8715 "default"
8716 }
8717 };
8718
8719 let target_path_label = Self::child_process_path_label(
8720 process_id,
8721 &target_path.iter().map(String::as_str).collect::<Vec<_>>(),
8722 );
8723 emit_security_audit_event(
8724 &self.bridge,
8725 vm_id,
8726 "security.process.kill",
8727 audit_fields([
8728 (String::from("source"), String::from("guest_process")),
8729 (String::from("source_pid"), source_pid.to_string()),
8730 (String::from("target_pid"), target_pid.to_string()),
8731 (String::from("process_id"), process_id.to_owned()),
8732 (
8733 String::from("target_process_path"),
8734 target_path_label.clone(),
8735 ),
8736 (String::from("signal"), signal_name.to_owned()),
8737 ]),
8738 );
8739
8740 Ok(json!({
8741 "self": false,
8742 "action": action,
8743 "signal": signal_name,
8744 "number": signal,
8745 "targetProcessPath": target_path_label,
8746 }))
8747 }
8748
8749 pub(crate) fn poll_javascript_child_process(
8750 &mut self,
8751 vm_id: &str,
8752 process_id: &str,
8753 child_process_id: &str,
8754 wait_ms: u64,
8755 ) -> Result<Value, SidecarError> {
8756 self.poll_descendant_javascript_child_process(
8757 vm_id,
8758 process_id,
8759 &[],
8760 child_process_id,
8761 wait_ms,
8762 )
8763 }
8764
8765 pub(crate) fn write_javascript_child_process_stdin(
8766 &mut self,
8767 vm_id: &str,
8768 process_id: &str,
8769 child_process_id: &str,
8770 chunk: &[u8],
8771 ) -> Result<(), SidecarError> {
8772 let Some(vm) = self.vms.get_mut(vm_id) else {
8773 return Err(javascript_child_process_gone_error(
8774 process_id,
8775 &[child_process_id],
8776 ));
8777 };
8778 let Some(child) = vm
8779 .active_processes
8780 .get_mut(process_id)
8781 .ok_or_else(|| missing_process_error(vm_id, process_id))?
8782 .child_processes
8783 .get_mut(child_process_id)
8784 else {
8785 return Err(javascript_child_process_gone_error(
8786 process_id,
8787 &[child_process_id],
8788 ));
8789 };
8790 if let Err(error) = child.execution.write_stdin(chunk) {
8791 if is_broken_pipe_error(&error) {
8792 return Ok(());
8793 }
8794 return Err(error);
8795 }
8796 write_kernel_process_stdin(&mut vm.kernel, child, chunk)
8797 }
8798
8799 pub(crate) fn close_javascript_child_process_stdin(
8800 &mut self,
8801 vm_id: &str,
8802 process_id: &str,
8803 child_process_id: &str,
8804 ) -> Result<(), SidecarError> {
8805 let Some(vm) = self.vms.get_mut(vm_id) else {
8806 return Err(javascript_child_process_gone_error(
8807 process_id,
8808 &[child_process_id],
8809 ));
8810 };
8811 let Some(child) = vm
8812 .active_processes
8813 .get_mut(process_id)
8814 .ok_or_else(|| missing_process_error(vm_id, process_id))?
8815 .child_processes
8816 .get_mut(child_process_id)
8817 else {
8818 return Err(javascript_child_process_gone_error(
8819 process_id,
8820 &[child_process_id],
8821 ));
8822 };
8823 child.execution.close_stdin()?;
8824 close_kernel_process_stdin(&mut vm.kernel, child)
8825 }
8826
8827 pub(crate) fn kill_javascript_child_process(
8828 &mut self,
8829 vm_id: &str,
8830 process_id: &str,
8831 child_process_id: &str,
8832 signal: &str,
8833 ) -> Result<(), SidecarError> {
8834 let signal_name = signal.to_owned();
8835 let signal = parse_signal(signal)?;
8836 let Some(vm) = self.vms.get_mut(vm_id) else {
8837 return Ok(());
8838 };
8839 let process = vm
8840 .active_processes
8841 .get_mut(process_id)
8842 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
8843 let source_pid = process.kernel_pid;
8844 let child = process
8845 .child_processes
8846 .get_mut(child_process_id)
8847 .ok_or_else(|| {
8848 SidecarError::InvalidState(format!(
8849 "unknown child process {child_process_id} during kill"
8850 ))
8851 })?;
8852 terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
8853 emit_security_audit_event(
8854 &self.bridge,
8855 vm_id,
8856 "security.process.kill",
8857 audit_fields([
8858 (String::from("source"), String::from("guest_child_process")),
8859 (String::from("source_pid"), source_pid.to_string()),
8860 (String::from("target_pid"), child.kernel_pid.to_string()),
8861 (String::from("process_id"), process_id.to_owned()),
8862 (
8863 String::from("child_process_id"),
8864 child_process_id.to_owned(),
8865 ),
8866 (String::from("signal"), signal_name),
8867 ]),
8868 );
8869 Ok(())
8870 }
8871
8872 pub(crate) fn signal_vm_kernel_pid(
8878 &mut self,
8879 vm_id: &str,
8880 target_kernel_pid: u32,
8881 signal_name: &str,
8882 ) -> Result<(), SidecarError> {
8883 let signal = parse_signal(signal_name)?;
8884 let located = {
8885 let Some(vm) = self.vms.get(vm_id) else {
8886 return Err(SidecarError::InvalidState(String::from(
8887 "ESRCH: unknown VM during process.kill",
8888 )));
8889 };
8890 let alive = vm
8891 .kernel
8892 .list_processes()
8893 .get(&target_kernel_pid)
8894 .is_some_and(|info| info.status != ProcessStatus::Exited);
8895 if !alive {
8896 return Err(SidecarError::InvalidState(format!(
8897 "ESRCH: no such process {target_kernel_pid}"
8898 )));
8899 }
8900 vm.active_processes.iter().find_map(|(process_id, root)| {
8901 Self::active_process_path_by_kernel_pid(root, target_kernel_pid)
8902 .map(|path| (process_id.clone(), path))
8903 })
8904 };
8905
8906 match located {
8907 Some((process_id, path)) if path.is_empty() => {
8908 self.kill_process_internal(vm_id, &process_id, signal_name)
8909 }
8910 Some((process_id, path)) => {
8911 let Some(vm) = self.vms.get_mut(vm_id) else {
8912 return Ok(());
8913 };
8914 let Some(root) = vm.active_processes.get_mut(&process_id) else {
8915 return Ok(());
8916 };
8917 let Some(target) = Self::active_process_by_owned_path_mut(root, &path) else {
8918 return Err(SidecarError::InvalidState(format!(
8919 "ESRCH: no such process {target_kernel_pid}"
8920 )));
8921 };
8922 terminate_tracked_child_process_for_signal(&mut vm.kernel, target, signal)?;
8923 emit_security_audit_event(
8924 &self.bridge,
8925 vm_id,
8926 "security.process.kill",
8927 audit_fields([
8928 (String::from("source"), String::from("guest_process")),
8929 (String::from("target_pid"), target_kernel_pid.to_string()),
8930 (String::from("process_id"), process_id),
8931 (String::from("signal"), signal_name.to_owned()),
8932 ]),
8933 );
8934 Ok(())
8935 }
8936 None => {
8937 let Some(vm) = self.vms.get_mut(vm_id) else {
8938 return Ok(());
8939 };
8940 let target_pid = i32::try_from(target_kernel_pid).map_err(|_| {
8941 SidecarError::InvalidState(format!(
8942 "EINVAL: invalid process pid {target_kernel_pid}"
8943 ))
8944 })?;
8945 vm.kernel
8946 .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
8947 .map_err(kernel_error)?;
8948 emit_security_audit_event(
8949 &self.bridge,
8950 vm_id,
8951 "security.process.kill",
8952 audit_fields([
8953 (String::from("source"), String::from("guest_process")),
8954 (String::from("target_pid"), target_kernel_pid.to_string()),
8955 (String::from("signal"), signal_name.to_owned()),
8956 ]),
8957 );
8958 Ok(())
8959 }
8960 }
8961 }
8962
8963 pub(crate) fn signal_vm_process_group(
8968 &mut self,
8969 vm_id: &str,
8970 caller_kernel_pid: u32,
8971 pgid: u32,
8972 signal_name: &str,
8973 ) -> Result<bool, SidecarError> {
8974 parse_signal(signal_name)?;
8975 let members = {
8976 let Some(vm) = self.vms.get(vm_id) else {
8977 return Err(SidecarError::InvalidState(String::from(
8978 "ESRCH: unknown VM during process.kill",
8979 )));
8980 };
8981 vm.kernel
8982 .list_processes()
8983 .into_iter()
8984 .filter(|(_, info)| info.pgid == pgid && info.status != ProcessStatus::Exited)
8985 .map(|(pid, _)| pid)
8986 .collect::<Vec<_>>()
8987 };
8988 if members.is_empty() {
8989 return Err(SidecarError::InvalidState(format!(
8990 "ESRCH: no such process group {pgid}"
8991 )));
8992 }
8993
8994 let mut caller_is_member = false;
8995 for member_pid in members {
8996 if member_pid == caller_kernel_pid {
8997 caller_is_member = true;
8998 continue;
8999 }
9000 match self.signal_vm_kernel_pid(vm_id, member_pid, signal_name) {
9001 Ok(()) => {}
9002 Err(error) if sidecar_error_is_esrch(&error) => {}
9005 Err(error) => return Err(error),
9006 }
9007 }
9008 Ok(caller_is_member)
9009 }
9010}
9011
9012fn terminate_tracked_child_process_for_signal(
9017 kernel: &mut SidecarKernel,
9018 child: &mut ActiveProcess,
9019 signal: i32,
9020) -> Result<(), SidecarError> {
9021 let should_terminate_shared_runtime = child.execution.uses_shared_v8_runtime()
9022 && signal != 0
9023 && !matches!(
9024 signal,
9025 libc::SIGHUP
9026 | libc::SIGINT
9027 | libc::SIGTERM
9028 | libc::SIGCHLD
9029 | libc::SIGWINCH
9030 | libc::SIGSTOP
9031 | libc::SIGCONT
9032 );
9033 if should_terminate_shared_runtime {
9034 child.execution.terminate()?;
9035 child.pending_self_signal_exit = Some(signal);
9036 child.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
9037 } else {
9038 kernel
9039 .kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, signal)
9040 .map_err(kernel_error)?;
9041 }
9042 Ok(())
9043}
9044
9045fn sidecar_error_is_esrch(error: &SidecarError) -> bool {
9046 error.to_string().contains("ESRCH")
9047}
9048
9049fn apply_active_process_default_signal(
9050 kernel: &mut SidecarKernel,
9051 process: &mut ActiveProcess,
9052 signal: i32,
9053) -> Result<(), SidecarError> {
9054 if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
9055 return kernel
9056 .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
9057 .map_err(kernel_error);
9058 }
9059
9060 if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
9061 close_kernel_process_stdin(kernel, process)?;
9062 }
9063
9064 if process.execution.uses_shared_v8_runtime() {
9065 process.execution.terminate()?;
9066 if signal != 0 && matches!(process.execution, ActiveExecution::Wasm(_)) {
9067 process.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
9068 }
9069 return Ok(());
9070 }
9071
9072 kernel
9073 .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
9074 .map_err(kernel_error)
9075}
9076
9077fn map_wasm_signal_registration(
9078 registration: secure_exec_execution::wasm::WasmSignalHandlerRegistration,
9079) -> SignalHandlerRegistration {
9080 SignalHandlerRegistration {
9081 action: match registration.action {
9082 secure_exec_execution::wasm::WasmSignalDispositionAction::Default => {
9083 crate::protocol::SignalDispositionAction::Default
9084 }
9085 secure_exec_execution::wasm::WasmSignalDispositionAction::Ignore => {
9086 crate::protocol::SignalDispositionAction::Ignore
9087 }
9088 secure_exec_execution::wasm::WasmSignalDispositionAction::User => {
9089 crate::protocol::SignalDispositionAction::User
9090 }
9091 },
9092 mask: registration.mask,
9093 flags: registration.flags,
9094 }
9095}
9096
9097fn map_node_signal_registration(
9098 registration: NodeSignalHandlerRegistration,
9099) -> SignalHandlerRegistration {
9100 SignalHandlerRegistration {
9101 action: match registration.action {
9102 NodeSignalDispositionAction::Default => SignalDispositionAction::Default,
9103 NodeSignalDispositionAction::Ignore => SignalDispositionAction::Ignore,
9104 NodeSignalDispositionAction::User => SignalDispositionAction::User,
9105 },
9106 mask: registration.mask,
9107 flags: registration.flags,
9108 }
9109}
9110
9111fn javascript_child_process_sync_input_bytes(
9112 value: Option<&Value>,
9113) -> Result<Option<Vec<u8>>, SidecarError> {
9114 let Some(value) = value else {
9115 return Ok(None);
9116 };
9117
9118 match value {
9119 Value::Null => Ok(None),
9120 Value::String(text) => Ok(Some(text.as_bytes().to_vec())),
9121 other => javascript_sync_rpc_bytes_arg(
9122 std::slice::from_ref(other),
9123 0,
9124 "child_process.spawn_sync input",
9125 )
9126 .map(Some),
9127 }
9128}
9129
9130fn resolve_execute_request(
9135 vm: &VmState,
9136 payload: &ExecuteRequest,
9137) -> Result<ResolvedChildProcessExecution, SidecarError> {
9138 let payload_env: BTreeMap<String, String> = payload
9139 .env
9140 .iter()
9141 .map(|(k, v)| (k.clone(), v.clone()))
9142 .collect();
9143 if let Some(command) = payload.command.as_deref() {
9144 return resolve_command_execution(
9145 vm,
9146 command,
9147 &payload.args,
9148 &payload_env,
9149 payload.cwd.as_deref(),
9150 payload.wasm_permission_tier,
9151 );
9152 }
9153
9154 let runtime = payload.runtime.clone().ok_or_else(|| {
9155 SidecarError::InvalidState(String::from("execute requires either command or runtime"))
9156 })?;
9157 let entrypoint = payload.entrypoint.clone().ok_or_else(|| {
9158 SidecarError::InvalidState(String::from(
9159 "execute requires either command or entrypoint",
9160 ))
9161 })?;
9162 let (guest_cwd, host_cwd, allow_host_path_overrides) =
9163 resolve_execution_cwds(vm, payload.cwd.as_deref());
9164 let mut env = vm.guest_env.clone();
9165 env.extend(payload_env.clone());
9166
9167 let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint);
9168 if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
9169 let requested_cwd = payload.cwd.as_deref().unwrap_or(guest_cwd.as_str());
9170 return Err(SidecarError::InvalidState(format!(
9171 "execution cwd {requested_cwd} is outside sandbox root {}",
9172 vm.host_cwd.to_string_lossy()
9173 )));
9174 }
9175 let host_entrypoint_override = allow_host_path_overrides
9176 .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint))
9177 .flatten();
9178
9179 let guest_entrypoint = host_entrypoint_override
9180 .as_ref()
9181 .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
9182 .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, &entrypoint));
9183 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
9184
9185 Ok(ResolvedChildProcessExecution {
9186 command: match runtime {
9187 GuestRuntimeKind::JavaScript => String::from(JAVASCRIPT_COMMAND),
9188 GuestRuntimeKind::Python => String::from(PYTHON_COMMAND),
9189 GuestRuntimeKind::WebAssembly => String::from(WASM_COMMAND),
9190 },
9191 process_args: std::iter::once(entrypoint.clone())
9192 .chain(payload.args.iter().cloned())
9193 .collect(),
9194 runtime,
9195 entrypoint: host_entrypoint_override
9196 .map(|(_, host_entrypoint)| host_entrypoint)
9197 .unwrap_or(entrypoint),
9198 execution_args: payload.args.clone(),
9199 env,
9200 guest_cwd,
9201 host_cwd,
9202 wasm_permission_tier: payload.wasm_permission_tier,
9203 tool_command: false,
9204 })
9205}
9206
9207fn resolve_command_execution(
9208 vm: &VmState,
9209 command: &str,
9210 args: &[String],
9211 extra_env: &BTreeMap<String, String>,
9212 cwd: Option<&str>,
9213 explicit_wasm_permission_tier: Option<WasmPermissionTier>,
9214) -> Result<ResolvedChildProcessExecution, SidecarError> {
9215 let (guest_cwd, host_cwd, allow_host_path_overrides) = resolve_execution_cwds(vm, cwd);
9216 let mut env = vm.guest_env.clone();
9217 env.extend(extra_env.clone());
9218 let args = apply_shell_cwd_prefix(command, args.to_vec(), &guest_cwd);
9219
9220 if is_tool_command(vm, command) {
9221 let command = normalized_tool_command_name(command).unwrap_or_else(|| command.to_owned());
9222 return Ok(ResolvedChildProcessExecution {
9223 command: command.clone(),
9224 process_args: std::iter::once(command.clone())
9225 .chain(args.iter().cloned())
9226 .collect(),
9227 runtime: GuestRuntimeKind::JavaScript,
9228 entrypoint: command,
9229 execution_args: args,
9230 env,
9231 guest_cwd,
9232 host_cwd,
9233 wasm_permission_tier: None,
9234 tool_command: true,
9235 });
9236 }
9237
9238 if is_python_runtime_command(command) {
9239 return resolve_python_command_execution(vm, command, &args, env, guest_cwd, host_cwd);
9240 }
9241
9242 if is_node_runtime_command(command) {
9243 if let Some(cli) = resolve_host_node_cli_entrypoint(command) {
9244 env.insert(
9245 String::from("AGENTOS_NODE_EVAL"),
9246 build_host_node_cli_eval(&cli),
9247 );
9248 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
9249 add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
9250 add_runtime_host_access_path(
9251 &mut env,
9252 "AGENTOS_EXTRA_FS_READ_PATHS",
9253 &cli.package_root,
9254 true,
9255 );
9256
9257 return Ok(ResolvedChildProcessExecution {
9258 command: String::from(JAVASCRIPT_COMMAND),
9259 process_args: std::iter::once(command.to_owned())
9260 .chain(args.iter().cloned())
9261 .collect(),
9262 runtime: GuestRuntimeKind::JavaScript,
9263 entrypoint: String::from("-e"),
9264 execution_args: std::iter::once(cli.guest_entrypoint.clone())
9265 .chain(args.iter().cloned())
9266 .collect(),
9267 env,
9268 guest_cwd,
9269 host_cwd,
9270 wasm_permission_tier: None,
9271 tool_command: false,
9272 });
9273 }
9274
9275 if args.is_empty() {
9276 env.insert(String::from("AGENTOS_NODE_EVAL"), String::new());
9277 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
9278
9279 return Ok(ResolvedChildProcessExecution {
9280 command: String::from(JAVASCRIPT_COMMAND),
9281 process_args: vec![command.to_owned()],
9282 runtime: GuestRuntimeKind::JavaScript,
9283 entrypoint: String::from("-e"),
9284 execution_args: Vec::new(),
9285 env,
9286 guest_cwd,
9287 host_cwd,
9288 wasm_permission_tier: None,
9289 tool_command: false,
9290 });
9291 }
9292
9293 if let Some((entrypoint, execution_args)) =
9294 resolve_special_node_cli_invocation(&args, &mut env)
9295 {
9296 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
9297
9298 return Ok(ResolvedChildProcessExecution {
9299 command: String::from(JAVASCRIPT_COMMAND),
9300 process_args: std::iter::once(command.to_owned())
9301 .chain(args.iter().cloned())
9302 .collect(),
9303 runtime: GuestRuntimeKind::JavaScript,
9304 entrypoint,
9305 execution_args,
9306 env,
9307 guest_cwd,
9308 host_cwd,
9309 wasm_permission_tier: None,
9310 tool_command: false,
9311 });
9312 }
9313
9314 let Some(entrypoint_specifier) = args.first() else {
9315 return Err(SidecarError::InvalidState(format!(
9316 "{command} execution requires an entrypoint"
9317 )));
9318 };
9319
9320 let (entrypoint, execution_args, guest_entrypoint) = {
9321 let requested_host_entrypoint =
9322 resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier);
9323 if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
9324 let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
9325 return Err(SidecarError::InvalidState(format!(
9326 "execution cwd {requested_cwd} is outside sandbox root {}",
9327 vm.host_cwd.to_string_lossy()
9328 )));
9329 }
9330 let host_entrypoint_override = allow_host_path_overrides
9331 .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier))
9332 .flatten();
9333 let guest_entrypoint = host_entrypoint_override
9334 .as_ref()
9335 .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
9336 .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, entrypoint_specifier));
9337 let entrypoint = host_entrypoint_override.map_or_else(
9338 || {
9339 guest_entrypoint.as_ref().map_or_else(
9340 || entrypoint_specifier.clone(),
9341 |guest_entrypoint| {
9342 resolve_vm_guest_path_to_host(vm, guest_entrypoint)
9343 .to_string_lossy()
9344 .into_owned()
9345 },
9346 )
9347 },
9348 |(_, host_entrypoint)| host_entrypoint,
9349 );
9350 (
9351 entrypoint,
9352 args.iter().skip(1).cloned().collect(),
9353 guest_entrypoint,
9354 )
9355 };
9356
9357 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
9358
9359 return Ok(ResolvedChildProcessExecution {
9360 command: String::from(JAVASCRIPT_COMMAND),
9361 process_args: std::iter::once(command.to_owned())
9362 .chain(args.iter().cloned())
9363 .collect(),
9364 runtime: GuestRuntimeKind::JavaScript,
9365 entrypoint,
9366 execution_args,
9367 env,
9368 guest_cwd,
9369 host_cwd,
9370 wasm_permission_tier: None,
9371 tool_command: false,
9372 });
9373 }
9374
9375 if command.ends_with(".js") || command.ends_with(".mjs") || command.ends_with(".cjs") {
9376 let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, command);
9377 if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
9378 let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
9379 return Err(SidecarError::InvalidState(format!(
9380 "execution cwd {requested_cwd} is outside sandbox root {}",
9381 vm.host_cwd.to_string_lossy()
9382 )));
9383 }
9384 let host_entrypoint_override = allow_host_path_overrides
9385 .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, command))
9386 .flatten();
9387 let guest_entrypoint = host_entrypoint_override
9388 .as_ref()
9389 .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
9390 .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, command));
9391 let entrypoint = host_entrypoint_override.map_or_else(
9392 || {
9393 guest_entrypoint.as_ref().map_or_else(
9394 || command.to_owned(),
9395 |guest_entrypoint| {
9396 resolve_vm_guest_path_to_host(vm, guest_entrypoint)
9397 .to_string_lossy()
9398 .into_owned()
9399 },
9400 )
9401 },
9402 |(_, host_entrypoint)| host_entrypoint,
9403 );
9404 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
9405
9406 return Ok(ResolvedChildProcessExecution {
9407 command: String::from(JAVASCRIPT_COMMAND),
9408 process_args: std::iter::once(command.to_owned())
9409 .chain(args.iter().cloned())
9410 .collect(),
9411 runtime: GuestRuntimeKind::JavaScript,
9412 entrypoint,
9413 execution_args: args.to_vec(),
9414 env,
9415 guest_cwd,
9416 host_cwd,
9417 wasm_permission_tier: None,
9418 tool_command: false,
9419 });
9420 }
9421
9422 let guest_entrypoint = resolve_guest_command_entrypoint(
9423 vm,
9424 &guest_cwd,
9425 command,
9426 env.get("PATH").map(String::as_str),
9427 )
9428 .ok_or_else(|| {
9429 SidecarError::InvalidState(format!(
9430 "command not found on native sidecar path: {command}"
9431 ))
9432 })?;
9433 let wasm_permission_tier = explicit_wasm_permission_tier
9434 .or_else(|| vm.command_permissions.get(command).copied())
9435 .or_else(|| {
9436 Path::new(&guest_entrypoint)
9437 .file_name()
9438 .and_then(|name| name.to_str())
9439 .and_then(|name| vm.command_permissions.get(name).copied())
9440 });
9441
9442 let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
9443 if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
9444 resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
9445 {
9446 prepare_guest_runtime_env(
9447 vm,
9448 &mut env,
9449 &guest_cwd,
9450 &host_cwd,
9451 Some(javascript_guest_entrypoint),
9452 )?;
9453
9454 return Ok(ResolvedChildProcessExecution {
9455 command: command.to_owned(),
9456 process_args: std::iter::once(command.to_owned())
9457 .chain(args.iter().cloned())
9458 .collect(),
9459 runtime: GuestRuntimeKind::JavaScript,
9460 entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
9461 execution_args: args.to_vec(),
9462 env,
9463 guest_cwd,
9464 host_cwd,
9465 wasm_permission_tier: None,
9466 tool_command: false,
9467 });
9468 }
9469 prepare_guest_runtime_env(
9470 vm,
9471 &mut env,
9472 &guest_cwd,
9473 &host_cwd,
9474 Some(guest_entrypoint.clone()),
9475 )?;
9476
9477 Ok(ResolvedChildProcessExecution {
9478 command: command.to_owned(),
9479 process_args: std::iter::once(command.to_owned())
9480 .chain(args.iter().cloned())
9481 .collect(),
9482 runtime: GuestRuntimeKind::WebAssembly,
9483 entrypoint: host_entrypoint.to_string_lossy().into_owned(),
9484 execution_args: args.to_vec(),
9485 env,
9486 guest_cwd,
9487 host_cwd,
9488 wasm_permission_tier,
9489 tool_command: false,
9490 })
9491}
9492
9493const MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH: usize = 4;
9494
9495fn resolve_javascript_command_entrypoint(
9496 vm: &VmState,
9497 guest_entrypoint: &str,
9498 host_entrypoint: &Path,
9499) -> Option<(String, PathBuf)> {
9500 resolve_javascript_command_entrypoint_inner(
9501 vm,
9502 guest_entrypoint,
9503 host_entrypoint,
9504 MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH,
9505 )
9506}
9507
9508fn resolve_javascript_command_entrypoint_inner(
9509 vm: &VmState,
9510 guest_entrypoint: &str,
9511 host_entrypoint: &Path,
9512 redirects_remaining: usize,
9513) -> Option<(String, PathBuf)> {
9514 if redirects_remaining > 0 {
9515 let symlink_target = fs::symlink_metadata(host_entrypoint)
9516 .ok()
9517 .filter(|metadata| metadata.file_type().is_symlink())
9518 .and_then(|_| fs::read_link(host_entrypoint).ok());
9519 if let Some(symlink_target) = symlink_target {
9520 let guest_parent = Path::new(guest_entrypoint)
9521 .parent()
9522 .and_then(|path| path.to_str())
9523 .unwrap_or("/");
9524 let symlink_guest_entrypoint = if symlink_target.is_absolute() {
9525 normalize_path(&symlink_target.to_string_lossy())
9526 } else {
9527 normalize_path(&format!(
9528 "{guest_parent}/{}",
9529 symlink_target.to_string_lossy().replace('\\', "/")
9530 ))
9531 };
9532 let symlink_host_entrypoint =
9533 resolve_vm_guest_path_to_host(vm, &symlink_guest_entrypoint);
9534 return resolve_javascript_command_entrypoint_inner(
9535 vm,
9536 &symlink_guest_entrypoint,
9537 &symlink_host_entrypoint,
9538 redirects_remaining - 1,
9539 );
9540 }
9541 }
9542
9543 let script = load_executable_script_preview(host_entrypoint)?;
9544 let interpreter = parse_script_interpreter_name(&script);
9545
9546 if interpreter.is_none() && is_probable_javascript_entrypoint(host_entrypoint, &script) {
9547 return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
9548 }
9549
9550 let interpreter = interpreter?;
9551 if interpreter == "node" {
9552 return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
9553 }
9554
9555 if redirects_remaining == 0 || !matches!(interpreter.as_str(), "sh" | "bash" | "dash") {
9556 return None;
9557 }
9558
9559 let shim_target = parse_node_shell_shim_target(&script)?;
9560 let guest_parent = Path::new(guest_entrypoint)
9561 .parent()
9562 .and_then(|path| path.to_str())
9563 .unwrap_or("/");
9564 let shim_guest_entrypoint = normalize_path(&format!("{guest_parent}/{shim_target}"));
9565 let shim_host_entrypoint = resolve_vm_guest_path_to_host(vm, &shim_guest_entrypoint);
9566 resolve_javascript_command_entrypoint_inner(
9567 vm,
9568 &shim_guest_entrypoint,
9569 &shim_host_entrypoint,
9570 redirects_remaining - 1,
9571 )
9572}
9573
9574fn load_executable_script_preview(path: &Path) -> Option<String> {
9575 let bytes = fs::read(path).ok()?;
9576 let preview_len = bytes.len().min(16 * 1024);
9577 Some(String::from_utf8_lossy(&bytes[..preview_len]).into_owned())
9578}
9579
9580fn parse_script_interpreter_name(script: &str) -> Option<String> {
9581 let shebang = script.lines().next()?.strip_prefix("#!")?.trim();
9582 let mut tokens = shebang.split_whitespace();
9583 let command = tokens.next()?;
9584 let command_name = Path::new(command).file_name()?.to_str()?;
9585 if command_name == "env" {
9586 for token in tokens {
9587 if token.starts_with('-') {
9588 continue;
9589 }
9590 return Path::new(token)
9591 .file_name()
9592 .and_then(|name| name.to_str())
9593 .map(ToOwned::to_owned);
9594 }
9595 return None;
9596 }
9597
9598 Some(command_name.to_owned())
9599}
9600
9601fn parse_node_shell_shim_target(script: &str) -> Option<String> {
9602 for line in script.lines() {
9603 let trimmed = line.trim();
9604 if !trimmed.starts_with("exec ") {
9605 continue;
9606 }
9607
9608 let mut remaining = trimmed;
9609 while let Some(start) = remaining.find("\"$basedir/") {
9610 let after_prefix = &remaining[start + "\"$basedir/".len()..];
9611 let end = after_prefix.find('"')?;
9612 let candidate = &after_prefix[..end];
9613 remaining = &after_prefix[end + 1..];
9614
9615 if candidate.is_empty() || candidate == "node" || candidate.ends_with("/node") {
9616 continue;
9617 }
9618
9619 return Some(candidate.to_owned());
9620 }
9621 }
9622
9623 None
9624}
9625
9626fn is_probable_javascript_entrypoint(path: &Path, script: &str) -> bool {
9627 let extension = path
9628 .extension()
9629 .and_then(|value| value.to_str())
9630 .unwrap_or_default();
9631 if matches!(extension, "js" | "cjs" | "mjs") {
9632 return true;
9633 }
9634
9635 if !path
9636 .components()
9637 .any(|component| component.as_os_str() == "node_modules")
9638 {
9639 return false;
9640 }
9641
9642 let preview = script.trim_start_matches('\u{feff}').trim_start();
9643 !preview.is_empty()
9644 && !preview.starts_with("#!")
9645 && (preview.starts_with("\"use strict\"")
9646 || preview.starts_with("'use strict'")
9647 || preview.starts_with("import ")
9648 || preview.starts_with("export ")
9649 || preview.starts_with("const ")
9650 || preview.starts_with("let ")
9651 || preview.starts_with("var ")
9652 || preview.starts_with("Object.defineProperty(exports")
9653 || preview.starts_with("module.exports")
9654 || preview.starts_with("require("))
9655}
9656
9657fn resolve_guest_execution_cwd(vm: &VmState, value: Option<&str>) -> String {
9658 value
9659 .map(normalize_path)
9660 .unwrap_or_else(|| vm.guest_cwd.clone())
9661}
9662
9663fn resolve_execution_cwds(vm: &VmState, value: Option<&str>) -> (String, PathBuf, bool) {
9664 if let Some(raw_cwd) = value {
9665 let normalized_vm_host_cwd = normalize_host_path(&vm.host_cwd);
9666 let requested_host_cwd = normalize_host_path(Path::new(raw_cwd));
9667 if path_is_within_root(&requested_host_cwd, &normalized_vm_host_cwd) {
9668 let relative = requested_host_cwd
9669 .strip_prefix(&normalized_vm_host_cwd)
9670 .unwrap_or_else(|_| Path::new(""));
9671 let relative = relative.to_string_lossy().replace('\\', "/");
9672 let guest_cwd = if relative.is_empty() {
9673 String::from("/")
9674 } else {
9675 normalize_path(&format!("/{relative}"))
9676 };
9677 return (guest_cwd, requested_host_cwd, true);
9678 }
9679 }
9680
9681 let guest_cwd = resolve_guest_execution_cwd(vm, value);
9682 let host_cwd = if value.is_none() {
9683 vm.host_cwd.clone()
9684 } else {
9685 resolve_vm_guest_path_to_host(vm, &guest_cwd)
9686 };
9687 (guest_cwd, host_cwd, value.is_none())
9688}
9689
9690fn resolve_vm_guest_path_to_host(vm: &VmState, guest_path: &str) -> PathBuf {
9691 host_mount_path_for_guest_path(vm, guest_path)
9692 .unwrap_or_else(|| shadow_path_for_guest(vm, guest_path))
9693}
9694
9695fn shadow_path_for_guest(vm: &VmState, guest_path: &str) -> PathBuf {
9696 let normalized = normalize_path(guest_path);
9697 let relative = normalized.trim_start_matches('/');
9698 if relative.is_empty() {
9699 return vm.cwd.clone();
9700 }
9701 vm.cwd.join(relative)
9702}
9703
9704fn apply_shell_cwd_prefix(command: &str, mut args: Vec<String>, guest_cwd: &str) -> Vec<String> {
9705 if guest_cwd == "/" || !is_shell_command(command) {
9706 return args;
9707 }
9708
9709 let Some(flag) = args.first() else {
9710 return args;
9711 };
9712 if !matches!(flag.as_str(), "-c" | "-lc") || args.len() < 2 {
9713 return args;
9714 }
9715
9716 let command_text = args[1].clone();
9717 let quoted_cwd = shell_single_quote(guest_cwd);
9718 args[1] = format!("cd {quoted_cwd} && {command_text}");
9719 args
9720}
9721
9722fn is_shell_command(command: &str) -> bool {
9723 Path::new(command)
9724 .file_name()
9725 .and_then(|name| name.to_str())
9726 .unwrap_or(command)
9727 .trim_end_matches(".exe")
9728 .eq("sh")
9729 || Path::new(command)
9730 .file_name()
9731 .and_then(|name| name.to_str())
9732 .unwrap_or(command)
9733 .trim_end_matches(".exe")
9734 .eq("bash")
9735}
9736
9737fn shell_single_quote(value: &str) -> String {
9738 if value.is_empty() {
9739 return String::from("''");
9740 }
9741 format!("'{}'", value.replace('\'', "'\"'\"'"))
9742}
9743
9744pub(crate) fn sync_active_process_host_writes_to_kernel(
9745 vm: &mut VmState,
9746) -> Result<(), SidecarError> {
9747 if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
9748 let shadow_root = vm.cwd.clone();
9749 sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
9750 }
9751
9752 let normalized_vm_root = normalize_host_path(&vm.cwd);
9753 let extra_roots = collect_active_process_host_sync_roots(vm, &normalized_vm_root);
9754 for (host_cwd, guest_cwd) in extra_roots {
9755 sync_host_directory_tree_to_kernel(vm, &host_cwd, &guest_cwd)?;
9756 }
9757
9758 Ok(())
9759}
9760
9761fn collect_active_process_host_sync_roots(
9762 vm: &VmState,
9763 normalized_vm_root: &Path,
9764) -> Vec<(PathBuf, String)> {
9765 let mut roots = Vec::new();
9766 let mut seen = BTreeSet::new();
9767
9768 for process in vm.active_processes.values() {
9769 collect_process_host_sync_roots(process, normalized_vm_root, &mut seen, &mut roots);
9770 }
9771
9772 roots
9773}
9774
9775fn collect_process_host_sync_roots(
9776 process: &ActiveProcess,
9777 normalized_vm_root: &Path,
9778 seen: &mut BTreeSet<(PathBuf, String)>,
9779 roots: &mut Vec<(PathBuf, String)>,
9780) {
9781 let normalized_host_cwd = normalize_host_path(&process.host_cwd);
9782 if !path_is_within_root(&normalized_host_cwd, normalized_vm_root) {
9783 let guest_cwd = normalize_path(&process.guest_cwd);
9784 if seen.insert((normalized_host_cwd.clone(), guest_cwd.clone())) {
9785 roots.push((normalized_host_cwd, guest_cwd));
9786 }
9787 }
9788
9789 for child in process.child_processes.values() {
9790 collect_process_host_sync_roots(child, normalized_vm_root, seen, roots);
9791 }
9792}
9793
9794fn sync_process_host_writes_to_kernel(
9795 vm: &mut VmState,
9796 process: &ActiveProcess,
9797) -> Result<(), SidecarError> {
9798 if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
9799 let shadow_root = vm.cwd.clone();
9800 sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
9801 }
9802
9803 if !path_is_within_root(
9804 &normalize_host_path(&process.host_cwd),
9805 &normalize_host_path(&vm.cwd),
9806 ) {
9807 sync_host_directory_tree_to_kernel(vm, &process.host_cwd, &process.guest_cwd)?;
9808 }
9809
9810 Ok(())
9811}
9812
9813fn host_sync_root_is_filesystem_root(host_root: &Path) -> bool {
9814 normalize_host_path(host_root) == Path::new("/")
9815}
9816
9817fn sync_host_directory_tree_to_kernel(
9818 vm: &mut VmState,
9819 host_root: &Path,
9820 guest_root: &str,
9821) -> Result<(), SidecarError> {
9822 let normalized_host_root = normalize_host_path(host_root);
9823 let normalized_guest_root = normalize_path(guest_root);
9824 if host_sync_root_is_filesystem_root(host_root) {
9825 tracing::warn!("skipping host shadow sync rooted at the host filesystem root");
9830 return Ok(());
9831 }
9832 let mut synced_file_times = BTreeMap::new();
9833 sync_host_directory_tree_to_kernel_inner(
9834 vm,
9835 &normalized_host_root,
9836 &normalized_host_root,
9837 &normalized_guest_root,
9838 &mut synced_file_times,
9839 )
9840}
9841
9842fn sync_host_directory_tree_to_kernel_inner(
9843 vm: &mut VmState,
9844 host_root: &Path,
9845 current_host_dir: &Path,
9846 guest_root: &str,
9847 synced_file_times: &mut BTreeMap<(u64, u64), (u64, u64)>,
9848) -> Result<(), SidecarError> {
9849 let entries = match fs::read_dir(current_host_dir) {
9850 Ok(entries) => entries,
9851 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
9852 Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
9853 tracing::warn!(
9857 path = %current_host_dir.display(),
9858 "skipping unreadable host shadow directory"
9859 );
9860 return Ok(());
9861 }
9862 Err(error) => {
9863 return Err(SidecarError::Io(format!(
9864 "failed to read host shadow directory {}: {error}",
9865 current_host_dir.display()
9866 )));
9867 }
9868 };
9869
9870 for entry in entries {
9871 let entry = entry.map_err(|error| {
9872 SidecarError::Io(format!(
9873 "failed to read host shadow entry in {}: {error}",
9874 current_host_dir.display()
9875 ))
9876 })?;
9877 let host_path = entry.path();
9878 let file_type = entry.file_type().map_err(|error| {
9879 SidecarError::Io(format!(
9880 "failed to stat host shadow entry {}: {error}",
9881 host_path.display()
9882 ))
9883 })?;
9884 let relative_path = host_path
9885 .strip_prefix(host_root)
9886 .map_err(|error| {
9887 SidecarError::InvalidState(format!(
9888 "failed to relativize host shadow path {} against {}: {error}",
9889 host_path.display(),
9890 host_root.display()
9891 ))
9892 })?
9893 .to_string_lossy()
9894 .replace('\\', "/");
9895 let guest_path = if guest_root == "/" {
9896 normalize_path(&format!("/{relative_path}"))
9897 } else {
9898 normalize_path(&format!(
9899 "{}/{}",
9900 guest_root.trim_end_matches('/'),
9901 relative_path
9902 ))
9903 };
9904
9905 if should_skip_shadow_sync_path(vm, &guest_path) {
9906 continue;
9907 }
9908
9909 if file_type.is_dir() {
9910 let metadata = entry.metadata().map_err(|error| {
9911 SidecarError::Io(format!(
9912 "failed to read host shadow metadata {}: {error}",
9913 host_path.display()
9914 ))
9915 })?;
9916 if !is_shadow_bootstrap_dir(&guest_path)
9917 && !vm.kernel.exists(&guest_path).unwrap_or(false)
9918 {
9919 vm.kernel.mkdir(&guest_path, true).map_err(|error| {
9920 SidecarError::InvalidState(format!(
9921 "failed to sync host shadow directory {} to guest {}: {}",
9922 host_path.display(),
9923 guest_path,
9924 kernel_error(error)
9925 ))
9926 })?;
9927 vm.kernel
9928 .chmod(&guest_path, host_shadow_mode(&metadata))
9929 .map_err(|error| {
9930 SidecarError::InvalidState(format!(
9931 "failed to sync host shadow directory mode {} to guest {}: {}",
9932 host_path.display(),
9933 guest_path,
9934 kernel_error(error)
9935 ))
9936 })?;
9937 }
9938 sync_host_directory_tree_to_kernel_inner(
9939 vm,
9940 host_root,
9941 &host_path,
9942 guest_root,
9943 synced_file_times,
9944 )?;
9945 continue;
9946 }
9947
9948 if file_type.is_file() {
9949 let metadata = entry.metadata().map_err(|error| {
9950 SidecarError::Io(format!(
9951 "failed to read host shadow metadata {}: {error}",
9952 host_path.display()
9953 ))
9954 })?;
9955 let timestamp_key = (metadata.dev(), metadata.ino());
9956 let (atime_ms, mtime_ms) =
9957 *synced_file_times.entry(timestamp_key).or_insert_with(|| {
9958 (
9959 metadata_time_ms(metadata.atime(), metadata.atime_nsec()),
9960 metadata_time_ms(metadata.mtime(), metadata.mtime_nsec()),
9961 )
9962 });
9963 let desired_mode = host_shadow_mode(&metadata);
9964 if let Ok(existing) = vm.kernel.lstat(&guest_path) {
9985 if !existing.is_directory
9986 && !existing.is_symbolic_link
9987 && existing.size == metadata.len()
9988 && (existing.mode & 0o7777) == (desired_mode & 0o7777)
9989 && existing.mtime_ms == mtime_ms
9990 {
9991 continue;
9992 }
9993 }
9994 let bytes = match read_host_shadow_file(&host_path, desired_mode) {
9995 Ok(bytes) => bytes,
9996 Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
10001 Err(error)
10005 if error.kind() == std::io::ErrorKind::PermissionDenied
10006 || error.raw_os_error() == Some(libc::EPERM) =>
10007 {
10008 tracing::warn!(
10009 path = %host_path.display(),
10010 "skipping unreadable host shadow file"
10011 );
10012 continue;
10013 }
10014 Err(error) => {
10015 return Err(SidecarError::Io(format!(
10016 "failed to read host shadow file {}: {error}",
10017 host_path.display()
10018 )));
10019 }
10020 };
10021 match vm.kernel.write_file(&guest_path, bytes) {
10022 Ok(()) => {}
10023 Err(error) if error.code() == "ENOENT" => continue,
10028 Err(error) => {
10029 return Err(SidecarError::InvalidState(format!(
10030 "failed to sync host shadow file {} to guest {}: {}",
10031 host_path.display(),
10032 guest_path,
10033 kernel_error(error)
10034 )));
10035 }
10036 }
10037 vm.kernel
10038 .chmod(&guest_path, desired_mode)
10039 .map_err(|error| {
10040 SidecarError::InvalidState(format!(
10041 "failed to sync host shadow file mode {} to guest {}: {}",
10042 host_path.display(),
10043 guest_path,
10044 kernel_error(error)
10045 ))
10046 })?;
10047 vm.kernel
10048 .utimes(&guest_path, atime_ms, mtime_ms)
10049 .map_err(|error| {
10050 SidecarError::InvalidState(format!(
10051 "failed to sync host shadow file times {} to guest {}: {}",
10052 host_path.display(),
10053 guest_path,
10054 kernel_error(error)
10055 ))
10056 })?;
10057 continue;
10058 }
10059
10060 if file_type.is_symlink() {
10061 let target = match fs::read_link(&host_path) {
10062 Ok(target) => target,
10063 Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
10064 Err(error) => {
10065 return Err(SidecarError::Io(format!(
10066 "failed to read host shadow symlink {}: {error}",
10067 host_path.display()
10068 )));
10069 }
10070 };
10071 replace_kernel_symlink(vm, &guest_path, &target.to_string_lossy())?;
10072 }
10073 }
10074
10075 Ok(())
10076}
10077
10078fn replace_kernel_symlink(
10079 vm: &mut VmState,
10080 guest_path: &str,
10081 target: &str,
10082) -> Result<(), SidecarError> {
10083 if vm.kernel.symlink(target, guest_path).is_ok() {
10084 return Ok(());
10085 }
10086
10087 if let Ok(existing_target) = vm.kernel.read_link(guest_path) {
10088 if existing_target == target {
10089 return Ok(());
10090 }
10091 }
10092
10093 let _ = vm.kernel.remove_file(guest_path);
10094 let _ = vm.kernel.remove_dir(guest_path);
10095 vm.kernel
10096 .symlink(target, guest_path)
10097 .map_err(kernel_error)?;
10098 Ok(())
10099}
10100
10101fn host_shadow_mode(metadata: &fs::Metadata) -> u32 {
10102 metadata.permissions().mode() & 0o7777
10103}
10104
10105fn read_host_shadow_file(host_path: &Path, mode: u32) -> std::io::Result<Vec<u8>> {
10111 match fs::read(host_path) {
10112 Ok(bytes) => Ok(bytes),
10113 Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
10114 fs::set_permissions(host_path, fs::Permissions::from_mode(mode | 0o400))?;
10115 let result = fs::read(host_path);
10116 fs::set_permissions(host_path, fs::Permissions::from_mode(mode))?;
10117 result
10118 }
10119 Err(error) => Err(error),
10120 }
10121}
10122
10123fn metadata_time_ms(seconds: i64, nanos: i64) -> u64 {
10124 let seconds = seconds.max(0) as u64;
10125 let nanos = nanos.max(0) as u64;
10126 seconds
10127 .saturating_mul(1_000)
10128 .saturating_add(nanos / 1_000_000)
10129}
10130
10131fn is_shadow_bootstrap_dir(path: &str) -> bool {
10132 matches!(
10133 path,
10134 "/dev"
10135 | "/proc"
10136 | "/tmp"
10137 | "/bin"
10138 | "/lib"
10139 | "/sbin"
10140 | "/boot"
10141 | "/etc"
10142 | "/root"
10143 | "/run"
10144 | "/srv"
10145 | "/sys"
10146 | "/opt"
10147 | "/mnt"
10148 | "/media"
10149 | "/home"
10150 | "/home/agentos"
10151 | "/usr"
10152 | "/usr/bin"
10153 | "/usr/games"
10154 | "/usr/include"
10155 | "/usr/lib"
10156 | "/usr/libexec"
10157 | "/usr/man"
10158 | "/usr/local"
10159 | "/usr/local/bin"
10160 | "/usr/sbin"
10161 | "/usr/share"
10162 | "/usr/share/man"
10163 | "/var"
10164 | "/var/cache"
10165 | "/var/empty"
10166 | "/var/lib"
10167 | "/var/lock"
10168 | "/var/log"
10169 | "/var/run"
10170 | "/var/spool"
10171 | "/var/tmp"
10172 | "/etc/agentos"
10173 | "/workspace"
10174 )
10175}
10176
10177#[cfg(test)]
10178mod shadow_sync_tests {
10179 use super::{is_protected_agentos_shadow_sync_path, is_shadow_bootstrap_dir};
10180
10181 #[test]
10182 fn shadow_bootstrap_sync_skips_virtual_home_tree() {
10183 assert!(is_shadow_bootstrap_dir("/home"));
10184 assert!(is_shadow_bootstrap_dir("/home/agentos"));
10185 }
10186
10187 #[test]
10188 fn protected_agentos_paths_are_not_shadow_synced() {
10189 assert!(is_protected_agentos_shadow_sync_path("/etc/agentos"));
10190 assert!(is_protected_agentos_shadow_sync_path(
10191 "/etc/agentos/instructions.md"
10192 ));
10193 assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos-copy"));
10194 assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos.md"));
10195 }
10196}
10197
10198fn is_kernel_owned_shadow_sync_path(path: &str) -> bool {
10199 matches!(path, "/dev" | "/proc" | "/sys")
10200 || path.starts_with("/dev/")
10201 || path.starts_with("/proc/")
10202 || path.starts_with("/sys/")
10203}
10204
10205pub(crate) fn is_protected_agentos_shadow_sync_path(path: &str) -> bool {
10206 path == "/etc/agentos" || path.starts_with("/etc/agentos/")
10207}
10208
10209fn should_skip_shadow_sync_path(vm: &VmState, guest_path: &str) -> bool {
10210 is_kernel_owned_shadow_sync_path(guest_path)
10211 || is_protected_agentos_shadow_sync_path(guest_path)
10212 || host_mount_path_for_guest_path_from_mounts(&vm.configuration.mounts, guest_path)
10213 .is_some()
10214}
10215
10216fn resolve_path_like_guest_specifier(cwd: &str, specifier: &str) -> String {
10217 if specifier.starts_with("file://") {
10218 normalize_path(specifier.trim_start_matches("file://"))
10219 } else if specifier.starts_with("file:") {
10220 normalize_path(specifier.trim_start_matches("file:"))
10221 } else if specifier.starts_with('/') {
10222 normalize_path(specifier)
10223 } else {
10224 normalize_path(&format!("{cwd}/{specifier}"))
10225 }
10226}
10227
10228fn guest_entrypoint_for_specifier(cwd: &str, specifier: &str) -> Option<String> {
10229 is_path_like_specifier(specifier).then(|| resolve_path_like_guest_specifier(cwd, specifier))
10230}
10231
10232fn is_node_runtime_command(command: &str) -> bool {
10233 matches!(command, "node" | "npm" | "npx")
10234 || Path::new(command)
10235 .file_name()
10236 .and_then(|name| name.to_str())
10237 .is_some_and(|name| matches!(name, "node" | "npm" | "npx"))
10238}
10239
10240fn python_command_base_name(command: &str) -> &str {
10241 Path::new(command)
10242 .file_name()
10243 .and_then(|name| name.to_str())
10244 .unwrap_or(command)
10245}
10246
10247fn is_python_runtime_command(command: &str) -> bool {
10251 matches!(
10252 python_command_base_name(command),
10253 "python" | "python3" | "pip" | "pip3"
10254 )
10255}
10256
10257fn resolve_python_command_execution(
10263 vm: &VmState,
10264 command: &str,
10265 args: &[String],
10266 mut env: BTreeMap<String, String>,
10267 guest_cwd: String,
10268 host_cwd: PathBuf,
10269) -> Result<ResolvedChildProcessExecution, SidecarError> {
10270 let base_name = python_command_base_name(command);
10271 let is_pip = matches!(base_name, "pip" | "pip3");
10272
10273 let mut entrypoint = String::new();
10274 let mut argv: Vec<String> = Vec::new();
10275 let mut module: Option<String> = None;
10276 let mut stdin_program = false;
10277 let mut interactive = false;
10278 let mut guest_entrypoint: Option<String> = None;
10279
10280 if is_pip {
10281 module = Some(String::from("pip"));
10282 argv.push(String::from("pip"));
10283 argv.extend(args.iter().cloned());
10284 } else {
10285 let mut idx = 0;
10288 while let Some(flag) = args.get(idx) {
10289 match flag.as_str() {
10290 "-B" | "-E" | "-I" | "-O" | "-OO" | "-q" | "-s" | "-S" | "-u" | "-v" | "-b"
10291 | "-d" | "-x" => idx += 1,
10292 _ => break,
10293 }
10294 }
10295 let rest = &args[idx..];
10296 match rest.first().map(String::as_str) {
10297 Some("-c") => {
10298 entrypoint = rest.get(1).cloned().ok_or_else(|| {
10299 SidecarError::InvalidState(String::from("argument expected for the -c option"))
10300 })?;
10301 argv.push(String::from("-c"));
10302 argv.extend(rest.iter().skip(2).cloned());
10303 }
10304 Some("-m") => {
10305 let name = rest.get(1).cloned().ok_or_else(|| {
10306 SidecarError::InvalidState(String::from("argument expected for the -m option"))
10307 })?;
10308 module = Some(name);
10309 argv.push(String::from("-m"));
10310 argv.extend(rest.iter().skip(2).cloned());
10311 }
10312 Some("-") => {
10313 stdin_program = true;
10314 argv.push(String::from("-"));
10315 argv.extend(rest.iter().skip(1).cloned());
10316 }
10317 Some(spec) if !spec.starts_with('-') => {
10318 let resolved_guest = guest_entrypoint_for_specifier(&guest_cwd, spec)
10319 .unwrap_or_else(|| spec.to_string());
10320 entrypoint = resolved_guest.clone();
10321 env.insert(String::from("AGENTOS_PYTHON_FILE"), resolved_guest.clone());
10322 guest_entrypoint = Some(resolved_guest);
10323 argv.push(spec.to_string());
10324 argv.extend(rest.iter().skip(1).cloned());
10325 }
10326 Some(other) => {
10327 return Err(SidecarError::InvalidState(format!(
10328 "unsupported python option: {other}"
10329 )));
10330 }
10331 None => {
10332 interactive = true;
10333 argv.push(String::new());
10334 }
10335 }
10336 }
10337
10338 env.insert(
10339 String::from("AGENTOS_PYTHON_ARGV"),
10340 serde_json::to_string(&argv).unwrap_or_else(|_| String::from("[]")),
10341 );
10342 if let Some(module) = &module {
10343 env.insert(String::from("AGENTOS_PYTHON_MODULE"), module.clone());
10344 }
10345 if stdin_program {
10346 env.insert(
10347 String::from("AGENTOS_PYTHON_STDIN_PROGRAM"),
10348 String::from("1"),
10349 );
10350 }
10351 if interactive {
10352 env.insert(
10353 String::from("AGENTOS_PYTHON_INTERACTIVE"),
10354 String::from("1"),
10355 );
10356 }
10357
10358 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
10359
10360 Ok(ResolvedChildProcessExecution {
10361 command: String::from(PYTHON_COMMAND),
10362 process_args: std::iter::once(command.to_owned())
10363 .chain(args.iter().cloned())
10364 .collect(),
10365 runtime: GuestRuntimeKind::Python,
10366 entrypoint,
10367 execution_args: args.to_vec(),
10368 env,
10369 guest_cwd,
10370 host_cwd,
10371 wasm_permission_tier: None,
10372 tool_command: false,
10373 })
10374}
10375
10376fn resolve_special_node_cli_invocation(
10377 args: &[String],
10378 env: &mut BTreeMap<String, String>,
10379) -> Option<(String, Vec<String>)> {
10380 let first = args.first()?;
10381 match first.as_str() {
10382 "-e" | "--eval" => {
10383 env.insert(
10384 String::from("AGENTOS_NODE_EVAL"),
10385 args.get(1).cloned().unwrap_or_default(),
10386 );
10387 Some((first.clone(), args.iter().skip(2).cloned().collect()))
10388 }
10389 "-v" | "--version" => {
10390 env.insert(
10391 String::from("AGENTOS_NODE_EVAL"),
10392 String::from("console.log(process.version);"),
10393 );
10394 Some((String::from("-e"), args.to_vec()))
10395 }
10396 _ => None,
10397 }
10398}
10399
10400fn node_runtime_command_name(command: &str) -> Option<&str> {
10401 let name = Path::new(command)
10402 .file_name()
10403 .and_then(|name| name.to_str())?;
10404 matches!(name, "node" | "npm" | "npx").then_some(name)
10405}
10406
10407struct ResolvedHostNodeCliEntrypoint {
10408 command_name: String,
10409 guest_root: String,
10410 guest_entrypoint: String,
10411 package_root: PathBuf,
10412}
10413
10414fn resolve_host_node_cli_entrypoint(command: &str) -> Option<ResolvedHostNodeCliEntrypoint> {
10415 let command_name = node_runtime_command_name(command)?;
10416 if !matches!(command_name, "npm" | "npx") {
10417 return None;
10418 }
10419
10420 let path = std::env::var_os("PATH")?;
10421 for root in std::env::split_paths(&path) {
10422 let candidate = root.join(command_name);
10423 if !candidate.is_file() {
10424 continue;
10425 }
10426 let entrypoint = candidate.canonicalize().ok().unwrap_or(candidate);
10427 let package_root = entrypoint.parent()?.parent()?.to_path_buf();
10428 let guest_root = format!("/__secure_exec/node-runtime/{command_name}");
10429 let relative_entrypoint = entrypoint.strip_prefix(&package_root).ok()?;
10430 let guest_entrypoint = normalize_path(&format!(
10431 "{guest_root}/{}",
10432 relative_entrypoint.to_string_lossy().replace('\\', "/")
10433 ));
10434 return Some(ResolvedHostNodeCliEntrypoint {
10435 command_name: command_name.to_owned(),
10436 guest_root,
10437 guest_entrypoint,
10438 package_root,
10439 });
10440 }
10441
10442 None
10443}
10444
10445fn build_host_node_cli_eval(cli: &ResolvedHostNodeCliEntrypoint) -> String {
10446 let guest_npm_main = normalize_path(&format!("{}/lib/npm.js", cli.guest_root));
10447 let guest_npm_cli = normalize_path(&format!("{}/bin/npm-cli.js", cli.guest_root));
10448 let guest_package_json = normalize_path(&format!("{}/package.json", cli.guest_root));
10449 let guest_display_module = normalize_path(&format!("{}/lib/utils/display.js", cli.guest_root));
10450 let guest_log_file_module =
10451 normalize_path(&format!("{}/lib/utils/log-file.js", cli.guest_root));
10452 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); } } }";
10453 let display_stub = format!(
10454 "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 }};",
10455 display_module = serde_json::to_string(&guest_display_module)
10456 .unwrap_or_else(|_| format!("\"{guest_display_module}\"")),
10457 log_file_module = serde_json::to_string(&guest_log_file_module)
10458 .unwrap_or_else(|_| format!("\"{guest_log_file_module}\"")),
10459 );
10460 let registry_fetch_stub = "const { createRequire: __agentOSCreateRequire } = require('module'); const __agentOSNpmRequire = __agentOSCreateRequire(require.resolve(__AGENTOS_NPM_MAIN__)); try { const __agentOSMinipassFetchPath = __agentOSNpmRequire.resolve('minipass-fetch'); const __agentOSMinipassFetch = __agentOSNpmRequire(__agentOSMinipassFetchPath); const { FetchError: __agentOSFetchError, Headers: __agentOSFetchHeaders, Request: __agentOSFetchRequest, Response: __agentOSFetchResponse, AbortError: __agentOSAbortError } = __agentOSMinipassFetch; const { Minipass: __agentOSMinipass } = __agentOSNpmRequire('minipass'); const __agentOSCreateBinaryMinipass = () => new __agentOSMinipass({ objectMode: false, encoding: null }); const __agentOSCloneBuffer = (buffer) => Buffer.isBuffer(buffer) ? Buffer.from(buffer) : Buffer.from(buffer ?? []); const __agentOSBufferToArrayBuffer = (buffer) => { const bytes = __agentOSCloneBuffer(buffer); return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); }; const __agentOSAttachBufferedBodyMethods = (response, responseBuffer) => { const __agentOSReadBuffer = async () => __agentOSCloneBuffer(responseBuffer); response.__agentOSBufferedBody = __agentOSCloneBuffer(responseBuffer); response.buffer = __agentOSReadBuffer; response.text = async () => (await __agentOSReadBuffer()).toString('utf8'); response.json = async () => JSON.parse(await response.text()); response.arrayBuffer = async () => __agentOSBufferToArrayBuffer(await __agentOSReadBuffer()); response.clone = () => { const clonedBody = __agentOSCreateBinaryMinipass(); const clonedBuffer = __agentOSCloneBuffer(responseBuffer); clonedBody.end(clonedBuffer); const clonedResponse = new __agentOSFetchResponse(clonedBody, { url: response.url, status: response.status, statusText: response.statusText, headers: response.headers, size: response.size, timeout: response.timeout, counter: response.counter, trailer: response.trailer }); return __agentOSAttachBufferedBodyMethods(clonedResponse, clonedBuffer); }; return response; }; const __agentOSNormalizeHeaders = (__agentOSHeaders) => { const normalized = {}; __agentOSHeaders.forEach((value, key) => { if (normalized[key] === undefined) { normalized[key] = value; return; } if (Array.isArray(normalized[key])) { normalized[key].push(value); return; } normalized[key] = [normalized[key], value]; }); return normalized; }; const __agentOSPatchedMinipassFetch = async (input, opts = {}) => { const request = input instanceof __agentOSFetchRequest ? input : new __agentOSFetchRequest(input, opts); const __agentOSController = !request.signal && typeof AbortController === 'function' ? new AbortController() : null; const __agentOSSignal = request.signal ?? __agentOSController?.signal; let __agentOSTimer = null; if (__agentOSController && Number.isFinite(request.timeout) && request.timeout > 0) { __agentOSTimer = setTimeout(() => __agentOSController.abort(new Error(`network timeout at: ${request.url}`)), request.timeout); __agentOSTimer.unref?.(); } try { const requestHeaders = {}; request.headers.forEach((value, key) => { requestHeaders[key] = value; }); const response = await fetch(request.url, { method: request.method, headers: requestHeaders, body: request.body ?? undefined, redirect: request.redirect ?? opts.redirect ?? 'follow', signal: __agentOSSignal, ...(request.body ? { duplex: 'half' } : {}) }); const responseBody = __agentOSCreateBinaryMinipass(); const contentType = String(response.headers.get('content-type') || '').toLowerCase(); const responseBuffer = contentType.includes('json') ? Buffer.from(JSON.stringify(await response.json())) : contentType.startsWith('text/') ? Buffer.from(await response.text()) : Buffer.from(await response.arrayBuffer()); responseBody.end(responseBuffer); return __agentOSAttachBufferedBodyMethods(new __agentOSFetchResponse(responseBody, { url: response.url, status: response.status, statusText: response.statusText, headers: __agentOSNormalizeHeaders(response.headers), size: request.size, timeout: request.timeout, counter: request.counter ?? opts.counter ?? 0, trailer: Promise.resolve(new __agentOSFetchHeaders()) }), responseBuffer); } catch (error) { if (error instanceof Error) { throw error; } throw new __agentOSFetchError(String(error), 'system', error); } finally { if (__agentOSTimer) { clearTimeout(__agentOSTimer); } } }; globalThis.__agentOSPatchedMinipassFetch = __agentOSPatchedMinipassFetch; __agentOSPatchedMinipassFetch.isRedirect = typeof __agentOSMinipassFetch.isRedirect === 'function' ? __agentOSMinipassFetch.isRedirect.bind(__agentOSMinipassFetch) : (code) => code === 301 || code === 302 || code === 303 || code === 307 || code === 308; __agentOSPatchedMinipassFetch.FetchError = __agentOSFetchError; __agentOSPatchedMinipassFetch.Headers = __agentOSFetchHeaders; __agentOSPatchedMinipassFetch.Request = __agentOSFetchRequest; __agentOSPatchedMinipassFetch.Response = __agentOSFetchResponse; __agentOSPatchedMinipassFetch.AbortError = __agentOSAbortError; globalThis._moduleCache[__agentOSMinipassFetchPath] = { exports: __agentOSPatchedMinipassFetch }; __agentOSDebugLog('patched-minipass-fetch', __agentOSMinipassFetchPath); const __agentOSCheckResponsePath = __agentOSNpmRequire.resolve('npm-registry-fetch/lib/check-response.js'); const __agentOSCheckResponse = __agentOSNpmRequire(__agentOSCheckResponsePath); const __agentOSEnsureResponseBodyStream = (response) => { if (!response || (response.body && typeof response.body.on === 'function')) { return response; } const body = __agentOSCreateBinaryMinipass(); const finishWithError = (error) => body.emit('error', error instanceof Error ? error : new Error(String(error))); try { if (typeof response.buffer === 'function') { Promise.resolve(response.buffer()).then((buffer) => body.end(buffer), finishWithError); } else if (Buffer.isBuffer(response.body) || typeof response.body === 'string') { body.end(response.body); } else if (response.body && typeof response.body[Symbol.asyncIterator] === 'function') { (async () => { try { for await (const chunk of response.body) { body.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } body.end(); } catch (error) { finishWithError(error); body.end(); } })(); } else { body.end(); } } catch (error) { finishWithError(error); body.end(); } return new __agentOSFetchResponse(body, response); }; globalThis._moduleCache[__agentOSCheckResponsePath] = { exports: (payload) => { const normalized = { ...payload, res: __agentOSEnsureResponseBodyStream(payload.res) }; __agentOSDebugLog('check-response-body', normalized.res && normalized.res.status, typeof (normalized.res && normalized.res.body), normalized.res && normalized.res.body && typeof normalized.res.body.on, normalized.res && normalized.res.body && normalized.res.body.constructor && normalized.res.body.constructor.name, !!(normalized.res && normalized.res.__agentOSBufferedBody), normalized.res && typeof normalized.res.json); return __agentOSCheckResponse(normalized); } }; __agentOSDebugLog('patched-check-response', __agentOSCheckResponsePath); } catch (error) { __agentOSDebugLog('patch-minipass-fetch-failed', error && error.stack ? error.stack : String(error)); } try { const __agentOSRegistryFetchPath = __agentOSNpmRequire.resolve('npm-registry-fetch'); const __agentOSRegistryFetch = __agentOSNpmRequire(__agentOSRegistryFetchPath); const __agentOSWrapRegistryFetch = (fn) => { const wrapResult = (promise) => Promise.resolve(promise).then((res) => { __agentOSDebugLog('registry-fetch-result', res && res.status, typeof (res && res.body), res && res.body && typeof res.body.on, res && res.body && res.body.constructor && res.body.constructor.name, !!(res && res.__agentOSBufferedBody), res && typeof res.json); return res; }); const wrapped = (uri, opts = {}) => wrapResult(globalThis.__agentOSPatchedMinipassFetch(uri, { method: opts.method, headers: opts.headers, body: opts.body, redirect: opts.redirect, signal: opts.signal, timeout: opts.timeout, size: opts.size, counter: opts.counter })); if (typeof fn.json === 'function') { wrapped.json = (uri, opts = {}) => wrapped(uri, opts).then((res) => res.json()); } if (fn.json && typeof fn.json.stream === 'function') { wrapped.json = wrapped.json || {}; wrapped.json.stream = (uri, path, opts = {}) => fn.json.stream(uri, path, { ...opts, agent: false }); } if (typeof fn.pickRegistry === 'function') { wrapped.pickRegistry = fn.pickRegistry.bind(fn); } if (typeof fn.getAuth === 'function') { wrapped.getAuth = fn.getAuth.bind(fn); } return wrapped; }; globalThis._moduleCache[__agentOSRegistryFetchPath] = { exports: __agentOSWrapRegistryFetch(__agentOSRegistryFetch) }; __agentOSDebugLog('patched-npm-registry-fetch', __agentOSRegistryFetchPath); } catch (error) { __agentOSDebugLog('patch-npm-registry-fetch-failed', error && error.stack ? error.stack : String(error)); }";
10461 match cli.command_name.as_str() {
10462 "npx" => format!(
10463 "{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); }});",
10464 debug_preamble = debug_preamble,
10465 display_stub = display_stub,
10466 registry_fetch_stub = registry_fetch_stub.replace(
10467 "__AGENTOS_NPM_MAIN__",
10468 &serde_json::to_string(&guest_npm_main)
10469 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
10470 ),
10471 npm_main = serde_json::to_string(&guest_npm_main)
10472 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
10473 npm_cli = serde_json::to_string(&guest_npm_cli)
10474 .unwrap_or_else(|_| format!("\"{guest_npm_cli}\"")),
10475 package_json = serde_json::to_string(&guest_package_json)
10476 .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
10477 ),
10478 _ => format!(
10479 "{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); }});",
10480 debug_preamble = debug_preamble,
10481 display_stub = display_stub,
10482 registry_fetch_stub = registry_fetch_stub.replace(
10483 "__AGENTOS_NPM_MAIN__",
10484 &serde_json::to_string(&guest_npm_main)
10485 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
10486 ),
10487 npm_main = serde_json::to_string(&guest_npm_main)
10488 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
10489 package_json = serde_json::to_string(&guest_package_json)
10490 .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
10491 ),
10492 }
10493}
10494
10495fn resolve_guest_command_entrypoint(
10496 vm: &VmState,
10497 guest_cwd: &str,
10498 command: &str,
10499 path_env: Option<&str>,
10500) -> Option<String> {
10501 if !is_path_like_specifier(command) {
10502 if let Some(entrypoint) = vm.command_guest_paths.get(command) {
10503 return Some(entrypoint.clone());
10504 }
10505
10506 for search_dir in guest_command_search_dirs(vm, guest_cwd, path_env) {
10507 let candidate = normalize_path(&format!("{search_dir}/{command}"));
10508 if let Some(entrypoint) = resolve_guest_command_path_candidate(vm, &candidate) {
10509 return Some(entrypoint);
10510 }
10511 }
10512
10513 return None;
10514 }
10515
10516 let normalized = resolve_path_like_guest_specifier(guest_cwd, command);
10517 resolve_guest_command_path_candidate(vm, &normalized).or_else(|| {
10518 let parent_dir = Path::new(&normalized).parent()?.to_str()?;
10522 if !guest_command_search_dirs(vm, guest_cwd, path_env)
10523 .iter()
10524 .any(|search_dir| normalize_path(search_dir) == normalize_path(parent_dir))
10525 {
10526 return None;
10527 }
10528
10529 let file_name = Path::new(&normalized).file_name()?.to_str()?;
10530 vm.command_guest_paths.get(file_name).cloned()
10531 })
10532}
10533
10534fn guest_command_search_dirs(vm: &VmState, guest_cwd: &str, path_env: Option<&str>) -> Vec<String> {
10535 let mut search_dirs = Vec::new();
10536 let mut seen = BTreeSet::new();
10537
10538 if let Some(path) = path_env.or_else(|| vm.guest_env.get("PATH").map(String::as_str)) {
10539 for segment in path.split(':') {
10540 let trimmed = segment.trim();
10541 if trimmed.is_empty() {
10542 continue;
10543 }
10544 let normalized = if trimmed.starts_with('/') {
10545 normalize_path(trimmed)
10546 } else {
10547 normalize_path(&format!("{guest_cwd}/{trimmed}"))
10548 };
10549 if seen.insert(normalized.clone()) {
10550 search_dirs.push(normalized);
10551 }
10552 }
10553 }
10554
10555 for fallback in ["/bin", "/usr/bin", "/usr/local/bin"] {
10556 let normalized = String::from(fallback);
10557 if seen.insert(normalized.clone()) {
10558 search_dirs.push(normalized);
10559 }
10560 }
10561
10562 search_dirs
10563}
10564
10565fn resolve_guest_command_path_candidate(vm: &VmState, candidate: &str) -> Option<String> {
10566 if candidate.starts_with("/bin/")
10567 || candidate.starts_with("/usr/bin/")
10568 || candidate.starts_with("/usr/local/bin/")
10569 || candidate.starts_with("/__secure_exec/commands/")
10570 {
10571 if let Some(file_name) = Path::new(candidate)
10572 .file_name()
10573 .and_then(|name| name.to_str())
10574 {
10575 if let Some(guest_entrypoint) = vm.command_guest_paths.get(file_name) {
10576 return Some(guest_entrypoint.clone());
10577 }
10578 }
10579 }
10580
10581 if vm
10582 .kernel
10583 .exists(candidate)
10584 .ok()
10585 .is_some_and(|exists| exists)
10586 {
10587 return Some(normalize_path(candidate));
10588 }
10589
10590 resolve_vm_guest_path_to_host(vm, candidate)
10591 .is_file()
10592 .then(|| normalize_path(candidate))
10593}
10594
10595fn resolve_host_entrypoint_within_vm_host_cwd(
10596 vm: &VmState,
10597 specifier: &str,
10598) -> Option<(String, String)> {
10599 let candidate = Path::new(specifier);
10600 if !candidate.is_absolute() {
10601 return None;
10602 }
10603
10604 let normalized_entrypoint = normalize_host_path(candidate);
10605 let normalized_host_cwd = normalize_host_path(&vm.host_cwd);
10606 if !path_is_within_root(&normalized_entrypoint, &normalized_host_cwd) {
10607 return None;
10608 }
10609
10610 let relative = normalized_entrypoint
10611 .strip_prefix(&normalized_host_cwd)
10612 .ok()?
10613 .to_string_lossy()
10614 .replace('\\', "/");
10615 let guest_entrypoint = if relative.is_empty() {
10616 String::from("/")
10617 } else {
10618 normalize_path(&format!("/{relative}"))
10619 };
10620 Some((
10621 guest_entrypoint,
10622 normalized_entrypoint.to_string_lossy().into_owned(),
10623 ))
10624}
10625
10626fn prepare_guest_runtime_env(
10627 vm: &VmState,
10628 env: &mut BTreeMap<String, String>,
10629 guest_cwd: &str,
10630 host_cwd: &Path,
10631 guest_entrypoint: Option<String>,
10632) -> Result<(), SidecarError> {
10633 let user = vm.kernel.user_profile();
10634 let path_mappings = runtime_guest_path_mappings(vm);
10635 let read_paths = expand_host_access_paths(
10636 std::iter::once(vm.cwd.clone())
10637 .chain(
10638 path_mappings
10639 .iter()
10640 .map(|mapping| PathBuf::from(&mapping.host_path)),
10641 )
10642 .chain(std::iter::once(host_cwd.to_path_buf()))
10643 .collect::<Vec<_>>()
10644 .as_slice(),
10645 );
10646 let write_paths = dedupe_host_paths(
10647 std::iter::once(vm.cwd.clone())
10648 .chain(std::iter::once(host_cwd.to_path_buf()))
10649 .chain(runtime_guest_writable_host_paths(vm))
10650 .collect::<Vec<_>>()
10651 .as_slice(),
10652 );
10653 let allowed_node_builtins = configured_allowed_node_builtins(vm);
10654 let loopback_exempt_ports = configured_loopback_exempt_ports(vm);
10655
10656 env.insert(
10657 String::from("AGENTOS_GUEST_PATH_MAPPINGS"),
10658 serde_json::to_string(&path_mappings).map_err(|error| {
10659 SidecarError::InvalidState(format!("failed to encode guest path mappings: {error}"))
10660 })?,
10661 );
10662 env.entry(String::from(EXECUTION_SANDBOX_ROOT_ENV))
10663 .or_insert_with(|| normalize_host_path(&vm.cwd).to_string_lossy().into_owned());
10664 env.insert(
10665 String::from("AGENTOS_EXTRA_FS_READ_PATHS"),
10666 serde_json::to_string(
10667 &read_paths
10668 .iter()
10669 .map(|path| path.to_string_lossy().into_owned())
10670 .collect::<Vec<_>>(),
10671 )
10672 .map_err(|error| {
10673 SidecarError::InvalidState(format!("failed to encode read paths: {error}"))
10674 })?,
10675 );
10676 env.insert(
10677 String::from("AGENTOS_EXTRA_FS_WRITE_PATHS"),
10678 serde_json::to_string(
10679 &write_paths
10680 .iter()
10681 .map(|path| path.to_string_lossy().into_owned())
10682 .collect::<Vec<_>>(),
10683 )
10684 .map_err(|error| {
10685 SidecarError::InvalidState(format!("failed to encode write paths: {error}"))
10686 })?,
10687 );
10688 env.insert(
10689 String::from("AGENTOS_ALLOWED_NODE_BUILTINS"),
10690 serde_json::to_string(&allowed_node_builtins).map_err(|error| {
10691 SidecarError::InvalidState(format!("failed to encode allowed builtins: {error}"))
10692 })?,
10693 );
10694 env.insert(
10697 String::from("AGENTOS_JS_PLATFORM"),
10698 js_runtime_platform_env(vm).to_owned(),
10699 );
10700 if let Some(resolution) = js_runtime_module_resolution_env(vm) {
10702 env.insert(
10703 String::from("AGENTOS_JS_MODULE_RESOLUTION"),
10704 resolution.to_owned(),
10705 );
10706 }
10707 if let Some(allowlist) = js_runtime_enforced_builtins(vm) {
10711 env.insert(
10712 String::from("AGENTOS_JS_BUILTIN_ALLOWLIST"),
10713 serde_json::to_string(&allowlist).map_err(|error| {
10714 SidecarError::InvalidState(format!(
10715 "failed to encode jsRuntime builtin allow-list: {error}"
10716 ))
10717 })?,
10718 );
10719 }
10720 env.entry(String::from("HOME"))
10727 .or_insert_with(|| user.homedir.clone());
10728 env.entry(String::from("USER"))
10729 .or_insert_with(|| user.username.clone());
10730 env.entry(String::from("LOGNAME"))
10731 .or_insert_with(|| user.username.clone());
10732 env.entry(String::from("SHELL"))
10733 .or_insert_with(|| user.shell.clone());
10734 env.entry(String::from("PATH")).or_insert_with(|| {
10735 vm.guest_env
10736 .get("PATH")
10737 .cloned()
10738 .unwrap_or_else(|| crate::vm::DEFAULT_GUEST_PATH_ENV.to_owned())
10739 });
10740 env.entry(String::from("TMPDIR"))
10741 .or_insert_with(|| String::from("/tmp"));
10742 env.insert(String::from("PWD"), guest_cwd.to_owned());
10743 if !loopback_exempt_ports.is_empty() {
10744 env.insert(
10745 String::from(LOOPBACK_EXEMPT_PORTS_ENV),
10746 serde_json::to_string(&loopback_exempt_ports).map_err(|error| {
10747 SidecarError::InvalidState(format!("failed to encode loopback exemptions: {error}"))
10748 })?,
10749 );
10750 }
10751 if let Some(guest_entrypoint) = guest_entrypoint {
10752 env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
10753 }
10754 Ok(())
10755}
10756
10757fn javascript_execution_limits(vm: &VmState) -> JavascriptExecutionLimits {
10762 JavascriptExecutionLimits {
10763 v8_heap_limit_mb: vm.limits.js_runtime.v8_heap_limit_mb,
10764 sync_rpc_wait_timeout_ms: vm.limits.js_runtime.sync_rpc_wait_timeout_ms,
10765 }
10766}
10767
10768fn guest_runtime_identity(
10774 vm: &VmState,
10775 virtual_pid: Option<u64>,
10776 virtual_ppid: Option<u64>,
10777) -> GuestRuntimeConfig {
10778 let user = vm.kernel.user_profile();
10779 let resource_limits = vm.kernel.resource_limits();
10780 let identity = shared_guest_runtime_identity(&user, resource_limits, virtual_pid, virtual_ppid);
10781 GuestRuntimeConfig {
10782 virtual_uid: Some(identity.virtual_uid),
10783 virtual_gid: Some(identity.virtual_gid),
10784 virtual_pid: identity.virtual_pid,
10785 virtual_ppid: identity.virtual_ppid,
10786 virtual_exec_path: None,
10787 os_cpu_count: Some(identity.os_cpu_count),
10788 os_totalmem: Some(identity.os_totalmem),
10789 os_freemem: Some(identity.os_freemem),
10790 os_homedir: Some(identity.os_homedir),
10791 os_hostname: Some(identity.os_hostname),
10792 os_tmpdir: Some(identity.os_tmpdir),
10793 os_type: Some(identity.os_type),
10794 os_release: Some(identity.os_release),
10795 os_version: Some(identity.os_version),
10796 os_machine: Some(identity.os_machine),
10797 os_shell: Some(identity.os_shell),
10798 os_user: Some(identity.os_user),
10799 high_resolution_time: vm
10800 .configuration
10801 .js_runtime
10802 .as_ref()
10803 .is_some_and(|cfg| cfg.high_resolution_time.unwrap_or(false)),
10804 snapshot_userland_code: vm
10809 .configuration
10810 .js_runtime
10811 .as_ref()
10812 .and_then(|cfg| cfg.snapshot_userland_code.clone()),
10813 }
10814}
10815
10816fn guest_virtual_home(vm: &VmState) -> String {
10821 let homedir = vm.kernel.user_profile().homedir;
10822 if homedir.starts_with('/') {
10823 homedir
10824 } else {
10825 String::from("/root")
10826 }
10827}
10828
10829fn python_execution_limits(vm: &VmState) -> PythonExecutionLimits {
10831 PythonExecutionLimits {
10832 output_buffer_max_bytes: Some(vm.limits.python.output_buffer_max_bytes),
10833 execution_timeout_ms: Some(vm.limits.python.execution_timeout_ms),
10834 max_old_space_mb: Some(vm.limits.python.max_old_space_mb),
10835 vfs_rpc_timeout_ms: Some(vm.limits.python.vfs_rpc_timeout_ms),
10836 }
10837}
10838
10839fn wasm_execution_limits(vm: &VmState) -> WasmExecutionLimits {
10844 let resource_limits = vm.kernel.resource_limits();
10845 WasmExecutionLimits {
10846 max_fuel: resource_limits.max_wasm_fuel,
10847 max_memory_bytes: resource_limits.max_wasm_memory_bytes,
10848 max_stack_bytes: resource_limits
10849 .max_wasm_stack_bytes
10850 .map(|value| value as u64),
10851 }
10852}
10853
10854fn js_runtime_platform(vm: &VmState) -> vm_config::JsRuntimePlatform {
10857 vm.configuration
10858 .js_runtime
10859 .as_ref()
10860 .map(|cfg| cfg.platform)
10861 .unwrap_or(vm_config::JsRuntimePlatform::Node)
10862}
10863
10864fn js_runtime_platform_env(vm: &VmState) -> &'static str {
10867 match js_runtime_platform(vm) {
10868 vm_config::JsRuntimePlatform::Node => "node",
10869 vm_config::JsRuntimePlatform::Browser => "browser",
10870 vm_config::JsRuntimePlatform::Neutral => "neutral",
10871 vm_config::JsRuntimePlatform::Bare => "bare",
10872 }
10873}
10874
10875fn js_runtime_module_resolution_env(vm: &VmState) -> Option<&'static str> {
10878 let resolution = vm
10879 .configuration
10880 .js_runtime
10881 .as_ref()
10882 .map(|cfg| cfg.module_resolution)
10883 .unwrap_or(vm_config::JsModuleResolution::Node);
10884 match resolution {
10885 vm_config::JsModuleResolution::Node => None,
10886 vm_config::JsModuleResolution::Relative => Some("relative"),
10887 vm_config::JsModuleResolution::None => Some("none"),
10888 }
10889}
10890
10891fn js_runtime_enforced_builtins(vm: &VmState) -> Option<Vec<String>> {
10895 if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
10896 return Some(Vec::new());
10897 }
10898 vm.configuration
10899 .js_runtime
10900 .as_ref()
10901 .and_then(|cfg| cfg.allowed_builtins.clone())
10902}
10903
10904fn configured_allowed_node_builtins(vm: &VmState) -> Vec<String> {
10905 if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
10907 return Vec::new();
10908 }
10909 let configured = match vm
10912 .configuration
10913 .js_runtime
10914 .as_ref()
10915 .and_then(|cfg| cfg.allowed_builtins.as_ref())
10916 {
10917 Some(list) => list.clone(),
10918 None => DEFAULT_ALLOWED_NODE_BUILTINS
10919 .iter()
10920 .map(|value| (*value).to_owned())
10921 .collect::<Vec<_>>(),
10922 };
10923 dedupe_strings(&configured)
10924}
10925
10926fn configured_loopback_exempt_ports(vm: &VmState) -> Vec<String> {
10927 if !vm.configuration.loopback_exempt_ports.is_empty() {
10928 return vm
10929 .configuration
10930 .loopback_exempt_ports
10931 .iter()
10932 .map(ToString::to_string)
10933 .collect();
10934 }
10935
10936 vm.create_loopback_exempt_ports
10937 .iter()
10938 .map(ToString::to_string)
10939 .collect()
10940}
10941
10942fn mount_config_host_path(config: &str) -> Option<String> {
10944 serde_json::from_str::<Value>(config)
10945 .ok()?
10946 .get("hostPath")
10947 .and_then(Value::as_str)
10948 .map(str::to_owned)
10949}
10950
10951fn runtime_guest_writable_host_paths(vm: &VmState) -> Vec<PathBuf> {
10952 vm.configuration
10953 .mounts
10954 .iter()
10955 .filter(|mount| !mount.read_only)
10956 .filter_map(|mount| {
10957 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
10958 .then(|| mount_config_host_path(&mount.plugin.config))
10959 .flatten()
10960 .map(PathBuf::from)
10961 })
10962 .collect()
10963}
10964
10965fn runtime_guest_path_mappings(vm: &VmState) -> Vec<RuntimeGuestPathMapping> {
10966 let mut mappings = vm
10967 .configuration
10968 .mounts
10969 .iter()
10970 .filter_map(|mount| {
10971 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
10972 .then(|| {
10973 mount_config_host_path(&mount.plugin.config).map(|host_path| {
10974 RuntimeGuestPathMapping {
10975 guest_path: normalize_path(&mount.guest_path),
10976 host_path,
10977 read_only: mount.read_only,
10978 }
10979 })
10980 })
10981 .flatten()
10982 })
10983 .collect::<Vec<_>>();
10984 let mut command_root_mappings = vm
10985 .command_guest_paths
10986 .values()
10987 .filter_map(|guest_path| {
10988 Path::new(guest_path)
10989 .parent()
10990 .and_then(|parent| parent.to_str())
10991 .map(normalize_path)
10992 })
10993 .collect::<BTreeSet<_>>()
10994 .into_iter()
10995 .map(|guest_path| RuntimeGuestPathMapping {
10996 host_path: resolve_vm_guest_path_to_host(vm, &guest_path)
10997 .to_string_lossy()
10998 .into_owned(),
10999 guest_path,
11000 read_only: false,
11001 })
11002 .collect::<Vec<_>>();
11003 mappings.append(&mut command_root_mappings);
11004 let mut extra_node_modules_roots = mappings
11005 .iter()
11006 .filter(|mapping| mapping.guest_path.starts_with("/root/node_modules/"))
11007 .filter_map(|mapping| {
11008 host_node_modules_root(Path::new(&mapping.host_path)).map(|host_root| {
11009 RuntimeGuestPathMapping {
11010 guest_path: String::from("/root/node_modules"),
11011 host_path: host_root.to_string_lossy().into_owned(),
11012 read_only: mapping.read_only,
11013 }
11014 })
11015 })
11016 .collect::<Vec<_>>();
11017 mappings.append(&mut extra_node_modules_roots);
11018 mappings.push(RuntimeGuestPathMapping {
11019 guest_path: String::from("/"),
11020 host_path: vm.cwd.to_string_lossy().into_owned(),
11021 read_only: false,
11022 });
11023 mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.guest_path.len()));
11024 mappings.dedup_by(|left, right| {
11025 left.guest_path == right.guest_path && left.host_path == right.host_path
11026 });
11027 mappings
11028}
11029
11030fn build_module_reader(
11041 vm: &VmState,
11042 resolved: &ResolvedChildProcessExecution,
11043) -> Option<crate::plugins::host_dir::HostDirModuleReader> {
11044 let mut pairs: Vec<(String, PathBuf)> = vm
11045 .configuration
11046 .mounts
11047 .iter()
11048 .filter(|mount| mount.read_only)
11049 .filter(|mount| (mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11050 .filter_map(|mount| {
11051 mount_config_host_path(&mount.plugin.config)
11052 .map(|host_path| (normalize_path(&mount.guest_path), PathBuf::from(host_path)))
11053 })
11054 .collect();
11055
11056 let guest_entrypoint = resolved
11057 .env
11058 .get("AGENTOS_GUEST_ENTRYPOINT")
11059 .map(|path| normalize_path(path));
11060 if let Some(guest_entrypoint) = guest_entrypoint.as_deref() {
11061 let entrypoint_in_read_only_mount = pairs.iter().any(|(guest_path, _)| {
11062 guest_entrypoint == guest_path
11063 || guest_entrypoint.starts_with(&format!("{guest_path}/"))
11064 });
11065 if !entrypoint_in_read_only_mount {
11066 return None;
11067 }
11068 }
11069
11070 let extra_roots: Vec<(String, PathBuf)> = pairs
11074 .iter()
11075 .filter(|(guest_path, _)| guest_path.starts_with("/root/node_modules/"))
11076 .filter_map(|(_, host_path)| {
11077 host_node_modules_root(host_path).map(|root| (String::from("/root/node_modules"), root))
11078 })
11079 .collect();
11080 pairs.extend(extra_roots);
11081
11082 crate::plugins::host_dir::HostDirModuleReader::from_mounts(pairs)
11083}
11084
11085fn host_node_modules_root(path: &Path) -> Option<PathBuf> {
11086 if let Some(root) = path
11087 .ancestors()
11088 .filter(|candidate| {
11089 candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
11090 })
11091 .last()
11092 .map(Path::to_path_buf)
11093 {
11094 return Some(root);
11095 }
11096
11097 fs::canonicalize(path)
11098 .ok()?
11099 .ancestors()
11100 .filter(|candidate| {
11101 candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
11102 })
11103 .last()
11104 .map(Path::to_path_buf)
11105}
11106
11107#[cfg(test)]
11108mod runtime_guest_path_mapping_tests {
11109 use super::{host_node_modules_root, javascript_sync_rpc_option_bool};
11110 use serde_json::json;
11111 use std::fs;
11112 use std::time::{SystemTime, UNIX_EPOCH};
11113
11114 #[test]
11115 fn host_node_modules_root_prefers_workspace_root_over_pnpm_package_node_modules() {
11116 let unique = SystemTime::now()
11117 .duration_since(UNIX_EPOCH)
11118 .expect("clock should be monotonic")
11119 .as_nanos();
11120 let temp = std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-{unique}"));
11121 let workspace_node_modules = temp.join("node_modules");
11122 let package_root = workspace_node_modules
11123 .join(".pnpm")
11124 .join("example@1.0.0")
11125 .join("node_modules")
11126 .join("@scope")
11127 .join("pkg");
11128 fs::create_dir_all(&package_root).expect("package root should be created");
11129
11130 let resolved =
11131 host_node_modules_root(&package_root).expect("node_modules root should resolve");
11132
11133 assert_eq!(resolved, workspace_node_modules);
11134
11135 fs::remove_dir_all(&temp).expect("temp tree should be removed");
11136 }
11137
11138 #[test]
11139 fn host_node_modules_root_preserves_symlinked_workspace_node_modules_path() {
11140 let unique = SystemTime::now()
11141 .duration_since(UNIX_EPOCH)
11142 .expect("clock should be monotonic")
11143 .as_nanos();
11144 let temp =
11145 std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-symlink-{unique}"));
11146 let workspace_node_modules = temp.join("node_modules");
11147 let package_link = workspace_node_modules.join("@scope").join("pkg");
11148 let real_package = temp.join("registry").join("agent").join("pkg");
11149 fs::create_dir_all(package_link.parent().expect("package parent should exist"))
11150 .expect("scoped parent should be created");
11151 fs::create_dir_all(&real_package).expect("real package root should be created");
11152 std::os::unix::fs::symlink(&real_package, &package_link)
11153 .expect("package symlink should be created");
11154
11155 let resolved =
11156 host_node_modules_root(&package_link).expect("node_modules root should resolve");
11157
11158 assert_eq!(resolved, workspace_node_modules);
11159
11160 fs::remove_dir_all(&temp).expect("temp tree should be removed");
11161 }
11162
11163 #[test]
11164 fn javascript_sync_rpc_option_bool_accepts_boolean_recursive_argument() {
11165 assert_eq!(
11166 javascript_sync_rpc_option_bool(&[json!("/workspace"), json!(true)], 1, "recursive"),
11167 Some(true)
11168 );
11169 assert_eq!(
11170 javascript_sync_rpc_option_bool(
11171 &[json!("/workspace"), json!({ "recursive": false })],
11172 1,
11173 "recursive"
11174 ),
11175 Some(false)
11176 );
11177 }
11178}
11179
11180#[cfg(test)]
11181mod kernel_poll_sync_rpc_tests {
11182 use super::{
11183 service_javascript_kernel_poll_sync_rpc, ActiveExecution, ActiveProcess,
11184 JavascriptSyncRpcRequest, KernelPollFdResponse, SidecarKernel, ToolExecution,
11185 EXECUTION_DRIVER_NAME, JAVASCRIPT_COMMAND,
11186 };
11187 use secure_exec_kernel::command_registry::CommandDriver;
11188 use secure_exec_kernel::kernel::{KernelVmConfig, SpawnOptions};
11189 use secure_exec_kernel::mount_table::MountTable;
11190 use secure_exec_kernel::permissions::Permissions;
11191 use secure_exec_kernel::poll::{POLLHUP, POLLIN};
11192 use secure_exec_kernel::vfs::MemoryFileSystem;
11193 use serde_json::{json, Value};
11194 use std::collections::HashMap;
11195 #[test]
11196 fn javascript_kernel_poll_sync_rpc_reports_multiple_kernel_fds() {
11197 let mut config = KernelVmConfig::new("vm-js-kernel-poll");
11198 config.permissions = Permissions::allow_all();
11199 let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config);
11200 kernel
11201 .register_driver(CommandDriver::new(
11202 EXECUTION_DRIVER_NAME,
11203 [JAVASCRIPT_COMMAND],
11204 ))
11205 .expect("register execution driver");
11206
11207 let kernel_handle = kernel
11208 .spawn_process(
11209 JAVASCRIPT_COMMAND,
11210 Vec::new(),
11211 SpawnOptions {
11212 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
11213 ..SpawnOptions::default()
11214 },
11215 )
11216 .expect("spawn javascript kernel process");
11217 let pid = kernel_handle.pid();
11218
11219 let (stdin_read_fd, stdin_write_fd) = kernel
11220 .open_pipe(EXECUTION_DRIVER_NAME, pid)
11221 .expect("open kernel stdin pipe");
11222 kernel
11223 .fd_dup2(EXECUTION_DRIVER_NAME, pid, stdin_read_fd, 0)
11224 .expect("dup stdin pipe onto fd 0");
11225 kernel
11226 .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_read_fd)
11227 .expect("close original stdin read fd");
11228
11229 let process = ActiveProcess::new(
11230 pid,
11231 kernel_handle,
11232 super::GuestRuntimeKind::JavaScript,
11233 ActiveExecution::Tool(ToolExecution::default()),
11234 );
11235
11236 kernel
11237 .fd_write(EXECUTION_DRIVER_NAME, pid, stdin_write_fd, b"poll-ready")
11238 .expect("write kernel stdin payload");
11239 kernel
11240 .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_write_fd)
11241 .expect("close kernel stdin writer");
11242
11243 let response = service_javascript_kernel_poll_sync_rpc(
11244 &mut kernel,
11245 &process,
11246 &JavascriptSyncRpcRequest {
11247 id: 1,
11248 method: String::from("__kernel_poll"),
11249 raw_bytes_args: HashMap::new(),
11250 args: vec![
11251 json!([
11252 { "fd": 0, "events": POLLIN.bits() },
11253 { "fd": 1, "events": POLLIN.bits() }
11254 ]),
11255 json!(250),
11256 ],
11257 },
11258 )
11259 .expect("poll kernel fds");
11260
11261 assert_eq!(response["readyCount"], Value::from(1));
11262 let fds: Vec<KernelPollFdResponse> =
11263 serde_json::from_value(response["fds"].clone()).expect("kernel poll fd response");
11264 assert_eq!(
11265 fds,
11266 vec![
11267 KernelPollFdResponse {
11268 fd: 0,
11269 events: POLLIN.bits(),
11270 revents: (POLLIN | POLLHUP).bits(),
11271 },
11272 KernelPollFdResponse {
11273 fd: 1,
11274 events: POLLIN.bits(),
11275 revents: 0,
11276 },
11277 ]
11278 );
11279
11280 process.kernel_handle.finish(0);
11281 kernel.waitpid(pid).expect("wait javascript kernel process");
11282 }
11283}
11284
11285fn dedupe_strings(values: &[String]) -> Vec<String> {
11286 let mut seen = BTreeSet::new();
11287 let mut deduped = Vec::new();
11288 for value in values {
11289 if seen.insert(value.clone()) {
11290 deduped.push(value.clone());
11291 }
11292 }
11293 deduped
11294}
11295
11296fn dedupe_host_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
11297 let mut seen = BTreeSet::new();
11298 let mut deduped = Vec::new();
11299 for path in paths {
11300 let normalized = normalize_host_path(path);
11301 let key = normalized.to_string_lossy().into_owned();
11302 if seen.insert(key) {
11303 deduped.push(normalized);
11304 }
11305 }
11306 deduped
11307}
11308
11309fn expand_host_access_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
11310 let mut expanded = Vec::new();
11311 let mut seen = BTreeSet::new();
11312
11313 let mut add_path = |candidate: PathBuf| {
11314 let normalized = normalize_host_path(&candidate);
11315 let key = normalized.to_string_lossy().into_owned();
11316 if seen.insert(key) {
11317 expanded.push(normalized);
11318 }
11319 };
11320
11321 for host_path in paths {
11322 add_path(host_path.clone());
11323 if let Ok(realpath) = fs::canonicalize(host_path) {
11324 add_path(realpath);
11325 }
11326
11327 if host_path.file_name().and_then(|name| name.to_str()) != Some("node_modules") {
11328 continue;
11329 }
11330
11331 let mut current = host_path.parent();
11332 while let Some(parent) = current {
11333 let candidate = parent.join("node_modules");
11334 if candidate.exists() {
11335 add_path(candidate.clone());
11336 if let Ok(realpath) = fs::canonicalize(&candidate) {
11337 add_path(realpath);
11338 }
11339 }
11340 current = parent.parent();
11341 }
11342 }
11343
11344 expanded
11345}
11346
11347fn prepare_javascript_shadow(
11348 vm: &mut VmState,
11349 resolved: &ResolvedChildProcessExecution,
11350) -> Result<(), SidecarError> {
11351 let guest_entrypoint = resolved
11352 .env
11353 .get("AGENTOS_GUEST_ENTRYPOINT")
11354 .cloned()
11355 .or_else(|| {
11363 resolve_host_entrypoint_within_vm_host_cwd(vm, &resolved.entrypoint)
11364 .map(|(guest_entrypoint, _)| guest_entrypoint)
11365 })
11366 .or_else(|| {
11367 resolved
11368 .entrypoint
11369 .starts_with('/')
11370 .then(|| normalize_path(&resolved.entrypoint))
11371 });
11372 let Some(guest_entrypoint) = guest_entrypoint else {
11373 return Ok(());
11374 };
11375 if host_mount_path_for_guest_path(vm, &guest_entrypoint).is_some() {
11376 return Ok(());
11377 }
11378 if vm.kernel.lstat(&guest_entrypoint).is_err() {
11379 let host_entrypoint = {
11380 let candidate = Path::new(&resolved.entrypoint);
11381 if candidate.is_absolute() {
11382 candidate.to_path_buf()
11383 } else {
11384 resolved.host_cwd.join(candidate)
11385 }
11386 };
11387 if host_entrypoint.exists() {
11388 materialize_host_path_to_shadow(vm, &guest_entrypoint, &host_entrypoint)?;
11389 return sync_shadow_entrypoint_into_kernel(vm, &guest_entrypoint);
11394 }
11395 }
11396 materialize_guest_path_to_shadow(vm, &guest_entrypoint)
11397}
11398
11399fn sync_shadow_entrypoint_into_kernel(
11404 vm: &mut VmState,
11405 guest_entrypoint: &str,
11406) -> Result<(), SidecarError> {
11407 if vm.kernel.exists(guest_entrypoint).unwrap_or(false) {
11408 return Ok(());
11409 }
11410 let shadow_path = shadow_path_for_guest(vm, guest_entrypoint);
11411 let bytes = match fs::read(&shadow_path) {
11412 Ok(bytes) => bytes,
11413 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
11414 Err(error) => {
11415 return Err(SidecarError::Io(format!(
11416 "failed to read staged shadow entrypoint {}: {error}",
11417 shadow_path.display()
11418 )));
11419 }
11420 };
11421 if let Some(parent) = guest_parent_path(guest_entrypoint) {
11422 if !vm.kernel.exists(&parent).unwrap_or(false) {
11423 vm.kernel.mkdir(&parent, true).map_err(kernel_error)?;
11424 }
11425 }
11426 vm.kernel
11427 .write_file(guest_entrypoint, bytes)
11428 .map_err(kernel_error)?;
11429 Ok(())
11430}
11431
11432fn guest_parent_path(guest_path: &str) -> Option<String> {
11433 let parent = Path::new(guest_path).parent()?;
11434 let parent = parent.to_string_lossy();
11435 if parent.is_empty() || parent == "/" {
11436 None
11437 } else {
11438 Some(parent.into_owned())
11439 }
11440}
11441
11442fn materialize_host_path_to_shadow(
11443 vm: &VmState,
11444 guest_path: &str,
11445 host_path: &Path,
11446) -> Result<(), SidecarError> {
11447 let shadow_path = shadow_path_for_guest(vm, guest_path);
11448 let metadata = fs::symlink_metadata(host_path)
11449 .map_err(|error| SidecarError::Io(format!("failed to stat host entrypoint: {error}")))?;
11450
11451 if metadata.file_type().is_symlink() {
11452 if let Some(parent) = shadow_path.parent() {
11453 fs::create_dir_all(parent).map_err(|error| {
11454 SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
11455 })?;
11456 }
11457 let _ = fs::remove_file(&shadow_path);
11458 let _ = fs::remove_dir_all(&shadow_path);
11459 let target = fs::read_link(host_path)
11460 .map_err(|error| SidecarError::Io(format!("failed to read host symlink: {error}")))?;
11461 std::os::unix::fs::symlink(&target, &shadow_path)
11462 .map_err(|error| SidecarError::Io(format!("failed to mirror host symlink: {error}")))?;
11463 return Ok(());
11464 }
11465
11466 if metadata.is_dir() {
11467 fs::create_dir_all(&shadow_path).map_err(|error| {
11468 SidecarError::Io(format!("failed to create shadow directory: {error}"))
11469 })?;
11470 fs::set_permissions(
11471 &shadow_path,
11472 fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
11473 )
11474 .map_err(|error| {
11475 SidecarError::Io(format!(
11476 "failed to set shadow directory mode on {}: {error}",
11477 shadow_path.display()
11478 ))
11479 })?;
11480 return Ok(());
11481 }
11482
11483 if let Some(parent) = shadow_path.parent() {
11484 fs::create_dir_all(parent).map_err(|error| {
11485 SidecarError::Io(format!("failed to create shadow parent: {error}"))
11486 })?;
11487 }
11488 let bytes = fs::read(host_path)
11489 .map_err(|error| SidecarError::Io(format!("failed to read host entrypoint: {error}")))?;
11490 fs::write(&shadow_path, bytes).map_err(|error| {
11491 SidecarError::Io(format!(
11492 "failed to mirror host file into shadow root: {error}"
11493 ))
11494 })?;
11495 fs::set_permissions(
11496 &shadow_path,
11497 fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
11498 )
11499 .map_err(|error| {
11500 SidecarError::Io(format!(
11501 "failed to set shadow file mode on {}: {error}",
11502 shadow_path.display()
11503 ))
11504 })?;
11505 Ok(())
11506}
11507
11508fn materialize_guest_path_to_shadow(
11509 vm: &mut VmState,
11510 guest_path: &str,
11511) -> Result<(), SidecarError> {
11512 let stat = vm.kernel.lstat(guest_path).map_err(kernel_error)?;
11513 let shadow_path = shadow_path_for_guest(vm, guest_path);
11514
11515 if stat.is_symbolic_link {
11516 if let Some(parent) = shadow_path.parent() {
11517 fs::create_dir_all(parent).map_err(|error| {
11518 SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
11519 })?;
11520 }
11521 let _ = fs::remove_file(&shadow_path);
11522 let _ = fs::remove_dir_all(&shadow_path);
11523 let target = vm.kernel.read_link(guest_path).map_err(kernel_error)?;
11524 std::os::unix::fs::symlink(&target, &shadow_path)
11525 .map_err(|error| SidecarError::Io(format!("failed to mirror symlink: {error}")))?;
11526 return Ok(());
11527 }
11528
11529 if stat.is_directory {
11530 fs::create_dir_all(&shadow_path).map_err(|error| {
11531 SidecarError::Io(format!("failed to create shadow directory: {error}"))
11532 })?;
11533 fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
11534 |error| {
11535 SidecarError::Io(format!(
11536 "failed to set shadow directory mode on {}: {error}",
11537 shadow_path.display()
11538 ))
11539 },
11540 )?;
11541 return Ok(());
11542 }
11543
11544 if let Some(parent) = shadow_path.parent() {
11545 fs::create_dir_all(parent).map_err(|error| {
11546 SidecarError::Io(format!("failed to create shadow parent: {error}"))
11547 })?;
11548 }
11549 let bytes = vm.kernel.read_file(guest_path).map_err(kernel_error)?;
11550 fs::write(&shadow_path, bytes).map_err(|error| {
11551 SidecarError::Io(format!(
11552 "failed to mirror guest file into shadow root: {error}"
11553 ))
11554 })?;
11555 fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
11556 |error| {
11557 SidecarError::Io(format!(
11558 "failed to set shadow file mode on {}: {error}",
11559 shadow_path.display()
11560 ))
11561 },
11562 )?;
11563 Ok(())
11564}
11565
11566fn load_javascript_entrypoint_source(
11567 vm: &mut VmState,
11568 host_cwd: &Path,
11569 entrypoint: &str,
11570 env: &BTreeMap<String, String>,
11571) -> Option<String> {
11572 let mut read_guest_file = |path: &str| {
11573 vm.kernel
11574 .read_file(path)
11575 .ok()
11576 .and_then(|bytes| String::from_utf8(bytes).ok())
11577 };
11578
11579 if let Some(source) = env
11580 .get("AGENTOS_GUEST_ENTRYPOINT")
11581 .filter(|path| path.starts_with('/'))
11582 .and_then(|path| read_guest_file(path))
11583 {
11584 return Some(source);
11585 }
11586
11587 if entrypoint.starts_with('/') {
11588 if let Some(source) = read_guest_file(entrypoint) {
11589 return Some(source);
11590 }
11591 }
11592
11593 let host_entrypoint = if Path::new(entrypoint).is_absolute() {
11594 PathBuf::from(entrypoint)
11595 } else {
11596 host_cwd.join(entrypoint)
11597 };
11598 let normalized_entrypoint = normalize_host_path(&host_entrypoint);
11599 let sandbox_root = normalize_host_path(&vm.cwd);
11600 let host_cwd = normalize_host_path(&vm.host_cwd);
11601 if !path_is_within_root(&normalized_entrypoint, &sandbox_root)
11602 && !path_is_within_root(&normalized_entrypoint, &host_cwd)
11603 {
11604 return None;
11605 }
11606
11607 fs::read_to_string(&normalized_entrypoint).ok()
11608}
11609
11610fn emit_dns_resolution_event<B>(
11611 bridge: &SharedBridge<B>,
11612 vm_id: &str,
11613 hostname: &str,
11614 source: KernelDnsResolutionSource,
11615 addresses: &[IpAddr],
11616 dns: &VmDnsConfig,
11617) where
11618 B: NativeSidecarBridge + Send + 'static,
11619 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11620{
11621 let _ = emit_structured_event(
11622 bridge,
11623 vm_id,
11624 "network.dns.resolved",
11625 audit_fields([
11626 ("hostname", hostname.to_owned()),
11627 ("source", source.as_str().to_owned()),
11628 (
11629 "addresses",
11630 addresses
11631 .iter()
11632 .map(ToString::to_string)
11633 .collect::<Vec<_>>()
11634 .join(","),
11635 ),
11636 ("address_count", addresses.len().to_string()),
11637 ("resolver_count", dns.name_servers.len().to_string()),
11638 (
11639 "resolvers",
11640 dns.name_servers
11641 .iter()
11642 .map(ToString::to_string)
11643 .collect::<Vec<_>>()
11644 .join(","),
11645 ),
11646 ]),
11647 );
11648}
11649
11650fn emit_dns_record_resolution_event<B>(
11651 bridge: &SharedBridge<B>,
11652 vm_id: &str,
11653 hostname: &str,
11654 resolution: &DnsRecordResolution,
11655 dns: &VmDnsConfig,
11656) where
11657 B: NativeSidecarBridge + Send + 'static,
11658 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11659{
11660 if let Some(addresses) = dns_resolution_ip_addrs(resolution.records()) {
11661 emit_dns_resolution_event(
11662 bridge,
11663 vm_id,
11664 hostname,
11665 resolution.source(),
11666 &addresses,
11667 dns,
11668 );
11669 return;
11670 }
11671
11672 let _ = emit_structured_event(
11673 bridge,
11674 vm_id,
11675 "network.dns.resolved",
11676 audit_fields([
11677 ("hostname", hostname.to_owned()),
11678 ("source", resolution.source().as_str().to_owned()),
11679 (
11680 "addresses",
11681 resolution
11682 .records()
11683 .iter()
11684 .map(summarize_dns_record)
11685 .collect::<Vec<_>>()
11686 .join(","),
11687 ),
11688 ("address_count", resolution.records().len().to_string()),
11689 ("resolver_count", dns.name_servers.len().to_string()),
11690 (
11691 "resolvers",
11692 dns.name_servers
11693 .iter()
11694 .map(ToString::to_string)
11695 .collect::<Vec<_>>()
11696 .join(","),
11697 ),
11698 ]),
11699 );
11700}
11701
11702fn emit_dns_resolution_failure_event<B>(
11703 bridge: &SharedBridge<B>,
11704 vm_id: &str,
11705 hostname: &str,
11706 dns: &VmDnsConfig,
11707 error: &SidecarError,
11708) where
11709 B: NativeSidecarBridge + Send + 'static,
11710 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11711{
11712 let _ = emit_structured_event(
11713 bridge,
11714 vm_id,
11715 "network.dns.resolve_failed",
11716 audit_fields([
11717 ("hostname", hostname.to_owned()),
11718 ("reason", error.to_string()),
11719 ("resolver_count", dns.name_servers.len().to_string()),
11720 (
11721 "resolvers",
11722 dns.name_servers
11723 .iter()
11724 .map(ToString::to_string)
11725 .collect::<Vec<_>>()
11726 .join(","),
11727 ),
11728 ]),
11729 );
11730}
11731
11732fn parse_dns_record_type(rrtype: &str) -> Result<RecordType, SidecarError> {
11733 match rrtype {
11734 "A" => Ok(RecordType::A),
11735 "AAAA" => Ok(RecordType::AAAA),
11736 "MX" => Ok(RecordType::MX),
11737 "TXT" => Ok(RecordType::TXT),
11738 "SRV" => Ok(RecordType::SRV),
11739 "CNAME" => Ok(RecordType::CNAME),
11740 "PTR" => Ok(RecordType::PTR),
11741 "NS" => Ok(RecordType::NS),
11742 "SOA" => Ok(RecordType::SOA),
11743 "NAPTR" => Ok(RecordType::NAPTR),
11744 "CAA" => Ok(RecordType::CAA),
11745 "ANY" => Ok(RecordType::ANY),
11746 other => Err(SidecarError::Execution(format!(
11747 "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
11748 ))),
11749 }
11750}
11751
11752fn dns_resolution_to_node_value(
11753 resolution: &DnsRecordResolution,
11754 requested_type: &str,
11755) -> Result<Value, SidecarError> {
11756 let safe_ips = dns_resolution_safe_ip_set(resolution.records(), resolution.hostname())?;
11757 match requested_type {
11758 "A" | "AAAA" => Ok(Value::Array(
11759 resolution
11760 .records()
11761 .iter()
11762 .filter_map(|record| dns_record_ip_string(record, &safe_ips))
11763 .map(Value::String)
11764 .collect(),
11765 )),
11766 "MX" => Ok(Value::Array(
11767 resolution
11768 .records()
11769 .iter()
11770 .filter_map(|record| match record.data() {
11771 RData::MX(mx) => Some(json!({
11772 "priority": mx.preference,
11773 "exchange": normalize_dns_name_for_node(&mx.exchange),
11774 "type": "MX",
11775 })),
11776 _ => None,
11777 })
11778 .collect(),
11779 )),
11780 "TXT" => Ok(Value::Array(
11781 resolution
11782 .records()
11783 .iter()
11784 .filter_map(|record| match record.data() {
11785 RData::TXT(txt) => Some(Value::Array(
11786 txt.txt_data
11787 .iter()
11788 .map(|entry| Value::String(String::from_utf8_lossy(entry).into_owned()))
11789 .collect(),
11790 )),
11791 _ => None,
11792 })
11793 .collect(),
11794 )),
11795 "SRV" => Ok(Value::Array(
11796 resolution
11797 .records()
11798 .iter()
11799 .filter_map(|record| match record.data() {
11800 RData::SRV(srv) => Some(json!({
11801 "priority": srv.priority,
11802 "weight": srv.weight,
11803 "port": srv.port,
11804 "name": normalize_dns_name_for_node(&srv.target),
11805 "type": "SRV",
11806 })),
11807 _ => None,
11808 })
11809 .collect(),
11810 )),
11811 "CNAME" => Ok(Value::Array(
11812 resolution
11813 .records()
11814 .iter()
11815 .filter_map(|record| match record.data() {
11816 RData::CNAME(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
11817 _ => None,
11818 })
11819 .collect(),
11820 )),
11821 "PTR" => Ok(Value::Array(
11822 resolution
11823 .records()
11824 .iter()
11825 .filter_map(|record| match record.data() {
11826 RData::PTR(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
11827 _ => None,
11828 })
11829 .collect(),
11830 )),
11831 "NS" => Ok(Value::Array(
11832 resolution
11833 .records()
11834 .iter()
11835 .filter_map(|record| match record.data() {
11836 RData::NS(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
11837 _ => None,
11838 })
11839 .collect(),
11840 )),
11841 "SOA" => resolution
11842 .records()
11843 .iter()
11844 .find_map(|record| match record.data() {
11845 RData::SOA(soa) => Some(json!({
11846 "nsname": normalize_dns_name_for_node(&soa.mname),
11847 "hostmaster": normalize_dns_name_for_node(&soa.rname),
11848 "serial": soa.serial,
11849 "refresh": soa.refresh,
11850 "retry": soa.retry,
11851 "expire": soa.expire,
11852 "minttl": soa.minimum,
11853 })),
11854 _ => None,
11855 })
11856 .ok_or_else(|| {
11857 SidecarError::Execution(String::from("failed to resolve DNS SOA record"))
11858 }),
11859 "NAPTR" => Ok(Value::Array(
11860 resolution
11861 .records()
11862 .iter()
11863 .filter_map(|record| match record.data() {
11864 RData::NAPTR(naptr) => Some(json!({
11865 "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
11866 "service": String::from_utf8_lossy(&naptr.services).into_owned(),
11867 "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
11868 "replacement": normalize_dns_name_for_node(&naptr.replacement),
11869 "order": naptr.order,
11870 "preference": naptr.preference,
11871 })),
11872 _ => None,
11873 })
11874 .collect(),
11875 )),
11876 "CAA" => Ok(Value::Array(
11877 resolution
11878 .records()
11879 .iter()
11880 .filter_map(|record| match record.data() {
11881 RData::CAA(caa) => {
11882 let mut value = serde_json::Map::new();
11883 value.insert(
11884 "critical".to_owned(),
11885 Value::from(u8::from(caa.issuer_critical)),
11886 );
11887 value.insert("type".to_owned(), Value::String(String::from("CAA")));
11888 if caa.tag.eq_ignore_ascii_case("iodef") {
11889 value.insert(
11890 "iodef".to_owned(),
11891 Value::String(
11892 caa.value_as_iodef()
11893 .map(|url| url.to_string())
11894 .unwrap_or_else(|_| {
11895 String::from_utf8_lossy(&caa.value).into_owned()
11896 }),
11897 ),
11898 );
11899 } else if let Ok((issuer, _params)) = caa.value_as_issue() {
11900 let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
11901 "issuewild"
11902 } else {
11903 "issue"
11904 };
11905 value.insert(
11906 field.to_owned(),
11907 Value::String(
11908 issuer.as_ref().map(ToString::to_string).unwrap_or_else(|| {
11909 String::from_utf8_lossy(&caa.value).into_owned()
11910 }),
11911 ),
11912 );
11913 } else {
11914 value.insert(
11915 caa.tag.to_ascii_lowercase(),
11916 Value::String(String::from_utf8_lossy(&caa.value).into_owned()),
11917 );
11918 }
11919 Some(Value::Object(value))
11920 }
11921 _ => None,
11922 })
11923 .collect(),
11924 )),
11925 "ANY" => Ok(Value::Array(
11926 resolution
11927 .records()
11928 .iter()
11929 .filter_map(|record| dns_any_record_to_value(record, &safe_ips))
11930 .collect(),
11931 )),
11932 other => Err(SidecarError::Execution(format!(
11933 "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
11934 ))),
11935 }
11936}
11937
11938fn dns_resolution_safe_ip_set(
11939 records: &[Record],
11940 hostname: &str,
11941) -> Result<BTreeSet<IpAddr>, SidecarError> {
11942 let ips = records
11943 .iter()
11944 .filter_map(dns_record_ip_addr)
11945 .collect::<Vec<_>>();
11946 if ips.is_empty() {
11947 return Ok(BTreeSet::new());
11948 }
11949 Ok(filter_dns_safe_ip_addrs(ips, hostname)?
11950 .into_iter()
11951 .collect())
11952}
11953
11954fn dns_resolution_ip_addrs(records: &[Record]) -> Option<Vec<IpAddr>> {
11955 let ips = records
11956 .iter()
11957 .filter_map(dns_record_ip_addr)
11958 .collect::<Vec<_>>();
11959 if ips.is_empty() {
11960 return None;
11961 }
11962 Some(ips)
11963}
11964
11965fn dns_record_ip_addr(record: &Record) -> Option<IpAddr> {
11966 match record.data() {
11967 RData::A(address) => Some(IpAddr::V4(**address)),
11968 RData::AAAA(address) => Some(IpAddr::V6(**address)),
11969 _ => None,
11970 }
11971}
11972
11973fn dns_record_ip_string(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<String> {
11974 let ip = dns_record_ip_addr(record)?;
11975 safe_ips.contains(&ip).then(|| ip.to_string())
11976}
11977
11978fn dns_any_record_to_value(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<Value> {
11979 let value = match record.data() {
11980 RData::A(_) | RData::AAAA(_) => json!({
11981 "address": dns_record_ip_string(record, safe_ips)?,
11982 "ttl": record.ttl(),
11983 "type": record.record_type().to_string(),
11984 }),
11985 RData::MX(mx) => json!({
11986 "exchange": normalize_dns_name_for_node(&mx.exchange),
11987 "priority": mx.preference,
11988 "type": "MX",
11989 }),
11990 RData::TXT(txt) => json!({
11991 "entries": txt
11992 .txt_data
11993 .iter()
11994 .map(|entry| String::from_utf8_lossy(entry).into_owned())
11995 .collect::<Vec<_>>(),
11996 "type": "TXT",
11997 }),
11998 RData::SRV(srv) => json!({
11999 "name": normalize_dns_name_for_node(&srv.target),
12000 "port": srv.port,
12001 "priority": srv.priority,
12002 "weight": srv.weight,
12003 "type": "SRV",
12004 }),
12005 RData::CNAME(name) => json!({
12006 "value": normalize_dns_name_for_node(&name.0),
12007 "type": "CNAME",
12008 }),
12009 RData::PTR(name) => json!({
12010 "value": normalize_dns_name_for_node(&name.0),
12011 "type": "PTR",
12012 }),
12013 RData::NS(name) => json!({
12014 "value": normalize_dns_name_for_node(&name.0),
12015 "type": "NS",
12016 }),
12017 RData::SOA(soa) => json!({
12018 "nsname": normalize_dns_name_for_node(&soa.mname),
12019 "hostmaster": normalize_dns_name_for_node(&soa.rname),
12020 "serial": soa.serial,
12021 "refresh": soa.refresh,
12022 "retry": soa.retry,
12023 "expire": soa.expire,
12024 "minttl": soa.minimum,
12025 "type": "SOA",
12026 }),
12027 RData::NAPTR(naptr) => json!({
12028 "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
12029 "service": String::from_utf8_lossy(&naptr.services).into_owned(),
12030 "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
12031 "replacement": normalize_dns_name_for_node(&naptr.replacement),
12032 "order": naptr.order,
12033 "preference": naptr.preference,
12034 "type": "NAPTR",
12035 }),
12036 RData::CAA(caa) => {
12037 let mut value = serde_json::Map::new();
12038 value.insert(
12039 "critical".to_owned(),
12040 Value::from(u8::from(caa.issuer_critical)),
12041 );
12042 value.insert("type".to_owned(), Value::String(String::from("CAA")));
12043 if caa.tag.eq_ignore_ascii_case("iodef") {
12044 value.insert(
12045 "iodef".to_owned(),
12046 Value::String(
12047 caa.value_as_iodef()
12048 .map(|url| url.to_string())
12049 .unwrap_or_else(|_| String::from_utf8_lossy(&caa.value).into_owned()),
12050 ),
12051 );
12052 } else if let Ok((issuer, _params)) = caa.value_as_issue() {
12053 let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
12054 "issuewild"
12055 } else {
12056 "issue"
12057 };
12058 value.insert(
12059 field.to_owned(),
12060 Value::String(
12061 issuer
12062 .as_ref()
12063 .map(ToString::to_string)
12064 .unwrap_or_else(|| String::from_utf8_lossy(&caa.value).into_owned()),
12065 ),
12066 );
12067 }
12068 Value::Object(value)
12069 }
12070 _ => return None,
12071 };
12072 Some(value)
12073}
12074
12075fn normalize_dns_name_for_node(name: &impl ToString) -> String {
12076 name.to_string().trim_end_matches('.').to_owned()
12077}
12078
12079fn summarize_dns_record(record: &Record) -> String {
12080 match record.data() {
12081 RData::A(_) | RData::AAAA(_) => record.data().to_string(),
12082 _ => format!("{} {}", record.record_type(), record.data()),
12083 }
12084}
12085
12086fn find_socket_state_entry(
12094 vm: Option<&VmState>,
12095 kind: SocketQueryKind,
12096 request: &FindListenerRequest,
12097) -> Result<Option<SocketStateEntry>, SidecarError> {
12098 let vm = vm.ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
12099
12100 for (process_id, process) in &vm.active_processes {
12101 if let Some(path) = request.path.as_deref() {
12102 if matches!(kind, SocketQueryKind::TcpListener) {
12103 for listener in process.unix_listeners.values() {
12104 if listener.path() != path {
12105 continue;
12106 }
12107 return Ok(Some(SocketStateEntry {
12108 process_id: process_id.to_owned(),
12109 host: None,
12110 port: None,
12111 path: Some(path.to_owned()),
12112 }));
12113 }
12114 }
12115 }
12116
12117 if request.path.is_none() {
12118 if let Some(entry) =
12119 find_kernel_socket_state_entry(&vm.kernel, process_id, process, kind, request)?
12120 {
12121 return Ok(Some(entry));
12122 }
12123
12124 match kind {
12125 SocketQueryKind::TcpListener => {
12126 for server in process.http_servers.values() {
12127 let local_addr = server.guest_local_addr;
12128 let local_host = local_addr.ip().to_string();
12129 if !socket_host_matches(request.host.as_deref(), &local_host) {
12130 continue;
12131 }
12132 if let Some(port) = request.port {
12133 if local_addr.port() != port {
12134 continue;
12135 }
12136 }
12137 return Ok(Some(SocketStateEntry {
12138 process_id: process_id.to_owned(),
12139 host: Some(local_host),
12140 port: Some(local_addr.port()),
12141 path: None,
12142 }));
12143 }
12144
12145 for listener in process.tcp_listeners.values() {
12146 if listener.kernel_socket_id.is_some() {
12147 continue;
12148 }
12149 let local_addr = listener.guest_local_addr();
12150 let local_host = local_addr.ip().to_string();
12151 if !socket_host_matches(request.host.as_deref(), &local_host) {
12152 continue;
12153 }
12154 if let Some(port) = request.port {
12155 if local_addr.port() != port {
12156 continue;
12157 }
12158 }
12159 return Ok(Some(SocketStateEntry {
12160 process_id: process_id.to_owned(),
12161 host: Some(local_host),
12162 port: Some(local_addr.port()),
12163 path: None,
12164 }));
12165 }
12166 }
12167 SocketQueryKind::UdpBound => {
12168 for socket in process.udp_sockets.values() {
12169 if socket.kernel_socket_id.is_some() {
12170 continue;
12171 }
12172 let Some(local_addr) = socket.local_addr() else {
12173 continue;
12174 };
12175 let local_host = local_addr.ip().to_string();
12176 if !socket_host_matches(request.host.as_deref(), &local_host) {
12177 continue;
12178 }
12179 if let Some(port) = request.port {
12180 if local_addr.port() != port {
12181 continue;
12182 }
12183 }
12184 return Ok(Some(SocketStateEntry {
12185 process_id: process_id.to_owned(),
12186 host: Some(local_host),
12187 port: Some(local_addr.port()),
12188 path: None,
12189 }));
12190 }
12191 }
12192 }
12193 }
12194
12195 let child_pid = process.execution.child_pid();
12196 let inodes = socket_inodes_for_pid(child_pid)?;
12197 if inodes.is_empty() {
12198 continue;
12199 }
12200
12201 if let Some(path) = request.path.as_deref() {
12202 if let Some(listener) = find_unix_socket_for_pid(child_pid, &inodes, path, process_id)?
12203 {
12204 return Ok(Some(listener));
12205 }
12206 continue;
12207 }
12208
12209 let table_paths = match kind {
12210 SocketQueryKind::TcpListener => [
12211 format!("/proc/{child_pid}/net/tcp"),
12212 format!("/proc/{child_pid}/net/tcp6"),
12213 ],
12214 SocketQueryKind::UdpBound => [
12215 format!("/proc/{child_pid}/net/udp"),
12216 format!("/proc/{child_pid}/net/udp6"),
12217 ],
12218 };
12219 for table_path in table_paths {
12220 if let Some(entry) = find_inet_socket_for_pid(
12221 &table_path,
12222 &inodes,
12223 kind,
12224 request.host.as_deref(),
12225 request.port,
12226 process_id,
12227 )? {
12228 return Ok(Some(entry));
12229 }
12230 }
12231 }
12232
12233 Ok(None)
12234}
12235
12236fn require_vm_inspection_permission<B>(
12237 bridge: &SharedBridge<B>,
12238 vm_id: &str,
12239 capability: &str,
12240 domain: &str,
12241 resource: &str,
12242) -> Result<(), SidecarError>
12243where
12244 B: NativeSidecarBridge + Send + 'static,
12245 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
12246{
12247 let decision = bridge.static_permission_decision(vm_id, capability, domain, Some(resource));
12248 if decision.as_ref().is_some_and(|decision| decision.allow) {
12249 return Ok(());
12250 }
12251
12252 let reason = decision
12253 .and_then(|decision| decision.reason)
12254 .unwrap_or_else(|| format!("{capability} permission required"));
12255 Err(SidecarError::Execution(format!(
12256 "EACCES: permission denied, {resource}: {reason}"
12257 )))
12258}
12259
12260fn socket_query_resource(kind: SocketQueryKind, request: &FindListenerRequest) -> String {
12261 if let Some(path) = request.path.as_deref() {
12262 return format!("unix://{path}");
12263 }
12264
12265 let host = request.host.as_deref().unwrap_or("*");
12266 let port = request
12267 .port
12268 .map_or_else(|| String::from("*"), |port| port.to_string());
12269 match kind {
12270 SocketQueryKind::TcpListener => format!("tcp://{host}:{port}"),
12271 SocketQueryKind::UdpBound => format!("udp://{host}:{port}"),
12272 }
12273}
12274
12275fn snapshot_vm_processes(vm: &VmState) -> Vec<ProcessSnapshotEntry> {
12276 let process_table = vm.kernel.list_processes();
12277 snapshot_vm_processes_inner(vm, &process_table)
12278}
12279
12280fn snapshot_vm_processes_inner(
12281 vm: &VmState,
12282 process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
12283) -> Vec<ProcessSnapshotEntry> {
12284 let mut entries = Vec::new();
12285
12286 for (process_id, process) in &vm.active_processes {
12287 collect_process_snapshot_entries(process_id, process, process_table, &mut entries);
12288 }
12289
12290 for exited in &vm.exited_process_snapshots {
12291 entries.push(exited.process.clone());
12292 }
12293
12294 entries
12295}
12296
12297fn prune_exited_process_snapshots(vm: &mut VmState) {
12298 let cutoff = Instant::now() - EXITED_PROCESS_SNAPSHOT_RETENTION;
12299 while vm
12300 .exited_process_snapshots
12301 .front()
12302 .is_some_and(|snapshot| snapshot.captured_at < cutoff)
12303 {
12304 vm.exited_process_snapshots.pop_front();
12305 }
12306}
12307
12308fn build_process_snapshot_entry(
12309 process_id: &str,
12310 process: &ActiveProcess,
12311 info: &secure_exec_kernel::process_table::ProcessInfo,
12312 exit_code: Option<i32>,
12313) -> ProcessSnapshotEntry {
12314 wire_process_snapshot_entry_from_shared(process_snapshot_entry_from_kernel(
12315 process_id,
12316 info,
12317 process.guest_cwd.clone(),
12318 exit_code,
12319 ))
12320}
12321
12322fn wire_process_snapshot_entry_from_shared(
12323 entry: SharedProcessSnapshotEntry,
12324) -> ProcessSnapshotEntry {
12325 ProcessSnapshotEntry {
12326 process_id: entry.process_id,
12327 pid: entry.pid,
12328 ppid: entry.ppid,
12329 pgid: entry.pgid,
12330 sid: entry.sid,
12331 driver: entry.driver,
12332 command: entry.command,
12333 args: entry.args,
12334 cwd: entry.cwd,
12335 status: match entry.status {
12336 SharedProcessSnapshotStatus::Running => ProcessSnapshotStatus::Running,
12337 SharedProcessSnapshotStatus::Stopped => ProcessSnapshotStatus::Stopped,
12338 SharedProcessSnapshotStatus::Exited => ProcessSnapshotStatus::Exited,
12339 },
12340 exit_code: entry.exit_code,
12341 }
12342}
12343
12344fn collect_process_snapshot_entries(
12345 process_id: &str,
12346 process: &ActiveProcess,
12347 process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
12348 entries: &mut Vec<ProcessSnapshotEntry>,
12349) {
12350 if let Some(info) = process_table.get(&process.kernel_pid) {
12351 entries.push(build_process_snapshot_entry(
12352 process_id, process, info, None,
12353 ));
12354 }
12355
12356 for (child_id, child) in &process.child_processes {
12357 let child_process_id = format!("{process_id}/{child_id}");
12358 collect_process_snapshot_entries(&child_process_id, child, process_table, entries);
12359 }
12360}
12361
12362fn find_kernel_socket_state_entry(
12363 kernel: &SidecarKernel,
12364 process_id: &str,
12365 process: &ActiveProcess,
12366 kind: SocketQueryKind,
12367 request: &FindListenerRequest,
12368) -> Result<Option<SocketStateEntry>, SidecarError> {
12369 let entry = match kind {
12370 SocketQueryKind::TcpListener => process
12371 .tcp_listeners
12372 .values()
12373 .filter_map(|listener| listener.kernel_socket_id)
12374 .find_map(|socket_id| {
12375 kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
12376 }),
12377 SocketQueryKind::UdpBound => process
12378 .udp_sockets
12379 .values()
12380 .filter_map(|socket| socket.kernel_socket_id)
12381 .find_map(|socket_id| {
12382 kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
12383 }),
12384 };
12385
12386 if entry.is_some() {
12387 return Ok(entry);
12388 }
12389
12390 for child in process.child_processes.values() {
12391 if let Some(entry) =
12392 find_kernel_socket_state_entry(kernel, process_id, child, kind, request)?
12393 {
12394 return Ok(Some(entry));
12395 }
12396 }
12397
12398 Ok(None)
12399}
12400
12401fn kernel_socket_state_entry(
12402 kernel: &SidecarKernel,
12403 process_id: &str,
12404 socket_id: SocketId,
12405 kind: SocketQueryKind,
12406 request: &FindListenerRequest,
12407) -> Option<SocketStateEntry> {
12408 let record = kernel.socket_get(socket_id)?;
12409 let local_address = record.local_address()?;
12410 match kind {
12411 SocketQueryKind::TcpListener if record.state() == SocketState::Listening => {}
12412 SocketQueryKind::TcpListener => return None,
12413 SocketQueryKind::UdpBound => {}
12414 }
12415
12416 if !socket_host_matches(request.host.as_deref(), local_address.host()) {
12417 return None;
12418 }
12419 if request
12420 .port
12421 .is_some_and(|port| local_address.port() != port)
12422 {
12423 return None;
12424 }
12425
12426 Some(SocketStateEntry {
12427 process_id: process_id.to_owned(),
12428 host: Some(local_address.host().to_owned()),
12429 port: Some(local_address.port()),
12430 path: None,
12431 })
12432}
12433
12434fn socket_inodes_for_pid(pid: u32) -> Result<BTreeSet<u64>, SidecarError> {
12435 let fd_dir = PathBuf::from(format!("/proc/{pid}/fd"));
12436 let entries = match fs::read_dir(&fd_dir) {
12437 Ok(entries) => entries,
12438 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(BTreeSet::new()),
12439 Err(error) => {
12440 return Err(SidecarError::Io(format!(
12441 "failed to read socket descriptors for process {pid}: {error}"
12442 )));
12443 }
12444 };
12445
12446 let mut inodes = BTreeSet::new();
12447 for entry in entries {
12448 let entry = entry.map_err(|error| {
12449 SidecarError::Io(format!(
12450 "failed to inspect fd entry for process {pid}: {error}"
12451 ))
12452 })?;
12453 let target = match fs::read_link(entry.path()) {
12454 Ok(target) => target,
12455 Err(_) => continue,
12456 };
12457 if let Some(inode) = parse_socket_inode(&target) {
12458 inodes.insert(inode);
12459 }
12460 }
12461
12462 Ok(inodes)
12463}
12464
12465fn parse_socket_inode(target: &Path) -> Option<u64> {
12466 let value = target.to_string_lossy();
12467 let trimmed = value.strip_prefix("socket:[")?.strip_suffix(']')?;
12468 trimmed.parse().ok()
12469}
12470
12471fn unix_socket_path(addr: &UnixSocketAddr) -> Option<String> {
12472 addr.as_pathname()
12473 .map(|path| path.to_string_lossy().into_owned())
12474}
12475
12476fn find_unix_socket_for_pid(
12477 pid: u32,
12478 inodes: &BTreeSet<u64>,
12479 path: &str,
12480 process_id: &str,
12481) -> Result<Option<SocketStateEntry>, SidecarError> {
12482 let table_path = format!("/proc/{pid}/net/unix");
12483 let contents = match fs::read_to_string(&table_path) {
12484 Ok(contents) => contents,
12485 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
12486 Err(error) => {
12487 return Err(SidecarError::Io(format!(
12488 "failed to inspect unix sockets for process {pid}: {error}"
12489 )));
12490 }
12491 };
12492
12493 for line in contents.lines().skip(1) {
12494 let columns = line.split_whitespace().collect::<Vec<_>>();
12495 if columns.len() < 8 {
12496 continue;
12497 }
12498 let Ok(inode) = columns[6].parse::<u64>() else {
12499 continue;
12500 };
12501 if !inodes.contains(&inode) || columns[7] != path {
12502 continue;
12503 }
12504 return Ok(Some(SocketStateEntry {
12505 process_id: process_id.to_owned(),
12506 host: None,
12507 port: None,
12508 path: Some(path.to_owned()),
12509 }));
12510 }
12511
12512 Ok(None)
12513}
12514
12515fn find_inet_socket_for_pid(
12516 table_path: &str,
12517 inodes: &BTreeSet<u64>,
12518 kind: SocketQueryKind,
12519 requested_host: Option<&str>,
12520 requested_port: Option<u16>,
12521 process_id: &str,
12522) -> Result<Option<SocketStateEntry>, SidecarError> {
12523 for entry in parse_proc_net_entries(table_path)? {
12524 if !inodes.contains(&entry.inode) {
12525 continue;
12526 }
12527 if matches!(kind, SocketQueryKind::TcpListener) && entry.state != "0A" {
12528 continue;
12529 }
12530 if !socket_host_matches(requested_host, &entry.local_host) {
12531 continue;
12532 }
12533 if let Some(port) = requested_port {
12534 if entry.local_port != port {
12535 continue;
12536 }
12537 }
12538 return Ok(Some(SocketStateEntry {
12539 process_id: process_id.to_owned(),
12540 host: Some(entry.local_host),
12541 port: Some(entry.local_port),
12542 path: None,
12543 }));
12544 }
12545
12546 Ok(None)
12547}
12548
12549fn is_unspecified_socket_host(host: &str) -> bool {
12550 host == "0.0.0.0" || host == "::"
12551}
12552
12553fn is_loopback_socket_host(host: &str) -> bool {
12554 host == "127.0.0.1" || host == "::1" || host.eq_ignore_ascii_case("localhost")
12555}
12556
12557pub(crate) fn vm_network_resource_counts(vm: &VmState) -> NetworkResourceCounts {
12558 let snapshot = vm.kernel.resource_snapshot();
12559 let mut counts = NetworkResourceCounts {
12560 sockets: snapshot.sockets,
12561 connections: snapshot.socket_connections,
12562 };
12563 for process in vm.active_processes.values() {
12564 let process_counts = process.sidecar_only_network_resource_counts();
12565 counts.sockets += process_counts.sockets;
12566 counts.connections += process_counts.connections;
12567 }
12568 counts
12569}
12570
12571#[allow(clippy::too_many_arguments)]
12572fn collect_javascript_socket_port_state(
12573 kernel: &SidecarKernel,
12574 process_id: &str,
12575 process: &ActiveProcess,
12576 tcp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
12577 http_loopback_targets: &mut BTreeMap<
12578 (JavascriptSocketFamily, u16),
12579 JavascriptHttpLoopbackTarget,
12580 >,
12581 udp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
12582 udp_host_to_guest: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
12583 used_tcp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
12584 used_udp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
12585) {
12586 for (family, port) in process.tcp_port_reservations.values() {
12587 used_tcp_ports.entry(*family).or_default().insert(*port);
12588 }
12589
12590 let mut record_tcp_listener = |guest_addr: SocketAddr, host_port: u16| {
12591 let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
12592 used_tcp_ports
12593 .entry(family)
12594 .or_default()
12595 .insert(guest_addr.port());
12596 tcp_guest_to_host.insert((family, guest_addr.port()), host_port);
12599 };
12600
12601 for listener in process.tcp_listeners.values() {
12602 let local_addr = listener
12603 .kernel_socket_id
12604 .and_then(|socket_id| kernel.socket_get(socket_id))
12605 .and_then(|record| record.local_address().cloned())
12606 .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
12607 .unwrap_or_else(|| listener.guest_local_addr());
12608 record_tcp_listener(local_addr, local_addr.port());
12609 }
12610
12611 for (server_id, server) in &process.http_servers {
12612 let host_port = match server.listener.local_addr() {
12613 Ok(addr) => addr.port(),
12614 Err(_) => continue,
12615 };
12616 record_tcp_listener(server.guest_local_addr, host_port);
12617 let family = JavascriptSocketFamily::from_ip(server.guest_local_addr.ip());
12618 http_loopback_targets.insert(
12619 (family, server.guest_local_addr.port()),
12620 JavascriptHttpLoopbackTarget {
12621 process_id: process_id.to_owned(),
12622 server_id: *server_id,
12623 },
12624 );
12625 }
12626
12627 if let Ok(http2) = process.http2.shared.lock() {
12628 for server in http2.servers.values() {
12629 record_tcp_listener(server.guest_local_addr, server.actual_local_addr.port());
12630 }
12631 }
12632
12633 for socket in process.tcp_sockets.values() {
12634 let guest_addr = socket
12635 .kernel_socket_id
12636 .and_then(|socket_id| kernel.socket_get(socket_id))
12637 .and_then(|record| record.local_address().cloned())
12638 .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
12639 .unwrap_or(socket.guest_local_addr);
12640 let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
12641 used_tcp_ports
12642 .entry(family)
12643 .or_default()
12644 .insert(guest_addr.port());
12645 }
12646
12647 for socket in process.udp_sockets.values() {
12648 let guest_addr = socket
12649 .kernel_socket_id
12650 .and_then(|socket_id| kernel.socket_get(socket_id))
12651 .and_then(|record| record.local_address().cloned())
12652 .and_then(|address| {
12653 resolve_udp_bind_addr(address.host(), address.port(), socket.family).ok()
12654 })
12655 .or_else(|| socket.local_addr());
12656 let Some(guest_addr) = guest_addr else {
12657 continue;
12658 };
12659 let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
12660 used_udp_ports
12661 .entry(family)
12662 .or_default()
12663 .insert(guest_addr.port());
12664 if let Some(host_addr) = socket
12665 .socket
12666 .as_ref()
12667 .and_then(|socket| socket.local_addr().ok())
12668 {
12669 if is_loopback_ip(guest_addr.ip()) {
12670 udp_guest_to_host.insert((family, guest_addr.port()), host_addr.port());
12671 udp_host_to_guest.insert((family, host_addr.port()), guest_addr.port());
12672 }
12673 } else if socket.kernel_socket_id.is_some() && is_loopback_ip(guest_addr.ip()) {
12674 udp_guest_to_host.insert((family, guest_addr.port()), guest_addr.port());
12675 udp_host_to_guest.insert((family, guest_addr.port()), guest_addr.port());
12676 }
12677 }
12678
12679 for (child_process_id, child) in &process.child_processes {
12680 let child_id = format!("{process_id}/{child_process_id}");
12681 collect_javascript_socket_port_state(
12682 kernel,
12683 &child_id,
12684 child,
12685 tcp_guest_to_host,
12686 http_loopback_targets,
12687 udp_guest_to_host,
12688 udp_host_to_guest,
12689 used_tcp_ports,
12690 used_udp_ports,
12691 );
12692 }
12693}
12694
12695pub(crate) fn build_javascript_socket_path_context(
12696 vm: &VmState,
12697) -> Result<JavascriptSocketPathContext, SidecarError> {
12698 let mut loopback_exempt_ports = vm.create_loopback_exempt_ports.clone();
12699 loopback_exempt_ports.extend(vm.configuration.loopback_exempt_ports.iter().copied());
12700 let mut tcp_loopback_guest_to_host_ports = BTreeMap::new();
12701 let mut http_loopback_targets = BTreeMap::new();
12702 let mut udp_loopback_guest_to_host_ports = BTreeMap::new();
12703 let mut udp_loopback_host_to_guest_ports = BTreeMap::new();
12704 let mut used_tcp_guest_ports = BTreeMap::new();
12705 let mut used_udp_guest_ports = BTreeMap::new();
12706 for (process_id, process) in &vm.active_processes {
12707 collect_javascript_socket_port_state(
12708 &vm.kernel,
12709 process_id,
12710 process,
12711 &mut tcp_loopback_guest_to_host_ports,
12712 &mut http_loopback_targets,
12713 &mut udp_loopback_guest_to_host_ports,
12714 &mut udp_loopback_host_to_guest_ports,
12715 &mut used_tcp_guest_ports,
12716 &mut used_udp_guest_ports,
12717 );
12718 }
12719 Ok(JavascriptSocketPathContext {
12720 sandbox_root: vm.cwd.clone(),
12721 mounts: vm.configuration.mounts.clone(),
12722 listen_policy: vm.listen_policy,
12723 loopback_exempt_ports,
12724 tcp_loopback_guest_to_host_ports,
12725 http_loopback_targets,
12726 udp_loopback_guest_to_host_ports,
12727 udp_loopback_host_to_guest_ports,
12728 used_tcp_guest_ports,
12729 used_udp_guest_ports,
12730 })
12731}
12732
12733fn check_network_resource_limit(
12734 limit: Option<usize>,
12735 current: usize,
12736 additional: usize,
12737 label: &str,
12738) -> Result<(), SidecarError> {
12739 if let Some(limit) = limit {
12740 if current.saturating_add(additional) > limit {
12741 return Err(SidecarError::Execution(format!(
12742 "EAGAIN: maximum {label} count reached"
12743 )));
12744 }
12745 }
12746 Ok(())
12747}
12748
12749fn normalize_tcp_listen_host(
12750 host: Option<&str>,
12751) -> Result<(JavascriptSocketFamily, &'static str, &'static str), SidecarError> {
12752 match host.unwrap_or("127.0.0.1") {
12753 "127.0.0.1" | "localhost" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "127.0.0.1")),
12754 "::1" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::1")),
12755 "0.0.0.0" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "0.0.0.0")),
12756 "::" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::")),
12757 other => Err(SidecarError::Execution(format!(
12758 "EACCES: TCP listeners must bind to loopback or unspecified addresses, got {other}"
12759 ))),
12760 }
12761}
12762
12763fn normalize_udp_bind_host(
12764 host: Option<&str>,
12765 family: JavascriptUdpFamily,
12766) -> Result<(&'static str, &'static str, JavascriptSocketFamily), SidecarError> {
12767 match (family, host) {
12768 (JavascriptUdpFamily::Ipv4, None) | (JavascriptUdpFamily::Ipv4, Some("0.0.0.0")) => {
12769 Ok(("127.0.0.1", "0.0.0.0", JavascriptSocketFamily::Ipv4))
12770 }
12771 (JavascriptUdpFamily::Ipv4, Some("127.0.0.1"))
12772 | (JavascriptUdpFamily::Ipv4, Some("localhost")) => {
12773 Ok(("127.0.0.1", "127.0.0.1", JavascriptSocketFamily::Ipv4))
12774 }
12775 (JavascriptUdpFamily::Ipv6, None) | (JavascriptUdpFamily::Ipv6, Some("::")) => {
12776 Ok(("::1", "::", JavascriptSocketFamily::Ipv6))
12777 }
12778 (JavascriptUdpFamily::Ipv6, Some("::1"))
12779 | (JavascriptUdpFamily::Ipv6, Some("localhost")) => {
12780 Ok(("::1", "::1", JavascriptSocketFamily::Ipv6))
12781 }
12782 (JavascriptUdpFamily::Ipv4, Some(other)) => Err(SidecarError::Execution(format!(
12783 "EACCES: udp4 sockets must bind to 127.0.0.1 or 0.0.0.0, got {other}"
12784 ))),
12785 (JavascriptUdpFamily::Ipv6, Some(other)) => Err(SidecarError::Execution(format!(
12786 "EACCES: udp6 sockets must bind to ::1 or ::, got {other}"
12787 ))),
12788 }
12789}
12790
12791fn allocate_guest_listen_port(
12792 requested_port: u16,
12793 family: JavascriptSocketFamily,
12794 used_ports: &BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
12795 policy: VmListenPolicy,
12796) -> Result<u16, SidecarError> {
12797 let is_allowed = |port: u16| {
12798 port >= policy.port_min
12799 && port <= policy.port_max
12800 && (policy.allow_privileged || port >= 1024)
12801 };
12802 let used = used_ports.get(&family);
12803
12804 if requested_port != 0 {
12805 if !is_allowed(requested_port) {
12806 let reason = if requested_port < 1024 && !policy.allow_privileged {
12807 format!(
12808 "EACCES: privileged listen port {requested_port} requires {}=true",
12809 VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY
12810 )
12811 } else {
12812 format!(
12813 "EACCES: listen port {requested_port} is outside the allowed range {}-{}",
12814 policy.port_min, policy.port_max
12815 )
12816 };
12817 return Err(SidecarError::Execution(reason));
12818 }
12819 if used.is_some_and(|ports| ports.contains(&requested_port)) {
12820 return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
12821 libc::EADDRINUSE,
12822 )));
12823 }
12824 return Ok(requested_port);
12825 }
12826
12827 let allocation_start = policy
12828 .port_min
12829 .max(if policy.allow_privileged { 1 } else { 1024 });
12830 for candidate in allocation_start..=policy.port_max {
12831 if used.is_some_and(|ports| ports.contains(&candidate)) {
12832 continue;
12833 }
12834 return Ok(candidate);
12835 }
12836
12837 Err(sidecar_net_error(std::io::Error::from_raw_os_error(
12838 libc::EADDRINUSE,
12839 )))
12840}
12841
12842fn socket_host_matches(requested: Option<&str>, actual: &str) -> bool {
12843 match requested {
12844 None => true,
12845 Some(requested) if requested == actual => true,
12846 Some(requested)
12847 if is_unspecified_socket_host(requested) && is_unspecified_socket_host(actual) =>
12848 {
12849 true
12850 }
12851 Some(requested) if is_unspecified_socket_host(requested) => is_loopback_socket_host(actual),
12852 Some(requested) if requested.eq_ignore_ascii_case("localhost") => {
12853 is_loopback_socket_host(actual)
12854 }
12855 _ => false,
12856 }
12857}
12858
12859fn parse_proc_net_entries(table_path: &str) -> Result<Vec<ProcNetEntry>, SidecarError> {
12860 let contents = match fs::read_to_string(table_path) {
12861 Ok(contents) => contents,
12862 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
12863 Err(error) => {
12864 return Err(SidecarError::Io(format!(
12865 "failed to inspect socket table {table_path}: {error}"
12866 )));
12867 }
12868 };
12869
12870 let mut entries = Vec::new();
12871 for line in contents.lines().skip(1) {
12872 let columns = line.split_whitespace().collect::<Vec<_>>();
12873 if columns.len() < 10 {
12874 continue;
12875 }
12876 let Some((host, port)) = parse_proc_ip_port(columns[1]) else {
12877 continue;
12878 };
12879 let Ok(inode) = columns[9].parse::<u64>() else {
12880 continue;
12881 };
12882 entries.push(ProcNetEntry {
12883 local_host: host,
12884 local_port: port,
12885 state: columns[3].to_owned(),
12886 inode,
12887 });
12888 }
12889
12890 Ok(entries)
12891}
12892
12893fn parse_proc_ip_port(value: &str) -> Option<(String, u16)> {
12894 let (raw_ip, raw_port) = value.split_once(':')?;
12895 let port = u16::from_str_radix(raw_port, 16).ok()?;
12896 let host = match raw_ip.len() {
12897 8 => {
12898 let raw = u32::from_str_radix(raw_ip, 16).ok()?;
12899 Ipv4Addr::from(raw.to_le_bytes()).to_string()
12900 }
12901 32 => {
12902 let mut bytes = [0_u8; 16];
12903 for (index, chunk) in raw_ip.as_bytes().chunks(8).enumerate() {
12904 let word = u32::from_str_radix(std::str::from_utf8(chunk).ok()?, 16).ok()?;
12905 bytes[index * 4..(index + 1) * 4].copy_from_slice(&word.to_le_bytes());
12906 }
12907 Ipv6Addr::from(bytes).to_string()
12908 }
12909 _ => return None,
12910 };
12911 Some((host, port))
12912}
12913
12914fn python_file_entrypoint(entrypoint: &str) -> Option<PathBuf> {
12915 let path = Path::new(entrypoint);
12916 (path.extension().and_then(|extension| extension.to_str()) == Some("py"))
12917 .then(|| path.to_path_buf())
12918}
12919
12920fn add_runtime_guest_path_mapping(
12921 env: &mut BTreeMap<String, String>,
12922 guest_path: &str,
12923 host_path: &Path,
12924) {
12925 let mut mappings = env
12926 .get("AGENTOS_GUEST_PATH_MAPPINGS")
12927 .and_then(|value| serde_json::from_str::<Vec<Value>>(value).ok())
12928 .unwrap_or_default();
12929 mappings.retain(|mapping| {
12930 mapping
12931 .get("guestPath")
12932 .and_then(Value::as_str)
12933 .map(|existing| normalize_path(existing) != normalize_path(guest_path))
12934 .unwrap_or(true)
12935 });
12936 mappings.push(json!({
12937 "guestPath": normalize_path(guest_path),
12938 "hostPath": host_path.display().to_string(),
12939 }));
12940 if let Ok(serialized) = serde_json::to_string(&mappings) {
12941 env.insert(String::from("AGENTOS_GUEST_PATH_MAPPINGS"), serialized);
12942 }
12943}
12944
12945fn add_runtime_host_access_path(
12946 env: &mut BTreeMap<String, String>,
12947 key: &str,
12948 host_path: &Path,
12949 expand: bool,
12950) {
12951 let existing = env
12952 .get(key)
12953 .and_then(|value| serde_json::from_str::<Vec<String>>(value).ok())
12954 .unwrap_or_default()
12955 .into_iter()
12956 .map(PathBuf::from)
12957 .collect::<Vec<_>>();
12958 let mut paths = existing;
12959 paths.push(host_path.to_path_buf());
12960 let normalized = if expand {
12961 expand_host_access_paths(&paths)
12962 } else {
12963 dedupe_host_paths(&paths)
12964 };
12965 let serialized = normalized
12966 .iter()
12967 .map(|path| path.to_string_lossy().into_owned())
12968 .collect::<Vec<_>>();
12969 if let Ok(serialized) = serde_json::to_string(&serialized) {
12970 env.insert(key.to_owned(), serialized);
12971 }
12972}
12973
12974fn is_path_like_specifier(specifier: &str) -> bool {
12977 specifier.starts_with('/')
12978 || specifier.starts_with("./")
12979 || specifier.starts_with("../")
12980 || specifier.starts_with("file:")
12981}
12982
12983fn execution_wasm_permission_tier(tier: WasmPermissionTier) -> ExecutionWasmPermissionTier {
12984 match tier {
12985 WasmPermissionTier::Full => ExecutionWasmPermissionTier::Full,
12986 WasmPermissionTier::ReadWrite => ExecutionWasmPermissionTier::ReadWrite,
12987 WasmPermissionTier::ReadOnly => ExecutionWasmPermissionTier::ReadOnly,
12988 WasmPermissionTier::Isolated => ExecutionWasmPermissionTier::Isolated,
12989 }
12990}
12991
12992fn resolve_wasm_permission_tier(
12993 vm: &VmState,
12994 command_name: Option<&str>,
12995 explicit_tier: Option<WasmPermissionTier>,
12996 entrypoint: &str,
12997) -> WasmPermissionTier {
12998 explicit_tier
12999 .or_else(|| command_name.and_then(|command| vm.command_permissions.get(command).copied()))
13000 .or_else(|| {
13001 Path::new(entrypoint)
13002 .file_name()
13003 .and_then(|name| name.to_str())
13004 .and_then(|command| vm.command_permissions.get(command).copied())
13005 })
13006 .unwrap_or(WasmPermissionTier::Full)
13007}
13008
13009fn tokenize_shell_free_command(command: &str) -> Vec<String> {
13010 command
13011 .split_whitespace()
13012 .filter(|segment| !segment.is_empty())
13013 .map(str::to_owned)
13014 .collect()
13015}
13016
13017fn is_posix_shell_builtin(command: &str) -> bool {
13018 matches!(
13019 command,
13020 "." | ":"
13021 | "break"
13022 | "cd"
13023 | "continue"
13024 | "eval"
13025 | "exec"
13026 | "exit"
13027 | "export"
13028 | "readonly"
13029 | "return"
13030 | "set"
13031 | "shift"
13032 | "times"
13033 | "trap"
13034 | "umask"
13035 | "unset"
13036 )
13037}
13038
13039fn shell_first_token_requires_shell(token: &str) -> bool {
13045 token.contains('=') || is_shell_reserved_word(token)
13046}
13047
13048fn is_shell_reserved_word(token: &str) -> bool {
13049 matches!(
13050 token,
13051 "if" | "then"
13052 | "elif"
13053 | "else"
13054 | "fi"
13055 | "for"
13056 | "in"
13057 | "do"
13058 | "done"
13059 | "while"
13060 | "until"
13061 | "case"
13062 | "esac"
13063 | "{"
13064 | "}"
13065 | "!"
13066 )
13067}
13068
13069fn command_requires_shell(command: &str) -> bool {
13070 command.chars().any(|ch| {
13071 matches!(
13072 ch,
13073 '|' | '&'
13074 | ';'
13075 | '<'
13076 | '>'
13077 | '('
13078 | ')'
13079 | '$'
13080 | '`'
13081 | '*'
13082 | '?'
13083 | '['
13084 | ']'
13085 | '{'
13086 | '}'
13087 | '~'
13088 | '\''
13089 | '"'
13090 | '\\'
13091 | '\n'
13092 )
13093 })
13094}
13095
13096fn host_mount_path_for_guest_path(vm: &VmState, guest_path: &str) -> Option<PathBuf> {
13097 let normalized = normalize_path(guest_path);
13098
13099 let mut mounts = vm
13100 .configuration
13101 .mounts
13102 .iter()
13103 .filter_map(|mount| {
13104 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
13105 .then(|| {
13106 mount_config_host_path(&mount.plugin.config)
13107 .map(|host_path| (mount.guest_path.as_str(), host_path))
13108 })
13109 .flatten()
13110 })
13111 .collect::<Vec<_>>();
13112 mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
13113
13114 for (guest_root, host_root) in mounts {
13115 if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
13116 continue;
13117 }
13118
13119 let suffix = normalized
13120 .strip_prefix(guest_root)
13121 .unwrap_or_default()
13122 .trim_start_matches('/');
13123 let mut path = PathBuf::from(host_root);
13124 if !suffix.is_empty() {
13125 path.push(suffix);
13126 }
13127 return Some(path);
13128 }
13129
13130 None
13131}
13132
13133fn host_runtime_path_for_guest_path_with_env(
13134 vm: &VmState,
13135 runtime_env: &BTreeMap<String, String>,
13136 guest_path: &str,
13137 default_host_cwd: &Path,
13138) -> Option<PathBuf> {
13139 if let Some(path) = host_mount_path_for_guest_path(vm, guest_path) {
13140 return Some(path);
13141 }
13142 if let Some(path) = host_path_from_runtime_guest_mappings(runtime_env, guest_path) {
13143 return Some(path);
13144 }
13145
13146 let normalized = normalize_path(guest_path);
13147 let virtual_home = guest_virtual_home(vm);
13148
13149 if normalized == virtual_home || normalized.starts_with(&format!("{virtual_home}/")) {
13150 let suffix = normalized
13151 .strip_prefix(&virtual_home)
13152 .unwrap_or_default()
13153 .trim_start_matches('/');
13154 let mut host_path = default_host_cwd.to_path_buf();
13155 if !suffix.is_empty() {
13156 host_path.push(suffix);
13157 }
13158 return Some(host_path);
13159 }
13160
13161 None
13162}
13163
13164#[derive(Deserialize, Serialize)]
13165struct RuntimeGuestPathMapping {
13166 #[serde(rename = "guestPath")]
13167 guest_path: String,
13168 #[serde(rename = "hostPath")]
13169 host_path: String,
13170 #[serde(rename = "readOnly", default)]
13171 read_only: bool,
13172}
13173
13174pub(crate) fn host_path_from_runtime_guest_mappings(
13175 runtime_env: &BTreeMap<String, String>,
13176 guest_path: &str,
13177) -> Option<PathBuf> {
13178 let mappings = runtime_env
13179 .get("AGENTOS_GUEST_PATH_MAPPINGS")
13180 .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
13181 let normalized = normalize_path(guest_path);
13182
13183 let mut sorted_mappings = mappings
13184 .into_iter()
13185 .filter_map(|mapping| {
13186 (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
13187 normalize_path(&mapping.guest_path),
13188 PathBuf::from(mapping.host_path),
13189 ))
13190 })
13191 .collect::<Vec<_>>();
13192 sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.0.len()));
13193
13194 for (guest_root, mut host_root) in sorted_mappings {
13195 if guest_root != "/"
13196 && normalized != guest_root
13197 && !normalized.starts_with(&format!("{guest_root}/"))
13198 {
13199 continue;
13200 }
13201 if guest_root == "/" && !normalized.starts_with('/') {
13202 continue;
13203 }
13204
13205 if host_root.is_relative() {
13206 host_root = std::env::current_dir().ok()?.join(host_root);
13207 }
13208
13209 let suffix = if guest_root == "/" {
13210 normalized.trim_start_matches('/')
13211 } else {
13212 normalized
13213 .strip_prefix(&guest_root)
13214 .unwrap_or_default()
13215 .trim_start_matches('/')
13216 };
13217 if !suffix.is_empty() {
13218 host_root.push(suffix);
13219 }
13220 return Some(host_root);
13221 }
13222
13223 None
13224}
13225
13226fn guest_runtime_path_for_host_path(
13227 runtime_env: &BTreeMap<String, String>,
13228 virtual_home: &str,
13229 cwd: &Path,
13230 host_path: &str,
13231) -> Option<String> {
13232 let resolved = if host_path.starts_with("file://") {
13233 PathBuf::from(host_path.trim_start_matches("file://"))
13234 } else if host_path.starts_with("file:") {
13235 PathBuf::from(host_path.trim_start_matches("file:"))
13236 } else {
13237 let candidate = PathBuf::from(host_path);
13238 if candidate.is_absolute() {
13239 candidate
13240 } else if host_path.starts_with("./") || host_path.starts_with("../") {
13241 cwd.join(candidate)
13242 } else {
13243 return None;
13244 }
13245 };
13246 let normalized = normalize_host_path(&resolved);
13247
13248 if let Some(path) = guest_path_from_runtime_host_mappings(runtime_env, &normalized) {
13249 return Some(path);
13250 }
13251
13252 let normalized_cwd = normalize_host_path(cwd);
13253 if !path_is_within_root(&normalized, &normalized_cwd) {
13254 return None;
13255 }
13256
13257 let virtual_home = if virtual_home.starts_with('/') {
13258 virtual_home.to_string()
13259 } else {
13260 String::from("/root")
13261 };
13262 let suffix = normalized
13263 .strip_prefix(&normalized_cwd)
13264 .ok()?
13265 .to_string_lossy()
13266 .replace('\\', "/")
13267 .trim_start_matches('/')
13268 .to_owned();
13269
13270 Some(if suffix.is_empty() {
13271 virtual_home
13272 } else {
13273 normalize_path(&format!("{virtual_home}/{suffix}"))
13274 })
13275}
13276
13277fn guest_path_from_runtime_host_mappings(
13278 runtime_env: &BTreeMap<String, String>,
13279 host_path: &Path,
13280) -> Option<String> {
13281 let mappings = runtime_env
13282 .get("AGENTOS_GUEST_PATH_MAPPINGS")
13283 .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
13284 let normalized = normalize_host_path(host_path);
13285
13286 let mut sorted_mappings = mappings
13287 .into_iter()
13288 .filter_map(|mapping| {
13289 (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
13290 normalize_path(&mapping.guest_path),
13291 normalize_host_path(Path::new(&mapping.host_path)),
13292 ))
13293 })
13294 .collect::<Vec<_>>();
13295 sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.1.as_os_str().len()));
13296
13297 for (guest_root, host_root) in sorted_mappings {
13298 if !path_is_within_root(&normalized, &host_root) {
13299 continue;
13300 }
13301 let suffix = normalized
13302 .strip_prefix(&host_root)
13303 .ok()?
13304 .to_string_lossy()
13305 .replace('\\', "/")
13306 .trim_start_matches('/')
13307 .to_owned();
13308
13309 return Some(if suffix.is_empty() {
13310 guest_root
13311 } else if guest_root == "/" {
13312 normalize_path(&format!("/{suffix}"))
13313 } else {
13314 normalize_path(&format!("{guest_root}/{suffix}"))
13315 });
13316 }
13317
13318 None
13319}
13320
13321fn host_mount_path_for_guest_path_from_mounts(
13322 mounts: &[crate::protocol::MountDescriptor],
13323 guest_path: &str,
13324) -> Option<PathBuf> {
13325 let normalized = normalize_path(guest_path);
13326
13327 let mut host_mounts = mounts
13328 .iter()
13329 .filter_map(|mount| {
13330 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
13331 .then(|| {
13332 mount_config_host_path(&mount.plugin.config)
13333 .map(|host_path| (mount.guest_path.as_str(), host_path))
13334 })
13335 .flatten()
13336 })
13337 .collect::<Vec<_>>();
13338 host_mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
13339
13340 for (guest_root, host_root) in host_mounts {
13341 if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
13342 continue;
13343 }
13344
13345 let suffix = normalized
13346 .strip_prefix(guest_root)
13347 .unwrap_or_default()
13348 .trim_start_matches('/');
13349 let mut path = PathBuf::from(host_root);
13350 if !suffix.is_empty() {
13351 path.push(suffix);
13352 }
13353 return Some(path);
13354 }
13355
13356 None
13357}
13358
13359#[cfg(test)]
13360mod host_mount_path_for_guest_path_from_mounts_tests {
13361 use super::host_mount_path_for_guest_path_from_mounts;
13362 use crate::protocol::{MountDescriptor, MountPluginDescriptor};
13363 use serde_json::json;
13364 use std::path::PathBuf;
13365
13366 #[test]
13367 fn resolves_module_access_mount_paths() {
13368 let mounts = vec![MountDescriptor {
13369 guest_path: String::from("/root/node_modules"),
13370 read_only: true,
13371 plugin: MountPluginDescriptor {
13372 id: String::from("module_access"),
13373 config: json!({
13374 "hostPath": "/tmp/workspace/node_modules",
13375 })
13376 .to_string(),
13377 },
13378 }];
13379
13380 let resolved =
13381 host_mount_path_for_guest_path_from_mounts(&mounts, "/root/node_modules/pkg/index.js")
13382 .expect("module_access mount should resolve");
13383
13384 assert_eq!(
13385 resolved,
13386 PathBuf::from("/tmp/workspace/node_modules/pkg/index.js")
13387 );
13388 }
13389}
13390
13391fn resolve_guest_socket_host_path(
13392 context: &JavascriptSocketPathContext,
13393 guest_path: &str,
13394) -> PathBuf {
13395 if let Some(path) = host_mount_path_for_guest_path_from_mounts(&context.mounts, guest_path) {
13396 return path;
13397 }
13398
13399 let normalized = normalize_path(guest_path);
13400 let mut host_path = context.sandbox_root.clone();
13401 let suffix = normalized.trim_start_matches('/');
13402 if !suffix.is_empty() {
13403 host_path.push(suffix);
13404 }
13405 host_path
13406}
13407
13408fn ensure_kernel_parent_directories(
13409 kernel: &mut SidecarKernel,
13410 path: &str,
13411) -> Result<(), SidecarError> {
13412 let parent = dirname(path);
13413 if parent != "/" && !kernel.exists(&parent).map_err(kernel_error)? {
13414 kernel.mkdir(&parent, true).map_err(kernel_error)?;
13415 }
13416 Ok(())
13417}
13418
13419pub(crate) fn sanitize_javascript_child_process_internal_bootstrap_env(
13423 env: &BTreeMap<String, String>,
13424) -> BTreeMap<String, String> {
13425 const ALLOWED_KEYS: &[&str] = &[
13426 "AGENTOS_ALLOWED_NODE_BUILTINS",
13427 "AGENTOS_GUEST_PATH_MAPPINGS",
13428 "AGENTOS_LOOPBACK_EXEMPT_PORTS",
13429 "AGENTOS_VIRTUAL_PROCESS_EXEC_PATH",
13430 "AGENTOS_VIRTUAL_PROCESS_UID",
13431 "AGENTOS_VIRTUAL_PROCESS_GID",
13432 "AGENTOS_VIRTUAL_PROCESS_VERSION",
13433 ];
13434
13435 env.iter()
13436 .filter(|(key, _)| {
13437 ALLOWED_KEYS.contains(&key.as_str()) || key.starts_with("AGENTOS_VIRTUAL_OS_")
13438 })
13439 .map(|(key, value)| (key.clone(), value.clone()))
13440 .collect()
13441}
13442
13443fn resolve_tcp_bind_addr(host: &str, port: u16) -> Result<SocketAddr, SidecarError> {
13448 (host, port)
13449 .to_socket_addrs()
13450 .map_err(sidecar_net_error)?
13451 .next()
13452 .ok_or_else(|| {
13453 SidecarError::Execution(format!("failed to resolve TCP bind address {host}:{port}"))
13454 })
13455}
13456
13457pub(crate) fn format_dns_resource(hostname: &str) -> String {
13458 format!("dns://{hostname}")
13459}
13460
13461const PYTHON_SOCKET_READ_POLL: Duration = Duration::from_millis(25);
13469const PYTHON_SOCKET_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
13470const PYTHON_SOCKET_WRITE_TIMEOUT: Duration = Duration::from_secs(30);
13471const PYTHON_SOCKET_DEFAULT_RECV: usize = 65536;
13472const PYTHON_SOCKET_MAX_RECV: usize = 4 * 1024 * 1024;
13473
13474fn python_socket_host(request: &PythonVfsRpcRequest) -> Result<String, SidecarError> {
13475 request
13476 .hostname
13477 .clone()
13478 .ok_or_else(|| SidecarError::InvalidState(String::from("python socket op requires a host")))
13479}
13480
13481fn python_socket_port(request: &PythonVfsRpcRequest) -> Result<u16, SidecarError> {
13482 request
13483 .port
13484 .ok_or_else(|| SidecarError::InvalidState(String::from("python socket op requires a port")))
13485}
13486
13487fn python_socket_payload(request: &PythonVfsRpcRequest) -> Result<Vec<u8>, SidecarError> {
13488 let Some(body) = request.body_base64.as_deref() else {
13489 return Ok(Vec::new());
13490 };
13491 base64::engine::general_purpose::STANDARD
13492 .decode(body)
13493 .map_err(|error| {
13494 SidecarError::InvalidState(format!("invalid base64 python socket payload: {error}"))
13495 })
13496}
13497
13498fn python_socket_recv_len(request: &PythonVfsRpcRequest) -> usize {
13499 request
13500 .max_buffer
13501 .unwrap_or(PYTHON_SOCKET_DEFAULT_RECV)
13502 .clamp(1, PYTHON_SOCKET_MAX_RECV)
13503}
13504
13505fn python_connect_tcp(addrs: &[IpAddr], port: u16) -> Result<TcpStream, SidecarError> {
13506 let mut last_error: Option<String> = None;
13507 for ip in addrs {
13508 let addr = SocketAddr::new(*ip, port);
13509 match TcpStream::connect_timeout(&addr, PYTHON_SOCKET_CONNECT_TIMEOUT) {
13510 Ok(stream) => return Ok(stream),
13511 Err(error) => last_error = Some(error.to_string()),
13512 }
13513 }
13514 Err(SidecarError::Execution(format!(
13515 "ECONNREFUSED: {}",
13516 last_error.unwrap_or_else(|| String::from("no resolved addresses"))
13517 )))
13518}
13519
13520fn python_socket_would_block(error: &std::io::Error) -> bool {
13521 matches!(
13522 error.kind(),
13523 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
13524 )
13525}
13526
13527fn python_socket_io_error(error: std::io::Error) -> SidecarError {
13528 SidecarError::Execution(format!("EIO: python socket: {error}"))
13529}
13530
13531fn python_socket_kind_error(op: &str, expected: &str) -> SidecarError {
13532 SidecarError::Execution(format!(
13533 "EOPNOTSUPP: python socket {op} requires a {expected} socket"
13534 ))
13535}
13536
13537fn blocked_dns_resolution_error(
13538 resource: &str,
13539 ip: IpAddr,
13540 cidr: &str,
13541 label: &str,
13542) -> SidecarError {
13543 SidecarError::Execution(format!(
13544 "EACCES: blocked outbound network access to {resource}: {ip} is within restricted {label} range {cidr}"
13545 ))
13546}
13547
13548fn blocked_loopback_connect_error(resource: &str, ip: IpAddr, port: u16) -> SidecarError {
13549 SidecarError::Execution(format!(
13550 "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}",
13551 loopback_cidr(ip)
13552 ))
13553}
13554
13555fn filter_dns_safe_ip_addrs(
13556 addresses: Vec<IpAddr>,
13557 hostname: &str,
13558) -> Result<Vec<IpAddr>, SidecarError> {
13559 let resource = format_dns_resource(hostname);
13560 let mut allowed = Vec::new();
13561 let mut blocked = None;
13562
13563 for ip in addresses {
13564 if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
13565 blocked.get_or_insert((ip, cidr, label));
13566 continue;
13567 }
13568 allowed.push(ip);
13569 }
13570
13571 if allowed.is_empty() {
13572 let (ip, cidr, label) = blocked.expect("blocked DNS results should capture a reason");
13573 return Err(blocked_dns_resolution_error(&resource, ip, cidr, label));
13574 }
13575
13576 Ok(allowed)
13577}
13578
13579fn loopback_connect_allowed(context: &JavascriptSocketPathContext, port: u16) -> bool {
13580 context.loopback_port_allowed(port)
13581}
13582
13583fn filter_tcp_connect_ip_addrs(
13584 addresses: Vec<IpAddr>,
13585 host: &str,
13586 port: u16,
13587 context: &JavascriptSocketPathContext,
13588) -> Result<Vec<IpAddr>, SidecarError> {
13589 let resource = format_tcp_resource(host, port);
13590 let mut allowed = Vec::new();
13591 let mut blocked = None;
13592
13593 for ip in addresses {
13594 if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
13595 blocked.get_or_insert_with(|| blocked_dns_resolution_error(&resource, ip, cidr, label));
13596 continue;
13597 }
13598 if is_loopback_ip(ip) && !loopback_connect_allowed(context, port) {
13599 blocked.get_or_insert_with(|| blocked_loopback_connect_error(&resource, ip, port));
13600 continue;
13601 }
13602 allowed.push(ip);
13603 }
13604
13605 if allowed.is_empty() {
13606 return Err(blocked.expect("blocked TCP connect results should capture a reason"));
13607 }
13608
13609 Ok(allowed)
13610}
13611
13612fn resolve_tcp_connect_addr<B>(
13613 bridge: &SharedBridge<B>,
13614 kernel: &SidecarKernel,
13615 vm_id: &str,
13616 dns: &VmDnsConfig,
13617 host: &str,
13618 port: u16,
13619 context: &JavascriptSocketPathContext,
13620) -> Result<ResolvedTcpConnectAddr, SidecarError>
13621where
13622 B: NativeSidecarBridge + Send + 'static,
13623 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
13624{
13625 let allowed = filter_tcp_connect_ip_addrs(
13626 resolve_dns_ip_addrs(
13627 bridge,
13628 kernel,
13629 vm_id,
13630 dns,
13631 host,
13632 DnsLookupPolicy::SkipPermissions,
13633 )?,
13634 host,
13635 port,
13636 context,
13637 )?;
13638 let ip = allowed
13639 .iter()
13640 .copied()
13641 .find(|candidate| {
13642 let family = JavascriptSocketFamily::from_ip(*candidate);
13643 context.translate_tcp_loopback_port(family, port).is_some()
13644 })
13645 .or_else(|| allowed.iter().copied().find(IpAddr::is_ipv4))
13648 .or_else(|| allowed.first().copied())
13649 .ok_or_else(|| {
13650 SidecarError::Execution(format!("failed to resolve TCP address {host}:{port}"))
13651 })?;
13652 let family = JavascriptSocketFamily::from_ip(ip);
13653 let translated_loopback_port = context.translate_tcp_loopback_port(family, port);
13654 let use_kernel_loopback = is_loopback_ip(ip) && translated_loopback_port == Some(port);
13655 let actual_port = if is_loopback_ip(ip) {
13656 translated_loopback_port.unwrap_or(port)
13657 } else {
13658 port
13659 };
13660 Ok(ResolvedTcpConnectAddr {
13661 actual_addr: SocketAddr::new(ip, actual_port),
13662 guest_remote_addr: SocketAddr::new(ip, port),
13663 use_kernel_loopback,
13664 })
13665}
13666
13667fn resolve_dns_ip_addrs<B>(
13668 bridge: &SharedBridge<B>,
13669 kernel: &SidecarKernel,
13670 vm_id: &str,
13671 dns: &VmDnsConfig,
13672 hostname: &str,
13673 policy: DnsLookupPolicy,
13674) -> Result<Vec<IpAddr>, SidecarError>
13675where
13676 B: NativeSidecarBridge + Send + 'static,
13677 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
13678{
13679 let resolution = match kernel.resolve_dns(hostname, policy) {
13680 Ok(resolution) => resolution,
13681 Err(error) => {
13682 let sidecar_error = kernel_error(error.clone());
13683 if error.code() != "EACCES" {
13684 emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
13685 }
13686 return Err(sidecar_error);
13687 }
13688 };
13689 emit_dns_resolution_event(
13690 bridge,
13691 vm_id,
13692 hostname,
13693 resolution.source(),
13694 resolution.addresses(),
13695 dns,
13696 );
13697 Ok(resolution.addresses().to_vec())
13698}
13699
13700fn resolve_dns_records<B>(
13701 bridge: &SharedBridge<B>,
13702 kernel: &SidecarKernel,
13703 vm_id: &str,
13704 dns: &VmDnsConfig,
13705 hostname: &str,
13706 record_type: RecordType,
13707 policy: DnsLookupPolicy,
13708) -> Result<DnsRecordResolution, SidecarError>
13709where
13710 B: NativeSidecarBridge + Send + 'static,
13711 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
13712{
13713 let resolution = match kernel.resolve_dns_records(hostname, record_type, policy) {
13714 Ok(resolution) => resolution,
13715 Err(error) => {
13716 let sidecar_error = kernel_error(error.clone());
13717 if error.code() != "EACCES" {
13718 emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
13719 }
13720 return Err(sidecar_error);
13721 }
13722 };
13723 emit_dns_record_resolution_event(bridge, vm_id, hostname, &resolution, dns);
13724 Ok(resolution)
13725}
13726
13727fn filter_dns_ip_addrs(
13728 addresses: Vec<IpAddr>,
13729 family: Option<u8>,
13730) -> Result<Vec<IpAddr>, SidecarError> {
13731 let filtered: Vec<_> = match family.unwrap_or(0) {
13732 0 => addresses,
13733 4 => addresses
13734 .into_iter()
13735 .filter(|ip| matches!(ip, IpAddr::V4(_)))
13736 .collect(),
13737 6 => addresses
13738 .into_iter()
13739 .filter(|ip| matches!(ip, IpAddr::V6(_)))
13740 .collect(),
13741 other => {
13742 return Err(SidecarError::InvalidState(format!(
13743 "unsupported dns family {other}"
13744 )));
13745 }
13746 };
13747
13748 if filtered.is_empty() {
13749 return Err(SidecarError::Execution(String::from(
13750 "failed to resolve DNS address for requested family",
13751 )));
13752 }
13753
13754 Ok(filtered)
13755}
13756
13757fn resolve_udp_bind_addr(
13758 host: &str,
13759 port: u16,
13760 family: JavascriptUdpFamily,
13761) -> Result<SocketAddr, SidecarError> {
13762 (host, port)
13763 .to_socket_addrs()
13764 .map_err(sidecar_net_error)?
13765 .find(|addr| family.matches_addr(addr))
13766 .ok_or_else(|| {
13767 SidecarError::Execution(format!(
13768 "failed to resolve {} UDP bind address {host}:{port}",
13769 family.socket_type()
13770 ))
13771 })
13772}
13773
13774fn resolve_udp_addr<B>(request: UdpRemoteAddrRequest<'_, B>) -> Result<SocketAddr, SidecarError>
13775where
13776 B: NativeSidecarBridge + Send + 'static,
13777 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
13778{
13779 let UdpRemoteAddrRequest {
13780 bridge,
13781 kernel,
13782 vm_id,
13783 dns,
13784 host,
13785 port,
13786 family,
13787 context,
13788 } = request;
13789 resolve_dns_ip_addrs(
13790 bridge,
13791 kernel,
13792 vm_id,
13793 dns,
13794 host,
13795 DnsLookupPolicy::SkipPermissions,
13796 )?
13797 .into_iter()
13798 .map(|ip| {
13799 let family_key = JavascriptSocketFamily::from_ip(ip);
13800 let actual_port = if is_loopback_ip(ip) {
13801 context
13802 .translate_udp_loopback_port(family_key, port)
13803 .unwrap_or(port)
13804 } else {
13805 port
13806 };
13807 SocketAddr::new(ip, actual_port)
13808 })
13809 .find(|addr| family.matches_addr(addr))
13810 .ok_or_else(|| {
13811 SidecarError::Execution(format!(
13812 "failed to resolve {} UDP address {host}:{port}",
13813 family.socket_type()
13814 ))
13815 })
13816}
13817
13818fn javascript_net_timeout_value() -> Value {
13819 Value::String(String::from(JAVASCRIPT_NET_TIMEOUT_SENTINEL))
13820}
13821
13822fn javascript_net_json_string(value: Value, label: &str) -> Result<Value, SidecarError> {
13823 serde_json::to_string(&value)
13824 .map(Value::String)
13825 .map_err(|error| {
13826 SidecarError::InvalidState(format!("failed to serialize {label} payload: {error}"))
13827 })
13828}
13829
13830fn javascript_net_read_value(
13831 event: Option<JavascriptTcpSocketEvent>,
13832) -> Result<Value, SidecarError> {
13833 match event {
13834 Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(Value::String(
13835 base64::engine::general_purpose::STANDARD.encode(chunk),
13836 )),
13837 Some(JavascriptTcpSocketEvent::End | JavascriptTcpSocketEvent::Close { .. }) => {
13838 Ok(Value::Null)
13839 }
13840 Some(JavascriptTcpSocketEvent::Error { code, message }) => {
13841 let detail = code.unwrap_or_else(|| String::from("socket read"));
13842 Err(SidecarError::Execution(format!("{detail}: {message}")))
13843 }
13844 None => Ok(javascript_net_timeout_value()),
13845 }
13846}
13847
13848fn io_error_code(error: &std::io::Error) -> Option<String> {
13849 match error.raw_os_error() {
13850 Some(libc::EADDRINUSE) => Some(String::from("EADDRINUSE")),
13851 Some(libc::EADDRNOTAVAIL) => Some(String::from("EADDRNOTAVAIL")),
13852 Some(libc::ECONNREFUSED) => Some(String::from("ECONNREFUSED")),
13853 Some(libc::ECONNRESET) => Some(String::from("ECONNRESET")),
13854 Some(libc::EINVAL) => Some(String::from("EINVAL")),
13855 Some(libc::EPIPE) => Some(String::from("EPIPE")),
13856 Some(libc::ETIMEDOUT) => Some(String::from("ETIMEDOUT")),
13857 Some(libc::EHOSTUNREACH) => Some(String::from("EHOSTUNREACH")),
13858 Some(libc::ENETUNREACH) => Some(String::from("ENETUNREACH")),
13859 _ => None,
13860 }
13861}
13862
13863fn sidecar_net_error(error: std::io::Error) -> SidecarError {
13864 let message = match io_error_code(&error) {
13865 Some(code) => format!("{code}: {error}"),
13866 None => error.to_string(),
13867 };
13868 SidecarError::Execution(message)
13869}
13870
13871fn tls_provider() -> Arc<rustls::crypto::CryptoProvider> {
13872 Arc::new(aws_lc_rs::default_provider())
13873}
13874
13875fn tls_local_certificates(
13876 options: &JavascriptTlsBridgeOptions,
13877) -> Result<Vec<Vec<u8>>, SidecarError> {
13878 let Some(certificates) = options.cert.as_ref() else {
13879 return Ok(Vec::new());
13880 };
13881 tls_material_entries(certificates)
13882}
13883
13884fn tls_material_entries(material: &JavascriptTlsMaterial) -> Result<Vec<Vec<u8>>, SidecarError> {
13885 match material {
13886 JavascriptTlsMaterial::Single(entry) => tls_data_value(entry).map(|value| vec![value]),
13887 JavascriptTlsMaterial::Many(entries) => entries.iter().map(tls_data_value).collect(),
13888 }
13889}
13890
13891fn tls_data_value(value: &JavascriptTlsDataValue) -> Result<Vec<u8>, SidecarError> {
13892 match value {
13893 JavascriptTlsDataValue::Buffer { data } => base64::engine::general_purpose::STANDARD
13894 .decode(data)
13895 .map_err(|error| {
13896 SidecarError::InvalidState(format!("TLS material contains invalid base64: {error}"))
13897 }),
13898 JavascriptTlsDataValue::String { data } => Ok(data.as_bytes().to_vec()),
13899 }
13900}
13901
13902fn tls_certificates_from_material(
13903 material: &JavascriptTlsMaterial,
13904) -> Result<Vec<CertificateDer<'static>>, SidecarError> {
13905 let mut certificates = Vec::new();
13906 for entry in tls_material_entries(material)? {
13907 let mut reader = std::io::BufReader::new(Cursor::new(entry.clone()));
13908 let parsed = rustls_pemfile::certs(&mut reader)
13909 .collect::<Result<Vec<_>, _>>()
13910 .map_err(sidecar_net_error)?;
13911 if parsed.is_empty() {
13912 certificates.push(CertificateDer::from(entry));
13913 } else {
13914 certificates.extend(parsed);
13915 }
13916 }
13917 if certificates.is_empty() {
13918 return Err(SidecarError::InvalidState(String::from(
13919 "TLS certificate material did not contain any certificates",
13920 )));
13921 }
13922 Ok(certificates)
13923}
13924
13925fn tls_private_key_from_material(
13926 material: &JavascriptTlsMaterial,
13927) -> Result<PrivateKeyDer<'static>, SidecarError> {
13928 for entry in tls_material_entries(material)? {
13929 let mut reader = std::io::BufReader::new(Cursor::new(entry));
13930 if let Some(key) = rustls_pemfile::private_key(&mut reader).map_err(sidecar_net_error)? {
13931 return Ok(key);
13932 }
13933 }
13934 Err(SidecarError::InvalidState(String::from(
13935 "TLS private key material did not contain a supported key",
13936 )))
13937}
13938
13939fn tls_root_store(options: &JavascriptTlsBridgeOptions) -> Result<RootCertStore, SidecarError> {
13940 let mut roots = RootCertStore::empty();
13941 if let Some(ca) = options.ca.as_ref() {
13942 for certificate in tls_certificates_from_material(ca)? {
13943 roots.add(certificate).map_err(|error| {
13944 SidecarError::InvalidState(format!("failed to add TLS CA certificate: {error}"))
13945 })?;
13946 }
13947 return Ok(roots);
13948 }
13949
13950 for certificate in rustls_native_certs::load_native_certs().certs {
13951 roots.add(certificate).map_err(|error| {
13952 SidecarError::InvalidState(format!(
13953 "failed to add native TLS certificate to root store: {error}"
13954 ))
13955 })?;
13956 }
13957 Ok(roots)
13958}
13959
13960fn build_client_tls_stream(
13961 stream: TcpStream,
13962 options: &JavascriptTlsBridgeOptions,
13963) -> Result<rustls::StreamOwned<ClientConnection, TcpStream>, SidecarError> {
13964 let config = build_client_tls_config(options)?;
13965 let server_name = options
13966 .servername
13967 .clone()
13968 .unwrap_or_else(|| String::from("localhost"));
13969 let server_name = ServerName::try_from(server_name)
13970 .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
13971 stream
13972 .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
13973 .map_err(sidecar_net_error)?;
13974 stream
13975 .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
13976 .map_err(sidecar_net_error)?;
13977 let mut tls_stream = rustls::StreamOwned::new(
13978 ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
13979 SidecarError::Execution(format!("failed to start TLS client: {error}"))
13980 })?,
13981 stream,
13982 );
13983 while tls_stream.conn.is_handshaking() {
13984 tls_stream
13985 .conn
13986 .complete_io(&mut tls_stream.sock)
13987 .map_err(sidecar_net_error)?;
13988 }
13989 tls_stream
13990 .sock
13991 .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
13992 .map_err(sidecar_net_error)?;
13993 tls_stream
13994 .sock
13995 .set_write_timeout(None)
13996 .map_err(sidecar_net_error)?;
13997 Ok(tls_stream)
13998}
13999
14000fn build_client_loopback_tls_stream(
14001 transport: crate::state::LoopbackTlsEndpoint,
14002 options: &JavascriptTlsBridgeOptions,
14003) -> Result<rustls::StreamOwned<ClientConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
14004{
14005 let config = build_client_tls_config(options)?;
14006 let server_name = options
14007 .servername
14008 .clone()
14009 .unwrap_or_else(|| String::from("localhost"));
14010 let server_name = ServerName::try_from(server_name)
14011 .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
14012 let mut tls_stream = rustls::StreamOwned::new(
14013 ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
14014 SidecarError::Execution(format!("failed to start TLS client: {error}"))
14015 })?,
14016 transport,
14017 );
14018 match tls_stream.conn.complete_io(&mut tls_stream.sock) {
14019 Ok(_) => {}
14020 Err(error)
14021 if matches!(
14022 error.kind(),
14023 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
14024 ) => {}
14025 Err(error) => return Err(sidecar_net_error(error)),
14026 }
14027 Ok(tls_stream)
14028}
14029
14030fn build_client_tls_config(
14031 options: &JavascriptTlsBridgeOptions,
14032) -> Result<ClientConfig, SidecarError> {
14033 let provider = tls_provider();
14034 let builder = ClientConfig::builder_with_provider(provider.clone())
14035 .with_safe_default_protocol_versions()
14036 .map_err(|error| {
14037 SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
14038 })?;
14039
14040 let mut config = if options.reject_unauthorized == Some(false) {
14041 let verifier = Arc::new(InsecureTlsVerifier {
14042 supported_schemes: provider
14043 .signature_verification_algorithms
14044 .supported_schemes(),
14045 });
14046 builder
14047 .dangerous()
14048 .with_custom_certificate_verifier(verifier)
14049 .with_no_client_auth()
14050 } else {
14051 builder
14052 .with_root_certificates(tls_root_store(options)?)
14053 .with_no_client_auth()
14054 };
14055
14056 if let Some(protocols) = options.alpn_protocols.as_ref() {
14057 config.alpn_protocols = protocols
14058 .iter()
14059 .map(|protocol| protocol.as_bytes().to_vec())
14060 .collect();
14061 }
14062 Ok(config)
14063}
14064
14065fn build_server_tls_stream(
14066 stream: TcpStream,
14067 options: &JavascriptTlsBridgeOptions,
14068) -> Result<rustls::StreamOwned<ServerConnection, TcpStream>, SidecarError> {
14069 let config = build_server_tls_config(options)?;
14070 stream
14071 .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
14072 .map_err(sidecar_net_error)?;
14073 stream
14074 .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
14075 .map_err(sidecar_net_error)?;
14076 let mut tls_stream = rustls::StreamOwned::new(
14077 ServerConnection::new(Arc::new(config)).map_err(|error| {
14078 SidecarError::Execution(format!("failed to start TLS server: {error}"))
14079 })?,
14080 stream,
14081 );
14082 while tls_stream.conn.is_handshaking() {
14083 tls_stream
14084 .conn
14085 .complete_io(&mut tls_stream.sock)
14086 .map_err(sidecar_net_error)?;
14087 }
14088 tls_stream
14089 .sock
14090 .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
14091 .map_err(sidecar_net_error)?;
14092 tls_stream
14093 .sock
14094 .set_write_timeout(None)
14095 .map_err(sidecar_net_error)?;
14096 Ok(tls_stream)
14097}
14098
14099fn build_server_loopback_tls_stream(
14100 transport: crate::state::LoopbackTlsEndpoint,
14101 options: &JavascriptTlsBridgeOptions,
14102) -> Result<rustls::StreamOwned<ServerConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
14103{
14104 let config = build_server_tls_config(options)?;
14105 Ok(rustls::StreamOwned::new(
14106 ServerConnection::new(Arc::new(config)).map_err(|error| {
14107 SidecarError::Execution(format!("failed to start TLS server: {error}"))
14108 })?,
14109 transport,
14110 ))
14111}
14112
14113fn build_server_tls_config(
14114 options: &JavascriptTlsBridgeOptions,
14115) -> Result<ServerConfig, SidecarError> {
14116 let certificates = tls_certificates_from_material(options.cert.as_ref().ok_or_else(|| {
14117 SidecarError::InvalidState(String::from("TLS server upgrade requires a certificate"))
14118 })?)?;
14119 let key = tls_private_key_from_material(options.key.as_ref().ok_or_else(|| {
14120 SidecarError::InvalidState(String::from("TLS server upgrade requires a private key"))
14121 })?)?;
14122
14123 let mut config = ServerConfig::builder_with_provider(tls_provider())
14124 .with_safe_default_protocol_versions()
14125 .map_err(|error| {
14126 SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
14127 })?
14128 .with_no_client_auth()
14129 .with_single_cert(certificates, key)
14130 .map_err(|error| {
14131 SidecarError::InvalidState(format!("invalid TLS server config: {error}"))
14132 })?;
14133
14134 if let Some(protocols) = options.alpn_protocols.as_ref() {
14135 config.alpn_protocols = protocols
14136 .iter()
14137 .map(|protocol| protocol.as_bytes().to_vec())
14138 .collect();
14139 }
14140 Ok(config)
14141}
14142
14143fn tls_protocol_name(version: rustls::ProtocolVersion) -> String {
14144 match version {
14145 rustls::ProtocolVersion::TLSv1_2 => String::from("TLSv1.2"),
14146 rustls::ProtocolVersion::TLSv1_3 => String::from("TLSv1.3"),
14147 other => other
14148 .as_str()
14149 .map(str::to_owned)
14150 .unwrap_or_else(|| format!("{other:?}")),
14151 }
14152}
14153
14154fn tls_cipher_bridge_value(suite: rustls::SupportedCipherSuite) -> Value {
14155 tls_bridge_object(vec![
14156 (
14157 "name",
14158 suite
14159 .suite()
14160 .as_str()
14161 .map(|value| Value::String(value.to_owned()))
14162 .unwrap_or(Value::Null),
14163 ),
14164 (
14165 "standardName",
14166 suite
14167 .suite()
14168 .as_str()
14169 .map(|value| Value::String(value.to_owned()))
14170 .unwrap_or(Value::Null),
14171 ),
14172 (
14173 "version",
14174 Value::String(if suite.tls13().is_some() {
14175 String::from("TLSv1.3")
14176 } else {
14177 String::from("TLSv1.2")
14178 }),
14179 ),
14180 ])
14181}
14182
14183fn tls_certificate_bridge_value(certificate: &[u8], detailed: bool) -> Value {
14184 let mut fields = vec![("raw", tls_bridge_buffer_value(certificate))];
14185 if detailed {
14186 fields.push(("issuerCertificate", tls_bridge_undefined_value()));
14187 }
14188 tls_bridge_object(fields)
14189}
14190
14191fn tls_bridge_buffer_value(bytes: &[u8]) -> Value {
14192 json!({
14193 "type": "buffer",
14194 "data": base64::engine::general_purpose::STANDARD.encode(bytes),
14195 })
14196}
14197
14198fn tls_bridge_object(entries: Vec<(&str, Value)>) -> Value {
14199 let value = entries
14200 .into_iter()
14201 .map(|(key, value)| (key.to_owned(), value))
14202 .collect::<serde_json::Map<String, Value>>();
14203 json!({
14204 "type": "object",
14205 "id": 1,
14206 "value": value,
14207 })
14208}
14209
14210fn tls_bridge_undefined_value() -> Value {
14211 json!({
14212 "type": "undefined",
14213 })
14214}
14215
14216fn spawn_tcp_socket_reader(
14217 stream: TcpStream,
14218 sender: Sender<JavascriptTcpSocketEvent>,
14219 event_pusher: Arc<Mutex<Option<JavascriptSocketEventPusher>>>,
14220 tls_mode: Arc<AtomicBool>,
14221 saw_local_shutdown: Arc<AtomicBool>,
14222 saw_remote_end: Arc<AtomicBool>,
14223 close_notified: Arc<AtomicBool>,
14224) {
14225 thread::spawn(move || {
14226 let mut stream = stream;
14227 let mut buffer = vec![0_u8; 64 * 1024];
14228 loop {
14229 if tls_mode.load(Ordering::SeqCst) {
14230 break;
14231 }
14232 match stream.read(&mut buffer) {
14233 Ok(0) => {
14234 saw_remote_end.store(true, Ordering::SeqCst);
14235 let _ = sender.send(JavascriptTcpSocketEvent::End);
14236 push_socket_event(&event_pusher, "end");
14237 if saw_local_shutdown.load(Ordering::SeqCst)
14238 && !close_notified.swap(true, Ordering::SeqCst)
14239 {
14240 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
14241 push_socket_event(&event_pusher, "close");
14242 }
14243 break;
14244 }
14245 Ok(bytes_read) => {
14246 if sender
14247 .send(JavascriptTcpSocketEvent::Data(
14248 buffer[..bytes_read].to_vec(),
14249 ))
14250 .is_err()
14251 {
14252 break;
14253 }
14254 push_socket_event(&event_pusher, "data");
14255 }
14256 Err(error)
14257 if matches!(
14258 error.kind(),
14259 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
14260 ) =>
14261 {
14262 continue;
14263 }
14264 Err(error) => {
14265 let code = io_error_code(&error);
14266 let _ = sender.send(JavascriptTcpSocketEvent::Error {
14267 code,
14268 message: error.to_string(),
14269 });
14270 push_socket_event(&event_pusher, "error");
14271 if !close_notified.swap(true, Ordering::SeqCst) {
14272 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
14273 push_socket_event(&event_pusher, "close");
14274 }
14275 break;
14276 }
14277 }
14278 }
14279 });
14280}
14281
14282fn push_socket_event(
14283 event_pusher: &Arc<Mutex<Option<JavascriptSocketEventPusher>>>,
14284 event: &'static str,
14285) {
14286 NET_TCP_TRACE_COUNTERS
14287 .socket_read_push_attempts
14288 .fetch_add(1, Ordering::Relaxed);
14289 let target = event_pusher.lock().ok().and_then(|guard| guard.clone());
14290 let Some(target) = target else {
14291 NET_TCP_TRACE_COUNTERS
14292 .socket_read_push_missing
14293 .fetch_add(1, Ordering::Relaxed);
14294 return;
14295 };
14296 let payload = v8_runtime::json_to_cbor_payload(&json!({
14297 "socketId": target.socket_id,
14298 "event": event,
14299 }))
14300 .unwrap_or_default();
14301 match target.session.send_stream_event("net_socket", payload) {
14302 Ok(()) => {
14303 NET_TCP_TRACE_COUNTERS
14304 .socket_read_push_sent
14305 .fetch_add(1, Ordering::Relaxed);
14306 }
14307 Err(_) => {
14308 NET_TCP_TRACE_COUNTERS
14309 .socket_read_push_errors
14310 .fetch_add(1, Ordering::Relaxed);
14311 }
14312 }
14313}
14314
14315fn send_tls_socket_error_and_close(
14316 sender: &Sender<JavascriptTcpSocketEvent>,
14317 event_pusher: &Arc<Mutex<Option<JavascriptSocketEventPusher>>>,
14318 close_notified: &Arc<AtomicBool>,
14319 code: Option<String>,
14320 message: String,
14321) {
14322 let _ = sender.send(JavascriptTcpSocketEvent::Error { code, message });
14323 push_socket_event(event_pusher, "error");
14324 if !close_notified.swap(true, Ordering::SeqCst) {
14325 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
14326 push_socket_event(event_pusher, "close");
14327 }
14328}
14329
14330fn fail_loopback_tls_pending_reader(
14331 pending_write: &LoopbackTlsPendingWriteHandle,
14332 sender: &Sender<JavascriptTcpSocketEvent>,
14333 event_pusher: &Arc<Mutex<Option<JavascriptSocketEventPusher>>>,
14334 close_notified: &Arc<AtomicBool>,
14335 code: Option<String>,
14336 message: String,
14337) {
14338 pending_write.mark_failed(message.clone());
14339 send_tls_socket_error_and_close(sender, event_pusher, close_notified, code, message);
14340}
14341
14342fn flush_loopback_tls_pending_writes(
14343 tls_stream: &Arc<Mutex<Option<ActiveTlsStream>>>,
14344 pending_write: &LoopbackTlsPendingWriteHandle,
14345 sender: &Sender<JavascriptTcpSocketEvent>,
14346 event_pusher: &Arc<Mutex<Option<JavascriptSocketEventPusher>>>,
14347 close_notified: &Arc<AtomicBool>,
14348) -> bool {
14349 loop {
14350 let bytes = match pending_write.take_buffer_for_flush() {
14351 Ok(bytes) => bytes,
14352 Err(error) => {
14353 fail_loopback_tls_pending_reader(
14354 pending_write,
14355 sender,
14356 event_pusher,
14357 close_notified,
14358 Some(String::from("EIO")),
14359 error.to_string(),
14360 );
14361 return false;
14362 }
14363 };
14364
14365 if let Some(bytes) = bytes {
14366 let write_result = {
14367 let mut guard = match tls_stream.lock() {
14368 Ok(guard) => guard,
14369 Err(_) => {
14370 pending_write.finish_flush();
14371 fail_loopback_tls_pending_reader(
14372 pending_write,
14373 sender,
14374 event_pusher,
14375 close_notified,
14376 Some(String::from("EIO")),
14377 String::from("TLS stream lock poisoned while flushing pending writes"),
14378 );
14379 return false;
14380 }
14381 };
14382 let Some(stream) = guard.as_mut() else {
14383 pending_write.finish_flush();
14384 fail_loopback_tls_pending_reader(
14385 pending_write,
14386 sender,
14387 event_pusher,
14388 close_notified,
14389 Some(String::from("EIO")),
14390 String::from("TLS stream missing while flushing pending writes"),
14391 );
14392 return false;
14393 };
14394 stream.write_all(&bytes)
14395 };
14396 pending_write.finish_flush();
14397 if let Err(error) = write_result {
14398 fail_loopback_tls_pending_reader(
14399 pending_write,
14400 sender,
14401 event_pusher,
14402 close_notified,
14403 Some(String::from("EIO")),
14404 format!("loopback TLS pending write flush failed: {error}"),
14405 );
14406 return false;
14407 }
14408 continue;
14409 }
14410
14411 let should_shutdown = match pending_write.take_deferred_shutdown_write() {
14412 Ok(should_shutdown) => should_shutdown,
14413 Err(error) => {
14414 fail_loopback_tls_pending_reader(
14415 pending_write,
14416 sender,
14417 event_pusher,
14418 close_notified,
14419 Some(String::from("EIO")),
14420 error.to_string(),
14421 );
14422 return false;
14423 }
14424 };
14425 if should_shutdown {
14426 let shutdown_result = {
14427 let mut guard = match tls_stream.lock() {
14428 Ok(guard) => guard,
14429 Err(_) => {
14430 fail_loopback_tls_pending_reader(
14431 pending_write,
14432 sender,
14433 event_pusher,
14434 close_notified,
14435 Some(String::from("EIO")),
14436 String::from("TLS stream lock poisoned during deferred shutdown"),
14437 );
14438 return false;
14439 }
14440 };
14441 let Some(stream) = guard.as_mut() else {
14442 fail_loopback_tls_pending_reader(
14443 pending_write,
14444 sender,
14445 event_pusher,
14446 close_notified,
14447 Some(String::from("EIO")),
14448 String::from("TLS stream missing during deferred shutdown"),
14449 );
14450 return false;
14451 };
14452 stream
14453 .send_close_notify()
14454 .and_then(|_| stream.shutdown_write())
14455 };
14456 if let Err(error) = shutdown_result {
14457 fail_loopback_tls_pending_reader(
14458 pending_write,
14459 sender,
14460 event_pusher,
14461 close_notified,
14462 Some(String::from("EIO")),
14463 format!("loopback TLS deferred shutdown failed: {error}"),
14464 );
14465 return false;
14466 }
14467 continue;
14468 }
14469
14470 return true;
14471 }
14472}
14473
14474fn spawn_tls_socket_reader(
14475 tls_stream: Arc<Mutex<Option<ActiveTlsStream>>>,
14476 loopback_pending_write: Option<LoopbackTlsPendingWriteHandle>,
14477 sender: Sender<JavascriptTcpSocketEvent>,
14478 event_pusher: Arc<Mutex<Option<JavascriptSocketEventPusher>>>,
14479 saw_local_shutdown: Arc<AtomicBool>,
14480 saw_remote_end: Arc<AtomicBool>,
14481 close_notified: Arc<AtomicBool>,
14482) {
14483 thread::spawn(move || {
14484 let mut buffer = vec![0_u8; 64 * 1024];
14485 loop {
14486 if let Some(pending_write) = loopback_pending_write.as_ref() {
14487 let is_handshaking = {
14488 let mut guard = match tls_stream.lock() {
14489 Ok(guard) => guard,
14490 Err(_) => {
14491 pending_write.mark_failed("TLS stream lock poisoned");
14492 return;
14493 }
14494 };
14495 let Some(stream) = guard.as_mut() else {
14496 pending_write.mark_failed("TLS stream missing");
14497 return;
14498 };
14499 if stream.is_loopback() {
14500 let is_handshaking = stream.is_handshaking();
14501 stream.set_loopback_poll_timeout(LOOPBACK_TLS_HANDSHAKE_POLL_TIMEOUT);
14502 is_handshaking
14503 } else {
14504 false
14505 }
14506 };
14507
14508 if is_handshaking {
14509 if pending_write.handshake_started_at.elapsed() >= TLS_HANDSHAKE_TIMEOUT {
14510 fail_loopback_tls_pending_reader(
14511 pending_write,
14512 &sender,
14513 &event_pusher,
14514 &close_notified,
14515 Some(String::from("ETIMEDOUT")),
14516 format!(
14517 "loopback TLS handshake timed out after {}ms",
14518 TLS_HANDSHAKE_TIMEOUT.as_millis()
14519 ),
14520 );
14521 return;
14522 }
14523 } else {
14524 pending_write
14525 .tls_handshake_complete
14526 .store(true, Ordering::SeqCst);
14527 if !flush_loopback_tls_pending_writes(
14528 &tls_stream,
14529 pending_write,
14530 &sender,
14531 &event_pusher,
14532 &close_notified,
14533 ) {
14534 return;
14535 }
14536 }
14537 }
14538
14539 let read_result = {
14540 let mut guard = match tls_stream.lock() {
14541 Ok(guard) => guard,
14542 Err(_) => {
14543 if let Some(pending_write) = loopback_pending_write.as_ref() {
14544 pending_write.mark_failed("TLS stream lock poisoned");
14545 }
14546 return;
14547 }
14548 };
14549 let Some(stream) = guard.as_mut() else {
14550 if let Some(pending_write) = loopback_pending_write.as_ref() {
14551 pending_write.mark_failed("TLS stream missing");
14552 }
14553 return;
14554 };
14555 stream.read(&mut buffer)
14556 };
14557
14558 match read_result {
14559 Ok(0) => {
14560 if let Some(pending_write) = loopback_pending_write.as_ref() {
14561 pending_write.fail_if_pending(
14562 "loopback TLS peer closed before buffered writes flushed",
14563 );
14564 }
14565 saw_remote_end.store(true, Ordering::SeqCst);
14566 let _ = sender.send(JavascriptTcpSocketEvent::End);
14567 push_socket_event(&event_pusher, "end");
14568 if saw_local_shutdown.load(Ordering::SeqCst)
14569 && !close_notified.swap(true, Ordering::SeqCst)
14570 {
14571 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
14572 push_socket_event(&event_pusher, "close");
14573 }
14574 break;
14575 }
14576 Ok(bytes_read) => {
14577 if sender
14578 .send(JavascriptTcpSocketEvent::Data(
14579 buffer[..bytes_read].to_vec(),
14580 ))
14581 .is_err()
14582 {
14583 break;
14584 }
14585 push_socket_event(&event_pusher, "data");
14586 }
14587 Err(error)
14588 if matches!(
14589 error.kind(),
14590 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
14591 ) =>
14592 {
14593 std::thread::sleep(Duration::from_millis(1));
14596 continue;
14597 }
14598 Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
14599 if let Some(pending_write) = loopback_pending_write.as_ref() {
14600 pending_write.fail_if_pending(
14601 "loopback TLS peer closed before buffered writes flushed",
14602 );
14603 }
14604 saw_remote_end.store(true, Ordering::SeqCst);
14605 let _ = sender.send(JavascriptTcpSocketEvent::End);
14606 push_socket_event(&event_pusher, "end");
14607 if saw_local_shutdown.load(Ordering::SeqCst)
14608 && !close_notified.swap(true, Ordering::SeqCst)
14609 {
14610 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
14611 push_socket_event(&event_pusher, "close");
14612 }
14613 break;
14614 }
14615 Err(error) => {
14616 if let Some(pending_write) = loopback_pending_write.as_ref() {
14617 pending_write.mark_failed(format!(
14618 "loopback TLS reader failed before buffered writes flushed: {error}"
14619 ));
14620 }
14621 let code = io_error_code(&error);
14622 let _ = sender.send(JavascriptTcpSocketEvent::Error {
14623 code,
14624 message: error.to_string(),
14625 });
14626 push_socket_event(&event_pusher, "error");
14627 if !close_notified.swap(true, Ordering::SeqCst) {
14628 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
14629 push_socket_event(&event_pusher, "close");
14630 }
14631 break;
14632 }
14633 }
14634 }
14635 });
14636}
14637
14638fn spawn_unix_socket_reader(
14639 stream: UnixStream,
14640 sender: Sender<JavascriptTcpSocketEvent>,
14641 event_pusher: Arc<Mutex<Option<JavascriptSocketEventPusher>>>,
14642 saw_local_shutdown: Arc<AtomicBool>,
14643 saw_remote_end: Arc<AtomicBool>,
14644 close_notified: Arc<AtomicBool>,
14645) {
14646 thread::spawn(move || {
14647 let mut stream = stream;
14648 let mut buffer = vec![0_u8; 64 * 1024];
14649 loop {
14650 match stream.read(&mut buffer) {
14651 Ok(0) => {
14652 saw_remote_end.store(true, Ordering::SeqCst);
14653 let _ = sender.send(JavascriptTcpSocketEvent::End);
14654 push_socket_event(&event_pusher, "end");
14655 if saw_local_shutdown.load(Ordering::SeqCst)
14656 && !close_notified.swap(true, Ordering::SeqCst)
14657 {
14658 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
14659 push_socket_event(&event_pusher, "close");
14660 }
14661 break;
14662 }
14663 Ok(bytes_read) => {
14664 if sender
14665 .send(JavascriptTcpSocketEvent::Data(
14666 buffer[..bytes_read].to_vec(),
14667 ))
14668 .is_err()
14669 {
14670 break;
14671 }
14672 push_socket_event(&event_pusher, "data");
14673 }
14674 Err(error) => {
14675 let code = io_error_code(&error);
14676 let _ = sender.send(JavascriptTcpSocketEvent::Error {
14677 code,
14678 message: error.to_string(),
14679 });
14680 push_socket_event(&event_pusher, "error");
14681 if !close_notified.swap(true, Ordering::SeqCst) {
14682 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
14683 push_socket_event(&event_pusher, "close");
14684 }
14685 break;
14686 }
14687 }
14688 }
14689 });
14690}
14691
14692fn flush_parked_kernel_wait_rpc(process: &mut ActiveProcess) {
14697 if let Some((request, _)) = process.deferred_kernel_wait_rpc.take() {
14698 let _ = process
14699 .execution
14700 .respond_javascript_sync_rpc_error(request.id, "EINTR", "process teardown")
14701 .or_else(ignore_stale_javascript_sync_rpc_response);
14702 }
14703}
14704
14705fn terminate_child_process_tree(
14706 kernel: &mut SidecarKernel,
14707 process: &mut ActiveProcess,
14708 kernel_readiness: &KernelSocketReadinessRegistry,
14709) {
14710 flush_parked_kernel_wait_rpc(process);
14711 let sqlite_database_ids = process.sqlite_databases.keys().copied().collect::<Vec<_>>();
14712 for database_id in sqlite_database_ids {
14713 let _ = close_sqlite_database(kernel, process, database_id);
14714 }
14715 process.sqlite_statements.clear();
14716 process.http_servers.clear();
14717 process.pending_http_requests.clear();
14718 if let Ok(mut http2) = process.http2.shared.lock() {
14719 let sessions = http2.sessions.values().cloned().collect::<Vec<_>>();
14720 http2.server_events.clear();
14721 http2.session_events.clear();
14722 http2.streams.clear();
14723 http2.servers.clear();
14724 http2.sessions.clear();
14725 http2.event_session = None;
14726 drop(http2);
14727 for session in sessions {
14728 let (respond_to, _rx) = mpsc::channel();
14729 let _ = session.command_tx.send(Http2SessionCommand::Close {
14730 abrupt: true,
14731 respond_to,
14732 });
14733 }
14734 }
14735
14736 let listener_ids = process.tcp_listeners.keys().cloned().collect::<Vec<_>>();
14737 for listener_id in listener_ids {
14738 if let Some(listener) = process.tcp_listeners.remove(&listener_id) {
14739 unregister_kernel_readiness_target(kernel_readiness, listener.kernel_socket_id);
14740 let _ = listener.close(kernel, process.kernel_pid);
14741 }
14742 }
14743
14744 let sockets = process.tcp_sockets.keys().cloned().collect::<Vec<_>>();
14745 for socket_id in sockets {
14746 if let Some(socket) = process.tcp_sockets.remove(&socket_id) {
14747 unregister_kernel_readiness_target(kernel_readiness, socket.kernel_socket_id);
14748 let _ = socket.close(kernel, process.kernel_pid);
14749 }
14750 }
14751
14752 let unix_listener_ids = process.unix_listeners.keys().cloned().collect::<Vec<_>>();
14753 for listener_id in unix_listener_ids {
14754 if let Some(listener) = process.unix_listeners.remove(&listener_id) {
14755 let _ = listener.close();
14756 }
14757 }
14758
14759 let unix_sockets = process.unix_sockets.keys().cloned().collect::<Vec<_>>();
14760 for socket_id in unix_sockets {
14761 if let Some(socket) = process.unix_sockets.remove(&socket_id) {
14762 let _ = socket.close();
14763 }
14764 }
14765
14766 let udp_socket_ids = process.udp_sockets.keys().cloned().collect::<Vec<_>>();
14767 for socket_id in udp_socket_ids {
14768 if let Some(mut socket) = process.udp_sockets.remove(&socket_id) {
14769 unregister_kernel_readiness_target(kernel_readiness, socket.kernel_socket_id);
14770 socket.close(kernel, process.kernel_pid);
14771 }
14772 }
14773
14774 let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
14775 for child_id in child_ids {
14776 let Some(mut child) = process.child_processes.remove(&child_id) else {
14777 continue;
14778 };
14779 terminate_child_process_tree(kernel, &mut child, kernel_readiness);
14780 let _ = kernel.kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, SIGTERM);
14781 let _ = signal_runtime_process(child.execution.child_pid(), SIGTERM);
14782 child.kernel_handle.finish(0);
14783 let _ = kernel.wait_and_reap(child.kernel_pid);
14784 }
14785}
14786
14787fn service_javascript_sqlite_sync_rpc(
14788 kernel: &mut SidecarKernel,
14789 process: &mut ActiveProcess,
14790 request: &JavascriptSyncRpcRequest,
14791) -> Result<Value, SidecarError> {
14792 match request.method.as_str() {
14793 "sqlite.constants" => Ok(json!({})),
14794 "sqlite.open" => sqlite_open_database(kernel, process, request),
14795 "sqlite.close" => {
14796 let database_id =
14797 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.close database id")?;
14798 close_sqlite_database(kernel, process, database_id)?;
14799 Ok(Value::Null)
14800 }
14801 "sqlite.exec" => sqlite_exec_database(kernel, process, request),
14802 "sqlite.query" => sqlite_query_database(process, request),
14803 "sqlite.prepare" => sqlite_prepare_statement(process, request),
14804 "sqlite.location" => {
14805 let database_id =
14806 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.location database id")?;
14807 let database = sqlite_database(process, database_id)?;
14808 Ok(database
14809 .vm_path
14810 .as_ref()
14811 .map(|path| Value::String(path.clone()))
14812 .unwrap_or(Value::Null))
14813 }
14814 "sqlite.checkpoint" => {
14815 let database_id =
14816 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.checkpoint database id")?;
14817 let kernel_pid = process.kernel_pid;
14818 let database = sqlite_database_mut(process, database_id)?;
14819 sqlite_sync_database(kernel, kernel_pid, database)?;
14820 Ok(Value::Null)
14821 }
14822 "sqlite.statement.run" => sqlite_run_statement(kernel, process, request),
14823 "sqlite.statement.get" => sqlite_get_statement(process, request),
14824 "sqlite.statement.all" | "sqlite.statement.iterate" => {
14825 sqlite_all_statement(process, request)
14826 }
14827 "sqlite.statement.columns" => sqlite_statement_columns(process, request),
14828 "sqlite.statement.setReturnArrays" => {
14829 let statement_id = javascript_sync_rpc_arg_u64(
14830 &request.args,
14831 0,
14832 "sqlite.statement.setReturnArrays statement id",
14833 )?;
14834 let enabled = javascript_sync_rpc_arg_bool(
14835 &request.args,
14836 1,
14837 "sqlite.statement.setReturnArrays enabled",
14838 )?;
14839 sqlite_statement_mut(process, statement_id)?.return_arrays = enabled;
14840 Ok(Value::Null)
14841 }
14842 "sqlite.statement.setReadBigInts" => {
14843 let statement_id = javascript_sync_rpc_arg_u64(
14844 &request.args,
14845 0,
14846 "sqlite.statement.setReadBigInts statement id",
14847 )?;
14848 let enabled = javascript_sync_rpc_arg_bool(
14849 &request.args,
14850 1,
14851 "sqlite.statement.setReadBigInts enabled",
14852 )?;
14853 sqlite_statement_mut(process, statement_id)?.read_bigints = enabled;
14854 Ok(Value::Null)
14855 }
14856 "sqlite.statement.setAllowBareNamedParameters" => {
14857 let statement_id = javascript_sync_rpc_arg_u64(
14858 &request.args,
14859 0,
14860 "sqlite.statement.setAllowBareNamedParameters statement id",
14861 )?;
14862 let enabled = javascript_sync_rpc_arg_bool(
14863 &request.args,
14864 1,
14865 "sqlite.statement.setAllowBareNamedParameters enabled",
14866 )?;
14867 sqlite_statement_mut(process, statement_id)?.allow_bare_named_parameters = enabled;
14868 Ok(Value::Null)
14869 }
14870 "sqlite.statement.setAllowUnknownNamedParameters" => {
14871 let statement_id = javascript_sync_rpc_arg_u64(
14872 &request.args,
14873 0,
14874 "sqlite.statement.setAllowUnknownNamedParameters statement id",
14875 )?;
14876 let enabled = javascript_sync_rpc_arg_bool(
14877 &request.args,
14878 1,
14879 "sqlite.statement.setAllowUnknownNamedParameters enabled",
14880 )?;
14881 sqlite_statement_mut(process, statement_id)?.allow_unknown_named_parameters = enabled;
14882 Ok(Value::Null)
14883 }
14884 "sqlite.statement.finalize" => {
14885 let statement_id = javascript_sync_rpc_arg_u64(
14886 &request.args,
14887 0,
14888 "sqlite.statement.finalize statement id",
14889 )?;
14890 process
14891 .sqlite_statements
14892 .remove(&statement_id)
14893 .ok_or_else(|| {
14894 SidecarError::InvalidState(format!(
14895 "sqlite statement handle not found: {statement_id}"
14896 ))
14897 })?;
14898 Ok(Value::Null)
14899 }
14900 other => Err(SidecarError::InvalidState(format!(
14901 "unsupported JavaScript sqlite sync RPC method {other}"
14902 ))),
14903 }
14904}
14905
14906fn sqlite_open_database(
14907 kernel: &mut SidecarKernel,
14908 process: &mut ActiveProcess,
14909 request: &JavascriptSyncRpcRequest,
14910) -> Result<Value, SidecarError> {
14911 ensure_per_process_state_handle_capacity(process.sqlite_databases.len(), "sqlite database")?;
14912 let path = request.args.first().and_then(Value::as_str);
14913 let vm_path = path.filter(|value| !value.is_empty() && *value != ":memory:");
14914 let options = request.args.get(1);
14915 let read_only = sqlite_option_bool(options, "readOnly").unwrap_or(false);
14916 let create = sqlite_option_bool(options, "create").unwrap_or(!read_only);
14917 let timeout_ms = sqlite_option_u64(options, "timeout");
14918
14919 process.next_sqlite_database_id += 1;
14920 let database_id = process.next_sqlite_database_id;
14921
14922 let host_path = if vm_path.is_some() {
14923 Some(
14924 std::env::temp_dir()
14925 .join(format!(
14926 "secure-exec-sidecar-sqlite-{}-{database_id}",
14927 process.kernel_pid
14928 ))
14929 .join("database.sqlite"),
14930 )
14931 } else {
14932 None
14933 };
14934
14935 if let Some(host_path) = host_path.as_ref() {
14936 if let Some(parent) = host_path.parent() {
14937 fs::create_dir_all(parent).map_err(|error| {
14938 SidecarError::Io(format!(
14939 "failed to prepare sqlite temp directory {}: {error}",
14940 parent.display()
14941 ))
14942 })?;
14943 }
14944 }
14945
14946 if let (Some(vm_path), Some(host_path)) = (vm_path, host_path.as_ref()) {
14947 if kernel
14948 .exists_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
14949 .map_err(kernel_error)?
14950 {
14951 let contents = kernel
14952 .read_file_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
14953 .map_err(kernel_error)?;
14954 fs::write(host_path, contents).map_err(|error| {
14955 SidecarError::Io(format!(
14956 "failed to materialize sqlite database {}: {error}",
14957 host_path.display()
14958 ))
14959 })?;
14960 } else if read_only && !create {
14961 return Err(SidecarError::InvalidState(format!(
14962 "sqlite database does not exist: {vm_path}"
14963 )));
14964 }
14965 }
14966
14967 let target = host_path
14968 .as_ref()
14969 .map(|path| path.to_string_lossy().into_owned())
14970 .unwrap_or_else(|| String::from(":memory:"));
14971 let mut flags = if read_only {
14972 SqliteOpenFlags::SQLITE_OPEN_READ_ONLY
14973 } else {
14974 SqliteOpenFlags::SQLITE_OPEN_READ_WRITE
14975 };
14976 if create && !read_only {
14977 flags |= SqliteOpenFlags::SQLITE_OPEN_CREATE;
14978 }
14979
14980 let connection = SqliteConnection::open_with_flags(&target, flags).map_err(|error| {
14981 SidecarError::InvalidState(format!(
14982 "sqlite database open failed for {}: {error}",
14983 vm_path.unwrap_or(":memory:")
14984 ))
14985 })?;
14986 if let Some(timeout_ms) = timeout_ms {
14987 connection
14988 .busy_timeout(Duration::from_millis(timeout_ms))
14989 .map_err(sqlite_error)?;
14990 }
14991 if host_path.is_some() && !read_only {
14992 let _ = connection.pragma_update(None, "journal_mode", "WAL");
14993 }
14994
14995 process.sqlite_databases.insert(
14996 database_id,
14997 ActiveSqliteDatabase {
14998 connection,
14999 host_path,
15000 vm_path: vm_path.map(String::from),
15001 dirty: false,
15002 transaction_depth: 0,
15003 read_only,
15004 },
15005 );
15006
15007 Ok(json!(database_id))
15008}
15009
15010fn sqlite_exec_database(
15011 kernel: &mut SidecarKernel,
15012 process: &mut ActiveProcess,
15013 request: &JavascriptSyncRpcRequest,
15014) -> Result<Value, SidecarError> {
15015 let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.exec database id")?;
15016 let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.exec sql")?;
15017 let kernel_pid = process.kernel_pid;
15018 let database = sqlite_database_mut(process, database_id)?;
15019 let before = database.connection.total_changes();
15020 database
15021 .connection
15022 .execute_batch(sql)
15023 .map_err(sqlite_error)?;
15024 mark_sqlite_mutation(database, sql);
15025 sqlite_sync_database(kernel, kernel_pid, database)?;
15026 Ok(json!(database
15027 .connection
15028 .total_changes()
15029 .saturating_sub(before)))
15030}
15031
15032fn sqlite_query_database(
15033 process: &mut ActiveProcess,
15034 request: &JavascriptSyncRpcRequest,
15035) -> Result<Value, SidecarError> {
15036 let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.query database id")?;
15037 let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.query sql")?;
15038 let params = request.args.get(2);
15039 let options = request.args.get(3);
15040 let return_arrays = sqlite_option_bool(options, "returnArrays").unwrap_or(false);
15041 let read_bigints = sqlite_option_bool(options, "readBigInts").unwrap_or(false);
15042 let database = sqlite_database_mut(process, database_id)?;
15043 sqlite_query_rows(
15044 &mut database.connection,
15045 sql,
15046 params,
15047 return_arrays,
15048 read_bigints,
15049 true,
15050 false,
15051 )
15052}
15053
15054fn sqlite_prepare_statement(
15055 process: &mut ActiveProcess,
15056 request: &JavascriptSyncRpcRequest,
15057) -> Result<Value, SidecarError> {
15058 ensure_per_process_state_handle_capacity(process.sqlite_statements.len(), "sqlite statement")?;
15059 let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.prepare database id")?;
15060 let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.prepare sql")?;
15061 let _ = sqlite_database(process, database_id)?;
15062 process.next_sqlite_statement_id += 1;
15063 let statement_id = process.next_sqlite_statement_id;
15064 process.sqlite_statements.insert(
15065 statement_id,
15066 ActiveSqliteStatement {
15067 database_id,
15068 sql: sql.to_owned(),
15069 return_arrays: false,
15070 read_bigints: false,
15071 allow_bare_named_parameters: false,
15072 allow_unknown_named_parameters: false,
15073 },
15074 );
15075 Ok(json!(statement_id))
15076}
15077
15078fn sqlite_run_statement(
15079 kernel: &mut SidecarKernel,
15080 process: &mut ActiveProcess,
15081 request: &JavascriptSyncRpcRequest,
15082) -> Result<Value, SidecarError> {
15083 let statement_id =
15084 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.run statement id")?;
15085 let params = request.args.get(1);
15086 let statement_state = sqlite_statement(process, statement_id)?.clone();
15087 let kernel_pid = process.kernel_pid;
15088 let database = sqlite_database_mut(process, statement_state.database_id)?;
15089 let before = database.connection.total_changes();
15090 {
15091 let mut statement = database
15092 .connection
15093 .prepare(&statement_state.sql)
15094 .map_err(sqlite_error)?;
15095 bind_sqlite_parameters(
15096 &mut statement,
15097 params,
15098 statement_state.allow_bare_named_parameters,
15099 statement_state.allow_unknown_named_parameters,
15100 )?;
15101 statement.raw_execute().map_err(sqlite_error)?;
15102 }
15103 let changes = database.connection.total_changes().saturating_sub(before);
15104 let last_insert_rowid = database.connection.last_insert_rowid();
15105 mark_sqlite_mutation(database, &statement_state.sql);
15106 sqlite_sync_database(kernel, kernel_pid, database)?;
15107 let result = json!({
15108 "changes": changes,
15109 "lastInsertRowid": encode_sqlite_integer(last_insert_rowid, true),
15110 });
15111 Ok(result)
15112}
15113
15114fn sqlite_get_statement(
15115 process: &mut ActiveProcess,
15116 request: &JavascriptSyncRpcRequest,
15117) -> Result<Value, SidecarError> {
15118 let statement_id =
15119 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.get statement id")?;
15120 let params = request.args.get(1);
15121 let statement_state = sqlite_statement(process, statement_id)?.clone();
15122 let database = sqlite_database_mut(process, statement_state.database_id)?;
15123 let rows = sqlite_query_rows(
15124 &mut database.connection,
15125 &statement_state.sql,
15126 params,
15127 statement_state.return_arrays,
15128 statement_state.read_bigints,
15129 statement_state.allow_bare_named_parameters,
15130 statement_state.allow_unknown_named_parameters,
15131 )?;
15132 Ok(rows
15133 .as_array()
15134 .and_then(|rows| rows.first().cloned())
15135 .unwrap_or(Value::Null))
15136}
15137
15138fn sqlite_all_statement(
15139 process: &mut ActiveProcess,
15140 request: &JavascriptSyncRpcRequest,
15141) -> Result<Value, SidecarError> {
15142 let statement_id =
15143 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.all statement id")?;
15144 let params = request.args.get(1);
15145 let statement_state = sqlite_statement(process, statement_id)?.clone();
15146 let database = sqlite_database_mut(process, statement_state.database_id)?;
15147 sqlite_query_rows(
15148 &mut database.connection,
15149 &statement_state.sql,
15150 params,
15151 statement_state.return_arrays,
15152 statement_state.read_bigints,
15153 statement_state.allow_bare_named_parameters,
15154 statement_state.allow_unknown_named_parameters,
15155 )
15156}
15157
15158fn sqlite_statement_columns(
15159 process: &mut ActiveProcess,
15160 request: &JavascriptSyncRpcRequest,
15161) -> Result<Value, SidecarError> {
15162 let statement_id =
15163 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.columns statement id")?;
15164 let statement_state = sqlite_statement(process, statement_id)?.clone();
15165 let database = sqlite_database_mut(process, statement_state.database_id)?;
15166 let statement = database
15167 .connection
15168 .prepare(&statement_state.sql)
15169 .map_err(sqlite_error)?;
15170 Ok(Value::Array(
15171 statement
15172 .column_names()
15173 .iter()
15174 .map(|name| json!({ "name": name }))
15175 .collect(),
15176 ))
15177}
15178
15179fn sqlite_query_rows(
15180 connection: &mut SqliteConnection,
15181 sql: &str,
15182 params: Option<&Value>,
15183 return_arrays: bool,
15184 read_bigints: bool,
15185 allow_bare_named_parameters: bool,
15186 allow_unknown_named_parameters: bool,
15187) -> Result<Value, SidecarError> {
15188 let mut statement = connection.prepare(sql).map_err(sqlite_error)?;
15189 let column_names = statement
15190 .column_names()
15191 .iter()
15192 .map(|name| (*name).to_owned())
15193 .collect::<Vec<_>>();
15194 let column_count = statement.column_count();
15195 bind_sqlite_parameters(
15196 &mut statement,
15197 params,
15198 allow_bare_named_parameters,
15199 allow_unknown_named_parameters,
15200 )?;
15201 let mut rows = statement.raw_query();
15202 let mut encoded_rows = Vec::new();
15203 while let Some(row) = rows.next().map_err(sqlite_error)? {
15204 encoded_rows.push(encode_sqlite_row(
15205 row,
15206 &column_names,
15207 column_count,
15208 return_arrays,
15209 read_bigints,
15210 )?);
15211 }
15212 Ok(Value::Array(encoded_rows))
15213}
15214
15215fn encode_sqlite_row(
15216 row: &rusqlite::Row<'_>,
15217 column_names: &[String],
15218 column_count: usize,
15219 return_arrays: bool,
15220 read_bigints: bool,
15221) -> Result<Value, SidecarError> {
15222 if return_arrays {
15223 let mut values = Vec::with_capacity(column_count);
15224 for index in 0..column_count {
15225 values.push(encode_sqlite_value_ref(
15226 row.get_ref(index).map_err(sqlite_error)?,
15227 read_bigints,
15228 )?);
15229 }
15230 return Ok(Value::Array(values));
15231 }
15232
15233 let mut object = Map::with_capacity(column_count);
15234 for (index, name) in column_names.iter().enumerate() {
15235 object.insert(
15236 name.clone(),
15237 encode_sqlite_value_ref(row.get_ref(index).map_err(sqlite_error)?, read_bigints)?,
15238 );
15239 }
15240 Ok(Value::Object(object))
15241}
15242
15243fn encode_sqlite_value_ref(
15244 value: SqliteValueRef<'_>,
15245 read_bigints: bool,
15246) -> Result<Value, SidecarError> {
15247 Ok(match value {
15248 SqliteValueRef::Null => Value::Null,
15249 SqliteValueRef::Integer(number) => encode_sqlite_integer(number, read_bigints),
15250 SqliteValueRef::Real(number) => json!(number),
15251 SqliteValueRef::Text(text) => Value::String(String::from_utf8_lossy(text).into_owned()),
15252 SqliteValueRef::Blob(bytes) => json!({
15253 "__agentosSqliteType": "uint8array",
15254 "value": base64::engine::general_purpose::STANDARD.encode(bytes),
15255 }),
15256 })
15257}
15258
15259fn encode_sqlite_integer(number: i64, read_bigints: bool) -> Value {
15260 if read_bigints || number.abs() > SQLITE_JS_SAFE_INTEGER_MAX {
15261 json!({
15262 "__agentosSqliteType": "bigint",
15263 "value": number.to_string(),
15264 })
15265 } else {
15266 json!(number)
15267 }
15268}
15269
15270fn bind_sqlite_parameters(
15271 statement: &mut SqliteStatement<'_>,
15272 params: Option<&Value>,
15273 allow_bare_named_parameters: bool,
15274 allow_unknown_named_parameters: bool,
15275) -> Result<(), SidecarError> {
15276 let Some(params) = params else {
15277 return Ok(());
15278 };
15279 match params {
15280 Value::Null => Ok(()),
15281 Value::Array(values) => {
15282 for (index, value) in values.iter().enumerate() {
15283 statement
15284 .raw_bind_parameter(index + 1, decode_sqlite_parameter(value)?)
15285 .map_err(sqlite_error)?;
15286 }
15287 Ok(())
15288 }
15289 Value::Object(map)
15290 if map
15291 .get("__agentosSqliteType")
15292 .and_then(Value::as_str)
15293 .is_none() =>
15294 {
15295 for (key, value) in map {
15296 let index =
15297 resolve_sqlite_parameter_index(statement, key, allow_bare_named_parameters)?;
15298 let Some(index) = index else {
15299 if allow_unknown_named_parameters {
15300 continue;
15301 }
15302 return Err(SidecarError::InvalidState(format!(
15303 "sqlite named parameter not found: {key}"
15304 )));
15305 };
15306 statement
15307 .raw_bind_parameter(index, decode_sqlite_parameter(value)?)
15308 .map_err(sqlite_error)?;
15309 }
15310 Ok(())
15311 }
15312 other => statement
15313 .raw_bind_parameter(1, decode_sqlite_parameter(other)?)
15314 .map_err(sqlite_error),
15315 }
15316}
15317
15318fn resolve_sqlite_parameter_index(
15319 statement: &mut SqliteStatement<'_>,
15320 key: &str,
15321 allow_bare_named_parameters: bool,
15322) -> Result<Option<usize>, SidecarError> {
15323 let mut candidates = vec![key.to_owned()];
15324 if allow_bare_named_parameters
15325 && !key.starts_with(':')
15326 && !key.starts_with('@')
15327 && !key.starts_with('$')
15328 {
15329 candidates.push(format!(":{key}"));
15330 candidates.push(format!("@{key}"));
15331 candidates.push(format!("${key}"));
15332 }
15333 for candidate in candidates {
15334 if let Some(index) = statement
15335 .parameter_index(&candidate)
15336 .map_err(sqlite_error)?
15337 {
15338 return Ok(Some(index));
15339 }
15340 }
15341 Ok(None)
15342}
15343
15344fn decode_sqlite_parameter(value: &Value) -> Result<rusqlite::types::Value, SidecarError> {
15345 Ok(match value {
15346 Value::Null => rusqlite::types::Value::Null,
15347 Value::Bool(value) => rusqlite::types::Value::Integer(i64::from(*value)),
15348 Value::Number(value) => match (value.as_i64(), value.as_f64()) {
15349 (Some(integer), _) => rusqlite::types::Value::Integer(integer),
15350 (_, Some(real)) => rusqlite::types::Value::Real(real),
15351 _ => {
15352 return Err(SidecarError::InvalidState(String::from(
15353 "sqlite parameter number is not representable",
15354 )));
15355 }
15356 },
15357 Value::String(value) => rusqlite::types::Value::Text(value.clone()),
15358 Value::Array(_) => {
15359 return Err(SidecarError::InvalidState(String::from(
15360 "sqlite parameters do not support nested arrays",
15361 )));
15362 }
15363 Value::Object(map) => match map.get("__agentosSqliteType").and_then(Value::as_str) {
15364 Some("bigint") => rusqlite::types::Value::Integer(
15365 map.get("value")
15366 .and_then(Value::as_str)
15367 .ok_or_else(|| {
15368 SidecarError::InvalidState(String::from(
15369 "sqlite bigint parameter missing string value",
15370 ))
15371 })?
15372 .parse::<i64>()
15373 .map_err(|error| {
15374 SidecarError::InvalidState(format!(
15375 "sqlite bigint parameter is not a signed 64-bit integer: {error}"
15376 ))
15377 })?,
15378 ),
15379 Some("uint8array") => rusqlite::types::Value::Blob(
15380 base64::engine::general_purpose::STANDARD
15381 .decode(map.get("value").and_then(Value::as_str).ok_or_else(|| {
15382 SidecarError::InvalidState(String::from(
15383 "sqlite blob parameter missing base64 value",
15384 ))
15385 })?)
15386 .map_err(|error| {
15387 SidecarError::InvalidState(format!(
15388 "sqlite blob parameter contains invalid base64: {error}"
15389 ))
15390 })?,
15391 ),
15392 Some(other) => {
15393 return Err(SidecarError::InvalidState(format!(
15394 "unsupported sqlite tagged parameter type {other}"
15395 )));
15396 }
15397 None => {
15398 return Err(SidecarError::InvalidState(String::from(
15399 "sqlite named parameter objects must be passed as the top-level params object",
15400 )));
15401 }
15402 },
15403 })
15404}
15405
15406fn close_sqlite_database(
15407 kernel: &mut SidecarKernel,
15408 process: &mut ActiveProcess,
15409 database_id: u64,
15410) -> Result<(), SidecarError> {
15411 let mut database = process
15412 .sqlite_databases
15413 .remove(&database_id)
15414 .ok_or_else(|| {
15415 SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
15416 })?;
15417 process
15418 .sqlite_statements
15419 .retain(|_, statement| statement.database_id != database_id);
15420 sqlite_sync_database(kernel, process.kernel_pid, &mut database)?;
15421 let host_path = database.host_path.clone();
15422 drop(database);
15423 cleanup_sqlite_host_artifacts(host_path.as_deref())?;
15424 Ok(())
15425}
15426
15427fn ensure_per_process_state_handle_capacity(len: usize, label: &str) -> Result<(), SidecarError> {
15428 if len >= MAX_PER_PROCESS_STATE_HANDLES {
15429 return Err(SidecarError::InvalidState(format!(
15430 "{label} handle limit exceeded: limit is {MAX_PER_PROCESS_STATE_HANDLES}"
15431 )));
15432 }
15433 Ok(())
15434}
15435
15436fn sqlite_sync_database(
15437 kernel: &mut SidecarKernel,
15438 kernel_pid: u32,
15439 database: &mut ActiveSqliteDatabase,
15440) -> Result<(), SidecarError> {
15441 if !database.dirty
15442 || database.transaction_depth > 0
15443 || database.read_only
15444 || database.host_path.is_none()
15445 || database.vm_path.is_none()
15446 {
15447 return Ok(());
15448 }
15449
15450 let _ = database
15451 .connection
15452 .execute_batch("PRAGMA wal_checkpoint(TRUNCATE)");
15453 let host_path = database.host_path.as_ref().expect("sqlite host path");
15454 if !host_path.exists() {
15455 return Ok(());
15456 }
15457 ensure_vm_parent_dir(
15458 kernel,
15459 kernel_pid,
15460 database.vm_path.as_deref().expect("sqlite vm path"),
15461 )?;
15462 let contents = fs::read(host_path).map_err(|error| {
15463 SidecarError::Io(format!(
15464 "failed to read sqlite temp database {}: {error}",
15465 host_path.display()
15466 ))
15467 })?;
15468 kernel
15469 .write_file_for_process(
15470 EXECUTION_DRIVER_NAME,
15471 kernel_pid,
15472 database.vm_path.as_deref().expect("sqlite vm path"),
15473 contents,
15474 None,
15475 )
15476 .map_err(kernel_error)?;
15477 database.dirty = false;
15478 Ok(())
15479}
15480
15481fn cleanup_sqlite_host_artifacts(host_path: Option<&Path>) -> Result<(), SidecarError> {
15482 let Some(host_path) = host_path else {
15483 return Ok(());
15484 };
15485 let parent = host_path.parent().map(PathBuf::from);
15486 for suffix in ["", "-wal", "-shm"] {
15487 let path = PathBuf::from(format!("{}{}", host_path.display(), suffix));
15488 if path.exists() {
15489 fs::remove_file(&path).map_err(|error| {
15490 SidecarError::Io(format!(
15491 "failed to remove sqlite temp artifact {}: {error}",
15492 path.display()
15493 ))
15494 })?;
15495 }
15496 }
15497 if let Some(parent) = parent {
15498 let _ = fs::remove_dir_all(parent);
15499 }
15500 Ok(())
15501}
15502
15503fn ensure_vm_parent_dir(
15504 kernel: &mut SidecarKernel,
15505 kernel_pid: u32,
15506 path: &str,
15507) -> Result<(), SidecarError> {
15508 let parent = dirname(path);
15509 if parent == "/" || parent == "." {
15510 return Ok(());
15511 }
15512 let mut current = String::new();
15513 for segment in parent.split('/').filter(|segment| !segment.is_empty()) {
15514 current.push('/');
15515 current.push_str(segment);
15516 if !kernel
15517 .exists_for_process(EXECUTION_DRIVER_NAME, kernel_pid, ¤t)
15518 .map_err(kernel_error)?
15519 {
15520 kernel
15521 .mkdir_for_process(EXECUTION_DRIVER_NAME, kernel_pid, ¤t, false, None)
15522 .map_err(kernel_error)?;
15523 }
15524 }
15525 Ok(())
15526}
15527
15528fn sqlite_database(
15529 process: &ActiveProcess,
15530 database_id: u64,
15531) -> Result<&ActiveSqliteDatabase, SidecarError> {
15532 process.sqlite_databases.get(&database_id).ok_or_else(|| {
15533 SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
15534 })
15535}
15536
15537fn sqlite_database_mut(
15538 process: &mut ActiveProcess,
15539 database_id: u64,
15540) -> Result<&mut ActiveSqliteDatabase, SidecarError> {
15541 process
15542 .sqlite_databases
15543 .get_mut(&database_id)
15544 .ok_or_else(|| {
15545 SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
15546 })
15547}
15548
15549fn sqlite_statement(
15550 process: &ActiveProcess,
15551 statement_id: u64,
15552) -> Result<&ActiveSqliteStatement, SidecarError> {
15553 process.sqlite_statements.get(&statement_id).ok_or_else(|| {
15554 SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
15555 })
15556}
15557
15558fn sqlite_statement_mut(
15559 process: &mut ActiveProcess,
15560 statement_id: u64,
15561) -> Result<&mut ActiveSqliteStatement, SidecarError> {
15562 process
15563 .sqlite_statements
15564 .get_mut(&statement_id)
15565 .ok_or_else(|| {
15566 SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
15567 })
15568}
15569
15570fn mark_sqlite_mutation(database: &mut ActiveSqliteDatabase, sql: &str) {
15571 let normalized = sql.trim_start().to_ascii_lowercase();
15572 if normalized.starts_with("begin") || normalized.starts_with("savepoint") {
15573 database.dirty = true;
15574 database.transaction_depth += 1;
15575 return;
15576 }
15577 if normalized.starts_with("commit") || normalized.starts_with("release savepoint") {
15578 database.dirty = true;
15579 database.transaction_depth = database.transaction_depth.saturating_sub(1);
15580 return;
15581 }
15582 if normalized.starts_with("rollback") && !normalized.starts_with("rollback to") {
15583 database.dirty = true;
15584 database.transaction_depth = database.transaction_depth.saturating_sub(1);
15585 return;
15586 }
15587 if normalized.starts_with("insert")
15588 || normalized.starts_with("update")
15589 || normalized.starts_with("delete")
15590 || normalized.starts_with("replace")
15591 || normalized.starts_with("create")
15592 || normalized.starts_with("alter")
15593 || normalized.starts_with("drop")
15594 || normalized.starts_with("vacuum")
15595 || normalized.starts_with("reindex")
15596 || normalized.starts_with("analyze")
15597 || normalized.starts_with("attach")
15598 || normalized.starts_with("detach")
15599 || normalized.starts_with("pragma")
15600 {
15601 database.dirty = true;
15602 }
15603}
15604
15605fn sqlite_option_bool(options: Option<&Value>, key: &str) -> Option<bool> {
15606 options
15607 .and_then(|value| value.get(key))
15608 .and_then(Value::as_bool)
15609}
15610
15611fn sqlite_option_u64(options: Option<&Value>, key: &str) -> Option<u64> {
15612 options
15613 .and_then(|value| value.get(key))
15614 .and_then(Value::as_u64)
15615}
15616
15617fn sqlite_error(error: rusqlite::Error) -> SidecarError {
15618 SidecarError::InvalidState(format!("sqlite error: {error}"))
15619}
15620
15621pub(crate) fn javascript_sync_rpc_arg_str<'a>(
15622 args: &'a [Value],
15623 index: usize,
15624 label: &str,
15625) -> Result<&'a str, SidecarError> {
15626 args.get(index)
15627 .and_then(Value::as_str)
15628 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a string argument")))
15629}
15630
15631pub(crate) fn javascript_sync_rpc_arg_bool(
15632 args: &[Value],
15633 index: usize,
15634 label: &str,
15635) -> Result<bool, SidecarError> {
15636 args.get(index)
15637 .and_then(Value::as_bool)
15638 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a boolean argument")))
15639}
15640
15641pub(crate) fn javascript_sync_rpc_encoding(args: &[Value]) -> Option<String> {
15642 args.get(1).and_then(|value| {
15643 value.as_str().map(str::to_owned).or_else(|| {
15644 value
15645 .get("encoding")
15646 .and_then(Value::as_str)
15647 .map(str::to_owned)
15648 })
15649 })
15650}
15651
15652pub(crate) fn javascript_sync_rpc_option_bool(
15653 args: &[Value],
15654 index: usize,
15655 key: &str,
15656) -> Option<bool> {
15657 let value = args.get(index)?;
15658 if key == "recursive" {
15659 if let Some(boolean) = value.as_bool() {
15660 return Some(boolean);
15661 }
15662 }
15663 value.get(key).and_then(Value::as_bool)
15664}
15665
15666pub(crate) fn javascript_sync_rpc_option_u32(
15667 args: &[Value],
15668 index: usize,
15669 key: &str,
15670) -> Result<Option<u32>, SidecarError> {
15671 let Some(value) = args.get(index).and_then(|value| {
15672 if value.is_object() {
15673 value.get(key)
15674 } else if key == "mode" && value.is_number() {
15675 Some(value)
15676 } else {
15677 None
15678 }
15679 }) else {
15680 return Ok(None);
15681 };
15682 if value.is_null() {
15683 return Ok(None);
15684 }
15685
15686 let numeric = value
15687 .as_u64()
15688 .or_else(|| {
15689 value
15690 .as_f64()
15691 .filter(|number| number.is_finite() && *number >= 0.0)
15692 .map(|number| number as u64)
15693 })
15694 .ok_or_else(|| SidecarError::InvalidState(format!("{key} must be numeric")))?;
15695
15696 u32::try_from(numeric)
15697 .map(Some)
15698 .map_err(|_| SidecarError::InvalidState(format!("{key} must fit within u32")))
15699}
15700
15701pub(crate) fn javascript_sync_rpc_arg_u32(
15702 args: &[Value],
15703 index: usize,
15704 label: &str,
15705) -> Result<u32, SidecarError> {
15706 let value = javascript_sync_rpc_arg_u64(args, index, label)?;
15707 u32::try_from(value)
15708 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
15709}
15710
15711pub(crate) fn javascript_sync_rpc_arg_i32(
15712 args: &[Value],
15713 index: usize,
15714 label: &str,
15715) -> Result<i32, SidecarError> {
15716 let Some(value) = args.get(index) else {
15717 return Err(SidecarError::InvalidState(format!("{label} is required")));
15718 };
15719
15720 let numeric = value
15721 .as_i64()
15722 .or_else(|| {
15723 value
15724 .as_f64()
15725 .filter(|number| number.is_finite())
15726 .map(|number| number as i64)
15727 })
15728 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))?;
15729
15730 i32::try_from(numeric)
15731 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within i32")))
15732}
15733
15734pub(crate) fn javascript_sync_rpc_arg_u32_optional(
15735 args: &[Value],
15736 index: usize,
15737 label: &str,
15738) -> Result<Option<u32>, SidecarError> {
15739 javascript_sync_rpc_arg_u64_optional(args, index, label)?
15740 .map(|value| {
15741 u32::try_from(value)
15742 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
15743 })
15744 .transpose()
15745}
15746
15747pub(crate) fn javascript_sync_rpc_arg_u64(
15748 args: &[Value],
15749 index: usize,
15750 label: &str,
15751) -> Result<u64, SidecarError> {
15752 let Some(value) = args.get(index) else {
15753 return Err(SidecarError::InvalidState(format!("{label} is required")));
15754 };
15755
15756 value
15757 .as_u64()
15758 .or_else(|| {
15759 value
15760 .as_f64()
15761 .filter(|number| number.is_finite() && *number >= 0.0)
15762 .map(|number| number as u64)
15763 })
15764 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))
15765}
15766
15767pub(crate) fn javascript_sync_rpc_arg_u64_optional(
15768 args: &[Value],
15769 index: usize,
15770 label: &str,
15771) -> Result<Option<u64>, SidecarError> {
15772 let Some(value) = args.get(index) else {
15773 return Ok(None);
15774 };
15775 if value.is_null() {
15776 return Ok(None);
15777 }
15778 javascript_sync_rpc_arg_u64(args, index, label).map(Some)
15779}
15780
15781pub(crate) fn javascript_sync_rpc_bytes_arg(
15782 args: &[Value],
15783 index: usize,
15784 label: &str,
15785) -> Result<Vec<u8>, SidecarError> {
15786 let Some(value) = args.get(index) else {
15787 return Err(SidecarError::InvalidState(format!("{label} is required")));
15788 };
15789
15790 if let Some(text) = value.as_str() {
15791 return Ok(text.as_bytes().to_vec());
15792 }
15793
15794 decode_encoded_bytes_value(value)
15795 .map_err(|error| SidecarError::InvalidState(format!("{label} {error}")))
15796}
15797
15798pub(crate) fn javascript_sync_rpc_bytes_value(bytes: &[u8]) -> Value {
15799 encoded_bytes_value(bytes)
15800}
15801
15802#[derive(Debug, Deserialize)]
15803pub(crate) struct KernelPollFdRequest {
15804 fd: u32,
15805 events: u16,
15806}
15807
15808#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
15809struct KernelPollFdResponse {
15810 fd: u32,
15811 events: u16,
15812 revents: u16,
15813}
15814
15815fn javascript_sync_rpc_base64_arg(
15816 args: &[Value],
15817 index: usize,
15818 label: &str,
15819) -> Result<Vec<u8>, SidecarError> {
15820 let value = javascript_sync_rpc_arg_str(args, index, label)?;
15821 decode_base64(value).map_err(|error| SidecarError::InvalidState(format!("{label} {error}")))
15822}
15823
15824static SYNC_RPC_STATS: std::sync::OnceLock<
15831 std::sync::Mutex<std::collections::BTreeMap<String, u64>>,
15832> = std::sync::OnceLock::new();
15833
15834#[derive(Default)]
15835struct ExecutePhaseStats {
15836 calls: u64,
15837 total_ns: u128,
15838 max_ns: u128,
15839}
15840
15841static EXECUTE_PHASES: OnceLock<Mutex<BTreeMap<String, ExecutePhaseStats>>> = OnceLock::new();
15842static EXECUTE_LIFETIMES: OnceLock<Mutex<BTreeMap<String, Instant>>> = OnceLock::new();
15843static EXECUTE_EXIT_EVENT_QUEUED: OnceLock<Mutex<BTreeMap<String, Instant>>> = OnceLock::new();
15844
15845fn execute_phases_enabled() -> bool {
15846 std::env::var("AGENTOS_EXECUTE_PHASES").as_deref() == Ok("1")
15847}
15848
15849fn execute_phase_key(vm_id: &str, process_id: &str) -> String {
15850 format!("{vm_id}/{process_id}")
15851}
15852
15853pub(crate) fn record_execute_phase(stage: &str, elapsed: Duration) {
15854 if !execute_phases_enabled() {
15855 return;
15856 }
15857 let phases = EXECUTE_PHASES.get_or_init(|| Mutex::new(BTreeMap::new()));
15858 let Ok(mut phases) = phases.lock() else {
15859 return;
15860 };
15861 let stats = phases.entry(stage.to_string()).or_default();
15862 stats.calls += 1;
15863 let elapsed_ns = elapsed.as_nanos();
15864 stats.total_ns += elapsed_ns;
15865 stats.max_ns = stats.max_ns.max(elapsed_ns);
15866
15867 let Some(path) = std::env::var_os("AGENTOS_EXECUTE_PHASES_FILE") else {
15868 return;
15869 };
15870 let mut output = String::new();
15871 for (stage, stats) in phases.iter() {
15872 let total_us = stats.total_ns / 1_000;
15873 let avg_us = if stats.calls == 0 {
15874 0
15875 } else {
15876 total_us / u128::from(stats.calls)
15877 };
15878 let max_us = stats.max_ns / 1_000;
15879 output.push_str(&format!(
15880 "stage={stage} calls={} total_us={total_us} avg_us={avg_us} max_us={max_us}\n",
15881 stats.calls
15882 ));
15883 }
15884 let _ = fs::write(path, output);
15885}
15886
15887fn mark_execute_response_ready(vm_id: &str, process_id: &str) {
15888 if !execute_phases_enabled() {
15889 return;
15890 }
15891 let lifetimes = EXECUTE_LIFETIMES.get_or_init(|| Mutex::new(BTreeMap::new()));
15892 if let Ok(mut lifetimes) = lifetimes.lock() {
15893 lifetimes.insert(execute_phase_key(vm_id, process_id), Instant::now());
15894 }
15895}
15896
15897pub(crate) fn mark_execute_exit_event_queued(vm_id: &str, process_id: &str) {
15898 if !execute_phases_enabled() {
15899 return;
15900 }
15901 let queued = EXECUTE_EXIT_EVENT_QUEUED.get_or_init(|| Mutex::new(BTreeMap::new()));
15902 if let Ok(mut queued) = queued.lock() {
15903 let key = execute_phase_key(vm_id, process_id);
15904 if let std::collections::btree_map::Entry::Vacant(entry) = queued.entry(key) {
15905 record_execute_response_to_exit_milestone(
15906 "execute_response_to_exit_event_queued",
15907 vm_id,
15908 process_id,
15909 );
15910 entry.insert(Instant::now());
15911 }
15912 }
15913}
15914
15915pub(crate) fn record_execute_exit_event_queue_wait(stage: &str, vm_id: &str, process_id: &str) {
15916 if !execute_phases_enabled() {
15917 return;
15918 }
15919 let Some(queued) = EXECUTE_EXIT_EVENT_QUEUED.get() else {
15920 return;
15921 };
15922 let Ok(mut queued) = queued.lock() else {
15923 return;
15924 };
15925 if let Some(started) = queued.remove(&execute_phase_key(vm_id, process_id)) {
15926 record_execute_phase(stage, started.elapsed());
15927 }
15928}
15929
15930pub(crate) fn record_execute_response_to_exit_milestone(
15931 stage: &str,
15932 vm_id: &str,
15933 process_id: &str,
15934) {
15935 if !execute_phases_enabled() {
15936 return;
15937 }
15938 let Some(lifetimes) = EXECUTE_LIFETIMES.get() else {
15939 return;
15940 };
15941 let Ok(lifetimes) = lifetimes.lock() else {
15942 return;
15943 };
15944 if let Some(started) = lifetimes.get(&execute_phase_key(vm_id, process_id)) {
15945 record_execute_phase(stage, started.elapsed());
15946 }
15947}
15948
15949fn record_execute_response_to_exit(vm_id: &str, process_id: &str) {
15950 if !execute_phases_enabled() {
15951 return;
15952 }
15953 let Some(lifetimes) = EXECUTE_LIFETIMES.get() else {
15954 return;
15955 };
15956 let Ok(mut lifetimes) = lifetimes.lock() else {
15957 return;
15958 };
15959 if let Some(started) = lifetimes.remove(&execute_phase_key(vm_id, process_id)) {
15960 record_execute_phase("execute_response_to_exit_event", started.elapsed());
15961 }
15962}
15963
15964fn sync_rpc_trace_enabled() -> bool {
15965 std::env::var("AGENTOS_SYNC_RPC_TRACE").as_deref() == Ok("1")
15966}
15967
15968fn record_sync_rpc(method: &str) {
15969 let stats =
15970 SYNC_RPC_STATS.get_or_init(|| std::sync::Mutex::new(std::collections::BTreeMap::new()));
15971 let Ok(mut map) = stats.lock() else {
15972 return;
15973 };
15974 *map.entry(method.to_string()).or_insert(0) += 1;
15975 let total: u64 = map.values().sum();
15976 if total == 1 || total.is_multiple_of(50) {
15977 let mut top: Vec<(&String, &u64)> = map.iter().collect();
15978 top.sort_by(|a, b| b.1.cmp(a.1));
15979 let breakdown = top
15980 .iter()
15981 .take(8)
15982 .map(|(m, c)| format!("{m}={c}"))
15983 .collect::<Vec<_>>()
15984 .join(" ");
15985 tracing::info!(target: "secure_exec_sidecar::perf", total, %breakdown, "sync_rpc count");
15986 }
15987}
15988
15989fn net_tcp_trace_enabled(env: &BTreeMap<String, String>) -> bool {
15990 env.get("AGENTOS_NET_BRIDGE_TRACE").map(String::as_str) == Some("1")
15991}
15992
15993fn duration_micros_u64(duration: Duration) -> u64 {
15994 u64::try_from(duration.as_micros()).unwrap_or(u64::MAX)
15995}
15996
15997fn net_tcp_trace_reset() {
15998 reset_socket_read_trace();
15999 set_socket_read_trace_enabled(true);
16000 for counter in [
16001 &NET_TCP_TRACE_COUNTERS.socket_read_calls,
16002 &NET_TCP_TRACE_COUNTERS.socket_read_zero_wait_calls,
16003 &NET_TCP_TRACE_COUNTERS.socket_read_data_events,
16004 &NET_TCP_TRACE_COUNTERS.socket_read_bytes,
16005 &NET_TCP_TRACE_COUNTERS.socket_read_kernel_us,
16006 &NET_TCP_TRACE_COUNTERS.socket_read_end_events,
16007 &NET_TCP_TRACE_COUNTERS.socket_read_eagain,
16008 &NET_TCP_TRACE_COUNTERS.socket_read_errors,
16009 &NET_TCP_TRACE_COUNTERS.socket_read_push_attempts,
16010 &NET_TCP_TRACE_COUNTERS.socket_read_push_sent,
16011 &NET_TCP_TRACE_COUNTERS.socket_read_push_missing,
16012 &NET_TCP_TRACE_COUNTERS.socket_read_push_errors,
16013 &NET_TCP_TRACE_COUNTERS.socket_write_calls,
16014 &NET_TCP_TRACE_COUNTERS.socket_write_bytes,
16015 &NET_TCP_TRACE_COUNTERS.socket_write_kernel_us,
16016 &NET_TCP_TRACE_COUNTERS.socket_write_errors,
16017 &NET_TCP_TRACE_COUNTERS.server_accept_calls,
16018 &NET_TCP_TRACE_COUNTERS.server_accept_zero_wait_calls,
16019 &NET_TCP_TRACE_COUNTERS.server_accept_connections,
16020 &NET_TCP_TRACE_COUNTERS.server_accept_eagain,
16021 &NET_TCP_TRACE_COUNTERS.server_accept_errors,
16022 &NET_TCP_TRACE_COUNTERS.kernel_poll_targets,
16023 &NET_TCP_TRACE_COUNTERS.kernel_poll_zero_wait_calls,
16024 &NET_TCP_TRACE_COUNTERS.kernel_poll_wait_us,
16025 &NET_TCP_TRACE_COUNTERS.kernel_poll_elapsed_us,
16026 &NET_TCP_TRACE_COUNTERS.kernel_poll_empty,
16027 &NET_TCP_TRACE_COUNTERS.kernel_poll_ready,
16028 &NET_TCP_TRACE_COUNTERS.kernel_poll_revents_read,
16029 &NET_TCP_TRACE_COUNTERS.kernel_poll_revents_hup,
16030 &NET_TCP_TRACE_COUNTERS.kernel_poll_revents_err,
16031 &NET_TCP_TRACE_COUNTERS.kernel_poll_revents_bits_or,
16032 ] {
16033 counter.store(0, Ordering::Relaxed);
16034 }
16035}
16036
16037fn net_tcp_trace_snapshot() -> Value {
16038 let load = |counter: &AtomicU64| counter.load(Ordering::Relaxed);
16039 let socket_read = socket_read_trace_snapshot();
16040 json!({
16041 "socketReadCalls": load(&NET_TCP_TRACE_COUNTERS.socket_read_calls),
16042 "socketReadZeroWaitCalls": load(&NET_TCP_TRACE_COUNTERS.socket_read_zero_wait_calls),
16043 "socketReadDataEvents": load(&NET_TCP_TRACE_COUNTERS.socket_read_data_events),
16044 "socketReadBytes": load(&NET_TCP_TRACE_COUNTERS.socket_read_bytes),
16045 "socketReadKernelUs": load(&NET_TCP_TRACE_COUNTERS.socket_read_kernel_us),
16046 "socketReadRecordCloneCalls": socket_read.socket_record_clone_calls,
16047 "socketReadRecordCloneUs": socket_read.socket_record_clone_us,
16048 "socketReadRecvCalls": socket_read.read_recv_calls,
16049 "socketReadRecvBytes": socket_read.read_recv_bytes,
16050 "socketReadRecvChunks": socket_read.read_recv_chunks,
16051 "socketReadRecvCopyUs": socket_read.read_recv_copy_us,
16052 "socketReadEndEvents": load(&NET_TCP_TRACE_COUNTERS.socket_read_end_events),
16053 "socketReadEagain": load(&NET_TCP_TRACE_COUNTERS.socket_read_eagain),
16054 "socketReadErrors": load(&NET_TCP_TRACE_COUNTERS.socket_read_errors),
16055 "socketReadPushAttempts": load(&NET_TCP_TRACE_COUNTERS.socket_read_push_attempts),
16056 "socketReadPushSent": load(&NET_TCP_TRACE_COUNTERS.socket_read_push_sent),
16057 "socketReadPushMissing": load(&NET_TCP_TRACE_COUNTERS.socket_read_push_missing),
16058 "socketReadPushErrors": load(&NET_TCP_TRACE_COUNTERS.socket_read_push_errors),
16059 "socketWriteCalls": load(&NET_TCP_TRACE_COUNTERS.socket_write_calls),
16060 "socketWriteBytes": load(&NET_TCP_TRACE_COUNTERS.socket_write_bytes),
16061 "socketWriteKernelUs": load(&NET_TCP_TRACE_COUNTERS.socket_write_kernel_us),
16062 "socketWriteErrors": load(&NET_TCP_TRACE_COUNTERS.socket_write_errors),
16063 "serverAcceptCalls": load(&NET_TCP_TRACE_COUNTERS.server_accept_calls),
16064 "serverAcceptZeroWaitCalls": load(&NET_TCP_TRACE_COUNTERS.server_accept_zero_wait_calls),
16065 "serverAcceptConnections": load(&NET_TCP_TRACE_COUNTERS.server_accept_connections),
16066 "serverAcceptEagain": load(&NET_TCP_TRACE_COUNTERS.server_accept_eagain),
16067 "serverAcceptErrors": load(&NET_TCP_TRACE_COUNTERS.server_accept_errors),
16068 "kernelPollTargets": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_targets),
16069 "kernelPollZeroWaitCalls": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_zero_wait_calls),
16070 "kernelPollWaitUs": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_wait_us),
16071 "kernelPollElapsedUs": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_elapsed_us),
16072 "kernelPollEmpty": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_empty),
16073 "kernelPollReady": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_ready),
16074 "kernelPollReventsRead": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_revents_read),
16075 "kernelPollReventsHup": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_revents_hup),
16076 "kernelPollReventsErr": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_revents_err),
16077 "kernelPollReventsBitsOr": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_revents_bits_or),
16078 })
16079}
16080
16081fn record_net_tcp_kernel_poll(
16082 enabled: bool,
16083 wait: Duration,
16084 elapsed: Duration,
16085 revents: PollEvents,
16086) {
16087 if !enabled {
16088 return;
16089 }
16090 NET_TCP_TRACE_COUNTERS
16091 .kernel_poll_targets
16092 .fetch_add(1, Ordering::Relaxed);
16093 if wait.is_zero() {
16094 NET_TCP_TRACE_COUNTERS
16095 .kernel_poll_zero_wait_calls
16096 .fetch_add(1, Ordering::Relaxed);
16097 }
16098 NET_TCP_TRACE_COUNTERS
16099 .kernel_poll_wait_us
16100 .fetch_add(duration_micros_u64(wait), Ordering::Relaxed);
16101 NET_TCP_TRACE_COUNTERS
16102 .kernel_poll_elapsed_us
16103 .fetch_add(duration_micros_u64(elapsed), Ordering::Relaxed);
16104 if revents.is_empty() {
16105 NET_TCP_TRACE_COUNTERS
16106 .kernel_poll_empty
16107 .fetch_add(1, Ordering::Relaxed);
16108 } else {
16109 NET_TCP_TRACE_COUNTERS
16110 .kernel_poll_ready
16111 .fetch_add(1, Ordering::Relaxed);
16112 NET_TCP_TRACE_COUNTERS
16113 .kernel_poll_revents_bits_or
16114 .fetch_or(u64::from(revents.bits()), Ordering::Relaxed);
16115 }
16116 if revents.intersects(POLLIN) {
16117 NET_TCP_TRACE_COUNTERS
16118 .kernel_poll_revents_read
16119 .fetch_add(1, Ordering::Relaxed);
16120 }
16121 if revents.intersects(POLLHUP) {
16122 NET_TCP_TRACE_COUNTERS
16123 .kernel_poll_revents_hup
16124 .fetch_add(1, Ordering::Relaxed);
16125 }
16126 if revents.intersects(POLLERR) {
16127 NET_TCP_TRACE_COUNTERS
16128 .kernel_poll_revents_err
16129 .fetch_add(1, Ordering::Relaxed);
16130 }
16131}
16132
16133pub(crate) fn service_javascript_sync_rpc<B>(
16134 request: JavascriptSyncRpcServiceRequest<'_, B>,
16135) -> Result<JavascriptSyncRpcServiceResponse, SidecarError>
16136where
16137 B: NativeSidecarBridge + Send + 'static,
16138 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16139{
16140 if sync_rpc_trace_enabled() {
16141 record_sync_rpc(request.sync_request.method.as_str());
16142 }
16143 let JavascriptSyncRpcServiceRequest {
16144 bridge,
16145 vm_id,
16146 dns,
16147 socket_paths,
16148 kernel,
16149 kernel_readiness,
16150 process,
16151 sync_request: request,
16152 resource_limits,
16153 network_counts,
16154 } = request;
16155 if request.raw_bytes_args.contains_key(&usize::MAX) && request.method == "fs.readSync" {
16156 let kernel_pid = process.kernel_pid;
16157 let bytes = service_javascript_fs_read_sync_rpc(kernel, process, kernel_pid, request)?;
16158 return Ok(JavascriptSyncRpcServiceResponse::Raw(bytes));
16159 }
16160 if request.method == "fs.readdirSync" {
16161 let kernel_pid = process.kernel_pid;
16162 let bytes =
16163 service_javascript_fs_readdir_raw_sync_rpc(kernel, process, kernel_pid, request)?;
16164 return Ok(JavascriptSyncRpcServiceResponse::Raw(bytes));
16165 }
16166 let response = match request.method.as_str() {
16167 "__bench.noop" => Ok(Value::Null),
16168 "__bench.net_tcp_metrics_reset" => {
16169 net_tcp_trace_reset();
16170 Ok(Value::Null)
16171 }
16172 "__bench.net_tcp_metrics_snapshot" => Ok(net_tcp_trace_snapshot()),
16173 "_resolveModule"
16176 | "_resolveModuleSync"
16177 | "__resolve_module"
16178 | "_batchResolveModules"
16179 | "__batch_resolve_modules"
16180 | "_loadFile"
16181 | "_loadFileSync"
16182 | "__load_file"
16183 | "_moduleFormat"
16184 | "__module_format" => service_javascript_module_sync_rpc(kernel, process, request),
16185 "_loadPolyfill" | "__load_polyfill" => {
16187 service_javascript_internal_bridge_sync_rpc(process, request)
16188 }
16189 "__kernel_stdin_read" => {
16190 let js_local_bridge = matches!(process.execution, ActiveExecution::Javascript(_))
16196 && process.tty_master_fd.is_none();
16197 if js_local_bridge {
16198 match &process.execution {
16199 ActiveExecution::Javascript(execution) => execution
16200 .read_kernel_stdin_sync_rpc(request)
16201 .map_err(|error| SidecarError::Execution(error.to_string())),
16202 _ => unreachable!("js_local_bridge implies a JavaScript execution"),
16203 }
16204 } else {
16205 service_javascript_kernel_stdin_sync_rpc(kernel, process, request)
16206 }
16207 }
16208 "__kernel_stdio_write" => {
16209 service_javascript_kernel_stdio_write_sync_rpc(kernel, process, request)
16210 }
16211 "__kernel_isatty" => service_javascript_kernel_isatty_sync_rpc(kernel, process, request),
16212 "__kernel_tty_size" => {
16213 service_javascript_kernel_tty_size_sync_rpc(kernel, process, request)
16214 }
16215 "__kernel_poll" => service_javascript_kernel_poll_sync_rpc(kernel, process, request),
16216 "__pty_set_raw_mode" => {
16217 service_javascript_pty_set_raw_mode_sync_rpc(kernel, process, request)
16218 }
16219 "crypto.hashDigest"
16220 | "crypto.hmacDigest"
16221 | "crypto.pbkdf2"
16222 | "crypto.scrypt"
16223 | "crypto.cipheriv"
16224 | "crypto.decipheriv"
16225 | "crypto.cipherivCreate"
16226 | "crypto.cipherivUpdate"
16227 | "crypto.cipherivFinal"
16228 | "crypto.sign"
16229 | "crypto.verify"
16230 | "crypto.asymmetricOp"
16231 | "crypto.createKeyObject"
16232 | "crypto.generateKeyPairSync"
16233 | "crypto.generateKeySync"
16234 | "crypto.generatePrimeSync"
16235 | "crypto.diffieHellman"
16236 | "crypto.diffieHellmanGroup"
16237 | "crypto.diffieHellmanSessionCreate"
16238 | "crypto.diffieHellmanSessionCall"
16239 | "crypto.diffieHellmanSessionDestroy"
16240 | "crypto.subtle" => service_javascript_crypto_sync_rpc(process, request),
16241 "dns.lookup" | "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
16242 service_javascript_dns_sync_rpc(bridge, kernel, vm_id, dns, request)
16243 }
16244 "net.http_listen" | "net.http_close" | "net.http_wait" | "net.http_respond" => {
16245 return service_javascript_net_sync_rpc_response(JavascriptNetSyncRpcServiceRequest {
16246 bridge,
16247 vm_id,
16248 dns,
16249 socket_paths,
16250 kernel,
16251 kernel_readiness: Arc::clone(&kernel_readiness),
16252 process,
16253 sync_request: request,
16254 resource_limits,
16255 network_counts,
16256 })
16257 }
16258 "net.http2_server_listen"
16259 | "net.http2_server_poll"
16260 | "net.http2_server_close"
16261 | "net.http2_server_respond"
16262 | "net.http2_server_wait"
16263 | "net.http2_session_connect"
16264 | "net.http2_session_request"
16265 | "net.http2_session_settings"
16266 | "net.http2_session_set_local_window_size"
16267 | "net.http2_session_goaway"
16268 | "net.http2_session_close"
16269 | "net.http2_session_destroy"
16270 | "net.http2_session_poll"
16271 | "net.http2_session_wait"
16272 | "net.http2_stream_respond"
16273 | "net.http2_stream_push_stream"
16274 | "net.http2_stream_write"
16275 | "net.http2_stream_end"
16276 | "net.http2_stream_close"
16277 | "net.http2_stream_pause"
16278 | "net.http2_stream_resume"
16279 | "net.http2_stream_respond_with_file" => {
16280 service_javascript_http2_sync_rpc(JavascriptHttp2SyncRpcServiceRequest {
16281 bridge,
16282 kernel,
16283 vm_id,
16284 dns,
16285 socket_paths,
16286 process,
16287 sync_request: request,
16288 resource_limits,
16289 network_counts,
16290 })
16291 }
16292 "net.connect"
16293 | "net.reserve_tcp_port"
16294 | "net.release_tcp_port"
16295 | "net.listen"
16296 | "net.poll"
16297 | "net.socket_wait_connect"
16298 | "net.socket_read"
16299 | "net.socket_set_no_delay"
16300 | "net.socket_set_keep_alive"
16301 | "net.socket_upgrade_tls"
16302 | "net.socket_get_tls_client_hello"
16303 | "net.socket_tls_query"
16304 | "net.server_poll"
16305 | "net.server_accept"
16306 | "net.server_connections"
16307 | "net.upgrade_socket_write"
16308 | "net.upgrade_socket_end"
16309 | "net.upgrade_socket_destroy"
16310 | "net.write"
16311 | "net.shutdown"
16312 | "net.destroy"
16313 | "net.server_close"
16314 | "tls.get_ciphers" => {
16315 return service_javascript_net_sync_rpc_response(JavascriptNetSyncRpcServiceRequest {
16316 bridge,
16317 vm_id,
16318 dns,
16319 socket_paths,
16320 kernel,
16321 kernel_readiness: Arc::clone(&kernel_readiness),
16322 process,
16323 sync_request: request,
16324 resource_limits,
16325 network_counts,
16326 })
16327 }
16328 "dgram.createSocket"
16329 | "dgram.bind"
16330 | "dgram.send"
16331 | "dgram.poll"
16332 | "dgram.close"
16333 | "dgram.address"
16334 | "dgram.setBufferSize"
16335 | "dgram.getBufferSize" => {
16336 service_javascript_dgram_sync_rpc(JavascriptDgramSyncRpcServiceRequest {
16337 bridge,
16338 kernel,
16339 vm_id,
16340 dns,
16341 socket_paths,
16342 process,
16343 kernel_readiness,
16344 sync_request: request,
16345 resource_limits,
16346 network_counts,
16347 })
16348 }
16349 "sqlite.constants"
16350 | "sqlite.open"
16351 | "sqlite.close"
16352 | "sqlite.exec"
16353 | "sqlite.query"
16354 | "sqlite.prepare"
16355 | "sqlite.location"
16356 | "sqlite.checkpoint"
16357 | "sqlite.statement.run"
16358 | "sqlite.statement.get"
16359 | "sqlite.statement.all"
16360 | "sqlite.statement.iterate"
16361 | "sqlite.statement.columns"
16362 | "sqlite.statement.setReturnArrays"
16363 | "sqlite.statement.setReadBigInts"
16364 | "sqlite.statement.setAllowBareNamedParameters"
16365 | "sqlite.statement.setAllowUnknownNamedParameters"
16366 | "sqlite.statement.finalize" => {
16367 service_javascript_sqlite_sync_rpc(kernel, process, request)
16368 }
16369 "process.kill" => {
16370 let target_pid =
16371 javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
16372 let signal = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
16373 let parsed_signal = parse_signal(signal)?;
16374 if parsed_signal == 0 {
16375 kernel
16376 .signal_process(EXECUTION_DRIVER_NAME, target_pid, parsed_signal)
16377 .map_err(kernel_error)?;
16378 return Ok(Value::Null.into());
16379 }
16380 let process_pid = i32::try_from(process.kernel_pid)
16381 .map_err(|_| SidecarError::InvalidState("process pid exceeds i32".into()))?;
16382 if target_pid != process_pid {
16383 return Err(SidecarError::InvalidState(format!(
16384 "unknown process pid {target_pid}"
16385 )));
16386 }
16387 process.pending_self_signal_exit = None;
16388 if parsed_signal != 0
16389 && !matches!(
16390 canonical_signal_name(parsed_signal),
16391 Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
16392 )
16393 {
16394 process.pending_self_signal_exit = Some(parsed_signal);
16395 }
16396 Ok(json!({
16397 "self": true,
16398 "action": "default",
16399 }))
16400 }
16401 "process.umask" => {
16402 let new_mask = javascript_sync_rpc_arg_u32_optional(&request.args, 0, "process umask")?;
16403 kernel
16404 .umask(EXECUTION_DRIVER_NAME, process.kernel_pid, new_mask)
16405 .map(|mask| json!(mask))
16406 .map_err(kernel_error)
16407 }
16408 "fs.chmodSync" | "fs.promises.chmod" => {
16409 let response =
16410 service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request)?;
16411 mirror_process_chmod_to_host(process, request)?;
16412 Ok(response)
16413 }
16414 _ => service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request),
16415 }?;
16416 Ok(response.into())
16417}
16418
16419fn service_javascript_internal_bridge_sync_rpc(
16420 process: &ActiveProcess,
16421 request: &JavascriptSyncRpcRequest,
16422) -> Result<Value, SidecarError> {
16423 let method = match request.method.as_str() {
16427 "_loadPolyfill" | "__load_polyfill" => "_loadPolyfill",
16428 other => {
16429 return Err(SidecarError::InvalidState(format!(
16430 "unsupported JavaScript internal bridge method {other}"
16431 )));
16432 }
16433 };
16434
16435 handle_internal_bridge_call_from_host_context(
16436 &process.host_cwd,
16437 &process.guest_cwd,
16438 &process.env,
16439 method,
16440 &request.args,
16441 )
16442 .ok_or_else(|| {
16443 SidecarError::InvalidState(format!(
16444 "JavaScript internal bridge method {method} returned no value"
16445 ))
16446 })
16447}
16448
16449fn mirror_process_chmod_to_host(
16450 process: &ActiveProcess,
16451 request: &JavascriptSyncRpcRequest,
16452) -> Result<(), SidecarError> {
16453 let guest_path = javascript_sync_rpc_arg_str(&request.args, 0, "filesystem chmod path")?;
16454 let mode = javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem chmod mode")? & 0o7777;
16455 let Some(host_path) = resolve_process_guest_path_to_host(process, guest_path) else {
16456 return Ok(());
16457 };
16458 if !host_path.exists() {
16459 return Ok(());
16460 }
16461 fs::set_permissions(&host_path, fs::Permissions::from_mode(mode)).map_err(|error| {
16462 SidecarError::Io(format!(
16463 "failed to mirror chmod to host path {}: {error}",
16464 host_path.display()
16465 ))
16466 })
16467}
16468
16469fn resolve_process_guest_path_to_host(
16470 process: &ActiveProcess,
16471 guest_path: &str,
16472) -> Option<PathBuf> {
16473 let normalized_guest_path = if guest_path.starts_with('/') {
16474 normalize_path(guest_path)
16475 } else {
16476 normalize_path(&format!(
16477 "{}/{}",
16478 process.guest_cwd.trim_end_matches('/'),
16479 guest_path
16480 ))
16481 };
16482 if let Some(host_path) =
16483 host_path_from_runtime_guest_mappings(&process.env, &normalized_guest_path)
16484 {
16485 return Some(host_path);
16486 }
16487 let normalized_guest_cwd = normalize_path(&process.guest_cwd);
16488 let mut host_root = normalize_host_path(&process.host_cwd);
16489 for _ in normalized_guest_cwd
16490 .trim_start_matches('/')
16491 .split('/')
16492 .filter(|segment| !segment.is_empty())
16493 {
16494 host_root = host_root.parent()?.to_path_buf();
16495 }
16496 if normalized_guest_path == "/" {
16497 Some(host_root)
16498 } else {
16499 Some(host_root.join(normalized_guest_path.trim_start_matches('/')))
16500 }
16501}
16502
16503pub(crate) fn service_javascript_crypto_sync_rpc(
16504 process: &mut ActiveProcess,
16505 request: &JavascriptSyncRpcRequest,
16506) -> Result<Value, SidecarError> {
16507 match request.method.as_str() {
16508 "crypto.hashDigest" => {
16509 let algorithm = javascript_crypto_digest_algorithm(
16510 &request.args,
16511 0,
16512 "crypto.hashDigest algorithm",
16513 )?;
16514 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hashDigest data")?;
16515 Ok(Value::String(
16516 base64::engine::general_purpose::STANDARD.encode(algorithm.digest(&data)),
16517 ))
16518 }
16519 "crypto.hmacDigest" => {
16520 let algorithm = javascript_crypto_digest_algorithm(
16521 &request.args,
16522 0,
16523 "crypto.hmacDigest algorithm",
16524 )?;
16525 let key = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hmacDigest key")?;
16526 let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.hmacDigest data")?;
16527 Ok(Value::String(
16528 base64::engine::general_purpose::STANDARD.encode(algorithm.hmac(&key, &data)?),
16529 ))
16530 }
16531 "crypto.pbkdf2" => {
16532 let password =
16533 javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.pbkdf2 password")?;
16534 let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.pbkdf2 salt")?;
16535 let iterations =
16536 javascript_sync_rpc_arg_u32(&request.args, 2, "crypto.pbkdf2 iterations")?;
16537 if iterations == 0 {
16538 return Err(SidecarError::InvalidState(String::from(
16539 "crypto.pbkdf2 iterations must be greater than zero",
16540 )));
16541 }
16542 let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
16543 &request.args,
16544 3,
16545 "crypto.pbkdf2 key length",
16546 )?)
16547 .map_err(|_| {
16548 SidecarError::InvalidState(String::from(
16549 "crypto.pbkdf2 key length must fit within usize",
16550 ))
16551 })?;
16552 let algorithm =
16553 javascript_crypto_digest_algorithm(&request.args, 4, "crypto.pbkdf2 digest")?;
16554 let mut output = vec![0u8; key_len];
16555 algorithm.pbkdf2(&password, &salt, iterations, &mut output);
16556 Ok(Value::String(
16557 base64::engine::general_purpose::STANDARD.encode(output),
16558 ))
16559 }
16560 "crypto.scrypt" => {
16561 let password =
16562 javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.scrypt password")?;
16563 let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.scrypt salt")?;
16564 let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
16565 &request.args,
16566 2,
16567 "crypto.scrypt key length",
16568 )?)
16569 .map_err(|_| {
16570 SidecarError::InvalidState(String::from(
16571 "crypto.scrypt key length must fit within usize",
16572 ))
16573 })?;
16574 let options_json =
16575 javascript_sync_rpc_arg_str(&request.args, 3, "crypto.scrypt options")?;
16576 let options: JavascriptScryptOptions =
16577 serde_json::from_str(options_json).map_err(|error| {
16578 SidecarError::InvalidState(format!(
16579 "crypto.scrypt options must be valid JSON: {error}"
16580 ))
16581 })?;
16582 let cost = options.cost.unwrap_or(DEFAULT_SCRYPT_COST);
16583 if cost == 0 || !cost.is_power_of_two() {
16584 return Err(SidecarError::InvalidState(String::from(
16585 "crypto.scrypt cost must be a positive power of two",
16586 )));
16587 }
16588 let log_n = u8::try_from(cost.ilog2()).map_err(|_| {
16589 SidecarError::InvalidState(String::from(
16590 "crypto.scrypt cost exceeds supported parameter range",
16591 ))
16592 })?;
16593 let params = ScryptParams::new(
16594 log_n,
16595 options.block_size.unwrap_or(DEFAULT_SCRYPT_BLOCK_SIZE),
16596 options
16597 .parallelization
16598 .unwrap_or(DEFAULT_SCRYPT_PARALLELIZATION),
16599 key_len,
16600 )
16601 .map_err(|error| {
16602 SidecarError::InvalidState(format!("crypto.scrypt options are invalid: {error}"))
16603 })?;
16604 let mut output = vec![0u8; key_len];
16605 scrypt(&password, &salt, ¶ms, &mut output).map_err(|error| {
16606 SidecarError::Execution(format!("crypto.scrypt failed: {error}"))
16607 })?;
16608 Ok(Value::String(
16609 base64::engine::general_purpose::STANDARD.encode(output),
16610 ))
16611 }
16612 "crypto.cipheriv" => service_javascript_crypto_cipheriv_sync_rpc(request),
16613 "crypto.decipheriv" => service_javascript_crypto_decipheriv_sync_rpc(request),
16614 "crypto.cipherivCreate" => {
16615 service_javascript_crypto_cipheriv_create_sync_rpc(process, request)
16616 }
16617 "crypto.cipherivUpdate" => {
16618 service_javascript_crypto_cipheriv_update_sync_rpc(process, request)
16619 }
16620 "crypto.cipherivFinal" => {
16621 service_javascript_crypto_cipheriv_final_sync_rpc(process, request)
16622 }
16623 "crypto.sign" => service_javascript_crypto_sign_sync_rpc(request),
16624 "crypto.verify" => service_javascript_crypto_verify_sync_rpc(request),
16625 "crypto.asymmetricOp" => service_javascript_crypto_asymmetric_op_sync_rpc(request),
16626 "crypto.createKeyObject" => service_javascript_crypto_create_key_object_sync_rpc(request),
16627 "crypto.generateKeyPairSync" => {
16628 service_javascript_crypto_generate_key_pair_sync_rpc(request)
16629 }
16630 "crypto.generateKeySync" => service_javascript_crypto_generate_key_sync_rpc(request),
16631 "crypto.generatePrimeSync" => service_javascript_crypto_generate_prime_sync_rpc(request),
16632 "crypto.diffieHellman" => service_javascript_crypto_diffie_hellman_sync_rpc(request),
16633 "crypto.diffieHellmanGroup" => {
16634 service_javascript_crypto_diffie_hellman_group_sync_rpc(request)
16635 }
16636 "crypto.diffieHellmanSessionCreate" => {
16637 service_javascript_crypto_diffie_hellman_session_create_sync_rpc(process, request)
16638 }
16639 "crypto.diffieHellmanSessionCall" => {
16640 service_javascript_crypto_diffie_hellman_session_call_sync_rpc(process, request)
16641 }
16642 "crypto.diffieHellmanSessionDestroy" => {
16643 service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(process, request)
16644 }
16645 "crypto.subtle" => service_javascript_crypto_subtle_sync_rpc(request),
16646 _ => Err(SidecarError::InvalidState(format!(
16647 "unsupported JavaScript crypto sync RPC method {}",
16648 request.method
16649 ))),
16650 }
16651}
16652
16653fn javascript_crypto_digest_algorithm(
16654 args: &[Value],
16655 index: usize,
16656 label: &str,
16657) -> Result<JavascriptCryptoDigestAlgorithm, SidecarError> {
16658 JavascriptCryptoDigestAlgorithm::parse(javascript_sync_rpc_arg_str(args, index, label)?)
16659}
16660
16661impl JavascriptCryptoDigestAlgorithm {
16662 fn parse(value: &str) -> Result<Self, SidecarError> {
16663 match value.trim().to_ascii_lowercase().replace('-', "").as_str() {
16664 "md5" => Ok(Self::Md5),
16665 "sha1" => Ok(Self::Sha1),
16666 "sha224" => Ok(Self::Sha224),
16667 "sha256" => Ok(Self::Sha256),
16668 "sha384" => Ok(Self::Sha384),
16669 "sha512" => Ok(Self::Sha512),
16670 _ => Err(SidecarError::InvalidState(format!(
16671 "unsupported crypto digest algorithm {value}"
16672 ))),
16673 }
16674 }
16675
16676 fn digest(self, data: &[u8]) -> Vec<u8> {
16677 match self {
16678 Self::Md5 => Md5::digest(data).to_vec(),
16679 Self::Sha1 => Sha1::digest(data).to_vec(),
16680 Self::Sha224 => Sha224::digest(data).to_vec(),
16681 Self::Sha256 => Sha256::digest(data).to_vec(),
16682 Self::Sha384 => Sha384::digest(data).to_vec(),
16683 Self::Sha512 => Sha512::digest(data).to_vec(),
16684 }
16685 }
16686
16687 fn hmac(self, key: &[u8], data: &[u8]) -> Result<Vec<u8>, SidecarError> {
16688 match self {
16689 Self::Md5 => {
16690 let mut mac = Hmac::<Md5>::new_from_slice(key).map_err(|error| {
16691 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
16692 })?;
16693 mac.update(data);
16694 Ok(mac.finalize().into_bytes().to_vec())
16695 }
16696 Self::Sha1 => {
16697 let mut mac = Hmac::<Sha1>::new_from_slice(key).map_err(|error| {
16698 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
16699 })?;
16700 mac.update(data);
16701 Ok(mac.finalize().into_bytes().to_vec())
16702 }
16703 Self::Sha224 => {
16704 let mut mac = Hmac::<Sha224>::new_from_slice(key).map_err(|error| {
16705 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
16706 })?;
16707 mac.update(data);
16708 Ok(mac.finalize().into_bytes().to_vec())
16709 }
16710 Self::Sha256 => {
16711 let mut mac = Hmac::<Sha256>::new_from_slice(key).map_err(|error| {
16712 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
16713 })?;
16714 mac.update(data);
16715 Ok(mac.finalize().into_bytes().to_vec())
16716 }
16717 Self::Sha384 => {
16718 let mut mac = Hmac::<Sha384>::new_from_slice(key).map_err(|error| {
16719 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
16720 })?;
16721 mac.update(data);
16722 Ok(mac.finalize().into_bytes().to_vec())
16723 }
16724 Self::Sha512 => {
16725 let mut mac = Hmac::<Sha512>::new_from_slice(key).map_err(|error| {
16726 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
16727 })?;
16728 mac.update(data);
16729 Ok(mac.finalize().into_bytes().to_vec())
16730 }
16731 }
16732 }
16733
16734 fn pbkdf2(self, password: &[u8], salt: &[u8], iterations: u32, output: &mut [u8]) {
16735 match self {
16736 Self::Md5 => pbkdf2_hmac::<Md5>(password, salt, iterations, output),
16737 Self::Sha1 => pbkdf2_hmac::<Sha1>(password, salt, iterations, output),
16738 Self::Sha224 => pbkdf2_hmac::<Sha224>(password, salt, iterations, output),
16739 Self::Sha256 => pbkdf2_hmac::<Sha256>(password, salt, iterations, output),
16740 Self::Sha384 => pbkdf2_hmac::<Sha384>(password, salt, iterations, output),
16741 Self::Sha512 => pbkdf2_hmac::<Sha512>(password, salt, iterations, output),
16742 }
16743 }
16744}
16745
16746#[derive(Debug, Clone)]
16747enum JavascriptCryptoKeyMaterial {
16748 Private(PKey<Private>),
16749 Public(PKey<Public>),
16750 Secret(Vec<u8>),
16751}
16752
16753#[derive(Debug, Clone, Deserialize, Serialize)]
16754struct JavascriptSerializedSandboxKeyObject {
16755 #[serde(rename = "type")]
16756 kind: String,
16757 #[serde(skip_serializing_if = "Option::is_none")]
16758 pem: Option<String>,
16759 #[serde(skip_serializing_if = "Option::is_none")]
16760 raw: Option<String>,
16761 #[serde(skip_serializing_if = "Option::is_none", rename = "asymmetricKeyType")]
16762 asymmetric_key_type: Option<String>,
16763 #[serde(
16764 skip_serializing_if = "Option::is_none",
16765 rename = "asymmetricKeyDetails"
16766 )]
16767 asymmetric_key_details: Option<Map<String, Value>>,
16768 #[serde(skip_serializing_if = "Option::is_none")]
16769 jwk: Option<Value>,
16770}
16771
16772#[derive(Debug, Clone)]
16773struct JavascriptDirectKeyInput {
16774 key: JavascriptCryptoKeyMaterial,
16775 padding: Option<Padding>,
16776}
16777
16778fn service_javascript_crypto_cipheriv_sync_rpc(
16779 request: &JavascriptSyncRpcRequest,
16780) -> Result<Value, SidecarError> {
16781 service_javascript_crypto_cipheriv_inner(request, false)
16782}
16783
16784fn service_javascript_crypto_decipheriv_sync_rpc(
16785 request: &JavascriptSyncRpcRequest,
16786) -> Result<Value, SidecarError> {
16787 service_javascript_crypto_cipheriv_inner(request, true)
16788}
16789
16790fn service_javascript_crypto_cipheriv_create_sync_rpc(
16791 process: &mut ActiveProcess,
16792 request: &JavascriptSyncRpcRequest,
16793) -> Result<Value, SidecarError> {
16794 ensure_per_process_state_handle_capacity(process.cipher_sessions.len(), "cipher session")?;
16795 let mode = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.cipherivCreate mode")?;
16796 let decrypt = mode == "decipher";
16797 let algorithm =
16798 javascript_sync_rpc_arg_str(&request.args, 1, "crypto.cipherivCreate algorithm")?;
16799 let key = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.cipherivCreate key")?;
16800 let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 3, "crypto.cipherivCreate iv")?;
16801 let options =
16802 javascript_sync_rpc_json_arg_optional(&request.args, 4, "crypto.cipherivCreate options")?;
16803 let context = javascript_crypto_build_cipher_session(
16804 algorithm,
16805 &key,
16806 iv.as_deref(),
16807 decrypt,
16808 options.as_ref(),
16809 )?;
16810 process.next_cipher_session_id += 1;
16811 let session_id = process.next_cipher_session_id;
16812 process
16813 .cipher_sessions
16814 .insert(session_id, ActiveCipherSession { context });
16815 Ok(json!(session_id))
16816}
16817
16818fn service_javascript_crypto_cipheriv_update_sync_rpc(
16819 process: &mut ActiveProcess,
16820 request: &JavascriptSyncRpcRequest,
16821) -> Result<Value, SidecarError> {
16822 let session_id =
16823 javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivUpdate session id")?;
16824 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.cipherivUpdate data")?;
16825 let session = process
16826 .cipher_sessions
16827 .get_mut(&session_id)
16828 .ok_or_else(|| {
16829 SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
16830 })?;
16831 let result = javascript_crypto_cipher_update(&mut session.context, &data)?;
16832 Ok(Value::String(
16833 base64::engine::general_purpose::STANDARD.encode(result),
16834 ))
16835}
16836
16837fn service_javascript_crypto_cipheriv_final_sync_rpc(
16838 process: &mut ActiveProcess,
16839 request: &JavascriptSyncRpcRequest,
16840) -> Result<Value, SidecarError> {
16841 let session_id =
16842 javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivFinal session id")?;
16843 let session = process.cipher_sessions.remove(&session_id).ok_or_else(|| {
16844 SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
16845 })?;
16846 let outcome = session
16847 .context
16848 .finalize()
16849 .map_err(javascript_crypto_cipher_error)?;
16850 let mut response = Map::new();
16851 response.insert(
16852 String::from("data"),
16853 Value::String(base64::engine::general_purpose::STANDARD.encode(outcome.data)),
16854 );
16855 if let Some(auth_tag) = outcome.auth_tag {
16856 response.insert(
16857 String::from("authTag"),
16858 Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
16859 );
16860 }
16861 Ok(Value::String(serde_json::to_string(&response).map_err(
16862 |error| SidecarError::InvalidState(format!("serialize cipher final response: {error}")),
16863 )?))
16864}
16865
16866fn service_javascript_crypto_sign_sync_rpc(
16867 request: &JavascriptSyncRpcRequest,
16868) -> Result<Value, SidecarError> {
16869 let algorithm = request.args.first().and_then(Value::as_str);
16870 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.sign data")?;
16871 let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.sign key")?;
16872 let key_input =
16873 javascript_crypto_parse_direct_key_input(key_json, Some("private"), "crypto.sign key")?;
16874 let private_key = javascript_crypto_expect_private_key(key_input.key, "crypto.sign key")?;
16875 let mut signer = javascript_crypto_new_signer(algorithm, &private_key)?;
16876 if let Some(padding) = key_input.padding {
16877 signer
16878 .set_rsa_padding(padding)
16879 .map_err(javascript_crypto_openssl_error)?;
16880 }
16881 signer
16882 .update(&data)
16883 .map_err(javascript_crypto_openssl_error)?;
16884 Ok(Value::String(
16885 base64::engine::general_purpose::STANDARD.encode(
16886 signer
16887 .sign_to_vec()
16888 .map_err(javascript_crypto_openssl_error)?,
16889 ),
16890 ))
16891}
16892
16893fn service_javascript_crypto_verify_sync_rpc(
16894 request: &JavascriptSyncRpcRequest,
16895) -> Result<Value, SidecarError> {
16896 let algorithm = request.args.first().and_then(Value::as_str);
16897 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.verify data")?;
16898 let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.verify key")?;
16899 let signature = javascript_sync_rpc_base64_arg(&request.args, 3, "crypto.verify signature")?;
16900 let key_input =
16901 javascript_crypto_parse_direct_key_input(key_json, Some("public"), "crypto.verify key")?;
16902 let public_key = javascript_crypto_expect_public_key(key_input.key, "crypto.verify key")?;
16903 let mut verifier = javascript_crypto_new_verifier(algorithm, &public_key)?;
16904 if let Some(padding) = key_input.padding {
16905 verifier
16906 .set_rsa_padding(padding)
16907 .map_err(javascript_crypto_openssl_error)?;
16908 }
16909 verifier
16910 .update(&data)
16911 .map_err(javascript_crypto_openssl_error)?;
16912 Ok(json!(verifier
16913 .verify(&signature)
16914 .map_err(javascript_crypto_openssl_error)?))
16915}
16916
16917fn service_javascript_crypto_asymmetric_op_sync_rpc(
16918 request: &JavascriptSyncRpcRequest,
16919) -> Result<Value, SidecarError> {
16920 let operation = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.asymmetricOp operation")?;
16921 let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.asymmetricOp key")?;
16922 let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.asymmetricOp data")?;
16923 let expect_kind = match operation {
16924 "publicEncrypt" | "publicDecrypt" => Some("public"),
16925 "privateEncrypt" | "privateDecrypt" => Some("private"),
16926 other => {
16927 return Err(SidecarError::InvalidState(format!(
16928 "Unsupported asymmetric crypto operation: {other}"
16929 )));
16930 }
16931 };
16932 let key_input =
16933 javascript_crypto_parse_direct_key_input(key_json, expect_kind, "crypto.asymmetricOp key")?;
16934 let padding = key_input.padding.unwrap_or(Padding::PKCS1);
16935 let mut output = vec![0_u8; javascript_crypto_rsa_output_size(&key_input.key)?];
16936 let written = match (operation, key_input.key) {
16937 ("publicEncrypt", JavascriptCryptoKeyMaterial::Public(key))
16938 | ("publicDecrypt", JavascriptCryptoKeyMaterial::Public(key)) => {
16939 let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
16940 if operation == "publicEncrypt" {
16941 rsa.public_encrypt(&data, &mut output, padding)
16942 .map_err(javascript_crypto_openssl_error)?
16943 } else {
16944 rsa.public_decrypt(&data, &mut output, padding)
16945 .map_err(javascript_crypto_openssl_error)?
16946 }
16947 }
16948 ("privateEncrypt", JavascriptCryptoKeyMaterial::Private(key))
16949 | ("privateDecrypt", JavascriptCryptoKeyMaterial::Private(key)) => {
16950 let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
16951 if operation == "privateEncrypt" {
16952 rsa.private_encrypt(&data, &mut output, padding)
16953 .map_err(javascript_crypto_openssl_error)?
16954 } else {
16955 rsa.private_decrypt(&data, &mut output, padding)
16956 .map_err(javascript_crypto_openssl_error)?
16957 }
16958 }
16959 _ => {
16960 return Err(SidecarError::InvalidState(format!(
16961 "{operation} requires an RSA {} key",
16962 expect_kind.unwrap_or("asymmetric")
16963 )));
16964 }
16965 };
16966 output.truncate(written);
16967 Ok(Value::String(
16968 base64::engine::general_purpose::STANDARD.encode(output),
16969 ))
16970}
16971
16972fn service_javascript_crypto_create_key_object_sync_rpc(
16973 request: &JavascriptSyncRpcRequest,
16974) -> Result<Value, SidecarError> {
16975 let operation =
16976 javascript_sync_rpc_arg_str(&request.args, 0, "crypto.createKeyObject operation")?;
16977 let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.createKeyObject key")?;
16978 let expected = match operation {
16979 "createPrivateKey" => Some("private"),
16980 "createPublicKey" => Some("public"),
16981 other => {
16982 return Err(SidecarError::InvalidState(format!(
16983 "Unsupported key creation operation: {other}"
16984 )));
16985 }
16986 };
16987 let key_input =
16988 javascript_crypto_parse_direct_key_input(key_json, expected, "crypto.createKeyObject key")?;
16989 Ok(Value::String(
16990 serde_json::to_string(&javascript_crypto_serialize_sandbox_key_object(
16991 &key_input.key,
16992 )?)
16993 .map_err(|error| {
16994 SidecarError::InvalidState(format!("serialize crypto key object: {error}"))
16995 })?,
16996 ))
16997}
16998
16999fn service_javascript_crypto_generate_key_pair_sync_rpc(
17000 request: &JavascriptSyncRpcRequest,
17001) -> Result<Value, SidecarError> {
17002 let key_type =
17003 javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeyPairSync type")?;
17004 let options = javascript_crypto_parse_serialized_options_arg(
17005 &request.args,
17006 1,
17007 "crypto.generateKeyPairSync options",
17008 )?
17009 .unwrap_or(Value::Object(Map::new()));
17010 let public_encoding = options.get("publicKeyEncoding").cloned();
17011 let private_encoding = options.get("privateKeyEncoding").cloned();
17012
17013 let private_key = match key_type {
17014 "rsa" => {
17015 let bits = options
17016 .get("modulusLength")
17017 .and_then(Value::as_u64)
17018 .unwrap_or(2048) as u32;
17019 let exponent = options
17020 .get("publicExponent")
17021 .map(|value| javascript_crypto_u32_from_bridge_value(value, "rsa publicExponent"))
17022 .transpose()?
17023 .unwrap_or(65_537);
17024 let exponent = BigNum::from_u32(exponent).map_err(javascript_crypto_openssl_error)?;
17025 let rsa =
17026 Rsa::generate_with_e(bits, &exponent).map_err(javascript_crypto_openssl_error)?;
17027 PKey::from_rsa(rsa).map_err(javascript_crypto_openssl_error)?
17028 }
17029 "ec" => {
17030 let named_curve = options
17031 .get("namedCurve")
17032 .and_then(Value::as_str)
17033 .ok_or_else(|| {
17034 SidecarError::InvalidState(String::from(
17035 "crypto.generateKeyPairSync ec requires namedCurve",
17036 ))
17037 })?;
17038 let group = EcGroup::from_curve_name(javascript_crypto_curve_nid(named_curve)?)
17039 .map_err(javascript_crypto_openssl_error)?;
17040 let key = EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?;
17041 PKey::from_ec_key(key).map_err(javascript_crypto_openssl_error)?
17042 }
17043 "ed25519" => PKey::generate_ed25519().map_err(javascript_crypto_openssl_error)?,
17044 "x25519" => PKey::generate_x25519().map_err(javascript_crypto_openssl_error)?,
17045 other => {
17046 return Err(SidecarError::InvalidState(format!(
17047 "unsupported crypto key pair type {other}"
17048 )));
17049 }
17050 };
17051 let public_key = PKey::public_key_from_pem(
17052 &private_key
17053 .public_key_to_pem()
17054 .map_err(javascript_crypto_openssl_error)?,
17055 )
17056 .map_err(javascript_crypto_openssl_error)?;
17057 let response = if public_encoding.is_some() || private_encoding.is_some() {
17058 json!({
17059 "publicKey": javascript_crypto_serialize_encoded_key_value_public(&public_key, public_encoding.as_ref())?,
17060 "privateKey": javascript_crypto_serialize_encoded_key_value_private(&private_key, private_encoding.as_ref())?,
17061 })
17062 } else {
17063 json!({
17064 "publicKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(public_key))?,
17065 "privateKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(private_key))?,
17066 })
17067 };
17068 Ok(Value::String(serde_json::to_string(&response).map_err(
17069 |error| SidecarError::InvalidState(format!("serialize generated key pair: {error}")),
17070 )?))
17071}
17072
17073fn service_javascript_crypto_generate_key_sync_rpc(
17074 request: &JavascriptSyncRpcRequest,
17075) -> Result<Value, SidecarError> {
17076 let key_type = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeySync type")?;
17077 let options = javascript_crypto_parse_serialized_options_arg(
17078 &request.args,
17079 1,
17080 "crypto.generateKeySync options",
17081 )?
17082 .unwrap_or(Value::Object(Map::new()));
17083 let bit_length = options
17084 .get("length")
17085 .and_then(Value::as_u64)
17086 .ok_or_else(|| {
17087 SidecarError::InvalidState(String::from(
17088 "crypto.generateKeySync options.length is required",
17089 ))
17090 })? as usize;
17091 let mut raw = vec![0_u8; bit_length.div_ceil(8)];
17092 rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
17093 let serialized = match key_type {
17094 "hmac" => javascript_crypto_serialize_sandbox_key_object(
17095 &JavascriptCryptoKeyMaterial::Secret(raw),
17096 )?,
17097 "aes" => javascript_crypto_serialize_sandbox_key_object(
17098 &JavascriptCryptoKeyMaterial::Secret(raw),
17099 )?,
17100 other => {
17101 return Err(SidecarError::InvalidState(format!(
17102 "unsupported crypto.generateKeySync type {other}"
17103 )));
17104 }
17105 };
17106 Ok(Value::String(serde_json::to_string(&serialized).map_err(
17107 |error| SidecarError::InvalidState(format!("serialize generated key: {error}")),
17108 )?))
17109}
17110
17111fn service_javascript_crypto_generate_prime_sync_rpc(
17112 request: &JavascriptSyncRpcRequest,
17113) -> Result<Value, SidecarError> {
17114 let bits =
17115 javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.generatePrimeSync size")? as i32;
17116 let options = javascript_crypto_parse_serialized_options_arg(
17117 &request.args,
17118 1,
17119 "crypto.generatePrimeSync options",
17120 )?
17121 .unwrap_or(Value::Object(Map::new()));
17122 let safe = options
17123 .get("safe")
17124 .and_then(Value::as_bool)
17125 .unwrap_or(false);
17126 let add = options
17127 .get("add")
17128 .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime add"))
17129 .transpose()?;
17130 let rem = options
17131 .get("rem")
17132 .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime rem"))
17133 .transpose()?;
17134 let mut prime = BigNum::new().map_err(javascript_crypto_openssl_error)?;
17135 prime
17136 .generate_prime(bits, safe, add.as_deref(), rem.as_deref())
17137 .map_err(javascript_crypto_openssl_error)?;
17138 let payload = if options
17139 .get("bigint")
17140 .and_then(Value::as_bool)
17141 .unwrap_or(false)
17142 {
17143 json!({
17144 "__type": "bigint",
17145 "value": prime.to_dec_str().map_err(javascript_crypto_openssl_error)?.to_string(),
17146 })
17147 } else {
17148 json!({
17149 "__type": "buffer",
17150 "value": base64::engine::general_purpose::STANDARD.encode(prime.to_vec()),
17151 })
17152 };
17153 Ok(Value::String(serde_json::to_string(&payload).map_err(
17154 |error| SidecarError::InvalidState(format!("serialize generated prime: {error}")),
17155 )?))
17156}
17157
17158fn service_javascript_crypto_diffie_hellman_sync_rpc(
17159 request: &JavascriptSyncRpcRequest,
17160) -> Result<Value, SidecarError> {
17161 let options = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellman options")?;
17162 let parsed: Value = serde_json::from_str(options).map_err(|error| {
17163 SidecarError::InvalidState(format!(
17164 "crypto.diffieHellman options must be valid JSON: {error}"
17165 ))
17166 })?;
17167 let private_key = javascript_crypto_parse_key_material_value(
17168 parsed.get("privateKey").ok_or_else(|| {
17169 SidecarError::InvalidState(String::from("crypto.diffieHellman missing privateKey"))
17170 })?,
17171 Some("private"),
17172 "crypto.diffieHellman privateKey",
17173 )?;
17174 let public_key = javascript_crypto_parse_key_material_value(
17175 parsed.get("publicKey").ok_or_else(|| {
17176 SidecarError::InvalidState(String::from("crypto.diffieHellman missing publicKey"))
17177 })?,
17178 Some("public"),
17179 "crypto.diffieHellman publicKey",
17180 )?;
17181 let private_key =
17182 javascript_crypto_expect_private_key(private_key, "crypto.diffieHellman privateKey")?;
17183 let public_key =
17184 javascript_crypto_expect_public_key(public_key, "crypto.diffieHellman publicKey")?;
17185 let mut deriver = Deriver::new(&private_key).map_err(javascript_crypto_openssl_error)?;
17186 deriver
17187 .set_peer(&public_key)
17188 .map_err(javascript_crypto_openssl_error)?;
17189 let secret = deriver
17190 .derive_to_vec()
17191 .map_err(javascript_crypto_openssl_error)?;
17192 Ok(Value::String(
17193 serde_json::to_string(&json!({
17194 "__type": "buffer",
17195 "value": base64::engine::general_purpose::STANDARD.encode(secret),
17196 }))
17197 .map_err(|error| {
17198 SidecarError::InvalidState(format!("serialize derived secret: {error}"))
17199 })?,
17200 ))
17201}
17202
17203fn service_javascript_crypto_diffie_hellman_group_sync_rpc(
17204 request: &JavascriptSyncRpcRequest,
17205) -> Result<Value, SidecarError> {
17206 let name = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellmanGroup name")?;
17207 let params = javascript_crypto_named_dh_group(name)?;
17208 let response = json!({
17209 "prime": {
17210 "__type": "buffer",
17211 "value": base64::engine::general_purpose::STANDARD.encode(params.prime_p().to_vec()),
17212 },
17213 "generator": {
17214 "__type": "buffer",
17215 "value": base64::engine::general_purpose::STANDARD.encode(params.generator().to_vec()),
17216 },
17217 });
17218 Ok(Value::String(serde_json::to_string(&response).map_err(
17219 |error| {
17220 SidecarError::InvalidState(format!("serialize diffieHellmanGroup response: {error}"))
17221 },
17222 )?))
17223}
17224
17225fn service_javascript_crypto_diffie_hellman_session_create_sync_rpc(
17226 process: &mut ActiveProcess,
17227 request: &JavascriptSyncRpcRequest,
17228) -> Result<Value, SidecarError> {
17229 ensure_per_process_state_handle_capacity(
17230 process.diffie_hellman_sessions.len(),
17231 "diffie-hellman session",
17232 )?;
17233 let raw = javascript_sync_rpc_arg_str(
17234 &request.args,
17235 0,
17236 "crypto.diffieHellmanSessionCreate request",
17237 )?;
17238 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
17239 SidecarError::InvalidState(format!(
17240 "crypto.diffieHellmanSessionCreate request must be valid JSON: {error}"
17241 ))
17242 })?;
17243 let session = match parsed.get("type").and_then(Value::as_str) {
17244 Some("group") => {
17245 let name = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
17246 SidecarError::InvalidState(String::from(
17247 "crypto.diffieHellmanSessionCreate group requires name",
17248 ))
17249 })?;
17250 ActiveDiffieHellmanSession::Dh(ActiveDhSession {
17251 params: javascript_crypto_named_dh_group(name)?,
17252 key_pair: None,
17253 })
17254 }
17255 Some("dh") => {
17256 let args = parsed
17257 .get("args")
17258 .and_then(Value::as_array)
17259 .ok_or_else(|| {
17260 SidecarError::InvalidState(String::from(
17261 "crypto.diffieHellmanSessionCreate dh requires args",
17262 ))
17263 })?;
17264 let params = javascript_crypto_build_dh_params(args)?;
17265 ActiveDiffieHellmanSession::Dh(ActiveDhSession {
17266 params,
17267 key_pair: None,
17268 })
17269 }
17270 Some("ecdh") => {
17271 let curve = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
17272 SidecarError::InvalidState(String::from(
17273 "crypto.diffieHellmanSessionCreate ecdh requires name",
17274 ))
17275 })?;
17276 ActiveDiffieHellmanSession::Ecdh(ActiveEcdhSession {
17277 curve: curve.to_string(),
17278 key_pair: None,
17279 })
17280 }
17281 other => {
17282 return Err(SidecarError::InvalidState(format!(
17283 "Unsupported Diffie-Hellman session type: {}",
17284 other.unwrap_or("<missing>")
17285 )));
17286 }
17287 };
17288 process.next_diffie_hellman_session_id += 1;
17289 let session_id = process.next_diffie_hellman_session_id;
17290 process.diffie_hellman_sessions.insert(session_id, session);
17291 Ok(json!(session_id))
17292}
17293
17294fn service_javascript_crypto_diffie_hellman_session_call_sync_rpc(
17295 process: &mut ActiveProcess,
17296 request: &JavascriptSyncRpcRequest,
17297) -> Result<Value, SidecarError> {
17298 let session_id = javascript_sync_rpc_arg_u64(
17299 &request.args,
17300 0,
17301 "crypto.diffieHellmanSessionCall session id",
17302 )?;
17303 let raw =
17304 javascript_sync_rpc_arg_str(&request.args, 1, "crypto.diffieHellmanSessionCall request")?;
17305 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
17306 SidecarError::InvalidState(format!(
17307 "crypto.diffieHellmanSessionCall request must be valid JSON: {error}"
17308 ))
17309 })?;
17310 let method = parsed
17311 .get("method")
17312 .and_then(Value::as_str)
17313 .ok_or_else(|| {
17314 SidecarError::InvalidState(String::from(
17315 "crypto.diffieHellmanSessionCall request missing method",
17316 ))
17317 })?;
17318 let args = parsed
17319 .get("args")
17320 .and_then(Value::as_array)
17321 .cloned()
17322 .unwrap_or_default();
17323 let session = process
17324 .diffie_hellman_sessions
17325 .get_mut(&session_id)
17326 .ok_or_else(|| {
17327 SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
17328 })?;
17329 let (result, has_result) = match session {
17330 ActiveDiffieHellmanSession::Dh(session) => {
17331 javascript_crypto_call_dh_session(session, method, &args)?
17332 }
17333 ActiveDiffieHellmanSession::Ecdh(session) => {
17334 javascript_crypto_call_ecdh_session(session, method, &args)?
17335 }
17336 };
17337 Ok(Value::String(
17338 serde_json::to_string(&json!({
17339 "result": result,
17340 "hasResult": has_result,
17341 }))
17342 .map_err(|error| {
17343 SidecarError::InvalidState(format!("serialize diffie session result: {error}"))
17344 })?,
17345 ))
17346}
17347
17348fn service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(
17349 process: &mut ActiveProcess,
17350 request: &JavascriptSyncRpcRequest,
17351) -> Result<Value, SidecarError> {
17352 let session_id = javascript_sync_rpc_arg_u64(
17353 &request.args,
17354 0,
17355 "crypto.diffieHellmanSessionDestroy session id",
17356 )?;
17357 process
17358 .diffie_hellman_sessions
17359 .remove(&session_id)
17360 .ok_or_else(|| {
17361 SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
17362 })?;
17363 Ok(Value::Null)
17364}
17365
17366fn service_javascript_crypto_subtle_sync_rpc(
17367 request: &JavascriptSyncRpcRequest,
17368) -> Result<Value, SidecarError> {
17369 let raw = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.subtle request")?;
17370 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
17371 SidecarError::InvalidState(format!("crypto.subtle request must be valid JSON: {error}"))
17372 })?;
17373 let op = parsed.get("op").and_then(Value::as_str).ok_or_else(|| {
17374 SidecarError::InvalidState(String::from("crypto.subtle request missing op"))
17375 })?;
17376 match op {
17377 "digest" => {
17378 let algorithm = parsed
17379 .get("algorithm")
17380 .and_then(Value::as_str)
17381 .ok_or_else(|| {
17382 SidecarError::InvalidState(String::from(
17383 "crypto.subtle.digest missing algorithm",
17384 ))
17385 })?;
17386 let data = parsed.get("data").and_then(Value::as_str).ok_or_else(|| {
17387 SidecarError::InvalidState(String::from("crypto.subtle.digest missing data"))
17388 })?;
17389 let bytes = base64::engine::general_purpose::STANDARD
17390 .decode(data)
17391 .map_err(|error| {
17392 SidecarError::InvalidState(format!("crypto.subtle.digest data base64: {error}"))
17393 })?;
17394 let digest = JavascriptCryptoDigestAlgorithm::parse(algorithm)?.digest(&bytes);
17395 Ok(Value::String(
17396 serde_json::to_string(&json!({
17397 "data": base64::engine::general_purpose::STANDARD.encode(digest),
17398 }))
17399 .map_err(|error| {
17400 SidecarError::InvalidState(format!("serialize crypto.subtle digest: {error}"))
17401 })?,
17402 ))
17403 }
17404 "generateKey" => {
17405 let algorithm = parsed.get("algorithm").ok_or_else(|| {
17406 SidecarError::InvalidState(String::from(
17407 "crypto.subtle.generateKey missing algorithm",
17408 ))
17409 })?;
17410 let name =
17411 javascript_crypto_subtle_algorithm_name(algorithm, "crypto.subtle.generateKey")?;
17412 if !matches!(name, "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW") {
17413 return Err(SidecarError::InvalidState(format!(
17414 "Unsupported key algorithm: {name}"
17415 )));
17416 }
17417 let length_bits = algorithm
17418 .get("length")
17419 .and_then(Value::as_u64)
17420 .ok_or_else(|| {
17421 SidecarError::InvalidState(String::from(
17422 "crypto.subtle.generateKey AES algorithm requires length",
17423 ))
17424 })?;
17425 if length_bits % 8 != 0 {
17426 return Err(SidecarError::InvalidState(String::from(
17427 "crypto.subtle.generateKey length must be byte-aligned",
17428 )));
17429 }
17430 let length_bytes = usize::try_from(length_bits / 8).map_err(|_| {
17431 SidecarError::InvalidState(String::from(
17432 "crypto.subtle.generateKey length is too large",
17433 ))
17434 })?;
17435 let mut raw = vec![0_u8; length_bytes];
17436 rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
17437 let key = javascript_crypto_serialize_subtle_secret_key(
17438 &raw,
17439 javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
17440 parsed
17441 .get("extractable")
17442 .and_then(Value::as_bool)
17443 .unwrap_or(false),
17444 parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
17445 )?;
17446 Ok(Value::String(
17447 serde_json::to_string(&json!({ "key": key })).map_err(|error| {
17448 SidecarError::InvalidState(format!(
17449 "serialize crypto.subtle generated key: {error}"
17450 ))
17451 })?,
17452 ))
17453 }
17454 "importKey" => {
17455 let format = parsed
17456 .get("format")
17457 .and_then(Value::as_str)
17458 .ok_or_else(|| {
17459 SidecarError::InvalidState(String::from(
17460 "crypto.subtle.importKey missing format",
17461 ))
17462 })?;
17463 if format != "raw" {
17464 return Err(SidecarError::InvalidState(format!(
17465 "Unsupported import format: {format}"
17466 )));
17467 }
17468 let key_data = parsed
17469 .get("keyData")
17470 .and_then(Value::as_str)
17471 .ok_or_else(|| {
17472 SidecarError::InvalidState(String::from(
17473 "crypto.subtle.importKey missing keyData",
17474 ))
17475 })?;
17476 let raw = base64::engine::general_purpose::STANDARD
17477 .decode(key_data)
17478 .map_err(|error| {
17479 SidecarError::InvalidState(format!(
17480 "crypto.subtle.importKey keyData base64: {error}"
17481 ))
17482 })?;
17483 let algorithm = parsed.get("algorithm").ok_or_else(|| {
17484 SidecarError::InvalidState(String::from(
17485 "crypto.subtle.importKey missing algorithm",
17486 ))
17487 })?;
17488 let key = javascript_crypto_serialize_subtle_secret_key(
17489 &raw,
17490 javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
17491 parsed
17492 .get("extractable")
17493 .and_then(Value::as_bool)
17494 .unwrap_or(false),
17495 parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
17496 )?;
17497 Ok(Value::String(
17498 serde_json::to_string(&json!({ "key": key })).map_err(|error| {
17499 SidecarError::InvalidState(format!(
17500 "serialize crypto.subtle imported key: {error}"
17501 ))
17502 })?,
17503 ))
17504 }
17505 "exportKey" => {
17506 let format = parsed
17507 .get("format")
17508 .and_then(Value::as_str)
17509 .ok_or_else(|| {
17510 SidecarError::InvalidState(String::from(
17511 "crypto.subtle.exportKey missing format",
17512 ))
17513 })?;
17514 if format != "raw" {
17515 return Err(SidecarError::InvalidState(format!(
17516 "Unsupported export format: {format}"
17517 )));
17518 }
17519 let raw = javascript_crypto_subtle_key_raw(
17520 parsed.get("key").ok_or_else(|| {
17521 SidecarError::InvalidState(String::from("crypto.subtle.exportKey missing key"))
17522 })?,
17523 "crypto.subtle.exportKey key",
17524 )?;
17525 Ok(Value::String(
17526 serde_json::to_string(&json!({
17527 "data": base64::engine::general_purpose::STANDARD.encode(raw),
17528 }))
17529 .map_err(|error| {
17530 SidecarError::InvalidState(format!("serialize crypto.subtle export: {error}"))
17531 })?,
17532 ))
17533 }
17534 "encrypt" | "decrypt" => service_javascript_crypto_subtle_aes_crypt_sync_rpc(op, &parsed),
17535 _ => Err(SidecarError::InvalidState(format!(
17536 "Unsupported subtle operation: {op}"
17537 ))),
17538 }
17539}
17540
17541fn javascript_crypto_subtle_algorithm_name<'a>(
17542 algorithm: &'a Value,
17543 label: &str,
17544) -> Result<&'a str, SidecarError> {
17545 if let Some(name) = algorithm.as_str() {
17546 return Ok(name);
17547 }
17548 algorithm
17549 .get("name")
17550 .and_then(Value::as_str)
17551 .ok_or_else(|| SidecarError::InvalidState(format!("{label} algorithm missing name")))
17552}
17553
17554fn javascript_crypto_normalize_subtle_secret_algorithm(
17555 algorithm: Value,
17556 raw: &[u8],
17557) -> Result<Value, SidecarError> {
17558 let mut object = match algorithm {
17559 Value::String(name) => {
17560 let mut object = Map::new();
17561 object.insert(String::from("name"), Value::String(name));
17562 object
17563 }
17564 Value::Object(object) => object,
17565 _ => {
17566 return Err(SidecarError::InvalidState(String::from(
17567 "crypto.subtle secret algorithm must be a string or object",
17568 )));
17569 }
17570 };
17571 let name = object
17572 .get("name")
17573 .and_then(Value::as_str)
17574 .ok_or_else(|| {
17575 SidecarError::InvalidState(String::from("crypto.subtle secret algorithm missing name"))
17576 })?
17577 .to_string();
17578 if matches!(name.as_str(), "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW")
17579 && !object.contains_key("length")
17580 {
17581 object.insert(String::from("length"), json!(raw.len() * 8));
17582 }
17583 Ok(Value::Object(object))
17584}
17585
17586fn javascript_crypto_serialize_subtle_secret_key(
17587 raw: &[u8],
17588 algorithm: Value,
17589 extractable: bool,
17590 usages: Value,
17591) -> Result<Value, SidecarError> {
17592 let raw_base64 = base64::engine::general_purpose::STANDARD.encode(raw);
17593 let source_key_object_data = javascript_crypto_serialize_sandbox_key_object(
17594 &JavascriptCryptoKeyMaterial::Secret(raw.to_vec()),
17595 )?;
17596 Ok(json!({
17597 "type": "secret",
17598 "algorithm": algorithm,
17599 "extractable": extractable,
17600 "usages": usages,
17601 "_raw": raw_base64,
17602 "_sourceKeyObjectData": source_key_object_data,
17603 }))
17604}
17605
17606fn javascript_crypto_subtle_key_raw(key: &Value, label: &str) -> Result<Vec<u8>, SidecarError> {
17607 let raw = key.get("_raw").and_then(Value::as_str).ok_or_else(|| {
17608 SidecarError::InvalidState(format!("{label} must be a raw secret CryptoKey"))
17609 })?;
17610 base64::engine::general_purpose::STANDARD
17611 .decode(raw)
17612 .map_err(|error| SidecarError::InvalidState(format!("{label} raw base64: {error}")))
17613}
17614
17615fn service_javascript_crypto_subtle_aes_crypt_sync_rpc(
17616 op: &str,
17617 parsed: &Value,
17618) -> Result<Value, SidecarError> {
17619 let algorithm = parsed.get("algorithm").ok_or_else(|| {
17620 SidecarError::InvalidState(format!("crypto.subtle.{op} missing algorithm"))
17621 })?;
17622 let name = javascript_crypto_subtle_algorithm_name(algorithm, &format!("crypto.subtle.{op}"))?;
17623 if name != "AES-GCM" {
17624 return Err(SidecarError::InvalidState(format!(
17625 "Unsupported subtle AES operation algorithm: {name}"
17626 )));
17627 }
17628 let key = javascript_crypto_subtle_key_raw(
17629 parsed
17630 .get("key")
17631 .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing key")))?,
17632 &format!("crypto.subtle.{op} key"),
17633 )?;
17634 let iv = algorithm.get("iv").and_then(Value::as_str).ok_or_else(|| {
17635 SidecarError::InvalidState(format!("crypto.subtle.{op} AES-GCM missing iv"))
17636 })?;
17637 let iv = base64::engine::general_purpose::STANDARD
17638 .decode(iv)
17639 .map_err(|error| {
17640 SidecarError::InvalidState(format!("crypto.subtle.{op} iv base64: {error}"))
17641 })?;
17642 let data = parsed
17643 .get("data")
17644 .and_then(Value::as_str)
17645 .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing data")))?;
17646 let mut data = base64::engine::general_purpose::STANDARD
17647 .decode(data)
17648 .map_err(|error| {
17649 SidecarError::InvalidState(format!("crypto.subtle.{op} data base64: {error}"))
17650 })?;
17651 let tag_len = javascript_crypto_subtle_aes_gcm_tag_len(algorithm)?;
17652 let mut options = Map::new();
17653 options.insert(String::from("authTagLength"), json!(tag_len));
17654 if let Some(additional_data) = algorithm.get("additionalData").and_then(Value::as_str) {
17655 options.insert(
17656 String::from("aad"),
17657 Value::String(additional_data.to_string()),
17658 );
17659 }
17660 let decrypt = op == "decrypt";
17661 if decrypt {
17662 if data.len() < tag_len {
17663 return Err(SidecarError::InvalidState(String::from(
17664 "crypto.subtle.decrypt AES-GCM data shorter than auth tag",
17665 )));
17666 }
17667 let auth_tag = data.split_off(data.len() - tag_len);
17668 options.insert(
17669 String::from("authTag"),
17670 Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
17671 );
17672 }
17673 let cipher_name = format!("aes-{}-gcm", key.len() * 8);
17674 let mut session = javascript_crypto_build_cipher_session(
17675 &cipher_name,
17676 &key,
17677 Some(&iv),
17678 decrypt,
17679 Some(&Value::Object(options)),
17680 )?;
17681 let mut output = javascript_crypto_cipher_update(&mut session, &data)?;
17682 let outcome = session.finalize().map_err(javascript_crypto_cipher_error)?;
17683 output.extend(outcome.data);
17684 if !decrypt {
17685 if let Some(auth_tag) = outcome.auth_tag {
17686 output.extend(auth_tag);
17687 }
17688 }
17689 Ok(Value::String(
17690 serde_json::to_string(&json!({
17691 "data": base64::engine::general_purpose::STANDARD.encode(output),
17692 }))
17693 .map_err(|error| {
17694 SidecarError::InvalidState(format!("serialize crypto.subtle {op}: {error}"))
17695 })?,
17696 ))
17697}
17698
17699fn javascript_crypto_subtle_aes_gcm_tag_len(algorithm: &Value) -> Result<usize, SidecarError> {
17700 let tag_bits = algorithm
17701 .get("tagLength")
17702 .and_then(Value::as_u64)
17703 .unwrap_or(128);
17704 if !tag_bits.is_multiple_of(8) {
17705 return Err(SidecarError::InvalidState(String::from(
17706 "crypto.subtle AES-GCM tagLength must be byte-aligned",
17707 )));
17708 }
17709 usize::try_from(tag_bits / 8).map_err(|_| {
17710 SidecarError::InvalidState(String::from("crypto.subtle AES-GCM tagLength too large"))
17711 })
17712}
17713
17714fn service_javascript_crypto_cipheriv_inner(
17715 request: &JavascriptSyncRpcRequest,
17716 decrypt: bool,
17717) -> Result<Value, SidecarError> {
17718 let label = if decrypt {
17719 "crypto.decipheriv"
17720 } else {
17721 "crypto.cipheriv"
17722 };
17723 let algorithm = javascript_sync_rpc_arg_str(&request.args, 0, &format!("{label} algorithm"))?;
17724 let key = javascript_sync_rpc_base64_arg(&request.args, 1, &format!("{label} key"))?;
17725 let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 2, &format!("{label} iv"))?;
17726 let data = javascript_sync_rpc_base64_arg(&request.args, 3, &format!("{label} data"))?;
17727 let options =
17728 javascript_sync_rpc_json_arg_optional(&request.args, 4, &format!("{label} options"))?;
17729 let mut session = javascript_crypto_build_cipher_session(
17730 algorithm,
17731 &key,
17732 iv.as_deref(),
17733 decrypt,
17734 options.as_ref(),
17735 )?;
17736 let payload = javascript_crypto_cipher_update(&mut session, &data)?;
17737 let outcome = session.finalize().map_err(javascript_crypto_cipher_error)?;
17738 if decrypt {
17739 let mut output = payload;
17740 output.extend(outcome.data);
17741 return Ok(Value::String(
17742 base64::engine::general_purpose::STANDARD.encode(output),
17743 ));
17744 }
17745
17746 let mut response = Map::new();
17747 let mut encrypted = payload;
17748 encrypted.extend(outcome.data);
17749 response.insert(
17750 String::from("data"),
17751 Value::String(base64::engine::general_purpose::STANDARD.encode(encrypted)),
17752 );
17753 if let Some(auth_tag) = outcome.auth_tag {
17754 response.insert(
17755 String::from("authTag"),
17756 Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
17757 );
17758 }
17759 Ok(Value::String(serde_json::to_string(&response).map_err(
17760 |error| SidecarError::InvalidState(format!("serialize {label} response: {error}")),
17761 )?))
17762}
17763
17764fn javascript_sync_rpc_base64_arg_optional(
17765 args: &[Value],
17766 index: usize,
17767 label: &str,
17768) -> Result<Option<Vec<u8>>, SidecarError> {
17769 if args.get(index).is_none() || args[index].is_null() {
17770 return Ok(None);
17771 }
17772 javascript_sync_rpc_base64_arg(args, index, label).map(Some)
17773}
17774
17775fn javascript_sync_rpc_json_arg_optional(
17776 args: &[Value],
17777 index: usize,
17778 label: &str,
17779) -> Result<Option<Value>, SidecarError> {
17780 if args.get(index).is_none() || args[index].is_null() {
17781 return Ok(None);
17782 }
17783 let raw = javascript_sync_rpc_arg_str(args, index, label)?;
17784 serde_json::from_str(raw)
17785 .map(Some)
17786 .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
17787}
17788
17789fn javascript_crypto_parse_direct_key_input(
17790 raw: &str,
17791 expected: Option<&str>,
17792 label: &str,
17793) -> Result<JavascriptDirectKeyInput, SidecarError> {
17794 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
17795 SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
17796 })?;
17797 let padding = match parsed.as_object().and_then(|value| value.get("padding")) {
17798 Some(value) => javascript_crypto_padding_from_value(value)?,
17799 None => None,
17800 };
17801 Ok(JavascriptDirectKeyInput {
17802 key: javascript_crypto_parse_key_material_value(&parsed, expected, label)?,
17803 padding,
17804 })
17805}
17806
17807fn javascript_crypto_parse_key_material_value(
17808 value: &Value,
17809 expected: Option<&str>,
17810 label: &str,
17811) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
17812 if let Some(object) = value.as_object() {
17813 if object.get("__type").and_then(Value::as_str) == Some("keyObject") {
17814 let serialized = object.get("value").ok_or_else(|| {
17815 SidecarError::InvalidState(format!("{label} keyObject is missing a value"))
17816 })?;
17817 return javascript_crypto_parse_serialized_key_object(serialized, expected, label);
17818 }
17819 if object.contains_key("type") && (object.contains_key("pem") || object.contains_key("raw"))
17820 {
17821 return javascript_crypto_parse_serialized_key_object(value, expected, label);
17822 }
17823 if let Some(source) = object.get("key") {
17824 return javascript_crypto_parse_key_source(
17825 source,
17826 object.get("format").and_then(Value::as_str),
17827 object.get("type").and_then(Value::as_str),
17828 expected,
17829 label,
17830 );
17831 }
17832 }
17833 javascript_crypto_parse_key_source(value, None, None, expected, label)
17834}
17835
17836fn javascript_crypto_parse_key_source(
17837 source: &Value,
17838 format: Option<&str>,
17839 kind: Option<&str>,
17840 expected: Option<&str>,
17841 label: &str,
17842) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
17843 match source {
17844 Value::String(pem) => javascript_crypto_parse_key_from_pem(pem.as_bytes(), expected, label),
17845 Value::Object(object) if object.get("__type").and_then(Value::as_str) == Some("buffer") => {
17846 let data = javascript_crypto_decode_bridge_buffer(source, label)?;
17847 javascript_crypto_parse_key_from_bytes(&data, format, kind, expected, label)
17848 }
17849 Value::Object(_) => {
17850 if format == Some("jwk") {
17851 return Err(SidecarError::InvalidState(format!(
17852 "{label} jwk inputs are not supported yet"
17853 )));
17854 }
17855 Err(SidecarError::InvalidState(format!(
17856 "{label} has an unsupported key shape"
17857 )))
17858 }
17859 _ => Err(SidecarError::InvalidState(format!(
17860 "{label} has an unsupported key value"
17861 ))),
17862 }
17863}
17864
17865fn javascript_crypto_parse_key_from_pem(
17866 pem: &[u8],
17867 expected: Option<&str>,
17868 label: &str,
17869) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
17870 match expected {
17871 Some("private") => PKey::private_key_from_pem(pem)
17872 .map(JavascriptCryptoKeyMaterial::Private)
17873 .map_err(|error| {
17874 SidecarError::InvalidState(format!("{label} private key is invalid: {error}"))
17875 }),
17876 Some("public") => PKey::public_key_from_pem(pem)
17877 .map(JavascriptCryptoKeyMaterial::Public)
17878 .map_err(|error| {
17879 SidecarError::InvalidState(format!("{label} public key is invalid: {error}"))
17880 }),
17881 _ => PKey::private_key_from_pem(pem)
17882 .map(JavascriptCryptoKeyMaterial::Private)
17883 .or_else(|_| PKey::public_key_from_pem(pem).map(JavascriptCryptoKeyMaterial::Public))
17884 .map_err(|error| {
17885 SidecarError::InvalidState(format!("{label} PEM key is invalid: {error}"))
17886 }),
17887 }
17888}
17889
17890fn javascript_crypto_parse_key_from_bytes(
17891 der: &[u8],
17892 format: Option<&str>,
17893 kind: Option<&str>,
17894 expected: Option<&str>,
17895 label: &str,
17896) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
17897 match (format.unwrap_or("der"), kind.or(expected)) {
17898 ("der", Some("pkcs8")) | ("der", Some("private")) => PKey::private_key_from_der(der)
17899 .map(JavascriptCryptoKeyMaterial::Private)
17900 .map_err(|error| {
17901 SidecarError::InvalidState(format!("{label} private key DER is invalid: {error}"))
17902 }),
17903 ("der", Some("spki")) | ("der", Some("public")) => PKey::public_key_from_der(der)
17904 .map(JavascriptCryptoKeyMaterial::Public)
17905 .map_err(|error| {
17906 SidecarError::InvalidState(format!("{label} public key DER is invalid: {error}"))
17907 }),
17908 _ => Err(SidecarError::InvalidState(format!(
17909 "{label} unsupported key bytes format"
17910 ))),
17911 }
17912}
17913
17914fn javascript_crypto_parse_serialized_key_object(
17915 value: &Value,
17916 expected: Option<&str>,
17917 label: &str,
17918) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
17919 let serialized: JavascriptSerializedSandboxKeyObject = serde_json::from_value(value.clone())
17920 .map_err(|error| {
17921 SidecarError::InvalidState(format!("{label} keyObject is invalid: {error}"))
17922 })?;
17923 match serialized.kind.as_str() {
17924 "secret" => {
17925 if expected == Some("public") || expected == Some("private") {
17926 return Err(SidecarError::InvalidState(format!(
17927 "{label} expected an asymmetric key"
17928 )));
17929 }
17930 Ok(JavascriptCryptoKeyMaterial::Secret(
17931 base64::engine::general_purpose::STANDARD
17932 .decode(serialized.raw.unwrap_or_default())
17933 .map_err(|error| {
17934 SidecarError::InvalidState(format!(
17935 "{label} secret key contains invalid base64: {error}"
17936 ))
17937 })?,
17938 ))
17939 }
17940 "private" => {
17941 let pem = serialized.pem.ok_or_else(|| {
17942 SidecarError::InvalidState(format!("{label} private keyObject is missing pem"))
17943 })?;
17944 javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("private"), label)
17945 }
17946 "public" => {
17947 let pem = serialized.pem.ok_or_else(|| {
17948 SidecarError::InvalidState(format!("{label} public keyObject is missing pem"))
17949 })?;
17950 javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("public"), label)
17951 }
17952 other => Err(SidecarError::InvalidState(format!(
17953 "{label} has unsupported keyObject type {other}"
17954 ))),
17955 }
17956}
17957
17958fn javascript_crypto_expect_private_key(
17959 key: JavascriptCryptoKeyMaterial,
17960 label: &str,
17961) -> Result<PKey<Private>, SidecarError> {
17962 match key {
17963 JavascriptCryptoKeyMaterial::Private(key) => Ok(key),
17964 _ => Err(SidecarError::InvalidState(format!(
17965 "{label} requires a private key"
17966 ))),
17967 }
17968}
17969
17970fn javascript_crypto_expect_public_key(
17971 key: JavascriptCryptoKeyMaterial,
17972 label: &str,
17973) -> Result<PKey<Public>, SidecarError> {
17974 match key {
17975 JavascriptCryptoKeyMaterial::Public(key) => Ok(key),
17976 JavascriptCryptoKeyMaterial::Private(key) => {
17977 let pem = key
17978 .public_key_to_pem()
17979 .map_err(javascript_crypto_openssl_error)?;
17980 PKey::public_key_from_pem(&pem).map_err(javascript_crypto_openssl_error)
17981 }
17982 _ => Err(SidecarError::InvalidState(format!(
17983 "{label} requires a public key"
17984 ))),
17985 }
17986}
17987
17988fn javascript_crypto_new_signer<'a>(
17989 algorithm: Option<&'a str>,
17990 key: &'a PKey<Private>,
17991) -> Result<Signer<'a>, SidecarError> {
17992 if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
17993 return Signer::new_without_digest(key).map_err(javascript_crypto_openssl_error);
17994 }
17995 Signer::new(
17996 javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
17997 SidecarError::InvalidState(String::from("crypto.sign requires a digest algorithm"))
17998 })?)?,
17999 key,
18000 )
18001 .map_err(javascript_crypto_openssl_error)
18002}
18003
18004fn javascript_crypto_new_verifier<'a>(
18005 algorithm: Option<&'a str>,
18006 key: &'a PKey<Public>,
18007) -> Result<Verifier<'a>, SidecarError> {
18008 if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
18009 return Verifier::new_without_digest(key).map_err(javascript_crypto_openssl_error);
18010 }
18011 Verifier::new(
18012 javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
18013 SidecarError::InvalidState(String::from("crypto.verify requires a digest algorithm"))
18014 })?)?,
18015 key,
18016 )
18017 .map_err(javascript_crypto_openssl_error)
18018}
18019
18020fn javascript_crypto_message_digest_from_name(name: &str) -> Result<MessageDigest, SidecarError> {
18021 match name.trim().to_ascii_lowercase().replace('-', "").as_str() {
18022 "md5" => Ok(MessageDigest::md5()),
18023 "sha1" => Ok(MessageDigest::sha1()),
18024 "sha256" => Ok(MessageDigest::sha256()),
18025 "sha384" => Ok(MessageDigest::sha384()),
18026 "sha512" => Ok(MessageDigest::sha512()),
18027 other => Err(SidecarError::InvalidState(format!(
18028 "unsupported crypto digest algorithm {other}"
18029 ))),
18030 }
18031}
18032
18033fn javascript_crypto_padding_from_value(value: &Value) -> Result<Option<Padding>, SidecarError> {
18034 let Some(number) = value.as_i64() else {
18035 return Ok(None);
18036 };
18037 let padding = match number {
18038 1 => Padding::PKCS1,
18039 3 => Padding::NONE,
18040 4 => Padding::PKCS1_OAEP,
18041 6 => Padding::PKCS1_PSS,
18042 other => {
18043 return Err(SidecarError::InvalidState(format!(
18044 "unsupported RSA padding constant {other}"
18045 )));
18046 }
18047 };
18048 Ok(Some(padding))
18049}
18050
18051fn javascript_crypto_decode_bridge_buffer(
18052 value: &Value,
18053 label: &str,
18054) -> Result<Vec<u8>, SidecarError> {
18055 decode_bridge_buffer_value(value)
18056 .map_err(|error| SidecarError::InvalidState(format!("{label} {error}")))
18057}
18058
18059fn javascript_crypto_serialize_sandbox_key_object(
18060 key: &JavascriptCryptoKeyMaterial,
18061) -> Result<Value, SidecarError> {
18062 let serialized = match key {
18063 JavascriptCryptoKeyMaterial::Private(key) => JavascriptSerializedSandboxKeyObject {
18064 kind: String::from("private"),
18065 pem: Some(
18066 String::from_utf8(
18067 key.private_key_to_pem_pkcs8()
18068 .map_err(javascript_crypto_openssl_error)?,
18069 )
18070 .map_err(|error| {
18071 SidecarError::InvalidState(format!("private key PEM is not utf8: {error}"))
18072 })?,
18073 ),
18074 raw: None,
18075 asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
18076 asymmetric_key_details: None,
18077 jwk: None,
18078 },
18079 JavascriptCryptoKeyMaterial::Public(key) => JavascriptSerializedSandboxKeyObject {
18080 kind: String::from("public"),
18081 pem: Some(
18082 String::from_utf8(
18083 key.public_key_to_pem()
18084 .map_err(javascript_crypto_openssl_error)?,
18085 )
18086 .map_err(|error| {
18087 SidecarError::InvalidState(format!("public key PEM is not utf8: {error}"))
18088 })?,
18089 ),
18090 raw: None,
18091 asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
18092 asymmetric_key_details: None,
18093 jwk: None,
18094 },
18095 JavascriptCryptoKeyMaterial::Secret(raw) => JavascriptSerializedSandboxKeyObject {
18096 kind: String::from("secret"),
18097 pem: None,
18098 raw: Some(base64::engine::general_purpose::STANDARD.encode(raw)),
18099 asymmetric_key_type: None,
18100 asymmetric_key_details: None,
18101 jwk: None,
18102 },
18103 };
18104 serde_json::to_value(serialized)
18105 .map_err(|error| SidecarError::InvalidState(format!("serialize key object: {error}")))
18106}
18107
18108fn javascript_crypto_pkey_type_name(id: PKeyId) -> Option<String> {
18109 match id {
18110 PKeyId::RSA => Some(String::from("rsa")),
18111 PKeyId::EC => Some(String::from("ec")),
18112 PKeyId::ED25519 => Some(String::from("ed25519")),
18113 PKeyId::ED448 => Some(String::from("ed448")),
18114 PKeyId::X25519 => Some(String::from("x25519")),
18115 PKeyId::X448 => Some(String::from("x448")),
18116 PKeyId::DH => Some(String::from("dh")),
18117 _ => None,
18118 }
18119}
18120
18121fn javascript_crypto_rsa_output_size(
18122 key: &JavascriptCryptoKeyMaterial,
18123) -> Result<usize, SidecarError> {
18124 match key {
18125 JavascriptCryptoKeyMaterial::Private(key) => key
18126 .rsa()
18127 .map(|rsa| rsa.size() as usize)
18128 .map_err(javascript_crypto_openssl_error),
18129 JavascriptCryptoKeyMaterial::Public(key) => key
18130 .rsa()
18131 .map(|rsa| rsa.size() as usize)
18132 .map_err(javascript_crypto_openssl_error),
18133 JavascriptCryptoKeyMaterial::Secret(_) => Err(SidecarError::InvalidState(String::from(
18134 "RSA operations require an asymmetric key",
18135 ))),
18136 }
18137}
18138
18139fn javascript_crypto_parse_serialized_options_arg(
18140 args: &[Value],
18141 index: usize,
18142 label: &str,
18143) -> Result<Option<Value>, SidecarError> {
18144 let Some(raw) = args.get(index).and_then(Value::as_str) else {
18145 return Ok(None);
18146 };
18147 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
18148 SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
18149 })?;
18150 if parsed.get("hasOptions").and_then(Value::as_bool) == Some(true) {
18151 Ok(parsed.get("options").cloned())
18152 } else {
18153 Ok(None)
18154 }
18155}
18156
18157fn javascript_crypto_u32_from_bridge_value(
18158 value: &Value,
18159 label: &str,
18160) -> Result<u32, SidecarError> {
18161 if let Some(number) = value.as_u64() {
18162 return u32::try_from(number)
18163 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")));
18164 }
18165 let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
18166 if bytes.len() > 4 {
18167 return Err(SidecarError::InvalidState(format!(
18168 "{label} buffer is too large for u32"
18169 )));
18170 }
18171 Ok(bytes
18172 .into_iter()
18173 .fold(0_u32, |acc, byte| (acc << 8) | u32::from(byte)))
18174}
18175
18176fn javascript_crypto_bignum_from_bridge_value(
18177 value: &Value,
18178 label: &str,
18179) -> Result<BigNum, SidecarError> {
18180 if let Some(object) = value.as_object() {
18181 if object.get("__type").and_then(Value::as_str) == Some("bigint") {
18182 let decimal = object.get("value").and_then(Value::as_str).ok_or_else(|| {
18183 SidecarError::InvalidState(format!("{label} bigint is missing a value"))
18184 })?;
18185 return BigNum::from_dec_str(decimal).map_err(javascript_crypto_openssl_error);
18186 }
18187 }
18188 let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
18189 BigNum::from_slice(&bytes).map_err(javascript_crypto_openssl_error)
18190}
18191
18192fn javascript_crypto_curve_nid(name: &str) -> Result<Nid, SidecarError> {
18193 match name {
18194 "prime256v1" | "P-256" => Ok(Nid::X9_62_PRIME256V1),
18195 "secp384r1" | "P-384" => Ok(Nid::SECP384R1),
18196 "secp521r1" | "P-521" => Ok(Nid::SECP521R1),
18197 "secp256k1" => Ok(Nid::SECP256K1),
18198 other => Err(SidecarError::InvalidState(format!(
18199 "unsupported EC curve {other}"
18200 ))),
18201 }
18202}
18203
18204fn javascript_crypto_named_dh_group(name: &str) -> Result<Dh<Params>, SidecarError> {
18205 match name {
18206 "modp2" => Dh::get_1024_160().map_err(javascript_crypto_openssl_error),
18207 "modp14" | "modp15" | "modp16" | "modp17" | "modp18" => {
18208 Dh::get_2048_256().map_err(javascript_crypto_openssl_error)
18209 }
18210 other => Err(SidecarError::InvalidState(format!(
18211 "unsupported Diffie-Hellman group {other}"
18212 ))),
18213 }
18214}
18215
18216fn javascript_crypto_clone_dh_params(params: &Dh<Params>) -> Result<Dh<Params>, SidecarError> {
18217 Dh::from_pqg(
18218 params
18219 .prime_p()
18220 .to_owned()
18221 .map_err(javascript_crypto_openssl_error)?,
18222 params
18223 .prime_q()
18224 .map(|value| value.to_owned().map_err(javascript_crypto_openssl_error))
18225 .transpose()?,
18226 params
18227 .generator()
18228 .to_owned()
18229 .map_err(javascript_crypto_openssl_error)?,
18230 )
18231 .map_err(javascript_crypto_openssl_error)
18232}
18233
18234fn javascript_crypto_build_dh_params(args: &[Value]) -> Result<Dh<Params>, SidecarError> {
18235 let Some(first) = args.first() else {
18236 return Err(SidecarError::InvalidState(String::from(
18237 "Diffie-Hellman session args are required",
18238 )));
18239 };
18240 if let Some(bits) = first.as_u64() {
18241 let generator = args
18242 .get(1)
18243 .map(|value| javascript_crypto_u32_from_bridge_value(value, "Diffie-Hellman generator"))
18244 .transpose()?
18245 .unwrap_or(2);
18246 return Dh::generate_params(bits as u32, generator)
18247 .map_err(javascript_crypto_openssl_error);
18248 }
18249 let prime = javascript_crypto_bignum_from_bridge_value(first, "Diffie-Hellman prime")?;
18250 let generator = args
18251 .get(1)
18252 .map(|value| javascript_crypto_bignum_from_bridge_value(value, "Diffie-Hellman generator"))
18253 .transpose()?
18254 .unwrap_or(BigNum::from_u32(2).map_err(javascript_crypto_openssl_error)?);
18255 Dh::from_pqg(prime, None, generator).map_err(javascript_crypto_openssl_error)
18256}
18257
18258fn javascript_crypto_call_dh_session(
18259 session: &mut ActiveDhSession,
18260 method: &str,
18261 args: &[Value],
18262) -> Result<(Value, bool), SidecarError> {
18263 match method {
18264 "verifyError" => Ok((Value::Null, false)),
18265 "generateKeys" => {
18266 if session.key_pair.is_none() {
18267 session.key_pair = Some(
18268 javascript_crypto_clone_dh_params(&session.params)?
18269 .generate_key()
18270 .map_err(javascript_crypto_openssl_error)?,
18271 );
18272 }
18273 let public = session
18274 .key_pair
18275 .as_ref()
18276 .expect("dh key pair")
18277 .public_key()
18278 .to_vec();
18279 Ok((javascript_crypto_bridge_buffer_value(&public), true))
18280 }
18281 "computeSecret" => {
18282 if session.key_pair.is_none() {
18283 session.key_pair = Some(
18284 javascript_crypto_clone_dh_params(&session.params)?
18285 .generate_key()
18286 .map_err(javascript_crypto_openssl_error)?,
18287 );
18288 }
18289 let peer = javascript_crypto_bignum_from_bridge_value(
18290 args.first().ok_or_else(|| {
18291 SidecarError::InvalidState(String::from(
18292 "computeSecret requires peer public key",
18293 ))
18294 })?,
18295 "Diffie-Hellman peer public key",
18296 )?;
18297 let private_key = session
18298 .key_pair
18299 .as_ref()
18300 .expect("dh key pair")
18301 .private_key();
18302 let mut secret = BigNum::new().map_err(javascript_crypto_openssl_error)?;
18303 let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
18304 secret
18305 .mod_exp(&peer, private_key, session.params.prime_p(), &mut ctx)
18306 .map_err(javascript_crypto_openssl_error)?;
18307 Ok((
18308 javascript_crypto_bridge_buffer_value(
18309 &secret
18310 .to_vec_padded(session.params.prime_p().num_bytes())
18311 .map_err(javascript_crypto_openssl_error)?,
18312 ),
18313 true,
18314 ))
18315 }
18316 "getPrime" => Ok((
18317 javascript_crypto_bridge_buffer_value(&session.params.prime_p().to_vec()),
18318 true,
18319 )),
18320 "getGenerator" => Ok((
18321 javascript_crypto_bridge_buffer_value(&session.params.generator().to_vec()),
18322 true,
18323 )),
18324 "getPublicKey" => {
18325 if session.key_pair.is_none() {
18326 session.key_pair = Some(
18327 javascript_crypto_clone_dh_params(&session.params)?
18328 .generate_key()
18329 .map_err(javascript_crypto_openssl_error)?,
18330 );
18331 }
18332 Ok((
18333 javascript_crypto_bridge_buffer_value(
18334 &session
18335 .key_pair
18336 .as_ref()
18337 .expect("dh key pair")
18338 .public_key()
18339 .to_vec(),
18340 ),
18341 true,
18342 ))
18343 }
18344 "getPrivateKey" => {
18345 if session.key_pair.is_none() {
18346 session.key_pair = Some(
18347 javascript_crypto_clone_dh_params(&session.params)?
18348 .generate_key()
18349 .map_err(javascript_crypto_openssl_error)?,
18350 );
18351 }
18352 Ok((
18353 javascript_crypto_bridge_buffer_value(
18354 &session
18355 .key_pair
18356 .as_ref()
18357 .expect("dh key pair")
18358 .private_key()
18359 .to_vec(),
18360 ),
18361 true,
18362 ))
18363 }
18364 "setPrivateKey" => {
18365 let private_key = javascript_crypto_bignum_from_bridge_value(
18366 args.first().ok_or_else(|| {
18367 SidecarError::InvalidState(String::from("setPrivateKey requires private key"))
18368 })?,
18369 "Diffie-Hellman private key",
18370 )?;
18371 let mut public_key = BigNum::new().map_err(javascript_crypto_openssl_error)?;
18372 let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
18373 public_key
18374 .mod_exp(
18375 session.params.generator(),
18376 &private_key,
18377 session.params.prime_p(),
18378 &mut ctx,
18379 )
18380 .map_err(javascript_crypto_openssl_error)?;
18381 session.key_pair = Some(
18382 javascript_crypto_clone_dh_params(&session.params)?
18383 .set_key(public_key, private_key)
18384 .map_err(javascript_crypto_openssl_error)?,
18385 );
18386 Ok((Value::Null, false))
18387 }
18388 "setPublicKey" => {
18389 let public_key = javascript_crypto_bignum_from_bridge_value(
18390 args.first().ok_or_else(|| {
18391 SidecarError::InvalidState(String::from("setPublicKey requires public key"))
18392 })?,
18393 "Diffie-Hellman public key",
18394 )?;
18395 let private_key = session
18396 .key_pair
18397 .as_ref()
18398 .ok_or_else(|| {
18399 SidecarError::InvalidState(String::from(
18400 "setPublicKey requires private key to be set first",
18401 ))
18402 })?
18403 .private_key()
18404 .to_owned()
18405 .map_err(javascript_crypto_openssl_error)?;
18406 session.key_pair = Some(
18407 javascript_crypto_clone_dh_params(&session.params)?
18408 .set_key(public_key, private_key)
18409 .map_err(javascript_crypto_openssl_error)?,
18410 );
18411 Ok((Value::Null, false))
18412 }
18413 other => Err(SidecarError::InvalidState(format!(
18414 "Unsupported Diffie-Hellman method: {other}"
18415 ))),
18416 }
18417}
18418
18419fn javascript_crypto_call_ecdh_session(
18420 session: &mut ActiveEcdhSession,
18421 method: &str,
18422 args: &[Value],
18423) -> Result<(Value, bool), SidecarError> {
18424 let nid = javascript_crypto_curve_nid(&session.curve)?;
18425 let group = EcGroup::from_curve_name(nid).map_err(javascript_crypto_openssl_error)?;
18426 match method {
18427 "verifyError" => Ok((Value::Null, false)),
18428 "generateKeys" => {
18429 if session.key_pair.is_none() {
18430 session.key_pair =
18431 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
18432 }
18433 let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
18434 let bytes = session
18435 .key_pair
18436 .as_ref()
18437 .expect("ecdh key pair")
18438 .public_key()
18439 .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
18440 .map_err(javascript_crypto_openssl_error)?;
18441 Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
18442 }
18443 "computeSecret" => {
18444 if session.key_pair.is_none() {
18445 session.key_pair =
18446 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
18447 }
18448 let peer_bytes = javascript_crypto_decode_bridge_buffer(
18449 args.first().ok_or_else(|| {
18450 SidecarError::InvalidState(String::from(
18451 "computeSecret requires peer public key",
18452 ))
18453 })?,
18454 "ECDH peer public key",
18455 )?;
18456 let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
18457 let peer_point = EcPoint::from_bytes(&group, &peer_bytes, &mut ctx)
18458 .map_err(javascript_crypto_openssl_error)?;
18459 let peer_key = EcKey::from_public_key(&group, &peer_point)
18460 .map_err(javascript_crypto_openssl_error)?;
18461 let private =
18462 PKey::from_ec_key(session.key_pair.as_ref().expect("ecdh key pair").to_owned())
18463 .map_err(javascript_crypto_openssl_error)?;
18464 let peer = PKey::from_ec_key(peer_key).map_err(javascript_crypto_openssl_error)?;
18465 let mut deriver = Deriver::new(&private).map_err(javascript_crypto_openssl_error)?;
18466 deriver
18467 .set_peer(&peer)
18468 .map_err(javascript_crypto_openssl_error)?;
18469 let secret = deriver
18470 .derive_to_vec()
18471 .map_err(javascript_crypto_openssl_error)?;
18472 Ok((javascript_crypto_bridge_buffer_value(&secret), true))
18473 }
18474 "getPublicKey" => {
18475 if session.key_pair.is_none() {
18476 session.key_pair =
18477 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
18478 }
18479 let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
18480 let bytes = session
18481 .key_pair
18482 .as_ref()
18483 .expect("ecdh key pair")
18484 .public_key()
18485 .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
18486 .map_err(javascript_crypto_openssl_error)?;
18487 Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
18488 }
18489 "getPrivateKey" => {
18490 if session.key_pair.is_none() {
18491 session.key_pair =
18492 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
18493 }
18494 Ok((
18495 javascript_crypto_bridge_buffer_value(
18496 &session
18497 .key_pair
18498 .as_ref()
18499 .expect("ecdh key pair")
18500 .private_key()
18501 .to_vec(),
18502 ),
18503 true,
18504 ))
18505 }
18506 "setPrivateKey" => {
18507 let private_key = javascript_crypto_bignum_from_bridge_value(
18508 args.first().ok_or_else(|| {
18509 SidecarError::InvalidState(String::from("setPrivateKey requires private key"))
18510 })?,
18511 "ECDH private key",
18512 )?;
18513 let ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
18514 let mut public_key = EcPoint::new(&group).map_err(javascript_crypto_openssl_error)?;
18515 public_key
18516 .mul_generator(&group, &private_key, &ctx)
18517 .map_err(javascript_crypto_openssl_error)?;
18518 session.key_pair = Some(
18519 EcKey::from_private_components(&group, &private_key, &public_key)
18520 .map_err(javascript_crypto_openssl_error)?,
18521 );
18522 Ok((Value::Null, false))
18523 }
18524 "setPublicKey" => {
18525 let public_key_bytes = javascript_crypto_decode_bridge_buffer(
18526 args.first().ok_or_else(|| {
18527 SidecarError::InvalidState(String::from("setPublicKey requires public key"))
18528 })?,
18529 "ECDH public key",
18530 )?;
18531 let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
18532 let public_key = EcPoint::from_bytes(&group, &public_key_bytes, &mut ctx)
18533 .map_err(javascript_crypto_openssl_error)?;
18534 let private_key = session
18535 .key_pair
18536 .as_ref()
18537 .ok_or_else(|| {
18538 SidecarError::InvalidState(String::from(
18539 "setPublicKey requires private key to be set first",
18540 ))
18541 })?
18542 .private_key()
18543 .to_owned()
18544 .map_err(javascript_crypto_openssl_error)?;
18545 session.key_pair = Some(
18546 EcKey::from_private_components(&group, &private_key, &public_key)
18547 .map_err(javascript_crypto_openssl_error)?,
18548 );
18549 Ok((Value::Null, false))
18550 }
18551 other => Err(SidecarError::InvalidState(format!(
18552 "Unsupported Diffie-Hellman method: {other}"
18553 ))),
18554 }
18555}
18556
18557fn javascript_crypto_serialize_encoded_key_value_public(
18558 key: &PKey<Public>,
18559 encoding: Option<&Value>,
18560) -> Result<Value, SidecarError> {
18561 if let Some(encoding) = encoding {
18562 let format = encoding
18563 .get("format")
18564 .and_then(Value::as_str)
18565 .unwrap_or("pem");
18566 return Ok(match format {
18567 "der" => json!({
18568 "kind": "buffer",
18569 "value": base64::engine::general_purpose::STANDARD
18570 .encode(key.public_key_to_der().map_err(javascript_crypto_openssl_error)?),
18571 }),
18572 _ => json!({
18573 "kind": "string",
18574 "value": String::from_utf8(
18575 key.public_key_to_pem().map_err(javascript_crypto_openssl_error)?,
18576 )
18577 .map_err(|error| SidecarError::InvalidState(format!("public key PEM utf8: {error}")))?,
18578 }),
18579 });
18580 }
18581 javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(
18582 key.to_owned(),
18583 ))
18584}
18585
18586fn javascript_crypto_serialize_encoded_key_value_private(
18587 key: &PKey<Private>,
18588 encoding: Option<&Value>,
18589) -> Result<Value, SidecarError> {
18590 if let Some(encoding) = encoding {
18591 let format = encoding
18592 .get("format")
18593 .and_then(Value::as_str)
18594 .unwrap_or("pem");
18595 return Ok(match format {
18596 "der" => json!({
18597 "kind": "buffer",
18598 "value": base64::engine::general_purpose::STANDARD
18599 .encode(key.private_key_to_der().map_err(javascript_crypto_openssl_error)?),
18600 }),
18601 _ => json!({
18602 "kind": "string",
18603 "value": String::from_utf8(
18604 key.private_key_to_pem_pkcs8().map_err(javascript_crypto_openssl_error)?,
18605 )
18606 .map_err(|error| SidecarError::InvalidState(format!("private key PEM utf8: {error}")))?,
18607 }),
18608 });
18609 }
18610 javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(
18611 key.to_owned(),
18612 ))
18613}
18614
18615fn javascript_crypto_bridge_buffer_value(bytes: &[u8]) -> Value {
18616 bridge_buffer_value(bytes)
18617}
18618
18619fn javascript_crypto_cipher_error(error: AesCipherError) -> SidecarError {
18620 SidecarError::InvalidState(error.0)
18621}
18622
18623fn javascript_crypto_decode_cipher_option_b64(
18624 options: Option<&Value>,
18625 field: &str,
18626) -> Result<Option<Vec<u8>>, SidecarError> {
18627 let Some(encoded) = options
18628 .and_then(|value| value.get(field))
18629 .and_then(Value::as_str)
18630 else {
18631 return Ok(None);
18632 };
18633 base64::engine::general_purpose::STANDARD
18634 .decode(encoded)
18635 .map(Some)
18636 .map_err(|error| {
18637 SidecarError::InvalidState(format!("cipher {field} contains invalid base64: {error}"))
18638 })
18639}
18640
18641fn javascript_crypto_build_cipher_session(
18642 algorithm: &str,
18643 key: &[u8],
18644 iv: Option<&[u8]>,
18645 decrypt: bool,
18646 options: Option<&Value>,
18647) -> Result<StreamCipherSession, SidecarError> {
18648 let pad = options
18649 .and_then(|value| value.get("autoPadding"))
18650 .and_then(Value::as_bool)
18651 .unwrap_or(true);
18652 let aad = if javascript_crypto_is_aead(algorithm) {
18653 javascript_crypto_decode_cipher_option_b64(options, "aad")?
18654 } else {
18655 None
18656 };
18657 let auth_tag = if decrypt && javascript_crypto_is_aead(algorithm) {
18658 javascript_crypto_decode_cipher_option_b64(options, "authTag")?
18659 } else {
18660 None
18661 };
18662 let tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options)?;
18663 StreamCipherSession::new(
18664 algorithm,
18665 key,
18666 iv,
18667 decrypt,
18668 pad,
18669 aad.as_deref(),
18670 auth_tag.as_deref(),
18671 tag_len,
18672 )
18673 .map_err(javascript_crypto_cipher_error)
18674}
18675
18676fn javascript_crypto_requested_aead_tag_len(
18677 algorithm: &str,
18678 options: Option<&Value>,
18679) -> Result<usize, SidecarError> {
18680 if !javascript_crypto_is_aead(algorithm) {
18681 return Ok(0);
18682 }
18683 let requested = options
18684 .and_then(|value| value.get("authTagLength"))
18685 .and_then(Value::as_u64)
18686 .unwrap_or(javascript_crypto_aead_tag_len(algorithm) as u64);
18687 usize::try_from(requested).map_err(|_| {
18688 SidecarError::InvalidState(String::from("cipher authTagLength must fit within usize"))
18689 })
18690}
18691
18692fn javascript_crypto_cipher_update(
18693 session: &mut StreamCipherSession,
18694 data: &[u8],
18695) -> Result<Vec<u8>, SidecarError> {
18696 session.update(data).map_err(javascript_crypto_cipher_error)
18697}
18698
18699fn javascript_crypto_is_aead(algorithm: &str) -> bool {
18700 crate::crypto_cipher::is_aead(algorithm)
18701}
18702
18703fn javascript_crypto_aead_tag_len(_algorithm: &str) -> usize {
18704 crate::crypto_cipher::default_aead_tag_len()
18705}
18706
18707fn javascript_crypto_openssl_error(error: openssl::error::ErrorStack) -> SidecarError {
18708 SidecarError::Execution(format!("crypto operation failed: {error}"))
18709}
18710
18711fn service_javascript_kernel_stdin_sync_rpc(
18712 kernel: &mut SidecarKernel,
18713 process: &mut ActiveProcess,
18714 request: &JavascriptSyncRpcRequest,
18715) -> Result<Value, SidecarError> {
18716 let (max_bytes, timeout_ms) = parse_kernel_stdin_read_args(request)?;
18717 kernel_stdin_read_response(
18718 kernel,
18719 process.kernel_pid,
18720 max_bytes,
18721 Duration::from_millis(timeout_ms),
18722 )
18723}
18724
18725pub(crate) fn parse_kernel_stdin_read_args(
18727 request: &JavascriptSyncRpcRequest,
18728) -> Result<(usize, u64), SidecarError> {
18729 let max_bytes =
18730 javascript_sync_rpc_arg_u64_optional(&request.args, 0, "__kernel_stdin_read max bytes")?
18731 .map(|value| value.clamp(1, DEFAULT_KERNEL_STDIN_READ_MAX_BYTES as u64) as usize)
18732 .unwrap_or(DEFAULT_KERNEL_STDIN_READ_MAX_BYTES);
18733 let timeout_ms =
18734 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_stdin_read timeout ms")?
18735 .unwrap_or(DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS);
18736 Ok((max_bytes, timeout_ms))
18737}
18738
18739pub(crate) fn kernel_stdin_read_response(
18742 kernel: &mut SidecarKernel,
18743 kernel_pid: u32,
18744 max_bytes: usize,
18745 timeout: Duration,
18746) -> Result<Value, SidecarError> {
18747 match kernel
18748 .fd_read_with_timeout_result(
18749 EXECUTION_DRIVER_NAME,
18750 kernel_pid,
18751 0,
18752 max_bytes,
18753 Some(timeout),
18754 )
18755 .map_err(kernel_error)
18756 {
18757 Ok(Some(chunk)) if !chunk.is_empty() => Ok(json!({
18758 "dataBase64": base64::engine::general_purpose::STANDARD.encode(chunk),
18759 })),
18760 Ok(Some(_)) => Ok(Value::Null),
18761 Ok(None) => Ok(json!({
18762 "done": true,
18763 })),
18764 Err(SidecarError::Kernel(error)) if error.starts_with("EAGAIN:") => Ok(Value::Null),
18765 Err(error) => Err(error),
18766 }
18767}
18768
18769fn service_javascript_pty_set_raw_mode_sync_rpc(
18770 kernel: &mut SidecarKernel,
18771 process: &mut ActiveProcess,
18772 request: &JavascriptSyncRpcRequest,
18773) -> Result<Value, SidecarError> {
18774 let enabled = javascript_sync_rpc_arg_bool(&request.args, 0, "__pty_set_raw_mode enabled")?;
18775 kernel
18776 .pty_set_discipline(
18777 EXECUTION_DRIVER_NAME,
18778 process.kernel_pid,
18779 0,
18780 LineDisciplineConfig {
18785 canonical: Some(!enabled),
18786 echo: Some(!enabled),
18787 isig: Some(!enabled),
18788 opost: Some(!enabled),
18789 onlcr: Some(!enabled),
18790 },
18791 )
18792 .map_err(kernel_error)?;
18793 Ok(Value::Null)
18794}
18795
18796fn service_javascript_kernel_isatty_sync_rpc(
18797 kernel: &mut SidecarKernel,
18798 process: &ActiveProcess,
18799 request: &JavascriptSyncRpcRequest,
18800) -> Result<Value, SidecarError> {
18801 let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_isatty fd")?;
18802 let is_tty = kernel
18803 .isatty(EXECUTION_DRIVER_NAME, process.kernel_pid, fd)
18804 .map_err(kernel_error)?;
18805 Ok(json!(is_tty))
18806}
18807
18808fn service_javascript_kernel_tty_size_sync_rpc(
18809 kernel: &mut SidecarKernel,
18810 process: &ActiveProcess,
18811 request: &JavascriptSyncRpcRequest,
18812) -> Result<Value, SidecarError> {
18813 let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_tty_size fd")?;
18814 let size = kernel
18815 .pty_window_size(EXECUTION_DRIVER_NAME, process.kernel_pid, fd)
18816 .map_err(kernel_error)?;
18817 Ok(json!({
18818 "cols": size.cols,
18819 "rows": size.rows,
18820 }))
18821}
18822
18823fn tty_is_raw_mode(kernel: &SidecarKernel, process: &ActiveProcess) -> bool {
18857 let Some(master_fd) = process.tty_master_fd else {
18858 return false;
18859 };
18860 match kernel.tcgetattr(EXECUTION_DRIVER_NAME, process.kernel_pid, master_fd) {
18861 Ok(termios) => !termios.echo && !termios.icanon,
18862 Err(_) => false,
18863 }
18864}
18865
18866pub(crate) fn drain_tty_master_output(
18875 kernel: &mut SidecarKernel,
18876 process: &mut ActiveProcess,
18877) -> Result<Option<Vec<u8>>, SidecarError> {
18878 let Some(master_fd) = process.tty_master_fd else {
18879 return Ok(None);
18880 };
18881 match kernel.fd_read_with_timeout_result(
18882 EXECUTION_DRIVER_NAME,
18883 process.kernel_pid,
18884 master_fd,
18885 MAX_PTY_BUFFER_BYTES,
18886 Some(Duration::ZERO),
18887 ) {
18888 Ok(Some(bytes)) if !bytes.is_empty() => Ok(Some(bytes)),
18889 Ok(_) => Ok(None),
18890 Err(error) if error.code() == "EAGAIN" => Ok(None),
18891 Err(error) => Err(kernel_error(error)),
18892 }
18893}
18894
18895fn service_javascript_kernel_stdio_write_sync_rpc(
18896 kernel: &mut SidecarKernel,
18897 process: &mut ActiveProcess,
18898 request: &JavascriptSyncRpcRequest,
18899) -> Result<Value, SidecarError> {
18900 let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_stdio_write fd")?;
18901 let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "__kernel_stdio_write chunk")?;
18902 if fd != 1 && fd != 2 {
18903 return Err(SidecarError::InvalidState(format!(
18904 "__kernel_stdio_write only supports fd 1/2, got {fd}"
18905 )));
18906 }
18907
18908 let raw_mode = tty_is_raw_mode(kernel, process);
18913 if process.tty_master_fd.is_some() && !raw_mode {
18914 let written = if fd == 1 {
18915 kernel
18916 .write_process_stdout(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
18917 .map_err(kernel_error)?
18918 } else {
18919 kernel
18920 .write_process_stderr(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
18921 .map_err(kernel_error)?
18922 };
18923 if let Some(master_bytes) = drain_tty_master_output(kernel, process)? {
18924 process.queue_pending_execution_event(ActiveExecutionEvent::Stdout(master_bytes))?;
18925 }
18926 return Ok(json!(written));
18927 }
18928
18929 let written = if process.tty_master_fd.is_some() {
18934 chunk.len()
18935 } else if fd == 1 {
18936 kernel
18937 .write_process_stdout(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
18938 .map_err(kernel_error)?
18939 } else {
18940 kernel
18941 .write_process_stderr(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
18942 .map_err(kernel_error)?
18943 };
18944
18945 let event = if fd == 1 {
18946 ActiveExecutionEvent::Stdout(chunk)
18947 } else {
18948 ActiveExecutionEvent::Stderr(chunk)
18949 };
18950 process.queue_pending_execution_event(event)?;
18951
18952 Ok(json!(written))
18953}
18954
18955fn service_javascript_kernel_poll_sync_rpc(
18956 kernel: &mut SidecarKernel,
18957 process: &ActiveProcess,
18958 request: &JavascriptSyncRpcRequest,
18959) -> Result<Value, SidecarError> {
18960 let (fd_requests, timeout_ms) = parse_kernel_poll_args(request)?;
18961 kernel_poll_response(kernel, process.kernel_pid, &fd_requests, timeout_ms)
18962}
18963
18964pub(crate) fn parse_kernel_poll_args(
18966 request: &JavascriptSyncRpcRequest,
18967) -> Result<(Vec<KernelPollFdRequest>, i32), SidecarError> {
18968 let fd_requests: Vec<KernelPollFdRequest> = serde_json::from_value(
18969 request
18970 .args
18971 .first()
18972 .cloned()
18973 .unwrap_or_else(|| Value::Array(Vec::new())),
18974 )
18975 .map_err(|error| {
18976 SidecarError::InvalidState(format!(
18977 "__kernel_poll fd list must be a JSON array of {{ fd, events }} objects: {error}"
18978 ))
18979 })?;
18980 let timeout_ms =
18981 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_poll timeout ms")?
18982 .unwrap_or_default();
18983 let timeout_ms = i32::try_from(timeout_ms).map_err(|_| {
18984 SidecarError::InvalidState(String::from("__kernel_poll timeout ms must fit within i32"))
18985 })?;
18986 Ok((fd_requests, timeout_ms))
18987}
18988
18989pub(crate) fn kernel_poll_response(
18992 kernel: &SidecarKernel,
18993 kernel_pid: u32,
18994 fd_requests: &[KernelPollFdRequest],
18995 timeout_ms: i32,
18996) -> Result<Value, SidecarError> {
18997 let poll_fds = fd_requests
18998 .iter()
18999 .map(|entry| PollFd {
19000 fd: entry.fd,
19001 events: PollEvents::from_bits(entry.events),
19002 revents: PollEvents::empty(),
19003 })
19004 .collect::<Vec<_>>();
19005 let result = kernel
19006 .poll_fds(EXECUTION_DRIVER_NAME, kernel_pid, poll_fds, timeout_ms)
19007 .map_err(kernel_error)?;
19008 Ok(json!({
19009 "readyCount": result.ready_count,
19010 "fds": result
19011 .fds
19012 .into_iter()
19013 .map(|entry| KernelPollFdResponse {
19014 fd: entry.fd,
19015 events: entry.events.bits(),
19016 revents: entry.revents.bits(),
19017 })
19018 .collect::<Vec<_>>(),
19019 }))
19020}
19021
19022fn install_kernel_stdin_pipe(kernel: &mut SidecarKernel, pid: u32) -> Result<u32, SidecarError> {
19023 let (read_fd, write_fd) = kernel
19024 .open_pipe(EXECUTION_DRIVER_NAME, pid)
19025 .map_err(kernel_error)?;
19026 kernel
19027 .fd_dup2(EXECUTION_DRIVER_NAME, pid, read_fd, 0)
19028 .map_err(kernel_error)?;
19029 kernel
19030 .fd_close(EXECUTION_DRIVER_NAME, pid, read_fd)
19031 .map_err(kernel_error)?;
19032 Ok(write_fd)
19033}
19034
19035fn requested_pty_window_size(env: &BTreeMap<String, String>) -> Option<(u16, u16)> {
19036 let cols = env
19037 .get("COLUMNS")
19038 .and_then(|value| value.parse::<u16>().ok())
19039 .filter(|value| *value > 0)?;
19040 let rows = env
19041 .get("LINES")
19042 .and_then(|value| value.parse::<u16>().ok())
19043 .filter(|value| *value > 0)?;
19044 Some((cols, rows))
19045}
19046
19047fn javascript_child_process_stdin_mode(request: &JavascriptChildProcessSpawnRequest) -> &str {
19048 request
19049 .options
19050 .stdio
19051 .first()
19052 .map(String::as_str)
19053 .unwrap_or("pipe")
19054}
19055
19056pub(crate) fn write_kernel_process_stdin(
19057 kernel: &mut SidecarKernel,
19058 process: &mut ActiveProcess,
19059 chunk: &[u8],
19060) -> Result<(), SidecarError> {
19061 if process.runtime == GuestRuntimeKind::JavaScript && process.tty_master_fd.is_none() {
19066 return Ok(());
19067 }
19068 let Some(writer_fd) = process.kernel_stdin_writer_fd else {
19069 return Ok(());
19070 };
19071 kernel
19072 .fd_write(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd, chunk)
19073 .map_err(kernel_error)?;
19074 if let Some(echo) = drain_tty_master_output(kernel, process)? {
19079 process.queue_pending_execution_event(ActiveExecutionEvent::Stdout(echo))?;
19080 }
19081 forward_tty_slave_input_to_javascript(kernel, process)?;
19082 Ok(())
19083}
19084
19085fn forward_tty_slave_input_to_javascript(
19094 kernel: &mut SidecarKernel,
19095 process: &mut ActiveProcess,
19096) -> Result<(), SidecarError> {
19097 if process.tty_master_fd.is_none()
19098 || !matches!(process.execution, ActiveExecution::Javascript(_))
19099 {
19100 return Ok(());
19101 }
19102 loop {
19103 match kernel.fd_read_with_timeout_result(
19104 EXECUTION_DRIVER_NAME,
19105 process.kernel_pid,
19106 0,
19107 MAX_PTY_BUFFER_BYTES,
19108 Some(Duration::ZERO),
19109 ) {
19110 Ok(Some(bytes)) if !bytes.is_empty() => {
19111 process.execution.write_stdin(&bytes)?;
19112 }
19113 Ok(Some(_)) => return Ok(()),
19114 Ok(None) => {
19115 process.execution.close_stdin()?;
19116 return Ok(());
19117 }
19118 Err(error) if error.code() == "EAGAIN" => return Ok(()),
19119 Err(error) => return Err(kernel_error(error)),
19120 }
19121 }
19122}
19123
19124pub(crate) fn close_kernel_process_stdin(
19125 kernel: &mut SidecarKernel,
19126 process: &mut ActiveProcess,
19127) -> Result<(), SidecarError> {
19128 let Some(writer_fd) = process.kernel_stdin_writer_fd.take() else {
19129 return Ok(());
19130 };
19131 kernel
19132 .fd_close(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd)
19133 .map_err(kernel_error)
19134}
19135
19136fn parse_http_header_collection(
19137 headers: &BTreeMap<String, Value>,
19138 label: &str,
19139) -> Result<HttpHeaderCollection, SidecarError> {
19140 let mut normalized = BTreeMap::<String, Vec<String>>::new();
19141 let mut raw_pairs = Vec::new();
19142
19143 for (raw_name, value) in headers {
19144 let normalized_name = raw_name.to_ascii_lowercase();
19145 let values = match value {
19146 Value::String(text) => vec![text.clone()],
19147 Value::Array(values) => values
19148 .iter()
19149 .map(|entry| {
19150 entry.as_str().map(str::to_owned).ok_or_else(|| {
19151 SidecarError::InvalidState(format!(
19152 "{label} header {raw_name} must contain only strings"
19153 ))
19154 })
19155 })
19156 .collect::<Result<Vec<_>, _>>()?,
19157 other => {
19158 return Err(SidecarError::InvalidState(format!(
19159 "{label} header {raw_name} must be a string or string array, received {other}"
19160 )));
19161 }
19162 };
19163 raw_pairs.extend(
19164 values
19165 .iter()
19166 .cloned()
19167 .map(|entry| (raw_name.clone(), entry)),
19168 );
19169 normalized
19170 .entry(normalized_name)
19171 .or_default()
19172 .extend(values);
19173 }
19174
19175 Ok(HttpHeaderCollection {
19176 normalized,
19177 raw_pairs,
19178 })
19179}
19180
19181fn http_headers_json(headers: &HttpHeaderCollection) -> Value {
19182 let map = headers
19183 .normalized
19184 .iter()
19185 .map(|(name, values)| {
19186 let value = if values.len() == 1 {
19187 Value::String(values[0].clone())
19188 } else {
19189 Value::Array(values.iter().cloned().map(Value::String).collect())
19190 };
19191 (name.clone(), value)
19192 })
19193 .collect::<Map<String, Value>>();
19194 Value::Object(map)
19195}
19196
19197fn http_raw_headers_json(headers: &HttpHeaderCollection) -> Value {
19198 Value::Array(
19199 headers
19200 .raw_pairs
19201 .iter()
19202 .flat_map(|(name, value)| [Value::String(name.clone()), Value::String(value.clone())])
19203 .collect(),
19204 )
19205}
19206
19207fn is_loopback_request_host(host: &str) -> bool {
19208 let bare = host
19209 .strip_prefix('[')
19210 .and_then(|value| value.strip_suffix(']'))
19211 .unwrap_or(host);
19212 matches!(bare, "localhost" | "127.0.0.1" | "::1")
19213}
19214
19215fn serialize_http_loopback_request(
19216 url: &Url,
19217 options: &JavascriptHttpRequestOptions,
19218 headers: &HttpHeaderCollection,
19219) -> Result<String, SidecarError> {
19220 let body_base64 = options
19221 .body
19222 .as_ref()
19223 .map(|body| base64::engine::general_purpose::STANDARD.encode(body.as_bytes()));
19224 serde_json::to_string(&json!({
19225 "method": options.method.clone().unwrap_or_else(|| String::from("GET")),
19226 "url": http_request_target(url),
19227 "headers": http_headers_json(headers),
19228 "rawHeaders": http_raw_headers_json(headers),
19229 "bodyBase64": body_base64,
19230 }))
19231 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
19232}
19233
19234fn http_request_target(url: &Url) -> String {
19235 let path = if url.path().is_empty() {
19236 "/"
19237 } else {
19238 url.path()
19239 };
19240 format!(
19241 "{path}{}",
19242 url.query()
19243 .map(|query| format!("?{query}"))
19244 .unwrap_or_default()
19245 )
19246}
19247
19248fn find_kernel_http_listener_process(vm: &VmState, port: u16) -> Option<String> {
19249 vm.active_processes
19250 .iter()
19251 .find_map(|(process_id, process)| {
19252 process.tcp_listeners.values().find_map(|listener| {
19253 let socket_id = listener.kernel_socket_id?;
19254 let record = vm.kernel.socket_get(socket_id)?;
19255 let local_addr = record
19256 .local_address()
19257 .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
19258 .unwrap_or_else(|| listener.guest_local_addr());
19259 if local_addr.port() == port && is_vm_local_http_listener_addr(local_addr.ip()) {
19260 Some(process_id.to_owned())
19261 } else {
19262 None
19263 }
19264 })
19265 })
19266}
19267
19268fn is_vm_local_http_listener_addr(ip: IpAddr) -> bool {
19269 ip.is_loopback() || ip.is_unspecified()
19270}
19271
19272fn serialize_kernel_http_fetch_request(
19273 port: u16,
19274 path: &str,
19275 options: &JavascriptHttpRequestOptions,
19276 headers: &HttpHeaderCollection,
19277) -> Vec<u8> {
19278 let method = options.method.as_deref().unwrap_or("GET");
19279 let mut lines = vec![format!("{method} {path} HTTP/1.1")];
19280 let mut has_host = false;
19281 let mut has_connection = false;
19282 let mut has_content_length = false;
19283 for (name, values) in &headers.normalized {
19284 match name.as_str() {
19285 "host" => has_host = true,
19286 "connection" => has_connection = true,
19287 "content-length" => has_content_length = true,
19288 _ => {}
19289 }
19290 lines.push(format!("{name}: {}", values.join(", ")));
19291 }
19292 if !has_host {
19293 lines.push(format!("Host: 127.0.0.1:{port}"));
19294 }
19295 if !has_connection {
19296 lines.push(String::from("Connection: close"));
19297 }
19298 let body = options.body.as_deref().unwrap_or("").as_bytes();
19299 if !has_content_length && !body.is_empty() {
19300 lines.push(format!("Content-Length: {}", body.len()));
19301 }
19302 lines.push(String::new());
19303 lines.push(String::new());
19304
19305 let mut request = lines.join("\r\n").into_bytes();
19306 request.extend_from_slice(body);
19307 request
19308}
19309
19310fn kernel_http_fetch_target_exit_code(error: &SidecarError) -> Option<i32> {
19311 let SidecarError::Execution(message) = error else {
19312 return None;
19313 };
19314 message
19315 .strip_prefix("vm.fetch target exited before responding (exit code ")?
19316 .strip_suffix(')')?
19317 .parse()
19318 .ok()
19319}
19320
19321#[allow(clippy::too_many_arguments)]
19322fn service_host_fetch_target_event<B>(
19323 bridge: &SharedBridge<B>,
19324 vm_id: &str,
19325 dns: &VmDnsConfig,
19326 socket_paths: &JavascriptSocketPathContext,
19327 kernel: &mut SidecarKernel,
19328 kernel_readiness: &KernelSocketReadinessRegistry,
19329 process: &mut ActiveProcess,
19330 resource_limits: &ResourceLimits,
19331 wait: Duration,
19332) -> Result<bool, SidecarError>
19333where
19334 B: NativeSidecarBridge + Send + 'static,
19335 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19336{
19337 let Some(event) = process
19338 .execution
19339 .poll_event_blocking(wait)
19340 .map_err(|error| SidecarError::Execution(error.to_string()))?
19341 else {
19342 return Ok(false);
19343 };
19344
19345 match event {
19346 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
19347 let network_counts = process.network_resource_counts();
19348 let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
19349 bridge,
19350 vm_id,
19351 dns,
19352 socket_paths,
19353 kernel,
19354 kernel_readiness: Arc::clone(kernel_readiness),
19355 process,
19356 sync_request: &request,
19357 resource_limits,
19358 network_counts,
19359 });
19360 match response {
19361 Ok(result) => process
19362 .execution
19363 .respond_javascript_sync_rpc_response(request.id, result)
19364 .or_else(ignore_stale_javascript_sync_rpc_response)?,
19365 Err(error) => process
19366 .execution
19367 .respond_javascript_sync_rpc_error(
19368 request.id,
19369 javascript_sync_rpc_error_code(&error),
19370 error.to_string(),
19371 )
19372 .or_else(ignore_stale_javascript_sync_rpc_response)?,
19373 }
19374 }
19375 ActiveExecutionEvent::Exited(code) => {
19376 return Err(SidecarError::Execution(format!(
19377 "vm.fetch target exited before responding (exit code {code})"
19378 )));
19379 }
19380 other => {
19381 process.queue_pending_execution_event(other)?;
19382 }
19383 }
19384 Ok(true)
19385}
19386
19387fn drain_host_fetch_target_events<B>(
19388 bridge: &SharedBridge<B>,
19389 vm_id: &str,
19390 vm: &mut VmState,
19391 target_process_id: &str,
19392 socket_paths: &JavascriptSocketPathContext,
19393 resource_limits: &ResourceLimits,
19394) -> Result<(), SidecarError>
19395where
19396 B: NativeSidecarBridge + Send + 'static,
19397 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19398{
19399 for _ in 0..32 {
19400 let dns = vm.dns.clone();
19401 let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness);
19402 let Some(process) = vm.active_processes.get_mut(target_process_id) else {
19403 break;
19404 };
19405 let serviced = service_host_fetch_target_event(
19406 bridge,
19407 vm_id,
19408 &dns,
19409 socket_paths,
19410 &mut vm.kernel,
19411 &kernel_readiness,
19412 process,
19413 resource_limits,
19414 Duration::from_millis(1),
19415 )?;
19416 if !serviced {
19417 break;
19418 }
19419 }
19420 Ok(())
19421}
19422
19423#[allow(clippy::too_many_arguments)]
19424fn dispatch_kernel_http_fetch<B>(
19425 bridge: &SharedBridge<B>,
19426 vm_id: &str,
19427 vm: &mut VmState,
19428 target_process_id: &str,
19429 port: u16,
19430 path: &str,
19431 options: &JavascriptHttpRequestOptions,
19432 headers: &HttpHeaderCollection,
19433 max_fetch_response_bytes: usize,
19434) -> Result<String, SidecarError>
19435where
19436 B: NativeSidecarBridge + Send + 'static,
19437 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19438{
19439 let socket_paths = build_javascript_socket_path_context(vm)?;
19440 let family = JavascriptSocketFamily::Ipv4;
19441 let local_port = allocate_guest_listen_port(
19442 0,
19443 family,
19444 &socket_paths.used_tcp_guest_ports,
19445 socket_paths.listen_policy,
19446 )?;
19447 let resource_limits = vm.kernel.resource_limits().clone();
19448 let network_counts = vm_network_resource_counts(vm);
19449 check_network_resource_limit(
19450 resource_limits.max_sockets,
19451 network_counts.sockets,
19452 2,
19453 "socket",
19454 )?;
19455 check_network_resource_limit(
19456 resource_limits.max_connections,
19457 network_counts.connections,
19458 2,
19459 "connection",
19460 )?;
19461
19462 let kernel_pid = vm
19463 .active_processes
19464 .get(target_process_id)
19465 .ok_or_else(|| {
19466 SidecarError::InvalidState(format!(
19467 "vm.fetch target process disappeared: {target_process_id}"
19468 ))
19469 })?
19470 .kernel_pid;
19471 let socket_id = vm
19472 .kernel
19473 .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, SocketSpec::tcp())
19474 .map_err(kernel_error)?;
19475
19476 let result = dispatch_kernel_http_fetch_with_socket(
19477 bridge,
19478 vm_id,
19479 vm,
19480 target_process_id,
19481 kernel_pid,
19482 socket_id,
19483 local_port,
19484 port,
19485 path,
19486 options,
19487 headers,
19488 &socket_paths,
19489 &resource_limits,
19490 max_fetch_response_bytes,
19491 );
19492 let close_result = vm
19493 .kernel
19494 .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
19495 .map_err(kernel_error);
19496 let cleanup_result = if result.is_err() {
19497 drain_host_fetch_target_events(
19498 bridge,
19499 vm_id,
19500 vm,
19501 target_process_id,
19502 &socket_paths,
19503 &resource_limits,
19504 )
19505 } else {
19506 Ok(())
19507 };
19508 match (result, close_result) {
19509 (Ok(response), Ok(())) => cleanup_result.map(|()| response),
19510 (Err(error), _) => Err(error),
19511 (Ok(_), Err(error)) => Err(error),
19512 }
19513}
19514
19515#[allow(clippy::too_many_arguments)]
19516fn dispatch_kernel_http_fetch_with_socket<B>(
19517 bridge: &SharedBridge<B>,
19518 vm_id: &str,
19519 vm: &mut VmState,
19520 target_process_id: &str,
19521 kernel_pid: u32,
19522 socket_id: SocketId,
19523 local_port: u16,
19524 port: u16,
19525 path: &str,
19526 options: &JavascriptHttpRequestOptions,
19527 headers: &HttpHeaderCollection,
19528 socket_paths: &JavascriptSocketPathContext,
19529 resource_limits: &ResourceLimits,
19530 max_fetch_response_bytes: usize,
19531) -> Result<String, SidecarError>
19532where
19533 B: NativeSidecarBridge + Send + 'static,
19534 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19535{
19536 vm.kernel
19537 .socket_bind_inet(
19538 EXECUTION_DRIVER_NAME,
19539 kernel_pid,
19540 socket_id,
19541 InetSocketAddress::new("127.0.0.1", local_port),
19542 )
19543 .map_err(kernel_error)?;
19544 vm.kernel
19545 .socket_connect_inet_loopback(
19546 EXECUTION_DRIVER_NAME,
19547 kernel_pid,
19548 socket_id,
19549 InetSocketAddress::new("127.0.0.1", port),
19550 )
19551 .map_err(kernel_error)?;
19552
19553 let request_bytes = serialize_kernel_http_fetch_request(port, path, options, headers);
19554 vm.kernel
19555 .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, &request_bytes)
19556 .map_err(kernel_error)?;
19557
19558 let mut response_buffer = Vec::new();
19559 let mut peer_closed = false;
19560 let url = format!("http://127.0.0.1:{port}{path}");
19561 let deadline = Instant::now() + http_loopback_request_timeout();
19562 loop {
19563 if let Some(response) =
19564 parse_kernel_http_fetch_response(&response_buffer, peer_closed, &url)
19565 .map_err(sidecar_core_execution_error)?
19566 {
19567 ensure_vm_fetch_response_within_limit(&response, "vm.fetch", max_fetch_response_bytes)
19568 .map_err(sidecar_core_execution_error)?;
19569 return Ok(response);
19570 }
19571 if Instant::now() >= deadline {
19572 let preview = String::from_utf8_lossy(&response_buffer);
19573 return Err(SidecarError::Execution(format!(
19574 "vm.fetch timed out waiting for kernel TCP HTTP response ({} buffered bytes: {:?})",
19575 response_buffer.len(),
19576 preview.chars().take(200).collect::<String>()
19577 )));
19578 }
19579
19580 {
19581 let dns = vm.dns.clone();
19582 let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness);
19583 let process = vm
19584 .active_processes
19585 .get_mut(target_process_id)
19586 .ok_or_else(|| {
19587 SidecarError::InvalidState(format!(
19588 "vm.fetch target process disappeared: {target_process_id}"
19589 ))
19590 })?;
19591 service_host_fetch_target_event(
19592 bridge,
19593 vm_id,
19594 &dns,
19595 socket_paths,
19596 &mut vm.kernel,
19597 &kernel_readiness,
19598 process,
19599 resource_limits,
19600 Duration::from_millis(5),
19601 )?;
19602 }
19603
19604 let poll = vm
19605 .kernel
19606 .poll_targets(
19607 EXECUTION_DRIVER_NAME,
19608 kernel_pid,
19609 vec![PollTargetEntry::socket(
19610 socket_id,
19611 POLLIN | POLLHUP | POLLERR,
19612 )],
19613 5,
19614 )
19615 .map_err(kernel_error)?;
19616 let revents = poll
19617 .targets
19618 .first()
19619 .map(|entry| entry.revents)
19620 .unwrap_or_else(PollEvents::empty);
19621 if revents.intersects(POLLERR) {
19622 return Err(SidecarError::Execution(String::from(
19623 "vm.fetch kernel TCP socket reported POLLERR",
19624 )));
19625 }
19626 if revents.intersects(POLLIN) {
19627 loop {
19628 match vm
19629 .kernel
19630 .socket_read(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, 64 * 1024)
19631 {
19632 Ok(Some(bytes)) if !bytes.is_empty() => {
19633 response_buffer.extend(bytes);
19634 ensure_vm_fetch_raw_response_buffer_within_limit(
19635 response_buffer.len(),
19636 "vm.fetch",
19637 )
19638 .map_err(sidecar_core_execution_error)?;
19639 }
19640 Ok(Some(_)) => break,
19641 Ok(None) => {
19642 peer_closed = true;
19643 break;
19644 }
19645 Err(error) if error.code() == "EAGAIN" => break,
19646 Err(error) => return Err(kernel_error(error)),
19647 }
19648 }
19649 }
19650 if revents.intersects(POLLHUP) {
19651 peer_closed = true;
19652 }
19653 }
19654}
19655
19656fn outbound_http_response_json(url: &Url, response: ureq::Response) -> Result<Value, SidecarError> {
19657 let status = response.status();
19658 let status_text = response.status_text().to_owned();
19659 let mut header_pairs = Vec::new();
19660 let mut raw_headers = Vec::new();
19661 for raw_name in response.headers_names() {
19662 for value in response.all(&raw_name) {
19663 header_pairs.push(json!([raw_name.to_ascii_lowercase(), value]));
19664 raw_headers.push(Value::String(raw_name.clone()));
19665 raw_headers.push(Value::String(value.to_owned()));
19666 }
19667 }
19668 let mut reader = response.into_reader();
19669 let mut body = Vec::new();
19670 reader.read_to_end(&mut body).map_err(|error| {
19671 SidecarError::Execution(format!("failed to read HTTP response: {error}"))
19672 })?;
19673 serde_json::to_string(&json!({
19674 "status": status,
19675 "statusText": status_text,
19676 "headers": header_pairs,
19677 "rawHeaders": raw_headers,
19678 "body": base64::engine::general_purpose::STANDARD.encode(body),
19679 "bodyEncoding": "base64",
19680 "url": url.as_str(),
19681 }))
19682 .map(Value::String)
19683 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
19684}
19685
19686fn split_netloc(netloc: &str) -> Option<(&str, u16)> {
19690 let (host, port) = netloc.rsplit_once(':')?;
19691 let port: u16 = port.parse().ok()?;
19692 let host = host
19693 .strip_prefix('[')
19694 .and_then(|rest| rest.strip_suffix(']'))
19695 .unwrap_or(host);
19696 Some((host, port))
19697}
19698
19699fn issue_outbound_http_request(
19700 url: &Url,
19701 options: &JavascriptHttpRequestOptions,
19702 headers: &HttpHeaderCollection,
19703 pinned_addresses: &[IpAddr],
19704) -> Result<Value, SidecarError> {
19705 let method = options.method.as_deref().unwrap_or("GET");
19706 let pinned_host = url.host_str().map(str::to_owned);
19715 let pinned: Vec<IpAddr> = pinned_addresses.to_vec();
19716 let resolver = move |netloc: &str| -> std::io::Result<Vec<SocketAddr>> {
19717 let (host, port) = split_netloc(netloc).ok_or_else(|| {
19718 std::io::Error::new(
19719 std::io::ErrorKind::InvalidInput,
19720 format!("invalid network location: {netloc}"),
19721 )
19722 })?;
19723 let expected_host = pinned_host.as_deref();
19724 if expected_host != Some(host) {
19725 return Err(std::io::Error::new(
19726 std::io::ErrorKind::PermissionDenied,
19727 format!(
19728 "EACCES: outbound HTTP resolver pinned to {expected_host:?}, refusing {host}"
19729 ),
19730 ));
19731 }
19732 if pinned.is_empty() {
19733 return Err(std::io::Error::new(
19734 std::io::ErrorKind::PermissionDenied,
19735 "EACCES: no egress-vetted address available for outbound HTTP request",
19736 ));
19737 }
19738 Ok(pinned.iter().map(|ip| SocketAddr::new(*ip, port)).collect())
19739 };
19740 let mut agent_builder = ureq::AgentBuilder::new()
19741 .resolver(resolver)
19742 .timeout_connect(Duration::from_secs(5))
19743 .timeout_read(Duration::from_secs(15))
19744 .timeout_write(Duration::from_secs(15));
19745 if url.scheme() == "https" {
19746 let tls_options = JavascriptTlsBridgeOptions {
19747 is_server: false,
19748 servername: url.host_str().map(str::to_owned),
19749 alpn_protocols: Some(vec![String::from("http/1.1")]),
19750 reject_unauthorized: options.reject_unauthorized,
19751 ..JavascriptTlsBridgeOptions::default()
19752 };
19753 agent_builder = agent_builder.tls_config(Arc::new(build_client_tls_config(&tls_options)?));
19754 }
19755 let agent = agent_builder.build();
19756 let mut request = agent.request_url(method, url);
19757 for (name, values) in &headers.normalized {
19758 if name == "host" {
19759 continue;
19760 }
19761 let header_value = values.join(", ");
19762 request = request.set(name, &header_value);
19763 }
19764 let response = match options.body.as_deref() {
19765 Some(body) => request.send_string(body),
19766 None => request.call(),
19767 };
19768
19769 match response {
19770 Ok(response) => outbound_http_response_json(url, response),
19771 Err(ureq::Error::Status(_, response)) => outbound_http_response_json(url, response),
19772 Err(ureq::Error::Transport(error)) => Err(SidecarError::Execution(format!(
19773 "ERR_HTTP_REQUEST_FAILED: {error}"
19774 ))),
19775 }
19776}
19777
19778fn wait_for_loopback_http_response<B>(
19779 request: LoopbackHttpResponseWaitRequest<'_, B>,
19780) -> Result<String, SidecarError>
19781where
19782 B: NativeSidecarBridge + Send + 'static,
19783 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19784{
19785 let LoopbackHttpResponseWaitRequest {
19786 bridge,
19787 vm_id,
19788 dns,
19789 socket_paths,
19790 kernel,
19791 kernel_readiness,
19792 process,
19793 resource_limits,
19794 request_key,
19795 } = request;
19796 let deadline = Instant::now() + http_loopback_request_timeout();
19797 loop {
19798 if let Some(response) = process
19799 .pending_http_requests
19800 .get(&request_key)
19801 .and_then(|response| response.clone())
19802 {
19803 process.pending_http_requests.remove(&request_key);
19804 return Ok(response);
19805 }
19806
19807 if Instant::now() >= deadline {
19808 process.pending_http_requests.remove(&request_key);
19809 return Err(SidecarError::Execution(String::from(
19810 "HTTP loopback request timed out waiting for net.http_respond",
19811 )));
19812 }
19813
19814 let Some(event) = process
19815 .execution
19816 .poll_event_blocking(Duration::from_millis(10))
19817 .map_err(|error| SidecarError::Execution(error.to_string()))?
19818 else {
19819 continue;
19820 };
19821
19822 match event {
19823 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
19824 let network_counts = process.network_resource_counts();
19825 let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
19826 bridge,
19827 vm_id,
19828 dns,
19829 socket_paths,
19830 kernel,
19831 kernel_readiness: Arc::clone(&kernel_readiness),
19832 process,
19833 sync_request: &request,
19834 resource_limits,
19835 network_counts,
19836 });
19837 match response {
19838 Ok(result) => process
19839 .execution
19840 .respond_javascript_sync_rpc_response(request.id, result)
19841 .or_else(ignore_stale_javascript_sync_rpc_response)?,
19842 Err(error) => process
19843 .execution
19844 .respond_javascript_sync_rpc_error(
19845 request.id,
19846 javascript_sync_rpc_error_code(&error),
19847 error.to_string(),
19848 )
19849 .or_else(ignore_stale_javascript_sync_rpc_response)?,
19850 }
19851 }
19852 ActiveExecutionEvent::Exited(code) => {
19853 process.pending_http_requests.remove(&request_key);
19854 return Err(SidecarError::Execution(format!(
19855 "HTTP loopback server exited before responding (exit code {code})"
19856 )));
19857 }
19858 ActiveExecutionEvent::Stdout(_)
19859 | ActiveExecutionEvent::Stderr(_)
19860 | ActiveExecutionEvent::PythonVfsRpcRequest(_)
19861 | ActiveExecutionEvent::SignalState { .. } => {}
19862 }
19863 }
19864}
19865
19866pub(crate) fn dispatch_loopback_http_request<B>(
19867 request: LoopbackHttpDispatchRequest<'_, B>,
19868) -> Result<String, SidecarError>
19869where
19870 B: NativeSidecarBridge + Send + 'static,
19871 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19872{
19873 let LoopbackHttpDispatchRequest {
19874 bridge,
19875 vm_id,
19876 dns,
19877 socket_paths,
19878 kernel,
19879 kernel_readiness,
19880 process,
19881 resource_limits,
19882 server_id,
19883 request_json,
19884 } = request;
19885 let request_id = {
19886 let server = process.http_servers.get_mut(&server_id).ok_or_else(|| {
19887 SidecarError::InvalidState(format!("HTTP target server disappeared: {server_id}"))
19888 })?;
19889 server.next_request_id += 1;
19890 server.next_request_id
19891 };
19892 process
19893 .pending_http_requests
19894 .insert((server_id, request_id), None);
19895 process.execution.send_javascript_stream_event(
19896 "http_request",
19897 json!({
19898 "serverId": server_id,
19899 "requestId": request_id,
19900 "request": request_json,
19901 }),
19902 )?;
19903 wait_for_loopback_http_response(LoopbackHttpResponseWaitRequest {
19904 bridge,
19905 vm_id,
19906 dns,
19907 socket_paths,
19908 kernel,
19909 kernel_readiness,
19910 process,
19911 resource_limits,
19912 request_key: (server_id, request_id),
19913 })
19914}
19915
19916fn sidecar_core_execution_error(error: SidecarCoreError) -> SidecarError {
19917 SidecarError::Execution(error.to_string())
19918}
19919
19920pub(crate) fn ensure_vm_fetch_response_frame_within_limit(
19921 response: &ResponseFrame,
19922 max_frame_bytes: usize,
19923) -> Result<(), SidecarError> {
19924 let max_frame_bytes = max_frame_bytes.min(VM_FETCH_BUFFER_LIMIT_BYTES);
19925 let frame = crate::protocol::to_generated_protocol_frame(
19926 &crate::protocol::ProtocolFrame::Response(response.clone()),
19927 )
19928 .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))?;
19929 let WireProtocolFrame::ResponseFrame(_) = &frame else {
19930 return Err(SidecarError::FrameTooLarge(String::from(
19931 "vm fetch response converted to non-response wire frame",
19932 )));
19933 };
19934 WireFrameCodec::new(max_frame_bytes)
19935 .encode(&frame)
19936 .map(|_| ())
19937 .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))
19938}
19939
19940fn service_javascript_dns_sync_rpc<B>(
19941 bridge: &SharedBridge<B>,
19942 kernel: &SidecarKernel,
19943 vm_id: &str,
19944 dns: &VmDnsConfig,
19945 request: &JavascriptSyncRpcRequest,
19946) -> Result<Value, SidecarError>
19947where
19948 B: NativeSidecarBridge + Send + 'static,
19949 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19950{
19951 match request.method.as_str() {
19952 "dns.lookup" => {
19953 let payload = request
19954 .args
19955 .first()
19956 .cloned()
19957 .ok_or_else(|| {
19958 SidecarError::InvalidState(String::from(
19959 "dns.lookup requires a request payload",
19960 ))
19961 })
19962 .and_then(|value| {
19963 serde_json::from_value::<JavascriptDnsLookupRequest>(value).map_err(|error| {
19964 SidecarError::InvalidState(format!("invalid dns.lookup payload: {error}"))
19965 })
19966 })?;
19967 let addresses = filter_dns_ip_addrs(
19968 resolve_dns_ip_addrs(
19969 bridge,
19970 kernel,
19971 vm_id,
19972 dns,
19973 &payload.hostname,
19974 DnsLookupPolicy::CheckPermissions,
19975 )?,
19976 payload.family,
19977 )?;
19978 let addresses = filter_dns_safe_ip_addrs(addresses, &payload.hostname)?;
19979 Ok(Value::Array(
19980 addresses
19981 .into_iter()
19982 .map(|ip| {
19983 json!({
19984 "address": ip.to_string(),
19985 "family": if ip.is_ipv6() { 6 } else { 4 },
19986 })
19987 })
19988 .collect(),
19989 ))
19990 }
19991 "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
19992 let payload = request
19993 .args
19994 .first()
19995 .cloned()
19996 .ok_or_else(|| {
19997 SidecarError::InvalidState(String::from(
19998 "dns.resolve requires a request payload",
19999 ))
20000 })
20001 .and_then(|value| {
20002 serde_json::from_value::<JavascriptDnsResolveRequest>(value).map_err(|error| {
20003 SidecarError::InvalidState(format!("invalid dns.resolve payload: {error}"))
20004 })
20005 })?;
20006 let requested_type = match request.method.as_str() {
20007 "dns.resolve4" => String::from("A"),
20008 "dns.resolve6" => String::from("AAAA"),
20009 _ => payload
20010 .rrtype
20011 .as_deref()
20012 .unwrap_or("A")
20013 .to_ascii_uppercase(),
20014 };
20015 let record_type = parse_dns_record_type(&requested_type)?;
20016 let resolution = resolve_dns_records(
20017 bridge,
20018 kernel,
20019 vm_id,
20020 dns,
20021 &payload.hostname,
20022 record_type,
20023 DnsLookupPolicy::CheckPermissions,
20024 )?;
20025 dns_resolution_to_node_value(&resolution, &requested_type)
20026 }
20027 other => Err(SidecarError::InvalidState(format!(
20028 "unsupported JavaScript dns sync RPC method {other}"
20029 ))),
20030 }
20031}
20032
20033fn service_javascript_dgram_sync_rpc<B>(
20034 request: JavascriptDgramSyncRpcServiceRequest<'_, B>,
20035) -> Result<Value, SidecarError>
20036where
20037 B: NativeSidecarBridge + Send + 'static,
20038 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
20039{
20040 let JavascriptDgramSyncRpcServiceRequest {
20041 bridge,
20042 kernel,
20043 vm_id,
20044 dns,
20045 socket_paths,
20046 process,
20047 kernel_readiness,
20048 sync_request: request,
20049 resource_limits,
20050 network_counts,
20051 } = request;
20052 match request.method.as_str() {
20053 "dgram.createSocket" => {
20054 check_network_resource_limit(
20055 resource_limits.max_sockets,
20056 network_counts.sockets,
20057 1,
20058 "socket",
20059 )?;
20060 let payload = request
20061 .args
20062 .first()
20063 .cloned()
20064 .ok_or_else(|| {
20065 SidecarError::InvalidState(String::from(
20066 "dgram.createSocket requires a request payload",
20067 ))
20068 })
20069 .and_then(|value| {
20070 serde_json::from_value::<JavascriptDgramCreateSocketRequest>(value).map_err(
20071 |error| {
20072 SidecarError::InvalidState(format!(
20073 "invalid dgram.createSocket payload: {error}"
20074 ))
20075 },
20076 )
20077 })?;
20078 let family = JavascriptUdpFamily::from_socket_type(&payload.socket_type)?;
20079 let socket_id = process.allocate_udp_socket_id();
20080 let socket = ActiveUdpSocket::new(kernel, process.kernel_pid, family)?;
20081 register_kernel_readiness_target(
20082 &kernel_readiness,
20083 socket.kernel_socket_id,
20084 process.execution.javascript_v8_session_handle(),
20085 socket_id.clone(),
20086 KernelSocketReadinessEvent::Datagram,
20087 );
20088 process.udp_sockets.insert(socket_id.clone(), socket);
20089 Ok(json!({
20090 "socketId": socket_id,
20091 "type": family.socket_type(),
20092 }))
20093 }
20094 "dgram.bind" => {
20095 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.bind socket id")?;
20096 let payload = request
20097 .args
20098 .get(1)
20099 .cloned()
20100 .ok_or_else(|| {
20101 SidecarError::InvalidState(String::from(
20102 "dgram.bind requires a request payload",
20103 ))
20104 })
20105 .and_then(|value| {
20106 serde_json::from_value::<JavascriptDgramBindRequest>(value).map_err(|error| {
20107 SidecarError::InvalidState(format!("invalid dgram.bind payload: {error}"))
20108 })
20109 })?;
20110 let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
20111 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
20112 })?;
20113 let local_addr = socket.bind(
20114 kernel,
20115 process.kernel_pid,
20116 payload.address.as_deref(),
20117 payload.port,
20118 socket_paths,
20119 )?;
20120 Ok(local_endpoint_value(&local_addr))
20121 }
20122 "dgram.send" => {
20123 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.send socket id")?;
20124 let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "dgram.send payload")?;
20125 let payload = request
20126 .args
20127 .get(2)
20128 .cloned()
20129 .ok_or_else(|| {
20130 SidecarError::InvalidState(String::from(
20131 "dgram.send requires a request payload",
20132 ))
20133 })
20134 .and_then(|value| {
20135 serde_json::from_value::<JavascriptDgramSendRequest>(value).map_err(|error| {
20136 SidecarError::InvalidState(format!("invalid dgram.send payload: {error}"))
20137 })
20138 })?;
20139 let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
20140 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
20141 })?;
20142 let (written, local_addr) = socket.send_to(ActiveUdpSendToRequest {
20143 bridge,
20144 kernel,
20145 kernel_pid: process.kernel_pid,
20146 vm_id,
20147 dns,
20148 host: payload.address.as_deref().unwrap_or("localhost"),
20149 port: payload.port,
20150 context: socket_paths,
20151 contents: &chunk,
20152 })?;
20153 Ok(json!({
20154 "bytes": written,
20155 "localAddress": local_addr.ip().to_string(),
20156 "localPort": local_addr.port(),
20157 "family": socket_addr_family(&local_addr),
20158 }))
20159 }
20160 "dgram.poll" => {
20161 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.poll socket id")?;
20162 let wait_ms =
20163 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "dgram.poll wait ms")?
20164 .unwrap_or_default();
20165 let event = {
20166 let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
20167 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
20168 })?;
20169 socket.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?
20170 };
20171
20172 match event {
20173 Some(JavascriptUdpSocketEvent::Message { data, remote_addr }) => {
20174 let family = JavascriptSocketFamily::from_ip(remote_addr.ip());
20175 let guest_remote_port = if is_loopback_ip(remote_addr.ip()) {
20176 socket_paths
20177 .guest_udp_port_for_host_port(family, remote_addr.port())
20178 .unwrap_or(remote_addr.port())
20179 } else {
20180 remote_addr.port()
20181 };
20182 let mut response = remote_endpoint_value(&remote_addr, guest_remote_port);
20183 if let Value::Object(fields) = &mut response {
20184 fields.insert(String::from("type"), Value::String(String::from("message")));
20185 fields.insert(String::from("data"), javascript_sync_rpc_bytes_value(&data));
20186 }
20187 Ok(response)
20188 }
20189 Some(JavascriptUdpSocketEvent::Error { code, message }) => Ok(json!({
20190 "type": "error",
20191 "code": code,
20192 "message": message,
20193 })),
20194 None => Ok(Value::Null),
20195 }
20196 }
20197 "dgram.close" => {
20198 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.close socket id")?;
20199 let mut socket = process.udp_sockets.remove(socket_id).ok_or_else(|| {
20200 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
20201 })?;
20202 unregister_kernel_readiness_target(&kernel_readiness, socket.kernel_socket_id);
20203 socket.close(kernel, process.kernel_pid);
20204 Ok(Value::Null)
20205 }
20206 "dgram.address" => {
20207 let socket_id =
20208 javascript_sync_rpc_arg_str(&request.args, 0, "dgram.address socket id")?;
20209 let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
20210 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
20211 })?;
20212 let local_addr = socket.local_addr().ok_or_else(|| {
20213 SidecarError::Execution(String::from("EBADF: bad file descriptor"))
20214 })?;
20215 javascript_net_json_string(
20216 json!({
20217 "address": local_addr.ip().to_string(),
20218 "port": local_addr.port(),
20219 "family": socket_addr_family(&local_addr),
20220 }),
20221 "dgram.address",
20222 )
20223 }
20224 "dgram.setBufferSize" => {
20225 let socket_id =
20226 javascript_sync_rpc_arg_str(&request.args, 0, "dgram.setBufferSize socket id")?;
20227 let which =
20228 javascript_sync_rpc_arg_str(&request.args, 1, "dgram.setBufferSize buffer kind")?;
20229 let size = javascript_sync_rpc_arg_u64(&request.args, 2, "dgram.setBufferSize size")?;
20230 let size = usize::try_from(size).map_err(|_| {
20231 SidecarError::InvalidState(String::from(
20232 "dgram.setBufferSize size must fit within usize",
20233 ))
20234 })?;
20235 let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
20236 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
20237 })?;
20238 socket.set_buffer_size(which, size)?;
20239 Ok(Value::Null)
20240 }
20241 "dgram.getBufferSize" => {
20242 let socket_id =
20243 javascript_sync_rpc_arg_str(&request.args, 0, "dgram.getBufferSize socket id")?;
20244 let which =
20245 javascript_sync_rpc_arg_str(&request.args, 1, "dgram.getBufferSize buffer kind")?;
20246 let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
20247 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
20248 })?;
20249 let size = socket.get_buffer_size(which)?;
20250 Ok(json!(size))
20251 }
20252 other => Err(SidecarError::InvalidState(format!(
20253 "unsupported JavaScript dgram sync RPC method {other}"
20254 ))),
20255 }
20256}
20257
20258#[derive(Debug)]
20259struct ClientHttp2StreamState {
20260 send_stream: Option<h2::SendStream<Bytes>>,
20261}
20262
20263#[derive(Debug)]
20264struct ServerHttp2StreamState {
20265 send_response: Option<ServerHttp2Responder>,
20266 send_stream: Option<h2::SendStream<Bytes>>,
20267}
20268
20269#[derive(Debug)]
20270enum ServerHttp2Responder {
20271 Regular(server::SendResponse<Bytes>),
20272 Pushed(server::SendPushedResponse<Bytes>),
20273}
20274
20275const HTTP2_DEFAULT_WINDOW_SIZE: u32 = 65_535;
20276const HTTP2_POLL_DELAY: Duration = Duration::from_millis(10);
20277
20278fn http2_runtime_snapshot() -> Http2RuntimeSnapshot {
20279 Http2RuntimeSnapshot {
20280 effective_local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
20281 local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
20282 remote_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
20283 next_stream_id: 1,
20284 outbound_queue_size: 1,
20285 deflate_dynamic_table_size: 0,
20286 inflate_dynamic_table_size: 0,
20287 }
20288}
20289
20290fn http2_snapshot_json(snapshot: &Http2SessionSnapshot) -> Result<String, SidecarError> {
20291 serde_json::to_string(snapshot)
20292 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
20293}
20294
20295fn http2_event_value(event: &Http2BridgeEvent) -> Result<Value, SidecarError> {
20296 serde_json::to_string(event)
20297 .map(Value::String)
20298 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
20299}
20300
20301fn push_http2_server_event(
20302 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
20303 server_id: u64,
20304 event: Http2BridgeEvent,
20305) {
20306 let (ready, wake_session, should_wake) = if let Ok(mut state) = shared.lock() {
20307 let queue = state.server_events.entry(server_id).or_default();
20308 let should_wake = queue.is_empty();
20309 queue.push_back(event);
20310 (
20311 Some(Arc::clone(&state.ready)),
20312 state.event_session.clone(),
20313 should_wake,
20314 )
20315 } else {
20316 (None, None, false)
20317 };
20318 if let Some(ready) = ready {
20319 ready.notify_all();
20320 }
20321 if should_wake {
20322 push_http2_retain_wake(wake_session, "server", server_id);
20323 }
20324}
20325
20326fn push_http2_session_event(
20327 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
20328 session_id: u64,
20329 event: Http2BridgeEvent,
20330) {
20331 let (ready, wake_session, should_wake) = if let Ok(mut state) = shared.lock() {
20332 let queue = state.session_events.entry(session_id).or_default();
20333 let should_wake = queue.is_empty();
20334 queue.push_back(event);
20335 (
20336 Some(Arc::clone(&state.ready)),
20337 state.event_session.clone(),
20338 should_wake,
20339 )
20340 } else {
20341 (None, None, false)
20342 };
20343 if let Some(ready) = ready {
20344 ready.notify_all();
20345 }
20346 if should_wake {
20347 push_http2_retain_wake(wake_session, "session", session_id);
20348 }
20349}
20350
20351fn push_http2_retain_wake(session: Option<V8SessionHandle>, kind: &'static str, id: u64) {
20352 let Some(session) = session else {
20353 return;
20354 };
20355 let payload = v8_runtime::json_to_cbor_payload(&json!({
20356 "event": "http2",
20357 "kind": kind,
20358 "id": id,
20359 }))
20360 .unwrap_or_default();
20361 let _ = session.send_stream_event("net_socket", payload);
20362}
20363
20364fn pop_http2_event(
20365 queue: &mut BTreeMap<u64, VecDeque<Http2BridgeEvent>>,
20366 id: u64,
20367) -> Option<Http2BridgeEvent> {
20368 queue.get_mut(&id).and_then(VecDeque::pop_front)
20369}
20370
20371fn wait_for_http2_event(
20372 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
20373 id: u64,
20374 is_server: bool,
20375 wait_ms: u64,
20376) -> Option<Http2BridgeEvent> {
20377 let deadline = Instant::now() + Duration::from_millis(wait_ms);
20378 let mut state = shared.lock().ok()?;
20379 loop {
20380 let queue = if is_server {
20381 &mut state.server_events
20382 } else {
20383 &mut state.session_events
20384 };
20385 if let Some(event) = pop_http2_event(queue, id) {
20386 return Some(event);
20387 }
20388 if wait_ms == 0 {
20389 return None;
20390 }
20391 let now = Instant::now();
20392 if now >= deadline {
20393 return None;
20394 }
20395 let remaining = deadline.saturating_duration_since(now);
20396 if remaining.is_zero() {
20397 return None;
20398 }
20399 let ready = Arc::clone(&state.ready);
20400 state = ready.wait_timeout(state, remaining).ok()?.0;
20401 }
20402}
20403
20404fn next_http2_session_id(shared: &mut crate::state::Http2SharedState) -> u64 {
20405 shared.next_session_id += 1;
20406 shared.next_session_id
20407}
20408
20409fn next_http2_stream_id(shared: &mut crate::state::Http2SharedState) -> u64 {
20410 shared.next_stream_id += 1;
20411 shared.next_stream_id
20412}
20413
20414fn http2_reason(code: Option<u32>) -> Reason {
20415 code.unwrap_or(Reason::NO_ERROR.into()).into()
20416}
20417
20418fn http2_error_payload(message: impl Into<String>) -> String {
20419 serde_json::to_string(&json!({
20420 "name": "Error",
20421 "code": "ERR_HTTP2_ERROR",
20422 "message": message.into(),
20423 }))
20424 .unwrap_or_else(|_| {
20425 String::from(
20426 "{\"name\":\"Error\",\"code\":\"ERR_HTTP2_ERROR\",\"message\":\"HTTP/2 bridge error\"}",
20427 )
20428 })
20429}
20430
20431fn http2_socket_snapshot(local_addr: SocketAddr, remote_addr: SocketAddr) -> Http2SocketSnapshot {
20432 Http2SocketSnapshot {
20433 encrypted: false,
20434 allow_half_open: false,
20435 local_address: Some(local_addr.ip().to_string()),
20436 local_port: Some(local_addr.port()),
20437 local_family: Some(socket_addr_family(&local_addr).to_string()),
20438 remote_address: Some(remote_addr.ip().to_string()),
20439 remote_port: Some(remote_addr.port()),
20440 remote_family: Some(socket_addr_family(&remote_addr).to_string()),
20441 servername: None,
20442 alpn_protocol: Some(String::from("h2c")),
20443 }
20444}
20445
20446fn http2_wait_result(kind: &str, id: u64) -> Value {
20447 json!({
20448 "kind": kind,
20449 "id": id,
20450 })
20451}
20452
20453fn is_http2_terminal_event(event: &Http2BridgeEvent, is_server: bool, id: u64) -> bool {
20454 if is_server {
20455 event.kind == "serverClose" && event.id == id
20456 } else {
20457 event.kind == "sessionClose" && event.id == id
20458 }
20459}
20460
20461fn dispatch_http2_wait_loop(
20462 process: &ActiveProcess,
20463 id: u64,
20464 is_server: bool,
20465) -> Result<Value, SidecarError> {
20466 loop {
20467 if let Some(event) = wait_for_http2_event(&process.http2.shared, id, is_server, 50) {
20468 let payload = serde_json::to_value(&event).map_err(|error| {
20469 SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}"))
20470 })?;
20471 process
20472 .execution
20473 .send_javascript_stream_event("http2", payload.clone())?;
20474 if is_http2_terminal_event(&event, is_server, id) {
20475 return Ok(payload);
20476 }
20477 continue;
20478 }
20479
20480 let exists = process
20481 .http2
20482 .shared
20483 .lock()
20484 .map(|state| {
20485 if is_server {
20486 state.servers.contains_key(&id)
20487 } else {
20488 state.sessions.contains_key(&id)
20489 }
20490 })
20491 .unwrap_or(false);
20492 if !exists {
20493 return Ok(if is_server {
20494 http2_wait_result("serverClose", id)
20495 } else {
20496 http2_wait_result("sessionClose", id)
20497 });
20498 }
20499 }
20500}
20501
20502fn dispatch_http_wait_loop(process: &ActiveProcess, server_id: u64) -> Result<Value, SidecarError> {
20503 loop {
20504 if !process.http_servers.contains_key(&server_id) {
20505 return Ok(json!({
20506 "kind": "serverClose",
20507 "id": server_id,
20508 }));
20509 }
20510 thread::sleep(Duration::from_millis(25));
20511 }
20512}
20513
20514fn http2_settings_from_value(settings: &BTreeMap<String, Value>) -> BTreeMap<String, Value> {
20515 settings.clone()
20516}
20517
20518fn parse_http2_headers_json(
20519 headers_json: &str,
20520 label: &str,
20521) -> Result<BTreeMap<String, Value>, SidecarError> {
20522 serde_json::from_str::<BTreeMap<String, Value>>(headers_json)
20523 .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
20524}
20525
20526fn apply_http2_header_values(
20527 header_map: &mut HeaderMap,
20528 name: &str,
20529 value: &Value,
20530) -> Result<(), SidecarError> {
20531 let header_name = HeaderName::from_bytes(name.as_bytes()).map_err(|error| {
20532 SidecarError::InvalidState(format!("invalid HTTP/2 header name {name:?}: {error}"))
20533 })?;
20534 match value {
20535 Value::Array(values) => {
20536 for value in values {
20537 apply_http2_header_values(header_map, name, value)?;
20538 }
20539 }
20540 Value::String(text) => {
20541 let value = HeaderValue::from_str(text).map_err(|error| {
20542 SidecarError::InvalidState(format!(
20543 "invalid HTTP/2 header value for {name}: {error}"
20544 ))
20545 })?;
20546 header_map.append(header_name.clone(), value);
20547 }
20548 Value::Number(number) => {
20549 let value = HeaderValue::from_str(&number.to_string()).map_err(|error| {
20550 SidecarError::InvalidState(format!(
20551 "invalid HTTP/2 numeric header value for {name}: {error}"
20552 ))
20553 })?;
20554 header_map.append(header_name.clone(), value);
20555 }
20556 Value::Bool(boolean) => {
20557 let value = HeaderValue::from_str(if *boolean { "true" } else { "false" }).map_err(
20558 |error| {
20559 SidecarError::InvalidState(format!(
20560 "invalid HTTP/2 boolean header value for {name}: {error}"
20561 ))
20562 },
20563 )?;
20564 header_map.append(header_name.clone(), value);
20565 }
20566 Value::Null => {}
20567 Value::Object(_) => {
20568 return Err(SidecarError::InvalidState(format!(
20569 "unsupported HTTP/2 header object value for {name}"
20570 )));
20571 }
20572 }
20573 Ok(())
20574}
20575
20576fn build_http2_request(headers_json: &str) -> Result<Request<()>, SidecarError> {
20577 let headers = parse_http2_headers_json(headers_json, "HTTP/2 request headers")?;
20578 let method = headers
20579 .get(":method")
20580 .and_then(Value::as_str)
20581 .unwrap_or("GET");
20582 let path = headers.get(":path").and_then(Value::as_str).unwrap_or("/");
20583 let mut builder = Request::builder()
20584 .method(Method::from_bytes(method.as_bytes()).map_err(|error| {
20585 SidecarError::InvalidState(format!("invalid HTTP/2 method {method:?}: {error}"))
20586 })?)
20587 .uri(path.parse::<Uri>().map_err(|error| {
20588 SidecarError::InvalidState(format!("invalid HTTP/2 path {path:?}: {error}"))
20589 })?);
20590 {
20591 let header_map = builder.headers_mut().expect("request header map");
20592 for (name, value) in &headers {
20593 if name.starts_with(':') {
20594 continue;
20595 }
20596 apply_http2_header_values(header_map, name, value)?;
20597 }
20598 }
20599 builder
20600 .body(())
20601 .map_err(|error| SidecarError::InvalidState(format!("invalid HTTP/2 request: {error}")))
20602}
20603
20604fn build_http2_response(headers_json: &str) -> Result<Response<()>, SidecarError> {
20605 let headers = parse_http2_headers_json(headers_json, "HTTP/2 response headers")?;
20606 let status = headers
20607 .get(":status")
20608 .and_then(Value::as_u64)
20609 .or_else(|| {
20610 headers
20611 .get(":status")
20612 .and_then(Value::as_str)
20613 .and_then(|value| value.parse::<u16>().ok().map(u64::from))
20614 })
20615 .unwrap_or(200);
20616 let mut builder = Response::builder().status(status as u16);
20617 {
20618 let header_map = builder.headers_mut().expect("response header map");
20619 for (name, value) in &headers {
20620 if name.starts_with(':') {
20621 continue;
20622 }
20623 apply_http2_header_values(header_map, name, value)?;
20624 }
20625 }
20626 builder.body(()).map_err(|error| {
20627 SidecarError::InvalidState(format!("invalid HTTP/2 response headers: {error}"))
20628 })
20629}
20630
20631fn serialize_http2_headers_map(
20632 pseudo: BTreeMap<String, Value>,
20633 headers: &HeaderMap,
20634) -> Result<String, SidecarError> {
20635 let mut serialized = pseudo;
20636 for (name, value) in headers {
20637 let name = name.as_str().to_string();
20638 let value = Value::String(
20639 value
20640 .to_str()
20641 .map_err(|error| {
20642 SidecarError::Execution(format!("invalid HTTP/2 header value: {error}"))
20643 })?
20644 .to_owned(),
20645 );
20646 match serialized.get_mut(&name) {
20647 Some(Value::Array(values)) => values.push(value),
20648 Some(existing) => {
20649 let first = existing.clone();
20650 *existing = Value::Array(vec![first, value]);
20651 }
20652 None => {
20653 serialized.insert(name, value);
20654 }
20655 }
20656 }
20657 serde_json::to_string(&serialized)
20658 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
20659}
20660
20661fn serialize_http2_request_headers(
20662 request: &Request<h2::RecvStream>,
20663) -> Result<String, SidecarError> {
20664 let mut pseudo = BTreeMap::new();
20665 pseudo.insert(
20666 String::from(":method"),
20667 Value::String(request.method().as_str().to_string()),
20668 );
20669 pseudo.insert(
20670 String::from(":path"),
20671 Value::String(
20672 request
20673 .uri()
20674 .path_and_query()
20675 .map(|value| value.as_str().to_string())
20676 .unwrap_or_else(|| String::from("/")),
20677 ),
20678 );
20679 serialize_http2_headers_map(pseudo, request.headers())
20680}
20681
20682fn serialize_http2_response_headers(
20683 response: &Response<h2::RecvStream>,
20684) -> Result<String, SidecarError> {
20685 let mut pseudo = BTreeMap::new();
20686 pseudo.insert(
20687 String::from(":status"),
20688 Value::Number(serde_json::Number::from(response.status().as_u16())),
20689 );
20690 serialize_http2_headers_map(pseudo, response.headers())
20691}
20692
20693fn remove_http2_session_resources(
20694 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
20695 session_id: u64,
20696) {
20697 if let Ok(mut state) = shared.lock() {
20698 state.sessions.remove(&session_id);
20699 state.session_events.remove(&session_id);
20700 let stream_ids = state
20701 .streams
20702 .iter()
20703 .filter_map(|(stream_id, stream)| {
20704 (stream.session_id == session_id).then_some(*stream_id)
20705 })
20706 .collect::<Vec<_>>();
20707 for stream_id in stream_ids {
20708 state.streams.remove(&stream_id);
20709 }
20710 }
20711}
20712
20713fn spawn_http2_client_session(
20714 shared: Arc<Mutex<crate::state::Http2SharedState>>,
20715 session_id: u64,
20716 remote_addr: SocketAddr,
20717 tls: Option<JavascriptTlsBridgeOptions>,
20718 snapshot: Arc<Mutex<Http2SessionSnapshot>>,
20719 mut command_rx: UnboundedReceiver<Http2SessionCommand>,
20720) {
20721 thread::spawn(move || {
20722 let runtime = match TokioRuntimeBuilder::new_current_thread()
20723 .enable_all()
20724 .build()
20725 {
20726 Ok(runtime) => runtime,
20727 Err(error) => {
20728 push_http2_session_event(
20729 &shared,
20730 session_id,
20731 Http2BridgeEvent {
20732 kind: String::from("sessionError"),
20733 id: session_id,
20734 data: Some(http2_error_payload(error.to_string())),
20735 ..Http2BridgeEvent::default()
20736 },
20737 );
20738 remove_http2_session_resources(&shared, session_id);
20739 return;
20740 }
20741 };
20742
20743 runtime.block_on(async move {
20744 let stream = match tokio::net::TcpStream::connect(remote_addr).await {
20745 Ok(stream) => stream,
20746 Err(error) => {
20747 push_http2_session_event(
20748 &shared,
20749 session_id,
20750 Http2BridgeEvent {
20751 kind: String::from("sessionError"),
20752 id: session_id,
20753 data: Some(http2_error_payload(error.to_string())),
20754 ..Http2BridgeEvent::default()
20755 },
20756 );
20757 remove_http2_session_resources(&shared, session_id);
20758 return;
20759 }
20760 };
20761
20762 let local_addr = match stream.local_addr() {
20763 Ok(addr) => addr,
20764 Err(error) => {
20765 push_http2_session_event(
20766 &shared,
20767 session_id,
20768 Http2BridgeEvent {
20769 kind: String::from("sessionError"),
20770 id: session_id,
20771 data: Some(http2_error_payload(error.to_string())),
20772 ..Http2BridgeEvent::default()
20773 },
20774 );
20775 remove_http2_session_resources(&shared, session_id);
20776 return;
20777 }
20778 };
20779
20780 {
20781 let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
20782 snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
20783 if let Some(options) = tls.as_ref() {
20784 snapshot_guard.encrypted = true;
20785 snapshot_guard.alpn_protocol = Some(String::from("h2"));
20786 snapshot_guard.socket.encrypted = true;
20787 snapshot_guard.socket.servername = options.servername.clone();
20788 snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
20789 }
20790 snapshot_guard.state = http2_runtime_snapshot();
20791 }
20792 if let Ok(snapshot_json) =
20793 http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
20794 {
20795 push_http2_session_event(
20796 &shared,
20797 session_id,
20798 Http2BridgeEvent {
20799 kind: String::from("sessionConnect"),
20800 id: session_id,
20801 data: Some(snapshot_json),
20802 ..Http2BridgeEvent::default()
20803 },
20804 );
20805 }
20806
20807 let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
20808 let server_name = match ServerName::try_from(
20809 options
20810 .servername
20811 .clone()
20812 .unwrap_or_else(|| String::from("localhost")),
20813 ) {
20814 Ok(server_name) => server_name,
20815 Err(_) => {
20816 push_http2_session_event(
20817 &shared,
20818 session_id,
20819 Http2BridgeEvent {
20820 kind: String::from("sessionError"),
20821 id: session_id,
20822 data: Some(http2_error_payload("invalid TLS servername")),
20823 ..Http2BridgeEvent::default()
20824 },
20825 );
20826 remove_http2_session_resources(&shared, session_id);
20827 return;
20828 }
20829 };
20830 let connector = match build_client_tls_config(options) {
20831 Ok(config) => TlsConnector::from(Arc::new(config)),
20832 Err(error) => {
20833 push_http2_session_event(
20834 &shared,
20835 session_id,
20836 Http2BridgeEvent {
20837 kind: String::from("sessionError"),
20838 id: session_id,
20839 data: Some(http2_error_payload(error.to_string())),
20840 ..Http2BridgeEvent::default()
20841 },
20842 );
20843 remove_http2_session_resources(&shared, session_id);
20844 return;
20845 }
20846 };
20847 match connector.connect(server_name, stream).await {
20848 Ok(tls_stream) => Box::pin(tls_stream),
20849 Err(error) => {
20850 push_http2_session_event(
20851 &shared,
20852 session_id,
20853 Http2BridgeEvent {
20854 kind: String::from("sessionError"),
20855 id: session_id,
20856 data: Some(http2_error_payload(error.to_string())),
20857 ..Http2BridgeEvent::default()
20858 },
20859 );
20860 remove_http2_session_resources(&shared, session_id);
20861 return;
20862 }
20863 }
20864 } else {
20865 Box::pin(stream)
20866 };
20867
20868 let (mut sender, connection) = match client::handshake(io).await {
20869 Ok(parts) => parts,
20870 Err(error) => {
20871 push_http2_session_event(
20872 &shared,
20873 session_id,
20874 Http2BridgeEvent {
20875 kind: String::from("sessionError"),
20876 id: session_id,
20877 data: Some(http2_error_payload(error.to_string())),
20878 ..Http2BridgeEvent::default()
20879 },
20880 );
20881 remove_http2_session_resources(&shared, session_id);
20882 return;
20883 }
20884 };
20885
20886 let (status_tx, mut status_rx) = unbounded_channel::<Result<(), String>>();
20887 tokio::spawn(async move {
20888 let _ = status_tx.send(connection.await.map_err(|error| error.to_string()));
20889 });
20890
20891 let streams: Arc<Mutex<BTreeMap<u64, ClientHttp2StreamState>>> =
20892 Arc::new(Mutex::new(BTreeMap::new()));
20893
20894 loop {
20895 tokio::select! {
20896 Some(result) = status_rx.recv() => {
20897 if let Err(message) = result {
20898 push_http2_session_event(
20899 &shared,
20900 session_id,
20901 Http2BridgeEvent {
20902 kind: String::from("sessionError"),
20903 id: session_id,
20904 data: Some(http2_error_payload(message)),
20905 ..Http2BridgeEvent::default()
20906 },
20907 );
20908 }
20909 push_http2_session_event(
20910 &shared,
20911 session_id,
20912 Http2BridgeEvent {
20913 kind: String::from("sessionClose"),
20914 id: session_id,
20915 ..Http2BridgeEvent::default()
20916 },
20917 );
20918 remove_http2_session_resources(&shared, session_id);
20919 break;
20920 }
20921 Some(command) = command_rx.recv() => {
20922 match command {
20923 Http2SessionCommand::Request { headers_json, options_json, respond_to } => {
20924 let request = match build_http2_request(&headers_json) {
20925 Ok(request) => request,
20926 Err(error) => {
20927 let _ = respond_to.send(Err(error.to_string()));
20928 continue;
20929 }
20930 };
20931 let options: JavascriptHttp2RequestOptions =
20932 serde_json::from_str(&options_json).unwrap_or_default();
20933 let stream_id = {
20934 let mut state = shared.lock().expect("http2 shared state");
20935 let stream_id = next_http2_stream_id(&mut state);
20936 state.streams.insert(
20937 stream_id,
20938 ActiveHttp2Stream {
20939 session_id,
20940 paused: Arc::new(AtomicBool::new(false)),
20941 resume_notify: Arc::new(tokio::sync::Notify::new()),
20942 },
20943 );
20944 stream_id
20945 };
20946 match sender.send_request(request, options.end_stream) {
20947 Ok((response_future, send_stream)) => {
20948 if !options.end_stream {
20949 streams
20950 .lock()
20951 .expect("http2 client streams")
20952 .insert(stream_id, ClientHttp2StreamState { send_stream: Some(send_stream) });
20953 }
20954 let shared_clone = Arc::clone(&shared);
20955 let snapshot_clone = Arc::clone(&snapshot);
20956 tokio::spawn(async move {
20957 match response_future.await {
20958 Ok(response) => {
20959 if let Ok(headers_json) = serialize_http2_response_headers(&response) {
20960 push_http2_session_event(
20961 &shared_clone,
20962 session_id,
20963 Http2BridgeEvent {
20964 kind: String::from("clientResponseHeaders"),
20965 id: stream_id,
20966 data: Some(headers_json),
20967 ..Http2BridgeEvent::default()
20968 },
20969 );
20970 }
20971 let mut body = response.into_body();
20972 while let Some(chunk) = body.data().await {
20973 match chunk {
20974 Ok(bytes) => {
20975 let pause_state = {
20976 let state = shared_clone.lock().expect("http2 shared state");
20977 state.streams.get(&stream_id).map(|stream| {
20978 (
20979 Arc::clone(&stream.paused),
20980 Arc::clone(&stream.resume_notify),
20981 )
20982 })
20983 };
20984 if let Some((paused, resume_notify)) = pause_state {
20985 while paused.load(Ordering::SeqCst) {
20986 let notified = resume_notify.notified();
20987 tokio::pin!(notified);
20988 notified.as_mut().enable();
20989 if !paused.load(Ordering::SeqCst) { break; }
20990 let _ = tokio::time::timeout(Duration::from_millis(250), notified).await;
20991 }
20992 }
20993 let _ = body.flow_control().release_capacity(bytes.len());
20994 push_http2_session_event(
20995 &shared_clone,
20996 session_id,
20997 Http2BridgeEvent {
20998 kind: String::from("clientData"),
20999 id: stream_id,
21000 data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
21001 ..Http2BridgeEvent::default()
21002 },
21003 );
21004 }
21005 Err(error) => {
21006 push_http2_session_event(
21007 &shared_clone,
21008 session_id,
21009 Http2BridgeEvent {
21010 kind: String::from("clientError"),
21011 id: stream_id,
21012 data: Some(http2_error_payload(error.to_string())),
21013 ..Http2BridgeEvent::default()
21014 },
21015 );
21016 break;
21017 }
21018 }
21019 }
21020 {
21021 let mut snapshot = snapshot_clone.lock().expect("http2 snapshot lock");
21022 snapshot.state.next_stream_id =
21023 snapshot.state.next_stream_id.saturating_add(2);
21024 }
21025 push_http2_session_event(
21026 &shared_clone,
21027 session_id,
21028 Http2BridgeEvent {
21029 kind: String::from("clientEnd"),
21030 id: stream_id,
21031 ..Http2BridgeEvent::default()
21032 },
21033 );
21034 push_http2_session_event(
21035 &shared_clone,
21036 session_id,
21037 Http2BridgeEvent {
21038 kind: String::from("clientClose"),
21039 id: stream_id,
21040 extra_number: Some(0),
21041 ..Http2BridgeEvent::default()
21042 },
21043 );
21044 if let Ok(mut state) = shared_clone.lock() {
21045 state.streams.remove(&stream_id);
21046 }
21047 }
21048 Err(error) => {
21049 push_http2_session_event(
21050 &shared_clone,
21051 session_id,
21052 Http2BridgeEvent {
21053 kind: String::from("clientError"),
21054 id: stream_id,
21055 data: Some(http2_error_payload(error.to_string())),
21056 ..Http2BridgeEvent::default()
21057 },
21058 );
21059 push_http2_session_event(
21060 &shared_clone,
21061 session_id,
21062 Http2BridgeEvent {
21063 kind: String::from("clientClose"),
21064 id: stream_id,
21065 extra_number: Some(u32::from(Reason::INTERNAL_ERROR) as u64),
21066 ..Http2BridgeEvent::default()
21067 },
21068 );
21069 if let Ok(mut state) = shared_clone.lock() {
21070 state.streams.remove(&stream_id);
21071 }
21072 }
21073 }
21074 });
21075 let _ = respond_to.send(Ok(json!(stream_id)));
21076 }
21077 Err(error) => {
21078 if let Ok(mut state) = shared.lock() {
21079 state.streams.remove(&stream_id);
21080 }
21081 let _ = respond_to.send(Err(error.to_string()));
21082 }
21083 }
21084 }
21085 Http2SessionCommand::Settings { settings_json, respond_to } => {
21086 let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
21087 .unwrap_or_default();
21088 {
21089 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
21090 snapshot.local_settings = http2_settings_from_value(&settings);
21091 }
21092 if let Ok(headers_json) = serde_json::to_string(&settings) {
21093 push_http2_session_event(
21094 &shared,
21095 session_id,
21096 Http2BridgeEvent {
21097 kind: String::from("sessionLocalSettings"),
21098 id: session_id,
21099 data: Some(headers_json.clone()),
21100 ..Http2BridgeEvent::default()
21101 },
21102 );
21103 push_http2_session_event(
21104 &shared,
21105 session_id,
21106 Http2BridgeEvent {
21107 kind: String::from("sessionSettingsAck"),
21108 id: session_id,
21109 ..Http2BridgeEvent::default()
21110 },
21111 );
21112 }
21113 let _ = respond_to.send(Ok(Value::Null));
21114 }
21115 Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
21116 {
21117 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
21118 snapshot.state.local_window_size = size;
21119 snapshot.state.effective_local_window_size = size;
21120 }
21121 let value = snapshot
21122 .lock()
21123 .ok()
21124 .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
21125 .map(Value::String)
21126 .unwrap_or(Value::Null);
21127 let _ = respond_to.send(Ok(value));
21128 }
21129 Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
21130 push_http2_session_event(
21131 &shared,
21132 session_id,
21133 Http2BridgeEvent {
21134 kind: String::from("sessionGoaway"),
21135 id: session_id,
21136 data: opaque_data.map(|value| {
21137 base64::engine::general_purpose::STANDARD.encode(value)
21138 }),
21139 extra_number: Some(error_code as u64),
21140 flags: Some(last_stream_id as u64),
21141 ..Http2BridgeEvent::default()
21142 },
21143 );
21144 let _ = respond_to.send(Ok(Value::Null));
21145 }
21146 Http2SessionCommand::Close { respond_to, .. } => {
21147 let _ = respond_to.send(Ok(Value::Null));
21148 push_http2_session_event(
21149 &shared,
21150 session_id,
21151 Http2BridgeEvent {
21152 kind: String::from("sessionClose"),
21153 id: session_id,
21154 ..Http2BridgeEvent::default()
21155 },
21156 );
21157 remove_http2_session_resources(&shared, session_id);
21158 break;
21159 }
21160 Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
21161 let result = streams
21162 .lock()
21163 .expect("http2 client streams")
21164 .get_mut(&stream_id)
21165 .and_then(|stream| stream.send_stream.as_mut())
21166 .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 client stream {stream_id}")))
21167 .and_then(|stream| stream.send_data(Bytes::from(chunk), end_stream).map_err(|error| SidecarError::Execution(error.to_string())));
21168 match result {
21169 Ok(()) => {
21170 if end_stream {
21171 streams.lock().expect("http2 client streams").remove(&stream_id);
21172 }
21173 let _ = respond_to.send(Ok(Value::Bool(true)));
21174 }
21175 Err(error) => {
21176 let _ = respond_to.send(Err(error.to_string()));
21177 }
21178 }
21179 }
21180 Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
21181 let mut streams = streams.lock().expect("http2 client streams");
21182 let Some(mut state) = streams.remove(&stream_id) else {
21183 let _ = respond_to.send(Err(format!("unknown HTTP/2 client stream {stream_id}")));
21184 continue;
21185 };
21186 if let Some(stream) = state.send_stream.as_mut() {
21187 stream.send_reset(http2_reason(error_code));
21188 }
21189 if let Ok(mut state) = shared.lock() {
21190 state.streams.remove(&stream_id);
21191 }
21192 push_http2_session_event(
21193 &shared,
21194 session_id,
21195 Http2BridgeEvent {
21196 kind: String::from("clientClose"),
21197 id: stream_id,
21198 extra_number: Some(u32::from(http2_reason(error_code)) as u64),
21199 ..Http2BridgeEvent::default()
21200 },
21201 );
21202 let _ = respond_to.send(Ok(Value::Null));
21203 }
21204 Http2SessionCommand::StreamRespond { respond_to, .. }
21205 | Http2SessionCommand::StreamPush { respond_to, .. }
21206 | Http2SessionCommand::StreamRespondWithFile { respond_to, .. } => {
21207 let _ = respond_to.send(Err(String::from("HTTP/2 client streams cannot send server responses")));
21208 }
21209 }
21210 }
21211 else => break,
21212 }
21213 }
21214 });
21215 });
21216}
21217
21218fn spawn_http2_server_session(
21219 shared: Arc<Mutex<crate::state::Http2SharedState>>,
21220 server_id: u64,
21221 session_id: u64,
21222 stream: TcpStream,
21223 tls: Option<JavascriptTlsBridgeOptions>,
21224 snapshot: Arc<Mutex<Http2SessionSnapshot>>,
21225 mut command_rx: UnboundedReceiver<Http2SessionCommand>,
21226) {
21227 thread::spawn(move || {
21228 let runtime = match TokioRuntimeBuilder::new_current_thread()
21229 .enable_all()
21230 .build()
21231 {
21232 Ok(runtime) => runtime,
21233 Err(error) => {
21234 push_http2_server_event(
21235 &shared,
21236 server_id,
21237 Http2BridgeEvent {
21238 kind: String::from("serverStreamError"),
21239 id: session_id,
21240 data: Some(http2_error_payload(error.to_string())),
21241 ..Http2BridgeEvent::default()
21242 },
21243 );
21244 remove_http2_session_resources(&shared, session_id);
21245 return;
21246 }
21247 };
21248
21249 runtime.block_on(async move {
21250 if let Err(error) = stream.set_nonblocking(true) {
21251 push_http2_server_event(
21252 &shared,
21253 server_id,
21254 Http2BridgeEvent {
21255 kind: String::from("serverStreamError"),
21256 id: session_id,
21257 data: Some(http2_error_payload(error.to_string())),
21258 ..Http2BridgeEvent::default()
21259 },
21260 );
21261 remove_http2_session_resources(&shared, session_id);
21262 return;
21263 }
21264 let stream = match tokio::net::TcpStream::from_std(stream) {
21265 Ok(stream) => stream,
21266 Err(error) => {
21267 push_http2_server_event(
21268 &shared,
21269 server_id,
21270 Http2BridgeEvent {
21271 kind: String::from("serverStreamError"),
21272 id: session_id,
21273 data: Some(http2_error_payload(error.to_string())),
21274 ..Http2BridgeEvent::default()
21275 },
21276 );
21277 remove_http2_session_resources(&shared, session_id);
21278 return;
21279 }
21280 };
21281 let local_addr = match stream.local_addr() {
21282 Ok(addr) => addr,
21283 Err(error) => {
21284 push_http2_server_event(
21285 &shared,
21286 server_id,
21287 Http2BridgeEvent {
21288 kind: String::from("serverStreamError"),
21289 id: session_id,
21290 data: Some(http2_error_payload(error.to_string())),
21291 ..Http2BridgeEvent::default()
21292 },
21293 );
21294 remove_http2_session_resources(&shared, session_id);
21295 return;
21296 }
21297 };
21298 let remote_addr = match stream.peer_addr() {
21299 Ok(addr) => addr,
21300 Err(error) => {
21301 push_http2_server_event(
21302 &shared,
21303 server_id,
21304 Http2BridgeEvent {
21305 kind: String::from("serverStreamError"),
21306 id: session_id,
21307 data: Some(http2_error_payload(error.to_string())),
21308 ..Http2BridgeEvent::default()
21309 },
21310 );
21311 remove_http2_session_resources(&shared, session_id);
21312 return;
21313 }
21314 };
21315 {
21316 let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
21317 snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
21318 if tls.is_some() {
21319 snapshot_guard.encrypted = true;
21320 snapshot_guard.alpn_protocol = Some(String::from("h2"));
21321 snapshot_guard.socket.encrypted = true;
21322 snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
21323 }
21324 snapshot_guard.state = http2_runtime_snapshot();
21325 }
21326 if let Ok(snapshot_json) =
21327 http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
21328 {
21329 push_http2_server_event(
21330 &shared,
21331 server_id,
21332 Http2BridgeEvent {
21333 kind: String::from(if tls.is_some() {
21334 "serverSecureConnection"
21335 } else {
21336 "serverConnection"
21337 }),
21338 id: server_id,
21339 data: Some(serde_json::to_string(&http2_socket_snapshot(local_addr, remote_addr)).unwrap_or_default()),
21340 ..Http2BridgeEvent::default()
21341 },
21342 );
21343 push_http2_server_event(
21344 &shared,
21345 server_id,
21346 Http2BridgeEvent {
21347 kind: String::from("serverSession"),
21348 id: server_id,
21349 data: Some(snapshot_json),
21350 extra_number: Some(session_id),
21351 ..Http2BridgeEvent::default()
21352 },
21353 );
21354 }
21355
21356 let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
21357 let acceptor = match build_server_tls_config(options) {
21358 Ok(config) => TlsAcceptor::from(Arc::new(config)),
21359 Err(error) => {
21360 push_http2_server_event(
21361 &shared,
21362 server_id,
21363 Http2BridgeEvent {
21364 kind: String::from("serverStreamError"),
21365 id: session_id,
21366 data: Some(http2_error_payload(error.to_string())),
21367 ..Http2BridgeEvent::default()
21368 },
21369 );
21370 remove_http2_session_resources(&shared, session_id);
21371 return;
21372 }
21373 };
21374 match acceptor.accept(stream).await {
21375 Ok(tls_stream) => Box::pin(tls_stream),
21376 Err(error) => {
21377 push_http2_server_event(
21378 &shared,
21379 server_id,
21380 Http2BridgeEvent {
21381 kind: String::from("serverStreamError"),
21382 id: session_id,
21383 data: Some(http2_error_payload(error.to_string())),
21384 ..Http2BridgeEvent::default()
21385 },
21386 );
21387 remove_http2_session_resources(&shared, session_id);
21388 return;
21389 }
21390 }
21391 } else {
21392 Box::pin(stream)
21393 };
21394
21395 let mut connection = match server::handshake(io).await {
21396 Ok(connection) => connection,
21397 Err(error) => {
21398 push_http2_server_event(
21399 &shared,
21400 server_id,
21401 Http2BridgeEvent {
21402 kind: String::from("serverStreamError"),
21403 id: session_id,
21404 data: Some(http2_error_payload(error.to_string())),
21405 ..Http2BridgeEvent::default()
21406 },
21407 );
21408 remove_http2_session_resources(&shared, session_id);
21409 return;
21410 }
21411 };
21412
21413 let streams: Arc<Mutex<BTreeMap<u64, ServerHttp2StreamState>>> =
21414 Arc::new(Mutex::new(BTreeMap::new()));
21415
21416 loop {
21417 tokio::select! {
21418 incoming = connection.accept() => {
21419 match incoming {
21420 Some(Ok((request, respond))) => {
21421 let headers_json = match serialize_http2_request_headers(&request) {
21422 Ok(headers) => headers,
21423 Err(error) => {
21424 push_http2_server_event(
21425 &shared,
21426 server_id,
21427 Http2BridgeEvent {
21428 kind: String::from("serverStreamError"),
21429 id: server_id,
21430 data: Some(http2_error_payload(error.to_string())),
21431 ..Http2BridgeEvent::default()
21432 },
21433 );
21434 continue;
21435 }
21436 };
21437 let stream_id = {
21438 let mut state = shared.lock().expect("http2 shared state");
21439 let stream_id = next_http2_stream_id(&mut state);
21440 state.streams.insert(
21441 stream_id,
21442 ActiveHttp2Stream {
21443 session_id,
21444 paused: Arc::new(AtomicBool::new(false)),
21445 resume_notify: Arc::new(tokio::sync::Notify::new()),
21446 },
21447 );
21448 stream_id
21449 };
21450 streams.lock().expect("http2 server streams").insert(
21451 stream_id,
21452 ServerHttp2StreamState {
21453 send_response: Some(ServerHttp2Responder::Regular(respond)),
21454 send_stream: None,
21455 },
21456 );
21457 let snapshot_json = snapshot
21458 .lock()
21459 .ok()
21460 .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok());
21461 push_http2_server_event(
21462 &shared,
21463 server_id,
21464 Http2BridgeEvent {
21465 kind: String::from("serverStream"),
21466 id: server_id,
21467 data: Some(stream_id.to_string()),
21468 extra: snapshot_json,
21469 extra_number: Some(session_id),
21470 extra_headers: Some(headers_json),
21471 flags: Some(0),
21472 },
21473 );
21474 let shared_clone = Arc::clone(&shared);
21475 tokio::spawn(async move {
21476 let mut body = request.into_body();
21477 while let Some(chunk) = body.data().await {
21478 match chunk {
21479 Ok(bytes) => {
21480 let pause_state = {
21481 let state = shared_clone.lock().expect("http2 shared state");
21482 state.streams.get(&stream_id).map(|stream| {
21483 (
21484 Arc::clone(&stream.paused),
21485 Arc::clone(&stream.resume_notify),
21486 )
21487 })
21488 };
21489 if let Some((paused, resume_notify)) = pause_state {
21490 while paused.load(Ordering::SeqCst) {
21491 let notified = resume_notify.notified();
21492 tokio::pin!(notified);
21493 notified.as_mut().enable();
21494 if !paused.load(Ordering::SeqCst) { break; }
21495 let _ = tokio::time::timeout(Duration::from_millis(250), notified).await;
21496 }
21497 }
21498 let _ = body.flow_control().release_capacity(bytes.len());
21499 push_http2_server_event(
21500 &shared_clone,
21501 server_id,
21502 Http2BridgeEvent {
21503 kind: String::from("serverStreamData"),
21504 id: stream_id,
21505 data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
21506 ..Http2BridgeEvent::default()
21507 },
21508 );
21509 }
21510 Err(error) => {
21511 push_http2_server_event(
21512 &shared_clone,
21513 server_id,
21514 Http2BridgeEvent {
21515 kind: String::from("serverStreamError"),
21516 id: stream_id,
21517 data: Some(http2_error_payload(error.to_string())),
21518 ..Http2BridgeEvent::default()
21519 },
21520 );
21521 break;
21522 }
21523 }
21524 }
21525 push_http2_server_event(
21526 &shared_clone,
21527 server_id,
21528 Http2BridgeEvent {
21529 kind: String::from("serverStreamEnd"),
21530 id: stream_id,
21531 ..Http2BridgeEvent::default()
21532 },
21533 );
21534 });
21535 }
21536 Some(Err(error)) => {
21537 push_http2_server_event(
21538 &shared,
21539 server_id,
21540 Http2BridgeEvent {
21541 kind: String::from("serverStreamError"),
21542 id: server_id,
21543 data: Some(http2_error_payload(error.to_string())),
21544 ..Http2BridgeEvent::default()
21545 },
21546 );
21547 break;
21548 }
21549 None => {
21550 push_http2_server_event(
21551 &shared,
21552 server_id,
21553 Http2BridgeEvent {
21554 kind: String::from("sessionClose"),
21555 id: session_id,
21556 ..Http2BridgeEvent::default()
21557 },
21558 );
21559 remove_http2_session_resources(&shared, session_id);
21560 break;
21561 }
21562 }
21563 }
21564 Some(command) = command_rx.recv() => {
21565 match command {
21566 Http2SessionCommand::Settings { settings_json, respond_to } => {
21567 let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
21568 .unwrap_or_default();
21569 if let Some(initial_window_size) = settings
21570 .get("initialWindowSize")
21571 .and_then(Value::as_u64)
21572 {
21573 let _ = connection.set_initial_window_size(initial_window_size as u32);
21574 }
21575 {
21576 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
21577 snapshot.local_settings = http2_settings_from_value(&settings);
21578 }
21579 if let Ok(headers_json) = serde_json::to_string(&settings) {
21580 push_http2_session_event(
21581 &shared,
21582 session_id,
21583 Http2BridgeEvent {
21584 kind: String::from("sessionLocalSettings"),
21585 id: session_id,
21586 data: Some(headers_json),
21587 ..Http2BridgeEvent::default()
21588 },
21589 );
21590 }
21591 let _ = respond_to.send(Ok(Value::Null));
21592 }
21593 Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
21594 connection.set_target_window_size(size);
21595 {
21596 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
21597 snapshot.state.local_window_size = size;
21598 snapshot.state.effective_local_window_size = size;
21599 }
21600 let value = snapshot
21601 .lock()
21602 .ok()
21603 .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
21604 .map(Value::String)
21605 .unwrap_or(Value::Null);
21606 let _ = respond_to.send(Ok(value));
21607 }
21608 Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
21609 connection.abrupt_shutdown(http2_reason(Some(error_code)));
21610 push_http2_session_event(
21611 &shared,
21612 session_id,
21613 Http2BridgeEvent {
21614 kind: String::from("sessionGoaway"),
21615 id: session_id,
21616 data: opaque_data.map(|value| {
21617 base64::engine::general_purpose::STANDARD.encode(value)
21618 }),
21619 extra_number: Some(error_code as u64),
21620 flags: Some(last_stream_id as u64),
21621 ..Http2BridgeEvent::default()
21622 },
21623 );
21624 let _ = respond_to.send(Ok(Value::Null));
21625 }
21626 Http2SessionCommand::Close { abrupt, respond_to } => {
21627 if abrupt {
21628 connection.abrupt_shutdown(Reason::NO_ERROR);
21629 } else {
21630 connection.graceful_shutdown();
21631 }
21632 let _ = respond_to.send(Ok(Value::Null));
21633 push_http2_session_event(
21634 &shared,
21635 session_id,
21636 Http2BridgeEvent {
21637 kind: String::from("sessionClose"),
21638 id: session_id,
21639 ..Http2BridgeEvent::default()
21640 },
21641 );
21642 remove_http2_session_resources(&shared, session_id);
21643 break;
21644 }
21645 Http2SessionCommand::StreamRespond { stream_id, headers_json, respond_to } => {
21646 let response = match build_http2_response(&headers_json) {
21647 Ok(response) => response,
21648 Err(error) => {
21649 let _ = respond_to.send(Err(error.to_string()));
21650 continue;
21651 }
21652 };
21653 let mut streams = streams.lock().expect("http2 server streams");
21654 let Some(state) = streams.get_mut(&stream_id) else {
21655 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
21656 continue;
21657 };
21658 let Some(send_response) = state.send_response.as_mut() else {
21659 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
21660 continue;
21661 };
21662 match match send_response {
21663 ServerHttp2Responder::Regular(send_response) => {
21664 send_response.send_response(response, false)
21665 }
21666 ServerHttp2Responder::Pushed(send_response) => {
21667 send_response.send_response(response, false)
21668 }
21669 } {
21670 Ok(send_stream) => {
21671 state.send_stream = Some(send_stream);
21672 state.send_response = None;
21673 let _ = respond_to.send(Ok(Value::Null));
21674 }
21675 Err(error) => {
21676 let _ = respond_to.send(Err(error.to_string()));
21677 }
21678 }
21679 }
21680 Http2SessionCommand::StreamPush { stream_id, headers_json, respond_to } => {
21681 let request = match build_http2_request(&headers_json) {
21682 Ok(request) => request,
21683 Err(error) => {
21684 let _ = respond_to.send(Err(error.to_string()));
21685 continue;
21686 }
21687 };
21688 let mut streams_guard = streams.lock().expect("http2 server streams");
21689 let Some(state) = streams_guard.get_mut(&stream_id) else {
21690 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
21691 continue;
21692 };
21693 let Some(send_response) = state.send_response.as_mut() else {
21694 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} cannot push after responding")));
21695 continue;
21696 };
21697 let ServerHttp2Responder::Regular(send_response) = send_response else {
21698 let _ = respond_to.send(Err(format!("HTTP/2 pushed stream {stream_id} cannot create nested push promises")));
21699 continue;
21700 };
21701 match send_response.push_request(request) {
21702 Ok(pushed) => {
21703 let pushed_stream_id = {
21704 let mut state = shared.lock().expect("http2 shared state");
21705 let pushed_stream_id = next_http2_stream_id(&mut state);
21706 state.streams.insert(
21707 pushed_stream_id,
21708 ActiveHttp2Stream {
21709 session_id,
21710 paused: Arc::new(AtomicBool::new(false)),
21711 resume_notify: Arc::new(tokio::sync::Notify::new()),
21712 },
21713 );
21714 pushed_stream_id
21715 };
21716 streams_guard.insert(
21717 pushed_stream_id,
21718 ServerHttp2StreamState {
21719 send_response: Some(ServerHttp2Responder::Pushed(pushed)),
21720 send_stream: None,
21721 },
21722 );
21723 let _ = respond_to.send(Ok(json!({
21724 "streamId": pushed_stream_id,
21725 "headers": headers_json,
21726 }).to_string().into()));
21727 }
21728 Err(error) => {
21729 let _ = respond_to.send(Err(error.to_string()));
21730 }
21731 }
21732 }
21733 Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
21734 let mut streams = streams.lock().expect("http2 server streams");
21735 let Some(state) = streams.get_mut(&stream_id) else {
21736 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
21737 continue;
21738 };
21739 let Some(send_stream) = state.send_stream.as_mut() else {
21740 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} has not sent response headers")));
21741 continue;
21742 };
21743 match send_stream.send_data(Bytes::from(chunk), end_stream) {
21744 Ok(()) => {
21745 if end_stream {
21746 streams.remove(&stream_id);
21747 if let Ok(mut state) = shared.lock() {
21748 state.streams.remove(&stream_id);
21749 }
21750 push_http2_server_event(
21751 &shared,
21752 server_id,
21753 Http2BridgeEvent {
21754 kind: String::from("serverStreamClose"),
21755 id: stream_id,
21756 extra_number: Some(0),
21757 ..Http2BridgeEvent::default()
21758 },
21759 );
21760 }
21761 let _ = respond_to.send(Ok(Value::Bool(true)));
21762 }
21763 Err(error) => {
21764 let _ = respond_to.send(Err(error.to_string()));
21765 }
21766 }
21767 }
21768 Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
21769 let mut streams_guard = streams.lock().expect("http2 server streams");
21770 let Some(mut state) = streams_guard.remove(&stream_id) else {
21771 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
21772 continue;
21773 };
21774 let reason = http2_reason(error_code);
21775 if let Some(send_stream) = state.send_stream.as_mut() {
21776 send_stream.send_reset(reason);
21777 }
21778 if let Some(send_response) = state.send_response.as_mut() {
21779 match send_response {
21780 ServerHttp2Responder::Regular(send_response) => {
21781 send_response.send_reset(reason)
21782 }
21783 ServerHttp2Responder::Pushed(send_response) => {
21784 send_response.send_reset(reason)
21785 }
21786 }
21787 }
21788 if let Ok(mut shared_guard) = shared.lock() {
21789 shared_guard.streams.remove(&stream_id);
21790 }
21791 push_http2_server_event(
21792 &shared,
21793 server_id,
21794 Http2BridgeEvent {
21795 kind: String::from("serverStreamClose"),
21796 id: stream_id,
21797 extra_number: Some(u32::from(reason) as u64),
21798 ..Http2BridgeEvent::default()
21799 },
21800 );
21801 let _ = respond_to.send(Ok(Value::Null));
21802 }
21803 Http2SessionCommand::StreamRespondWithFile { stream_id, body, headers_json, options_json, respond_to } => {
21804 let options: JavascriptHttp2FileResponseOptions =
21805 serde_json::from_str(&options_json).unwrap_or_default();
21806 let response = match build_http2_response(&headers_json) {
21807 Ok(response) => response,
21808 Err(error) => {
21809 let _ = respond_to.send(Err(error.to_string()));
21810 continue;
21811 }
21812 };
21813 let offset = usize::try_from(options.offset.unwrap_or_default()).unwrap_or(0);
21814 let body = if offset >= body.len() {
21815 Vec::new()
21816 } else {
21817 let body = &body[offset..];
21818 match options.length {
21819 Some(length) if length >= 0 => {
21820 body[..body.len().min(length as usize)].to_vec()
21821 }
21822 _ => body.to_vec(),
21823 }
21824 };
21825 let mut streams_guard = streams.lock().expect("http2 server streams");
21826 let Some(state) = streams_guard.get_mut(&stream_id) else {
21827 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
21828 continue;
21829 };
21830 let Some(send_response) = state.send_response.as_mut() else {
21831 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
21832 continue;
21833 };
21834 match match send_response {
21835 ServerHttp2Responder::Regular(send_response) => {
21836 send_response.send_response(response, body.is_empty())
21837 }
21838 ServerHttp2Responder::Pushed(send_response) => {
21839 send_response.send_response(response, body.is_empty())
21840 }
21841 } {
21842 Ok(mut send_stream) => {
21843 state.send_response = None;
21844 if body.is_empty() {
21845 streams_guard.remove(&stream_id);
21846 if let Ok(mut shared_guard) = shared.lock() {
21847 shared_guard.streams.remove(&stream_id);
21848 }
21849 } else {
21850 if let Err(error) = send_stream.send_data(Bytes::from(body), true) {
21851 let _ = respond_to.send(Err(error.to_string()));
21852 continue;
21853 }
21854 streams_guard.remove(&stream_id);
21855 if let Ok(mut shared_guard) = shared.lock() {
21856 shared_guard.streams.remove(&stream_id);
21857 }
21858 }
21859 push_http2_server_event(
21860 &shared,
21861 server_id,
21862 Http2BridgeEvent {
21863 kind: String::from("serverStreamClose"),
21864 id: stream_id,
21865 extra_number: Some(0),
21866 ..Http2BridgeEvent::default()
21867 },
21868 );
21869 let _ = respond_to.send(Ok(Value::Null));
21870 }
21871 Err(error) => {
21872 let _ = respond_to.send(Err(error.to_string()));
21873 }
21874 }
21875 }
21876 Http2SessionCommand::Request { respond_to, .. } => {
21877 let _ = respond_to.send(Err(String::from("HTTP/2 server sessions cannot initiate client requests")));
21878 }
21879 }
21880 }
21881 else => break,
21882 }
21883 }
21884 });
21885 });
21886}
21887
21888fn spawn_http2_server_accept_loop(
21889 shared: Arc<Mutex<crate::state::Http2SharedState>>,
21890 server_id: u64,
21891 listener: TcpListener,
21892) {
21893 thread::spawn(move || {
21894 let listener = listener;
21895 loop {
21896 let closed = shared
21897 .lock()
21898 .ok()
21899 .and_then(|state| {
21900 state
21901 .servers
21902 .get(&server_id)
21903 .map(|server| server.closed.load(Ordering::SeqCst))
21904 })
21905 .unwrap_or(true);
21906 if closed {
21907 break;
21908 }
21909 match listener.accept() {
21910 Ok((stream, _)) => {
21911 let (command_tx, command_rx) = unbounded_channel();
21912 let (guest_local_addr, secure, tls) = {
21913 let state = shared.lock().expect("http2 shared state");
21914 let server = state.servers.get(&server_id).expect("http2 server state");
21915 (server.guest_local_addr, server.secure, server.tls.clone())
21916 };
21917 let (local_addr, remote_addr) = match (stream.local_addr(), stream.peer_addr())
21918 {
21919 (Ok(local_addr), Ok(remote_addr)) => (local_addr, remote_addr),
21920 _ => continue,
21921 };
21922 let session_snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
21923 encrypted: secure,
21924 alpn_protocol: Some(if secure {
21925 String::from("h2")
21926 } else {
21927 String::from("h2c")
21928 }),
21929 local_settings: BTreeMap::new(),
21930 remote_settings: BTreeMap::new(),
21931 state: http2_runtime_snapshot(),
21932 socket: Http2SocketSnapshot {
21933 local_address: Some(guest_local_addr.ip().to_string()),
21934 local_port: Some(guest_local_addr.port()),
21935 local_family: Some(socket_addr_family(&guest_local_addr).to_string()),
21936 remote_address: Some(remote_addr.ip().to_string()),
21937 remote_port: Some(remote_addr.port()),
21938 remote_family: Some(socket_addr_family(&remote_addr).to_string()),
21939 ..http2_socket_snapshot(local_addr, remote_addr)
21940 },
21941 ..Http2SessionSnapshot::default()
21942 }));
21943 let session_id = {
21944 let mut state = shared.lock().expect("http2 shared state");
21945 let session_id = next_http2_session_id(&mut state);
21946 state
21947 .sessions
21948 .insert(session_id, ActiveHttp2Session { command_tx });
21949 session_id
21950 };
21951 spawn_http2_server_session(
21952 Arc::clone(&shared),
21953 server_id,
21954 session_id,
21955 stream,
21956 tls,
21957 session_snapshot,
21958 command_rx,
21959 );
21960 }
21961 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
21962 wait_fd_readable_until(
21963 listener.as_fd(),
21964 Instant::now() + Duration::from_millis(100),
21965 );
21966 }
21967 Err(error) => {
21968 push_http2_server_event(
21969 &shared,
21970 server_id,
21971 Http2BridgeEvent {
21972 kind: String::from("serverStreamError"),
21973 id: server_id,
21974 data: Some(http2_error_payload(error.to_string())),
21975 ..Http2BridgeEvent::default()
21976 },
21977 );
21978 thread::sleep(HTTP2_POLL_DELAY);
21981 }
21982 }
21983 }
21984 });
21985}
21986
21987fn send_http2_command(
21988 session: &ActiveHttp2Session,
21989 command: impl FnOnce(Sender<Result<Value, String>>) -> Http2SessionCommand,
21990) -> Result<Value, SidecarError> {
21991 let (respond_to, response_rx) = mpsc::channel();
21992 session.command_tx.send(command(respond_to)).map_err(|_| {
21993 SidecarError::InvalidState(String::from("HTTP/2 session command channel closed"))
21994 })?;
21995 response_rx
21996 .recv_timeout(Duration::from_secs(30))
21997 .map_err(|_| {
21998 SidecarError::Execution(String::from("timed out waiting for HTTP/2 session command"))
21999 })?
22000 .map_err(SidecarError::Execution)
22001}
22002
22003fn parse_http2_server_listen_payload(
22004 request: &JavascriptSyncRpcRequest,
22005) -> Result<JavascriptHttp2ServerListenRequest, SidecarError> {
22006 let payload_json =
22007 javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_server_listen payload")?;
22008 serde_json::from_str(payload_json).map_err(|error| {
22009 SidecarError::InvalidState(format!(
22010 "net.http2_server_listen payload must be valid JSON: {error}"
22011 ))
22012 })
22013}
22014
22015fn parse_http2_connect_payload(
22016 request: &JavascriptSyncRpcRequest,
22017) -> Result<JavascriptHttp2SessionConnectRequest, SidecarError> {
22018 let payload_json =
22019 javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_session_connect payload")?;
22020 serde_json::from_str(payload_json).map_err(|error| {
22021 SidecarError::InvalidState(format!(
22022 "net.http2_session_connect payload must be valid JSON: {error}"
22023 ))
22024 })
22025}
22026
22027fn http2_session_for_id(
22028 process: &ActiveProcess,
22029 session_id: u64,
22030) -> Result<ActiveHttp2Session, SidecarError> {
22031 let shared = process
22032 .http2
22033 .shared
22034 .lock()
22035 .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
22036 shared
22037 .sessions
22038 .get(&session_id)
22039 .cloned()
22040 .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 session {session_id}")))
22041}
22042
22043fn http2_stream_for_id(
22044 process: &ActiveProcess,
22045 stream_id: u64,
22046) -> Result<ActiveHttp2Stream, SidecarError> {
22047 let shared = process
22048 .http2
22049 .shared
22050 .lock()
22051 .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
22052 shared
22053 .streams
22054 .get(&stream_id)
22055 .cloned()
22056 .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 stream {stream_id}")))
22057}
22058
22059fn service_javascript_http2_sync_rpc<B>(
22060 request: JavascriptHttp2SyncRpcServiceRequest<'_, B>,
22061) -> Result<Value, SidecarError>
22062where
22063 B: NativeSidecarBridge + Send + 'static,
22064 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
22065{
22066 let JavascriptHttp2SyncRpcServiceRequest {
22067 bridge,
22068 kernel,
22069 vm_id,
22070 dns,
22071 socket_paths,
22072 process,
22073 sync_request: request,
22074 resource_limits,
22075 network_counts,
22076 } = request;
22077 match request.method.as_str() {
22078 "net.http2_server_listen" => {
22079 check_network_resource_limit(
22080 resource_limits.max_sockets,
22081 network_counts.sockets,
22082 1,
22083 "socket",
22084 )?;
22085 let payload = parse_http2_server_listen_payload(request)?;
22086 let (family, bind_host, guest_host) =
22087 normalize_tcp_listen_host(payload.host.as_deref())?;
22088 let requested_port = payload.port.unwrap_or(0);
22089 bridge.require_network_access(
22090 vm_id,
22091 NetworkOperation::Listen,
22092 format_tcp_resource(bind_host, requested_port),
22093 )?;
22094 let port = allocate_guest_listen_port(
22095 requested_port,
22096 family,
22097 &socket_paths.used_tcp_guest_ports,
22098 socket_paths.listen_policy,
22099 )?;
22100 let mut listener =
22101 ActiveTcpListener::bind(bind_host, guest_host, port, payload.backlog)?;
22102 let guest_local_addr = listener.guest_local_addr();
22103 let closed = Arc::new(AtomicBool::new(false));
22104 {
22105 let mut state = process.http2.shared.lock().map_err(|_| {
22106 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
22107 })?;
22108 state.servers.insert(
22109 payload.server_id,
22110 ActiveHttp2Server {
22111 actual_local_addr: listener.local_addr(),
22112 guest_local_addr,
22113 secure: payload.secure,
22114 tls: payload.tls.clone().map(|mut tls| {
22115 tls.is_server = payload.secure;
22116 if payload.secure && tls.alpn_protocols.is_none() {
22117 tls.alpn_protocols = Some(vec![String::from("h2")]);
22118 }
22119 tls
22120 }),
22121 closed: Arc::clone(&closed),
22122 },
22123 );
22124 if state.event_session.is_none() {
22125 state.event_session = process.execution.javascript_v8_session_handle();
22126 }
22127 state.server_events.entry(payload.server_id).or_default();
22128 }
22129 spawn_http2_server_accept_loop(
22130 Arc::clone(&process.http2.shared),
22131 payload.server_id,
22132 listener.listener.take().ok_or_else(|| {
22133 SidecarError::InvalidState(String::from(
22134 "HTTP/2 listener missing host TCP socket",
22135 ))
22136 })?,
22137 );
22138 javascript_net_json_string(
22139 json!({
22140 "address": socket_address_value(&guest_local_addr)
22141 }),
22142 "net.http2_server_listen",
22143 )
22144 }
22145 "net.http2_server_poll" => {
22146 let server_id =
22147 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_poll server id")?;
22148 let wait_ms = javascript_sync_rpc_arg_u64_optional(
22149 &request.args,
22150 1,
22151 "net.http2_server_poll wait ms",
22152 )?
22153 .unwrap_or_default();
22154 match wait_for_http2_event(&process.http2.shared, server_id, true, wait_ms) {
22155 Some(event) => http2_event_value(&event),
22156 None => Ok(Value::Null),
22157 }
22158 }
22159 "net.http2_server_wait" => {
22160 let server_id =
22161 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_wait server id")?;
22162 dispatch_http2_wait_loop(process, server_id, true)
22163 }
22164 "net.http2_server_close" => {
22165 let server_id =
22166 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_close server id")?;
22167 let server = {
22168 let mut state = process.http2.shared.lock().map_err(|_| {
22169 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
22170 })?;
22171 state.servers.remove(&server_id)
22172 }
22173 .ok_or_else(|| {
22174 SidecarError::InvalidState(format!("unknown HTTP/2 server {server_id}"))
22175 })?;
22176 server.closed.store(true, Ordering::SeqCst);
22177 push_http2_server_event(
22178 &process.http2.shared,
22179 server_id,
22180 Http2BridgeEvent {
22181 kind: String::from("serverClose"),
22182 id: server_id,
22183 ..Http2BridgeEvent::default()
22184 },
22185 );
22186 Ok(Value::Null)
22187 }
22188 "net.http2_server_respond" => {
22189 let server_id = javascript_sync_rpc_arg_u64(
22190 &request.args,
22191 0,
22192 "net.http2_server_respond server id",
22193 )?;
22194 let request_id = javascript_sync_rpc_arg_u64(
22195 &request.args,
22196 1,
22197 "net.http2_server_respond request id",
22198 )?;
22199 let response_json =
22200 javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_server_respond payload")?;
22201 ensure_vm_fetch_response_within_limit(
22202 response_json,
22203 "net.http2_server_respond",
22204 VM_FETCH_BUFFER_LIMIT_BYTES,
22205 )
22206 .map_err(sidecar_core_execution_error)?;
22207 serde_json::from_str::<Value>(response_json).map_err(|error| {
22208 SidecarError::Execution(format!(
22209 "net.http2_server_respond payload must be valid JSON: {error}"
22210 ))
22211 })?;
22212 let Some(pending) = process
22213 .pending_http_requests
22214 .get_mut(&(server_id, request_id))
22215 else {
22216 return Err(SidecarError::InvalidState(format!(
22217 "unknown pending HTTP/2 request {request_id} for server {server_id}"
22218 )));
22219 };
22220 *pending = Some(response_json.to_owned());
22221 Ok(Value::Bool(true))
22222 }
22223 "net.http2_session_connect" => {
22224 check_network_resource_limit(
22225 resource_limits.max_sockets,
22226 network_counts.sockets,
22227 1,
22228 "socket",
22229 )?;
22230 check_network_resource_limit(
22231 resource_limits.max_connections,
22232 network_counts.connections,
22233 1,
22234 "connection",
22235 )?;
22236 let payload = parse_http2_connect_payload(request)?;
22237 let authority = payload.authority.clone().unwrap_or_else(|| {
22238 format!(
22239 "{}://{}:{}",
22240 payload.protocol.as_deref().unwrap_or("http"),
22241 payload.host.as_deref().unwrap_or("localhost"),
22242 payload.port.unwrap_or(80)
22243 )
22244 });
22245 let url = Url::parse(&authority).map_err(|error| {
22246 SidecarError::InvalidState(format!(
22247 "invalid HTTP/2 authority {authority:?}: {error}"
22248 ))
22249 })?;
22250 let secure = url.scheme() == "https" || payload.protocol.as_deref() == Some("https:");
22251 let host = payload
22252 .host
22253 .as_deref()
22254 .or_else(|| url.host_str())
22255 .unwrap_or("localhost");
22256 let port = payload.port.or_else(|| url.port()).unwrap_or(80);
22257 bridge.require_network_access(
22258 vm_id,
22259 NetworkOperation::Http,
22260 format_tcp_resource(host, port),
22261 )?;
22262 let resolved = {
22263 let shared = process.http2.shared.lock().map_err(|_| {
22264 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
22265 })?;
22266 shared
22267 .servers
22268 .values()
22269 .find(|server| {
22270 is_loopback_request_host(host) && server.guest_local_addr.port() == port
22271 })
22272 .map(|server| ResolvedTcpConnectAddr {
22273 actual_addr: server.actual_local_addr,
22274 guest_remote_addr: server.guest_local_addr,
22275 use_kernel_loopback: false,
22276 })
22277 };
22278 let resolved = match resolved {
22279 Some(resolved) => resolved,
22280 None => {
22281 resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, socket_paths)?
22282 }
22283 };
22284 let (command_tx, command_rx) = unbounded_channel();
22285 let snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
22286 encrypted: secure,
22287 alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
22288 local_settings: http2_settings_from_value(&payload.settings),
22289 remote_settings: BTreeMap::new(),
22290 state: http2_runtime_snapshot(),
22291 socket: Http2SocketSnapshot {
22292 encrypted: secure,
22293 remote_address: Some(resolved.guest_remote_addr.ip().to_string()),
22294 remote_port: Some(resolved.guest_remote_addr.port()),
22295 remote_family: Some(
22296 socket_addr_family(&resolved.guest_remote_addr).to_string(),
22297 ),
22298 servername: if secure {
22299 payload
22300 .tls
22301 .as_ref()
22302 .and_then(|tls| tls.servername.clone())
22303 .or_else(|| Some(host.to_string()))
22304 } else {
22305 None
22306 },
22307 alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
22308 ..Http2SocketSnapshot::default()
22309 },
22310 ..Http2SessionSnapshot::default()
22311 }));
22312 let session_id = {
22313 let mut state = process.http2.shared.lock().map_err(|_| {
22314 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
22315 })?;
22316 let session_id = next_http2_session_id(&mut state);
22317 state
22318 .sessions
22319 .insert(session_id, ActiveHttp2Session { command_tx });
22320 if state.event_session.is_none() {
22321 state.event_session = process.execution.javascript_v8_session_handle();
22322 }
22323 state.session_events.entry(session_id).or_default();
22324 session_id
22325 };
22326 spawn_http2_client_session(
22327 Arc::clone(&process.http2.shared),
22328 session_id,
22329 resolved.actual_addr,
22330 if secure {
22331 Some(payload.tls.unwrap_or(JavascriptTlsBridgeOptions {
22332 is_server: false,
22333 servername: Some(host.to_string()),
22334 alpn_protocols: Some(vec![String::from("h2")]),
22335 ..JavascriptTlsBridgeOptions::default()
22336 }))
22337 } else {
22338 None
22339 },
22340 Arc::clone(&snapshot),
22341 command_rx,
22342 );
22343 let snapshot_json =
22344 http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())?;
22345 javascript_net_json_string(
22346 json!({
22347 "sessionId": session_id,
22348 "state": snapshot_json,
22349 }),
22350 "net.http2_session_connect",
22351 )
22352 }
22353 "net.http2_session_request" => {
22354 let session_id = javascript_sync_rpc_arg_u64(
22355 &request.args,
22356 0,
22357 "net.http2_session_request session id",
22358 )?;
22359 let headers_json =
22360 javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_session_request headers")?;
22361 let options_json =
22362 javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_session_request options")?;
22363 let session = http2_session_for_id(process, session_id)?;
22364 send_http2_command(&session, |respond_to| Http2SessionCommand::Request {
22365 headers_json: headers_json.to_owned(),
22366 options_json: options_json.to_owned(),
22367 respond_to,
22368 })
22369 }
22370 "net.http2_session_settings" => {
22371 let session_id = javascript_sync_rpc_arg_u64(
22372 &request.args,
22373 0,
22374 "net.http2_session_settings session id",
22375 )?;
22376 let settings_json = javascript_sync_rpc_arg_str(
22377 &request.args,
22378 1,
22379 "net.http2_session_settings settings",
22380 )?;
22381 let session = http2_session_for_id(process, session_id)?;
22382 send_http2_command(&session, |respond_to| Http2SessionCommand::Settings {
22383 settings_json: settings_json.to_owned(),
22384 respond_to,
22385 })
22386 }
22387 "net.http2_session_set_local_window_size" => {
22388 let session_id = javascript_sync_rpc_arg_u64(
22389 &request.args,
22390 0,
22391 "net.http2_session_set_local_window_size session id",
22392 )?;
22393 let window_size = javascript_sync_rpc_arg_u64(
22394 &request.args,
22395 1,
22396 "net.http2_session_set_local_window_size window size",
22397 )?;
22398 let session = http2_session_for_id(process, session_id)?;
22399 send_http2_command(&session, |respond_to| {
22400 Http2SessionCommand::SetLocalWindowSize {
22401 size: window_size as u32,
22402 respond_to,
22403 }
22404 })
22405 }
22406 "net.http2_session_goaway" => {
22407 let session_id = javascript_sync_rpc_arg_u64(
22408 &request.args,
22409 0,
22410 "net.http2_session_goaway session id",
22411 )?;
22412 let error_code = javascript_sync_rpc_arg_u64(
22413 &request.args,
22414 1,
22415 "net.http2_session_goaway error code",
22416 )?;
22417 let last_stream_id = javascript_sync_rpc_arg_u64(
22418 &request.args,
22419 2,
22420 "net.http2_session_goaway last stream id",
22421 )?;
22422 let opaque_data = request
22423 .args
22424 .get(3)
22425 .and_then(Value::as_str)
22426 .map(|value| {
22427 base64::engine::general_purpose::STANDARD
22428 .decode(value)
22429 .map_err(|error| {
22430 SidecarError::InvalidState(format!("invalid GOAWAY payload: {error}"))
22431 })
22432 })
22433 .transpose()?;
22434 let session = http2_session_for_id(process, session_id)?;
22435 send_http2_command(&session, |respond_to| Http2SessionCommand::Goaway {
22436 error_code: error_code as u32,
22437 last_stream_id: last_stream_id as u32,
22438 opaque_data,
22439 respond_to,
22440 })
22441 }
22442 "net.http2_session_close" | "net.http2_session_destroy" => {
22443 let session_id = javascript_sync_rpc_arg_u64(
22444 &request.args,
22445 0,
22446 "net.http2_session_close session id",
22447 )?;
22448 let session = http2_session_for_id(process, session_id)?;
22449 send_http2_command(&session, |respond_to| Http2SessionCommand::Close {
22450 abrupt: request.method == "net.http2_session_destroy",
22451 respond_to,
22452 })
22453 }
22454 "net.http2_session_poll" => {
22455 let session_id =
22456 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_poll session id")?;
22457 let wait_ms = javascript_sync_rpc_arg_u64_optional(
22458 &request.args,
22459 1,
22460 "net.http2_session_poll wait ms",
22461 )?
22462 .unwrap_or_default();
22463 match wait_for_http2_event(&process.http2.shared, session_id, false, wait_ms) {
22464 Some(event) => http2_event_value(&event),
22465 None => Ok(Value::Null),
22466 }
22467 }
22468 "net.http2_session_wait" => {
22469 let session_id =
22470 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_wait session id")?;
22471 dispatch_http2_wait_loop(process, session_id, false)
22472 }
22473 "net.http2_stream_respond" => {
22474 let stream_id = javascript_sync_rpc_arg_u64(
22475 &request.args,
22476 0,
22477 "net.http2_stream_respond stream id",
22478 )?;
22479 let headers_json =
22480 javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_stream_respond headers")?;
22481 let stream = http2_stream_for_id(process, stream_id)?;
22482 let session = http2_session_for_id(process, stream.session_id)?;
22483 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamRespond {
22484 stream_id,
22485 headers_json: headers_json.to_owned(),
22486 respond_to,
22487 })
22488 }
22489 "net.http2_stream_push_stream" => {
22490 let stream_id = javascript_sync_rpc_arg_u64(
22491 &request.args,
22492 0,
22493 "net.http2_stream_push_stream stream id",
22494 )?;
22495 let headers_json = javascript_sync_rpc_arg_str(
22496 &request.args,
22497 1,
22498 "net.http2_stream_push_stream headers",
22499 )?;
22500 let _options_json = javascript_sync_rpc_arg_str(
22501 &request.args,
22502 2,
22503 "net.http2_stream_push_stream options",
22504 )?;
22505 let stream = http2_stream_for_id(process, stream_id)?;
22506 let session = http2_session_for_id(process, stream.session_id)?;
22507 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamPush {
22508 stream_id,
22509 headers_json: headers_json.to_owned(),
22510 respond_to,
22511 })
22512 }
22513 "net.http2_stream_write" => {
22514 let stream_id =
22515 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_write stream id")?;
22516 let chunk =
22517 javascript_sync_rpc_base64_arg(&request.args, 1, "net.http2_stream_write data")?;
22518 let stream = http2_stream_for_id(process, stream_id)?;
22519 let session = http2_session_for_id(process, stream.session_id)?;
22520 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
22521 stream_id,
22522 chunk,
22523 end_stream: false,
22524 respond_to,
22525 })
22526 }
22527 "net.http2_stream_end" => {
22528 let stream_id =
22529 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_end stream id")?;
22530 let chunk = request
22531 .args
22532 .get(1)
22533 .and_then(Value::as_str)
22534 .map(|value| {
22535 base64::engine::general_purpose::STANDARD
22536 .decode(value)
22537 .map_err(|error| {
22538 SidecarError::InvalidState(format!(
22539 "invalid HTTP/2 stream payload: {error}"
22540 ))
22541 })
22542 })
22543 .transpose()?
22544 .unwrap_or_default();
22545 let stream = http2_stream_for_id(process, stream_id)?;
22546 let session = http2_session_for_id(process, stream.session_id)?;
22547 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
22548 stream_id,
22549 chunk,
22550 end_stream: true,
22551 respond_to,
22552 })
22553 }
22554 "net.http2_stream_close" => {
22555 let stream_id =
22556 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_close stream id")?;
22557 let code = javascript_sync_rpc_arg_u64_optional(
22558 &request.args,
22559 1,
22560 "net.http2_stream_close error code",
22561 )?
22562 .map(|value| value as u32);
22563 let stream = http2_stream_for_id(process, stream_id)?;
22564 let session = http2_session_for_id(process, stream.session_id)?;
22565 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamClose {
22566 stream_id,
22567 error_code: code,
22568 respond_to,
22569 })
22570 }
22571 "net.http2_stream_pause" => {
22572 let stream_id =
22573 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_pause stream id")?;
22574 let stream = http2_stream_for_id(process, stream_id)?;
22575 stream.paused.store(true, Ordering::SeqCst);
22576 Ok(Value::Null)
22577 }
22578 "net.http2_stream_resume" => {
22579 let stream_id =
22580 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_resume stream id")?;
22581 let stream = http2_stream_for_id(process, stream_id)?;
22582 stream.paused.store(false, Ordering::SeqCst);
22583 stream.resume_notify.notify_waiters();
22584 Ok(Value::Null)
22585 }
22586 "net.http2_stream_respond_with_file" => {
22587 let stream_id = javascript_sync_rpc_arg_u64(
22588 &request.args,
22589 0,
22590 "net.http2_stream_respond_with_file stream id",
22591 )?;
22592 let path = javascript_sync_rpc_arg_str(
22593 &request.args,
22594 1,
22595 "net.http2_stream_respond_with_file path",
22596 )?;
22597 let headers_json = javascript_sync_rpc_arg_str(
22598 &request.args,
22599 2,
22600 "net.http2_stream_respond_with_file headers",
22601 )?;
22602 let options_json = javascript_sync_rpc_arg_str(
22603 &request.args,
22604 3,
22605 "net.http2_stream_respond_with_file options",
22606 )?;
22607 let stream = http2_stream_for_id(process, stream_id)?;
22608 let session = http2_session_for_id(process, stream.session_id)?;
22609 let guest_path = resolve_http2_file_response_guest_path(process, path);
22610 let body = kernel.read_file(&guest_path).map_err(kernel_error)?;
22611 send_http2_command(&session, |respond_to| {
22612 Http2SessionCommand::StreamRespondWithFile {
22613 stream_id,
22614 body,
22615 headers_json: headers_json.to_owned(),
22616 options_json: options_json.to_owned(),
22617 respond_to,
22618 }
22619 })
22620 }
22621 other => Err(SidecarError::InvalidState(format!(
22622 "unsupported JavaScript HTTP/2 sync RPC method {other}"
22623 ))),
22624 }
22625}
22626
22627const JAVASCRIPT_NET_POLL_MAX_WAIT: Duration = Duration::from_millis(50);
22628const EXITED_PROCESS_SNAPSHOT_RETENTION: Duration = Duration::from_secs(2);
22629
22630fn resolve_http2_file_response_guest_path(process: &ActiveProcess, path: &str) -> String {
22631 if Path::new(path).is_absolute() {
22632 normalize_path(path)
22633 } else {
22634 normalize_path(&format!("{}/{}", process.guest_cwd, path))
22635 }
22636}
22637
22638pub(crate) fn clamp_javascript_net_poll_wait(wait_ms: u64) -> Duration {
22639 if wait_ms == 0 {
22642 Duration::ZERO
22643 } else {
22644 Duration::from_millis(wait_ms).min(JAVASCRIPT_NET_POLL_MAX_WAIT)
22645 }
22646}
22647
22648fn service_javascript_net_sync_rpc_response<B>(
22649 request: JavascriptNetSyncRpcServiceRequest<'_, B>,
22650) -> Result<JavascriptSyncRpcServiceResponse, SidecarError>
22651where
22652 B: NativeSidecarBridge + Send + 'static,
22653 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
22654{
22655 if request.sync_request.method != "net.socket_read" {
22656 return service_javascript_net_sync_rpc(request).map(Into::into);
22657 }
22658
22659 let JavascriptNetSyncRpcServiceRequest {
22660 kernel,
22661 process,
22662 sync_request: request,
22663 ..
22664 } = request;
22665 let trace_enabled = net_tcp_trace_enabled(&process.env);
22666 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_read socket id")?;
22667 if trace_enabled {
22668 NET_TCP_TRACE_COUNTERS
22669 .socket_read_calls
22670 .fetch_add(1, Ordering::Relaxed);
22671 NET_TCP_TRACE_COUNTERS
22672 .socket_read_zero_wait_calls
22673 .fetch_add(1, Ordering::Relaxed);
22674 }
22675
22676 let event = if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
22677 socket.poll(kernel, process.kernel_pid, Duration::ZERO, trace_enabled)?
22678 } else {
22679 let socket = process
22680 .unix_sockets
22681 .get_mut(socket_id)
22682 .ok_or_else(|| SidecarError::InvalidState(format!("unknown net socket {socket_id}")))?;
22683 socket.poll(Duration::ZERO)?
22684 };
22685
22686 match event {
22687 Some(JavascriptTcpSocketEvent::Data(chunk)) => {
22688 Ok(JavascriptSyncRpcServiceResponse::Raw(chunk))
22689 }
22690 other => javascript_net_read_value(other).map(Into::into),
22691 }
22692}
22693
22694pub(crate) fn service_javascript_net_sync_rpc<B>(
22695 request: JavascriptNetSyncRpcServiceRequest<'_, B>,
22696) -> Result<Value, SidecarError>
22697where
22698 B: NativeSidecarBridge + Send + 'static,
22699 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
22700{
22701 let JavascriptNetSyncRpcServiceRequest {
22702 bridge,
22703 vm_id,
22704 dns,
22705 socket_paths,
22706 kernel,
22707 kernel_readiness,
22708 process,
22709 sync_request: request,
22710 resource_limits,
22711 network_counts,
22712 } = request;
22713 let trace_enabled = net_tcp_trace_enabled(&process.env);
22714 match request.method.as_str() {
22715 "net.http_listen" => {
22716 check_network_resource_limit(
22717 resource_limits.max_sockets,
22718 network_counts.sockets,
22719 1,
22720 "socket",
22721 )?;
22722 let payload_json =
22723 javascript_sync_rpc_arg_str(&request.args, 0, "net.http_listen payload")?;
22724 let payload: JavascriptHttpListenRequest =
22725 serde_json::from_str(payload_json).map_err(|error| {
22726 SidecarError::InvalidState(format!(
22727 "net.http_listen payload must be valid JSON: {error}"
22728 ))
22729 })?;
22730 let (family, bind_host, guest_host) =
22731 normalize_tcp_listen_host(payload.hostname.as_deref())?;
22732 let requested_port = payload.port.unwrap_or(0);
22733 bridge.require_network_access(
22734 vm_id,
22735 NetworkOperation::Listen,
22736 format_tcp_resource(bind_host, requested_port),
22737 )?;
22738 let port = allocate_guest_listen_port(
22739 requested_port,
22740 family,
22741 &socket_paths.used_tcp_guest_ports,
22742 socket_paths.listen_policy,
22743 )?;
22744 let mut listener = ActiveTcpListener::bind(
22745 bind_host,
22746 guest_host,
22747 port,
22748 Some(DEFAULT_JAVASCRIPT_NET_BACKLOG),
22749 )?;
22750 let guest_local_addr = listener.guest_local_addr();
22751 process.http_servers.insert(
22752 payload.server_id,
22753 ActiveHttpServer {
22754 listener: listener.listener.take().ok_or_else(|| {
22755 SidecarError::InvalidState(String::from(
22756 "HTTP listener missing host TCP socket",
22757 ))
22758 })?,
22759 guest_local_addr,
22760 next_request_id: 0,
22761 },
22762 );
22763 serde_json::to_string(&json!({
22764 "address": socket_address_value(&guest_local_addr)
22765 }))
22766 .map(Value::String)
22767 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
22768 }
22769 "net.http_close" => {
22770 let server_id =
22771 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_close server id")?;
22772 let server = process.http_servers.remove(&server_id).ok_or_else(|| {
22773 SidecarError::InvalidState(format!("unknown HTTP server {server_id}"))
22774 })?;
22775 drop(server.listener);
22776 process
22777 .pending_http_requests
22778 .retain(|(pending_server_id, _), _| *pending_server_id != server_id);
22779 Ok(Value::Null)
22780 }
22781 "net.http_wait" => {
22782 let server_id =
22783 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_wait server id")?;
22784 dispatch_http_wait_loop(process, server_id)
22785 }
22786 "net.http_respond" => {
22787 let server_id =
22788 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_respond server id")?;
22789 let request_id =
22790 javascript_sync_rpc_arg_u64(&request.args, 1, "net.http_respond request id")?;
22791 let response_json =
22792 javascript_sync_rpc_arg_str(&request.args, 2, "net.http_respond payload")?;
22793 ensure_vm_fetch_response_within_limit(
22794 response_json,
22795 "net.http_respond",
22796 VM_FETCH_BUFFER_LIMIT_BYTES,
22797 )
22798 .map_err(sidecar_core_execution_error)?;
22799 serde_json::from_str::<Value>(response_json).map_err(|error| {
22800 SidecarError::Execution(format!(
22801 "net.http_respond payload must be valid JSON: {error}"
22802 ))
22803 })?;
22804 let Some(pending) = process
22805 .pending_http_requests
22806 .get_mut(&(server_id, request_id))
22807 else {
22808 return Err(SidecarError::InvalidState(format!(
22809 "unknown pending HTTP request {request_id} for server {server_id}"
22810 )));
22811 };
22812 *pending = Some(response_json.to_owned());
22813 Ok(Value::Null)
22814 }
22815 "net.reserve_tcp_port" => {
22816 let payload = request
22817 .args
22818 .first()
22819 .cloned()
22820 .ok_or_else(|| {
22821 SidecarError::InvalidState(String::from(
22822 "net.reserve_tcp_port requires a request payload",
22823 ))
22824 })
22825 .and_then(|value| {
22826 serde_json::from_value::<JavascriptNetReserveTcpPortRequest>(value).map_err(
22827 |error| {
22828 SidecarError::InvalidState(format!(
22829 "invalid net.reserve_tcp_port payload: {error}"
22830 ))
22831 },
22832 )
22833 })?;
22834 let (family, _bind_host, guest_host) =
22835 normalize_tcp_listen_host(payload.host.as_deref())?;
22836 let requested_port = payload.port.unwrap_or(0);
22837 let port = allocate_guest_listen_port(
22838 requested_port,
22839 family,
22840 &socket_paths.used_tcp_guest_ports,
22841 socket_paths.listen_policy,
22842 )?;
22843 let reservation_id = process.allocate_tcp_port_reservation_id();
22844 process
22845 .tcp_port_reservations
22846 .insert(reservation_id.clone(), (family, port));
22847 Ok(json!({
22848 "reservationId": reservation_id,
22849 "localAddress": guest_host,
22850 "localPort": port,
22851 "family": match family {
22852 JavascriptSocketFamily::Ipv4 => "IPv4",
22853 JavascriptSocketFamily::Ipv6 => "IPv6",
22854 },
22855 }))
22856 }
22857 "net.release_tcp_port" => {
22858 let reservation_id =
22859 javascript_sync_rpc_arg_str(&request.args, 0, "net.release_tcp_port reservation")?;
22860 process.tcp_port_reservations.remove(reservation_id);
22861 Ok(Value::Null)
22862 }
22863 "net.connect" => {
22864 check_network_resource_limit(
22865 resource_limits.max_sockets,
22866 network_counts.sockets,
22867 1,
22868 "socket",
22869 )?;
22870 check_network_resource_limit(
22871 resource_limits.max_connections,
22872 network_counts.connections,
22873 1,
22874 "connection",
22875 )?;
22876 let payload = request
22877 .args
22878 .first()
22879 .cloned()
22880 .ok_or_else(|| {
22881 SidecarError::InvalidState(String::from(
22882 "net.connect requires a request payload",
22883 ))
22884 })
22885 .and_then(|value| {
22886 serde_json::from_value::<JavascriptNetConnectRequest>(value).map_err(|error| {
22887 SidecarError::InvalidState(format!("invalid net.connect payload: {error}"))
22888 })
22889 })?;
22890 if let Some(path) = payload.path.as_deref() {
22891 let guest_path = normalize_path(path);
22892 let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
22893 let socket = ActiveUnixSocket::connect(&host_path, &guest_path)?;
22894 let socket_id = process.allocate_unix_socket_id();
22895 socket.set_event_pusher(
22896 process.execution.javascript_v8_session_handle(),
22897 socket_id.clone(),
22898 );
22899 process.unix_sockets.insert(socket_id.clone(), socket);
22900 Ok(json!({
22901 "socketId": socket_id,
22902 "remotePath": guest_path,
22903 }))
22904 } else {
22905 let port = payload.port.ok_or_else(|| {
22906 SidecarError::InvalidState(String::from(
22907 "net.connect requires either a path or port",
22908 ))
22909 })?;
22910 let host = payload.host.as_deref().unwrap_or("localhost");
22911 let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
22912 process
22913 .tcp_port_reservations
22914 .remove(id)
22915 .map(|reservation| (id.to_owned(), reservation))
22916 });
22917 bridge.require_network_access(
22918 vm_id,
22919 NetworkOperation::Http,
22920 format_tcp_resource(host, port),
22921 )?;
22922 if is_loopback_socket_host(host) {
22923 let families = [JavascriptSocketFamily::Ipv4, JavascriptSocketFamily::Ipv6];
22924 if let Some((family, target)) = families.iter().find_map(|family| {
22925 socket_paths
22926 .http_loopback_target(*family, port)
22927 .map(|target| (*family, target))
22928 }) {
22929 if let Some((reservation_id, reservation)) = local_reservation {
22930 process
22931 .tcp_port_reservations
22932 .insert(reservation_id, reservation);
22933 }
22934 let remote_address = match family {
22935 JavascriptSocketFamily::Ipv4 => "127.0.0.1",
22936 JavascriptSocketFamily::Ipv6 => "::1",
22937 };
22938 return Ok(json!({
22939 "loopbackHttpTarget": {
22940 "processId": target.process_id.clone(),
22941 "serverId": target.server_id,
22942 "host": remote_address,
22943 "port": port,
22944 },
22945 "localAddress": match family {
22946 JavascriptSocketFamily::Ipv4 => "127.0.0.1",
22947 JavascriptSocketFamily::Ipv6 => "::1",
22948 },
22949 "localPort": payload.local_port.unwrap_or(0),
22950 "remoteAddress": remote_address,
22951 "remotePort": port,
22952 "remoteFamily": match family {
22953 JavascriptSocketFamily::Ipv4 => "IPv4",
22954 JavascriptSocketFamily::Ipv6 => "IPv6",
22955 },
22956 }));
22957 }
22958 }
22959 let connect_result = ActiveTcpSocket::connect(ActiveTcpConnectRequest {
22960 bridge,
22961 kernel,
22962 kernel_pid: process.kernel_pid,
22963 vm_id,
22964 dns,
22965 host,
22966 port,
22967 local_address: payload.local_address.as_deref(),
22968 local_port: payload.local_port,
22969 local_reservation: local_reservation
22970 .as_ref()
22971 .map(|(_, reservation)| *reservation),
22972 context: socket_paths,
22973 });
22974 if let Err(error) = connect_result {
22975 if let Some((reservation_id, reservation)) = local_reservation {
22976 process
22977 .tcp_port_reservations
22978 .insert(reservation_id, reservation);
22979 }
22980 return Err(error);
22981 }
22982 let socket = connect_result?;
22983 let socket_id = process.allocate_tcp_socket_id();
22984 let local_addr = socket.guest_local_addr;
22985 let remote_addr = socket.guest_remote_addr;
22986 socket.set_event_pusher(
22987 process.execution.javascript_v8_session_handle(),
22988 socket_id.clone(),
22989 );
22990 register_kernel_readiness_target(
22991 &kernel_readiness,
22992 socket.kernel_socket_id,
22993 process.execution.javascript_v8_session_handle(),
22994 socket_id.clone(),
22995 KernelSocketReadinessEvent::Data,
22996 );
22997 process.tcp_sockets.insert(socket_id.clone(), socket);
22998 Ok(json!({
22999 "socketId": socket_id,
23000 "localAddress": local_addr.ip().to_string(),
23001 "localPort": local_addr.port(),
23002 "remoteAddress": remote_addr.ip().to_string(),
23003 "remotePort": remote_addr.port(),
23004 "remoteFamily": socket_addr_family(&remote_addr),
23005 }))
23006 }
23007 }
23008 "net.listen" => {
23009 check_network_resource_limit(
23010 resource_limits.max_sockets,
23011 network_counts.sockets,
23012 1,
23013 "socket",
23014 )?;
23015 let payload = request
23016 .args
23017 .first()
23018 .cloned()
23019 .ok_or_else(|| {
23020 SidecarError::InvalidState(String::from(
23021 "net.listen requires a request payload",
23022 ))
23023 })
23024 .and_then(|value| match value {
23025 Value::String(json) => {
23026 serde_json::from_str::<JavascriptNetListenRequest>(&json).map_err(|error| {
23027 SidecarError::InvalidState(format!(
23028 "invalid net.listen payload: {error}"
23029 ))
23030 })
23031 }
23032 other => serde_json::from_value::<JavascriptNetListenRequest>(other).map_err(
23033 |error| {
23034 SidecarError::InvalidState(format!(
23035 "invalid net.listen payload: {error}"
23036 ))
23037 },
23038 ),
23039 })?;
23040 if let Some(path) = payload.path.as_deref() {
23041 let guest_path = normalize_path(path);
23042 if kernel.exists(&guest_path).map_err(kernel_error)? {
23043 return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
23044 libc::EADDRINUSE,
23045 )));
23046 }
23047
23048 let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
23049 let on_host_mount =
23050 host_mount_path_for_guest_path_from_mounts(&socket_paths.mounts, &guest_path)
23051 .is_some();
23052 let listener = ActiveUnixListener::bind(&host_path, &guest_path, payload.backlog)?;
23053 if !on_host_mount {
23054 ensure_kernel_parent_directories(kernel, &guest_path)?;
23055 kernel
23056 .write_file(&guest_path, Vec::new())
23057 .map_err(kernel_error)?;
23058 }
23059 let listener_id = process.allocate_unix_listener_id();
23060 process.unix_listeners.insert(listener_id.clone(), listener);
23061 Ok(json!({
23062 "serverId": listener_id,
23063 "path": guest_path,
23064 }))
23065 } else {
23066 let (family, bind_host, guest_host) =
23067 normalize_tcp_listen_host(payload.host.as_deref())?;
23068 let requested_port = payload.port.unwrap_or(0);
23069 bridge.require_network_access(
23070 vm_id,
23071 NetworkOperation::Listen,
23072 format_tcp_resource(bind_host, requested_port),
23073 )?;
23074 let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
23075 process
23076 .tcp_port_reservations
23077 .remove(id)
23078 .map(|reservation| (id.to_owned(), reservation))
23079 });
23080 let port = if requested_port != 0
23081 && local_reservation
23082 .as_ref()
23083 .map(|(_, reservation)| *reservation)
23084 == Some((family, requested_port))
23085 {
23086 requested_port
23087 } else {
23088 allocate_guest_listen_port(
23089 requested_port,
23090 family,
23091 &socket_paths.used_tcp_guest_ports,
23092 socket_paths.listen_policy,
23093 )?
23094 };
23095 let listener_result = ActiveTcpListener::bind_kernel(
23096 kernel,
23097 process.kernel_pid,
23098 guest_host,
23099 port,
23100 payload.backlog,
23101 );
23102 if let Err(error) = listener_result {
23103 if let Some((reservation_id, reservation)) = local_reservation {
23104 process
23105 .tcp_port_reservations
23106 .insert(reservation_id, reservation);
23107 }
23108 return Err(error);
23109 }
23110 let listener = listener_result?;
23111 let listener_id = process.allocate_tcp_listener_id();
23112 let local_addr = listener.guest_local_addr();
23113 register_kernel_readiness_target(
23114 &kernel_readiness,
23115 listener.kernel_socket_id,
23116 process.execution.javascript_v8_session_handle(),
23117 listener_id.clone(),
23118 KernelSocketReadinessEvent::Accept,
23119 );
23120 process.tcp_listeners.insert(listener_id.clone(), listener);
23121 Ok(json!({
23122 "serverId": listener_id,
23123 "localAddress": local_addr.ip().to_string(),
23124 "localPort": local_addr.port(),
23125 "family": socket_addr_family(&local_addr),
23126 }))
23127 }
23128 }
23129 "net.poll" => {
23130 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.poll socket id")?;
23131 let wait_ms =
23132 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.poll wait ms")?
23133 .unwrap_or_default();
23134 let wait = clamp_javascript_net_poll_wait(wait_ms);
23135 let event = if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
23136 socket.poll(kernel, process.kernel_pid, wait, trace_enabled)?
23137 } else if let Some(socket) = process.unix_sockets.get_mut(socket_id) {
23138 socket.poll(wait)?
23139 } else {
23140 return Err(SidecarError::InvalidState(format!(
23141 "unknown net socket {socket_id}"
23142 )));
23143 };
23144
23145 match event {
23146 Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(json!({
23147 "type": "data",
23148 "data": javascript_sync_rpc_bytes_value(&chunk),
23149 })),
23150 Some(JavascriptTcpSocketEvent::End) => Ok(json!({
23151 "type": "end",
23152 })),
23153 Some(JavascriptTcpSocketEvent::Error { code, message }) => Ok(json!({
23154 "type": "error",
23155 "code": code,
23156 "message": message,
23157 })),
23158 Some(JavascriptTcpSocketEvent::Close { had_error }) => {
23159 if let Some(socket) = process.tcp_sockets.remove(socket_id) {
23160 release_tcp_socket_handle(
23161 process,
23162 socket_id,
23163 socket,
23164 kernel,
23165 &kernel_readiness,
23166 );
23167 } else if let Some(socket) = process.unix_sockets.remove(socket_id) {
23168 release_unix_socket_handle(process, socket_id, socket);
23169 }
23170 Ok(json!({
23171 "type": "close",
23172 "hadError": had_error,
23173 }))
23174 }
23175 None => Ok(Value::Null),
23176 }
23177 }
23178 "net.socket_wait_connect" => {
23179 let socket_id =
23180 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_wait_connect socket id")?;
23181 if let Some(socket) = process.tcp_sockets.get(socket_id) {
23182 javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
23183 } else {
23184 let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
23185 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
23186 })?;
23187 javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
23188 }
23189 }
23190 "net.socket_read" => {
23191 let socket_id =
23192 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_read socket id")?;
23193 if trace_enabled {
23194 NET_TCP_TRACE_COUNTERS
23195 .socket_read_calls
23196 .fetch_add(1, Ordering::Relaxed);
23197 NET_TCP_TRACE_COUNTERS
23198 .socket_read_zero_wait_calls
23199 .fetch_add(1, Ordering::Relaxed);
23200 }
23201 if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
23202 javascript_net_read_value(socket.poll(
23203 kernel,
23204 process.kernel_pid,
23205 Duration::ZERO,
23206 trace_enabled,
23207 )?)
23208 } else {
23209 let socket = process.unix_sockets.get_mut(socket_id).ok_or_else(|| {
23210 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
23211 })?;
23212 javascript_net_read_value(socket.poll(Duration::ZERO)?)
23213 }
23214 }
23215 "net.socket_set_no_delay" => {
23216 let socket_id =
23217 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_set_no_delay socket id")?;
23218 let enable =
23219 javascript_sync_rpc_arg_bool(&request.args, 1, "net.socket_set_no_delay enabled")?;
23220 if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
23221 socket.set_no_delay(enable)?;
23222 } else if !process.unix_sockets.contains_key(socket_id) {
23223 return Err(SidecarError::InvalidState(format!(
23224 "unknown net socket {socket_id}"
23225 )));
23226 }
23227 Ok(Value::Null)
23228 }
23229 "net.socket_set_keep_alive" => {
23230 let socket_id = javascript_sync_rpc_arg_str(
23231 &request.args,
23232 0,
23233 "net.socket_set_keep_alive socket id",
23234 )?;
23235 let enable = javascript_sync_rpc_arg_bool(
23236 &request.args,
23237 1,
23238 "net.socket_set_keep_alive enabled",
23239 )?;
23240 let initial_delay_secs = javascript_sync_rpc_arg_u64_optional(
23241 &request.args,
23242 2,
23243 "net.socket_set_keep_alive initial delay seconds",
23244 )?;
23245 if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
23246 socket.set_keep_alive(enable, initial_delay_secs)?;
23247 } else if !process.unix_sockets.contains_key(socket_id) {
23248 return Err(SidecarError::InvalidState(format!(
23249 "unknown net socket {socket_id}"
23250 )));
23251 }
23252 Ok(Value::Null)
23253 }
23254 "net.socket_upgrade_tls" => {
23255 let socket_id =
23256 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_upgrade_tls socket id")?;
23257 let options_json =
23258 javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_upgrade_tls options")?;
23259 let options: JavascriptTlsBridgeOptions =
23260 serde_json::from_str(options_json).map_err(|error| {
23261 SidecarError::InvalidState(format!(
23262 "net.socket_upgrade_tls options must be valid JSON: {error}"
23263 ))
23264 })?;
23265 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
23266 SidecarError::InvalidState(format!(
23267 "unknown TCP socket {socket_id} for TLS upgrade"
23268 ))
23269 })?;
23270 socket.upgrade_tls(vm_id, kernel, options)?;
23271 Ok(Value::Null)
23272 }
23273 "net.socket_get_tls_client_hello" => {
23274 let socket_id = javascript_sync_rpc_arg_str(
23275 &request.args,
23276 0,
23277 "net.socket_get_tls_client_hello socket id",
23278 )?;
23279 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
23280 SidecarError::InvalidState(format!(
23281 "unknown TCP socket {socket_id} for TLS client hello query"
23282 ))
23283 })?;
23284 socket.tls_client_hello_json(vm_id, kernel)
23285 }
23286 "net.socket_tls_query" => {
23287 let socket_id =
23288 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_tls_query socket id")?;
23289 let query =
23290 javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_tls_query query")?;
23291 let detailed = request
23292 .args
23293 .get(2)
23294 .and_then(Value::as_bool)
23295 .unwrap_or(false);
23296 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
23297 SidecarError::InvalidState(format!("unknown TCP socket {socket_id} for TLS query"))
23298 })?;
23299 socket.tls_query(query, detailed)
23300 }
23301 "net.server_poll" => {
23302 let listener_id =
23303 javascript_sync_rpc_arg_str(&request.args, 0, "net.server_poll listener id")?;
23304 let wait_ms =
23305 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.server_poll wait ms")?
23306 .unwrap_or_default();
23307 let tcp_event = if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
23308 Some(listener.poll(
23309 kernel,
23310 process.kernel_pid,
23311 Duration::from_millis(wait_ms),
23312 trace_enabled,
23313 )?)
23314 } else {
23315 None
23316 };
23317
23318 if let Some(event) = tcp_event {
23319 return match event {
23320 Some(JavascriptTcpListenerEvent::Connection(pending)) => {
23321 let PendingTcpSocket {
23322 stream,
23323 kernel_socket_id,
23324 preallocated,
23325 guest_local_addr,
23326 guest_remote_addr,
23327 } = pending;
23328 if !preallocated {
23329 if let Err(error) = check_network_resource_limit(
23330 resource_limits.max_sockets,
23331 network_counts.sockets,
23332 1,
23333 "socket",
23334 )
23335 .and_then(|()| {
23336 check_network_resource_limit(
23337 resource_limits.max_connections,
23338 network_counts.connections,
23339 1,
23340 "connection",
23341 )
23342 }) {
23343 if let Some(stream) = stream {
23344 let _ = stream.shutdown(Shutdown::Both);
23345 }
23346 return Ok(json!({
23347 "type": "error",
23348 "code": "EAGAIN",
23349 "message": error.to_string(),
23350 }));
23351 }
23352 }
23353 let socket = if let Some(stream) = stream {
23354 ActiveTcpSocket::from_stream(
23355 stream,
23356 Some(listener_id.to_string()),
23357 guest_local_addr,
23358 guest_remote_addr,
23359 )?
23360 } else {
23361 ActiveTcpSocket::from_kernel(
23362 kernel_socket_id.ok_or_else(|| {
23363 SidecarError::InvalidState(String::from(
23364 "kernel TCP accept missing socket id",
23365 ))
23366 })?,
23367 Some(listener_id.to_string()),
23368 guest_local_addr,
23369 guest_remote_addr,
23370 )
23371 };
23372 let socket_id = process.allocate_tcp_socket_id();
23373 socket.set_event_pusher(
23374 process.execution.javascript_v8_session_handle(),
23375 socket_id.clone(),
23376 );
23377 register_kernel_readiness_target(
23378 &kernel_readiness,
23379 socket.kernel_socket_id,
23380 process.execution.javascript_v8_session_handle(),
23381 socket_id.clone(),
23382 KernelSocketReadinessEvent::Data,
23383 );
23384 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
23385 listener.register_connection(&socket_id);
23386 }
23387 process.tcp_sockets.insert(socket_id.clone(), socket);
23388 Ok(json!({
23389 "type": "connection",
23390 "socketId": socket_id,
23391 "localAddress": guest_local_addr.ip().to_string(),
23392 "localPort": guest_local_addr.port(),
23393 "remoteAddress": guest_remote_addr.ip().to_string(),
23394 "remotePort": guest_remote_addr.port(),
23395 "remoteFamily": socket_addr_family(&guest_remote_addr),
23396 }))
23397 }
23398 Some(JavascriptTcpListenerEvent::Error { code, message }) => Ok(json!({
23399 "type": "error",
23400 "code": code,
23401 "message": message,
23402 })),
23403 None => Ok(Value::Null),
23404 };
23405 }
23406
23407 let event = {
23408 let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
23409 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
23410 })?;
23411 listener.poll(Duration::from_millis(wait_ms))?
23412 };
23413
23414 match event {
23415 Some(JavascriptUnixListenerEvent::Connection(pending)) => {
23416 if let Err(error) = check_network_resource_limit(
23417 resource_limits.max_sockets,
23418 network_counts.sockets,
23419 1,
23420 "socket",
23421 )
23422 .and_then(|()| {
23423 check_network_resource_limit(
23424 resource_limits.max_connections,
23425 network_counts.connections,
23426 1,
23427 "connection",
23428 )
23429 }) {
23430 let _ = pending.stream.shutdown(Shutdown::Both);
23431 return Ok(json!({
23432 "type": "error",
23433 "code": "EAGAIN",
23434 "message": error.to_string(),
23435 }));
23436 }
23437 let socket = ActiveUnixSocket::from_stream(
23438 pending.stream,
23439 Some(listener_id.to_string()),
23440 pending.local_path.clone(),
23441 pending.remote_path.clone(),
23442 )?;
23443 let socket_id = process.allocate_unix_socket_id();
23444 socket.set_event_pusher(
23445 process.execution.javascript_v8_session_handle(),
23446 socket_id.clone(),
23447 );
23448 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
23449 listener.register_connection(&socket_id);
23450 }
23451 process.unix_sockets.insert(socket_id.clone(), socket);
23452 Ok(json!({
23453 "type": "connection",
23454 "socketId": socket_id,
23455 "localPath": pending.local_path,
23456 "remotePath": pending.remote_path,
23457 }))
23458 }
23459 Some(JavascriptUnixListenerEvent::Error { code, message }) => Ok(json!({
23460 "type": "error",
23461 "code": code,
23462 "message": message,
23463 })),
23464 None => Ok(Value::Null),
23465 }
23466 }
23467 "net.server_accept" => {
23468 let listener_id =
23469 javascript_sync_rpc_arg_str(&request.args, 0, "net.server_accept listener id")?;
23470 if trace_enabled {
23471 NET_TCP_TRACE_COUNTERS
23472 .server_accept_calls
23473 .fetch_add(1, Ordering::Relaxed);
23474 NET_TCP_TRACE_COUNTERS
23475 .server_accept_zero_wait_calls
23476 .fetch_add(1, Ordering::Relaxed);
23477 }
23478 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
23479 return match listener.poll(
23480 kernel,
23481 process.kernel_pid,
23482 Duration::ZERO,
23483 trace_enabled,
23484 )? {
23485 Some(JavascriptTcpListenerEvent::Connection(pending)) => {
23486 let PendingTcpSocket {
23487 stream,
23488 kernel_socket_id,
23489 preallocated,
23490 guest_local_addr,
23491 guest_remote_addr,
23492 } = pending;
23493 if !preallocated {
23494 check_network_resource_limit(
23495 resource_limits.max_sockets,
23496 network_counts.sockets,
23497 1,
23498 "socket",
23499 )?;
23500 check_network_resource_limit(
23501 resource_limits.max_connections,
23502 network_counts.connections,
23503 1,
23504 "connection",
23505 )?;
23506 }
23507 let info = tcp_socket_info_value(&guest_local_addr, &guest_remote_addr);
23508 let socket = if let Some(stream) = stream {
23509 ActiveTcpSocket::from_stream(
23510 stream,
23511 Some(listener_id.to_string()),
23512 guest_local_addr,
23513 guest_remote_addr,
23514 )?
23515 } else {
23516 ActiveTcpSocket::from_kernel(
23517 kernel_socket_id.ok_or_else(|| {
23518 SidecarError::InvalidState(String::from(
23519 "kernel TCP accept missing socket id",
23520 ))
23521 })?,
23522 Some(listener_id.to_string()),
23523 guest_local_addr,
23524 guest_remote_addr,
23525 )
23526 };
23527 let socket_id = process.allocate_tcp_socket_id();
23528 socket.set_event_pusher(
23529 process.execution.javascript_v8_session_handle(),
23530 socket_id.clone(),
23531 );
23532 register_kernel_readiness_target(
23533 &kernel_readiness,
23534 socket.kernel_socket_id,
23535 process.execution.javascript_v8_session_handle(),
23536 socket_id.clone(),
23537 KernelSocketReadinessEvent::Data,
23538 );
23539 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
23540 listener.register_connection(&socket_id);
23541 }
23542 process.tcp_sockets.insert(socket_id.clone(), socket);
23543 javascript_net_json_string(
23544 json!({
23545 "socketId": socket_id,
23546 "info": info,
23547 }),
23548 "net.server_accept",
23549 )
23550 }
23551 Some(JavascriptTcpListenerEvent::Error { code, message }) => {
23552 let detail = code.unwrap_or_else(|| String::from("server accept"));
23553 Err(SidecarError::Execution(format!("{detail}: {message}")))
23554 }
23555 None => Ok(javascript_net_timeout_value()),
23556 };
23557 }
23558
23559 let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
23560 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
23561 })?;
23562 match listener.poll(Duration::ZERO)? {
23563 Some(JavascriptUnixListenerEvent::Connection(pending)) => {
23564 check_network_resource_limit(
23565 resource_limits.max_sockets,
23566 network_counts.sockets,
23567 1,
23568 "socket",
23569 )?;
23570 check_network_resource_limit(
23571 resource_limits.max_connections,
23572 network_counts.connections,
23573 1,
23574 "connection",
23575 )?;
23576 let info = json!({
23577 "localPath": pending.local_path.clone(),
23578 "remotePath": pending.remote_path.clone(),
23579 });
23580 let socket = ActiveUnixSocket::from_stream(
23581 pending.stream,
23582 Some(listener_id.to_string()),
23583 pending.local_path,
23584 pending.remote_path,
23585 )?;
23586 let socket_id = process.allocate_unix_socket_id();
23587 socket.set_event_pusher(
23588 process.execution.javascript_v8_session_handle(),
23589 socket_id.clone(),
23590 );
23591 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
23592 listener.register_connection(&socket_id);
23593 }
23594 process.unix_sockets.insert(socket_id.clone(), socket);
23595 javascript_net_json_string(
23596 json!({
23597 "socketId": socket_id,
23598 "info": info,
23599 }),
23600 "net.server_accept",
23601 )
23602 }
23603 Some(JavascriptUnixListenerEvent::Error { code, message }) => {
23604 let detail = code.unwrap_or_else(|| String::from("server accept"));
23605 Err(SidecarError::Execution(format!("{detail}: {message}")))
23606 }
23607 None => Ok(javascript_net_timeout_value()),
23608 }
23609 }
23610 "net.server_connections" => {
23611 let listener_id = javascript_sync_rpc_arg_str(
23612 &request.args,
23613 0,
23614 "net.server_connections listener id",
23615 )?;
23616 if let Some(listener) = process.tcp_listeners.get(listener_id) {
23617 Ok(json!(listener.active_connection_count()))
23618 } else {
23619 let listener = process.unix_listeners.get(listener_id).ok_or_else(|| {
23620 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
23621 })?;
23622 Ok(json!(listener.active_connection_count()))
23623 }
23624 }
23625 "net.upgrade_socket_write" => {
23626 let socket_id = javascript_sync_rpc_arg_str(
23627 &request.args,
23628 0,
23629 "net.upgrade_socket_write socket id",
23630 )?;
23631 let chunk =
23632 javascript_sync_rpc_base64_arg(&request.args, 1, "net.upgrade_socket_write chunk")?;
23633 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
23634 SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
23635 })?;
23636 socket
23637 .write_all(kernel, process.kernel_pid, &chunk)
23638 .map(|written| json!(written))
23639 }
23640 "net.upgrade_socket_end" => {
23641 let socket_id =
23642 javascript_sync_rpc_arg_str(&request.args, 0, "net.upgrade_socket_end socket id")?;
23643 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
23644 SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
23645 })?;
23646 socket.shutdown_write(kernel, process.kernel_pid)?;
23647 Ok(Value::Null)
23648 }
23649 "net.upgrade_socket_destroy" => {
23650 let socket_id = javascript_sync_rpc_arg_str(
23651 &request.args,
23652 0,
23653 "net.upgrade_socket_destroy socket id",
23654 )?;
23655 let socket = process.tcp_sockets.remove(socket_id).ok_or_else(|| {
23656 SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
23657 })?;
23658 release_tcp_socket_handle(process, socket_id, socket, kernel, &kernel_readiness);
23659 Ok(Value::Null)
23660 }
23661 "net.write" => {
23662 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.write socket id")?;
23663 let chunk = if let Some(bytes) = request.raw_bytes_args.get(&1) {
23664 bytes.clone()
23665 } else {
23666 javascript_sync_rpc_bytes_arg(&request.args, 1, "net.write chunk")?
23667 };
23668 if trace_enabled {
23669 NET_TCP_TRACE_COUNTERS
23670 .socket_write_calls
23671 .fetch_add(1, Ordering::Relaxed);
23672 NET_TCP_TRACE_COUNTERS.socket_write_bytes.fetch_add(
23673 u64::try_from(chunk.len()).unwrap_or(u64::MAX),
23674 Ordering::Relaxed,
23675 );
23676 }
23677 if let Some(socket) = process.tcp_sockets.get(socket_id) {
23678 let write_started = trace_enabled.then(Instant::now);
23679 let write_result = socket.write_all(kernel, process.kernel_pid, &chunk);
23680 if let Some(write_started) = write_started {
23681 NET_TCP_TRACE_COUNTERS.socket_write_kernel_us.fetch_add(
23682 duration_micros_u64(write_started.elapsed()),
23683 Ordering::Relaxed,
23684 );
23685 }
23686 match write_result {
23687 Ok(written) => Ok(json!(written)),
23688 Err(error) => {
23689 if trace_enabled {
23690 NET_TCP_TRACE_COUNTERS
23691 .socket_write_errors
23692 .fetch_add(1, Ordering::Relaxed);
23693 }
23694 Err(error)
23695 }
23696 }
23697 } else {
23698 let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
23699 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
23700 })?;
23701 socket.write_all(&chunk).map(|written| json!(written))
23702 }
23703 }
23704 "net.shutdown" => {
23705 let socket_id =
23706 javascript_sync_rpc_arg_str(&request.args, 0, "net.shutdown socket id")?;
23707 if let Some(socket) = process.tcp_sockets.get(socket_id) {
23708 socket.shutdown_write(kernel, process.kernel_pid)?;
23709 } else {
23710 let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
23711 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
23712 })?;
23713 socket.shutdown_write()?;
23714 }
23715 Ok(Value::Null)
23716 }
23717 "net.destroy" => {
23718 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.destroy socket id")?;
23719 if let Some(socket) = process.tcp_sockets.remove(socket_id) {
23720 release_tcp_socket_handle(process, socket_id, socket, kernel, &kernel_readiness);
23721 Ok(Value::Null)
23722 } else if let Some(socket) = process.unix_sockets.remove(socket_id) {
23723 release_unix_socket_handle(process, socket_id, socket);
23724 Ok(Value::Null)
23725 } else {
23726 Ok(Value::Null)
23727 }
23728 }
23729 "net.server_close" => {
23730 let listener_id =
23731 javascript_sync_rpc_arg_str(&request.args, 0, "net.server_close listener id")?;
23732 if let Some(listener) = process.tcp_listeners.remove(listener_id) {
23733 unregister_kernel_readiness_target(&kernel_readiness, listener.kernel_socket_id);
23734 listener.close(kernel, process.kernel_pid)?;
23735 Ok(Value::Null)
23736 } else {
23737 let listener = process.unix_listeners.remove(listener_id).ok_or_else(|| {
23738 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
23739 })?;
23740 listener.close()?;
23741 Ok(Value::Null)
23742 }
23743 }
23744 "tls.get_ciphers" => javascript_net_json_string(
23745 Value::Array(
23746 tls_provider()
23747 .cipher_suites
23748 .iter()
23749 .filter_map(|suite| {
23750 suite
23751 .suite()
23752 .as_str()
23753 .map(|value| Value::String(value.to_owned()))
23754 })
23755 .collect(),
23756 ),
23757 "tls.get_ciphers",
23758 ),
23759 _ => Err(SidecarError::InvalidState(format!(
23760 "unsupported JavaScript net sync RPC method {}",
23761 request.method
23762 ))),
23763 }
23764}
23765
23766fn signal_name_for_stream_event(signal: i32) -> Option<&'static str> {
23767 match signal {
23768 libc::SIGHUP => Some("SIGHUP"),
23769 libc::SIGINT => Some("SIGINT"),
23770 libc::SIGUSR1 => Some("SIGUSR1"),
23771 libc::SIGALRM => Some("SIGALRM"),
23772 libc::SIGCONT => Some("SIGCONT"),
23773 libc::SIGTERM => Some("SIGTERM"),
23774 libc::SIGCHLD => Some("SIGCHLD"),
23775 libc::SIGWINCH => Some("SIGWINCH"),
23776 _ => None,
23777 }
23778}
23779
23780pub(crate) fn canonical_signal_name(signal: i32) -> Option<&'static str> {
23781 secure_exec_sidecar_core::canonical_signal_name(signal)
23782}
23783
23784fn dispatch_v8_process_signal(process: &ActiveProcess, signal: i32) -> Result<bool, SidecarError> {
23785 let Some(signal_name) = signal_name_for_stream_event(signal) else {
23786 return Ok(false);
23787 };
23788 process.execution.send_javascript_stream_event(
23789 "signal",
23790 json!({
23791 "signal": signal_name,
23792 "number": signal,
23793 "action": "default",
23794 }),
23795 )?;
23796 Ok(true)
23797}
23798
23799fn dispatch_v8_session_signal_async(session: V8SessionHandle, signal: i32) {
23800 let Some(signal_name) = signal_name_for_stream_event(signal).map(str::to_owned) else {
23801 return;
23802 };
23803 thread::spawn(move || {
23804 thread::sleep(Duration::from_millis(1));
23805 let payload = v8_runtime::json_to_cbor_payload(&json!({
23806 "signal": signal_name,
23807 "number": signal,
23808 "action": "default",
23809 }))
23810 .unwrap_or_default();
23811 let _ = session.send_stream_event("signal", payload);
23812 });
23813}
23814
23815pub(crate) fn parse_signal(signal: &str) -> Result<i32, SidecarError> {
23816 let trimmed = signal.trim();
23817 if trimmed.is_empty() {
23818 return Err(SidecarError::InvalidState(String::from(
23819 "kill_process requires a non-empty signal",
23820 )));
23821 }
23822
23823 if let Ok(value) = trimmed.parse::<i32>() {
23824 return match value {
23825 0..=31 => Ok(value),
23826 _ => Err(SidecarError::InvalidState(format!(
23827 "unsupported kill_process signal {signal}"
23828 ))),
23829 };
23830 }
23831
23832 secure_exec_sidecar_core::parse_posix_signal(trimmed).ok_or_else(|| {
23833 SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
23834 })
23835}
23836
23837pub(crate) fn runtime_child_is_alive(child_pid: u32) -> Result<bool, SidecarError> {
23838 Ok(runtime_child_exit_status(child_pid)?.is_none())
23839}
23840
23841#[cfg(not(target_os = "macos"))]
23842fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
23843 if child_pid == 0 {
23844 return Ok(Some(0));
23845 }
23846
23847 let wait_flags = WaitPidFlag::WNOHANG
23848 | WaitPidFlag::WNOWAIT
23849 | WaitPidFlag::WEXITED
23850 | WaitPidFlag::WUNTRACED
23851 | WaitPidFlag::WCONTINUED;
23852 match wait_on_child(WaitId::Pid(Pid::from_raw(child_pid as i32)), wait_flags) {
23853 Ok(WaitStatus::StillAlive)
23854 | Ok(WaitStatus::Stopped(_, _))
23855 | Ok(WaitStatus::Continued(_)) => Ok(None),
23856 Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
23857 Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
23858 #[cfg(any(target_os = "linux", target_os = "android"))]
23859 Ok(WaitStatus::PtraceEvent(_, _, _) | WaitStatus::PtraceSyscall(_)) => Ok(None),
23860 Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
23861 Err(error) => Err(SidecarError::Execution(format!(
23862 "failed to inspect guest runtime process {child_pid}: {error}"
23863 ))),
23864 }
23865}
23866
23867#[cfg(target_os = "macos")]
23873fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
23874 if child_pid == 0 {
23875 return Ok(Some(0));
23876 }
23877
23878 match waitpid(Pid::from_raw(child_pid as i32), Some(WaitPidFlag::WNOHANG)) {
23879 Ok(WaitStatus::StillAlive)
23880 | Ok(WaitStatus::Stopped(_, _))
23881 | Ok(WaitStatus::Continued(_)) => Ok(None),
23882 Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
23883 Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
23884 Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
23885 Err(error) => Err(SidecarError::Execution(format!(
23886 "failed to inspect guest runtime process {child_pid}: {error}"
23887 ))),
23888 }
23889}
23890
23891pub(crate) fn signal_runtime_process(child_pid: u32, signal: i32) -> Result<(), SidecarError> {
23892 if child_pid == 0 {
23893 return Ok(());
23894 }
23895
23896 if !runtime_child_is_alive(child_pid)? {
23897 return Ok(());
23898 }
23899
23900 if signal == 0 {
23901 return Ok(());
23902 }
23903
23904 let parsed = Signal::try_from(signal).map_err(|_| {
23905 SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
23906 })?;
23907 let result = send_signal(Pid::from_raw(child_pid as i32), Some(parsed));
23908
23909 match result {
23910 Ok(()) => Ok(()),
23911 Err(nix::errno::Errno::ESRCH) => Ok(()),
23912 Err(error) => Err(SidecarError::Execution(format!(
23913 "failed to signal guest runtime process {child_pid}: {error}"
23914 ))),
23915 }
23916}
23917
23918pub(crate) fn error_code(error: &SidecarError) -> &'static str {
23919 match error {
23920 SidecarError::InvalidState(_) => "invalid_state",
23921 SidecarError::ProtocolVersionMismatch(_) => "protocol_version_mismatch",
23922 SidecarError::BridgeVersionMismatch(_) => "bridge_version_mismatch",
23923 SidecarError::Conflict(_) => "conflict",
23924 SidecarError::Unauthorized(_) => "unauthorized",
23925 SidecarError::Unsupported(_) => "unsupported",
23926 SidecarError::FrameTooLarge(_) => "frame_too_large",
23927 SidecarError::Kernel(_) => "kernel_error",
23928 SidecarError::Plugin(_) => "plugin_error",
23929 SidecarError::Execution(_) => "execution_error",
23930 SidecarError::Bridge(_) => "bridge_error",
23931 SidecarError::Io(_) => "io_error",
23932 }
23933}
23934
23935fn guest_errno_code(message: &str) -> Option<&str> {
23936 const TRUSTED_PREFIXES: &[&str] = &[
23937 "ERR_AGENTOS_NODE_SYNC_RPC",
23938 "ERR_AGENTOS_PYTHON_VFS_RPC",
23939 "ERR_AGENTOS_BRIDGE",
23940 ];
23941
23942 let mut segments = message.split(':').map(str::trim);
23943 let first = segments.next()?;
23944 if is_guest_errno_segment(first) {
23945 return Some(first);
23946 }
23947
23948 if TRUSTED_PREFIXES.contains(&first) {
23949 let second = segments.next()?;
23950 if is_guest_errno_segment(second) {
23951 return Some(second);
23952 }
23953 }
23954
23955 None
23956}
23957
23958fn is_guest_errno_segment(segment: &str) -> bool {
23959 segment.len() >= 2
23960 && segment.starts_with('E')
23961 && !segment.starts_with("ERR_")
23962 && segment[1..]
23963 .bytes()
23964 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
23965}
23966
23967pub(crate) fn javascript_sync_rpc_error_code(error: &SidecarError) -> String {
23968 let message = error.to_string();
23969 if let Some(code) = guest_errno_code(&message) {
23970 return code.to_owned();
23971 }
23972 if message.starts_with("ERR_NATIVE_BINARY_NOT_SUPPORTED:") {
23973 return String::from("ERR_NATIVE_BINARY_NOT_SUPPORTED");
23974 }
23975
23976 let lower = message.to_ascii_lowercase();
23977 if lower.contains("no such file or directory")
23978 || lower.contains("entry not found")
23979 || lower.contains("not found")
23980 {
23981 return String::from("ENOENT");
23982 }
23983 if lower.contains("permission denied") {
23984 return String::from("EACCES");
23985 }
23986 if lower.contains("already exists")
23987 || lower.contains("already registered")
23988 || lower.contains("file exists")
23989 {
23990 return String::from("EEXIST");
23991 }
23992 if lower.contains("invalid argument") {
23993 return String::from("EINVAL");
23994 }
23995
23996 String::from("ERR_AGENTOS_NODE_SYNC_RPC")
23997}
23998
23999pub(crate) fn ignore_stale_javascript_sync_rpc_response(
24000 error: SidecarError,
24001) -> Result<(), SidecarError> {
24002 match error {
24003 SidecarError::Execution(message)
24004 if message.ends_with("is no longer pending")
24005 && message.starts_with("sync RPC request ") =>
24006 {
24007 Ok(())
24008 }
24009 SidecarError::Execution(message) => {
24010 let lower = message.to_ascii_lowercase();
24011 if lower.contains("sync rpc response")
24012 && (lower.contains("broken pipe") || lower.contains("channel closed unexpectedly"))
24013 {
24014 Ok(())
24015 } else {
24016 Err(SidecarError::Execution(message))
24017 }
24018 }
24019 other => Err(other),
24020 }
24021}
24022
24023#[cfg(test)]
24024mod error_code_tests {
24025 use super::{guest_errno_code, javascript_sync_rpc_error_code, SidecarError};
24026
24027 #[test]
24028 fn guest_errno_code_rejects_guest_controlled_errno_segments() {
24029 assert_eq!(guest_errno_code("user said 'EACCES: denied'"), None);
24030 assert_eq!(
24031 guest_errno_code("prefix: user said 'EPERM': more text"),
24032 None
24033 );
24034 assert_eq!(guest_errno_code("ERR_AGENTOS_FAKE: EACCES: denied"), None);
24035 }
24036
24037 #[test]
24038 fn guest_errno_code_accepts_trusted_secure_exec_prefixes() {
24039 assert_eq!(
24040 guest_errno_code("ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo"),
24041 Some("EACCES")
24042 );
24043 assert_eq!(
24044 guest_errno_code("ERR_AGENTOS_PYTHON_VFS_RPC: ENOENT: missing file"),
24045 Some("ENOENT")
24046 );
24047 assert_eq!(guest_errno_code("EEXIST: already exists"), Some("EEXIST"));
24048 }
24049
24050 #[test]
24051 fn javascript_sync_rpc_error_code_ignores_spoofed_errnos() {
24052 let error = SidecarError::Execution(String::from("user said 'EACCES: denied'"));
24053 assert_eq!(
24054 javascript_sync_rpc_error_code(&error),
24055 "ERR_AGENTOS_NODE_SYNC_RPC"
24056 );
24057 }
24058
24059 #[test]
24060 fn javascript_sync_rpc_error_code_preserves_real_sidecar_errnos() {
24061 let error = SidecarError::Execution(String::from(
24062 "ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo",
24063 ));
24064 assert_eq!(javascript_sync_rpc_error_code(&error), "EACCES");
24065 }
24066
24067 #[test]
24068 fn javascript_sync_rpc_error_code_maps_file_exists_messages() {
24069 let error = SidecarError::Io(String::from(
24070 "failed to create mapped guest directory /.next/server: File exists (os error 17)",
24071 ));
24072 assert_eq!(javascript_sync_rpc_error_code(&error), "EEXIST");
24073 }
24074
24075 #[test]
24076 fn javascript_sync_rpc_error_code_preserves_native_binary_rejections() {
24077 let error = SidecarError::Execution(String::from(
24078 "ERR_NATIVE_BINARY_NOT_SUPPORTED: refused to execute native ELF guest binary at /tmp/fake-rg inside the VM",
24079 ));
24080 assert_eq!(
24081 javascript_sync_rpc_error_code(&error),
24082 "ERR_NATIVE_BINARY_NOT_SUPPORTED"
24083 );
24084 }
24085}
24086#[cfg(test)]
24087mod ssrf_egress_classifier_tests {
24088 use super::{
24098 filter_dns_safe_ip_addrs, is_loopback_ip, restricted_non_loopback_ip_range, SidecarError,
24099 };
24100 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
24101
24102 fn assert_restricted(ip: IpAddr, expected_label: &str) {
24103 let classification = restricted_non_loopback_ip_range(ip);
24104 assert!(
24105 classification.is_some(),
24106 "{ip} must be classified as a restricted egress target"
24107 );
24108 let (_cidr, label) = classification.unwrap();
24109 assert_eq!(
24110 label, expected_label,
24111 "{ip} should be labelled {expected_label}, got {label}"
24112 );
24113 }
24114
24115 fn assert_dns_denied(ip: IpAddr, label: &str) {
24116 match filter_dns_safe_ip_addrs(vec![ip], "attacker.example") {
24117 Err(SidecarError::Execution(message)) => assert!(
24118 message.starts_with("EACCES:"),
24119 "{label}: egress filter must deny with EACCES, got: {message}"
24120 ),
24121 other => panic!("{label}: expected EACCES denial, got {other:?}"),
24122 }
24123 }
24124
24125 #[test]
24127 fn classifier_denies_unspecified_and_cgnat_targets() {
24128 assert_restricted(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "unspecified");
24130 assert_restricted(IpAddr::V6(Ipv6Addr::UNSPECIFIED), "unspecified");
24132
24133 assert_restricted(
24135 IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
24136 "carrier-grade-nat",
24137 );
24138 assert_restricted(
24139 IpAddr::V4(Ipv4Addr::new(100, 127, 255, 254)),
24140 "carrier-grade-nat",
24141 );
24142
24143 assert!(
24145 restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 63, 255, 255)))
24146 .is_none(),
24147 "100.63.255.255 is outside CGNAT and must remain allowed"
24148 );
24149 assert!(
24150 restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 128, 0, 0))).is_none(),
24151 "100.128.0.0 is outside CGNAT and must remain allowed"
24152 );
24153
24154 assert_dns_denied(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "0.0.0.0 (unspecified)");
24156 assert_dns_denied(IpAddr::V6(Ipv6Addr::UNSPECIFIED), ":: (unspecified)");
24157 assert_dns_denied(
24158 IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
24159 "100.64.0.1 (CGNAT)",
24160 );
24161 }
24162
24163 #[test]
24165 fn classifier_denies_ipv6_spelled_metadata_addresses() {
24166 let mapped = "::ffff:169.254.169.254".parse::<Ipv6Addr>().unwrap();
24169 assert_restricted(IpAddr::V6(mapped), "link-local");
24170
24171 let compat = "::169.254.169.254".parse::<Ipv6Addr>().unwrap();
24172 assert_restricted(IpAddr::V6(compat), "link-local");
24173
24174 assert_restricted(
24176 IpAddr::V6("::10.0.0.1".parse::<Ipv6Addr>().unwrap()),
24177 "private",
24178 );
24179 assert_restricted(
24180 IpAddr::V6("::100.64.0.1".parse::<Ipv6Addr>().unwrap()),
24181 "carrier-grade-nat",
24182 );
24183
24184 assert_eq!(
24188 restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::UNSPECIFIED)),
24189 Some(("::/128", "unspecified")),
24190 ":: must classify as unspecified, not via the IPv4-compat path"
24191 );
24192 assert!(
24193 restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::LOCALHOST)).is_none()
24194 || is_loopback_ip(IpAddr::V6(Ipv6Addr::LOCALHOST)),
24195 "::1 must not be classified as a restricted IPv4-compatible target"
24196 );
24197 assert!(
24198 restricted_non_loopback_ip_range(IpAddr::V6("::8.8.8.8".parse::<Ipv6Addr>().unwrap()))
24199 .is_none(),
24200 "::8.8.8.8 (public IPv4-compatible) must remain allowed"
24201 );
24202
24203 assert_dns_denied(
24205 IpAddr::V6("::169.254.169.254".parse::<Ipv6Addr>().unwrap()),
24206 "::169.254.169.254 (IPv4-compat metadata)",
24207 );
24208 }
24209
24210 #[test]
24212 fn classifier_denies_reserved_and_multicast_targets() {
24213 assert_restricted(IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)), "multicast");
24217 assert_restricted(IpAddr::V4(Ipv4Addr::new(239, 255, 255, 255)), "multicast");
24218 assert_restricted(IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)), "reserved");
24219 assert_restricted(IpAddr::V4(Ipv4Addr::BROADCAST), "reserved");
24221
24222 assert_restricted(
24224 IpAddr::V6("::224.0.0.1".parse::<Ipv6Addr>().unwrap()),
24225 "multicast",
24226 );
24227 assert_restricted(
24228 IpAddr::V6("::240.0.0.1".parse::<Ipv6Addr>().unwrap()),
24229 "reserved",
24230 );
24231
24232 assert!(
24234 restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(223, 255, 255, 255)))
24235 .is_none(),
24236 "223.255.255.255 is outside 224/4 and must remain allowed"
24237 );
24238
24239 assert_dns_denied(
24241 IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)),
24242 "240.0.0.1 (reserved)",
24243 );
24244 assert_dns_denied(
24245 IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)),
24246 "224.0.0.1 (multicast)",
24247 );
24248 }
24249}
24250
24251#[cfg(test)]
24260mod dns_rebinding_pin_tests {
24261 use super::{issue_outbound_http_request, split_netloc, JavascriptHttpRequestOptions};
24262 use std::collections::BTreeMap;
24263 use std::io::{Read, Write};
24264 use std::net::{IpAddr, Ipv4Addr, TcpListener};
24265 use std::thread;
24266 use url::Url;
24267
24268 fn empty_headers() -> super::HttpHeaderCollection {
24269 super::parse_http_header_collection(&BTreeMap::new(), "test headers")
24270 .expect("empty header collection")
24271 }
24272
24273 fn options() -> JavascriptHttpRequestOptions {
24274 JavascriptHttpRequestOptions {
24275 method: Some(String::from("GET")),
24276 headers: BTreeMap::new(),
24277 body: None,
24278 reject_unauthorized: None,
24279 }
24280 }
24281
24282 #[test]
24283 fn split_netloc_handles_hostnames_and_bracketed_ipv6() {
24284 assert_eq!(
24285 split_netloc("attacker.example:80"),
24286 Some(("attacker.example", 80))
24287 );
24288 assert_eq!(split_netloc("[::1]:443"), Some(("::1", 443)));
24289 assert_eq!(split_netloc("10.0.0.1:8080"), Some(("10.0.0.1", 8080)));
24290 assert_eq!(split_netloc("no-port"), None);
24291 assert_eq!(split_netloc("host:notaport"), None);
24292 }
24293
24294 #[test]
24300 fn outbound_http_connect_is_pinned_to_vetted_ip() {
24301 let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind loopback server");
24302 let port = listener.local_addr().expect("local addr").port();
24303 let server = thread::spawn(move || {
24304 let (mut stream, _) = listener.accept().expect("accept");
24305 let mut buf = [0u8; 1024];
24306 let _ = stream.read(&mut buf);
24307 stream
24308 .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi")
24309 .expect("write response");
24310 let _ = stream.flush();
24311 });
24312
24313 let url = Url::parse(&format!("http://attacker.example:{port}/")).expect("url");
24314 let pinned = vec![IpAddr::V4(Ipv4Addr::LOCALHOST)];
24315 let result = issue_outbound_http_request(&url, &options(), &empty_headers(), &pinned)
24316 .expect("pinned request should reach the vetted loopback target");
24317 let payload = result.as_str().expect("string payload");
24318 assert!(
24319 payload.contains("\"status\":200"),
24320 "expected 200 from pinned target, got: {payload}"
24321 );
24322 server.join().expect("server thread");
24323 }
24324
24325 #[test]
24329 fn outbound_http_refuses_when_no_vetted_address() {
24330 let url = Url::parse("https://attacker.example/").expect("url");
24331 let error = issue_outbound_http_request(&url, &options(), &empty_headers(), &[])
24332 .expect_err("empty pinned set must be refused");
24333 let message = error.to_string();
24334 assert!(
24335 message.contains("EACCES") || message.contains("ERR_HTTP_REQUEST_FAILED"),
24336 "expected an egress refusal, got: {message}"
24337 );
24338 }
24339}