1use crate::wire::{
2 self, AuthenticatedResponse, ExtEnvelope, OwnershipScope, ProtocolCodecError, ProtocolFrame,
3 RequestFrame, RequestId, RequestPayload, ResponseFrame, ResponsePayload, SessionOpenedResponse,
4 SidecarResponseFrame, WireDispatchResult, WireFrameCodec,
5};
6use crate::{
7 EventSinkTransport, Extension, ExtensionInterruptRequest, NativeSidecar, NativeSidecarConfig,
8 SidecarError, SidecarRequestTransport,
9};
10use secure_exec_bridge::queue_tracker::{tracked_sync_channel, TrackedLimit, TrackedSyncSender};
11use secure_exec_bridge::{
12 BridgeTypes, ChmodRequest, ClockBridge, ClockRequest, CommandPermissionRequest,
13 CreateDirRequest, CreateJavascriptContextRequest, CreateWasmContextRequest, DiagnosticRecord,
14 DirectoryEntry, EnvironmentPermissionRequest, EventBridge, ExecutionBridge, ExecutionEvent,
15 ExecutionHandleRequest, FileMetadata, FilesystemBridge, FilesystemPermissionRequest,
16 FilesystemSnapshot, FlushFilesystemStateRequest, GuestContextHandle, KillExecutionRequest,
17 LifecycleEventRecord, LoadFilesystemStateRequest, LogRecord, NetworkPermissionRequest,
18 PathRequest, PermissionBridge, PermissionDecision, PersistenceBridge,
19 PollExecutionEventRequest, RandomBridge, RandomBytesRequest, ReadDirRequest, ReadFileRequest,
20 RenameRequest, ScheduleTimerRequest, ScheduledTimer, StartExecutionRequest, StartedExecution,
21 StructuredEventRecord, SymlinkRequest, TruncateRequest, WriteExecutionStdinRequest,
22 WriteFileRequest,
23};
24use std::collections::{BTreeMap, BTreeSet};
25use std::error::Error;
26use std::fmt;
27use std::fs::{self, OpenOptions};
28use std::io::{self, Read, Write};
29use std::os::unix::fs::{symlink as create_symlink, MetadataExt, PermissionsExt};
30use std::path::{Path, PathBuf};
31use std::sync::{mpsc, Arc, Mutex};
32use std::thread;
33use std::time::{Duration, Instant, SystemTime};
34use tokio::sync::mpsc::{channel, unbounded_channel, Receiver};
35use tokio::time;
36
37const EVENT_PUMP_INTERVAL: Duration = Duration::from_micros(250);
44const MAX_STDIN_FRAME_QUEUE: usize = 128;
45const MAX_EVENT_READY_QUEUE: usize = 1;
46const MAX_STDOUT_FRAME_QUEUE: usize = 4096;
50
51#[cfg(test)]
52fn request_frame(
53 request_id: RequestId,
54 ownership: OwnershipScope,
55 payload: RequestPayload,
56) -> RequestFrame {
57 RequestFrame {
58 schema: wire::protocol_schema(),
59 request_id,
60 ownership,
61 payload,
62 }
63}
64
65fn response_frame(
66 request_id: RequestId,
67 ownership: OwnershipScope,
68 payload: ResponsePayload,
69) -> ResponseFrame {
70 ResponseFrame {
71 schema: wire::protocol_schema(),
72 request_id,
73 ownership,
74 payload,
75 }
76}
77
78#[cfg(test)]
79fn connection_ownership(connection_id: &str) -> OwnershipScope {
80 OwnershipScope::ConnectionOwnership(wire::ConnectionOwnership {
81 connection_id: connection_id.to_owned(),
82 })
83}
84
85fn session_ownership(connection_id: &str, session_id: &str) -> OwnershipScope {
86 OwnershipScope::SessionOwnership(wire::SessionOwnership {
87 connection_id: connection_id.to_owned(),
88 session_id: session_id.to_owned(),
89 })
90}
91
92#[cfg(test)]
93fn vm_ownership(connection_id: &str, session_id: &str, vm_id: &str) -> OwnershipScope {
94 OwnershipScope::VmOwnership(wire::VmOwnership {
95 connection_id: connection_id.to_owned(),
96 session_id: session_id.to_owned(),
97 vm_id: vm_id.to_owned(),
98 })
99}
100
101fn wire_protocol_error(error: ProtocolCodecError) -> SidecarError {
102 SidecarError::InvalidState(format!("invalid generated wire protocol frame: {error}"))
103}
104
105pub fn run() -> Result<(), Box<dyn Error>> {
106 run_with_extensions(Vec::new())
107}
108
109pub fn run_with_extensions(extensions: Vec<Box<dyn Extension>>) -> Result<(), Box<dyn Error>> {
110 if let Err(error) = secure_exec_execution::v8_host::ensure_runtime_initialized() {
115 eprintln!("embedded V8 runtime init failed at startup: {error}");
116 }
117 tokio::runtime::Builder::new_current_thread()
118 .enable_all()
119 .build()?
120 .block_on(run_async(extensions))
121}
122
123async fn run_async(extensions: Vec<Box<dyn Extension>>) -> Result<(), Box<dyn Error>> {
124 let config = NativeSidecarConfig {
125 compile_cache_root: Some(default_compile_cache_root()),
126 ..NativeSidecarConfig::default()
127 };
128 let codec = WireFrameCodec::new(config.max_frame_bytes);
129 let mut sidecar =
130 NativeSidecar::with_config_and_extensions(LocalBridge::default(), config, extensions)?;
131 let mut active_sessions = BTreeSet::<SessionScope>::new();
132 let mut active_connections = BTreeSet::<String>::new();
133 let (stdin_tx, mut stdin_rx) =
134 channel::<Result<Option<ProtocolFrame>, String>>(MAX_STDIN_FRAME_QUEUE);
135 let stdin_gauge = secure_exec_bridge::queue_tracker::register_queue(
136 TrackedLimit::SidecarStdinFrames,
137 MAX_STDIN_FRAME_QUEUE,
138 );
139 let (event_ready_tx, mut event_ready_rx) = channel::<()>(MAX_EVENT_READY_QUEUE);
140 let (write_tx, write_rx) = tracked_sync_channel::<ProtocolFrame>(
141 TrackedLimit::SidecarStdoutFrames,
142 MAX_STDOUT_FRAME_QUEUE,
143 );
144 let (write_error_tx, mut write_error_rx) = unbounded_channel::<String>();
145
146 let (limit_warning_tx, mut limit_warning_rx) =
152 unbounded_channel::<secure_exec_bridge::queue_tracker::LimitWarning>();
153 secure_exec_bridge::queue_tracker::set_limit_warning_handler(Box::new(move |warning| {
154 let _ = limit_warning_tx.send(warning.clone());
155 }));
156 let callback_transport = Arc::new(FrameSidecarRequestTransport::new(write_tx.clone()));
157 sidecar.set_sidecar_request_transport(callback_transport.clone());
158 let event_transport = Arc::new(FrameEventTransport::new(write_tx.clone()));
163 sidecar.set_event_transport(event_transport);
164 let mut event_pump = time::interval(EVENT_PUMP_INTERVAL);
165 let writer_codec = codec.clone();
166 let reader_codec = codec.clone();
167 let writer_error_tx = write_error_tx.clone();
168 thread::spawn(move || {
169 let mut writer = io::BufWriter::new(io::stdout());
170 while let Ok(frame) = write_rx.recv() {
171 if let Err(error) = write_frame(&writer_codec, &mut writer, &frame) {
172 let _ = writer_error_tx.send(error.to_string());
173 break;
174 }
175 }
176 });
177
178 thread::spawn({
179 let callback_transport = callback_transport.clone();
180 let read_error_tx = write_error_tx.clone();
181 move || {
182 let mut stdin = io::stdin();
183 loop {
184 let frame = match read_frame(&reader_codec, &mut stdin) {
185 Ok(Some(ProtocolFrame::SidecarResponseFrame(response))) => {
186 if callback_transport.accept_response(response.clone()) {
187 continue;
188 }
189 Ok(Some(ProtocolFrame::SidecarResponseFrame(response)))
190 }
191 Ok(Some(frame)) => Ok(Some(frame)),
192 other => other,
193 }
194 .map_err(|error: Box<dyn Error>| error.to_string());
195 let should_stop = matches!(frame, Ok(None) | Err(_));
196 match enqueue_stdin_frame(&stdin_tx, frame) {
197 Ok(()) => {
198 stdin_gauge.observe_depth(
201 stdin_tx.max_capacity().saturating_sub(stdin_tx.capacity()),
202 );
203 }
204 Err(StdinFrameQueueError::Full(message)) => {
205 let _ = read_error_tx.send(message);
206 break;
207 }
208 Err(StdinFrameQueueError::Closed) => break,
209 }
210 if should_stop {
211 break;
212 }
213 }
214 }
215 });
216
217 flush_sidecar_requests(&mut sidecar, &write_tx)?;
218 let mut pending_frame: Option<ProtocolFrame> = None;
219 let mut limit_warning_closed = false;
220
221 loop {
222 if let Some(frame) = pending_frame.take() {
223 handle_protocol_frame(
224 frame,
225 &mut sidecar,
226 &mut stdin_rx,
227 &mut pending_frame,
228 &write_tx,
229 &mut active_sessions,
230 &mut active_connections,
231 )
232 .await?;
233 continue;
234 }
235
236 tokio::select! {
237 maybe_frame = stdin_rx.recv() => {
238 let Some(frame) = maybe_frame else {
239 break;
240 };
241 let Some(frame) = frame.map_err(io::Error::other)? else {
242 break;
243 };
244 handle_protocol_frame(
245 frame,
246 &mut sidecar,
247 &mut stdin_rx,
248 &mut pending_frame,
249 &write_tx,
250 &mut active_sessions,
251 &mut active_connections,
252 ).await?;
253 }
254 maybe_warning = limit_warning_rx.recv(), if !limit_warning_closed => {
255 match maybe_warning {
256 Some(warning) => {
257 if let Some(connection_id) = active_connections.iter().next() {
263 let mut detail = std::collections::HashMap::new();
264 detail.insert(String::from("limit"), warning.name.as_str().to_string());
265 detail.insert(
266 String::from("category"),
267 warning.category.as_str().to_string(),
268 );
269 detail.insert(String::from("observed"), warning.observed.to_string());
270 detail.insert(String::from("capacity"), warning.capacity.to_string());
271 detail.insert(
272 String::from("fillPercent"),
273 warning.fill_percent.to_string(),
274 );
275 let frame = crate::service::structured_event_frame(
276 connection_id,
277 "limit_warning",
278 detail,
279 )?;
280 send_output_frame(&write_tx, ProtocolFrame::EventFrame(frame))?;
281 }
282 }
283 None => {
284 limit_warning_closed = true;
289 }
290 }
291 }
292 maybe_ready = event_ready_rx.recv() => {
293 let Some(()) = maybe_ready else {
294 break;
295 };
296 loop {
297 let mut emitted_frame = false;
298 for session in active_sessions.iter().cloned().collect::<Vec<_>>() {
299 if let Some(frame) = sidecar
300 .poll_event_wire(&session.ownership_scope(), Duration::ZERO)
301 .await?
302 {
303 send_output_frame(&write_tx, ProtocolFrame::EventFrame(frame))?;
304 emitted_frame = true;
305 }
306 }
307
308 if !emitted_frame {
309 break;
310 }
311 }
312 flush_sidecar_requests(&mut sidecar, &write_tx)?;
313 }
314 _ = event_pump.tick() => {
315 for session in active_sessions.iter().cloned().collect::<Vec<_>>() {
316 if sidecar.pump_process_events(&session.compat_ownership_scope()).await? {
317 let _ = event_ready_tx.try_send(());
318 }
319 }
320 flush_sidecar_requests(&mut sidecar, &write_tx)?;
321 }
322 maybe_write_error = write_error_rx.recv() => {
323 if let Some(error) = maybe_write_error {
324 return Err(io::Error::new(io::ErrorKind::BrokenPipe, error).into());
325 }
326 }
327 }
328 }
329
330 cleanup_connections(&mut sidecar, &active_connections, &mut active_sessions).await;
331 Ok(())
332}
333
334async fn handle_protocol_frame(
335 frame: ProtocolFrame,
336 sidecar: &mut NativeSidecar<LocalBridge>,
337 stdin_rx: &mut Receiver<Result<Option<ProtocolFrame>, String>>,
338 pending_frame: &mut Option<ProtocolFrame>,
339 write_tx: &TrackedSyncSender<ProtocolFrame>,
340 active_sessions: &mut BTreeSet<SessionScope>,
341 active_connections: &mut BTreeSet<String>,
342) -> Result<(), Box<dyn Error>> {
343 match frame {
344 ProtocolFrame::RequestFrame(request) => {
345 let (dispatch, extra_responses) =
346 dispatch_with_prompt_interrupt(sidecar, request.clone(), stdin_rx, pending_frame)
347 .await?;
348 track_session_state(
349 &dispatch.response.payload,
350 active_sessions,
351 active_connections,
352 );
353
354 send_output_frame(write_tx, ProtocolFrame::ResponseFrame(dispatch.response))?;
355 for response in extra_responses {
356 send_output_frame(write_tx, ProtocolFrame::ResponseFrame(response))?;
357 }
358 for event in dispatch.events {
359 send_output_frame(write_tx, ProtocolFrame::EventFrame(event))?;
360 }
361 flush_sidecar_requests(sidecar, write_tx)?;
362 }
363 ProtocolFrame::SidecarResponseFrame(response) => {
364 sidecar.accept_wire_sidecar_response(response)?;
365 flush_sidecar_requests(sidecar, write_tx)?;
366 }
367 other => {
368 return Err(format!(
369 "expected request or sidecar_response frame on stdin, received {}",
370 frame_kind(&other)
371 )
372 .into());
373 }
374 }
375 untrack_disposed_sessions(&sidecar.take_disposed_sessions(), active_sessions);
378 Ok(())
379}
380
381fn untrack_disposed_sessions(
386 disposed: &[(String, String)],
387 active_sessions: &mut BTreeSet<SessionScope>,
388) {
389 for (connection_id, session_id) in disposed {
390 active_sessions.remove(&SessionScope {
391 connection_id: connection_id.clone(),
392 session_id: session_id.clone(),
393 });
394 }
395}
396
397async fn dispatch_with_prompt_interrupt(
398 sidecar: &mut NativeSidecar<LocalBridge>,
399 request: RequestFrame,
400 stdin_rx: &mut Receiver<Result<Option<ProtocolFrame>, String>>,
401 pending_frame: &mut Option<ProtocolFrame>,
402) -> Result<(WireDispatchResult, Vec<ResponseFrame>), Box<dyn Error>> {
403 let Some(blocking_request) = blocking_extension_request(sidecar, &request) else {
404 return Ok((sidecar.dispatch_wire(request).await?, Vec::new()));
405 };
406
407 let mut dispatch = Box::pin(sidecar.dispatch_wire(request.clone()));
408 tokio::select! {
409 result = dispatch.as_mut() => Ok((result?, Vec::new())),
410 maybe_frame = stdin_rx.recv() => {
411 let frame = decode_stdin_frame(maybe_frame)?;
412 if let Some(frame) = frame {
413 if let Some(interrupt) = extension_interrupt_response(&blocking_request, &request, &frame) {
414 drop(dispatch);
415 let mut extra_responses = Vec::new();
416 if let Some(response) = interrupt.interrupting_response {
417 extra_responses.push(response);
418 } else {
419 *pending_frame = Some(frame);
420 }
421 return Ok((interrupt.interrupted_dispatch, extra_responses));
422 }
423 *pending_frame = Some(frame);
424 }
425 Ok((dispatch.await?, Vec::new()))
426 }
427 }
428}
429
430fn decode_stdin_frame(
431 maybe_frame: Option<Result<Option<ProtocolFrame>, String>>,
432) -> Result<Option<ProtocolFrame>, Box<dyn Error>> {
433 let Some(frame) = maybe_frame else {
434 return Ok(None);
435 };
436 Ok(frame.map_err(io::Error::other)?)
437}
438
439struct BlockingExtensionRequest {
440 namespace: String,
441 payload: Vec<u8>,
442 extension: Arc<dyn Extension>,
443}
444
445struct ExtensionInterruptDispatch {
446 interrupted_dispatch: WireDispatchResult,
447 interrupting_response: Option<ResponseFrame>,
448}
449
450fn blocking_extension_request(
451 sidecar: &NativeSidecar<LocalBridge>,
452 request: &RequestFrame,
453) -> Option<BlockingExtensionRequest> {
454 let RequestPayload::ExtEnvelope(envelope) = &request.payload else {
455 return None;
456 };
457 let extension = sidecar.extensions.get(&envelope.namespace)?.clone();
458 if !extension.is_blocking_request(&envelope.payload) {
459 return None;
460 }
461 Some(BlockingExtensionRequest {
462 namespace: envelope.namespace.clone(),
463 payload: envelope.payload.clone(),
464 extension,
465 })
466}
467
468fn extension_interrupt_response(
469 blocking_request: &BlockingExtensionRequest,
470 active_request: &RequestFrame,
471 frame: &ProtocolFrame,
472) -> Option<ExtensionInterruptDispatch> {
473 match frame {
474 ProtocolFrame::RequestFrame(request) => {
475 if request.ownership != active_request.ownership {
476 return None;
477 }
478 let interrupt = match &request.payload {
479 RequestPayload::ExtEnvelope(envelope)
480 if envelope.namespace == blocking_request.namespace =>
481 {
482 blocking_request.extension.interrupt_blocking_request(
483 &blocking_request.payload,
484 ExtensionInterruptRequest::ExtensionPayload(&envelope.payload),
485 )?
486 }
487 RequestPayload::ExtEnvelope(_) => return None,
488 RequestPayload::KillProcessRequest(_) => {
489 blocking_request.extension.interrupt_blocking_request(
490 &blocking_request.payload,
491 ExtensionInterruptRequest::KillProcess,
492 )?
493 }
494 RequestPayload::AuthenticateRequest(_)
499 | RequestPayload::OpenSessionRequest(_)
500 | RequestPayload::CreateVmRequest(_)
501 | RequestPayload::DisposeVmRequest(_)
502 | RequestPayload::BootstrapRootFilesystemRequest(_)
503 | RequestPayload::ConfigureVmRequest(_)
504 | RequestPayload::RegisterHostCallbacksRequest(_)
505 | RequestPayload::CreateLayerRequest
506 | RequestPayload::SealLayerRequest(_)
507 | RequestPayload::ImportSnapshotRequest(_)
508 | RequestPayload::ExportSnapshotRequest(_)
509 | RequestPayload::CreateOverlayRequest(_)
510 | RequestPayload::GuestFilesystemCallRequest(_)
511 | RequestPayload::SnapshotRootFilesystemRequest
512 | RequestPayload::ExecuteRequest(_)
513 | RequestPayload::WriteStdinRequest(_)
514 | RequestPayload::CloseStdinRequest(_)
515 | RequestPayload::GetProcessSnapshotRequest
516 | RequestPayload::FindListenerRequest(_)
517 | RequestPayload::FindBoundUdpRequest(_)
518 | RequestPayload::VmFetchRequest(_)
519 | RequestPayload::GetSignalStateRequest(_)
520 | RequestPayload::GetZombieTimerCountRequest
521 | RequestPayload::HostFilesystemCallRequest(_)
522 | RequestPayload::PersistenceLoadRequest(_)
523 | RequestPayload::PersistenceFlushRequest(_) => return None,
524 };
525 let interrupted_dispatch = interrupted_extension_dispatch(
526 active_request,
527 &blocking_request.namespace,
528 interrupt.interrupted_response_payload,
529 );
530 let interrupting_response = interrupt.interrupting_response_payload.map(|payload| {
531 response_frame(
532 request.request_id,
533 request.ownership.clone(),
534 ResponsePayload::ExtEnvelope(ExtEnvelope {
535 namespace: blocking_request.namespace.clone(),
536 payload,
537 }),
538 )
539 });
540 Some(ExtensionInterruptDispatch {
541 interrupted_dispatch,
542 interrupting_response,
543 })
544 }
545 ProtocolFrame::ResponseFrame(_)
551 | ProtocolFrame::EventFrame(_)
552 | ProtocolFrame::SidecarRequestFrame(_)
553 | ProtocolFrame::SidecarResponseFrame(_) => None,
554 }
555}
556
557fn interrupted_extension_dispatch(
558 request: &RequestFrame,
559 namespace: &str,
560 payload: Vec<u8>,
561) -> WireDispatchResult {
562 match &request.payload {
563 RequestPayload::ExtEnvelope(_) => {
564 let response = ResponsePayload::ExtEnvelope(ExtEnvelope {
565 namespace: namespace.to_string(),
566 payload,
567 });
568 WireDispatchResult {
569 response: response_frame(request.request_id, request.ownership.clone(), response),
570 events: Vec::new(),
571 }
572 }
573 RequestPayload::AuthenticateRequest(_)
574 | RequestPayload::OpenSessionRequest(_)
575 | RequestPayload::CreateVmRequest(_)
576 | RequestPayload::DisposeVmRequest(_)
577 | RequestPayload::BootstrapRootFilesystemRequest(_)
578 | RequestPayload::ConfigureVmRequest(_)
579 | RequestPayload::RegisterHostCallbacksRequest(_)
580 | RequestPayload::CreateLayerRequest
581 | RequestPayload::SealLayerRequest(_)
582 | RequestPayload::ImportSnapshotRequest(_)
583 | RequestPayload::ExportSnapshotRequest(_)
584 | RequestPayload::CreateOverlayRequest(_)
585 | RequestPayload::GuestFilesystemCallRequest(_)
586 | RequestPayload::SnapshotRootFilesystemRequest
587 | RequestPayload::ExecuteRequest(_)
588 | RequestPayload::WriteStdinRequest(_)
589 | RequestPayload::CloseStdinRequest(_)
590 | RequestPayload::KillProcessRequest(_)
591 | RequestPayload::GetProcessSnapshotRequest
592 | RequestPayload::FindListenerRequest(_)
593 | RequestPayload::FindBoundUdpRequest(_)
594 | RequestPayload::VmFetchRequest(_)
595 | RequestPayload::GetSignalStateRequest(_)
596 | RequestPayload::GetZombieTimerCountRequest
597 | RequestPayload::HostFilesystemCallRequest(_)
598 | RequestPayload::PersistenceLoadRequest(_)
599 | RequestPayload::PersistenceFlushRequest(_) => {
600 unreachable!("interrupted extension dispatch requires an extension request");
601 }
602 }
603}
604
605async fn cleanup_connections(
606 sidecar: &mut NativeSidecar<LocalBridge>,
607 active_connections: &BTreeSet<String>,
608 active_sessions: &mut BTreeSet<SessionScope>,
609) {
610 for connection_id in active_connections {
611 let _ = sidecar.remove_connection(connection_id).await;
612 }
613 untrack_disposed_sessions(&sidecar.take_disposed_sessions(), active_sessions);
614}
615
616fn track_session_state(
617 payload: &ResponsePayload,
618 active_sessions: &mut BTreeSet<SessionScope>,
619 active_connections: &mut BTreeSet<String>,
620) {
621 match payload {
622 ResponsePayload::AuthenticatedResponse(AuthenticatedResponse { connection_id, .. }) => {
623 active_connections.insert(connection_id.clone());
624 }
625 ResponsePayload::SessionOpenedResponse(SessionOpenedResponse {
626 session_id,
627 owner_connection_id,
628 }) => {
629 active_sessions.insert(SessionScope {
630 connection_id: owner_connection_id.clone(),
631 session_id: session_id.clone(),
632 });
633 }
634 _ => {}
635 }
636}
637
638fn read_frame(
639 codec: &WireFrameCodec,
640 reader: &mut impl Read,
641) -> Result<Option<ProtocolFrame>, Box<dyn Error>> {
642 let mut prefix = [0u8; 4];
643 match reader.read_exact(&mut prefix) {
644 Ok(()) => {}
645 Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => {
646 return Ok(None);
647 }
648 Err(error) => return Err(error.into()),
649 }
650
651 let declared_len = u32::from_be_bytes(prefix) as usize;
652 if declared_len > codec.max_frame_bytes() {
653 return Err(ProtocolCodecError::FrameTooLarge {
654 size: declared_len,
655 max: codec.max_frame_bytes(),
656 }
657 .into());
658 }
659 let total_len = prefix.len().saturating_add(declared_len);
660 let mut bytes = Vec::with_capacity(total_len);
661 bytes.extend_from_slice(&prefix);
662 bytes.resize(total_len, 0);
663 reader.read_exact(&mut bytes[prefix.len()..])?;
664
665 Ok(Some(codec.decode(&bytes)?))
666}
667
668fn write_frame(
669 codec: &WireFrameCodec,
670 writer: &mut impl Write,
671 frame: &ProtocolFrame,
672) -> Result<(), Box<dyn Error>> {
673 let bytes = codec.encode(frame)?;
674 writer.write_all(&bytes)?;
675 writer.flush()?;
676 Ok(())
677}
678
679fn frame_kind(frame: &ProtocolFrame) -> &'static str {
680 match frame {
681 ProtocolFrame::RequestFrame(_) => "request",
682 ProtocolFrame::ResponseFrame(_) => "response",
683 ProtocolFrame::EventFrame(_) => "event",
684 ProtocolFrame::SidecarRequestFrame(_) => "sidecar_request",
685 ProtocolFrame::SidecarResponseFrame(_) => "sidecar_response",
686 }
687}
688
689#[derive(Debug, Clone, PartialEq, Eq)]
690enum StdinFrameQueueError {
691 Full(String),
692 Closed,
693}
694
695fn enqueue_stdin_frame(
696 sender: &tokio::sync::mpsc::Sender<Result<Option<ProtocolFrame>, String>>,
697 frame: Result<Option<ProtocolFrame>, String>,
698) -> Result<(), StdinFrameQueueError> {
699 sender.try_send(frame).map_err(|error| match error {
700 tokio::sync::mpsc::error::TrySendError::Full(_) => StdinFrameQueueError::Full(format!(
701 "stdin frame queue exceeded {MAX_STDIN_FRAME_QUEUE} pending frames"
702 )),
703 tokio::sync::mpsc::error::TrySendError::Closed(_) => StdinFrameQueueError::Closed,
704 })
705}
706
707fn flush_sidecar_requests(
708 sidecar: &mut NativeSidecar<LocalBridge>,
709 writer: &TrackedSyncSender<ProtocolFrame>,
710) -> Result<(), Box<dyn Error>> {
711 while let Some(request) = sidecar.pop_wire_sidecar_request()? {
712 send_output_frame(writer, ProtocolFrame::SidecarRequestFrame(request))?;
713 }
714 Ok(())
715}
716
717fn send_output_frame(
718 writer: &TrackedSyncSender<ProtocolFrame>,
719 frame: ProtocolFrame,
720) -> Result<(), io::Error> {
721 writer.send(frame).map_err(|_disconnected| {
732 io::Error::new(io::ErrorKind::BrokenPipe, "stdout writer disconnected")
733 })
734}
735
736fn default_compile_cache_root() -> PathBuf {
737 std::env::temp_dir().join("secure-exec-sidecar-compile-cache")
744}
745
746#[cfg(test)]
747mod tests {
748 use super::*;
749 use crate::wire::{AuthenticateRequest, KillProcessRequest};
750 use crate::{ExtensionContext, ExtensionFuture, ExtensionInterruptResponse, ExtensionResponse};
751 use std::io::Cursor;
752
753 const TEST_EXTENSION_NAMESPACE: &str = "dev.rivet.secure-exec.test.blocking";
754
755 #[test]
756 fn read_frame_rejects_oversized_prefix_before_allocating_payload() {
757 let codec = WireFrameCodec::new(16);
758 let mut reader = Cursor::new((32_u32).to_be_bytes().to_vec());
759
760 let error = read_frame(&codec, &mut reader).expect_err("oversized frame should fail");
761 let error = error
762 .downcast::<ProtocolCodecError>()
763 .expect("protocol codec error");
764 assert!(matches!(
765 *error,
766 ProtocolCodecError::FrameTooLarge { size: 32, max: 16 }
767 ));
768 }
769
770 #[test]
771 fn stdio_work_queues_are_bounded() {
772 let (stdin_tx, _stdin_rx) =
773 channel::<Result<Option<ProtocolFrame>, String>>(MAX_STDIN_FRAME_QUEUE);
774 for _ in 0..MAX_STDIN_FRAME_QUEUE {
775 enqueue_stdin_frame(&stdin_tx, Ok(None))
776 .expect("stdin frame queue should accept capacity");
777 }
778 assert!(matches!(
779 enqueue_stdin_frame(&stdin_tx, Ok(None)),
780 Err(StdinFrameQueueError::Full(_))
781 ));
782
783 let (event_ready_tx, _event_ready_rx) = channel::<()>(MAX_EVENT_READY_QUEUE);
784 event_ready_tx
785 .try_send(())
786 .expect("event-ready queue should accept capacity");
787 assert!(matches!(
788 event_ready_tx.try_send(()),
789 Err(tokio::sync::mpsc::error::TrySendError::Full(_))
790 ));
791 }
792
793 #[test]
800 fn stdout_frame_queue_applies_backpressure_instead_of_crashing() {
801 let queue_frame = |request_id: RequestId| {
802 ProtocolFrame::RequestFrame(request_frame(
803 request_id,
804 connection_ownership("conn-queue"),
805 RequestPayload::AuthenticateRequest(AuthenticateRequest {
806 client_name: String::from("queue-test"),
807 auth_token: String::from("token"),
808 protocol_version: wire::PROTOCOL_VERSION,
809 bridge_version: secure_exec_bridge::bridge_contract().version,
810 }),
811 ))
812 };
813
814 let queue_cap = 8usize;
819 let total_frames = queue_cap * 3;
820 let (stdout_tx, stdout_rx) =
821 tracked_sync_channel::<ProtocolFrame>(TrackedLimit::SidecarStdoutFrames, queue_cap);
822 let drainer = std::thread::spawn(move || {
823 let mut drained = 0usize;
824 while stdout_rx.recv().is_ok() {
825 drained += 1;
826 std::thread::sleep(std::time::Duration::from_millis(1));
827 }
828 drained
829 });
830
831 for request_id in 0..total_frames {
832 send_output_frame(&stdout_tx, queue_frame(request_id as RequestId))
833 .expect("backpressured stdout queue must accept frames, not crash");
834 }
835 drop(stdout_tx);
836 let drained = drainer.join().expect("drainer thread panicked");
837 assert_eq!(
838 drained, total_frames,
839 "every frame must survive the backpressured queue"
840 );
841
842 let (closed_tx, closed_rx) =
845 tracked_sync_channel::<ProtocolFrame>(TrackedLimit::SidecarStdoutFrames, queue_cap);
846 drop(closed_rx);
847 let error = send_output_frame(&closed_tx, queue_frame(0))
848 .expect_err("send to a dropped writer must error");
849 assert_eq!(error.kind(), io::ErrorKind::BrokenPipe);
850 }
851
852 #[test]
858 fn disposed_sessions_are_untracked_from_active_sessions() {
859 let mut active_sessions = BTreeSet::<SessionScope>::new();
860 let mut active_connections = BTreeSet::<String>::new();
861 track_session_state(
862 &ResponsePayload::SessionOpenedResponse(SessionOpenedResponse {
863 session_id: String::from("session-1"),
864 owner_connection_id: String::from("conn-1"),
865 }),
866 &mut active_sessions,
867 &mut active_connections,
868 );
869 assert_eq!(
870 active_sessions.len(),
871 1,
872 "opening a session should track it for the event pump"
873 );
874
875 untrack_disposed_sessions(
876 &[(String::from("conn-1"), String::from("session-1"))],
877 &mut active_sessions,
878 );
879 assert!(
880 active_sessions.is_empty(),
881 "a disposed session must be removed from the active-session set"
882 );
883 }
884
885 #[test]
886 fn read_frame_decodes_wire_authenticate_request() {
887 let codec = WireFrameCodec::new(wire::DEFAULT_MAX_FRAME_BYTES);
888 let frame = ProtocolFrame::RequestFrame(request_frame(
889 1,
890 connection_ownership("client-hint"),
891 RequestPayload::AuthenticateRequest(AuthenticateRequest {
892 client_name: "probe".to_string(),
893 auth_token: "probe-token".to_string(),
894 protocol_version: wire::PROTOCOL_VERSION,
895 bridge_version: secure_exec_bridge::bridge_contract().version,
896 }),
897 ));
898 let encoded = codec.encode(&frame).expect("encode wire frame");
899 let mut reader = Cursor::new(encoded);
900
901 let decoded = read_frame(&codec, &mut reader)
902 .expect("decode bare frame")
903 .expect("frame present");
904
905 assert_eq!(decoded, frame);
906 }
907
908 #[test]
909 fn extension_close_interrupts_matching_blocking_request() {
910 let ownership = vm_ownership("conn-1", "session-1", "vm-1");
911 let prompt = test_extension_request_frame(10, ownership.clone(), "prompt:ext-session-1");
912 let close = ProtocolFrame::RequestFrame(test_extension_request_frame(
913 11,
914 ownership,
915 "close:ext-session-1",
916 ));
917
918 let blocking_request = blocking_extension_request(&prompt);
919 let interrupt = extension_interrupt_response(&blocking_request, &prompt, &close)
920 .expect("close should interrupt prompt");
921
922 assert_eq!(interrupt.interrupted_dispatch.response.request_id, 10);
923 let ResponsePayload::ExtEnvelope(envelope) =
924 interrupt.interrupted_dispatch.response.payload
925 else {
926 panic!("expected extension response");
927 };
928 assert_eq!(envelope.namespace, TEST_EXTENSION_NAMESPACE);
929 assert_eq!(envelope.payload, b"prompt-cancelled:ext-session-1");
930 }
931
932 #[test]
933 fn extension_cancel_interrupt_gets_synthetic_response() {
934 let ownership = vm_ownership("conn-1", "session-1", "vm-1");
935 let prompt = test_extension_request_frame(10, ownership.clone(), "prompt:ext-session-1");
936 let cancel = ProtocolFrame::RequestFrame(test_extension_request_frame(
937 11,
938 ownership,
939 "cancel:ext-session-1",
940 ));
941
942 let blocking_request = blocking_extension_request(&prompt);
943 let interrupt = extension_interrupt_response(&blocking_request, &prompt, &cancel)
944 .expect("cancel should interrupt prompt");
945 let response = interrupt
946 .interrupting_response
947 .expect("cancel should get a response");
948
949 assert_eq!(response.request_id, 11);
950 let ResponsePayload::ExtEnvelope(envelope) = response.payload else {
951 panic!("expected extension response");
952 };
953 assert_eq!(envelope.namespace, TEST_EXTENSION_NAMESPACE);
954 assert_eq!(envelope.payload, b"cancelled:ext-session-1");
955 }
956
957 #[test]
958 fn kill_process_interrupts_blocking_extension_request() {
959 let ownership = vm_ownership("conn-1", "session-1", "vm-1");
960 let prompt = test_extension_request_frame(10, ownership.clone(), "prompt:ext-session-1");
961 let kill = ProtocolFrame::RequestFrame(request_frame(
962 11,
963 ownership,
964 RequestPayload::KillProcessRequest(KillProcessRequest {
965 process_id: "adapter-process".to_string(),
966 signal: "SIGTERM".to_string(),
967 }),
968 ));
969
970 let blocking_request = blocking_extension_request(&prompt);
971 let interrupt = extension_interrupt_response(&blocking_request, &prompt, &kill)
972 .expect("kill should interrupt prompt");
973
974 assert_eq!(interrupt.interrupted_dispatch.response.request_id, 10);
975 assert!(interrupt.interrupting_response.is_none());
976 }
977
978 fn test_extension_request_frame(
979 request_id: RequestId,
980 ownership: OwnershipScope,
981 payload: &str,
982 ) -> RequestFrame {
983 request_frame(
984 request_id,
985 ownership,
986 RequestPayload::ExtEnvelope(ExtEnvelope {
987 namespace: TEST_EXTENSION_NAMESPACE.to_string(),
988 payload: payload.as_bytes().to_vec(),
989 }),
990 )
991 }
992
993 fn blocking_extension_request(request: &RequestFrame) -> BlockingExtensionRequest {
994 let RequestPayload::ExtEnvelope(envelope) = &request.payload else {
995 panic!("expected extension request");
996 };
997 BlockingExtensionRequest {
998 namespace: TEST_EXTENSION_NAMESPACE.to_string(),
999 payload: envelope.payload.clone(),
1000 extension: Arc::new(TestBlockingInterruptExtension),
1001 }
1002 }
1003
1004 struct TestBlockingInterruptExtension;
1005
1006 impl Extension for TestBlockingInterruptExtension {
1007 fn namespace(&self) -> &str {
1008 TEST_EXTENSION_NAMESPACE
1009 }
1010
1011 fn handle_request<'a>(
1012 &'a self,
1013 _ctx: ExtensionContext<'a>,
1014 _payload: Vec<u8>,
1015 ) -> ExtensionFuture<'a, ExtensionResponse> {
1016 Box::pin(async { Ok(ExtensionResponse::new(Vec::new())) })
1017 }
1018
1019 fn is_blocking_request(&self, payload: &[u8]) -> bool {
1020 parse_test_payload(payload).is_some_and(|(kind, _session_id)| kind == "prompt")
1021 }
1022
1023 fn interrupt_blocking_request(
1024 &self,
1025 blocking_payload: &[u8],
1026 interrupt: ExtensionInterruptRequest<'_>,
1027 ) -> Option<ExtensionInterruptResponse> {
1028 let (blocking_kind, blocking_session_id) = parse_test_payload(blocking_payload)?;
1029 if blocking_kind != "prompt" {
1030 return None;
1031 }
1032
1033 let interrupted_response_payload =
1034 encode_test_response("prompt-cancelled", blocking_session_id);
1035 match interrupt {
1036 ExtensionInterruptRequest::KillProcess => Some(ExtensionInterruptResponse {
1037 interrupted_response_payload,
1038 interrupting_response_payload: None,
1039 }),
1040 ExtensionInterruptRequest::ExtensionPayload(payload) => {
1041 let (interrupt_kind, interrupt_session_id) = parse_test_payload(payload)?;
1042 match interrupt_kind {
1043 "close" if interrupt_session_id == blocking_session_id => {
1044 Some(ExtensionInterruptResponse {
1045 interrupted_response_payload,
1046 interrupting_response_payload: None,
1047 })
1048 }
1049 "cancel" if interrupt_session_id == blocking_session_id => {
1050 Some(ExtensionInterruptResponse {
1051 interrupted_response_payload,
1052 interrupting_response_payload: Some(encode_test_response(
1053 "cancelled",
1054 interrupt_session_id,
1055 )),
1056 })
1057 }
1058 "prompt" | "close" | "cancel" => None,
1059 _ => None,
1060 }
1061 }
1062 }
1063 }
1064 }
1065
1066 fn parse_test_payload(payload: &[u8]) -> Option<(&str, &str)> {
1067 let payload = std::str::from_utf8(payload).ok()?;
1068 payload.split_once(':')
1069 }
1070
1071 fn encode_test_response(kind: &str, session_id: &str) -> Vec<u8> {
1072 format!("{kind}:{session_id}").into_bytes()
1073 }
1074}
1075
1076#[derive(Debug, Clone)]
1077pub(crate) struct LocalBridge {
1078 started_at: Instant,
1079 next_timer_id: usize,
1080 snapshots: BTreeMap<String, FilesystemSnapshot>,
1081}
1082
1083impl Default for LocalBridge {
1084 fn default() -> Self {
1085 Self {
1086 started_at: Instant::now(),
1087 next_timer_id: 0,
1088 snapshots: BTreeMap::new(),
1089 }
1090 }
1091}
1092
1093impl BridgeTypes for LocalBridge {
1094 type Error = LocalBridgeError;
1095}
1096
1097impl FilesystemBridge for LocalBridge {
1098 fn read_file(&mut self, request: ReadFileRequest) -> Result<Vec<u8>, Self::Error> {
1099 fs::read(Self::host_path(&request.path))
1100 .map_err(|error| LocalBridgeError::io("read", &request.path, error))
1101 }
1102
1103 fn write_file(&mut self, request: WriteFileRequest) -> Result<(), Self::Error> {
1104 let host_path = Self::host_path(&request.path);
1105 if let Some(parent) = host_path.parent() {
1106 fs::create_dir_all(parent)
1107 .map_err(|error| LocalBridgeError::io("mkdir", &request.path, error))?;
1108 }
1109 fs::write(host_path, request.contents)
1110 .map_err(|error| LocalBridgeError::io("write", &request.path, error))
1111 }
1112
1113 fn stat(&mut self, request: PathRequest) -> Result<FileMetadata, Self::Error> {
1114 fs::metadata(Self::host_path(&request.path))
1115 .map(Self::file_metadata)
1116 .map_err(|error| LocalBridgeError::io("stat", &request.path, error))
1117 }
1118
1119 fn lstat(&mut self, request: PathRequest) -> Result<FileMetadata, Self::Error> {
1120 fs::symlink_metadata(Self::host_path(&request.path))
1121 .map(Self::file_metadata)
1122 .map_err(|error| LocalBridgeError::io("lstat", &request.path, error))
1123 }
1124
1125 fn read_dir(&mut self, request: ReadDirRequest) -> Result<Vec<DirectoryEntry>, Self::Error> {
1126 let mut entries = fs::read_dir(Self::host_path(&request.path))
1127 .map_err(|error| LocalBridgeError::io("readdir", &request.path, error))?
1128 .map(|entry| {
1129 let entry =
1130 entry.map_err(|error| LocalBridgeError::io("readdir", &request.path, error))?;
1131 let kind = entry
1132 .file_type()
1133 .map(Self::file_kind)
1134 .map_err(|error| LocalBridgeError::io("readdir", &request.path, error))?;
1135 Ok(DirectoryEntry {
1136 name: entry.file_name().to_string_lossy().into_owned(),
1137 kind,
1138 })
1139 })
1140 .collect::<Result<Vec<_>, LocalBridgeError>>()?;
1141 entries.sort_by(|left, right| left.name.cmp(&right.name));
1142 Ok(entries)
1143 }
1144
1145 fn create_dir(&mut self, request: CreateDirRequest) -> Result<(), Self::Error> {
1146 let host_path = Self::host_path(&request.path);
1147 if request.recursive {
1148 fs::create_dir_all(host_path)
1149 } else {
1150 fs::create_dir(host_path)
1151 }
1152 .map_err(|error| LocalBridgeError::io("mkdir", &request.path, error))
1153 }
1154
1155 fn remove_file(&mut self, request: PathRequest) -> Result<(), Self::Error> {
1156 fs::remove_file(Self::host_path(&request.path))
1157 .map_err(|error| LocalBridgeError::io("unlink", &request.path, error))
1158 }
1159
1160 fn remove_dir(&mut self, request: PathRequest) -> Result<(), Self::Error> {
1161 fs::remove_dir(Self::host_path(&request.path))
1162 .map_err(|error| LocalBridgeError::io("rmdir", &request.path, error))
1163 }
1164
1165 fn rename(&mut self, request: RenameRequest) -> Result<(), Self::Error> {
1166 let from_path = Self::host_path(&request.from_path);
1167 let to_path = Self::host_path(&request.to_path);
1168 if let Some(parent) = to_path.parent() {
1169 fs::create_dir_all(parent)
1170 .map_err(|error| LocalBridgeError::io("mkdir", &request.to_path, error))?;
1171 }
1172 fs::rename(from_path, to_path).map_err(|error| {
1173 LocalBridgeError::unsupported(format!(
1174 "rename {} -> {}: {}",
1175 request.from_path, request.to_path, error
1176 ))
1177 })
1178 }
1179
1180 fn symlink(&mut self, request: SymlinkRequest) -> Result<(), Self::Error> {
1181 let link_path = Self::host_path(&request.link_path);
1182 if let Some(parent) = link_path.parent() {
1183 fs::create_dir_all(parent)
1184 .map_err(|error| LocalBridgeError::io("mkdir", &request.link_path, error))?;
1185 }
1186 create_symlink(&request.target_path, link_path)
1187 .map_err(|error| LocalBridgeError::io("symlink", &request.link_path, error))
1188 }
1189
1190 fn read_link(&mut self, request: PathRequest) -> Result<String, Self::Error> {
1191 fs::read_link(Self::host_path(&request.path))
1192 .map(|target| target.to_string_lossy().into_owned())
1193 .map_err(|error| LocalBridgeError::io("readlink", &request.path, error))
1194 }
1195
1196 fn chmod(&mut self, request: ChmodRequest) -> Result<(), Self::Error> {
1197 let permissions = fs::Permissions::from_mode(request.mode);
1198 fs::set_permissions(Self::host_path(&request.path), permissions)
1199 .map_err(|error| LocalBridgeError::io("chmod", &request.path, error))
1200 }
1201
1202 fn truncate(&mut self, request: TruncateRequest) -> Result<(), Self::Error> {
1203 OpenOptions::new()
1204 .write(true)
1205 .create(false)
1206 .open(Self::host_path(&request.path))
1207 .and_then(|file| file.set_len(request.len))
1208 .map_err(|error| LocalBridgeError::io("truncate", &request.path, error))
1209 }
1210
1211 fn exists(&mut self, request: PathRequest) -> Result<bool, Self::Error> {
1212 Ok(fs::symlink_metadata(Self::host_path(&request.path)).is_ok())
1213 }
1214}
1215
1216impl PermissionBridge for LocalBridge {
1217 fn check_filesystem_access(
1218 &mut self,
1219 request: FilesystemPermissionRequest,
1220 ) -> Result<PermissionDecision, Self::Error> {
1221 Ok(PermissionDecision::deny(format!(
1222 "no static filesystem policy registered for {}:{}",
1223 request.vm_id, request.path
1224 )))
1225 }
1226
1227 fn check_network_access(
1228 &mut self,
1229 request: NetworkPermissionRequest,
1230 ) -> Result<PermissionDecision, Self::Error> {
1231 Ok(PermissionDecision::deny(format!(
1232 "no static network policy registered for {}:{}",
1233 request.vm_id, request.resource
1234 )))
1235 }
1236
1237 fn check_command_execution(
1238 &mut self,
1239 request: CommandPermissionRequest,
1240 ) -> Result<PermissionDecision, Self::Error> {
1241 Ok(PermissionDecision::deny(format!(
1242 "no static child_process policy registered for {}:{}",
1243 request.vm_id, request.command
1244 )))
1245 }
1246
1247 fn check_environment_access(
1248 &mut self,
1249 request: EnvironmentPermissionRequest,
1250 ) -> Result<PermissionDecision, Self::Error> {
1251 Ok(PermissionDecision::deny(format!(
1252 "no static env policy registered for {}:{}",
1253 request.vm_id, request.key
1254 )))
1255 }
1256}
1257
1258impl PersistenceBridge for LocalBridge {
1259 fn load_filesystem_state(
1260 &mut self,
1261 request: LoadFilesystemStateRequest,
1262 ) -> Result<Option<FilesystemSnapshot>, Self::Error> {
1263 Ok(self.snapshots.get(&request.vm_id).cloned())
1264 }
1265
1266 fn flush_filesystem_state(
1267 &mut self,
1268 request: FlushFilesystemStateRequest,
1269 ) -> Result<(), Self::Error> {
1270 self.snapshots.insert(request.vm_id, request.snapshot);
1271 Ok(())
1272 }
1273}
1274
1275impl ClockBridge for LocalBridge {
1276 fn wall_clock(&mut self, _request: ClockRequest) -> Result<SystemTime, Self::Error> {
1277 Ok(SystemTime::now())
1278 }
1279
1280 fn monotonic_clock(&mut self, _request: ClockRequest) -> Result<Duration, Self::Error> {
1281 Ok(self.started_at.elapsed())
1282 }
1283
1284 fn schedule_timer(
1285 &mut self,
1286 request: ScheduleTimerRequest,
1287 ) -> Result<ScheduledTimer, Self::Error> {
1288 self.next_timer_id += 1;
1289 Ok(ScheduledTimer {
1290 timer_id: format!("timer-{}", self.next_timer_id),
1291 delay: request.delay,
1292 })
1293 }
1294}
1295
1296impl RandomBridge for LocalBridge {
1297 fn fill_random_bytes(&mut self, request: RandomBytesRequest) -> Result<Vec<u8>, Self::Error> {
1298 Ok(vec![0u8; request.len])
1299 }
1300}
1301
1302impl EventBridge for LocalBridge {
1303 fn emit_structured_event(&mut self, _event: StructuredEventRecord) -> Result<(), Self::Error> {
1304 Ok(())
1305 }
1306
1307 fn emit_diagnostic(&mut self, _event: DiagnosticRecord) -> Result<(), Self::Error> {
1308 Ok(())
1309 }
1310
1311 fn emit_log(&mut self, _event: LogRecord) -> Result<(), Self::Error> {
1312 Ok(())
1313 }
1314
1315 fn emit_lifecycle(&mut self, _event: LifecycleEventRecord) -> Result<(), Self::Error> {
1316 Ok(())
1317 }
1318}
1319
1320impl ExecutionBridge for LocalBridge {
1321 fn create_javascript_context(
1322 &mut self,
1323 _request: CreateJavascriptContextRequest,
1324 ) -> Result<GuestContextHandle, Self::Error> {
1325 Err(LocalBridgeError::unsupported(
1326 "execution bridge is handled internally by the native sidecar",
1327 ))
1328 }
1329
1330 fn create_wasm_context(
1331 &mut self,
1332 _request: CreateWasmContextRequest,
1333 ) -> Result<GuestContextHandle, Self::Error> {
1334 Err(LocalBridgeError::unsupported(
1335 "execution bridge is handled internally by the native sidecar",
1336 ))
1337 }
1338
1339 fn start_execution(
1340 &mut self,
1341 _request: StartExecutionRequest,
1342 ) -> Result<StartedExecution, Self::Error> {
1343 Err(LocalBridgeError::unsupported(
1344 "execution bridge is handled internally by the native sidecar",
1345 ))
1346 }
1347
1348 fn write_stdin(&mut self, _request: WriteExecutionStdinRequest) -> Result<(), Self::Error> {
1349 Err(LocalBridgeError::unsupported(
1350 "execution bridge is handled internally by the native sidecar",
1351 ))
1352 }
1353
1354 fn close_stdin(&mut self, _request: ExecutionHandleRequest) -> Result<(), Self::Error> {
1355 Err(LocalBridgeError::unsupported(
1356 "execution bridge is handled internally by the native sidecar",
1357 ))
1358 }
1359
1360 fn kill_execution(&mut self, _request: KillExecutionRequest) -> Result<(), Self::Error> {
1361 Err(LocalBridgeError::unsupported(
1362 "execution bridge is handled internally by the native sidecar",
1363 ))
1364 }
1365
1366 fn poll_execution_event(
1367 &mut self,
1368 _request: PollExecutionEventRequest,
1369 ) -> Result<Option<ExecutionEvent>, Self::Error> {
1370 Ok(None)
1371 }
1372}
1373
1374#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
1375struct SessionScope {
1376 connection_id: String,
1377 session_id: String,
1378}
1379
1380impl SessionScope {
1381 fn ownership_scope(&self) -> OwnershipScope {
1382 session_ownership(&self.connection_id, &self.session_id)
1383 }
1384
1385 fn compat_ownership_scope(&self) -> crate::protocol::OwnershipScope {
1386 wire::ownership_scope_to_compat(self.ownership_scope())
1387 }
1388}
1389
1390struct FrameEventTransport {
1396 writer: TrackedSyncSender<ProtocolFrame>,
1397}
1398
1399impl FrameEventTransport {
1400 fn new(writer: TrackedSyncSender<ProtocolFrame>) -> Self {
1401 Self { writer }
1402 }
1403}
1404
1405impl EventSinkTransport for FrameEventTransport {
1406 fn emit_event(&self, event: crate::wire::EventFrame) -> Result<(), SidecarError> {
1407 send_output_frame(&self.writer, ProtocolFrame::EventFrame(event))
1408 .map_err(|error| SidecarError::Bridge(error.to_string()))
1409 }
1410}
1411
1412struct FrameSidecarRequestTransport {
1413 writer: TrackedSyncSender<ProtocolFrame>,
1414 pending: Arc<Mutex<BTreeMap<RequestId, mpsc::SyncSender<SidecarResponseFrame>>>>,
1415}
1416
1417impl FrameSidecarRequestTransport {
1418 fn new(writer: TrackedSyncSender<ProtocolFrame>) -> Self {
1419 Self {
1420 writer,
1421 pending: Arc::new(Mutex::new(BTreeMap::new())),
1422 }
1423 }
1424
1425 fn accept_response(&self, response: SidecarResponseFrame) -> bool {
1426 let sender = {
1427 let mut pending = match self.pending.lock() {
1428 Ok(pending) => pending,
1429 Err(_) => return false,
1430 };
1431 pending.remove(&response.request_id)
1432 };
1433 let Some(sender) = sender else {
1434 return false;
1435 };
1436 let _ = sender.send(response);
1437 true
1438 }
1439}
1440
1441impl SidecarRequestTransport for FrameSidecarRequestTransport {
1442 fn send_request(
1443 &self,
1444 request: crate::protocol::SidecarRequestFrame,
1445 timeout: Duration,
1446 ) -> Result<crate::protocol::SidecarResponseFrame, SidecarError> {
1447 let request =
1448 wire::sidecar_request_frame_from_compat(request).map_err(wire_protocol_error)?;
1449 let (sender, receiver) = mpsc::sync_channel(1);
1450 self.pending
1451 .lock()
1452 .map_err(|_| {
1453 SidecarError::Bridge(String::from("sidecar callback waiter map lock poisoned"))
1454 })?
1455 .insert(request.request_id, sender);
1456 let write_deadline = Instant::now() + timeout;
1463 let mut frame = ProtocolFrame::SidecarRequestFrame(request.clone());
1464 let write_result = loop {
1465 match self.writer.try_send(frame) {
1466 Ok(()) => break Ok(()),
1467 Err(mpsc::TrySendError::Disconnected(_)) => {
1468 break Err(String::from("stdout writer disconnected"));
1469 }
1470 Err(mpsc::TrySendError::Full(returned)) => {
1471 if Instant::now() >= write_deadline {
1472 break Err(format!(
1473 "timed out writing sidecar request frame after {}s",
1474 timeout.as_secs()
1475 ));
1476 }
1477 frame = returned;
1478 thread::sleep(Duration::from_millis(1));
1479 }
1480 }
1481 };
1482 if let Err(message) = write_result {
1483 let _ = self
1484 .pending
1485 .lock()
1486 .map(|mut pending| pending.remove(&request.request_id));
1487 return Err(SidecarError::Io(format!(
1488 "failed to write sidecar request frame: {message}"
1489 )));
1490 }
1491 match receiver.recv_timeout(timeout) {
1492 Ok(response) => {
1493 wire::sidecar_response_frame_to_compat(response).map_err(wire_protocol_error)
1494 }
1495 Err(mpsc::RecvTimeoutError::Timeout) => {
1496 let _ = self
1497 .pending
1498 .lock()
1499 .map(|mut pending| pending.remove(&request.request_id));
1500 Err(SidecarError::Io(format!(
1501 "timed out waiting for sidecar response after {}s",
1502 timeout.as_secs()
1503 )))
1504 }
1505 Err(mpsc::RecvTimeoutError::Disconnected) => Err(SidecarError::Io(String::from(
1506 "sidecar response waiter disconnected",
1507 ))),
1508 }
1509 }
1510}
1511
1512#[derive(Debug, Clone, PartialEq, Eq)]
1513pub(crate) struct LocalBridgeError {
1514 message: String,
1515}
1516
1517impl LocalBridgeError {
1518 fn unsupported(message: impl Into<String>) -> Self {
1519 Self {
1520 message: message.into(),
1521 }
1522 }
1523
1524 fn io(operation: &str, path: &str, error: io::Error) -> Self {
1525 Self::unsupported(format!("{operation} {path}: {error}"))
1526 }
1527}
1528
1529impl fmt::Display for LocalBridgeError {
1530 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1531 f.write_str(&self.message)
1532 }
1533}
1534
1535impl Error for LocalBridgeError {}
1536
1537impl LocalBridge {
1538 fn host_path(path: &str) -> PathBuf {
1539 let candidate = Path::new(path);
1540 if candidate.is_absolute() {
1541 candidate.to_path_buf()
1542 } else {
1543 std::env::current_dir()
1544 .unwrap_or_else(|_| PathBuf::from("."))
1545 .join(candidate)
1546 }
1547 }
1548
1549 fn file_metadata(metadata: fs::Metadata) -> FileMetadata {
1550 FileMetadata {
1551 mode: metadata.permissions().mode(),
1552 size: metadata.size(),
1553 kind: Self::file_kind(metadata.file_type()),
1554 }
1555 }
1556
1557 fn file_kind(file_type: fs::FileType) -> secure_exec_bridge::FileKind {
1558 if file_type.is_file() {
1559 secure_exec_bridge::FileKind::File
1560 } else if file_type.is_dir() {
1561 secure_exec_bridge::FileKind::Directory
1562 } else if file_type.is_symlink() {
1563 secure_exec_bridge::FileKind::SymbolicLink
1564 } else {
1565 secure_exec_bridge::FileKind::Other
1566 }
1567 }
1568}