Skip to main content

microsandbox_agentd/
agent.rs

1//! Main agent loop: serial I/O, session management, heartbeat.
2
3use std::collections::HashMap;
4use std::fs::{File, OpenOptions};
5use std::os::fd::AsRawFd;
6use std::sync::Arc;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::time::Instant;
9use std::{env, ptr};
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::codec::{self, MAX_FRAME_SIZE};
18use microsandbox_protocol::core::{
19    ClockSync, CoreError, CoreErrorKind, InitAck, InitResolved, Ready, RelayClientDisconnected,
20    ResolvedUser,
21};
22use microsandbox_protocol::exec::{
23    ExecExited, ExecFailed, ExecFailureKind, ExecRequest, ExecResize, ExecSignal, ExecStarted,
24    ExecStderr, ExecStdin, ExecStdinError, ExecStdout,
25};
26use microsandbox_protocol::fs::{FsData, FsRequest};
27use microsandbox_protocol::heartbeat::{ActivityCounters, Heartbeat};
28use microsandbox_protocol::message::{Message, MessageType};
29use microsandbox_protocol::tcp::{TcpClose, TcpConnect, TcpData, TcpEof, TcpFailed};
30
31use crate::config::AgentdConfig;
32use crate::error::{AgentdError, AgentdResult};
33use crate::fs::{FsReadSession, FsState, FsStreamSession, FsWriteSession};
34use crate::serial::AGENT_PORT_NAME;
35use crate::session::{
36    ExecSession, RawActivity, RawSessionCompletion, SessionOutput, resolve_default_user,
37};
38use crate::tcp::TcpSession;
39use crate::{clock, fs, handoff, heartbeat, serial};
40
41//--------------------------------------------------------------------------------------------------
42// Constants
43//--------------------------------------------------------------------------------------------------
44
45/// Heartbeat interval in seconds.
46///
47/// Keep this short so small idle timeouts (for example `--idle-timeout 1`)
48/// can be enforced without multi-second scheduling drift.
49const HEARTBEAT_INTERVAL_SECS: u64 = 1;
50
51/// Read buffer size for the serial port.
52const SERIAL_READ_BUF_SIZE: usize = 64 * 1024;
53
54/// Maximum allowed input buffer size (frame size limit + 4 bytes for length prefix).
55const MAX_INPUT_BUF_SIZE: usize = MAX_FRAME_SIZE as usize + 4;
56
57/// Maximum time to wait for the host to acknowledge the init context.
58const INIT_ACK_TIMEOUT_SECS: u64 = 60;
59
60//--------------------------------------------------------------------------------------------------
61// Types
62//--------------------------------------------------------------------------------------------------
63
64#[derive(Default)]
65struct AgentState {
66    sessions: HashMap<u32, ExecSession>,
67    write_sessions: HashMap<u32, FsWriteSession>,
68    read_sessions: HashMap<u32, FsReadSession>,
69    tcp_sessions: HashMap<u32, TcpSession>,
70    fs: FsState,
71}
72
73struct ActivityTracker {
74    activity_seq: u64,
75    counters: ActivityCounters,
76}
77
78#[derive(Clone)]
79struct HeartbeatSnapshot {
80    activity_seq: u64,
81    active_exec_sessions: u32,
82    active_fs_streams: u32,
83    active_tcp_streams: u32,
84    counters: ActivityCounters,
85}
86
87//--------------------------------------------------------------------------------------------------
88// Methods
89//--------------------------------------------------------------------------------------------------
90
91impl ActivityTracker {
92    fn new() -> Self {
93        Self {
94            activity_seq: 0,
95            counters: ActivityCounters::default(),
96        }
97    }
98
99    fn record_host_message(&mut self) {
100        self.touch();
101        self.counters.host_messages = self.counters.host_messages.saturating_add(1);
102    }
103
104    fn record_guest_message(&mut self) {
105        self.touch();
106        self.counters.guest_messages = self.counters.guest_messages.saturating_add(1);
107    }
108
109    fn add_exec_output_bytes(&mut self, len: usize) {
110        self.counters.exec_output_bytes =
111            self.counters.exec_output_bytes.saturating_add(len as u64);
112    }
113
114    fn add_fs_bytes(&mut self, len: usize) {
115        self.counters.fs_bytes = self.counters.fs_bytes.saturating_add(len as u64);
116    }
117
118    fn add_tcp_bytes(&mut self, len: usize) {
119        self.counters.tcp_bytes = self.counters.tcp_bytes.saturating_add(len as u64);
120    }
121
122    fn touch(&mut self) {
123        self.activity_seq = self.activity_seq.saturating_add(1);
124    }
125}
126
127//--------------------------------------------------------------------------------------------------
128// Functions
129//--------------------------------------------------------------------------------------------------
130
131/// Runs the main agent loop.
132///
133/// Reuses the already-open virtio serial port, sends `core.ready` with boot timing data,
134/// then enters the main select loop handling serial I/O, process output, and heartbeat.
135///
136/// - `boot_time_ns`: `CLOCK_BOOTTIME` at `main()` start (kernel boot duration).
137/// - `init_time_ns`: nanoseconds spent in `init::init()`.
138pub async fn run(
139    boot_time_ns: u64,
140    init_time_ns: u64,
141    config: &AgentdConfig,
142    port_file: File,
143) -> AgentdResult<()> {
144    // Set non-blocking for async I/O. Early boot handshakes use the same fd
145    // in blocking mode before it is moved into the async loop.
146    let port_fd = port_file.as_raw_fd();
147    set_nonblocking(port_fd)?;
148
149    // A single AsyncFd tracks both readable and writable readiness.
150    let async_port = AsyncFd::new(port_file)?;
151
152    // Buffer for serial reads.
153    let mut read_buf = vec![0u8; SERIAL_READ_BUF_SIZE];
154    let mut serial_in_buf = Vec::new();
155    let mut serial_out_buf = Vec::new();
156
157    let mut state = AgentState::default();
158
159    // Channel for session output events.
160    let (session_tx, mut session_rx) = mpsc::unbounded_channel::<(u32, SessionOutput)>();
161
162    // Heartbeat/activity state.
163    let mut activity = ActivityTracker::new();
164    let (heartbeat_tx, heartbeat_rx) = watch::channel(heartbeat_snapshot(&state, &activity));
165    // The liveness pulse runs on a dedicated OS thread, NOT a Tokio task. On the
166    // single-threaded agent runtime a flood of exec output can monopolize the
167    // executor and starve a heartbeat *task*, freezing the pulse even though the
168    // agent is alive — which makes the host wrongly declare it unresponsive and
169    // kill the sandbox. A plain OS thread is scheduled by the guest kernel
170    // independently of the async runtime, so the pulse keeps ticking under load.
171    let heartbeat_shutdown = Arc::new(AtomicBool::new(false));
172    let heartbeat_thread = spawn_heartbeat_thread(heartbeat_rx, Arc::clone(&heartbeat_shutdown));
173
174    // Send core.ready with boot timing data.
175    let ready_time_ns = clock::boottime_ns();
176    let ready_msg = Message::with_payload(
177        MessageType::Ready,
178        0,
179        &Ready {
180            boot_time_ns,
181            init_time_ns,
182            ready_time_ns,
183            agent_version: env!("CARGO_PKG_VERSION").to_string(),
184        },
185    )
186    .map_err(|e| AgentdError::ExecSession(format!("encode ready: {e}")))?;
187    codec::encode_to_buf(&ready_msg, &mut serial_out_buf)
188        .map_err(|e| AgentdError::ExecSession(format!("encode ready frame: {e}")))?;
189    flush_write_buf(&async_port, &mut serial_out_buf).await?;
190
191    // Main loop.
192    'agent: loop {
193        tokio::select! {
194            // Read from serial port.
195            result = async_port.readable() => {
196                let Ok(mut guard) = result else {
197                    break;
198                };
199
200                loop {
201                    match guard.try_io(|inner| read_from_fd(inner.get_ref().as_raw_fd(), &mut read_buf)) {
202                        Ok(Ok(0)) => {
203                            // EOF on serial — host disconnected.
204                            if !handoff::is_pid_1() {
205                                guard.clear_ready();
206                                drop(guard);
207                                time::sleep(Duration::from_millis(100)).await;
208                                break;
209                            }
210                            break 'agent;
211                        }
212                        Ok(Ok(n)) => {
213                            serial_in_buf.extend_from_slice(&read_buf[..n]);
214
215                            // Guard against unbounded buffer growth.
216                            if serial_in_buf.len() > MAX_INPUT_BUF_SIZE {
217                                return Err(AgentdError::ExecSession(
218                                    "serial input buffer exceeded maximum size".into(),
219                                ));
220                            }
221
222                            // Try to parse complete frames. Recoverable
223                            // message-level failures are reported on the same
224                            // correlation ID with `core.error`; unrecoverable
225                            // frame-level failures still close the agent loop.
226                            while let Some(frame) = codec::try_decode_raw_from_buf(&mut serial_in_buf)
227                                .map_err(|e| AgentdError::ExecSession(format!("decode frame: {e}")))?
228                            {
229                                let id = frame.id;
230                                let msg = match codec::raw_frame_to_message(frame) {
231                                    Ok(msg) => msg,
232                                    Err(e) => {
233                                        return Err(AgentdError::ExecSession(format!(
234                                            "decode message for id {id}: {e}"
235                                        )));
236                                    }
237                                };
238
239                                if msg.flags != msg.t.flags() {
240                                    let out_before = serial_out_buf.len();
241                                    encode_core_error_if_supported(
242                                        &msg,
243                                        msg.id,
244                                        CoreErrorKind::InvalidFlags,
245                                        format!(
246                                            "invalid flags for {}: got {}, expected {}",
247                                            msg.t.as_str(),
248                                            msg.flags,
249                                            msg.t.flags()
250                                        ),
251                                        Some(msg.t.as_str().to_string()),
252                                        &mut serial_out_buf,
253                                    )?;
254                                    record_encoded_guest_messages(
255                                        &serial_out_buf,
256                                        out_before,
257                                        &mut activity,
258                                    );
259                                    publish_heartbeat_snapshot(&heartbeat_tx, &state, &activity);
260                                    continue;
261                                }
262
263                                if message_refreshes_idle_timer(&msg.t) {
264                                    activity.record_host_message();
265                                    publish_heartbeat_snapshot(&heartbeat_tx, &state, &activity);
266                                }
267
268                                let out_before = serial_out_buf.len();
269                                handle_message(
270                                    msg,
271                                    &mut state,
272                                    &mut activity,
273                                    &session_tx,
274                                    &mut serial_out_buf,
275                                    config,
276                                ).await?;
277                                record_encoded_guest_messages(
278                                    &serial_out_buf,
279                                    out_before,
280                                    &mut activity,
281                                );
282                                publish_heartbeat_snapshot(&heartbeat_tx, &state, &activity);
283                            }
284
285                            // Flush any outgoing messages.
286                            if !serial_out_buf.is_empty() {
287                                flush_write_buf(&async_port, &mut serial_out_buf).await?;
288                            }
289                        }
290                        Ok(Err(e)) if e.kind() == std::io::ErrorKind::Interrupted => continue,
291                        Ok(Err(_)) if !handoff::is_pid_1() => {
292                            guard.clear_ready();
293                            drop(guard);
294                            time::sleep(Duration::from_millis(100)).await;
295                            break;
296                        }
297                        Ok(Err(e)) => return Err(e.into()),
298                        Err(_would_block) => break,
299                    }
300                }
301            }
302
303            // Receive output events from session reader tasks.
304            Some((id, output)) = session_rx.recv() => {
305                match output {
306                    SessionOutput::Stdout(data) => {
307                        let len = data.len();
308                        let msg = Message::with_payload(MessageType::ExecStdout, id, &ExecStdout { data })
309                            .map_err(|e| AgentdError::ExecSession(format!("encode stdout: {e}")))?;
310                        codec::encode_to_buf(&msg, &mut serial_out_buf)
311                            .map_err(|e| AgentdError::ExecSession(format!("encode stdout frame: {e}")))?;
312                        activity.record_guest_message();
313                        activity.add_exec_output_bytes(len);
314                    }
315                    SessionOutput::Stderr(data) => {
316                        let len = data.len();
317                        let msg = Message::with_payload(MessageType::ExecStderr, id, &ExecStderr { data })
318                            .map_err(|e| AgentdError::ExecSession(format!("encode stderr: {e}")))?;
319                        codec::encode_to_buf(&msg, &mut serial_out_buf)
320                            .map_err(|e| AgentdError::ExecSession(format!("encode stderr frame: {e}")))?;
321                        activity.record_guest_message();
322                        activity.add_exec_output_bytes(len);
323                    }
324                    SessionOutput::Exited(code) => {
325                        let msg = Message::with_payload(MessageType::ExecExited, id, &ExecExited { code })
326                            .map_err(|e| AgentdError::ExecSession(format!("encode exited: {e}")))?;
327                        codec::encode_to_buf(&msg, &mut serial_out_buf)
328                            .map_err(|e| AgentdError::ExecSession(format!("encode exited frame: {e}")))?;
329                        state.sessions.remove(&id);
330                        activity.record_guest_message();
331                    }
332                    SessionOutput::Raw(output) => {
333                        apply_raw_activity(output.activity, &mut activity);
334                        complete_raw_session(
335                            id,
336                            output.completion,
337                            &mut state.read_sessions,
338                            &mut state.tcp_sessions,
339                        );
340                        // Pre-encoded frame — write directly to output buffer.
341                        serial_out_buf.extend_from_slice(&output.frame);
342                    }
343                }
344                publish_heartbeat_snapshot(&heartbeat_tx, &state, &activity);
345
346                if !serial_out_buf.is_empty() {
347                    flush_write_buf(&async_port, &mut serial_out_buf).await?;
348                }
349            }
350        }
351    }
352
353    heartbeat_shutdown.store(true, Ordering::Relaxed);
354    let _ = heartbeat_thread.join();
355
356    Ok(())
357}
358
359/// Opens the agent virtio-serial port once for early boot handshakes and the agent loop.
360pub fn open_serial_port() -> AgentdResult<File> {
361    // Discover serial port.
362    let port_path = serial::find_serial_port(AGENT_PORT_NAME)?;
363
364    // Open the port once with read+write. Virtio-console multiport devices
365    // only allow a single open; a second open returns EBUSY.
366    Ok(OpenOptions::new().read(true).write(true).open(&port_path)?)
367}
368
369/// Reports init-time guest context to the host and waits for an acknowledgement.
370pub fn report_init_context(port_file: &File, default_user: Option<&str>) -> AgentdResult<()> {
371    let (uid, gid) = resolve_default_user(default_user)?;
372    let deadline = init_ack_deadline();
373    let fd = port_file.as_raw_fd();
374    set_nonblocking(fd)?;
375
376    let msg = Message::with_payload(
377        MessageType::InitResolved,
378        0,
379        &InitResolved {
380            default_user: ResolvedUser { uid, gid },
381        },
382    )
383    .map_err(|e| AgentdError::ExecSession(format!("encode init context: {e}")))?;
384
385    let mut out = Vec::new();
386    codec::encode_to_buf(&msg, &mut out)
387        .map_err(|e| AgentdError::ExecSession(format!("encode init context frame: {e}")))?;
388    write_all_to_fd(fd, &out, deadline)?;
389    wait_for_init_ack(fd, deadline)
390}
391
392//--------------------------------------------------------------------------------------------------
393// Functions: Helpers
394//--------------------------------------------------------------------------------------------------
395
396/// Handles a single incoming message from the host.
397async fn handle_message(
398    msg: Message,
399    state: &mut AgentState,
400    activity: &mut ActivityTracker,
401    session_tx: &mpsc::UnboundedSender<(u32, SessionOutput)>,
402    out_buf: &mut Vec<u8>,
403    config: &AgentdConfig,
404) -> AgentdResult<()> {
405    match msg.t {
406        MessageType::ExecRequest => {
407            let Some(mut req) = decode_payload_or_core_error::<ExecRequest>(&msg, out_buf)? else {
408                return Ok(());
409            };
410            prepend_scripts_to_path(&mut req);
411            match ExecSession::spawn(
412                msg.id,
413                &req,
414                session_tx.clone(),
415                config.user.as_deref(),
416                config.security_profile,
417            ) {
418                Ok(session) => {
419                    let reply = Message::with_payload(
420                        MessageType::ExecStarted,
421                        msg.id,
422                        &ExecStarted { pid: session.pid() },
423                    )
424                    .map_err(|e| AgentdError::ExecSession(format!("encode started: {e}")))?;
425                    codec::encode_to_buf(&reply, out_buf).map_err(|e| {
426                        AgentdError::ExecSession(format!("encode started frame: {e}"))
427                    })?;
428                    state.sessions.insert(msg.id, session);
429                }
430                Err(e) => {
431                    // Send a typed `ExecFailed` so the host can render a
432                    // useful message + hint. `ExecSpawnFailed` already
433                    // carries the structured payload; other error
434                    // variants (free-form `ExecSession(_)` etc.) get
435                    // wrapped as `Other` with the message preserved.
436                    let payload = match &e {
437                        AgentdError::ExecSpawnFailed(p) => p.clone(),
438                        other => ExecFailed {
439                            kind: ExecFailureKind::Other,
440                            errno: None,
441                            errno_name: None,
442                            message: other.to_string(),
443                            stage: None,
444                        },
445                    };
446                    let reply = Message::with_payload(MessageType::ExecFailed, msg.id, &payload)
447                        .map_err(|e| AgentdError::ExecSession(format!("encode failed: {e}")))?;
448                    codec::encode_to_buf(&reply, out_buf).map_err(|e| {
449                        AgentdError::ExecSession(format!("encode failed frame: {e}"))
450                    })?;
451                    eprintln!("failed to spawn exec session {}: {e}", msg.id);
452                }
453            }
454        }
455
456        MessageType::ExecStdin => {
457            let Some(stdin) = decode_payload_or_core_error::<ExecStdin>(&msg, out_buf)? else {
458                return Ok(());
459            };
460            if let Some(session) = state.sessions.get_mut(&msg.id) {
461                if stdin.data.is_empty() {
462                    // Empty data signals EOF — close stdin.
463                    session.close_stdin();
464                } else if let Err(e) = session.write_stdin(&stdin.data).await {
465                    let payload = stdin_error_payload(&e);
466                    eprintln!("stdin write error on session {}: {e}", msg.id);
467                    let reply =
468                        Message::with_payload(MessageType::ExecStdinError, msg.id, &payload)
469                            .map_err(|e| {
470                                AgentdError::ExecSession(format!("encode stdin error: {e}"))
471                            })?;
472                    codec::encode_to_buf(&reply, out_buf).map_err(|e| {
473                        AgentdError::ExecSession(format!("encode stdin error frame: {e}"))
474                    })?;
475                }
476            }
477        }
478
479        MessageType::ExecResize => {
480            let Some(resize) = decode_payload_or_core_error::<ExecResize>(&msg, out_buf)? else {
481                return Ok(());
482            };
483            if let Some(session) = state.sessions.get(&msg.id) {
484                let _ = session.resize(resize.rows, resize.cols);
485            }
486        }
487
488        MessageType::ExecSignal => {
489            let Some(signal) = decode_payload_or_core_error::<ExecSignal>(&msg, out_buf)? else {
490                return Ok(());
491            };
492            if let Some(session) = state.sessions.get(&msg.id) {
493                let _ = session.send_signal(signal.signal);
494            }
495        }
496
497        MessageType::FsRequest => {
498            let Some(req) = decode_payload_or_core_error::<FsRequest>(&msg, out_buf)? else {
499                return Ok(());
500            };
501            match fs::handle_fs_request(msg.id, req, &mut state.fs, out_buf, session_tx).await {
502                Ok(Some(FsStreamSession::Read(rs))) => {
503                    state.read_sessions.insert(msg.id, rs);
504                }
505                Ok(Some(FsStreamSession::Write(ws))) => {
506                    state.write_sessions.insert(msg.id, ws);
507                }
508                Ok(None) => {}
509                Err(e) => {
510                    eprintln!("fs request error for {}: {e}", msg.id);
511                }
512            }
513        }
514
515        MessageType::FsData => {
516            let Some(data) = decode_payload_or_core_error::<FsData>(&msg, out_buf)? else {
517                return Ok(());
518            };
519            let len = data.data.len();
520            if let Some(session) = state.write_sessions.get_mut(&msg.id) {
521                match fs::handle_fs_data(msg.id, data, session, out_buf).await {
522                    Ok(true) => {
523                        // Session complete — remove it.
524                        state.write_sessions.remove(&msg.id);
525                    }
526                    Ok(false) => {
527                        activity.add_fs_bytes(len);
528                    }
529                    Err(e) => {
530                        eprintln!("fs data error for {}: {e}", msg.id);
531                        state.write_sessions.remove(&msg.id);
532                    }
533                }
534            } else {
535                // No write session for this ID — send error response.
536                let resp = microsandbox_protocol::fs::FsResponse {
537                    ok: false,
538                    error: Some(format!("unknown write session: {}", msg.id)),
539                    data: None,
540                };
541                let reply = Message::with_payload(MessageType::FsResponse, msg.id, &resp)
542                    .map_err(|e| AgentdError::ExecSession(format!("encode fs error: {e}")))?;
543                codec::encode_to_buf(&reply, out_buf)
544                    .map_err(|e| AgentdError::ExecSession(format!("encode fs error frame: {e}")))?;
545            }
546        }
547
548        MessageType::TcpConnect => {
549            let Some(req) = decode_payload_or_core_error::<TcpConnect>(&msg, out_buf)? else {
550                return Ok(());
551            };
552            // The connect runs inside the session task; the agent loop never
553            // blocks on it. Success or failure arrives later as a tcp frame.
554            let session = TcpSession::open(msg.id, req, session_tx);
555            state.tcp_sessions.insert(msg.id, session);
556        }
557
558        MessageType::TcpData => {
559            let Some(data) = decode_payload_or_core_error::<TcpData>(&msg, out_buf)? else {
560                return Ok(());
561            };
562            let len = data.data.len();
563            if let Some(session) = state.tcp_sessions.get(&msg.id) {
564                if let Err(e) = session.write_data(data.data).await {
565                    state.tcp_sessions.remove(&msg.id);
566                    encode_tcp_failed(msg.id, e, out_buf)?;
567                } else {
568                    activity.add_tcp_bytes(len);
569                }
570            } else {
571                encode_tcp_failed(msg.id, format!("unknown TCP session: {}", msg.id), out_buf)?;
572            }
573        }
574
575        MessageType::TcpEof => {
576            let Some(_) = decode_payload_or_core_error::<TcpEof>(&msg, out_buf)? else {
577                return Ok(());
578            };
579            if let Some(session) = state.tcp_sessions.get(&msg.id)
580                && let Err(e) = session.close_write().await
581            {
582                state.tcp_sessions.remove(&msg.id);
583                encode_tcp_failed(msg.id, e, out_buf)?;
584            }
585        }
586
587        MessageType::TcpClose => {
588            let Some(_) = decode_payload_or_core_error::<TcpClose>(&msg, out_buf)? else {
589                return Ok(());
590            };
591            if let Some(session) = state.tcp_sessions.remove(&msg.id) {
592                session.close();
593            }
594        }
595
596        MessageType::RelayClientDisconnected => {
597            let Some(disconnected) =
598                decode_payload_or_core_error::<RelayClientDisconnected>(&msg, out_buf)?
599            else {
600                return Ok(());
601            };
602            state
603                .fs
604                .close_owner_range(disconnected.id_start, disconnected.id_end_exclusive);
605            abort_read_sessions_in_owner_range(
606                &mut state.read_sessions,
607                disconnected.id_start,
608                disconnected.id_end_exclusive,
609            );
610            state.write_sessions.retain(|_, session| {
611                let owner_id = session.owner_id();
612                owner_id < disconnected.id_start || owner_id >= disconnected.id_end_exclusive
613            });
614            close_tcp_sessions_in_owner_range(
615                &mut state.tcp_sessions,
616                disconnected.id_start,
617                disconnected.id_end_exclusive,
618            );
619        }
620
621        MessageType::ClockSync => {
622            let Some(sync) = decode_payload_or_core_error::<ClockSync>(&msg, out_buf)? else {
623                return Ok(());
624            };
625            if let Err(e) = clock::sync_realtime_unix_nanos(sync.unix_time_nanos) {
626                eprintln!("clock: failed to sync realtime clock: {e}");
627            }
628        }
629
630        MessageType::Shutdown => {
631            // Graceful shutdown — signal all sessions, then ask the guest
632            // kernel to power off so block-root filesystems can shut down
633            // cleanly instead of leaving ext4 journal recovery pending.
634            for (_, session) in state.sessions.drain() {
635                let _ = session.send_signal(15); // SIGTERM
636            }
637            state.write_sessions.clear();
638            for (_, session) in state.tcp_sessions.drain() {
639                session.close();
640            }
641            state.fs.clear();
642
643            request_guest_poweroff()?;
644            return Err(AgentdError::Shutdown);
645        }
646
647        _ => {
648            // Ignore unknown or unexpected message types.
649        }
650    }
651
652    Ok(())
653}
654
655/// Prepends `/.msb/scripts` to PATH in the exec request's environment.
656///
657/// If the request already has a PATH entry, prepends to it. Otherwise
658/// inherits from agentd's environment and prepends.
659/// Default PATH for the guest when no PATH is inherited.
660const DEFAULT_GUEST_PATH: &str = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
661
662/// Returns whether a host message should refresh the sandbox idle timer.
663///
664/// Maintenance traffic such as clock synchronization must not count as user
665/// activity, otherwise periodic host tasks would keep an idle sandbox alive.
666fn message_refreshes_idle_timer(t: &MessageType) -> bool {
667    !matches!(t, MessageType::ClockSync)
668}
669
670/// Spawns the heartbeat pulse on a dedicated OS thread.
671///
672/// This thread is intentionally outside the Tokio runtime: it reads the latest
673/// [`HeartbeatSnapshot`] (a lock-free `watch` borrow) and writes the heartbeat
674/// file with blocking `std::fs` once per [`HEARTBEAT_INTERVAL_SECS`]. Because it
675/// is an ordinary kernel-scheduled thread, a CPU-bound or I/O-saturated async
676/// runtime cannot delay the pulse — which is exactly the starvation that made
677/// the host kill busy-but-healthy sandboxes. The sleep is chunked so the thread
678/// observes the shutdown flag promptly when the agent loop exits.
679fn spawn_heartbeat_thread(
680    snapshot_rx: watch::Receiver<HeartbeatSnapshot>,
681    shutdown: Arc<AtomicBool>,
682) -> std::thread::JoinHandle<()> {
683    std::thread::Builder::new()
684        .name("agentd-heartbeat".to_string())
685        .spawn(move || {
686            let mut heartbeat_seq = 0u64;
687            let mut last_activity_seq = snapshot_rx.borrow().activity_seq;
688            let mut last_activity = Utc::now();
689
690            let interval = Duration::from_secs(HEARTBEAT_INTERVAL_SECS);
691            let step = Duration::from_millis(100);
692
693            while !shutdown.load(Ordering::Relaxed) {
694                let mut slept = Duration::ZERO;
695                while slept < interval {
696                    if shutdown.load(Ordering::Relaxed) {
697                        return;
698                    }
699                    std::thread::sleep(step);
700                    slept += step;
701                }
702
703                if !heartbeat::heartbeat_dir_exists() {
704                    continue;
705                }
706
707                heartbeat_seq = heartbeat_seq.saturating_add(1);
708                let snapshot = snapshot_rx.borrow().clone();
709                let timestamp = Utc::now();
710                if snapshot.activity_seq != last_activity_seq {
711                    last_activity_seq = snapshot.activity_seq;
712                    last_activity = timestamp;
713                }
714                let heartbeat = Heartbeat {
715                    heartbeat_seq,
716                    activity_seq: snapshot.activity_seq,
717                    timestamp,
718                    last_activity,
719                    active_exec_sessions: snapshot.active_exec_sessions,
720                    active_fs_streams: snapshot.active_fs_streams,
721                    active_tcp_streams: snapshot.active_tcp_streams,
722                    activity_counters: snapshot.counters,
723                };
724                let _ = heartbeat::write_heartbeat(&heartbeat);
725            }
726        })
727        .expect("failed to spawn agentd heartbeat thread")
728}
729
730fn heartbeat_snapshot(state: &AgentState, activity: &ActivityTracker) -> HeartbeatSnapshot {
731    HeartbeatSnapshot {
732        activity_seq: activity.activity_seq,
733        active_exec_sessions: state.sessions.len() as u32,
734        active_fs_streams: state
735            .read_sessions
736            .len()
737            .saturating_add(state.write_sessions.len()) as u32,
738        active_tcp_streams: state.tcp_sessions.len() as u32,
739        counters: activity.counters,
740    }
741}
742
743fn publish_heartbeat_snapshot(
744    heartbeat_tx: &watch::Sender<HeartbeatSnapshot>,
745    state: &AgentState,
746    activity: &ActivityTracker,
747) {
748    let _ = heartbeat_tx.send(heartbeat_snapshot(state, activity));
749}
750
751fn record_encoded_guest_messages(out_buf: &[u8], start: usize, activity: &mut ActivityTracker) {
752    let mut offset = start;
753    while offset + 4 <= out_buf.len() {
754        let frame_len = u32::from_be_bytes([
755            out_buf[offset],
756            out_buf[offset + 1],
757            out_buf[offset + 2],
758            out_buf[offset + 3],
759        ]) as usize;
760        let total = 4usize.saturating_add(frame_len);
761        if offset.saturating_add(total) > out_buf.len() {
762            break;
763        }
764
765        activity.record_guest_message();
766        offset += total;
767    }
768}
769
770fn apply_raw_activity(raw: RawActivity, activity: &mut ActivityTracker) {
771    if raw.guest_message {
772        activity.record_guest_message();
773    }
774    if raw.fs_bytes > 0 {
775        activity.add_fs_bytes(raw.fs_bytes);
776    }
777    if raw.tcp_bytes > 0 {
778        activity.add_tcp_bytes(raw.tcp_bytes);
779    }
780}
781
782fn complete_raw_session(
783    id: u32,
784    completion: Option<RawSessionCompletion>,
785    read_sessions: &mut HashMap<u32, FsReadSession>,
786    tcp_sessions: &mut HashMap<u32, TcpSession>,
787) {
788    match completion {
789        Some(RawSessionCompletion::FsRead) => {
790            read_sessions.remove(&id);
791        }
792        Some(RawSessionCompletion::Tcp) => {
793            tcp_sessions.remove(&id);
794        }
795        None => {}
796    }
797}
798
799fn abort_read_sessions_in_owner_range(
800    read_sessions: &mut HashMap<u32, FsReadSession>,
801    id_start: u32,
802    id_end_exclusive: u32,
803) {
804    let mut retained = HashMap::new();
805    for (id, session) in read_sessions.drain() {
806        let owner_id = session.owner_id();
807        if owner_id >= id_start && owner_id < id_end_exclusive {
808            session.abort();
809        } else {
810            retained.insert(id, session);
811        }
812    }
813    *read_sessions = retained;
814}
815
816fn close_tcp_sessions_in_owner_range(
817    tcp_sessions: &mut HashMap<u32, TcpSession>,
818    id_start: u32,
819    id_end_exclusive: u32,
820) {
821    let mut retained = HashMap::new();
822    for (id, session) in tcp_sessions.drain() {
823        let owner_id = session.owner_id();
824        if owner_id >= id_start && owner_id < id_end_exclusive {
825            session.close();
826        } else {
827            retained.insert(id, session);
828        }
829    }
830    *tcp_sessions = retained;
831}
832
833fn encode_tcp_failed(id: u32, error: String, out_buf: &mut Vec<u8>) -> AgentdResult<()> {
834    let reply = Message::with_payload(MessageType::TcpFailed, id, &TcpFailed { error })
835        .map_err(|e| AgentdError::ExecSession(format!("encode tcp failed: {e}")))?;
836    codec::encode_to_buf(&reply, out_buf)
837        .map_err(|e| AgentdError::ExecSession(format!("encode tcp failed frame: {e}")))?;
838    Ok(())
839}
840
841fn encode_core_error_if_supported(
842    source: &Message,
843    id: u32,
844    kind: CoreErrorKind,
845    message: String,
846    offending_type: Option<String>,
847    out_buf: &mut Vec<u8>,
848) -> AgentdResult<()> {
849    if !MessageType::CoreError.is_available_at(source.v) {
850        return Err(AgentdError::ExecSession(format!(
851            "cannot send core.error to protocol generation {}",
852            source.v
853        )));
854    }
855
856    encode_core_error(id, kind, message, offending_type, out_buf)
857}
858
859fn encode_core_error(
860    id: u32,
861    kind: CoreErrorKind,
862    message: String,
863    offending_type: Option<String>,
864    out_buf: &mut Vec<u8>,
865) -> AgentdResult<()> {
866    let reply = Message::with_payload(
867        MessageType::CoreError,
868        id,
869        &CoreError {
870            kind,
871            message,
872            offending_type,
873        },
874    )
875    .map_err(|e| AgentdError::ExecSession(format!("encode core error: {e}")))?;
876    codec::encode_to_buf(&reply, out_buf)
877        .map_err(|e| AgentdError::ExecSession(format!("encode core error frame: {e}")))?;
878    Ok(())
879}
880
881fn decode_payload_or_core_error<T>(msg: &Message, out_buf: &mut Vec<u8>) -> AgentdResult<Option<T>>
882where
883    T: serde::de::DeserializeOwned,
884{
885    match msg.payload::<T>() {
886        Ok(payload) => Ok(Some(payload)),
887        Err(error) => {
888            encode_core_error_if_supported(
889                msg,
890                msg.id,
891                CoreErrorKind::InvalidPayload,
892                format!("decode payload for {}: {error}", msg.t.as_str()),
893                Some(msg.t.as_str().to_string()),
894                out_buf,
895            )?;
896            Ok(None)
897        }
898    }
899}
900
901/// Build an `ExecStdinError` payload from a failed `write_stdin` result.
902fn stdin_error_payload(err: &AgentdError) -> ExecStdinError {
903    let io_err = match err {
904        AgentdError::Io(e) => Some(e),
905        _ => None,
906    };
907    let errno = io_err.and_then(|e| e.raw_os_error());
908    ExecStdinError {
909        errno,
910        errno_name: errno.and_then(errno_name),
911        message: err.to_string(),
912    }
913}
914
915/// Map common errno values to their standard names. Returns `None` for
916/// codes we don't recognize; callers fall back to the numeric `errno`.
917fn errno_name(code: i32) -> Option<String> {
918    let name = match code {
919        libc::EPIPE => "EPIPE",
920        libc::EBADF => "EBADF",
921        libc::EINVAL => "EINVAL",
922        libc::EIO => "EIO",
923        libc::ENOSPC => "ENOSPC",
924        libc::EFBIG => "EFBIG",
925        _ => return None,
926    };
927    Some(name.to_string())
928}
929
930fn prepend_scripts_to_path(req: &mut microsandbox_protocol::exec::ExecRequest) {
931    let scripts = microsandbox_protocol::SCRIPTS_PATH;
932
933    // Check if the request already specifies PATH.
934    if let Some(entry) = req.env.iter_mut().find(|e| e.starts_with("PATH=")) {
935        let existing = &entry["PATH=".len()..];
936        *entry = format!("PATH={scripts}:{existing}");
937    } else {
938        // Inherit from agentd's process environment, falling back to a
939        // sensible default since PID 1 in a minimal guest may not have PATH.
940        let inherited = env::var("PATH").unwrap_or_else(|_| DEFAULT_GUEST_PATH.to_string());
941        req.env.push(format!("PATH={scripts}:{inherited}"));
942    }
943}
944
945/// Sets a file descriptor to non-blocking mode.
946fn set_nonblocking(fd: i32) -> AgentdResult<()> {
947    let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
948    if flags < 0 {
949        return Err(std::io::Error::last_os_error().into());
950    }
951    let ret = unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) };
952    if ret < 0 {
953        return Err(std::io::Error::last_os_error().into());
954    }
955    Ok(())
956}
957
958fn init_ack_deadline() -> Instant {
959    Instant::now() + std::time::Duration::from_secs(INIT_ACK_TIMEOUT_SECS)
960}
961
962fn init_ack_timeout() -> AgentdError {
963    AgentdError::ExecSession("timed out waiting for init ack".into())
964}
965
966fn wait_for_init_ack(fd: i32, deadline: Instant) -> AgentdResult<()> {
967    let mut serial_in_buf = Vec::new();
968    let mut read_buf = [0u8; 4096];
969
970    loop {
971        if let Some(msg) = codec::try_decode_from_buf(&mut serial_in_buf)
972            .map_err(|e| AgentdError::ExecSession(format!("decode init ack: {e}")))?
973        {
974            if msg.t == MessageType::InitAck {
975                let _: InitAck = msg.payload().map_err(|e| {
976                    AgentdError::ExecSession(format!("decode init ack payload: {e}"))
977                })?;
978                return Ok(());
979            }
980
981            return Err(AgentdError::ExecSession(format!(
982                "expected core.init.ack, got {}",
983                msg.t.as_str()
984            )));
985        }
986
987        if serial_in_buf.len() > MAX_INPUT_BUF_SIZE {
988            return Err(AgentdError::ExecSession(
989                "serial input buffer exceeded maximum size while waiting for init ack".into(),
990            ));
991        }
992
993        if !poll_fd_until(fd, libc::POLLIN, deadline)? {
994            return Err(init_ack_timeout());
995        }
996
997        let n = match read_from_fd(fd, &mut read_buf) {
998            Ok(n) => n,
999            Err(e)
1000                if matches!(
1001                    e.kind(),
1002                    std::io::ErrorKind::Interrupted | std::io::ErrorKind::WouldBlock
1003                ) =>
1004            {
1005                continue;
1006            }
1007            Err(e) => return Err(e.into()),
1008        };
1009        if n == 0 {
1010            return Err(AgentdError::ExecSession(
1011                "serial port closed while waiting for init ack".into(),
1012            ));
1013        }
1014        serial_in_buf.extend_from_slice(&read_buf[..n]);
1015    }
1016}
1017
1018fn poll_fd_until(fd: i32, events: i16, deadline: Instant) -> AgentdResult<bool> {
1019    loop {
1020        let remaining = deadline.saturating_duration_since(Instant::now());
1021        if remaining.is_zero() {
1022            return Ok(false);
1023        }
1024
1025        let timeout_ms = remaining.as_millis().min(i32::MAX as u128) as i32;
1026        let timeout_ms = if timeout_ms == 0 { 1 } else { timeout_ms };
1027        let mut pfd = libc::pollfd {
1028            fd,
1029            events,
1030            revents: 0,
1031        };
1032        let ret = unsafe { libc::poll(&mut pfd, 1, timeout_ms) };
1033        if ret > 0 {
1034            return Ok(true);
1035        }
1036        if ret == 0 {
1037            return Ok(false);
1038        }
1039        let err = std::io::Error::last_os_error();
1040        if err.raw_os_error() == Some(libc::EINTR) {
1041            continue;
1042        }
1043        return Err(err.into());
1044    }
1045}
1046
1047/// Reads from a raw fd (non-blocking).
1048fn read_from_fd(fd: i32, buf: &mut [u8]) -> std::io::Result<usize> {
1049    let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
1050    if n < 0 {
1051        Err(std::io::Error::last_os_error())
1052    } else {
1053        Ok(n as usize)
1054    }
1055}
1056
1057fn write_all_to_fd(fd: i32, mut buf: &[u8], deadline: Instant) -> AgentdResult<()> {
1058    while !buf.is_empty() {
1059        match write_to_fd(fd, buf) {
1060            Ok(0) => return Err(std::io::Error::from(std::io::ErrorKind::WriteZero).into()),
1061            Ok(n) => buf = &buf[n..],
1062            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1063            Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
1064                if !poll_fd_until(fd, libc::POLLOUT, deadline)? {
1065                    return Err(init_ack_timeout());
1066                }
1067            }
1068            Err(e) => return Err(e.into()),
1069        }
1070    }
1071
1072    Ok(())
1073}
1074
1075/// Flushes the write buffer to the async fd.
1076async fn flush_write_buf(fd: &AsyncFd<std::fs::File>, buf: &mut Vec<u8>) -> AgentdResult<()> {
1077    while !buf.is_empty() {
1078        let mut guard = fd.writable().await?;
1079        match guard.try_io(|inner| write_to_fd(inner.get_ref().as_raw_fd(), buf)) {
1080            Ok(Ok(n)) => {
1081                buf.drain(..n);
1082            }
1083            Ok(Err(e)) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1084            Ok(Err(e)) => return Err(e.into()),
1085            Err(_would_block) => continue,
1086        }
1087    }
1088    Ok(())
1089}
1090
1091/// Writes to a raw fd (non-blocking).
1092fn write_to_fd(fd: i32, buf: &[u8]) -> std::io::Result<usize> {
1093    let n = unsafe { libc::write(fd, buf.as_ptr() as *const libc::c_void, buf.len()) };
1094    if n < 0 {
1095        Err(std::io::Error::last_os_error())
1096    } else {
1097        Ok(n as usize)
1098    }
1099}
1100
1101fn request_guest_poweroff() -> AgentdResult<()> {
1102    unsafe {
1103        libc::sync();
1104    }
1105
1106    if crate::handoff::is_pid_1() {
1107        // PID 1 mode (no handoff): remount root RO and reboot.
1108        let _ = remount_root_readonly();
1109        unsafe {
1110            libc::sync();
1111        }
1112        let ret = unsafe { libc::reboot(libc::RB_POWER_OFF) };
1113        if ret != 0 {
1114            return Err(std::io::Error::last_os_error().into());
1115        }
1116        return Ok(());
1117    }
1118
1119    // Handoff mode: ask the new init (PID 1) to shut down.
1120    // SIGRTMIN+4 is systemd's poweroff signal; sysvinit-derived inits
1121    // typically default-handle it as a clean exit. Either way, PID 1
1122    // exiting causes the kernel to panic the guest, which the VMM
1123    // observes as a clean shutdown.
1124    if crate::handoff::signal_init_shutdown().is_ok() {
1125        std::thread::sleep(HANDOFF_POWEROFF_TIMEOUT);
1126    }
1127
1128    // SIGTERM fallback for inits that didn't act on SIGRTMIN+4. If
1129    // both are ignored, we return Ok and let the host's outer
1130    // VMM-process kill be the backstop — the VM still dies, just
1131    // less gracefully.
1132    let _ = crate::handoff::signal_init_term();
1133    Ok(())
1134}
1135
1136fn remount_root_readonly() -> AgentdResult<()> {
1137    let target = std::ffi::CString::new("/").expect("static path contains no NUL");
1138    let ret = unsafe {
1139        libc::mount(
1140            ptr::null(),
1141            target.as_ptr(),
1142            ptr::null(),
1143            (libc::MS_REMOUNT | libc::MS_RDONLY) as libc::c_ulong,
1144            ptr::null(),
1145        )
1146    };
1147
1148    if ret != 0 {
1149        return Err(std::io::Error::last_os_error().into());
1150    }
1151
1152    Ok(())
1153}
1154
1155//--------------------------------------------------------------------------------------------------
1156// Tests
1157//--------------------------------------------------------------------------------------------------
1158
1159#[cfg(test)]
1160mod tests {
1161    use super::*;
1162
1163    #[test]
1164    fn record_encoded_guest_messages_counts_only_appended_frames() {
1165        let mut out_buf = Vec::new();
1166        let existing =
1167            Message::with_payload(MessageType::ExecStarted, 1, &ExecStarted { pid: 123 }).unwrap();
1168        codec::encode_to_buf(&existing, &mut out_buf).unwrap();
1169        let start = out_buf.len();
1170
1171        let appended =
1172            Message::with_payload(MessageType::ExecStarted, 2, &ExecStarted { pid: 456 }).unwrap();
1173        codec::encode_to_buf(&appended, &mut out_buf).unwrap();
1174
1175        let mut activity = ActivityTracker::new();
1176        record_encoded_guest_messages(&out_buf, start, &mut activity);
1177
1178        assert_eq!(activity.activity_seq, 1);
1179        assert_eq!(activity.counters.guest_messages, 1);
1180    }
1181
1182    #[test]
1183    fn apply_raw_activity_updates_guest_and_byte_counters() {
1184        let mut activity = ActivityTracker::new();
1185
1186        apply_raw_activity(RawActivity::fs_bytes(42), &mut activity);
1187        apply_raw_activity(RawActivity::tcp_bytes(7), &mut activity);
1188
1189        assert_eq!(activity.activity_seq, 2);
1190        assert_eq!(activity.counters.guest_messages, 2);
1191        assert_eq!(activity.counters.fs_bytes, 42);
1192        assert_eq!(activity.counters.tcp_bytes, 7);
1193    }
1194}