subc_os/privacy_identity/
command.rs1use std::{
2 ffi::{OsStr, OsString},
3 fmt, io,
4 path::{Path, PathBuf},
5 process::{Command, Stdio},
6 time::Instant,
7};
8
9#[derive(Debug)]
11pub struct ExecError {
12 code: i32,
13 detail: String,
14}
15
16impl ExecError {
17 pub fn exit_code(&self) -> i32 {
18 self.code
19 }
20
21 #[cfg(target_os = "macos")]
22 pub(super) fn new(code: i32, detail: impl fmt::Display) -> Self {
23 Self {
24 code,
25 detail: detail.to_string(),
26 }
27 }
28}
29
30impl fmt::Display for ExecError {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 write!(
33 f,
34 "{}: {}",
35 super::failure_cause(Some(self.code)).unwrap_or("privacy identity failure"),
36 self.detail
37 )
38 }
39}
40
41impl std::error::Error for ExecError {}
42
43#[derive(Debug)]
45pub enum ConfirmationError {
46 Refused(ExecError),
48 TimedOut,
50 Io(io::Error),
52}
53
54impl fmt::Display for ConfirmationError {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 match self {
57 Self::Refused(error) => error.fmt(f),
58 Self::TimedOut => f.write_str("privacy identity confirmation timed out"),
59 Self::Io(error) => write!(f, "privacy identity acknowledgement failed: {error}"),
60 }
61 }
62}
63
64impl std::error::Error for ConfirmationError {
65 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
66 match self {
67 Self::Refused(error) => Some(error),
68 Self::Io(error) => Some(error),
69 Self::TimedOut => None,
70 }
71 }
72}
73
74impl From<io::Error> for ConfirmationError {
75 fn from(error: io::Error) -> Self {
76 Self::Io(error)
77 }
78}
79
80pub struct DisclaimedCommand {
87 command: Command,
88 #[cfg(target_os = "macos")]
89 program: OsString,
90 #[cfg(target_os = "macos")]
91 args: Vec<OsString>,
92}
93
94impl DisclaimedCommand {
95 pub fn new(trampoline: impl Into<PathBuf>, program: impl Into<OsString>) -> Self {
96 let trampoline = trampoline.into();
97 let program = program.into();
98 #[cfg(target_os = "macos")]
99 {
100 Self {
101 command: Command::new(trampoline),
102 program,
103 args: Vec::new(),
104 }
105 }
106 #[cfg(not(target_os = "macos"))]
107 {
108 let _ = trampoline;
109 Self {
110 command: Command::new(program),
111 }
112 }
113 }
114
115 pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
116 #[cfg(target_os = "macos")]
117 self.args.push(arg.as_ref().to_owned());
118 #[cfg(not(target_os = "macos"))]
119 self.command.arg(arg);
120 self
121 }
122
123 pub fn args<I, S>(&mut self, args: I) -> &mut Self
124 where
125 I: IntoIterator<Item = S>,
126 S: AsRef<OsStr>,
127 {
128 for arg in args {
129 self.arg(arg);
130 }
131 self
132 }
133
134 pub fn env(&mut self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> &mut Self {
135 self.command.env(key, value);
136 self
137 }
138
139 pub fn env_clear(&mut self) -> &mut Self {
140 self.command.env_clear();
141 self
142 }
143
144 pub fn env_remove(&mut self, key: impl AsRef<OsStr>) -> &mut Self {
145 self.command.env_remove(key);
146 self
147 }
148
149 pub fn current_dir(&mut self, dir: impl AsRef<Path>) -> &mut Self {
150 self.command.current_dir(dir);
151 self
152 }
153
154 pub fn stdin(&mut self, stdio: impl Into<Stdio>) -> &mut Self {
155 self.command.stdin(stdio);
156 self
157 }
158
159 pub fn stdout(&mut self, stdio: impl Into<Stdio>) -> &mut Self {
160 self.command.stdout(stdio);
161 self
162 }
163
164 pub fn stderr(&mut self, stdio: impl Into<Stdio>) -> &mut Self {
165 self.command.stderr(stdio);
166 self
167 }
168
169 pub fn into_command(self) -> io::Result<(Command, ExecConfirmation)> {
177 #[cfg(target_os = "macos")]
178 {
179 let mut command = self.command;
180 let (reader, ack) = super::ExecAcknowledgement::pipe()?;
181 command
182 .arg("__disclaim-exec")
183 .arg(ack.fd().to_string())
184 .arg(self.program)
185 .args(self.args);
186 ack.install_owned(&mut command);
187 Ok((command, ExecConfirmation { reader }))
188 }
189 #[cfg(not(target_os = "macos"))]
190 {
191 Ok((self.command, ExecConfirmation {}))
192 }
193 }
194
195 #[cfg(feature = "tokio")]
198 pub fn into_tokio_command(self) -> io::Result<(tokio::process::Command, ExecConfirmation)> {
199 let (command, confirmation) = self.into_command()?;
200 Ok((command.into(), confirmation))
201 }
202}
203
204#[derive(Debug)]
210pub struct ExecConfirmation {
211 #[cfg(target_os = "macos")]
212 reader: std::io::PipeReader,
213}
214
215impl ExecConfirmation {
216 pub fn confirm(self, deadline: Instant) -> Result<(), ConfirmationError> {
219 #[cfg(target_os = "macos")]
220 {
221 let record = macos::read_until_eof(self.reader, deadline, 1024)?;
222 if record.is_empty() {
223 return Ok(());
224 }
225 let cause = std::str::from_utf8(&record)
226 .ok()
227 .and_then(|record| record.trim().strip_prefix(super::EXEC_REFUSAL_TAG))
228 .ok_or_else(|| macos::invalid_response("invalid privacy exec refusal record"))?;
229 for code in 120..=123 {
230 let name = super::failure_cause(Some(code)).expect("reserved status");
231 if let Some(detail) = cause.strip_prefix(name).and_then(|s| s.strip_prefix(": ")) {
232 return Err(ConfirmationError::Refused(ExecError::new(code, detail)));
233 }
234 }
235 Err(macos::invalid_response(
236 "unknown privacy exec refusal cause",
237 ))
238 }
239 #[cfg(not(target_os = "macos"))]
240 {
241 let _ = deadline;
242 Ok(())
243 }
244 }
245}
246
247pub fn probe(trampoline: impl AsRef<Path>, deadline: Instant) -> Result<(), ConfirmationError> {
254 #[cfg(target_os = "macos")]
255 {
256 macos::probe(trampoline.as_ref(), deadline)
257 }
258 #[cfg(not(target_os = "macos"))]
259 {
260 let _ = (trampoline, deadline);
261 Ok(())
262 }
263}
264
265#[cfg(all(test, target_os = "macos"))]
266mod tests {
267 use super::*;
268 use std::{io::Write, time::Duration};
269
270 #[test]
271 fn malformed_acknowledgements_are_not_exec_confirmation() {
272 for record in [
273 b"not a tagged refusal\n".as_slice(),
274 b"SUBC_PRIVACY_REFUSAL_V1 unknown cause\n".as_slice(),
275 &[b'x'; 1025],
276 ] {
277 let (reader, ack) = super::super::ExecAcknowledgement::pipe().unwrap();
278 let mut writer = std::fs::File::from(ack.into_writer());
279 writer.write_all(record).unwrap();
280 drop(writer);
281 let result =
282 ExecConfirmation { reader }.confirm(Instant::now() + Duration::from_secs(5));
283 assert!(
284 matches!(result, Err(ConfirmationError::Io(_))),
285 "{result:?}"
286 );
287 }
288 }
289
290 #[test]
291 fn partial_refusal_without_eof_still_obeys_deadline() {
292 let (reader, ack) = super::super::ExecAcknowledgement::pipe().unwrap();
293 let mut writer = std::fs::File::from(ack.into_writer());
294 writer.write_all(b"SUBC_PRIVACY_REFUSAL_V1 ").unwrap();
295 let result =
296 ExecConfirmation { reader }.confirm(Instant::now() + Duration::from_millis(10));
297 assert!(
298 matches!(result, Err(ConfirmationError::TimedOut)),
299 "{result:?}"
300 );
301 drop(writer);
302 }
303}
304
305#[cfg(target_os = "macos")]
306mod macos {
307 use super::*;
308 use std::{io::Read, os::fd::AsRawFd, time::Duration};
309
310 pub(super) fn invalid_response(detail: &str) -> ConfirmationError {
311 io::Error::new(io::ErrorKind::InvalidData, detail).into()
312 }
313
314 pub(super) fn read_until_eof(
317 mut reader: io::PipeReader,
318 deadline: Instant,
319 limit: usize,
320 ) -> Result<Vec<u8>, ConfirmationError> {
321 let mut record = Vec::new();
322 loop {
323 let remaining = deadline
324 .checked_duration_since(Instant::now())
325 .filter(|remaining| !remaining.is_zero())
326 .ok_or(ConfirmationError::TimedOut)?;
327 let millis =
329 remaining.as_millis() + u128::from(remaining.subsec_nanos() % 1_000_000 != 0);
330 let mut pollfd = libc::pollfd {
331 fd: reader.as_raw_fd(),
332 events: libc::POLLIN,
333 revents: 0,
334 };
335 #[allow(unsafe_code)]
338 let ready = unsafe { libc::poll(&mut pollfd, 1, millis.min(i32::MAX as u128) as i32) };
339 if ready < 0 {
340 let error = io::Error::last_os_error();
341 if error.kind() == io::ErrorKind::Interrupted {
342 continue;
343 }
344 return Err(error.into());
345 }
346 if ready == 0 {
347 continue; }
349 let mut buffer = [0u8; 256];
350 match reader.read(&mut buffer) {
351 Ok(0) => return Ok(record),
352 Ok(count) if record.len() + count <= limit => {
353 record.extend_from_slice(&buffer[..count]);
354 }
355 Ok(_) => return Err(invalid_response("privacy acknowledgement is too long")),
356 Err(error)
357 if matches!(
358 error.kind(),
359 io::ErrorKind::Interrupted | io::ErrorKind::WouldBlock
360 ) => {}
361 Err(error) => return Err(error.into()),
362 }
363 }
364 }
365
366 pub(super) fn probe(trampoline: &Path, deadline: Instant) -> Result<(), ConfirmationError> {
367 if Instant::now() >= deadline {
368 return Err(ConfirmationError::TimedOut);
369 }
370 let (reader, writer) = super::super::ExecAcknowledgement::pipe()?;
373 use std::os::fd::OwnedFd;
374 let stdout: OwnedFd = writer.into_writer();
375 let mut command = Command::new(trampoline);
376 command
377 .args(["__disclaim-exec", "--probe"])
378 .stdin(Stdio::null())
379 .stdout(Stdio::from(stdout))
380 .stderr(Stdio::null());
381 let mut child = command.spawn()?;
382 drop(command);
383 let result = (|| {
384 let answer = read_until_eof(reader, deadline, 256)?;
385 let status = loop {
386 if let Some(status) = child.try_wait()? {
387 break status;
388 }
389 let remaining = deadline
390 .checked_duration_since(Instant::now())
391 .filter(|remaining| !remaining.is_zero())
392 .ok_or(ConfirmationError::TimedOut)?;
393 std::thread::sleep(remaining.min(Duration::from_millis(5)));
394 };
395 if !status.success() {
396 if let Some(code) = status
397 .code()
398 .filter(|code| super::super::failure_cause(Some(*code)).is_some())
399 {
400 return Err(ConfirmationError::Refused(ExecError::new(
401 code,
402 "trampoline probe refused",
403 )));
404 }
405 return Err(invalid_response(
406 "privacy trampoline probe exited unsuccessfully",
407 ));
408 }
409 if std::str::from_utf8(&answer)
410 .is_ok_and(|s| s.trim() == super::super::TRAMPOLINE_PROBE)
411 {
412 Ok(())
413 } else {
414 Err(invalid_response(
415 "binary does not implement the privacy trampoline protocol",
416 ))
417 }
418 })();
419 if result.is_err() {
420 let _ = child.kill();
421 let _ = child.wait();
422 }
423 result
424 }
425}