1use std::collections::HashMap;
4use std::env;
5use std::fs::{File, OpenOptions};
6use std::os::fd::AsRawFd;
7use std::sync::Arc;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::time::Instant;
10
11use chrono::Utc;
12use tokio::io::unix::AsyncFd;
13use tokio::sync::{mpsc, watch};
14use tokio::time::{self, Duration};
15
16use microsandbox_protocol::HANDOFF_POWEROFF_TIMEOUT;
17use microsandbox_protocol::bootstrap::GuestBootstrap;
18use microsandbox_protocol::codec::{self, MAX_FRAME_SIZE};
19use microsandbox_protocol::core::{
20 ClockSync, CoreError, CoreErrorKind, InitAck, InitResolved, Ping, Pong, Ready,
21 RelayClientDisconnected, ResolvedUser, Touch, Touched,
22};
23use microsandbox_protocol::exec::{
24 ExecExited, ExecFailed, ExecFailureKind, ExecRequest, ExecResize, ExecSignal, ExecStarted,
25 ExecStderr, ExecStdin, ExecStdinError, ExecStdout,
26};
27use microsandbox_protocol::fs::{FsData, FsRequest};
28use microsandbox_protocol::heartbeat::{ActivityCounters, Heartbeat};
29use microsandbox_protocol::message::{Message, MessageType};
30use microsandbox_protocol::tcp::{TcpClose, TcpConnect, TcpData, TcpEof, TcpFailed};
31
32use crate::config::{AgentdConfig, scripts_path};
33use crate::error::{AgentdError, AgentdResult};
34use crate::fs::{FsReadSession, FsState, FsStreamSession, FsWriteSession};
35use crate::process::ProcessManager;
36use crate::serial::AGENT_PORT_NAME;
37use crate::session::{
38 ExecSession, RawActivity, RawSessionCompletion, SessionOutput, resolve_default_user,
39};
40use crate::tcp::TcpSession;
41use crate::{clock, fs, handoff, heartbeat, serial};
42
43const HEARTBEAT_INTERVAL_SECS: u64 = 1;
52
53const SERIAL_READ_BUF_SIZE: usize = 64 * 1024;
55
56const MAX_INPUT_BUF_SIZE: usize = MAX_FRAME_SIZE as usize + 4;
58
59const INIT_ACK_TIMEOUT_SECS: u64 = 60;
61
62#[derive(Default)]
67struct AgentState {
68 sessions: HashMap<u32, ExecSession>,
69 write_sessions: HashMap<u32, FsWriteSession>,
70 read_sessions: HashMap<u32, FsReadSession>,
71 tcp_sessions: HashMap<u32, TcpSession>,
72 fs: FsState,
73}
74
75struct ActivityTracker {
76 activity_seq: u64,
77 counters: ActivityCounters,
78}
79
80#[derive(Default)]
85pub struct BootConsoleState {
86 input: Vec<u8>,
87}
88
89#[derive(Clone)]
90struct HeartbeatSnapshot {
91 activity_seq: u64,
92 active_exec_sessions: u32,
93 active_fs_streams: u32,
94 active_tcp_streams: u32,
95 counters: ActivityCounters,
96}
97
98impl ActivityTracker {
103 fn new() -> Self {
104 Self {
105 activity_seq: 0,
106 counters: ActivityCounters::default(),
107 }
108 }
109
110 fn record_host_message(&mut self) {
111 self.touch();
112 self.counters.host_messages = self.counters.host_messages.saturating_add(1);
113 }
114
115 fn record_guest_message(&mut self) {
116 self.touch();
117 self.counters.guest_messages = self.counters.guest_messages.saturating_add(1);
118 }
119
120 fn add_exec_output_bytes(&mut self, len: usize) {
121 self.counters.exec_output_bytes =
122 self.counters.exec_output_bytes.saturating_add(len as u64);
123 }
124
125 fn add_fs_bytes(&mut self, len: usize) {
126 self.counters.fs_bytes = self.counters.fs_bytes.saturating_add(len as u64);
127 }
128
129 fn add_tcp_bytes(&mut self, len: usize) {
130 self.counters.tcp_bytes = self.counters.tcp_bytes.saturating_add(len as u64);
131 }
132
133 fn touch(&mut self) {
134 self.activity_seq = self.activity_seq.saturating_add(1);
135 }
136}
137
138pub async fn run(
150 boot_time_ns: u64,
151 init_time_ns: u64,
152 config: &AgentdConfig,
153 port_file: File,
154 boot_console: BootConsoleState,
155) -> AgentdResult<()> {
156 let process_manager = ProcessManager::get()?;
157 let mut process_manager_failure = process_manager.subscribe_failure()?;
158
159 let port_fd = port_file.as_raw_fd();
162 set_nonblocking(port_fd)?;
163
164 let async_port = AsyncFd::new(port_file)?;
166
167 let mut read_buf = vec![0u8; SERIAL_READ_BUF_SIZE];
169 let mut serial_in_buf = boot_console.input;
170 let mut serial_out_buf = Vec::new();
171
172 let mut state = AgentState::default();
173
174 let (session_tx, mut session_rx) = mpsc::unbounded_channel::<(u32, SessionOutput)>();
176
177 let mut activity = ActivityTracker::new();
179 let (heartbeat_tx, heartbeat_rx) = watch::channel(heartbeat_snapshot(&state, &activity));
180 let heartbeat_shutdown = Arc::new(AtomicBool::new(false));
187 let heartbeat_thread = spawn_heartbeat_thread(heartbeat_rx, Arc::clone(&heartbeat_shutdown));
188
189 let ready_time_ns = clock::boottime_ns();
191 let ready_msg = Message::with_payload(
192 MessageType::Ready,
193 0,
194 &Ready {
195 boot_time_ns,
196 init_time_ns,
197 ready_time_ns,
198 agent_version: env!("CARGO_PKG_VERSION").to_string(),
199 },
200 )
201 .map_err(|e| AgentdError::ExecSession(format!("encode ready: {e}")))?;
202 codec::encode_to_buf(&ready_msg, &mut serial_out_buf)
203 .map_err(|e| AgentdError::ExecSession(format!("encode ready frame: {e}")))?;
204 flush_write_buf(&async_port, &mut serial_out_buf).await?;
205
206 'agent: loop {
208 tokio::select! {
209 failure = process_manager_failure.changed() => {
210 let error = match failure {
211 Ok(()) => process_manager_failure
212 .borrow()
213 .clone()
214 .unwrap_or_else(|| "process manager stopped without an error".to_string()),
215 Err(error) => format!("process manager failure channel closed: {error}"),
216 };
217 return Err(AgentdError::ExecSession(error));
218 }
219
220 result = async_port.readable() => {
222 let Ok(mut guard) = result else {
223 break;
224 };
225
226 loop {
227 match guard.try_io(|inner| read_from_fd(inner.get_ref().as_raw_fd(), &mut read_buf)) {
228 Ok(Ok(0)) => {
229 if !handoff::is_pid_1() {
231 guard.clear_ready();
232 drop(guard);
233 time::sleep(Duration::from_millis(100)).await;
234 break;
235 }
236 break 'agent;
237 }
238 Ok(Ok(n)) => {
239 serial_in_buf.extend_from_slice(&read_buf[..n]);
240
241 if serial_in_buf.len() > MAX_INPUT_BUF_SIZE {
243 return Err(AgentdError::ExecSession(
244 "serial input buffer exceeded maximum size".into(),
245 ));
246 }
247
248 while let Some(frame) = codec::try_decode_raw_from_buf(&mut serial_in_buf)
253 .map_err(|e| AgentdError::ExecSession(format!("decode frame: {e}")))?
254 {
255 let id = frame.id;
256 let msg = match codec::raw_frame_to_message(frame) {
257 Ok(msg) => msg,
258 Err(e) => {
259 return Err(AgentdError::ExecSession(format!(
260 "decode message for id {id}: {e}"
261 )));
262 }
263 };
264
265 if msg.flags != msg.t.flags() {
266 let out_before = serial_out_buf.len();
267 encode_core_error_if_supported(
268 &msg,
269 msg.id,
270 CoreErrorKind::InvalidFlags,
271 format!(
272 "invalid flags for {}: got {}, expected {}",
273 msg.t.as_str(),
274 msg.flags,
275 msg.t.flags()
276 ),
277 Some(msg.t.as_str().to_string()),
278 &mut serial_out_buf,
279 )?;
280 record_encoded_guest_messages(
281 &serial_out_buf,
282 out_before,
283 &mut activity,
284 );
285 publish_heartbeat_snapshot(&heartbeat_tx, &state, &activity);
286 continue;
287 }
288
289 if message_refreshes_idle_timer(&msg.t) {
290 activity.record_host_message();
291 publish_heartbeat_snapshot(&heartbeat_tx, &state, &activity);
292 }
293
294 let out_before = serial_out_buf.len();
295 handle_message(
296 msg,
297 &mut state,
298 &mut activity,
299 &session_tx,
300 &mut serial_out_buf,
301 config,
302 ).await?;
303 record_encoded_guest_messages(
304 &serial_out_buf,
305 out_before,
306 &mut activity,
307 );
308 publish_heartbeat_snapshot(&heartbeat_tx, &state, &activity);
309 }
310
311 if !serial_out_buf.is_empty() {
313 flush_write_buf(&async_port, &mut serial_out_buf).await?;
314 }
315 }
316 Ok(Err(e)) if e.kind() == std::io::ErrorKind::Interrupted => continue,
317 Ok(Err(_)) if !handoff::is_pid_1() => {
318 guard.clear_ready();
319 drop(guard);
320 time::sleep(Duration::from_millis(100)).await;
321 break;
322 }
323 Ok(Err(e)) => return Err(e.into()),
324 Err(_would_block) => break,
325 }
326 }
327 }
328
329 Some((id, output)) = session_rx.recv() => {
331 match output {
332 SessionOutput::Stdout(data) => {
333 let len = data.len();
334 let msg = Message::with_payload(MessageType::ExecStdout, id, &ExecStdout { data })
335 .map_err(|e| AgentdError::ExecSession(format!("encode stdout: {e}")))?;
336 codec::encode_to_buf(&msg, &mut serial_out_buf)
337 .map_err(|e| AgentdError::ExecSession(format!("encode stdout frame: {e}")))?;
338 activity.record_guest_message();
339 activity.add_exec_output_bytes(len);
340 }
341 SessionOutput::Stderr(data) => {
342 let len = data.len();
343 let msg = Message::with_payload(MessageType::ExecStderr, id, &ExecStderr { data })
344 .map_err(|e| AgentdError::ExecSession(format!("encode stderr: {e}")))?;
345 codec::encode_to_buf(&msg, &mut serial_out_buf)
346 .map_err(|e| AgentdError::ExecSession(format!("encode stderr frame: {e}")))?;
347 activity.record_guest_message();
348 activity.add_exec_output_bytes(len);
349 }
350 SessionOutput::Exited(code) => {
351 let msg = Message::with_payload(MessageType::ExecExited, id, &ExecExited { code })
352 .map_err(|e| AgentdError::ExecSession(format!("encode exited: {e}")))?;
353 codec::encode_to_buf(&msg, &mut serial_out_buf)
354 .map_err(|e| AgentdError::ExecSession(format!("encode exited frame: {e}")))?;
355 state.sessions.remove(&id);
356 activity.record_guest_message();
357 }
358 SessionOutput::Raw(output) => {
359 apply_raw_activity(output.activity, &mut activity);
360 complete_raw_session(
361 id,
362 output.completion,
363 &mut state.read_sessions,
364 &mut state.tcp_sessions,
365 );
366 serial_out_buf.extend_from_slice(&output.frame);
368 }
369 }
370 publish_heartbeat_snapshot(&heartbeat_tx, &state, &activity);
371
372 if !serial_out_buf.is_empty() {
373 flush_write_buf(&async_port, &mut serial_out_buf).await?;
374 }
375 }
376 }
377 }
378
379 heartbeat_shutdown.store(true, Ordering::Relaxed);
380 let _ = heartbeat_thread.join();
381
382 Ok(())
383}
384
385pub fn open_serial_port() -> AgentdResult<File> {
387 let port_path = serial::find_serial_port(AGENT_PORT_NAME)?;
389
390 Ok(OpenOptions::new().read(true).write(true).open(&port_path)?)
393}
394
395pub fn receive_bootstrap(port_file: &File) -> AgentdResult<(GuestBootstrap, BootConsoleState)> {
397 let fd = port_file.as_raw_fd();
398 set_nonblocking(fd)?;
399 let deadline = init_ack_deadline();
400 let mut state = BootConsoleState::default();
401 let msg = read_boot_message(fd, &mut state, deadline, "guest bootstrap")?;
402 let bootstrap = decode_bootstrap_message(msg)?;
403 Ok((bootstrap, state))
404}
405
406fn decode_bootstrap_message(msg: Message) -> AgentdResult<GuestBootstrap> {
407 if msg.id != 0 || msg.flags != 0 {
408 return Err(AgentdError::Config(format!(
409 "guest bootstrap requires id=0 and flags=0, got id={} flags={}",
410 msg.id, msg.flags
411 )));
412 }
413 if msg.t != MessageType::Bootstrap {
414 return Err(AgentdError::Config(format!(
415 "expected core.bootstrap as first console frame, got {}",
416 msg.t.as_str()
417 )));
418 }
419 let min_version = MessageType::Bootstrap.min_protocol_version();
420 if msg.v < min_version {
421 return Err(AgentdError::Config(format!(
422 "guest bootstrap requires protocol generation {min_version} or newer, got {}",
423 msg.v
424 )));
425 }
426
427 msg.payload::<GuestBootstrap>()
428 .map_err(|e| AgentdError::Config(format!("decode guest bootstrap payload: {e}")))
429}
430
431pub fn report_init_context(
433 port_file: &File,
434 boot_console: &mut BootConsoleState,
435 default_user: Option<&str>,
436) -> AgentdResult<()> {
437 let (uid, gid) = resolve_default_user(default_user)?;
438 let deadline = init_ack_deadline();
439 let fd = port_file.as_raw_fd();
440 set_nonblocking(fd)?;
441
442 let msg = Message::with_payload(
443 MessageType::InitResolved,
444 0,
445 &InitResolved {
446 default_user: ResolvedUser { uid, gid },
447 },
448 )
449 .map_err(|e| AgentdError::ExecSession(format!("encode init context: {e}")))?;
450
451 let mut out = Vec::new();
452 codec::encode_to_buf(&msg, &mut out)
453 .map_err(|e| AgentdError::ExecSession(format!("encode init context frame: {e}")))?;
454 write_all_to_fd(fd, &out, deadline)?;
455 wait_for_init_ack(fd, boot_console, deadline)
456}
457
458async fn handle_message(
464 msg: Message,
465 state: &mut AgentState,
466 activity: &mut ActivityTracker,
467 session_tx: &mpsc::UnboundedSender<(u32, SessionOutput)>,
468 out_buf: &mut Vec<u8>,
469 config: &AgentdConfig,
470) -> AgentdResult<()> {
471 match msg.t {
472 MessageType::Ping => {
473 let Some(_) = decode_payload_or_core_error::<Ping>(&msg, out_buf)? else {
474 return Ok(());
475 };
476 let reply = Message::with_payload(MessageType::Pong, msg.id, &Pong {})
477 .map_err(|e| AgentdError::ExecSession(format!("encode pong: {e}")))?;
478 codec::encode_to_buf(&reply, out_buf)
479 .map_err(|e| AgentdError::ExecSession(format!("encode pong frame: {e}")))?;
480 }
481
482 MessageType::Touch => {
483 let Some(_) = decode_payload_or_core_error::<Touch>(&msg, out_buf)? else {
484 return Ok(());
485 };
486 activity.record_host_message();
487 let reply = Message::with_payload(
488 MessageType::Touched,
489 msg.id,
490 &Touched {
491 activity_seq: activity.activity_seq,
492 },
493 )
494 .map_err(|e| AgentdError::ExecSession(format!("encode touched: {e}")))?;
495 codec::encode_to_buf(&reply, out_buf)
496 .map_err(|e| AgentdError::ExecSession(format!("encode touched frame: {e}")))?;
497 }
498
499 MessageType::ExecRequest => {
500 let Some(mut req) = decode_payload_or_core_error::<ExecRequest>(&msg, out_buf)? else {
501 return Ok(());
502 };
503 if req.cwd.is_none() {
504 req.cwd = config.default_cwd().map(str::to_string);
505 }
506 prepend_scripts_to_path(&mut req);
507 match ExecSession::spawn(
508 msg.id,
509 &req,
510 session_tx.clone(),
511 config.user.as_deref(),
512 config.security_profile,
513 ) {
514 Ok(session) => {
515 let reply = Message::with_payload(
516 MessageType::ExecStarted,
517 msg.id,
518 &ExecStarted { pid: session.pid() },
519 )
520 .map_err(|e| AgentdError::ExecSession(format!("encode started: {e}")))?;
521 codec::encode_to_buf(&reply, out_buf).map_err(|e| {
522 AgentdError::ExecSession(format!("encode started frame: {e}"))
523 })?;
524 state.sessions.insert(msg.id, session);
525 }
526 Err(e) => {
527 let payload = match &e {
533 AgentdError::ExecSpawnFailed(p) => p.clone(),
534 other => ExecFailed {
535 kind: ExecFailureKind::Other,
536 errno: None,
537 errno_name: None,
538 message: other.to_string(),
539 stage: None,
540 },
541 };
542 let reply = Message::with_payload(MessageType::ExecFailed, msg.id, &payload)
543 .map_err(|e| AgentdError::ExecSession(format!("encode failed: {e}")))?;
544 codec::encode_to_buf(&reply, out_buf).map_err(|e| {
545 AgentdError::ExecSession(format!("encode failed frame: {e}"))
546 })?;
547 eprintln!("failed to spawn exec session {}: {e}", msg.id);
548 }
549 }
550 }
551
552 MessageType::ExecStdin => {
553 let Some(stdin) = decode_payload_or_core_error::<ExecStdin>(&msg, out_buf)? else {
554 return Ok(());
555 };
556 if let Some(session) = state.sessions.get_mut(&msg.id) {
557 if stdin.data.is_empty() {
558 session.close_stdin();
560 } else if let Err(e) = session.write_stdin(&stdin.data).await {
561 let payload = stdin_error_payload(&e);
562 eprintln!("stdin write error on session {}: {e}", msg.id);
563 let reply =
564 Message::with_payload(MessageType::ExecStdinError, msg.id, &payload)
565 .map_err(|e| {
566 AgentdError::ExecSession(format!("encode stdin error: {e}"))
567 })?;
568 codec::encode_to_buf(&reply, out_buf).map_err(|e| {
569 AgentdError::ExecSession(format!("encode stdin error frame: {e}"))
570 })?;
571 }
572 }
573 }
574
575 MessageType::ExecResize => {
576 let Some(resize) = decode_payload_or_core_error::<ExecResize>(&msg, out_buf)? else {
577 return Ok(());
578 };
579 if let Some(session) = state.sessions.get(&msg.id) {
580 let _ = session.resize(resize.rows, resize.cols);
581 }
582 }
583
584 MessageType::ExecSignal => {
585 let Some(signal) = decode_payload_or_core_error::<ExecSignal>(&msg, out_buf)? else {
586 return Ok(());
587 };
588 if let Some(session) = state.sessions.get(&msg.id) {
589 let _ = session.send_signal(signal.signal);
590 }
591 }
592
593 MessageType::FsRequest => {
594 let Some(req) = decode_payload_or_core_error::<FsRequest>(&msg, out_buf)? else {
595 return Ok(());
596 };
597 match fs::handle_fs_request(msg.id, req, &mut state.fs, out_buf, session_tx).await {
598 Ok(Some(FsStreamSession::Read(rs))) => {
599 state.read_sessions.insert(msg.id, rs);
600 }
601 Ok(Some(FsStreamSession::Write(ws))) => {
602 state.write_sessions.insert(msg.id, ws);
603 }
604 Ok(None) => {}
605 Err(e) => {
606 eprintln!("fs request error for {}: {e}", msg.id);
607 }
608 }
609 }
610
611 MessageType::FsData => {
612 let Some(data) = decode_payload_or_core_error::<FsData>(&msg, out_buf)? else {
613 return Ok(());
614 };
615 let len = data.data.len();
616 if let Some(session) = state.write_sessions.get_mut(&msg.id) {
617 match fs::handle_fs_data(msg.id, data, session, out_buf).await {
618 Ok(true) => {
619 state.write_sessions.remove(&msg.id);
621 }
622 Ok(false) => {
623 activity.add_fs_bytes(len);
624 }
625 Err(e) => {
626 eprintln!("fs data error for {}: {e}", msg.id);
627 state.write_sessions.remove(&msg.id);
628 }
629 }
630 } else {
631 let resp = microsandbox_protocol::fs::FsResponse {
633 ok: false,
634 error: Some(format!("unknown write session: {}", msg.id)),
635 data: None,
636 };
637 let reply = Message::with_payload(MessageType::FsResponse, msg.id, &resp)
638 .map_err(|e| AgentdError::ExecSession(format!("encode fs error: {e}")))?;
639 codec::encode_to_buf(&reply, out_buf)
640 .map_err(|e| AgentdError::ExecSession(format!("encode fs error frame: {e}")))?;
641 }
642 }
643
644 MessageType::TcpConnect => {
645 let Some(req) = decode_payload_or_core_error::<TcpConnect>(&msg, out_buf)? else {
646 return Ok(());
647 };
648 let session = TcpSession::open(msg.id, req, session_tx);
651 state.tcp_sessions.insert(msg.id, session);
652 }
653
654 MessageType::TcpData => {
655 let Some(data) = decode_payload_or_core_error::<TcpData>(&msg, out_buf)? else {
656 return Ok(());
657 };
658 let len = data.data.len();
659 if let Some(session) = state.tcp_sessions.get(&msg.id) {
660 if let Err(e) = session.write_data(data.data).await {
661 state.tcp_sessions.remove(&msg.id);
662 encode_tcp_failed(msg.id, e, out_buf)?;
663 } else {
664 activity.add_tcp_bytes(len);
665 }
666 } else {
667 encode_tcp_failed(msg.id, format!("unknown TCP session: {}", msg.id), out_buf)?;
668 }
669 }
670
671 MessageType::TcpEof => {
672 let Some(_) = decode_payload_or_core_error::<TcpEof>(&msg, out_buf)? else {
673 return Ok(());
674 };
675 if let Some(session) = state.tcp_sessions.get(&msg.id)
676 && let Err(e) = session.close_write().await
677 {
678 state.tcp_sessions.remove(&msg.id);
679 encode_tcp_failed(msg.id, e, out_buf)?;
680 }
681 }
682
683 MessageType::TcpClose => {
684 let Some(_) = decode_payload_or_core_error::<TcpClose>(&msg, out_buf)? else {
685 return Ok(());
686 };
687 if let Some(session) = state.tcp_sessions.remove(&msg.id) {
688 session.close();
689 }
690 }
691
692 MessageType::RelayClientDisconnected => {
693 let Some(disconnected) =
694 decode_payload_or_core_error::<RelayClientDisconnected>(&msg, out_buf)?
695 else {
696 return Ok(());
697 };
698 state
699 .fs
700 .close_owner_range(disconnected.id_start, disconnected.id_end_exclusive);
701 abort_read_sessions_in_owner_range(
702 &mut state.read_sessions,
703 disconnected.id_start,
704 disconnected.id_end_exclusive,
705 );
706 state.write_sessions.retain(|_, session| {
707 let owner_id = session.owner_id();
708 owner_id < disconnected.id_start || owner_id >= disconnected.id_end_exclusive
709 });
710 close_tcp_sessions_in_owner_range(
711 &mut state.tcp_sessions,
712 disconnected.id_start,
713 disconnected.id_end_exclusive,
714 );
715 }
716
717 MessageType::ClockSync => {
718 let Some(sync) = decode_payload_or_core_error::<ClockSync>(&msg, out_buf)? else {
719 return Ok(());
720 };
721 if let Err(e) = clock::sync_realtime_unix_nanos(sync.unix_time_nanos) {
722 eprintln!("clock: failed to sync realtime clock: {e}");
723 }
724 }
725
726 MessageType::Shutdown => {
727 for (_, session) in state.sessions.drain() {
731 let _ = session.send_signal(15); }
733 state.write_sessions.clear();
734 for (_, session) in state.tcp_sessions.drain() {
735 session.close();
736 }
737 state.fs.clear();
738
739 request_guest_poweroff()?;
740 return Err(AgentdError::Shutdown);
741 }
742
743 _ => {
744 }
746 }
747
748 Ok(())
749}
750
751fn message_refreshes_idle_timer(t: &MessageType) -> bool {
762 !matches!(
763 t,
764 MessageType::ClockSync | MessageType::Ping | MessageType::Touch
765 )
766}
767
768fn guest_message_refreshes_idle_timer(t: &MessageType) -> bool {
776 !matches!(
777 t,
778 MessageType::Pong | MessageType::Touched | MessageType::CoreError
779 )
780}
781
782fn spawn_heartbeat_thread(
792 snapshot_rx: watch::Receiver<HeartbeatSnapshot>,
793 shutdown: Arc<AtomicBool>,
794) -> std::thread::JoinHandle<()> {
795 std::thread::Builder::new()
796 .name("agentd-heartbeat".to_string())
797 .spawn(move || {
798 let mut heartbeat_seq = 0u64;
799 let mut last_activity_seq = snapshot_rx.borrow().activity_seq;
800 let mut last_activity = Utc::now();
801
802 let interval = Duration::from_secs(HEARTBEAT_INTERVAL_SECS);
803 let step = Duration::from_millis(100);
804
805 while !shutdown.load(Ordering::Relaxed) {
806 let mut slept = Duration::ZERO;
807 while slept < interval {
808 if shutdown.load(Ordering::Relaxed) {
809 return;
810 }
811 std::thread::sleep(step);
812 slept += step;
813 }
814
815 if !heartbeat::heartbeat_dir_exists() {
816 continue;
817 }
818
819 heartbeat_seq = heartbeat_seq.saturating_add(1);
820 let snapshot = snapshot_rx.borrow().clone();
821 let timestamp = Utc::now();
822 if snapshot.activity_seq != last_activity_seq {
823 last_activity_seq = snapshot.activity_seq;
824 last_activity = timestamp;
825 }
826 let heartbeat = Heartbeat {
827 heartbeat_seq,
828 activity_seq: snapshot.activity_seq,
829 timestamp,
830 last_activity,
831 active_exec_sessions: snapshot.active_exec_sessions,
832 active_fs_streams: snapshot.active_fs_streams,
833 active_tcp_streams: snapshot.active_tcp_streams,
834 activity_counters: snapshot.counters,
835 };
836 let _ = heartbeat::write_heartbeat(&heartbeat);
837 }
838 })
839 .expect("failed to spawn agentd heartbeat thread")
840}
841
842fn heartbeat_snapshot(state: &AgentState, activity: &ActivityTracker) -> HeartbeatSnapshot {
843 HeartbeatSnapshot {
844 activity_seq: activity.activity_seq,
845 active_exec_sessions: state.sessions.len() as u32,
846 active_fs_streams: state
847 .read_sessions
848 .len()
849 .saturating_add(state.write_sessions.len()) as u32,
850 active_tcp_streams: state.tcp_sessions.len() as u32,
851 counters: activity.counters,
852 }
853}
854
855fn publish_heartbeat_snapshot(
856 heartbeat_tx: &watch::Sender<HeartbeatSnapshot>,
857 state: &AgentState,
858 activity: &ActivityTracker,
859) {
860 let _ = heartbeat_tx.send(heartbeat_snapshot(state, activity));
861}
862
863fn record_encoded_guest_messages(out_buf: &[u8], start: usize, activity: &mut ActivityTracker) {
864 let mut offset = start;
865 while offset + 4 <= out_buf.len() {
866 let frame_len = u32::from_be_bytes([
867 out_buf[offset],
868 out_buf[offset + 1],
869 out_buf[offset + 2],
870 out_buf[offset + 3],
871 ]) as usize;
872 let total = 4usize.saturating_add(frame_len);
873 if offset.saturating_add(total) > out_buf.len() {
874 break;
875 }
876
877 if encoded_guest_message_refreshes_idle_timer(out_buf, offset, frame_len) {
878 activity.record_guest_message();
879 }
880 offset += total;
881 }
882}
883
884fn encoded_guest_message_refreshes_idle_timer(
885 out_buf: &[u8],
886 offset: usize,
887 frame_len: usize,
888) -> bool {
889 if frame_len < microsandbox_protocol::message::FRAME_HEADER_SIZE {
890 return true;
891 }
892
893 let id_start = offset + 4;
894 let flags_index = id_start + 4;
895 let body_start = flags_index + 1;
896 let body_end = offset + 4 + frame_len;
897 if body_end > out_buf.len() || body_start > body_end {
898 return true;
899 }
900
901 let id = u32::from_be_bytes([
902 out_buf[id_start],
903 out_buf[id_start + 1],
904 out_buf[id_start + 2],
905 out_buf[id_start + 3],
906 ]);
907 let frame = codec::RawFrame {
908 id,
909 flags: out_buf[flags_index],
910 body: out_buf[body_start..body_end].to_vec(),
911 };
912
913 codec::raw_frame_to_message(frame)
914 .map(|msg| guest_message_refreshes_idle_timer(&msg.t))
915 .unwrap_or(true)
916}
917
918fn apply_raw_activity(raw: RawActivity, activity: &mut ActivityTracker) {
919 if raw.guest_message {
920 activity.record_guest_message();
921 }
922 if raw.fs_bytes > 0 {
923 activity.add_fs_bytes(raw.fs_bytes);
924 }
925 if raw.tcp_bytes > 0 {
926 activity.add_tcp_bytes(raw.tcp_bytes);
927 }
928}
929
930fn complete_raw_session(
931 id: u32,
932 completion: Option<RawSessionCompletion>,
933 read_sessions: &mut HashMap<u32, FsReadSession>,
934 tcp_sessions: &mut HashMap<u32, TcpSession>,
935) {
936 match completion {
937 Some(RawSessionCompletion::FsRead) => {
938 read_sessions.remove(&id);
939 }
940 Some(RawSessionCompletion::Tcp) => {
941 tcp_sessions.remove(&id);
942 }
943 None => {}
944 }
945}
946
947fn abort_read_sessions_in_owner_range(
948 read_sessions: &mut HashMap<u32, FsReadSession>,
949 id_start: u32,
950 id_end_exclusive: u32,
951) {
952 let mut retained = HashMap::new();
953 for (id, session) in read_sessions.drain() {
954 let owner_id = session.owner_id();
955 if owner_id >= id_start && owner_id < id_end_exclusive {
956 session.abort();
957 } else {
958 retained.insert(id, session);
959 }
960 }
961 *read_sessions = retained;
962}
963
964fn close_tcp_sessions_in_owner_range(
965 tcp_sessions: &mut HashMap<u32, TcpSession>,
966 id_start: u32,
967 id_end_exclusive: u32,
968) {
969 let mut retained = HashMap::new();
970 for (id, session) in tcp_sessions.drain() {
971 let owner_id = session.owner_id();
972 if owner_id >= id_start && owner_id < id_end_exclusive {
973 session.close();
974 } else {
975 retained.insert(id, session);
976 }
977 }
978 *tcp_sessions = retained;
979}
980
981fn encode_tcp_failed(id: u32, error: String, out_buf: &mut Vec<u8>) -> AgentdResult<()> {
982 let reply = Message::with_payload(MessageType::TcpFailed, id, &TcpFailed { error })
983 .map_err(|e| AgentdError::ExecSession(format!("encode tcp failed: {e}")))?;
984 codec::encode_to_buf(&reply, out_buf)
985 .map_err(|e| AgentdError::ExecSession(format!("encode tcp failed frame: {e}")))?;
986 Ok(())
987}
988
989fn encode_core_error_if_supported(
990 source: &Message,
991 id: u32,
992 kind: CoreErrorKind,
993 message: String,
994 offending_type: Option<String>,
995 out_buf: &mut Vec<u8>,
996) -> AgentdResult<()> {
997 if !MessageType::CoreError.is_available_at(source.v) {
998 return Err(AgentdError::ExecSession(format!(
999 "cannot send core.error to protocol generation {}",
1000 source.v
1001 )));
1002 }
1003
1004 encode_core_error(id, kind, message, offending_type, out_buf)
1005}
1006
1007fn encode_core_error(
1008 id: u32,
1009 kind: CoreErrorKind,
1010 message: String,
1011 offending_type: Option<String>,
1012 out_buf: &mut Vec<u8>,
1013) -> AgentdResult<()> {
1014 let reply = Message::with_payload(
1015 MessageType::CoreError,
1016 id,
1017 &CoreError {
1018 kind,
1019 message,
1020 offending_type,
1021 },
1022 )
1023 .map_err(|e| AgentdError::ExecSession(format!("encode core error: {e}")))?;
1024 codec::encode_to_buf(&reply, out_buf)
1025 .map_err(|e| AgentdError::ExecSession(format!("encode core error frame: {e}")))?;
1026 Ok(())
1027}
1028
1029fn decode_payload_or_core_error<T>(msg: &Message, out_buf: &mut Vec<u8>) -> AgentdResult<Option<T>>
1030where
1031 T: serde::de::DeserializeOwned,
1032{
1033 match msg.payload::<T>() {
1034 Ok(payload) => Ok(Some(payload)),
1035 Err(error) => {
1036 encode_core_error_if_supported(
1037 msg,
1038 msg.id,
1039 CoreErrorKind::InvalidPayload,
1040 format!("decode payload for {}: {error}", msg.t.as_str()),
1041 Some(msg.t.as_str().to_string()),
1042 out_buf,
1043 )?;
1044 Ok(None)
1045 }
1046 }
1047}
1048
1049fn stdin_error_payload(err: &AgentdError) -> ExecStdinError {
1051 let io_err = match err {
1052 AgentdError::Io(e) => Some(e),
1053 _ => None,
1054 };
1055 let errno = io_err.and_then(|e| e.raw_os_error());
1056 ExecStdinError {
1057 errno,
1058 errno_name: errno.and_then(errno_name),
1059 message: err.to_string(),
1060 }
1061}
1062
1063fn errno_name(code: i32) -> Option<String> {
1066 let name = match code {
1067 libc::EPIPE => "EPIPE",
1068 libc::EBADF => "EBADF",
1069 libc::EINVAL => "EINVAL",
1070 libc::EIO => "EIO",
1071 libc::ENOSPC => "ENOSPC",
1072 libc::EFBIG => "EFBIG",
1073 _ => return None,
1074 };
1075 Some(name.to_string())
1076}
1077
1078fn prepend_scripts_to_path(req: &mut microsandbox_protocol::exec::ExecRequest) {
1079 if let Some(entry) = req.env.iter_mut().find(|e| e.starts_with("PATH=")) {
1081 let existing = &entry["PATH=".len()..];
1082 *entry = format!("PATH={}", scripts_path(Some(existing)));
1083 } else {
1084 let inherited = env::var("PATH").ok();
1085 req.env
1086 .push(format!("PATH={}", scripts_path(inherited.as_deref())));
1087 }
1088}
1089
1090fn set_nonblocking(fd: i32) -> AgentdResult<()> {
1092 let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
1093 if flags < 0 {
1094 return Err(std::io::Error::last_os_error().into());
1095 }
1096 let ret = unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) };
1097 if ret < 0 {
1098 return Err(std::io::Error::last_os_error().into());
1099 }
1100 Ok(())
1101}
1102
1103fn init_ack_deadline() -> Instant {
1104 Instant::now() + std::time::Duration::from_secs(INIT_ACK_TIMEOUT_SECS)
1105}
1106
1107fn init_ack_timeout() -> AgentdError {
1108 AgentdError::ExecSession("timed out waiting for init ack".into())
1109}
1110
1111fn wait_for_init_ack(
1112 fd: i32,
1113 boot_console: &mut BootConsoleState,
1114 deadline: Instant,
1115) -> AgentdResult<()> {
1116 let msg = read_boot_message(fd, boot_console, deadline, "init ack")?;
1117 if msg.t == MessageType::InitAck {
1118 let _: InitAck = msg
1119 .payload()
1120 .map_err(|e| AgentdError::ExecSession(format!("decode init ack payload: {e}")))?;
1121 return Ok(());
1122 }
1123
1124 Err(AgentdError::ExecSession(format!(
1125 "expected core.init.ack, got {}",
1126 msg.t.as_str()
1127 )))
1128}
1129
1130fn read_boot_message(
1131 fd: i32,
1132 state: &mut BootConsoleState,
1133 deadline: Instant,
1134 context: &str,
1135) -> AgentdResult<Message> {
1136 let mut read_buf = [0u8; 4096];
1137 loop {
1138 if let Some(msg) = codec::try_decode_from_buf(&mut state.input)
1139 .map_err(|e| AgentdError::ExecSession(format!("decode {context}: {e}")))?
1140 {
1141 return Ok(msg);
1142 }
1143 if state.input.len() > MAX_INPUT_BUF_SIZE {
1144 return Err(AgentdError::ExecSession(format!(
1145 "serial input buffer exceeded maximum size while waiting for {context}"
1146 )));
1147 }
1148 if !poll_fd_until(fd, libc::POLLIN, deadline)? {
1149 return Err(if context == "init ack" {
1150 init_ack_timeout()
1151 } else {
1152 AgentdError::ExecSession(format!("timed out waiting for {context}"))
1153 });
1154 }
1155 let n = match read_from_fd(fd, &mut read_buf) {
1156 Ok(n) => n,
1157 Err(error)
1158 if matches!(
1159 error.kind(),
1160 std::io::ErrorKind::Interrupted | std::io::ErrorKind::WouldBlock
1161 ) =>
1162 {
1163 continue;
1164 }
1165 Err(error) => return Err(error.into()),
1166 };
1167 if n == 0 {
1168 return Err(AgentdError::ExecSession(format!(
1169 "serial port closed while waiting for {context}"
1170 )));
1171 }
1172 state.input.extend_from_slice(&read_buf[..n]);
1173 }
1174}
1175
1176fn poll_fd_until(fd: i32, events: i16, deadline: Instant) -> AgentdResult<bool> {
1177 loop {
1178 let remaining = deadline.saturating_duration_since(Instant::now());
1179 if remaining.is_zero() {
1180 return Ok(false);
1181 }
1182
1183 let timeout_ms = remaining.as_millis().min(i32::MAX as u128) as i32;
1184 let timeout_ms = if timeout_ms == 0 { 1 } else { timeout_ms };
1185 let mut pfd = libc::pollfd {
1186 fd,
1187 events,
1188 revents: 0,
1189 };
1190 let ret = unsafe { libc::poll(&mut pfd, 1, timeout_ms) };
1191 if ret > 0 {
1192 return Ok(true);
1193 }
1194 if ret == 0 {
1195 return Ok(false);
1196 }
1197 let err = std::io::Error::last_os_error();
1198 if err.raw_os_error() == Some(libc::EINTR) {
1199 continue;
1200 }
1201 return Err(err.into());
1202 }
1203}
1204
1205fn read_from_fd(fd: i32, buf: &mut [u8]) -> std::io::Result<usize> {
1207 let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
1208 if n < 0 {
1209 Err(std::io::Error::last_os_error())
1210 } else {
1211 Ok(n as usize)
1212 }
1213}
1214
1215fn write_all_to_fd(fd: i32, mut buf: &[u8], deadline: Instant) -> AgentdResult<()> {
1216 while !buf.is_empty() {
1217 match write_to_fd(fd, buf) {
1218 Ok(0) => return Err(std::io::Error::from(std::io::ErrorKind::WriteZero).into()),
1219 Ok(n) => buf = &buf[n..],
1220 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1221 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
1222 if !poll_fd_until(fd, libc::POLLOUT, deadline)? {
1223 return Err(init_ack_timeout());
1224 }
1225 }
1226 Err(e) => return Err(e.into()),
1227 }
1228 }
1229
1230 Ok(())
1231}
1232
1233async fn flush_write_buf(fd: &AsyncFd<std::fs::File>, buf: &mut Vec<u8>) -> AgentdResult<()> {
1235 while !buf.is_empty() {
1236 let mut guard = fd.writable().await?;
1237 match guard.try_io(|inner| write_to_fd(inner.get_ref().as_raw_fd(), buf)) {
1238 Ok(Ok(n)) => {
1239 buf.drain(..n);
1240 }
1241 Ok(Err(e)) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1242 Ok(Err(e)) => return Err(e.into()),
1243 Err(_would_block) => continue,
1244 }
1245 }
1246 Ok(())
1247}
1248
1249fn write_to_fd(fd: i32, buf: &[u8]) -> std::io::Result<usize> {
1251 let n = unsafe { libc::write(fd, buf.as_ptr() as *const libc::c_void, buf.len()) };
1252 if n < 0 {
1253 Err(std::io::Error::last_os_error())
1254 } else {
1255 Ok(n as usize)
1256 }
1257}
1258
1259fn request_guest_poweroff() -> AgentdResult<()> {
1260 if crate::handoff::is_pid_1() {
1261 crate::teardown::teardown_filesystems(true);
1264 let ret = unsafe { libc::reboot(libc::RB_POWER_OFF) };
1265 if ret != 0 {
1266 return Err(std::io::Error::last_os_error().into());
1267 }
1268 return Ok(());
1269 }
1270
1271 unsafe {
1272 libc::sync();
1273 }
1274
1275 if crate::handoff::signal_init_shutdown().is_ok() {
1281 std::thread::sleep(HANDOFF_POWEROFF_TIMEOUT);
1282 }
1283
1284 crate::teardown::teardown_filesystems(false);
1290
1291 let _ = crate::handoff::signal_init_term();
1292 Ok(())
1293}
1294
1295#[cfg(test)]
1300mod tests {
1301 use super::*;
1302 use microsandbox_protocol::message::PROTOCOL_VERSION;
1303
1304 #[test]
1305 fn coalesced_bootstrap_and_init_ack_retain_the_second_frame() {
1306 let bootstrap = GuestBootstrap::default();
1307 let bootstrap_message =
1308 Message::with_payload(MessageType::Bootstrap, 0, &bootstrap).unwrap();
1309 let ack_message = Message::with_payload(MessageType::InitAck, 0, &InitAck {}).unwrap();
1310 let mut state = BootConsoleState::default();
1311 codec::encode_to_buf(&bootstrap_message, &mut state.input).unwrap();
1312 codec::encode_to_buf(&ack_message, &mut state.input).unwrap();
1313
1314 let decoded = read_boot_message(
1315 -1,
1316 &mut state,
1317 Instant::now() + std::time::Duration::from_secs(1),
1318 "guest bootstrap",
1319 )
1320 .unwrap();
1321 assert_eq!(decode_bootstrap_message(decoded).unwrap(), bootstrap);
1322 assert!(
1323 !state.input.is_empty(),
1324 "init ack frame should remain buffered"
1325 );
1326
1327 wait_for_init_ack(
1328 -1,
1329 &mut state,
1330 Instant::now() + std::time::Duration::from_secs(1),
1331 )
1332 .unwrap();
1333 assert!(state.input.is_empty());
1334 }
1335
1336 #[test]
1337 fn bootstrap_rejects_non_control_correlation_fields() {
1338 let mut message =
1339 Message::with_payload(MessageType::Bootstrap, 1, &GuestBootstrap::default()).unwrap();
1340 message.flags = 1;
1341
1342 let error = decode_bootstrap_message(message).unwrap_err();
1343 assert!(error.to_string().contains("requires id=0 and flags=0"));
1344 }
1345
1346 #[test]
1347 fn bootstrap_rejects_wrong_first_message_type() {
1348 let message = Message::with_payload(MessageType::Ping, 0, &Ping {}).unwrap();
1349
1350 let error = decode_bootstrap_message(message).unwrap_err();
1351 assert!(error.to_string().contains("expected core.bootstrap"));
1352 }
1353
1354 #[test]
1355 fn bootstrap_rejects_older_protocol_generation() {
1356 let mut message =
1357 Message::with_payload(MessageType::Bootstrap, 0, &GuestBootstrap::default()).unwrap();
1358 message.v = PROTOCOL_VERSION - 1;
1359
1360 let error = decode_bootstrap_message(message).unwrap_err();
1361 assert!(error.to_string().contains("or newer"));
1362 }
1363
1364 #[test]
1365 fn bootstrap_accepts_newer_additive_protocol_generation() {
1366 let mut message =
1367 Message::with_payload(MessageType::Bootstrap, 0, &GuestBootstrap::default()).unwrap();
1368 message.v = PROTOCOL_VERSION + 1;
1369
1370 assert_eq!(
1371 decode_bootstrap_message(message).unwrap(),
1372 GuestBootstrap::default()
1373 );
1374 }
1375
1376 #[test]
1377 fn bootstrap_rejects_malformed_payload() {
1378 let message = Message::new(MessageType::Bootstrap, 0, vec![0xff]);
1379
1380 let error = decode_bootstrap_message(message).unwrap_err();
1381 assert!(error.to_string().contains("decode guest bootstrap payload"));
1382 }
1383
1384 #[test]
1385 fn record_encoded_guest_messages_counts_only_appended_frames() {
1386 let mut out_buf = Vec::new();
1387 let existing =
1388 Message::with_payload(MessageType::ExecStarted, 1, &ExecStarted { pid: 123 }).unwrap();
1389 codec::encode_to_buf(&existing, &mut out_buf).unwrap();
1390 let start = out_buf.len();
1391
1392 let appended =
1393 Message::with_payload(MessageType::ExecStarted, 2, &ExecStarted { pid: 456 }).unwrap();
1394 codec::encode_to_buf(&appended, &mut out_buf).unwrap();
1395
1396 let mut activity = ActivityTracker::new();
1397 record_encoded_guest_messages(&out_buf, start, &mut activity);
1398
1399 assert_eq!(activity.activity_seq, 1);
1400 assert_eq!(activity.counters.guest_messages, 1);
1401 }
1402
1403 #[test]
1404 fn apply_raw_activity_updates_guest_and_byte_counters() {
1405 let mut activity = ActivityTracker::new();
1406
1407 apply_raw_activity(RawActivity::fs_bytes(42), &mut activity);
1408 apply_raw_activity(RawActivity::tcp_bytes(7), &mut activity);
1409
1410 assert_eq!(activity.activity_seq, 2);
1411 assert_eq!(activity.counters.guest_messages, 2);
1412 assert_eq!(activity.counters.fs_bytes, 42);
1413 assert_eq!(activity.counters.tcp_bytes, 7);
1414 }
1415
1416 #[test]
1417 fn maintenance_messages_do_not_implicitly_refresh_idle_timer() {
1418 assert!(!message_refreshes_idle_timer(&MessageType::ClockSync));
1419 assert!(!message_refreshes_idle_timer(&MessageType::Ping));
1420 assert!(!message_refreshes_idle_timer(&MessageType::Touch));
1421 assert!(message_refreshes_idle_timer(&MessageType::ExecRequest));
1422 }
1423
1424 #[test]
1425 fn maintenance_replies_do_not_refresh_idle_timer() {
1426 assert!(!guest_message_refreshes_idle_timer(&MessageType::Pong));
1427 assert!(!guest_message_refreshes_idle_timer(&MessageType::Touched));
1428 assert!(!guest_message_refreshes_idle_timer(&MessageType::CoreError));
1429 assert!(guest_message_refreshes_idle_timer(&MessageType::ExecStdout));
1430 }
1431
1432 #[test]
1433 fn record_encoded_guest_messages_ignores_pong_and_touched() {
1434 let mut out_buf = Vec::new();
1435 let pong = Message::with_payload(MessageType::Pong, 1, &Pong {}).unwrap();
1436 codec::encode_to_buf(&pong, &mut out_buf).unwrap();
1437
1438 let touched =
1439 Message::with_payload(MessageType::Touched, 2, &Touched { activity_seq: 42 }).unwrap();
1440 codec::encode_to_buf(&touched, &mut out_buf).unwrap();
1441
1442 let mut activity = ActivityTracker::new();
1443 record_encoded_guest_messages(&out_buf, 0, &mut activity);
1444
1445 assert_eq!(activity.activity_seq, 0);
1446 assert_eq!(activity.counters.guest_messages, 0);
1447 }
1448}