1use std::collections::BTreeMap;
23use std::io::{self, Read, Write};
24use std::net::Shutdown;
25#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
26use std::os::unix::ffi::OsStrExt;
27#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
28use std::os::unix::fs::PermissionsExt;
29use std::os::unix::net::UnixStream;
30#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
31use std::os::unix::process::CommandExt;
32use std::path::Path;
33use std::path::PathBuf;
34use std::process::ExitStatus;
35#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
36use std::process::{ChildStdin, Command, Stdio};
37use std::sync::atomic::AtomicBool;
38use std::sync::mpsc::{channel, Receiver, RecvTimeoutError, Sender, TryRecvError};
39use std::sync::{Arc, Mutex};
40use std::thread::{self, JoinHandle};
41use std::time::{Duration, Instant};
42
43#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
44use serde::Deserialize;
45use serde::Serialize;
46
47const FRAME_REQUEST: u8 = 0xff;
49const FRAME_CONTROL: u8 = 0xfe;
50const FRAME_STDIN: u8 = 0;
51const FRAME_STDOUT: u8 = 1;
52const FRAME_STDERR: u8 = 2;
53const FRAME_RESIZE: u8 = 3;
54const FRAME_SIGNAL: u8 = 4;
55const FRAME_EXIT: u8 = 5;
56const FRAME_ERROR: u8 = 6;
57
58#[derive(Clone)]
74pub struct ExecBuilder {
75 exec_path: PathBuf,
76 argv: Vec<String>,
77 env: BTreeMap<String, String>,
78 cwd: Option<String>,
79 tty: bool,
80 cols: Option<u16>,
81 rows: Option<u16>,
82 timeout: Option<Duration>,
83 stage_files: Vec<StageFile>,
86 chain: Vec<Vec<String>>,
90 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
97 warm_pool: Option<std::sync::Arc<crate::sentry::Pool>>,
98 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
102 runsc: Option<RunscExecTarget>,
103 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
106 kboxlike: Option<KboxlikeExecTarget>,
107}
108
109#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
110#[derive(Clone, Debug, Serialize, Deserialize)]
111#[doc(hidden)]
112pub struct RunscExecTarget {
113 pub runsc_bin: PathBuf,
114 pub runsc_root: PathBuf,
115 #[serde(default)]
116 pub network: Option<String>,
117 pub container_id: String,
118 pub image_env: Vec<(String, String)>,
119 #[serde(default)]
120 pub agent_path: Option<String>,
121 pub file_agent_rootfs: Option<PathBuf>,
122}
123
124#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
125#[derive(Clone, Debug, Serialize, Deserialize)]
126#[doc(hidden)]
127pub struct KboxlikeExecTarget {
128 pub rootfs: PathBuf,
129 pub image_env: Vec<(String, String)>,
130 #[serde(default)]
131 pub user: Option<(u32, u32)>,
132 #[serde(default)]
133 pub mounts: Vec<(PathBuf, String)>,
134}
135
136#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
137pub(crate) const RUNSC_EXEC_TOKEN_PREFIX: &str = "supermachine-runsc-exec-v1:";
138#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
139pub(crate) const KBOXLIKE_EXEC_TOKEN_PREFIX: &str = "supermachine-kboxlike-exec-v1:";
140#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
141pub(crate) const RUNSC_NETWORK_NONE: &str = "none";
142#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
143pub(crate) const RUNSC_NETWORK_SANDBOX: &str = "sandbox";
144
145#[derive(Clone)]
146struct StageFile {
147 path: String,
148 data: Vec<u8>,
149 mode: Option<u32>,
150}
151
152impl ExecBuilder {
153 pub fn new(exec_path: PathBuf) -> Self {
159 Self {
160 exec_path,
161 argv: Vec::new(),
162 env: BTreeMap::new(),
163 cwd: None,
164 tty: false,
165 cols: None,
166 rows: None,
167 timeout: None,
168 stage_files: Vec::new(),
169 chain: Vec::new(),
170 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
171 warm_pool: None,
172 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
173 runsc: None,
174 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
175 kboxlike: None,
176 }
177 }
178
179 #[doc(hidden)]
184 pub fn from_binding_token(token: impl AsRef<str>) -> io::Result<Self> {
185 let token = token.as_ref();
186 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
187 if let Some(raw) = token.strip_prefix(RUNSC_EXEC_TOKEN_PREFIX) {
188 let bytes = crate::api::b64_decode(raw)
189 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
190 let target: RunscExecTarget = serde_json::from_slice(&bytes)
191 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
192 return Ok(Self::new(PathBuf::from("runsc-exec-token")).with_runsc_exec(target));
193 }
194 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
195 if let Some(raw) = token.strip_prefix(KBOXLIKE_EXEC_TOKEN_PREFIX) {
196 let bytes = crate::api::b64_decode(raw)
197 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
198 let target: KboxlikeExecTarget = serde_json::from_slice(&bytes)
199 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
200 return Ok(Self::new(PathBuf::from("kboxlike-exec-token")).with_kboxlike_exec(target));
201 }
202 Ok(Self::new(PathBuf::from(token)))
203 }
204
205 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
209 pub(crate) fn with_warm_pool(mut self, pool: std::sync::Arc<crate::sentry::Pool>) -> Self {
210 self.warm_pool = Some(pool);
211 self
212 }
213
214 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
217 #[doc(hidden)]
218 pub fn with_runsc_exec(mut self, target: RunscExecTarget) -> Self {
219 self.runsc = Some(target);
220 self
221 }
222
223 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
226 #[doc(hidden)]
227 pub fn with_kboxlike_exec(mut self, target: KboxlikeExecTarget) -> Self {
228 self.kboxlike = Some(target);
229 self
230 }
231
232 pub fn argv<I, S>(mut self, argv: I) -> Self
235 where
236 I: IntoIterator<Item = S>,
237 S: Into<String>,
238 {
239 self.argv = argv.into_iter().map(Into::into).collect();
240 self
241 }
242
243 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
248 self.env.insert(key.into(), value.into());
249 self
250 }
251
252 pub fn cwd(mut self, path: impl Into<String>) -> Self {
254 self.cwd = Some(path.into());
255 self
256 }
257
258 pub fn tty(mut self, on: bool) -> Self {
262 self.tty = on;
263 self
264 }
265
266 pub fn winsize(mut self, cols: u16, rows: u16) -> Self {
269 self.cols = Some(cols);
270 self.rows = Some(rows);
271 self
272 }
273
274 pub fn timeout(mut self, d: Duration) -> Self {
293 self.timeout = Some(d);
294 self
295 }
296
297 pub fn stage_file(mut self, path: impl Into<String>, bytes: impl Into<Vec<u8>>) -> Self {
307 self.stage_files.push(StageFile {
308 path: path.into(),
309 data: bytes.into(),
310 mode: None,
311 });
312 self
313 }
314
315 pub fn stage_file_mode(
317 mut self,
318 path: impl Into<String>,
319 bytes: impl Into<Vec<u8>>,
320 mode: u32,
321 ) -> Self {
322 self.stage_files.push(StageFile {
323 path: path.into(),
324 data: bytes.into(),
325 mode: Some(mode),
326 });
327 self
328 }
329
330 pub fn chain<I, S>(mut self, argv: I) -> Self
335 where
336 I: IntoIterator<Item = S>,
337 S: Into<String>,
338 {
339 self.chain.push(argv.into_iter().map(Into::into).collect());
340 self
341 }
342
343 pub fn spawn(self) -> io::Result<ExecChild> {
348 if self.argv.is_empty() {
349 return Err(io::Error::new(
350 io::ErrorKind::InvalidInput,
351 "exec: argv is empty",
352 ));
353 }
354 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
355 if self.runsc.is_some() {
356 return runsc_spawn(self);
357 }
358 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
359 if self.kboxlike.is_some() {
360 return kboxlike_spawn(self);
361 }
362 spawn(self)
363 }
364
365 pub fn output(self) -> io::Result<ExecOutcome> {
391 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
397 if let Some(pool) = &self.warm_pool {
398 let started = std::time::Instant::now();
399 let stage_payload: Vec<StageFilePayload> = self
405 .stage_files
406 .iter()
407 .map(|s| StageFilePayload {
408 path: &s.path,
409 data_b64: crate::api::b64_encode(&s.data),
410 mode: s.mode,
411 })
412 .collect();
413 let payload = RequestPayload {
414 argv: &self.argv,
415 env: &self.env,
416 cwd: self.cwd.as_deref(),
417 tty: false,
418 cols: None,
419 rows: None,
420 stage_files: stage_payload,
421 chain: &self.chain,
422 };
423 let body = serde_json::to_vec(&payload)
424 .map_err(|e| io::Error::other(format!("sentry exec request encode: {e}")))?;
425 let (code, out) = pool
426 .exec_capture_full(&body)
427 .map_err(|e| io::Error::other(format!("sentry warm-daemon exec: {e}")))?;
428 return Ok(ExecOutcome {
429 status: synthesize_exit(code as u32),
430 stdout: out,
431 stderr: Vec::new(),
432 duration: started.elapsed(),
433 peak_rss_kib: None,
434 timed_out: false,
435 });
436 }
437 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
438 if self.runsc.is_some() {
439 return runsc_output(self);
440 }
441 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
442 if self.kboxlike.is_some() {
443 return kboxlike_output(self);
444 }
445 let (never_tx, never_rx) = channel::<()>();
451 let result = self.output_with_cancel(never_rx);
455 drop(never_tx);
456 result
457 }
458
459 pub fn output_resilient(self, retries: u8) -> io::Result<ExecOutcome> {
474 if retries == 0 {
475 return self.output();
476 }
477 let mut attempt: u8 = 0;
478 loop {
479 let result = self.clone().output();
482 let setup_failure = result
483 .as_ref()
484 .err()
485 .and_then(|e| e.get_ref())
486 .and_then(|e| e.downcast_ref::<ExecEof>())
487 .map(ExecEof::is_setup_failure)
488 .unwrap_or(false);
489 if setup_failure && attempt < retries {
490 attempt += 1;
491 eprintln!(
492 "[exec] connection-setup EOF (agent not ready / worker gone) — retry \
493 {attempt}/{retries}"
494 );
495 thread::sleep(Duration::from_millis(50 * attempt as u64));
496 continue;
497 }
498 return result;
499 }
500 }
501
502 pub fn output_with_cancel(self, cancel: Receiver<()>) -> io::Result<ExecOutcome> {
528 let timeout = self.timeout;
529 let t0 = Instant::now();
530 let mut child = self.spawn()?;
531 drop(child.stdin());
538 let mut stdout = child.stdout();
539 let mut stderr = child.stderr();
540 let stdout_handle = stdout.take().map(|mut s| {
543 thread::spawn(move || {
544 let mut buf = Vec::new();
545 let _ = s.read_to_end(&mut buf);
546 buf
547 })
548 });
549 let stderr_handle = stderr.take().map(|mut s| {
550 thread::spawn(move || {
551 let mut buf = Vec::new();
552 let _ = s.read_to_end(&mut buf);
553 buf
554 })
555 });
556 let (stop_tx, stop_rx) = channel::<()>();
564 let timed_out = Arc::new(std::sync::atomic::AtomicBool::new(false));
565 let signaler = child.signaler();
566 let watchdog = {
567 let timed_out = Arc::clone(&timed_out);
568 Some(thread::spawn(move || {
569 const TICK: Duration = Duration::from_millis(10);
585 const ABORT_GRACE: Duration = Duration::from_secs(2);
586 let deadline = timeout.map(|d| Instant::now() + d);
587
588 let escalate = |stop_rx: &Receiver<()>| {
592 let _ = signaler.kill();
593 match stop_rx.recv_timeout(ABORT_GRACE) {
594 Ok(()) | Err(RecvTimeoutError::Disconnected) => {
595 }
598 Err(RecvTimeoutError::Timeout) => {
599 let _ = signaler.abort();
604 }
605 }
606 };
607
608 loop {
609 match stop_rx.try_recv() {
611 Ok(()) | Err(TryRecvError::Disconnected) => return,
612 Err(TryRecvError::Empty) => {}
613 }
614 match cancel.try_recv() {
616 Ok(()) => {
617 escalate(&stop_rx);
618 return;
619 }
620 Err(TryRecvError::Disconnected) => {
621 }
625 Err(TryRecvError::Empty) => {}
626 }
627 if let Some(dl) = deadline {
629 if Instant::now() >= dl {
630 timed_out.store(true, std::sync::atomic::Ordering::SeqCst);
631 if crate::trace::enabled("timeout") {
632 eprintln!("[exec.timeout] fired at +{:?}", timeout.unwrap());
633 }
634 escalate(&stop_rx);
635 return;
636 }
637 }
638 let sleep_for = deadline
642 .map(|dl| dl.saturating_duration_since(Instant::now()).min(TICK))
643 .unwrap_or(TICK);
644 if sleep_for.is_zero() {
645 continue;
646 }
647 match stop_rx.recv_timeout(sleep_for) {
650 Ok(()) => return,
651 Err(RecvTimeoutError::Timeout) => continue,
652 Err(RecvTimeoutError::Disconnected) => return,
653 }
654 }
655 }))
656 };
657 let exit = child.wait_with_rss();
658 let _ = stop_tx.send(());
660 if let Some(h) = watchdog {
661 let _ = h.join();
662 }
663 let stdout_bytes = stdout_handle
664 .map(|h| h.join().unwrap_or_default())
665 .unwrap_or_default();
666 let stderr_bytes = stderr_handle
667 .map(|h| h.join().unwrap_or_default())
668 .unwrap_or_default();
669 let (status, peak_rss_kib) = exit?;
670 Ok(ExecOutcome {
671 status,
672 stdout: stdout_bytes,
673 stderr: stderr_bytes,
674 duration: t0.elapsed(),
675 peak_rss_kib,
676 timed_out: timed_out.load(std::sync::atomic::Ordering::SeqCst),
677 })
678 }
679}
680
681#[derive(Debug)]
689pub struct ExecOutcome {
690 pub status: ExitStatus,
692 pub stdout: Vec<u8>,
694 pub stderr: Vec<u8>,
696 pub duration: Duration,
699 pub peak_rss_kib: Option<u64>,
702 pub timed_out: bool,
706}
707
708impl ExecOutcome {
709 pub fn success(&self) -> bool {
712 self.status.success() && !self.timed_out
713 }
714}
715
716#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
717fn runsc_output(builder: ExecBuilder) -> io::Result<ExecOutcome> {
718 let target = builder.runsc.clone().ok_or_else(|| {
719 io::Error::new(
720 io::ErrorKind::InvalidInput,
721 "runsc_output called without a runsc target",
722 )
723 })?;
724 if builder.argv.is_empty() {
725 return Err(io::Error::new(
726 io::ErrorKind::InvalidInput,
727 "exec: argv is empty",
728 ));
729 }
730 if builder.tty {
731 if target.agent_path.is_some() {
732 return runsc_agent_output(builder, target);
733 }
734 return Err(io::Error::new(
735 io::ErrorKind::Unsupported,
736 "runsc backend does not support TTY exec without a bundled agent",
737 ));
738 }
739 if builder.tty || builder.cols.is_some() || builder.rows.is_some() {
740 return Err(io::Error::new(
741 io::ErrorKind::Unsupported,
742 "runsc backend does not support winsize without TTY",
743 ));
744 }
745 runsc_stage_files(&target, &builder.stage_files)?;
746
747 let started = Instant::now();
748 let mut stdout = Vec::new();
749 let mut stderr = Vec::new();
750 let mut status = synthesize_exit(0);
751 let mut timed_out = false;
752 let mut argvs = Vec::with_capacity(1 + builder.chain.len());
753 argvs.push(builder.argv.clone());
754 argvs.extend(builder.chain.iter().cloned());
755
756 for argv in argvs {
757 let out = if target.file_agent_rootfs.is_some() {
758 runsc_file_agent_output_one(&target, &builder, &argv, started)?
759 } else {
760 runsc_output_one(&target, &builder, &argv, started)?
761 };
762 timed_out |= out.timed_out;
763 stdout.extend(out.stdout);
764 stderr.extend(out.stderr);
765 status = out.status;
766 if timed_out || !status.success() {
767 break;
768 }
769 }
770
771 Ok(ExecOutcome {
772 status,
773 stdout,
774 stderr,
775 duration: started.elapsed(),
776 peak_rss_kib: None,
777 timed_out,
778 })
779}
780
781#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
782fn runsc_agent_output(builder: ExecBuilder, target: RunscExecTarget) -> io::Result<ExecOutcome> {
783 let started = Instant::now();
784 runsc_stage_files(&target, &builder.stage_files)?;
785 let agent_path = target.agent_path.as_deref().ok_or_else(|| {
786 io::Error::new(
787 io::ErrorKind::InvalidInput,
788 "runsc agent exec target missing agent path",
789 )
790 })?;
791 let mut cmd = Command::new(&target.runsc_bin);
792 add_runsc_global_flags_with_network(&mut cmd, &target.runsc_root, runsc_network(&target));
793 cmd.arg("exec")
794 .arg(&target.container_id)
795 .arg(agent_path)
796 .arg("--exec-stdio")
797 .stdin(Stdio::piped())
798 .stdout(Stdio::piped())
799 .stderr(Stdio::piped());
800 let mut child = cmd.spawn().map_err(|e| {
801 io::Error::new(
802 e.kind(),
803 format!(
804 "runsc exec {} {} --exec-stdio: {e}",
805 target.container_id, agent_path
806 ),
807 )
808 })?;
809 let stage_payload: Vec<StageFilePayload> = builder
810 .stage_files
811 .iter()
812 .map(|s| StageFilePayload {
813 path: &s.path,
814 data_b64: crate::api::b64_encode(&s.data),
815 mode: s.mode,
816 })
817 .collect();
818 let payload = RequestPayload {
819 argv: &builder.argv,
820 env: &builder.env,
821 cwd: builder.cwd.as_deref(),
822 tty: builder.tty,
823 cols: builder.cols,
824 rows: builder.rows,
825 stage_files: stage_payload,
826 chain: &builder.chain,
827 };
828 let json = serde_json::to_vec(&payload).map_err(|e| {
829 io::Error::new(
830 io::ErrorKind::InvalidInput,
831 format!("runsc agent exec: encode REQUEST: {e}"),
832 )
833 })?;
834 {
835 let stdin = child.stdin.as_mut().ok_or_else(|| {
836 io::Error::new(io::ErrorKind::Other, "runsc agent exec stdin pipe missing")
837 })?;
838 write_raw_frame(stdin, FRAME_REQUEST, &json)?;
839 write_raw_frame(stdin, FRAME_STDIN, &[])?;
840 }
841
842 let mut child_stdout = child.stdout.take().ok_or_else(|| {
843 io::Error::new(io::ErrorKind::Other, "runsc agent exec stdout pipe missing")
844 })?;
845 let mut child_stderr = child.stderr.take().ok_or_else(|| {
846 io::Error::new(io::ErrorKind::Other, "runsc agent exec stderr pipe missing")
847 })?;
848 let stderr_handle = thread::Builder::new()
849 .name("supermachine-runsc-agent-output-stderr".into())
850 .spawn(move || {
851 let mut buf = Vec::new();
852 let _ = child_stderr.read_to_end(&mut buf);
853 buf
854 })
855 .map_err(|e| {
856 io::Error::new(
857 io::ErrorKind::Other,
858 format!("runsc agent output stderr thread: {e}"),
859 )
860 })?;
861
862 let mut stdout = Vec::new();
863 let mut stderr = Vec::new();
864 let mut status = synthesize_exit(0);
865 let done = Arc::new(AtomicBool::new(false));
866 let timed_out = Arc::new(AtomicBool::new(false));
867 let watchdog = builder.timeout.map(|timeout| {
868 let done = Arc::clone(&done);
869 let timed_out = Arc::clone(&timed_out);
870 let child_pid = child.id() as i32;
871 thread::spawn(move || {
872 let elapsed = started.elapsed();
873 if elapsed < timeout {
874 thread::sleep(timeout - elapsed);
875 }
876 if !done.load(std::sync::atomic::Ordering::SeqCst) {
877 timed_out.store(true, std::sync::atomic::Ordering::SeqCst);
878 let _ = unsafe { libc::kill(child_pid, libc::SIGKILL) };
879 }
880 })
881 });
882
883 loop {
884 let (kind, payload) = match read_frame(&mut child_stdout) {
885 Ok(frame) => frame,
886 Err(e) => {
887 if timed_out.load(std::sync::atomic::Ordering::SeqCst) {
888 break;
889 }
890 let _ = child.kill();
891 let _ = child.wait();
892 done.store(true, std::sync::atomic::Ordering::SeqCst);
893 if let Some(h) = watchdog {
894 let _ = h.join();
895 }
896 let stderr = stderr_handle.join().unwrap_or_default();
897 if !stderr.is_empty() {
898 return Err(io::Error::new(
899 e.kind(),
900 format!(
901 "runsc agent exec frame read failed: {e}; runsc stderr={}",
902 String::from_utf8_lossy(&stderr)
903 ),
904 ));
905 }
906 return Err(e);
907 }
908 };
909 match kind {
910 FRAME_STDOUT => stdout.extend_from_slice(&payload),
911 FRAME_STDERR => stderr.extend_from_slice(&payload),
912 FRAME_EXIT => {
913 match decode_exit_payload(&payload) {
914 DemuxExit::Status { code, .. } => status = synthesize_exit(code),
915 DemuxExit::Error(msg) => {
916 let _ = child.kill();
917 let _ = child.wait();
918 done.store(true, std::sync::atomic::Ordering::SeqCst);
919 if let Some(h) = watchdog {
920 let _ = h.join();
921 }
922 let mut runsc_stderr = stderr_handle.join().unwrap_or_default();
923 stderr.append(&mut runsc_stderr);
924 return Err(io::Error::new(io::ErrorKind::InvalidData, msg));
925 }
926 _ => {}
927 }
928 break;
929 }
930 FRAME_ERROR => {
931 let msg = String::from_utf8_lossy(&payload).into_owned();
932 let _ = child.kill();
933 let _ = child.wait();
934 done.store(true, std::sync::atomic::Ordering::SeqCst);
935 if let Some(h) = watchdog {
936 let _ = h.join();
937 }
938 let mut runsc_stderr = stderr_handle.join().unwrap_or_default();
939 stderr.append(&mut runsc_stderr);
940 return Err(io::Error::new(io::ErrorKind::Other, msg));
941 }
942 _ => {}
943 }
944 }
945 drop(child.stdin.take());
946 let host_status = child.wait()?;
947 done.store(true, std::sync::atomic::Ordering::SeqCst);
948 if let Some(h) = watchdog {
949 let _ = h.join();
950 }
951 let mut runsc_stderr = stderr_handle.join().unwrap_or_default();
952 stderr.append(&mut runsc_stderr);
953 if timed_out.load(std::sync::atomic::Ordering::SeqCst) {
954 status = synthesize_exit(runsc_status_code(host_status));
955 }
956 Ok(ExecOutcome {
957 status,
958 stdout,
959 stderr,
960 duration: started.elapsed(),
961 peak_rss_kib: None,
962 timed_out: timed_out.load(std::sync::atomic::Ordering::SeqCst),
963 })
964}
965
966#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
967fn runsc_file_agent_output_one(
968 target: &RunscExecTarget,
969 builder: &ExecBuilder,
970 argv: &[String],
971 started: Instant,
972) -> io::Result<ExecOutcome> {
973 let rootfs = target.file_agent_rootfs.as_ref().ok_or_else(|| {
974 io::Error::new(
975 io::ErrorKind::InvalidInput,
976 "runsc file-agent exec target missing rootfs",
977 )
978 })?;
979 let req_dir = rootfs
980 .join(".supermachine")
981 .join("runsc-exec")
982 .join("requests")
983 .join(format!(
984 "req-{}-{}",
985 std::process::id(),
986 unique_suffix_nanos()
987 ));
988 std::fs::create_dir_all(&req_dir)?;
989
990 let mut script = String::from("set -e\n");
991 if let Some(cwd) = builder.cwd.as_deref() {
992 script.push_str("cd ");
993 script.push_str(&sh_quote(cwd));
994 script.push('\n');
995 }
996 let mut env = BTreeMap::new();
997 for (k, v) in &target.image_env {
998 env.insert(k.clone(), v.clone());
999 }
1000 for (k, v) in &builder.env {
1001 env.insert(k.clone(), v.clone());
1002 }
1003 for (k, v) in env {
1004 script.push_str("export ");
1005 script.push_str(&k);
1006 script.push('=');
1007 script.push_str(&sh_quote(&v));
1008 script.push('\n');
1009 }
1010 script.push_str("exec");
1011 for arg in argv {
1012 script.push(' ');
1013 script.push_str(&sh_quote(arg));
1014 }
1015 script.push('\n');
1016
1017 std::fs::write(req_dir.join("script"), script)?;
1018 std::fs::write(req_dir.join("ready"), b"1\n")?;
1019
1020 let mut timed_out = false;
1021 loop {
1022 if req_dir.join("done").is_file() {
1023 break;
1024 }
1025 if let Some(timeout) = builder.timeout {
1026 if started.elapsed() >= timeout {
1027 timed_out = true;
1028 break;
1029 }
1030 }
1031 thread::sleep(Duration::from_millis(20));
1032 }
1033
1034 let stdout = std::fs::read(req_dir.join("stdout")).unwrap_or_default();
1035 let stderr = std::fs::read(req_dir.join("stderr")).unwrap_or_default();
1036 let code = std::fs::read_to_string(req_dir.join("status"))
1037 .ok()
1038 .and_then(|s| s.trim().parse::<u32>().ok())
1039 .unwrap_or(if timed_out { 124 } else { 1 });
1040 let _ = std::fs::remove_dir_all(&req_dir);
1041 Ok(ExecOutcome {
1042 status: synthesize_exit(code),
1043 stdout,
1044 stderr,
1045 duration: started.elapsed(),
1046 peak_rss_kib: None,
1047 timed_out,
1048 })
1049}
1050
1051#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1052fn kboxlike_output(builder: ExecBuilder) -> io::Result<ExecOutcome> {
1053 let target = builder.kboxlike.clone().ok_or_else(|| {
1054 io::Error::new(
1055 io::ErrorKind::InvalidInput,
1056 "kboxlike_output called without a kboxlike target",
1057 )
1058 })?;
1059 if builder.argv.is_empty() {
1060 return Err(io::Error::new(
1061 io::ErrorKind::InvalidInput,
1062 "exec: argv is empty",
1063 ));
1064 }
1065 if builder.tty || builder.cols.is_some() || builder.rows.is_some() {
1066 return Err(io::Error::new(
1067 io::ErrorKind::Unsupported,
1068 "kboxlike backend does not support TTY exec yet",
1069 ));
1070 }
1071 kboxlike_stage_files(&target, &builder.stage_files)?;
1072
1073 let started = Instant::now();
1074 let mut stdout = Vec::new();
1075 let mut stderr = Vec::new();
1076 let mut status = synthesize_exit(0);
1077 let mut timed_out = false;
1078 let mut argvs = Vec::with_capacity(1 + builder.chain.len());
1079 argvs.push(builder.argv.clone());
1080 argvs.extend(builder.chain.iter().cloned());
1081
1082 for argv in argvs {
1083 let out = kboxlike_output_one(&target, &builder, &argv, started)?;
1084 timed_out |= out.timed_out;
1085 stdout.extend(out.stdout);
1086 stderr.extend(out.stderr);
1087 status = out.status;
1088 if timed_out || !status.success() {
1089 break;
1090 }
1091 }
1092
1093 Ok(ExecOutcome {
1094 status,
1095 stdout,
1096 stderr,
1097 duration: started.elapsed(),
1098 peak_rss_kib: None,
1099 timed_out,
1100 })
1101}
1102
1103#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1104fn kboxlike_output_one(
1105 target: &KboxlikeExecTarget,
1106 builder: &ExecBuilder,
1107 argv: &[String],
1108 started: Instant,
1109) -> io::Result<ExecOutcome> {
1110 let mut cmd = kboxlike_exec_command(target, builder, argv)?;
1111 cmd.stdin(Stdio::null());
1112 cmd.stdout(Stdio::piped());
1113 cmd.stderr(Stdio::piped());
1114 let mut child = cmd
1115 .spawn()
1116 .map_err(|e| io::Error::new(e.kind(), format!("kboxlike exec {:?}: {e}", argv)))?;
1117 let mut timed_out = false;
1118 loop {
1119 if child.try_wait()?.is_some() {
1120 break;
1121 }
1122 if let Some(timeout) = builder.timeout {
1123 if started.elapsed() >= timeout {
1124 timed_out = true;
1125 let _ = child.kill();
1126 break;
1127 }
1128 }
1129 thread::sleep(Duration::from_millis(10));
1130 }
1131 let out = child.wait_with_output()?;
1132 Ok(ExecOutcome {
1133 status: out.status,
1134 stdout: out.stdout,
1135 stderr: out.stderr,
1136 duration: started.elapsed(),
1137 peak_rss_kib: None,
1138 timed_out,
1139 })
1140}
1141
1142#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1143fn kboxlike_spawn(builder: ExecBuilder) -> io::Result<ExecChild> {
1144 let target = builder.kboxlike.clone().ok_or_else(|| {
1145 io::Error::new(
1146 io::ErrorKind::InvalidInput,
1147 "kboxlike_spawn called without a kboxlike target",
1148 )
1149 })?;
1150 if builder.argv.is_empty() {
1151 return Err(io::Error::new(
1152 io::ErrorKind::InvalidInput,
1153 "exec: argv is empty",
1154 ));
1155 }
1156 if builder.tty || builder.cols.is_some() || builder.rows.is_some() {
1157 return Err(io::Error::new(
1158 io::ErrorKind::Unsupported,
1159 "kboxlike backend does not support TTY exec yet",
1160 ));
1161 }
1162 kboxlike_stage_files(&target, &builder.stage_files)?;
1163
1164 let argv = if builder.chain.is_empty() {
1165 builder.argv.clone()
1166 } else {
1167 runsc_spawn_argv(&builder)
1168 };
1169 let mut cmd = kboxlike_exec_command(&target, &builder, &argv)?;
1170 cmd.stdin(Stdio::piped());
1171 cmd.stdout(Stdio::piped());
1172 cmd.stderr(Stdio::piped());
1173 let mut child = cmd
1174 .spawn()
1175 .map_err(|e| io::Error::new(e.kind(), format!("kboxlike exec {:?}: {e}", argv)))?;
1176 let child_pid = child.id() as i32;
1177 let child_stdin = Arc::new(Mutex::new(child.stdin.take()));
1178 let child_stdout = child
1179 .stdout
1180 .take()
1181 .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "kboxlike exec stdout pipe missing"))?;
1182 let child_stderr = child
1183 .stderr
1184 .take()
1185 .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "kboxlike exec stderr pipe missing"))?;
1186
1187 let (host_sock, bridge_sock) = UnixStream::pair()?;
1188 let sock_r = host_sock.try_clone()?;
1189 let interrupt_fd = Arc::new(host_sock.try_clone()?);
1190 let sock_w = Arc::new(Mutex::new(host_sock));
1191
1192 let (stdout_tx, stdout_r) = channel::<Vec<u8>>();
1193 let (stderr_tx, stderr_r) = channel::<Vec<u8>>();
1194 let (exit_tx, exit_rx) = channel::<DemuxExit>();
1195 let demux = thread::Builder::new()
1196 .name("supermachine-kboxlike-exec-demux".into())
1197 .spawn(move || {
1198 demux_loop(sock_r, stdout_tx, stderr_tx, exit_tx);
1199 })
1200 .map_err(|e| {
1201 io::Error::new(
1202 io::ErrorKind::Other,
1203 format!("kboxlike exec: demux thread: {e}"),
1204 )
1205 })?;
1206
1207 let bridge_w = Arc::new(Mutex::new(bridge_sock.try_clone()?));
1208 let control_stdin = Arc::clone(&child_stdin);
1209 let process_done = Arc::new(AtomicBool::new(false));
1210 let control_done = Arc::clone(&process_done);
1211 let mut control_r = bridge_sock.try_clone()?;
1212 thread::Builder::new()
1213 .name("supermachine-kboxlike-exec-control".into())
1214 .spawn(move || {
1215 runsc_spawn_control_loop(&mut control_r, child_pid, control_stdin, control_done);
1216 })
1217 .map_err(|e| {
1218 io::Error::new(
1219 io::ErrorKind::Other,
1220 format!("kboxlike exec: control thread: {e}"),
1221 )
1222 })?;
1223
1224 let stdout_pump = runsc_spawn_pump(
1225 "supermachine-kboxlike-exec-stdout",
1226 child_stdout,
1227 FRAME_STDOUT,
1228 Arc::clone(&bridge_w),
1229 )?;
1230 let stderr_pump = runsc_spawn_pump(
1231 "supermachine-kboxlike-exec-stderr",
1232 child_stderr,
1233 FRAME_STDERR,
1234 Arc::clone(&bridge_w),
1235 )?;
1236 thread::Builder::new()
1237 .name("supermachine-kboxlike-exec-wait".into())
1238 .spawn(move || {
1239 drop(child_stdin);
1240 let status = child.wait();
1241 let payload = match status {
1242 Ok(status) => runsc_status_code(status).to_be_bytes(),
1243 Err(_) => 1u32.to_be_bytes(),
1244 };
1245 process_done.store(true, std::sync::atomic::Ordering::SeqCst);
1246 let _ = send_frame(&bridge_w, FRAME_EXIT, &payload);
1247 if let Ok(sock) = bridge_w.lock() {
1248 let _ = sock.shutdown(Shutdown::Both);
1249 }
1250 let _ = stdout_pump.join();
1251 let _ = stderr_pump.join();
1252 })
1253 .map_err(|e| {
1254 io::Error::new(
1255 io::ErrorKind::Other,
1256 format!("kboxlike exec: wait thread: {e}"),
1257 )
1258 })?;
1259
1260 let stdin = ExecStdin::new(sock_w.clone());
1261
1262 Ok(ExecChild {
1263 sock_w,
1264 we_signaled: Arc::new(AtomicBool::new(false)),
1265 aborted: Arc::new(AtomicBool::new(false)),
1266 interrupt_fd,
1267 stdout_r: Some(stdout_r),
1268 stderr_r: Some(stderr_r),
1269 stdin: Some(stdin),
1270 demux: Some(demux),
1271 exit_rx,
1272 })
1273}
1274
1275#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1276fn kboxlike_exec_command(
1277 target: &KboxlikeExecTarget,
1278 builder: &ExecBuilder,
1279 argv: &[String],
1280) -> io::Result<Command> {
1281 if argv.is_empty() {
1282 return Err(io::Error::new(
1283 io::ErrorKind::InvalidInput,
1284 "exec: argv is empty",
1285 ));
1286 }
1287 let rootfs = std::ffi::CString::new(target.rootfs.as_os_str().as_bytes()).map_err(|_| {
1288 io::Error::new(
1289 io::ErrorKind::InvalidInput,
1290 "kboxlike rootfs path contains NUL byte",
1291 )
1292 })?;
1293 let cwd = std::ffi::CString::new(builder.cwd.as_deref().unwrap_or("/")).map_err(|_| {
1294 io::Error::new(
1295 io::ErrorKind::InvalidInput,
1296 "kboxlike exec cwd contains NUL byte",
1297 )
1298 })?;
1299 let mounts = kboxlike_prepare_runtime_mounts(&target.rootfs, &target.mounts)?;
1300 let mut cmd = Command::new(&argv[0]);
1301 cmd.args(&argv[1..]);
1302 cmd.env_clear();
1303 let mut env = BTreeMap::new();
1304 for (key, value) in &target.image_env {
1305 env.insert(key.clone(), value.clone());
1306 }
1307 for (key, value) in &builder.env {
1308 env.insert(key.clone(), value.clone());
1309 }
1310 for (key, value) in env {
1311 cmd.env(key, value);
1312 }
1313 let user = target.user;
1314 unsafe {
1315 cmd.pre_exec(move || {
1316 kboxlike_setup_runtime_mounts(&mounts)?;
1317 if libc::chroot(rootfs.as_ptr()) < 0 {
1318 return Err(io::Error::last_os_error());
1319 }
1320 if libc::chdir(cwd.as_ptr()) < 0 {
1321 return Err(io::Error::last_os_error());
1322 }
1323 if let Some((uid, gid)) = user {
1324 kboxlike_drop_uid_gid(uid, gid)?;
1325 }
1326 Ok(())
1327 });
1328 }
1329 Ok(cmd)
1330}
1331
1332#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1333struct KboxlikePreparedRuntimeMounts {
1334 binds: Vec<(std::ffi::CString, std::ffi::CString)>,
1335 proc_target: std::ffi::CString,
1336 shm_target: std::ffi::CString,
1337}
1338
1339#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1340fn kboxlike_prepare_runtime_mounts(
1341 rootfs: &Path,
1342 mounts: &[(PathBuf, String)],
1343) -> io::Result<KboxlikePreparedRuntimeMounts> {
1344 kboxlike_create_nested_mount_targets(mounts)?;
1345 let mut prepared = Vec::with_capacity(mounts.len());
1346 for (host_path, guest_path) in mounts {
1347 let target = kboxlike_rootfs_join(rootfs, guest_path)?;
1348 std::fs::create_dir_all(&target)?;
1349 let host = std::ffi::CString::new(host_path.as_os_str().as_bytes()).map_err(|_| {
1350 io::Error::new(
1351 io::ErrorKind::InvalidInput,
1352 format!(
1353 "kboxlike mount host path contains NUL byte: {}",
1354 host_path.display()
1355 ),
1356 )
1357 })?;
1358 let target = std::ffi::CString::new(target.as_os_str().as_bytes()).map_err(|_| {
1359 io::Error::new(
1360 io::ErrorKind::InvalidInput,
1361 format!("kboxlike mount target contains NUL byte: {guest_path}"),
1362 )
1363 })?;
1364 prepared.push((host, target));
1365 }
1366 kboxlike_append_standard_dev_bind_mounts(rootfs, &mut prepared)?;
1367 let proc_target = rootfs.join("proc");
1368 std::fs::create_dir_all(&proc_target)?;
1369 let shm_target = rootfs.join("dev").join("shm");
1370 std::fs::create_dir_all(&shm_target)?;
1371 let proc_target = std::ffi::CString::new(proc_target.as_os_str().as_bytes()).map_err(|_| {
1372 io::Error::new(
1373 io::ErrorKind::InvalidInput,
1374 format!(
1375 "kboxlike proc target path contains NUL byte: {}",
1376 proc_target.display()
1377 ),
1378 )
1379 })?;
1380 let shm_target = std::ffi::CString::new(shm_target.as_os_str().as_bytes()).map_err(|_| {
1381 io::Error::new(
1382 io::ErrorKind::InvalidInput,
1383 format!(
1384 "kboxlike shm target path contains NUL byte: {}",
1385 shm_target.display()
1386 ),
1387 )
1388 })?;
1389 Ok(KboxlikePreparedRuntimeMounts {
1390 binds: prepared,
1391 proc_target,
1392 shm_target,
1393 })
1394}
1395
1396#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1397fn kboxlike_create_nested_mount_targets(mounts: &[(PathBuf, String)]) -> io::Result<()> {
1398 for (parent_host, parent_guest) in mounts {
1399 for (_, child_guest) in mounts {
1400 let Some(relative_child) = kboxlike_nested_mount_suffix(parent_guest, child_guest)
1401 else {
1402 continue;
1403 };
1404 std::fs::create_dir_all(parent_host.join(relative_child))?;
1405 }
1406 }
1407 Ok(())
1408}
1409
1410#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1411fn kboxlike_nested_mount_suffix(parent_guest: &str, child_guest: &str) -> Option<PathBuf> {
1412 let parent = Path::new(parent_guest.strip_prefix('/').unwrap_or(parent_guest));
1413 let child = Path::new(child_guest.strip_prefix('/').unwrap_or(child_guest));
1414 if parent == child {
1415 return None;
1416 }
1417 child.strip_prefix(parent).ok().map(Path::to_path_buf)
1418}
1419
1420#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1421fn kboxlike_append_standard_dev_bind_mounts(
1422 rootfs: &Path,
1423 prepared: &mut Vec<(std::ffi::CString, std::ffi::CString)>,
1424) -> io::Result<()> {
1425 let dev_dir = rootfs.join("dev");
1426 std::fs::create_dir_all(&dev_dir)?;
1427 for name in ["null", "zero", "random", "urandom"] {
1428 let host_path = PathBuf::from("/dev").join(name);
1429 if !host_path.exists() {
1430 continue;
1431 }
1432 let target = dev_dir.join(name);
1433 if !target.exists() {
1434 std::fs::File::create(&target)?;
1435 }
1436 let host = std::ffi::CString::new(host_path.as_os_str().as_bytes()).map_err(|_| {
1437 io::Error::new(
1438 io::ErrorKind::InvalidInput,
1439 format!(
1440 "kboxlike device host path contains NUL byte: {}",
1441 host_path.display()
1442 ),
1443 )
1444 })?;
1445 let target = std::ffi::CString::new(target.as_os_str().as_bytes()).map_err(|_| {
1446 io::Error::new(
1447 io::ErrorKind::InvalidInput,
1448 format!(
1449 "kboxlike device target path contains NUL byte: {}",
1450 target.display()
1451 ),
1452 )
1453 })?;
1454 prepared.push((host, target));
1455 }
1456 Ok(())
1457}
1458
1459#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1460fn kboxlike_setup_runtime_mounts(mounts: &KboxlikePreparedRuntimeMounts) -> io::Result<()> {
1461 unsafe {
1462 if libc::unshare(libc::CLONE_NEWNS) < 0 {
1463 return Err(io::Error::last_os_error());
1464 }
1465 if libc::mount(
1466 std::ptr::null(),
1467 c"/".as_ptr(),
1468 std::ptr::null(),
1469 (libc::MS_REC | libc::MS_PRIVATE) as libc::c_ulong,
1470 std::ptr::null(),
1471 ) < 0
1472 {
1473 return Err(io::Error::last_os_error());
1474 }
1475 for (host, target) in &mounts.binds {
1476 if libc::mount(
1477 host.as_ptr(),
1478 target.as_ptr(),
1479 std::ptr::null(),
1480 (libc::MS_BIND | libc::MS_REC) as libc::c_ulong,
1481 std::ptr::null(),
1482 ) < 0
1483 {
1484 return Err(io::Error::last_os_error());
1485 }
1486 }
1487 if libc::mount(
1488 c"proc".as_ptr(),
1489 mounts.proc_target.as_ptr(),
1490 c"proc".as_ptr(),
1491 (libc::MS_NOSUID | libc::MS_NOEXEC | libc::MS_NODEV) as libc::c_ulong,
1492 std::ptr::null(),
1493 ) < 0
1494 {
1495 return Err(io::Error::last_os_error());
1496 }
1497 if libc::mount(
1498 c"tmpfs".as_ptr(),
1499 mounts.shm_target.as_ptr(),
1500 c"tmpfs".as_ptr(),
1501 (libc::MS_NOSUID | libc::MS_NODEV) as libc::c_ulong,
1502 c"mode=1777,size=64m".as_ptr().cast(),
1503 ) < 0
1504 {
1505 return Err(io::Error::last_os_error());
1506 }
1507 }
1508 Ok(())
1509}
1510
1511#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1512fn kboxlike_drop_uid_gid(uid: u32, gid: u32) -> io::Result<()> {
1513 unsafe {
1514 if libc::setgroups(0, std::ptr::null()) < 0 {
1515 return Err(io::Error::last_os_error());
1516 }
1517 if libc::setgid(gid as libc::gid_t) < 0 {
1518 return Err(io::Error::last_os_error());
1519 }
1520 if libc::setuid(uid as libc::uid_t) < 0 {
1521 return Err(io::Error::last_os_error());
1522 }
1523 }
1524 Ok(())
1525}
1526
1527#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1528fn kboxlike_stage_files(target: &KboxlikeExecTarget, files: &[StageFile]) -> io::Result<()> {
1529 for file in files {
1530 let target_path = kboxlike_rootfs_join(&target.rootfs, &file.path)?;
1531 let parent = target_path.parent().ok_or_else(|| {
1532 io::Error::new(
1533 io::ErrorKind::InvalidInput,
1534 format!("stage file has no parent: {}", file.path),
1535 )
1536 })?;
1537 std::fs::create_dir_all(parent)?;
1538 let name = target_path
1539 .file_name()
1540 .and_then(|name| name.to_str())
1541 .unwrap_or("stage");
1542 let tmp = parent.join(format!(".supermachine-stage-{}-{name}", std::process::id()));
1543 std::fs::write(&tmp, &file.data)?;
1544 let mode = file.mode.unwrap_or(0o644);
1545 std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(mode))?;
1546 std::fs::rename(&tmp, &target_path)?;
1547 }
1548 Ok(())
1549}
1550
1551#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1552fn kboxlike_rootfs_join(rootfs: &Path, guest_path: &str) -> io::Result<PathBuf> {
1553 let rel = guest_path.strip_prefix('/').unwrap_or(guest_path);
1554 let p = Path::new(rel);
1555 if rel.is_empty()
1556 || p.components().any(|component| {
1557 matches!(
1558 component,
1559 std::path::Component::ParentDir | std::path::Component::RootDir
1560 )
1561 })
1562 {
1563 return Err(io::Error::new(
1564 io::ErrorKind::InvalidInput,
1565 format!("guest path escapes rootfs: {guest_path}"),
1566 ));
1567 }
1568 Ok(rootfs.join(p))
1569}
1570
1571#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1572fn runsc_output_one(
1573 target: &RunscExecTarget,
1574 builder: &ExecBuilder,
1575 argv: &[String],
1576 started: Instant,
1577) -> io::Result<ExecOutcome> {
1578 let mut cmd = Command::new(&target.runsc_bin);
1579 add_runsc_global_flags_with_network(&mut cmd, &target.runsc_root, runsc_network(target));
1580 cmd.arg("exec");
1581 if let Some(cwd) = builder.cwd.as_deref() {
1582 cmd.arg("-cwd").arg(cwd);
1583 }
1584 let mut env = BTreeMap::new();
1585 for (k, v) in &target.image_env {
1586 env.insert(k.clone(), v.clone());
1587 }
1588 for (k, v) in &builder.env {
1589 env.insert(k.clone(), v.clone());
1590 }
1591 for (k, v) in env {
1592 cmd.arg("-env").arg(format!("{k}={v}"));
1593 }
1594 cmd.arg(&target.container_id);
1595 cmd.args(argv);
1596 cmd.stdin(Stdio::null());
1597 cmd.stdout(Stdio::piped());
1598 cmd.stderr(Stdio::piped());
1599
1600 let mut child = cmd.spawn().map_err(|e| {
1601 io::Error::new(e.kind(), format!("runsc exec {}: {e}", target.container_id))
1602 })?;
1603 let mut timed_out = false;
1604 loop {
1605 if child.try_wait()?.is_some() {
1606 break;
1607 }
1608 if let Some(timeout) = builder.timeout {
1609 if started.elapsed() >= timeout {
1610 timed_out = true;
1611 let _ = child.kill();
1612 break;
1613 }
1614 }
1615 thread::sleep(Duration::from_millis(10));
1616 }
1617 let out = child.wait_with_output()?;
1618 Ok(ExecOutcome {
1619 status: out.status,
1620 stdout: out.stdout,
1621 stderr: out.stderr,
1622 duration: started.elapsed(),
1623 peak_rss_kib: None,
1624 timed_out,
1625 })
1626}
1627
1628#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1629fn runsc_spawn(builder: ExecBuilder) -> io::Result<ExecChild> {
1630 let target = builder.runsc.clone().ok_or_else(|| {
1631 io::Error::new(
1632 io::ErrorKind::InvalidInput,
1633 "runsc_spawn called without a runsc target",
1634 )
1635 })?;
1636 if builder.argv.is_empty() {
1637 return Err(io::Error::new(
1638 io::ErrorKind::InvalidInput,
1639 "exec: argv is empty",
1640 ));
1641 }
1642 if target.agent_path.is_some() {
1643 return runsc_agent_spawn(builder, target);
1644 }
1645 if builder.tty || builder.cols.is_some() || builder.rows.is_some() {
1646 return Err(io::Error::new(
1647 io::ErrorKind::Unsupported,
1648 "runsc backend does not support TTY exec yet",
1649 ));
1650 }
1651 runsc_stage_files(&target, &builder.stage_files)?;
1652
1653 let argv = runsc_spawn_argv(&builder);
1654 let mut cmd = runsc_exec_command(&target, &builder, &argv);
1655 cmd.stdin(Stdio::piped());
1656 cmd.stdout(Stdio::piped());
1657 cmd.stderr(Stdio::piped());
1658 let mut child = cmd.spawn().map_err(|e| {
1659 io::Error::new(e.kind(), format!("runsc exec {}: {e}", target.container_id))
1660 })?;
1661 let child_pid = child.id() as i32;
1662 let child_stdin = Arc::new(Mutex::new(child.stdin.take()));
1663 let child_stdout = child
1664 .stdout
1665 .take()
1666 .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "runsc exec stdout pipe missing"))?;
1667 let child_stderr = child
1668 .stderr
1669 .take()
1670 .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "runsc exec stderr pipe missing"))?;
1671
1672 let (host_sock, bridge_sock) = UnixStream::pair()?;
1673 let sock_r = host_sock.try_clone()?;
1674 let interrupt_fd = Arc::new(host_sock.try_clone()?);
1675 let sock_w = Arc::new(Mutex::new(host_sock));
1676
1677 let (stdout_tx, stdout_r) = channel::<Vec<u8>>();
1678 let (stderr_tx, stderr_r) = channel::<Vec<u8>>();
1679 let (exit_tx, exit_rx) = channel::<DemuxExit>();
1680 let demux = thread::Builder::new()
1681 .name("supermachine-runsc-exec-demux".into())
1682 .spawn(move || {
1683 demux_loop(sock_r, stdout_tx, stderr_tx, exit_tx);
1684 })
1685 .map_err(|e| {
1686 io::Error::new(
1687 io::ErrorKind::Other,
1688 format!("runsc exec: demux thread: {e}"),
1689 )
1690 })?;
1691
1692 let bridge_w = Arc::new(Mutex::new(bridge_sock.try_clone()?));
1693 let control_stdin = Arc::clone(&child_stdin);
1694 let process_done = Arc::new(AtomicBool::new(false));
1695 let control_done = Arc::clone(&process_done);
1696 let mut control_r = bridge_sock.try_clone()?;
1697 thread::Builder::new()
1698 .name("supermachine-runsc-exec-control".into())
1699 .spawn(move || {
1700 runsc_spawn_control_loop(&mut control_r, child_pid, control_stdin, control_done);
1701 })
1702 .map_err(|e| {
1703 io::Error::new(
1704 io::ErrorKind::Other,
1705 format!("runsc exec: control thread: {e}"),
1706 )
1707 })?;
1708
1709 let stdout_pump = runsc_spawn_pump(
1710 "supermachine-runsc-exec-stdout",
1711 child_stdout,
1712 FRAME_STDOUT,
1713 Arc::clone(&bridge_w),
1714 )?;
1715 let stderr_pump = runsc_spawn_pump(
1716 "supermachine-runsc-exec-stderr",
1717 child_stderr,
1718 FRAME_STDERR,
1719 Arc::clone(&bridge_w),
1720 )?;
1721 thread::Builder::new()
1722 .name("supermachine-runsc-exec-wait".into())
1723 .spawn(move || {
1724 drop(child_stdin);
1725 let status = child.wait();
1726 let payload = match status {
1727 Ok(status) => runsc_status_code(status).to_be_bytes(),
1728 Err(_) => 1u32.to_be_bytes(),
1729 };
1730 process_done.store(true, std::sync::atomic::Ordering::SeqCst);
1731 let _ = send_frame(&bridge_w, FRAME_EXIT, &payload);
1732 if let Ok(sock) = bridge_w.lock() {
1733 let _ = sock.shutdown(Shutdown::Both);
1734 }
1735 let _ = stdout_pump.join();
1736 let _ = stderr_pump.join();
1737 })
1738 .map_err(|e| {
1739 io::Error::new(
1740 io::ErrorKind::Other,
1741 format!("runsc exec: wait thread: {e}"),
1742 )
1743 })?;
1744
1745 let stdin = ExecStdin::new(sock_w.clone());
1746
1747 Ok(ExecChild {
1748 sock_w,
1749 we_signaled: Arc::new(AtomicBool::new(false)),
1750 aborted: Arc::new(AtomicBool::new(false)),
1751 interrupt_fd,
1752 stdout_r: Some(stdout_r),
1753 stderr_r: Some(stderr_r),
1754 stdin: Some(stdin),
1755 demux: Some(demux),
1756 exit_rx,
1757 })
1758}
1759
1760#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1761fn runsc_agent_spawn(builder: ExecBuilder, target: RunscExecTarget) -> io::Result<ExecChild> {
1762 let debug = std::env::var_os("SUPERMACHINE_RUNSC_EXECDBG").is_some();
1763 let agent_path = target.agent_path.as_deref().ok_or_else(|| {
1764 io::Error::new(
1765 io::ErrorKind::InvalidInput,
1766 "runsc agent exec target missing agent path",
1767 )
1768 })?;
1769 let mut cmd = Command::new(&target.runsc_bin);
1770 add_runsc_global_flags_with_network(&mut cmd, &target.runsc_root, runsc_network(&target));
1771 cmd.arg("exec")
1772 .arg(&target.container_id)
1773 .arg(agent_path)
1774 .arg("--exec-stdio");
1775 cmd.stdin(Stdio::piped());
1776 cmd.stdout(Stdio::piped());
1777 cmd.stderr(Stdio::piped());
1778 if debug {
1779 eprintln!(
1780 "[runsc.execdbg] spawn agent container={} path={} argv0={}",
1781 target.container_id,
1782 agent_path,
1783 builder.argv.first().map(String::as_str).unwrap_or("")
1784 );
1785 }
1786 let mut child = cmd.spawn().map_err(|e| {
1787 io::Error::new(
1788 e.kind(),
1789 format!(
1790 "runsc exec {} {} --exec-stdio: {e}",
1791 target.container_id, agent_path
1792 ),
1793 )
1794 })?;
1795 let child_pid = child.id() as i32;
1796 let mut child_stdin = child.stdin.take().ok_or_else(|| {
1797 io::Error::new(io::ErrorKind::Other, "runsc agent exec stdin pipe missing")
1798 })?;
1799 let mut child_stdout = child.stdout.take().ok_or_else(|| {
1800 io::Error::new(io::ErrorKind::Other, "runsc agent exec stdout pipe missing")
1801 })?;
1802 let child_stderr = child.stderr.take().ok_or_else(|| {
1803 io::Error::new(io::ErrorKind::Other, "runsc agent exec stderr pipe missing")
1804 })?;
1805
1806 let (host_sock, bridge_sock) = UnixStream::pair()?;
1807 let sock_r = host_sock.try_clone()?;
1808 let interrupt_fd = Arc::new(host_sock.try_clone()?);
1809 let sock_w = Arc::new(Mutex::new(host_sock));
1810
1811 let (stdout_tx, stdout_r) = channel::<Vec<u8>>();
1812 let (stderr_tx, stderr_r) = channel::<Vec<u8>>();
1813 let (exit_tx, exit_rx) = channel::<DemuxExit>();
1814 let demux = thread::Builder::new()
1815 .name("supermachine-runsc-agent-demux".into())
1816 .spawn(move || {
1817 demux_loop(sock_r, stdout_tx, stderr_tx, exit_tx);
1818 })
1819 .map_err(|e| {
1820 io::Error::new(
1821 io::ErrorKind::Other,
1822 format!("runsc agent exec: demux thread: {e}"),
1823 )
1824 })?;
1825
1826 let bridge_w = Arc::new(Mutex::new(bridge_sock.try_clone()?));
1827 let mut bridge_to_agent = bridge_sock.try_clone()?;
1828 let process_done = Arc::new(AtomicBool::new(false));
1829 let pump_done = Arc::clone(&process_done);
1830 let debug_c2g = debug;
1831 thread::Builder::new()
1832 .name("supermachine-runsc-agent-c2g".into())
1833 .spawn(move || {
1834 let mut copied: u64 = 0;
1835 let mut buf = [0u8; 8192];
1836 let copied_result: io::Result<u64> = loop {
1837 match bridge_to_agent.read(&mut buf) {
1838 Ok(0) => break Ok(copied),
1839 Ok(n) => {
1840 copied += n as u64;
1841 if debug_c2g {
1842 eprintln!("[runsc.execdbg] c2g read n={n} total={copied}");
1843 }
1844 if let Err(e) = child_stdin.write_all(&buf[..n]) {
1845 break Err(e);
1846 }
1847 if let Err(e) = child_stdin.flush() {
1848 break Err(e);
1849 }
1850 }
1851 Err(e) => break Err(e),
1852 }
1853 };
1854 if debug_c2g {
1855 eprintln!("[runsc.execdbg] c2g done copied={copied_result:?}");
1856 }
1857 let _ = child_stdin.flush();
1858 drop(child_stdin);
1859 if !pump_done.load(std::sync::atomic::Ordering::SeqCst) {
1860 if debug_c2g {
1861 eprintln!("[runsc.execdbg] c2g killing host runsc exec pid={child_pid}");
1862 }
1863 let _ = unsafe { libc::kill(child_pid, libc::SIGKILL) };
1864 }
1865 })
1866 .map_err(|e| {
1867 io::Error::new(
1868 io::ErrorKind::Other,
1869 format!("runsc agent exec: stdin bridge thread: {e}"),
1870 )
1871 })?;
1872
1873 let stdout_bridge_w = Arc::clone(&bridge_w);
1874 let debug_g2c = debug;
1875 let stdout_pump = thread::Builder::new()
1876 .name("supermachine-runsc-agent-g2c".into())
1877 .spawn(move || {
1878 let mut buf = [0u8; 8192];
1879 let mut total: u64 = 0;
1880 loop {
1881 match child_stdout.read(&mut buf) {
1882 Ok(0) => {
1883 if debug_g2c {
1884 eprintln!("[runsc.execdbg] g2c eof total={total}");
1885 }
1886 return;
1887 }
1888 Ok(n) => {
1889 total += n as u64;
1890 if debug_g2c {
1891 eprintln!("[runsc.execdbg] g2c read n={n} total={total}");
1892 }
1893 let mut guard = match stdout_bridge_w.lock() {
1894 Ok(g) => g,
1895 Err(_) => return,
1896 };
1897 if guard.write_all(&buf[..n]).is_err() {
1898 return;
1899 }
1900 }
1901 Err(e) => {
1902 if debug_g2c {
1903 eprintln!("[runsc.execdbg] g2c read error {e}");
1904 }
1905 return;
1906 }
1907 }
1908 }
1909 })
1910 .map_err(|e| {
1911 io::Error::new(
1912 io::ErrorKind::Other,
1913 format!("runsc agent exec: stdout bridge thread: {e}"),
1914 )
1915 })?;
1916 let stderr_pump = runsc_spawn_pump(
1917 "supermachine-runsc-agent-stderr",
1918 child_stderr,
1919 FRAME_STDERR,
1920 Arc::clone(&bridge_w),
1921 )?;
1922
1923 thread::Builder::new()
1924 .name("supermachine-runsc-agent-wait".into())
1925 .spawn(move || {
1926 let status = child.wait();
1927 if debug {
1928 eprintln!("[runsc.execdbg] wait status={status:?}");
1929 }
1930 process_done.store(true, std::sync::atomic::Ordering::SeqCst);
1931 if let Ok(sock) = bridge_w.lock() {
1932 let _ = sock.shutdown(Shutdown::Both);
1933 }
1934 let _ = stdout_pump.join();
1935 let _ = stderr_pump.join();
1936 })
1937 .map_err(|e| {
1938 io::Error::new(
1939 io::ErrorKind::Other,
1940 format!("runsc agent exec: wait thread: {e}"),
1941 )
1942 })?;
1943
1944 let stage_payload: Vec<StageFilePayload> = builder
1945 .stage_files
1946 .iter()
1947 .map(|s| StageFilePayload {
1948 path: &s.path,
1949 data_b64: crate::api::b64_encode(&s.data),
1950 mode: s.mode,
1951 })
1952 .collect();
1953 let payload = RequestPayload {
1954 argv: &builder.argv,
1955 env: &builder.env,
1956 cwd: builder.cwd.as_deref(),
1957 tty: builder.tty,
1958 cols: builder.cols,
1959 rows: builder.rows,
1960 stage_files: stage_payload,
1961 chain: &builder.chain,
1962 };
1963 let json = serde_json::to_vec(&payload).map_err(|e| {
1964 io::Error::new(
1965 io::ErrorKind::InvalidInput,
1966 format!("runsc agent exec: encode REQUEST: {e}"),
1967 )
1968 })?;
1969 send_frame(&sock_w, FRAME_REQUEST, &json)?;
1970
1971 let stdin = ExecStdin::new(sock_w.clone());
1972
1973 Ok(ExecChild {
1974 sock_w,
1975 we_signaled: Arc::new(AtomicBool::new(false)),
1976 aborted: Arc::new(AtomicBool::new(false)),
1977 interrupt_fd,
1978 stdout_r: Some(stdout_r),
1979 stderr_r: Some(stderr_r),
1980 stdin: Some(stdin),
1981 demux: Some(demux),
1982 exit_rx,
1983 })
1984}
1985
1986#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
1987fn runsc_exec_command(target: &RunscExecTarget, builder: &ExecBuilder, argv: &[String]) -> Command {
1988 let mut cmd = Command::new(&target.runsc_bin);
1989 add_runsc_global_flags_with_network(&mut cmd, &target.runsc_root, runsc_network(target));
1990 cmd.arg("exec");
1991 if let Some(cwd) = builder.cwd.as_deref() {
1992 cmd.arg("-cwd").arg(cwd);
1993 }
1994 let mut env = BTreeMap::new();
1995 for (k, v) in &target.image_env {
1996 env.insert(k.clone(), v.clone());
1997 }
1998 for (k, v) in &builder.env {
1999 env.insert(k.clone(), v.clone());
2000 }
2001 for (k, v) in env {
2002 cmd.arg("-env").arg(format!("{k}={v}"));
2003 }
2004 cmd.arg(&target.container_id);
2005 cmd.args(argv);
2006 cmd
2007}
2008
2009#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2010fn runsc_spawn_argv(builder: &ExecBuilder) -> Vec<String> {
2011 if builder.chain.is_empty() {
2012 return builder.argv.clone();
2013 }
2014 let mut commands = Vec::with_capacity(1 + builder.chain.len());
2015 commands.push(shell_join_argv(&builder.argv));
2016 for argv in &builder.chain {
2017 commands.push(shell_join_argv(argv));
2018 }
2019 vec!["/bin/sh".into(), "-c".into(), commands.join(" && ")]
2020}
2021
2022#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2023fn shell_join_argv(argv: &[String]) -> String {
2024 argv.iter()
2025 .map(|arg| sh_quote(arg))
2026 .collect::<Vec<_>>()
2027 .join(" ")
2028}
2029
2030#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2031fn runsc_stage_files(target: &RunscExecTarget, files: &[StageFile]) -> io::Result<()> {
2032 for file in files {
2033 let q_path = sh_quote(&file.path);
2034 let mode = file.mode.unwrap_or(0o644);
2035 let script = format!(
2036 "set -e\n\
2037 p={q_path}\n\
2038 d=$(dirname -- \"$p\")\n\
2039 mkdir -p -- \"$d\"\n\
2040 t=\"$d/.supermachine-stage-$$-$(basename -- \"$p\")\"\n\
2041 cat > \"$t\"\n\
2042 chmod {mode:o} \"$t\"\n\
2043 mv -f -- \"$t\" \"$p\"\n"
2044 );
2045 let mut cmd = Command::new(&target.runsc_bin);
2046 add_runsc_global_flags_with_network(&mut cmd, &target.runsc_root, runsc_network(target));
2047 cmd.arg("exec")
2048 .arg(&target.container_id)
2049 .arg("/bin/sh")
2050 .arg("-c")
2051 .arg(script);
2052 cmd.stdin(Stdio::piped());
2053 cmd.stdout(Stdio::piped());
2054 cmd.stderr(Stdio::piped());
2055 let mut child = cmd.spawn().map_err(|e| {
2056 io::Error::new(
2057 e.kind(),
2058 format!(
2059 "runsc stage_file {} in {}: {e}",
2060 file.path, target.container_id
2061 ),
2062 )
2063 })?;
2064 if let Some(mut stdin) = child.stdin.take() {
2065 stdin.write_all(&file.data)?;
2066 }
2067 let out = child.wait_with_output()?;
2068 if !out.status.success() {
2069 return Err(io::Error::other(format!(
2070 "runsc stage_file {} failed: status={} stdout={} stderr={}",
2071 file.path,
2072 out.status,
2073 String::from_utf8_lossy(&out.stdout),
2074 String::from_utf8_lossy(&out.stderr)
2075 )));
2076 }
2077 }
2078 Ok(())
2079}
2080
2081#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2082fn runsc_spawn_control_loop(
2083 sock: &mut UnixStream,
2084 child_pid: i32,
2085 child_stdin: Arc<Mutex<Option<ChildStdin>>>,
2086 process_done: Arc<AtomicBool>,
2087) {
2088 loop {
2089 let (kind, payload) = match read_frame(sock) {
2090 Ok(frame) => frame,
2091 Err(_) => {
2092 if !process_done.load(std::sync::atomic::Ordering::SeqCst) {
2093 let _ = unsafe { libc::kill(child_pid, libc::SIGKILL) };
2094 }
2095 return;
2096 }
2097 };
2098 match kind {
2099 FRAME_STDIN => {
2100 let mut guard = match child_stdin.lock() {
2101 Ok(g) => g,
2102 Err(_) => return,
2103 };
2104 if payload.is_empty() {
2105 guard.take();
2106 } else if let Some(stdin) = guard.as_mut() {
2107 let _ = stdin.write_all(&payload);
2108 let _ = stdin.flush();
2109 }
2110 }
2111 FRAME_SIGNAL => {
2112 if let Some(signum) = payload.first().copied() {
2113 let _ = unsafe { libc::kill(child_pid, signum as i32) };
2114 }
2115 }
2116 FRAME_RESIZE => {}
2117 _ => {}
2118 }
2119 }
2120}
2121
2122#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2123fn runsc_spawn_pump<R>(
2124 name: &'static str,
2125 mut reader: R,
2126 kind: u8,
2127 bridge_w: Arc<Mutex<UnixStream>>,
2128) -> io::Result<JoinHandle<()>>
2129where
2130 R: Read + Send + 'static,
2131{
2132 thread::Builder::new()
2133 .name(name.into())
2134 .spawn(move || {
2135 let mut buf = [0u8; 8192];
2136 loop {
2137 match reader.read(&mut buf) {
2138 Ok(0) => return,
2139 Ok(n) => {
2140 if send_frame(&bridge_w, kind, &buf[..n]).is_err() {
2141 return;
2142 }
2143 }
2144 Err(_) => return,
2145 }
2146 }
2147 })
2148 .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("runsc exec: {name}: {e}")))
2149}
2150
2151#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2152fn write_raw_frame<W: Write>(writer: &mut W, kind: u8, payload: &[u8]) -> io::Result<()> {
2153 let mut header = [0u8; 5];
2154 header[0] = kind;
2155 header[1..5].copy_from_slice(&(payload.len() as u32).to_be_bytes());
2156 writer.write_all(&header)?;
2157 if !payload.is_empty() {
2158 writer.write_all(payload)?;
2159 }
2160 writer.flush()
2161}
2162
2163#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2164fn runsc_status_code(status: ExitStatus) -> u32 {
2165 #[cfg(unix)]
2166 {
2167 use std::os::unix::process::ExitStatusExt;
2168 if let Some(code) = status.code() {
2169 return code as u32;
2170 }
2171 if let Some(signal) = status.signal() {
2172 return 128 + signal as u32;
2173 }
2174 }
2175 1
2176}
2177
2178#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2179fn sh_quote(s: &str) -> String {
2180 let mut out = String::from("'");
2181 for ch in s.chars() {
2182 if ch == '\'' {
2183 out.push_str("'\\''");
2184 } else {
2185 out.push(ch);
2186 }
2187 }
2188 out.push('\'');
2189 out
2190}
2191
2192#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2193fn unique_suffix_nanos() -> u128 {
2194 std::time::SystemTime::now()
2195 .duration_since(std::time::UNIX_EPOCH)
2196 .map(|d| d.as_nanos())
2197 .unwrap_or(0)
2198}
2199
2200#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2201pub(crate) fn add_runsc_global_flags(cmd: &mut Command, root: &Path) {
2202 add_runsc_global_flags_with_network(cmd, root, RUNSC_NETWORK_NONE);
2203}
2204
2205#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2206pub(crate) fn add_runsc_global_flags_with_network(cmd: &mut Command, root: &Path, network: &str) {
2207 cmd.arg("--root")
2208 .arg(root)
2209 .arg("--platform=systrap")
2210 .arg(format!("--network={network}"))
2211 .arg("--overlay2=none")
2212 .arg("--file-access=shared");
2213 if network == RUNSC_NETWORK_SANDBOX {
2214 cmd.arg("--reproduce-nat").arg("--reproduce-nftables");
2215 }
2216}
2217
2218#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2219fn runsc_network(target: &RunscExecTarget) -> &str {
2220 target.network.as_deref().unwrap_or(RUNSC_NETWORK_NONE)
2221}
2222
2223#[derive(Serialize)]
2226struct RequestPayload<'a> {
2227 argv: &'a [String],
2228 env: &'a BTreeMap<String, String>,
2229 #[serde(skip_serializing_if = "Option::is_none")]
2230 cwd: Option<&'a str>,
2231 tty: bool,
2232 #[serde(skip_serializing_if = "Option::is_none")]
2233 cols: Option<u16>,
2234 #[serde(skip_serializing_if = "Option::is_none")]
2235 rows: Option<u16>,
2236 #[serde(skip_serializing_if = "Vec::is_empty")]
2237 stage_files: Vec<StageFilePayload<'a>>,
2238 #[serde(skip_serializing_if = "<[_]>::is_empty")]
2239 chain: &'a [Vec<String>],
2240}
2241
2242#[derive(Serialize)]
2243struct StageFilePayload<'a> {
2244 path: &'a str,
2245 data_b64: String,
2246 #[serde(skip_serializing_if = "Option::is_none")]
2247 mode: Option<u32>,
2248}
2249
2250pub struct ExecChild {
2256 sock_w: Arc<Mutex<UnixStream>>,
2260 we_signaled: Arc<AtomicBool>,
2272 aborted: Arc<AtomicBool>,
2278 interrupt_fd: Arc<UnixStream>,
2291 stdout_r: Option<Receiver<Vec<u8>>>,
2300 stderr_r: Option<Receiver<Vec<u8>>>,
2302 stdin: Option<ExecStdin>,
2305 demux: Option<JoinHandle<()>>,
2307 exit_rx: Receiver<DemuxExit>,
2309}
2310
2311#[derive(Clone)]
2342pub struct ExecSignaler {
2343 sock_w: Arc<Mutex<UnixStream>>,
2344 interrupt_fd: Arc<UnixStream>,
2345 we_signaled: Arc<AtomicBool>,
2351 aborted: Arc<AtomicBool>,
2356}
2357
2358impl std::fmt::Debug for ExecSignaler {
2359 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2360 f.debug_struct("ExecSignaler").finish_non_exhaustive()
2361 }
2362}
2363
2364impl ExecSignaler {
2365 pub fn signal(&self, signum: i32) -> io::Result<()> {
2374 self.we_signaled
2380 .store(true, std::sync::atomic::Ordering::SeqCst);
2381 let payload = [signum as u8];
2382 send_frame(&self.sock_w, FRAME_SIGNAL, &payload)
2383 }
2384
2385 pub fn kill(&self) -> io::Result<()> {
2388 self.signal(libc::SIGKILL)
2389 }
2390
2391 pub fn abort(&self) -> io::Result<()> {
2405 self.aborted
2408 .store(true, std::sync::atomic::Ordering::SeqCst);
2409 match self.interrupt_fd.shutdown(Shutdown::Both) {
2411 Ok(()) => Ok(()),
2412 Err(e) if e.kind() == io::ErrorKind::NotConnected => Ok(()),
2413 Err(e) => Err(e),
2414 }
2415 }
2416}
2417
2418enum DemuxExit {
2420 Status {
2424 code: u32,
2425 peak_rss_kib: Option<u64>,
2426 },
2427 EofBeforeExit { frames: u64, bytes: u64 },
2433 Error(String),
2435 Io(io::Error),
2437}
2438
2439impl ExecChild {
2440 pub fn stdin(&mut self) -> Option<ExecStdin> {
2443 self.stdin.take()
2444 }
2445
2446 pub fn stdout(&mut self) -> Option<ExecStdout> {
2449 self.stdout_r.take().map(ExecStdout::from_rx)
2450 }
2451
2452 pub fn stderr(&mut self) -> Option<ExecStderr> {
2454 self.stderr_r.take().map(ExecStderr::from_rx)
2455 }
2456
2457 pub fn signal(&self, signum: i32) -> io::Result<()> {
2459 self.we_signaled
2462 .store(true, std::sync::atomic::Ordering::SeqCst);
2463 let payload = [signum as u8];
2464 send_frame(&self.sock_w, FRAME_SIGNAL, &payload)
2465 }
2466
2467 pub fn signaler(&self) -> ExecSignaler {
2481 ExecSignaler {
2482 sock_w: Arc::clone(&self.sock_w),
2483 interrupt_fd: Arc::clone(&self.interrupt_fd),
2484 we_signaled: Arc::clone(&self.we_signaled),
2485 aborted: Arc::clone(&self.aborted),
2486 }
2487 }
2488
2489 pub fn abort(&self) -> io::Result<()> {
2503 self.aborted
2504 .store(true, std::sync::atomic::Ordering::SeqCst);
2505 match self.interrupt_fd.shutdown(Shutdown::Both) {
2506 Ok(()) => Ok(()),
2507 Err(e) if e.kind() == io::ErrorKind::NotConnected => Ok(()),
2508 Err(e) => Err(e),
2509 }
2510 }
2511
2512 pub fn try_wait(&self) -> io::Result<Option<(ExitStatus, Option<u64>)>> {
2521 match self.exit_rx.try_recv() {
2522 Ok(DemuxExit::Status { code, peak_rss_kib }) => {
2523 Ok(Some((synthesize_exit(code), peak_rss_kib)))
2524 }
2525 Ok(DemuxExit::EofBeforeExit { frames, bytes }) => {
2526 if self.we_signaled.load(std::sync::atomic::Ordering::SeqCst) {
2532 Ok(Some((synthesize_killed_by_signal(libc::SIGKILL), None)))
2533 } else {
2534 Err(eof_before_exit_error(
2535 self.aborted.load(std::sync::atomic::Ordering::SeqCst),
2536 frames,
2537 bytes,
2538 ))
2539 }
2540 }
2541 Ok(DemuxExit::Error(msg)) => Err(io::Error::new(io::ErrorKind::Other, msg)),
2542 Ok(DemuxExit::Io(e)) => Err(e),
2543 Err(TryRecvError::Empty) => Ok(None),
2544 Err(TryRecvError::Disconnected) => Err(io::Error::new(
2545 io::ErrorKind::Other,
2546 "exec: demux thread died",
2547 )),
2548 }
2549 }
2550
2551 pub fn resize(&self, cols: u16, rows: u16) -> io::Result<()> {
2554 let mut payload = [0u8; 4];
2555 payload[0..2].copy_from_slice(&cols.to_be_bytes());
2556 payload[2..4].copy_from_slice(&rows.to_be_bytes());
2557 send_frame(&self.sock_w, FRAME_RESIZE, &payload)
2558 }
2559
2560 pub fn wait(self) -> io::Result<ExitStatus> {
2563 Ok(self.wait_with_rss()?.0)
2564 }
2565
2566 pub fn wait_with_rss(mut self) -> io::Result<(ExitStatus, Option<u64>)> {
2572 self.stdin.take();
2575
2576 let exit = self
2577 .exit_rx
2578 .recv()
2579 .map_err(|_| io::Error::new(io::ErrorKind::Other, "exec: demux thread died"))?;
2580 if let Some(h) = self.demux.take() {
2581 let _ = h.join();
2582 }
2583 match exit {
2584 DemuxExit::Status { code, peak_rss_kib } => Ok((synthesize_exit(code), peak_rss_kib)),
2585 DemuxExit::EofBeforeExit { frames, bytes } => {
2586 if self.we_signaled.load(std::sync::atomic::Ordering::SeqCst) {
2591 Ok((synthesize_killed_by_signal(libc::SIGKILL), None))
2592 } else {
2593 Err(eof_before_exit_error(
2594 self.aborted.load(std::sync::atomic::Ordering::SeqCst),
2595 frames,
2596 bytes,
2597 ))
2598 }
2599 }
2600 DemuxExit::Error(msg) => Err(io::Error::new(io::ErrorKind::Other, msg)),
2601 DemuxExit::Io(e) => Err(e),
2602 }
2603 }
2604}
2605
2606impl Drop for ExecChild {
2607 fn drop(&mut self) {
2608 self.stdin.take();
2629 self.aborted
2633 .store(true, std::sync::atomic::Ordering::SeqCst);
2634 let _ = self.interrupt_fd.shutdown(Shutdown::Both);
2635 }
2636}
2637
2638pub struct ExecStdin {
2644 sock_w: Arc<Mutex<UnixStream>>,
2645 closed: bool,
2646}
2647
2648impl ExecStdin {
2649 fn new(sock_w: Arc<Mutex<UnixStream>>) -> Self {
2650 Self {
2651 sock_w,
2652 closed: false,
2653 }
2654 }
2655
2656 pub fn close(mut self) -> io::Result<()> {
2660 if self.closed {
2661 return Ok(());
2662 }
2663 self.closed = true;
2664 send_frame(&self.sock_w, FRAME_STDIN, &[])
2665 }
2666}
2667
2668impl Write for ExecStdin {
2669 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
2670 if buf.is_empty() {
2671 return Ok(0);
2672 }
2673 send_frame(&self.sock_w, FRAME_STDIN, buf)?;
2674 Ok(buf.len())
2675 }
2676 fn flush(&mut self) -> io::Result<()> {
2677 Ok(())
2678 }
2679}
2680
2681impl Drop for ExecStdin {
2682 fn drop(&mut self) {
2683 if !self.closed {
2684 let _ = send_frame(&self.sock_w, FRAME_STDIN, &[]);
2687 }
2688 }
2689}
2690
2691pub struct ExecStdout {
2701 rx: Receiver<Vec<u8>>,
2702 leftover: Vec<u8>,
2705}
2706
2707impl ExecStdout {
2708 fn from_rx(rx: Receiver<Vec<u8>>) -> Self {
2709 Self {
2710 rx,
2711 leftover: Vec::new(),
2712 }
2713 }
2714
2715 pub fn try_read(&mut self, buf: &mut [u8]) -> io::Result<Option<usize>> {
2720 chunk_channel_try_read(&self.rx, &mut self.leftover, buf)
2721 }
2722}
2723
2724impl Read for ExecStdout {
2725 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
2726 chunk_channel_read(&self.rx, &mut self.leftover, buf)
2727 }
2728}
2729
2730pub struct ExecStderr {
2732 rx: Receiver<Vec<u8>>,
2733 leftover: Vec<u8>,
2734}
2735
2736impl ExecStderr {
2737 fn from_rx(rx: Receiver<Vec<u8>>) -> Self {
2738 Self {
2739 rx,
2740 leftover: Vec::new(),
2741 }
2742 }
2743
2744 pub fn try_read(&mut self, buf: &mut [u8]) -> io::Result<Option<usize>> {
2746 chunk_channel_try_read(&self.rx, &mut self.leftover, buf)
2747 }
2748}
2749
2750impl Read for ExecStderr {
2751 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
2752 chunk_channel_read(&self.rx, &mut self.leftover, buf)
2753 }
2754}
2755
2756fn chunk_channel_read(
2761 rx: &Receiver<Vec<u8>>,
2762 leftover: &mut Vec<u8>,
2763 buf: &mut [u8],
2764) -> io::Result<usize> {
2765 if buf.is_empty() {
2766 return Ok(0);
2767 }
2768 if leftover.is_empty() {
2769 match rx.recv() {
2770 Ok(chunk) => {
2771 if chunk.is_empty() {
2772 return chunk_channel_read(rx, leftover, buf);
2775 }
2776 *leftover = chunk;
2777 }
2778 Err(_) => return Ok(0), }
2780 }
2781 let n = buf.len().min(leftover.len());
2782 buf[..n].copy_from_slice(&leftover[..n]);
2783 leftover.drain(..n);
2784 Ok(n)
2785}
2786
2787fn chunk_channel_try_read(
2788 rx: &Receiver<Vec<u8>>,
2789 leftover: &mut Vec<u8>,
2790 buf: &mut [u8],
2791) -> io::Result<Option<usize>> {
2792 if buf.is_empty() {
2793 return Ok(Some(0));
2794 }
2795 while leftover.is_empty() {
2796 match rx.try_recv() {
2797 Ok(chunk) => {
2798 if chunk.is_empty() {
2799 continue;
2800 }
2801 *leftover = chunk;
2802 }
2803 Err(TryRecvError::Empty) => return Ok(None),
2804 Err(TryRecvError::Disconnected) => return Ok(Some(0)),
2805 }
2806 }
2807 let n = buf.len().min(leftover.len());
2808 buf[..n].copy_from_slice(&leftover[..n]);
2809 leftover.drain(..n);
2810 Ok(Some(n))
2811}
2812
2813pub fn send_control(exec_path: &Path, action_json: &serde_json::Value) -> io::Result<()> {
2823 let _ = send_control_with_ack(exec_path, action_json, None)?;
2824 Ok(())
2825}
2826
2827pub fn send_control_with_ack(
2840 exec_path: &Path,
2841 action_json: &serde_json::Value,
2842 read_timeout: Option<std::time::Duration>,
2843) -> io::Result<serde_json::Value> {
2844 use std::io::Read;
2845 let mut sock = UnixStream::connect(exec_path)?;
2846 sock.set_read_timeout(read_timeout.or(Some(std::time::Duration::from_secs(5))))?;
2847 sock.set_write_timeout(Some(std::time::Duration::from_secs(5)))?;
2848 let body = serde_json::to_vec(action_json)
2850 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, format!("encode CONTROL: {e}")))?;
2851 let mut header = [0u8; 5];
2852 header[0] = FRAME_CONTROL;
2853 header[1..5].copy_from_slice(&(body.len() as u32).to_be_bytes());
2854 sock.write_all(&header)?;
2855 if !body.is_empty() {
2856 sock.write_all(&body)?;
2857 }
2858
2859 let mut ack_hdr = [0u8; 5];
2862 sock.read_exact(&mut ack_hdr)?;
2863 let kind = ack_hdr[0];
2864 let len = u32::from_be_bytes([ack_hdr[1], ack_hdr[2], ack_hdr[3], ack_hdr[4]]) as usize;
2865 if len > 16 * 1024 * 1024 {
2866 return Err(io::Error::new(
2867 io::ErrorKind::InvalidData,
2868 format!("CONTROL ack frame too large: {len}"),
2869 ));
2870 }
2871 let mut ack_body = vec![0u8; len];
2872 if !ack_body.is_empty() {
2873 sock.read_exact(&mut ack_body)?;
2874 }
2875
2876 if kind != FRAME_CONTROL {
2877 return Err(io::Error::new(
2878 io::ErrorKind::InvalidData,
2879 format!("expected CONTROL ack frame, got {kind:#x}"),
2880 ));
2881 }
2882 let ack: serde_json::Value = serde_json::from_slice(&ack_body).map_err(|e| {
2883 io::Error::new(io::ErrorKind::InvalidData, format!("CONTROL ack JSON: {e}"))
2884 })?;
2885 if ack.get("ok").and_then(|v| v.as_bool()) == Some(true) {
2886 return Ok(ack);
2887 }
2888 let msg = ack
2889 .get("error")
2890 .and_then(|v| v.as_str())
2891 .unwrap_or("CONTROL: agent reported failure")
2892 .to_owned();
2893 Err(io::Error::new(io::ErrorKind::Other, msg))
2894}
2895
2896fn spawn(builder: ExecBuilder) -> io::Result<ExecChild> {
2899 let sock = UnixStream::connect(&builder.exec_path)?;
2900 let sock_r = sock.try_clone()?;
2901 let interrupt_fd = Arc::new(sock.try_clone()?);
2906 let sock_w = Arc::new(Mutex::new(sock));
2907
2908 let stage_payload: Vec<StageFilePayload> = builder
2910 .stage_files
2911 .iter()
2912 .map(|s| StageFilePayload {
2913 path: &s.path,
2914 data_b64: crate::api::b64_encode(&s.data),
2915 mode: s.mode,
2916 })
2917 .collect();
2918 let payload = RequestPayload {
2919 argv: &builder.argv,
2920 env: &builder.env,
2921 cwd: builder.cwd.as_deref(),
2922 tty: builder.tty,
2923 cols: builder.cols,
2924 rows: builder.rows,
2925 stage_files: stage_payload,
2926 chain: &builder.chain,
2927 };
2928 let json = serde_json::to_vec(&payload).map_err(|e| {
2929 io::Error::new(
2930 io::ErrorKind::InvalidInput,
2931 format!("exec: encode REQUEST: {e}"),
2932 )
2933 })?;
2934 send_frame(&sock_w, FRAME_REQUEST, &json)?;
2935
2936 let (stdout_tx, stdout_r) = channel::<Vec<u8>>();
2945 let (stderr_tx, stderr_r) = channel::<Vec<u8>>();
2946
2947 let (exit_tx, exit_rx) = channel::<DemuxExit>();
2948 let demux = thread::Builder::new()
2949 .name("supermachine-exec-demux".into())
2950 .spawn(move || {
2951 demux_loop(sock_r, stdout_tx, stderr_tx, exit_tx);
2952 })
2953 .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("exec: demux thread: {e}")))?;
2954
2955 let stdin = ExecStdin::new(sock_w.clone());
2956
2957 Ok(ExecChild {
2958 sock_w,
2959 we_signaled: Arc::new(AtomicBool::new(false)),
2960 aborted: Arc::new(AtomicBool::new(false)),
2961 interrupt_fd,
2962 stdout_r: Some(stdout_r),
2963 stderr_r: Some(stderr_r),
2964 stdin: Some(stdin),
2965 demux: Some(demux),
2966 exit_rx,
2967 })
2968}
2969
2970fn demux_loop(
2971 mut sock: UnixStream,
2972 stdout_tx: Sender<Vec<u8>>,
2973 stderr_tx: Sender<Vec<u8>>,
2974 exit_tx: Sender<DemuxExit>,
2975) {
2976 let mut frames: u64 = 0;
2985 let mut bytes: u64 = 0;
2986 loop {
2987 let (kind, payload) = match read_frame(&mut sock) {
2988 Ok(f) => f,
2989 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
2990 let _ = exit_tx.send(DemuxExit::EofBeforeExit { frames, bytes });
2991 return;
2992 }
2993 Err(e) => {
2994 let _ = exit_tx.send(DemuxExit::Io(e));
2995 return;
2996 }
2997 };
2998 match kind {
2999 FRAME_STDOUT => {
3000 frames += 1;
3001 bytes += payload.len() as u64;
3002 let _ = stdout_tx.send(payload);
3003 }
3004 FRAME_STDERR => {
3005 frames += 1;
3006 bytes += payload.len() as u64;
3007 let _ = stderr_tx.send(payload);
3008 }
3009 FRAME_EXIT => {
3010 let _ = exit_tx.send(decode_exit_payload(&payload));
3011 return;
3012 }
3013 FRAME_ERROR => {
3014 let msg = String::from_utf8_lossy(&payload).into_owned();
3015 let _ = exit_tx.send(DemuxExit::Error(msg));
3016 return;
3017 }
3018 _ => {
3019 }
3024 }
3025 }
3026}
3027
3028fn send_frame(sock: &Arc<Mutex<UnixStream>>, kind: u8, payload: &[u8]) -> io::Result<()> {
3031 let mut header = [0u8; 5];
3032 header[0] = kind;
3033 header[1..5].copy_from_slice(&(payload.len() as u32).to_be_bytes());
3034 let mut g = sock
3035 .lock()
3036 .map_err(|_| io::Error::new(io::ErrorKind::Other, "exec: socket mutex poisoned"))?;
3037 g.write_all(&header)?;
3038 if !payload.is_empty() {
3039 g.write_all(payload)?;
3040 }
3041 Ok(())
3042}
3043
3044fn read_frame<R: Read>(sock: &mut R) -> io::Result<(u8, Vec<u8>)> {
3045 let mut header = [0u8; 5];
3046 sock.read_exact(&mut header)?;
3047 let kind = header[0];
3048 let len = u32::from_be_bytes([header[1], header[2], header[3], header[4]]) as usize;
3049 if len > 16 * 1024 * 1024 {
3053 return Err(io::Error::new(
3054 io::ErrorKind::InvalidData,
3055 format!("exec: frame len {len} > 16 MiB"),
3056 ));
3057 }
3058 let mut payload = vec![0u8; len];
3059 if !payload.is_empty() {
3060 sock.read_exact(&mut payload)?;
3061 }
3062 Ok((kind, payload))
3063}
3064
3065#[derive(Debug, Clone, Copy)]
3075pub struct ExecEof {
3076 pub aborted: bool,
3079 pub frames: u64,
3081 pub bytes: u64,
3083}
3084
3085impl ExecEof {
3086 pub fn is_setup_failure(&self) -> bool {
3095 !self.aborted && self.frames == 0 && self.bytes == 0
3096 }
3097}
3098
3099impl std::fmt::Display for ExecEof {
3100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3101 if self.aborted {
3102 f.write_str(
3103 "exec: connection closed before EXIT because the host aborted it locally \
3104 (abort()/Drop) — not an agent-side failure",
3105 )
3106 } else if self.frames == 0 && self.bytes == 0 {
3107 f.write_str(
3108 "exec: agent closed the connection before producing any output or EXIT — the \
3109 in-guest agent likely failed to start the process, or the VM went away \
3110 (guest panic / reboot / OOM-kill)",
3111 )
3112 } else {
3113 write!(
3114 f,
3115 "exec: agent closed the connection after {} output frame(s) / {} byte(s) \
3116 without sending EXIT — the channel dropped mid-stream (the host may be \
3117 oversubscribed; set SUPERMACHINE_WORKER_LOG_FILE to capture worker logs \
3118 for a post-mortem)",
3119 self.frames, self.bytes
3120 )
3121 }
3122 }
3123}
3124
3125impl std::error::Error for ExecEof {}
3126
3127fn eof_before_exit_error(aborted: bool, frames: u64, bytes: u64) -> io::Error {
3131 io::Error::new(
3132 io::ErrorKind::UnexpectedEof,
3133 ExecEof {
3134 aborted,
3135 frames,
3136 bytes,
3137 },
3138 )
3139}
3140
3141fn decode_exit_payload(payload: &[u8]) -> DemuxExit {
3149 if payload.len() < 4 {
3150 return DemuxExit::Error(format!(
3151 "truncated EXIT frame: {} byte(s), need >= 4 for the exit code",
3152 payload.len()
3153 ));
3154 }
3155 let code = u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]);
3156 let peak_rss_kib = if payload.len() >= 12 {
3157 Some(u64::from_be_bytes([
3158 payload[4],
3159 payload[5],
3160 payload[6],
3161 payload[7],
3162 payload[8],
3163 payload[9],
3164 payload[10],
3165 payload[11],
3166 ]))
3167 } else {
3168 None
3169 };
3170 DemuxExit::Status { code, peak_rss_kib }
3171}
3172
3173#[cfg(unix)]
3174fn synthesize_exit(code: u32) -> ExitStatus {
3175 use std::os::unix::process::ExitStatusExt;
3181 ExitStatus::from_raw(((code as i32) & 0xff) << 8)
3182}
3183
3184#[cfg(unix)]
3196fn synthesize_killed_by_signal(sig: i32) -> ExitStatus {
3197 use std::os::unix::process::ExitStatusExt;
3198 ExitStatus::from_raw(sig & 0x7f)
3201}
3202
3203#[cfg(test)]
3209mod tests {
3210 use super::*;
3211 use std::os::unix::net::UnixListener;
3212 use std::os::unix::process::ExitStatusExt;
3213 use std::path::PathBuf;
3214
3215 #[test]
3216 fn decode_exit_payload_reads_code_and_optional_rss() {
3217 match decode_exit_payload(&[0, 0, 0, 42]) {
3219 DemuxExit::Status { code, peak_rss_kib } => {
3220 assert_eq!(code, 42);
3221 assert_eq!(peak_rss_kib, None);
3222 }
3223 _ => panic!("expected Status"),
3224 }
3225 let mut p = vec![0, 0, 0, 7];
3227 p.extend_from_slice(&1234u64.to_be_bytes());
3228 match decode_exit_payload(&p) {
3229 DemuxExit::Status { code, peak_rss_kib } => {
3230 assert_eq!(code, 7);
3231 assert_eq!(peak_rss_kib, Some(1234));
3232 }
3233 _ => panic!("expected Status"),
3234 }
3235 match decode_exit_payload(&[0, 0, 1, 0, 0xaa, 0xbb]) {
3237 DemuxExit::Status { code, peak_rss_kib } => {
3238 assert_eq!(code, 256);
3239 assert_eq!(peak_rss_kib, None);
3240 }
3241 _ => panic!("expected Status"),
3242 }
3243 }
3244
3245 #[test]
3246 fn decode_exit_payload_truncated_is_error_not_exit_zero() {
3247 for short in [&[][..], &[0][..], &[0, 0][..], &[0, 0, 0][..]] {
3250 match decode_exit_payload(short) {
3251 DemuxExit::Error(msg) => assert!(msg.contains("truncated EXIT"), "{msg}"),
3252 _ => panic!("{}-byte EXIT frame must be an Error", short.len()),
3253 }
3254 }
3255 }
3256
3257 #[test]
3258 fn synthesize_exit_round_trips_exit_code() {
3259 for c in [0u32, 1, 42, 255] {
3260 assert_eq!(synthesize_exit(c).code(), Some(c as i32), "code {c}");
3261 }
3262 assert!(synthesize_exit(0).signal().is_none());
3263 }
3264
3265 #[test]
3266 fn synthesize_killed_by_signal_reports_signal_not_code() {
3267 assert_eq!(synthesize_killed_by_signal(9).signal(), Some(9));
3269 assert_eq!(synthesize_killed_by_signal(9).code(), None);
3270 assert_eq!(synthesize_killed_by_signal(15).signal(), Some(15));
3271 }
3272
3273 #[test]
3274 fn eof_before_exit_error_is_self_diagnosing() {
3275 let aborted = eof_before_exit_error(true, 0, 0);
3278 assert_eq!(aborted.kind(), io::ErrorKind::UnexpectedEof);
3279 assert!(aborted.to_string().contains("aborted it locally"));
3280
3281 let never_started = eof_before_exit_error(false, 0, 0);
3282 assert_eq!(never_started.kind(), io::ErrorKind::UnexpectedEof);
3283 assert!(never_started
3284 .to_string()
3285 .contains("before producing any output"));
3286
3287 let mid_stream = eof_before_exit_error(false, 7, 4096);
3288 assert_eq!(mid_stream.kind(), io::ErrorKind::UnexpectedEof);
3289 let msg = mid_stream.to_string();
3290 assert!(
3291 msg.contains("7 output frame"),
3292 "should report frame count: {msg}"
3293 );
3294 assert!(msg.contains("4096 byte"), "should report byte count: {msg}");
3295 assert!(
3296 msg.contains("mid-stream"),
3297 "should flag a mid-stream drop: {msg}"
3298 );
3299
3300 let aborted_after_output = eof_before_exit_error(true, 7, 4096);
3303 assert!(aborted_after_output
3304 .to_string()
3305 .contains("aborted it locally"));
3306 }
3307
3308 #[test]
3309 fn exec_eof_is_structured_and_downcastable() {
3310 let setup = eof_before_exit_error(false, 0, 0);
3314 let eof = setup
3315 .get_ref()
3316 .and_then(|e| e.downcast_ref::<ExecEof>())
3317 .expect("UnexpectedEof must wrap a downcastable ExecEof");
3318 assert!(
3319 eof.is_setup_failure(),
3320 "no output + not aborted ⇒ retryable setup failure"
3321 );
3322
3323 let mid = eof_before_exit_error(false, 3, 100);
3325 let mid_eof = mid.get_ref().unwrap().downcast_ref::<ExecEof>().unwrap();
3326 assert!(
3327 !mid_eof.is_setup_failure(),
3328 "mid-stream drop must never be retried"
3329 );
3330
3331 let aborted = eof_before_exit_error(true, 0, 0);
3333 let ab_eof = aborted
3334 .get_ref()
3335 .unwrap()
3336 .downcast_ref::<ExecEof>()
3337 .unwrap();
3338 assert!(
3339 !ab_eof.is_setup_failure(),
3340 "local abort is not an agent setup failure"
3341 );
3342
3343 }
3347
3348 #[test]
3354 fn exec_signaler_is_send_sync_clone() {
3355 fn assert_send<T: Send>() {}
3356 fn assert_sync<T: Sync>() {}
3357 fn assert_clone<T: Clone>() {}
3358 assert_send::<ExecSignaler>();
3359 assert_sync::<ExecSignaler>();
3360 assert_clone::<ExecSignaler>();
3361 }
3362
3363 #[test]
3368 fn exec_child_is_send() {
3369 fn assert_send<T: Send>() {}
3370 assert_send::<ExecChild>();
3371 }
3372
3373 fn short_sock_dir() -> PathBuf {
3392 use std::sync::atomic::{AtomicU64, Ordering};
3393 static SEQ: AtomicU64 = AtomicU64::new(0);
3394 let dir = PathBuf::from("/tmp").join(format!(
3395 "sm-exec-{}-{}",
3396 std::process::id(),
3397 SEQ.fetch_add(1, Ordering::Relaxed)
3398 ));
3399 std::fs::create_dir_all(&dir).unwrap();
3400 dir
3401 }
3402
3403 #[test]
3404 fn signaler_abort_unblocks_blocked_wait() {
3405 let dir = short_sock_dir();
3406 let sock_path: PathBuf = dir.join("exec.sock");
3407 let listener = UnixListener::bind(&sock_path).unwrap();
3408 let listener_handle = thread::spawn(move || {
3413 if let Ok((mut conn, _addr)) = listener.accept() {
3414 let mut hdr = [0u8; 5];
3417 if conn.read_exact(&mut hdr).is_ok() {
3418 let len = u32::from_be_bytes([hdr[1], hdr[2], hdr[3], hdr[4]]) as usize;
3419 let mut payload = vec![0u8; len];
3420 let _ = conn.read_exact(&mut payload);
3421 }
3422 thread::sleep(Duration::from_secs(60));
3424 }
3425 });
3426
3427 let builder = ExecBuilder::new(sock_path.clone()).argv(["true"]);
3429 let child = builder.spawn().expect("spawn against listener");
3430 let signaler = child.signaler();
3431
3432 let abort_handle = thread::spawn(move || {
3434 thread::sleep(Duration::from_millis(50));
3435 signaler.abort().expect("abort must succeed");
3436 });
3437
3438 let t0 = Instant::now();
3442 let res = child.wait();
3443 let elapsed = t0.elapsed();
3444 let _ = abort_handle.join();
3445 assert!(
3448 elapsed < Duration::from_secs(5),
3449 "wait must unblock quickly after abort; took {:?}",
3450 elapsed
3451 );
3452 assert!(
3453 res.is_err(),
3454 "wait after abort must return Err (the agent didn't send EXIT); got {:?}",
3455 res.as_ref().ok()
3456 );
3457 let err = res.unwrap_err();
3458 assert_eq!(
3459 err.kind(),
3460 io::ErrorKind::UnexpectedEof,
3461 "expected UnexpectedEof, got {:?}: {err}",
3462 err.kind()
3463 );
3464
3465 let _ = std::fs::remove_file(&sock_path);
3467 let _ = std::fs::remove_dir(&dir);
3468 std::mem::forget(listener_handle);
3471 }
3472
3473 #[test]
3477 fn try_wait_returns_none_then_some_after_abort() {
3478 let dir = short_sock_dir();
3479 let sock_path: PathBuf = dir.join("exec.sock");
3480 let listener = UnixListener::bind(&sock_path).unwrap();
3481 let listener_handle = thread::spawn(move || {
3482 if let Ok((mut conn, _)) = listener.accept() {
3483 let mut hdr = [0u8; 5];
3484 if conn.read_exact(&mut hdr).is_ok() {
3485 let len = u32::from_be_bytes([hdr[1], hdr[2], hdr[3], hdr[4]]) as usize;
3486 let mut payload = vec![0u8; len];
3487 let _ = conn.read_exact(&mut payload);
3488 }
3489 thread::sleep(Duration::from_secs(60));
3490 }
3491 });
3492
3493 let builder = ExecBuilder::new(sock_path.clone()).argv(["true"]);
3494 let child = builder.spawn().expect("spawn");
3495
3496 assert!(
3499 child.try_wait().expect("try_wait pre-exit").is_none(),
3500 "try_wait must return None while child is running"
3501 );
3502
3503 child.abort().expect("abort");
3507
3508 let t0 = Instant::now();
3512 let result = loop {
3513 match child.try_wait() {
3514 Ok(Some(_)) => break Ok(()), Ok(None) => {
3516 if t0.elapsed() > Duration::from_secs(2) {
3517 panic!("try_wait never observed post-abort completion");
3518 }
3519 thread::sleep(Duration::from_millis(5));
3520 }
3521 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
3522 break Err(e);
3523 }
3524 Err(e) => panic!("unexpected try_wait error: {e}"),
3525 }
3526 };
3527 assert!(result.is_err(), "try_wait must surface UnexpectedEof");
3529
3530 let _ = std::fs::remove_file(&sock_path);
3531 let _ = std::fs::remove_dir(&dir);
3532 std::mem::forget(listener_handle);
3533 }
3534
3535 #[test]
3539 fn signaler_clone_shares_underlying_socket() {
3540 let dir = short_sock_dir();
3541 let sock_path: PathBuf = dir.join("exec.sock");
3542 let listener = UnixListener::bind(&sock_path).unwrap();
3543 let listener_handle = thread::spawn(move || {
3544 if let Ok((mut conn, _)) = listener.accept() {
3545 let mut hdr = [0u8; 5];
3546 if conn.read_exact(&mut hdr).is_ok() {
3547 let len = u32::from_be_bytes([hdr[1], hdr[2], hdr[3], hdr[4]]) as usize;
3548 let mut payload = vec![0u8; len];
3549 let _ = conn.read_exact(&mut payload);
3550 }
3551 thread::sleep(Duration::from_secs(60));
3552 }
3553 });
3554
3555 let builder = ExecBuilder::new(sock_path.clone()).argv(["true"]);
3556 let child = builder.spawn().expect("spawn");
3557 let s1 = child.signaler();
3558 let s2 = s1.clone(); drop(s1); thread::spawn(move || {
3562 thread::sleep(Duration::from_millis(50));
3563 let _ = s2.abort();
3564 });
3565
3566 let res = child.wait();
3567 assert!(res.is_err(), "cloned signaler's abort must unblock wait");
3568
3569 let _ = std::fs::remove_file(&sock_path);
3570 let _ = std::fs::remove_dir(&dir);
3571 std::mem::forget(listener_handle);
3572 }
3573}
3574
3575#[cfg(test)]
3588mod frame_proptests {
3589 use super::{read_frame, send_frame};
3590 use proptest::prelude::*;
3591 use std::io::Write;
3592 use std::os::unix::net::UnixStream;
3593 use std::sync::{Arc, Mutex};
3594
3595 proptest! {
3596 #![proptest_config(ProptestConfig::with_cases(512))]
3597
3598 #[test]
3602 fn frame_roundtrips(kind in any::<u8>(), payload in prop::collection::vec(any::<u8>(), 0..8192)) {
3603 let (tx, mut rx) = UnixStream::pair().unwrap();
3604 let tx = Arc::new(Mutex::new(tx));
3605 send_frame(&tx, kind, &payload).unwrap();
3606 let (got_kind, got_payload) = read_frame(&mut rx).unwrap();
3607 prop_assert_eq!(got_kind, kind);
3608 prop_assert_eq!(got_payload, payload);
3609 }
3610
3611 #[test]
3616 fn read_frame_tolerates_garbage(raw in prop::collection::vec(any::<u8>(), 0..512)) {
3617 let (mut tx, mut rx) = UnixStream::pair().unwrap();
3618 tx.write_all(&raw).ok();
3619 drop(tx); if let Ok((_kind, payload)) = read_frame(&mut rx) {
3623 prop_assert!(payload.len() <= 16 * 1024 * 1024);
3624 }
3625 }
3626 }
3627
3628 const CAP: usize = 16 * 1024 * 1024;
3629
3630 #[test]
3634 fn read_frame_length_cap_boundary() {
3635 use std::io::ErrorKind;
3636
3637 let (mut tx, mut rx) = UnixStream::pair().unwrap();
3639 let mut hdr = [0u8; 5];
3640 hdr[0] = 1; hdr[1..5].copy_from_slice(&((CAP + 1) as u32).to_be_bytes());
3642 tx.write_all(&hdr).unwrap();
3643 drop(tx);
3644 let err = read_frame(&mut rx).unwrap_err();
3645 assert_eq!(
3646 err.kind(),
3647 ErrorKind::InvalidData,
3648 "over-cap must be InvalidData"
3649 );
3650 assert!(err.to_string().contains("16 MiB"), "got: {err}");
3651
3652 let (tx, mut rx) = UnixStream::pair().unwrap();
3656 let tx = Arc::new(Mutex::new(tx));
3657 let writer = std::thread::spawn(move || {
3658 send_frame(&tx, 7, &vec![0xabu8; CAP]).unwrap();
3659 });
3660 let (kind, payload) = read_frame(&mut rx).unwrap();
3661 writer.join().unwrap();
3662 assert_eq!(kind, 7);
3663 assert_eq!(payload.len(), CAP);
3664 assert!(payload.iter().all(|&b| b == 0xab));
3665 }
3666}