text_mirror/runner/
helper.rs1use std::ffi::CString;
21use std::os::unix::ffi::OsStrExt;
22use std::path::PathBuf;
23
24use super::{SANDBOX_SETUP_EXIT, WORKER_BINARY};
25
26pub struct HelperConfig {
28 pub jail: PathBuf,
30 pub worker: PathBuf,
32 pub adapter: String,
34 pub address_space_bytes: u64,
36 pub cpu_seconds: u64,
38 pub file_size_bytes: u64,
40 pub max_processes: u64,
42 pub seccomp: bool,
44}
45
46pub fn parse_args(args: &[String]) -> Result<HelperConfig, String> {
48 let mut jail = None;
49 let mut worker = None;
50 let mut adapter = None;
51 let mut address_space_bytes = None;
52 let mut cpu_seconds = None;
53 let mut file_size_bytes = None;
54 let mut max_processes = None;
55 let mut seccomp = None;
56 let mut iter = args.iter();
57 while let Some(flag) = iter.next() {
58 let value = iter
59 .next()
60 .ok_or_else(|| format!("flag {flag} is missing its value"))?;
61 match flag.as_str() {
62 "--jail" => jail = Some(PathBuf::from(value)),
63 "--worker" => worker = Some(PathBuf::from(value)),
64 "--adapter" => adapter = Some(value.clone()),
65 "--rlimit-as" => address_space_bytes = Some(parse_number(flag, value)?),
66 "--rlimit-cpu" => cpu_seconds = Some(parse_number(flag, value)?),
67 "--rlimit-fsize" => file_size_bytes = Some(parse_number(flag, value)?),
68 "--rlimit-nproc" => max_processes = Some(parse_number(flag, value)?),
69 "--seccomp" => {
70 seccomp = Some(match value.as_str() {
71 "kill" => true,
72 "off" => false,
73 other => return Err(format!("unknown seccomp mode {other:?}")),
74 })
75 }
76 other => return Err(format!("unknown flag {other:?}")),
77 }
78 }
79 Ok(HelperConfig {
80 jail: jail.ok_or("missing --jail")?,
81 worker: worker.ok_or("missing --worker")?,
82 adapter: adapter.ok_or("missing --adapter")?,
83 address_space_bytes: address_space_bytes.ok_or("missing --rlimit-as")?,
84 cpu_seconds: cpu_seconds.ok_or("missing --rlimit-cpu")?,
85 file_size_bytes: file_size_bytes.ok_or("missing --rlimit-fsize")?,
86 max_processes: max_processes.ok_or("missing --rlimit-nproc")?,
87 seccomp: seccomp.ok_or("missing --seccomp")?,
88 })
89}
90
91fn parse_number(flag: &str, value: &str) -> Result<u64, String> {
92 value
93 .parse()
94 .map_err(|e| format!("flag {flag} value {value:?} is not a number: {e}"))
95}
96
97pub fn helper_main(args: &[String]) -> ! {
100 let config = match parse_args(args) {
101 Ok(config) => config,
102 Err(detail) => die(&detail),
103 };
104 if let Err(detail) = engage(&config) {
105 die(&detail);
106 }
107 let error = exec_adapter(&config);
109 die(&error)
110}
111
112fn die(detail: &str) -> ! {
113 eprintln!("sandbox_setup_failed: {detail}");
114 std::process::exit(SANDBOX_SETUP_EXIT)
115}
116
117#[cfg(target_os = "linux")]
118fn engage(config: &HelperConfig) -> Result<(), String> {
119 super::linux::unshare_namespaces()?;
120 apply_rlimits(config)?;
121 close_extra_fds()?;
122 super::linux::apply_landlock(&config.jail, &config.worker)?;
123 if config.seccomp {
124 super::linux::apply_seccomp()?;
125 }
126 Ok(())
127}
128
129#[cfg(target_os = "macos")]
130fn engage(config: &HelperConfig) -> Result<(), String> {
131 apply_rlimits(config)?;
133 close_extra_fds()?;
134 Ok(())
135}
136
137#[cfg(not(any(target_os = "linux", target_os = "macos")))]
138fn engage(_config: &HelperConfig) -> Result<(), String> {
139 Err("no jail backend exists for this platform".to_string())
140}
141
142fn apply_rlimits(config: &HelperConfig) -> Result<(), String> {
154 use rlimit::{Resource, setrlimit};
155 let apply = |resource: Resource, name: &str, value: u64| {
156 setrlimit(resource, value, value).map_err(|e| format!("setrlimit {name} failed: {e}"))
157 };
158 #[cfg(target_os = "linux")]
159 apply(Resource::AS, "RLIMIT_AS", config.address_space_bytes)?;
160 apply(Resource::CPU, "RLIMIT_CPU", config.cpu_seconds)?;
161 apply(Resource::FSIZE, "RLIMIT_FSIZE", config.file_size_bytes)?;
162 apply(Resource::NPROC, "RLIMIT_NPROC", config.max_processes)?;
163 apply(Resource::CORE, "RLIMIT_CORE", 0)?;
164 Ok(())
165}
166
167fn close_extra_fds() -> Result<(), String> {
175 #[cfg(target_os = "linux")]
176 const FD_DIR: &str = "/proc/self/fd";
177 #[cfg(not(target_os = "linux"))]
178 const FD_DIR: &str = "/dev/fd";
179 let entries =
180 std::fs::read_dir(FD_DIR).map_err(|e| format!("cannot enumerate descriptors: {e}"))?;
181 let mut fds = Vec::new();
182 for entry in entries {
183 let entry = entry.map_err(|e| format!("cannot enumerate descriptors: {e}"))?;
184 if let Some(fd) = entry
185 .file_name()
186 .to_str()
187 .and_then(|name| name.parse::<i32>().ok())
188 && fd > 2
189 {
190 fds.push(fd);
191 }
192 }
193 for fd in fds {
194 let _ = nix::unistd::close(fd);
196 }
197 Ok(())
198}
199
200fn exec_adapter(config: &HelperConfig) -> String {
203 let path = match CString::new(config.worker.as_os_str().as_bytes()) {
204 Ok(path) => path,
205 Err(_) => return "worker path contains a NUL byte".to_string(),
206 };
207 let argv0 = CString::new(WORKER_BINARY).expect("static name has no NUL");
208 let mode = match CString::new(config.adapter.as_str()) {
209 Ok(mode) => mode,
210 Err(_) => return "adapter mode contains a NUL byte".to_string(),
211 };
212 match nix::unistd::execv(&path, &[argv0.as_c_str(), mode.as_c_str()]) {
213 Ok(infallible) => match infallible {},
214 Err(e) => format!("exec of the adapter mode failed: {e}"),
215 }
216}