mermaid_cli/app/
sandbox_exec.rs1use std::ffi::OsString;
20use std::path::PathBuf;
21
22pub const SANDBOX_EXEC_SUBCOMMAND: &str = "__sandbox-exec";
24
25pub fn maybe_dispatch<I: IntoIterator<Item = OsString>>(args: I) -> Option<i32> {
34 let mut it = args.into_iter();
35 let _exe = it.next();
36 if it.next()? != SANDBOX_EXEC_SUBCOMMAND {
37 return None;
38 }
39
40 let mut no_network = false;
41 let mut confine_writes: Vec<PathBuf> = Vec::new();
42 let mut argv: Vec<OsString> = Vec::new();
43 let mut in_argv = false;
44 while let Some(arg) = it.next() {
45 if in_argv {
46 argv.push(arg);
47 } else if arg == "--" {
48 in_argv = true;
49 } else if arg == "--no-network" {
50 no_network = true;
51 } else if arg == "--confine-writes" {
52 match it.next() {
53 Some(dir) => confine_writes.push(PathBuf::from(dir)),
54 None => {
55 eprintln!(
56 "mermaid {SANDBOX_EXEC_SUBCOMMAND}: --confine-writes needs a directory"
57 );
58 return Some(2);
59 },
60 }
61 } else {
62 eprintln!("mermaid {SANDBOX_EXEC_SUBCOMMAND}: unexpected argument {arg:?}");
63 return Some(2);
64 }
65 }
66
67 if argv.is_empty() {
68 eprintln!("mermaid {SANDBOX_EXEC_SUBCOMMAND}: missing command after `--`");
69 return Some(2);
70 }
71
72 let policy = crate::runtime::SandboxPolicy {
77 deny_network: no_network,
78 allowed_writes: confine_writes,
79 };
80 match crate::runtime::enforce(&policy, &argv) {
81 Ok(crate::runtime::Enforcement::SelfApplied { fs_enforced }) => {
82 if !fs_enforced {
85 eprintln!(
86 "mermaid {SANDBOX_EXEC_SUBCOMMAND}: filesystem confinement not enforced (kernel without Landlock); continuing"
87 );
88 }
89 Some(exec_wrapped(&argv))
90 },
91 Ok(crate::runtime::Enforcement::ExecArgv(wrapped)) => Some(exec_wrapped(&wrapped)),
92 Ok(crate::runtime::Enforcement::Ran(code)) => Some(code),
93 Err(err) => {
94 eprintln!("mermaid {SANDBOX_EXEC_SUBCOMMAND}: sandbox unavailable: {err:#}");
95 Some(126)
96 },
97 }
98}
99
100fn exec_wrapped(argv: &[OsString]) -> i32 {
103 let program = &argv[0];
104 let rest = &argv[1..];
105
106 #[cfg(unix)]
107 {
108 use std::os::unix::process::CommandExt;
109 let err = std::process::Command::new(program).args(rest).exec();
111 eprintln!("mermaid {SANDBOX_EXEC_SUBCOMMAND}: failed to exec {program:?}: {err}");
112 127
113 }
114 #[cfg(not(unix))]
115 {
116 match std::process::Command::new(program).args(rest).status() {
117 Ok(status) => status.code().unwrap_or(1),
118 Err(err) => {
119 eprintln!("mermaid {SANDBOX_EXEC_SUBCOMMAND}: failed to spawn {program:?}: {err}");
120 127
121 },
122 }
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 fn os(items: &[&str]) -> Vec<OsString> {
131 items.iter().map(OsString::from).collect()
132 }
133
134 #[test]
135 fn non_sandbox_invocation_is_ignored() {
136 assert_eq!(maybe_dispatch(os(&["mermaid", "run", "hi"])), None);
137 assert_eq!(maybe_dispatch(os(&["mermaid"])), None);
138 }
139
140 #[test]
141 fn missing_command_after_separator_errors() {
142 assert_eq!(
143 maybe_dispatch(os(&["mermaid", "__sandbox-exec", "--no-network", "--"])),
144 Some(2)
145 );
146 }
147
148 #[test]
149 fn unknown_flag_before_separator_errors() {
150 assert_eq!(
151 maybe_dispatch(os(&["mermaid", "__sandbox-exec", "--bogus", "--", "true"])),
152 Some(2)
153 );
154 }
155
156 #[test]
157 fn confine_writes_without_a_directory_errors() {
158 assert_eq!(
159 maybe_dispatch(os(&["mermaid", "__sandbox-exec", "--confine-writes"])),
160 Some(2)
161 );
162 assert_eq!(
165 maybe_dispatch(os(&["mermaid", "__sandbox-exec", "--confine-writes", "--"])),
166 Some(2)
167 );
168 }
169}