microsandbox_protocol/exec.rs
1//! Exec-related protocol message payloads.
2
3use std::str::FromStr;
4
5use serde::{Deserialize, Serialize};
6
7//--------------------------------------------------------------------------------------------------
8// Constants
9//--------------------------------------------------------------------------------------------------
10
11/// Window charge for each output chunk, bounding even very small writes.
12pub use microsandbox_types::EXEC_OUTPUT_EVENT_OVERHEAD;
13
14/// Runtime-owned execution policy advertised in Ready.
15pub use microsandbox_types::ExecLimits;
16
17/// Maximum encoded execution acknowledgement, signal or resize envelope.
18pub const MAX_EXEC_CONTROL_BYTES: usize = 4096;
19
20//--------------------------------------------------------------------------------------------------
21// Types
22//--------------------------------------------------------------------------------------------------
23
24/// Request to execute a command in the guest.
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct ExecRequest {
27 /// The command to execute (program path).
28 pub cmd: String,
29
30 /// Arguments to the command.
31 #[serde(default)]
32 pub args: Vec<String>,
33
34 /// Environment variables as key=value pairs.
35 #[serde(default)]
36 pub env: Vec<String>,
37
38 /// Working directory for the command.
39 #[serde(default)]
40 pub cwd: Option<String>,
41
42 /// Optional guest user override for the command.
43 #[serde(default)]
44 pub user: Option<String>,
45
46 /// Whether to allocate a PTY for the command.
47 #[serde(default)]
48 pub tty: bool,
49
50 /// Initial terminal rows (only used when `tty` is true).
51 #[serde(default = "default_rows")]
52 pub rows: u16,
53
54 /// Initial terminal columns (only used when `tty` is true).
55 #[serde(default = "default_cols")]
56 pub cols: u16,
57
58 /// POSIX resource limits to apply to the spawned process via `setrlimit()`.
59 #[serde(default)]
60 pub rlimits: Vec<ExecRlimit>,
61}
62
63/// A POSIX resource limit to apply to a spawned process.
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub struct ExecRlimit {
66 /// Resource name (lowercase): "nofile", "nproc", "as", "cpu", etc.
67 pub resource: String,
68
69 /// Soft limit (can be raised up to hard limit by the process).
70 pub soft: u64,
71
72 /// Hard limit (ceiling, requires privileges to raise).
73 pub hard: u64,
74}
75
76/// Confirmation that a command has been started.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct ExecStarted {
79 /// The PID of the spawned process.
80 pub pid: u32,
81}
82
83/// Stdin data sent to a running command.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct ExecStdin {
86 /// The raw input data.
87 #[serde(with = "serde_bytes")]
88 pub data: Vec<u8>,
89}
90
91/// Notification that an `ExecStdin` write to the child's stdin failed.
92/// The most common cause is the child closing its read end (EPIPE);
93/// the session is otherwise alive and may still produce output and an
94/// exit code, so this is delivered as a non-terminal event.
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct ExecStdinError {
97 /// `errno` from the underlying write, if available.
98 #[serde(default)]
99 pub errno: Option<i32>,
100
101 /// Standard errno name like `"EPIPE"`, populated when `errno` is.
102 #[serde(default)]
103 pub errno_name: Option<String>,
104
105 /// Human-readable description from agentd.
106 pub message: String,
107}
108
109/// Stdout data from a running command.
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct ExecStdout {
112 /// The raw output data.
113 #[serde(with = "serde_bytes")]
114 pub data: Vec<u8>,
115}
116
117/// Stderr data from a running command.
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct ExecStderr {
120 /// The raw error output data.
121 #[serde(with = "serde_bytes")]
122 pub data: Vec<u8>,
123}
124
125/// Notification that a command has exited.
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct ExecExited {
128 /// The exit code of the process.
129 pub code: i32,
130}
131
132/// Notification that a command failed to start (the user's program
133/// never got to run). Distinct from `ExecExited`, which means the
134/// process ran and reported an exit code.
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct ExecFailed {
137 /// Coarse classification used by the CLI/SDK to pick hints.
138 pub kind: ExecFailureKind,
139
140 /// `errno` if the underlying failure was a syscall.
141 #[serde(default)]
142 pub errno: Option<i32>,
143
144 /// Standard errno name like `"ENOENT"`. Easier to grep than the
145 /// raw number; populated by the agentd classifier.
146 #[serde(default)]
147 pub errno_name: Option<String>,
148
149 /// Human-readable description from agentd. Always populated.
150 pub message: String,
151
152 /// Which step failed when the kind alone isn't enough — e.g.
153 /// `"execvp"`, `"setrlimit(RLIMIT_NOFILE)"`, `"posix_openpt"`.
154 #[serde(default)]
155 pub stage: Option<String>,
156}
157
158/// Coarse classification of an `ExecFailed` cause. The CLI's
159/// stage-to-hint mapper keys off this; the SDK exposes it directly
160/// for programmatic consumers.
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
162#[serde(rename_all = "snake_case")]
163pub enum ExecFailureKind {
164 /// Binary not found on PATH or at the explicit path. ENOENT on
165 /// the binary path itself (not on the cwd — see `BadCwd`).
166 NotFound,
167
168 /// Binary found but not executable: EACCES or EPERM on file.
169 PermissionDenied,
170
171 /// File exists but the kernel can't run it: bad ELF, missing
172 /// interpreter for a shebang script, wrong architecture, etc.
173 /// ENOEXEC.
174 NotExecutable,
175
176 /// Working directory unusable: doesn't exist (ENOENT on cwd),
177 /// not a directory (ENOTDIR), or no permission to chdir.
178 BadCwd,
179
180 /// Argument or env list too large (E2BIG), too many symlinks
181 /// resolving the path (ELOOP), path too long (ENAMETOOLONG),
182 /// or invalid bytes in argv (e.g. interior NUL — EINVAL).
183 BadArgs,
184
185 /// Resource limit prevented the spawn: rejected `setrlimit`
186 /// (EPERM/EINVAL), per-process fork limit (EAGAIN with NPROC),
187 /// fd table exhaustion (EMFILE/ENFILE).
188 ResourceLimit,
189
190 /// User/group setup failed: requested user doesn't exist in the
191 /// sandbox, or `setuid`/`setgid` rejected (EPERM).
192 UserSetupFailed,
193
194 /// Memory pressure: kernel couldn't allocate (ENOMEM, or EAGAIN
195 /// on fork without an explicit rlimit cause).
196 OutOfMemory,
197
198 /// PTY allocation or attachment failed (pty mode only).
199 PtySetupFailed,
200
201 /// Anything else: `errno` is carried verbatim, `message` and
202 /// `stage` describe the specifics.
203 Other,
204}
205
206/// Request to resize the PTY of a running command.
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct ExecResize {
209 /// New number of rows.
210 pub rows: u16,
211
212 /// New number of columns.
213 pub cols: u16,
214}
215
216/// Request to send a signal to a running command.
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct ExecSignal {
219 /// The signal number to send (e.g. 15 for SIGTERM).
220 pub signal: i32,
221}
222
223/// Acknowledges cumulative bytes written into the child's stdin pipe.
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct ExecStdinAck {
226 /// Bytes written to the child's stdin since this execution started.
227 pub consumed: u64,
228}
229
230/// Acknowledges cumulative output consumed by the SDK, including per-chunk overhead.
231#[derive(Debug, Clone, Default, Serialize, Deserialize)]
232pub struct ExecOutputAck {
233 /// Payload bytes plus per-chunk overhead consumed since this exec started.
234 pub consumed: u64,
235}
236
237//--------------------------------------------------------------------------------------------------
238// Methods
239//--------------------------------------------------------------------------------------------------
240
241impl ExecOutputAck {
242 /// Record one consumed output chunk, including its per-chunk overhead.
243 ///
244 /// Returns `None` on counter overflow without changing the acknowledgement.
245 pub fn consume(&mut self, payload_bytes: usize) -> Option<()> {
246 let charge = u64::try_from(payload_bytes)
247 .ok()?
248 .checked_add(u64::from(EXEC_OUTPUT_EVENT_OVERHEAD))?;
249
250 self.consumed = self.consumed.checked_add(charge)?;
251
252 Some(())
253 }
254}
255
256//--------------------------------------------------------------------------------------------------
257// Functions
258//--------------------------------------------------------------------------------------------------
259
260fn default_rows() -> u16 {
261 24
262}
263
264fn default_cols() -> u16 {
265 80
266}
267
268impl FromStr for ExecRlimit {
269 type Err = String;
270
271 fn from_str(spec: &str) -> Result<Self, Self::Err> {
272 let (resource, limit) = spec
273 .split_once('=')
274 .ok_or_else(|| "rlimit must be in format RESOURCE=LIMIT".to_string())?;
275
276 let mut parts = limit.split(':');
277 let soft = parts
278 .next()
279 .ok_or_else(|| "missing soft limit".to_string())?
280 .parse::<u64>()
281 .map_err(|err| format!("invalid soft limit: {err}"))?;
282 let hard = match parts.next() {
283 Some(value) => value
284 .parse::<u64>()
285 .map_err(|err| format!("invalid hard limit: {err}"))?,
286 None => soft,
287 };
288
289 if parts.next().is_some() {
290 return Err("too many ':' separators".into());
291 }
292
293 if soft > hard {
294 return Err("soft limit cannot exceed hard limit".into());
295 }
296
297 Ok(Self {
298 resource: resource.to_ascii_lowercase(),
299 soft,
300 hard,
301 })
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::ExecRlimit;
308
309 #[test]
310 fn test_exec_rlimit_from_str_uses_soft_for_hard_when_omitted() {
311 assert_eq!(
312 "NOFILE=65535".parse::<ExecRlimit>().unwrap(),
313 ExecRlimit {
314 resource: "nofile".to_string(),
315 soft: 65_535,
316 hard: 65_535,
317 }
318 );
319 }
320
321 #[test]
322 fn test_exec_rlimit_from_str_parses_soft_and_hard() {
323 assert_eq!(
324 "nofile=4096:65535".parse::<ExecRlimit>().unwrap(),
325 ExecRlimit {
326 resource: "nofile".to_string(),
327 soft: 4_096,
328 hard: 65_535,
329 }
330 );
331 }
332
333 #[test]
334 fn test_exec_rlimit_from_str_rejects_soft_above_hard() {
335 let err = "nofile=65535:4096".parse::<ExecRlimit>().unwrap_err();
336 assert_eq!(err, "soft limit cannot exceed hard limit");
337 }
338}
339
340//--------------------------------------------------------------------------------------------------
341// Re-Exports
342//--------------------------------------------------------------------------------------------------
343
344pub use microsandbox_types::RlimitResource;