Skip to main content

microsandbox_protocol/
exec.rs

1//! Exec-related protocol message payloads.
2
3use std::str::FromStr;
4
5use serde::{Deserialize, Serialize};
6
7//--------------------------------------------------------------------------------------------------
8// Types
9//--------------------------------------------------------------------------------------------------
10
11/// Request to execute a command in the guest.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ExecRequest {
14    /// The command to execute (program path).
15    pub cmd: String,
16
17    /// Arguments to the command.
18    #[serde(default)]
19    pub args: Vec<String>,
20
21    /// Environment variables as key=value pairs.
22    #[serde(default)]
23    pub env: Vec<String>,
24
25    /// Working directory for the command.
26    #[serde(default)]
27    pub cwd: Option<String>,
28
29    /// Optional guest user override for the command.
30    #[serde(default)]
31    pub user: Option<String>,
32
33    /// Whether to allocate a PTY for the command.
34    #[serde(default)]
35    pub tty: bool,
36
37    /// Initial terminal rows (only used when `tty` is true).
38    #[serde(default = "default_rows")]
39    pub rows: u16,
40
41    /// Initial terminal columns (only used when `tty` is true).
42    #[serde(default = "default_cols")]
43    pub cols: u16,
44
45    /// POSIX resource limits to apply to the spawned process via `setrlimit()`.
46    #[serde(default)]
47    pub rlimits: Vec<ExecRlimit>,
48}
49
50/// POSIX resource limit identifiers (maps to `RLIMIT_*` constants).
51///
52/// This is the canonical set of resource names understood by the protocol;
53/// both the host-side builders and the guest-side parser agree on it.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55pub enum RlimitResource {
56    /// Max CPU time in seconds (`RLIMIT_CPU`).
57    Cpu,
58    /// Max file size in bytes (`RLIMIT_FSIZE`).
59    Fsize,
60    /// Max data segment size (`RLIMIT_DATA`).
61    Data,
62    /// Max stack size (`RLIMIT_STACK`).
63    Stack,
64    /// Max core file size (`RLIMIT_CORE`).
65    Core,
66    /// Max resident set size (`RLIMIT_RSS`).
67    Rss,
68    /// Max number of processes (`RLIMIT_NPROC`).
69    Nproc,
70    /// Max open file descriptors (`RLIMIT_NOFILE`).
71    Nofile,
72    /// Max locked memory (`RLIMIT_MEMLOCK`).
73    Memlock,
74    /// Max address space size (`RLIMIT_AS`).
75    As,
76    /// Max file locks (`RLIMIT_LOCKS`).
77    Locks,
78    /// Max pending signals (`RLIMIT_SIGPENDING`).
79    Sigpending,
80    /// Max bytes in POSIX message queues (`RLIMIT_MSGQUEUE`).
81    Msgqueue,
82    /// Max nice priority (`RLIMIT_NICE`).
83    Nice,
84    /// Max real-time priority (`RLIMIT_RTPRIO`).
85    Rtprio,
86    /// Max real-time timeout (`RLIMIT_RTTIME`).
87    Rttime,
88}
89
90/// A POSIX resource limit to apply to a spawned process.
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92pub struct ExecRlimit {
93    /// Resource name (lowercase): "nofile", "nproc", "as", "cpu", etc.
94    pub resource: String,
95
96    /// Soft limit (can be raised up to hard limit by the process).
97    pub soft: u64,
98
99    /// Hard limit (ceiling, requires privileges to raise).
100    pub hard: u64,
101}
102
103/// Confirmation that a command has been started.
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct ExecStarted {
106    /// The PID of the spawned process.
107    pub pid: u32,
108}
109
110/// Stdin data sent to a running command.
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct ExecStdin {
113    /// The raw input data.
114    #[serde(with = "serde_bytes")]
115    pub data: Vec<u8>,
116}
117
118/// Stdout data from a running command.
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct ExecStdout {
121    /// The raw output data.
122    #[serde(with = "serde_bytes")]
123    pub data: Vec<u8>,
124}
125
126/// Stderr data from a running command.
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct ExecStderr {
129    /// The raw error output data.
130    #[serde(with = "serde_bytes")]
131    pub data: Vec<u8>,
132}
133
134/// Notification that a command has exited.
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct ExecExited {
137    /// The exit code of the process.
138    pub code: i32,
139}
140
141/// Request to resize the PTY of a running command.
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct ExecResize {
144    /// New number of rows.
145    pub rows: u16,
146
147    /// New number of columns.
148    pub cols: u16,
149}
150
151/// Request to send a signal to a running command.
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct ExecSignal {
154    /// The signal number to send (e.g. 15 for SIGTERM).
155    pub signal: i32,
156}
157
158//--------------------------------------------------------------------------------------------------
159// Functions
160//--------------------------------------------------------------------------------------------------
161
162fn default_rows() -> u16 {
163    24
164}
165
166fn default_cols() -> u16 {
167    80
168}
169
170//--------------------------------------------------------------------------------------------------
171// Methods
172//--------------------------------------------------------------------------------------------------
173
174impl RlimitResource {
175    /// Returns the lowercase string representation used on the wire.
176    pub fn as_str(&self) -> &'static str {
177        match self {
178            Self::Cpu => "cpu",
179            Self::Fsize => "fsize",
180            Self::Data => "data",
181            Self::Stack => "stack",
182            Self::Core => "core",
183            Self::Rss => "rss",
184            Self::Nproc => "nproc",
185            Self::Nofile => "nofile",
186            Self::Memlock => "memlock",
187            Self::As => "as",
188            Self::Locks => "locks",
189            Self::Sigpending => "sigpending",
190            Self::Msgqueue => "msgqueue",
191            Self::Nice => "nice",
192            Self::Rtprio => "rtprio",
193            Self::Rttime => "rttime",
194        }
195    }
196}
197
198//--------------------------------------------------------------------------------------------------
199// Trait Implementations
200//--------------------------------------------------------------------------------------------------
201
202/// Case-insensitive string to [`RlimitResource`] conversion.
203impl TryFrom<&str> for RlimitResource {
204    type Error = String;
205
206    fn try_from(s: &str) -> Result<Self, Self::Error> {
207        match s.to_ascii_lowercase().as_str() {
208            "cpu" => Ok(Self::Cpu),
209            "fsize" => Ok(Self::Fsize),
210            "data" => Ok(Self::Data),
211            "stack" => Ok(Self::Stack),
212            "core" => Ok(Self::Core),
213            "rss" => Ok(Self::Rss),
214            "nproc" => Ok(Self::Nproc),
215            "nofile" => Ok(Self::Nofile),
216            "memlock" => Ok(Self::Memlock),
217            "as" => Ok(Self::As),
218            "locks" => Ok(Self::Locks),
219            "sigpending" => Ok(Self::Sigpending),
220            "msgqueue" => Ok(Self::Msgqueue),
221            "nice" => Ok(Self::Nice),
222            "rtprio" => Ok(Self::Rtprio),
223            "rttime" => Ok(Self::Rttime),
224            _ => Err(format!("unknown rlimit resource: {s}")),
225        }
226    }
227}
228
229impl FromStr for ExecRlimit {
230    type Err = String;
231
232    fn from_str(spec: &str) -> Result<Self, Self::Err> {
233        let (resource, limit) = spec
234            .split_once('=')
235            .ok_or_else(|| "rlimit must be in format RESOURCE=LIMIT".to_string())?;
236
237        let mut parts = limit.split(':');
238        let soft = parts
239            .next()
240            .ok_or_else(|| "missing soft limit".to_string())?
241            .parse::<u64>()
242            .map_err(|err| format!("invalid soft limit: {err}"))?;
243        let hard = match parts.next() {
244            Some(value) => value
245                .parse::<u64>()
246                .map_err(|err| format!("invalid hard limit: {err}"))?,
247            None => soft,
248        };
249
250        if parts.next().is_some() {
251            return Err("too many ':' separators".into());
252        }
253
254        if soft > hard {
255            return Err("soft limit cannot exceed hard limit".into());
256        }
257
258        Ok(Self {
259            resource: resource.to_ascii_lowercase(),
260            soft,
261            hard,
262        })
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::ExecRlimit;
269
270    #[test]
271    fn test_exec_rlimit_from_str_uses_soft_for_hard_when_omitted() {
272        assert_eq!(
273            "NOFILE=65535".parse::<ExecRlimit>().unwrap(),
274            ExecRlimit {
275                resource: "nofile".to_string(),
276                soft: 65_535,
277                hard: 65_535,
278            }
279        );
280    }
281
282    #[test]
283    fn test_exec_rlimit_from_str_parses_soft_and_hard() {
284        assert_eq!(
285            "nofile=4096:65535".parse::<ExecRlimit>().unwrap(),
286            ExecRlimit {
287                resource: "nofile".to_string(),
288                soft: 4_096,
289                hard: 65_535,
290            }
291        );
292    }
293
294    #[test]
295    fn test_exec_rlimit_from_str_rejects_soft_above_hard() {
296        let err = "nofile=65535:4096".parse::<ExecRlimit>().unwrap_err();
297        assert_eq!(err, "soft limit cannot exceed hard limit");
298    }
299}