1use std::os::fd::RawFd;
43use std::path::Path;
44
45use detcore_model::HERMIT_POLICY_REFUSAL_EXIT;
46use reverie::Guest;
47use reverie::syscalls::Syscall;
48use reverie::syscalls::SyscallInfo;
49use reverie::syscalls::Sysno;
50
51use crate::record_or_replay::RecordOrReplay;
52use crate::syscalls::scm_rights::delivered_descriptors;
53use crate::tool_global::unrecoverable_shutdown;
54use crate::tool_local::Detcore;
55
56static BACKEND_ADVICE: std::sync::OnceLock<&'static str> = std::sync::OnceLock::new();
58
59pub fn set_backend_advice(advice: &'static str) {
63 let _ = BACKEND_ADVICE.set(advice);
64}
65
66fn refusal(what: &str, fd: RawFd, how: &str) -> String {
69 let mut refusal = format!(
70 "hermit: refusing a guest that holds {what} (descriptor {fd}, {how}): this backend \
71 completes process exits asynchronously, so no other guest runs until an exit is \
72 complete, and a guest serving that descriptor could make another guest's exit wait \
73 forever."
74 );
75 if let Some(advice) = BACKEND_ADVICE.get() {
76 refusal.push(' ');
77 refusal.push_str(advice);
78 }
79 refusal
80}
81
82const MISC_MAJOR: u64 = 10;
84const FUSE_MINOR: u64 = 229;
86const CUSE_MINOR: u64 = 203;
88
89pub const SECCOMP_NOTIFY_LINK: &str = "anon_inode:seccomp notify";
91
92fn device_numbers(rdev: u64) -> (u64, u64) {
94 let major = ((rdev >> 8) & 0xfff) | ((rdev >> 32) & !0xfff);
95 let minor = (rdev & 0xff) | ((rdev >> 12) & !0xff);
96 (major, minor)
97}
98
99pub fn device_exit_dependency(rdev: u64) -> Option<&'static str> {
102 match device_numbers(rdev) {
103 (MISC_MAJOR, FUSE_MINOR) => Some("a FUSE device (/dev/fuse)"),
104 (MISC_MAJOR, CUSE_MINOR) => Some("a CUSE device (/dev/cuse)"),
105 _ => None,
106 }
107}
108
109pub fn descriptor_exit_dependency(fd_link: &Path) -> Option<&'static str> {
113 descriptor_exit_dependency_with(fd_link, |number, args| {
116 let result =
119 unsafe { crate::util::raw_syscall(number as libc::c_long, args.map(|arg| arg as u64)) };
120 reverie::Errno::from_ret(result as usize)
121 })
122}
123
124fn descriptor_exit_dependency_with(
127 fd_link: &Path,
128 mut execute: impl FnMut(Sysno, [usize; 6]) -> Result<usize, reverie::Errno>,
129) -> Option<&'static str> {
130 use std::os::unix::ffi::OsStrExt;
131
132 let link = std::ffi::CString::new(fd_link.as_os_str().as_bytes()).ok()?;
133 let mut target = [0_u8; 64];
134 let length = execute(
135 Sysno::readlinkat,
136 [
137 libc::AT_FDCWD as usize,
138 link.as_ptr() as usize,
139 target.as_mut_ptr() as usize,
140 target.len(),
141 0,
142 0,
143 ],
144 );
145 if let Ok(length) = length
146 && &target[..length] == SECCOMP_NOTIFY_LINK.as_bytes()
147 {
148 return Some("a seccomp user-notification listener");
149 }
150 let mut metadata: libc::stat = unsafe { std::mem::zeroed() };
152 let status = execute(
153 crate::arch::FSTATAT,
154 [
155 libc::AT_FDCWD as usize,
156 link.as_ptr() as usize,
157 (&raw mut metadata) as usize,
158 0,
159 0,
160 0,
161 ],
162 );
163 if status != Ok(0) || metadata.st_mode & libc::S_IFMT != libc::S_IFCHR {
164 return None;
165 }
166 device_exit_dependency(metadata.st_rdev)
167}
168
169pub fn held_exit_dependency(
177 fd_directory: &Path,
178) -> std::io::Result<Option<(String, &'static str)>> {
179 crate::util::find_in_directory(fd_directory, |name| {
180 descriptor_exit_dependency(&fd_directory.join(name))
181 .map(|what| (name.to_string_lossy().into_owned(), what))
182 })
183}
184
185impl<T: RecordOrReplay> Detcore<T> {
186 pub(crate) async fn refuse_exit_dependencies_in_view<G: Guest<Self>>(
193 &self,
194 guest: &mut G,
195 call: &Syscall,
196 ret: i64,
197 ) -> Result<(), reverie::Error> {
198 let entry = crate::entry_view(guest, call);
199 let view = entry
200 .as_deref()
201 .map(|entry| (entry, entry.reinstate_returned(&mut guest.memory())));
202 self.refuse_exit_dependencies(guest, call, ret).await;
203 crate::restore_entry_view(guest, view)
204 }
205
206 pub(crate) async fn refuse_exit_dependencies<G: Guest<Self>>(
211 &self,
212 guest: &mut G,
213 call: &Syscall,
214 ret: i64,
215 ) {
216 if !self.cfg.backend.process_exits_complete_asynchronously || ret < 0 {
217 return;
218 }
219 let (arrived, how): (Vec<RawFd>, &str) = match call {
220 Syscall::Openat(_) => (vec![ret as RawFd], "opened"),
221 #[cfg(target_arch = "x86_64")]
222 Syscall::Open(_) => (vec![ret as RawFd], "opened"),
223 _ if call.number() == Sysno::openat2 => (vec![ret as RawFd], "opened"),
224 _ if call.number() == Sysno::pidfd_getfd => {
225 (vec![ret as RawFd], "received through pidfd_getfd")
226 }
227 Syscall::Recvmsg(_) | Syscall::Recvmmsg(_) => {
228 let Some(received) = delivered_descriptors(&guest.memory(), call, ret) else {
229 return;
230 };
231 (received, "received through SCM_RIGHTS")
232 }
233 _ => return,
234 };
235 let pid = guest.pid().as_raw();
236 for fd in arrived {
237 let Some(what) = descriptor_exit_dependency_with(
238 Path::new(&format!("/proc/{pid}/fd/{fd}")),
239 |number, args| {
240 unsafe { guest.tool_syscall(number, args) }
245 },
246 ) else {
247 continue;
248 };
249 eprintln!("{}", refusal(what, fd, how));
250 unrecoverable_shutdown(guest, HERMIT_POLICY_REFUSAL_EXIT).await
251 }
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use std::os::fd::AsRawFd;
258
259 use super::*;
260 use crate::syscalls::scm_rights::received_descriptors;
261
262 fn makedev(major: u64, minor: u64) -> u64 {
264 ((major & 0xfff) << 8) | ((major & !0xfff) << 32) | (minor & 0xff) | ((minor & !0xff) << 12)
265 }
266
267 #[test]
270 fn descriptor_inspection_keeps_operands_results_and_failure_order() {
271 use reverie::Errno;
272
273 let cases = [
274 (
275 Ok(SECCOMP_NOTIFY_LINK.as_bytes()),
276 Ok(0),
277 libc::S_IFCHR,
278 makedev(10, 229),
279 Some("a seccomp user-notification listener"),
280 1,
281 ),
282 (
283 Ok(b"/dev/ordinary".as_slice()),
284 Ok(0),
285 libc::S_IFCHR,
286 makedev(10, 229),
287 Some("a FUSE device (/dev/fuse)"),
288 2,
289 ),
290 (
291 Ok(b"/dev/ordinary".as_slice()),
292 Ok(0),
293 libc::S_IFCHR,
294 makedev(10, 203),
295 Some("a CUSE device (/dev/cuse)"),
296 2,
297 ),
298 (
299 Err(Errno::EACCES),
300 Ok(0),
301 libc::S_IFCHR,
302 makedev(10, 229),
303 Some("a FUSE device (/dev/fuse)"),
304 2,
305 ),
306 (
307 Err(Errno::EINTR),
308 Ok(0),
309 libc::S_IFREG,
310 makedev(10, 229),
311 None,
312 2,
313 ),
314 (
315 Ok(b"anon_inode:seccomp notify suffix".as_slice()),
316 Ok(0),
317 libc::S_IFCHR,
318 makedev(10, 232),
319 None,
320 2,
321 ),
322 (
323 Ok(b"".as_slice()),
324 Err(Errno::EINTR),
325 libc::S_IFCHR,
326 makedev(10, 229),
327 None,
328 2,
329 ),
330 (
331 Err(Errno::ENOENT),
332 Err(Errno::EBADF),
333 libc::S_IFCHR,
334 makedev(10, 229),
335 None,
336 2,
337 ),
338 (
339 Ok(b"/dev/ordinary".as_slice()),
340 Ok(1),
341 libc::S_IFCHR,
342 makedev(10, 229),
343 None,
344 2,
345 ),
346 (
347 Ok(&[b'x'; 64]),
348 Ok(0),
349 libc::S_IFCHR,
350 makedev(10, 203),
351 Some("a CUSE device (/dev/cuse)"),
352 2,
353 ),
354 ];
355 for (link_result, stat_result, mode, device, expected, expected_calls) in cases {
356 let mut calls = 0;
357 let mut path_address = None;
358 let actual =
359 descriptor_exit_dependency_with(Path::new("/proc/123/fd/456"), |number, args| {
360 calls += 1;
361 assert_eq!(args[0], libc::AT_FDCWD as usize);
362 assert_eq!(&args[4..], &[0, 0]);
363 let path = unsafe { std::ffi::CStr::from_ptr(args[1] as *const libc::c_char) };
366 assert_eq!(path.to_bytes(), b"/proc/123/fd/456");
367 if let Some(address) = path_address {
368 assert_eq!(args[1], address);
369 } else {
370 path_address = Some(args[1]);
371 }
372 match number {
373 Sysno::readlinkat => {
374 assert_eq!(calls, 1);
375 assert_eq!(args[3], 64);
376 let bytes = link_result?;
377 unsafe {
380 std::ptr::copy_nonoverlapping(
381 bytes.as_ptr(),
382 args[2] as *mut u8,
383 bytes.len(),
384 );
385 }
386 Ok(bytes.len())
387 }
388 Sysno::newfstatat => {
389 assert_eq!(calls, 2);
390 assert_eq!(args[3], 0, "follow the descriptor's target");
391 unsafe {
395 let output = &mut *(args[2] as *mut libc::stat);
396 output.st_mode = mode;
397 output.st_rdev = device;
398 }
399 stat_result
400 }
401 other => panic!("unexpected Tool syscall {other:?}"),
402 }
403 });
404 assert_eq!(actual, expected);
405 assert_eq!(calls, expected_calls);
406 }
407 }
408
409 #[test]
410 fn an_invalid_descriptor_path_executes_no_inspection() {
411 use std::os::unix::ffi::OsStrExt;
412
413 let path = Path::new(std::ffi::OsStr::from_bytes(b"/proc/self/fd/1\0suffix"));
414 assert_eq!(
415 descriptor_exit_dependency_with(path, |_, _| panic!("invalid path reached executor")),
416 None
417 );
418 }
419
420 #[test]
421 fn only_the_fuse_and_cuse_devices_are_exit_dependencies() {
422 assert_eq!(
423 device_exit_dependency(makedev(10, 229)),
424 Some("a FUSE device (/dev/fuse)")
425 );
426 assert_eq!(
427 device_exit_dependency(makedev(10, 203)),
428 Some("a CUSE device (/dev/cuse)")
429 );
430 for (major, minor) in [
433 (10, 232),
434 (10, 200),
435 (10, 125),
436 (1, 229),
437 (4, 203),
438 (259, 229),
439 ] {
440 assert_eq!(
441 device_exit_dependency(makedev(major, minor)),
442 None,
443 "{major}:{minor}"
444 );
445 }
446 assert_eq!(device_numbers(makedev(4095, 1 << 19)), (4095, 1 << 19));
448 }
449
450 #[test]
451 fn ordinary_descriptors_are_not_exit_dependencies() {
452 let file = tempfile::tempfile().unwrap();
453 let null = std::fs::File::open("/dev/null").unwrap();
454 let (left, _right) = std::os::unix::net::UnixStream::pair().unwrap();
455 for fd in [file.as_raw_fd(), null.as_raw_fd(), left.as_raw_fd()] {
456 assert_eq!(
457 descriptor_exit_dependency(Path::new(&format!("/proc/self/fd/{fd}"))),
458 None,
459 "fd {fd}"
460 );
461 }
462 assert_eq!(
464 descriptor_exit_dependency(Path::new("/proc/self/fd/987654")),
465 None
466 );
467 }
468
469 #[test]
470 fn a_received_seccomp_listener_is_recognised_by_its_anon_inode() {
471 const SECCOMP_SET_MODE_FILTER: libc::c_ulong = 1;
475 const SECCOMP_FILTER_FLAG_NEW_LISTENER: libc::c_ulong = 1 << 3;
476 const SECCOMP_RET_ALLOW: u32 = 0x7fff_0000;
477 let (sender, receiver) = std::os::unix::net::UnixDatagram::pair().unwrap();
478 let filter = [libc::sock_filter {
479 code: (libc::BPF_RET | libc::BPF_K) as u16,
480 jt: 0,
481 jf: 0,
482 k: SECCOMP_RET_ALLOW,
483 }];
484 let program = libc::sock_fprog {
485 len: 1,
486 filter: filter.as_ptr().cast_mut(),
487 };
488 let mut payload = [0u8; 1];
489 let mut iov = libc::iovec {
490 iov_base: payload.as_mut_ptr().cast(),
491 iov_len: 1,
492 };
493 let mut control = [0u64; 3];
494 let mut message: libc::msghdr = unsafe { std::mem::zeroed() };
495 message.msg_iov = &mut iov;
496 message.msg_iovlen = 1;
497 message.msg_control = control.as_mut_ptr().cast();
498 message.msg_controllen = 24;
499 let child = unsafe { libc::fork() };
502 assert!(child >= 0, "fork: {}", std::io::Error::last_os_error());
503 if child == 0 {
504 unsafe {
505 if libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0 {
506 libc::_exit(2);
507 }
508 let listener = libc::syscall(
509 libc::SYS_seccomp,
510 SECCOMP_SET_MODE_FILTER,
511 SECCOMP_FILTER_FLAG_NEW_LISTENER,
512 &program as *const libc::sock_fprog,
513 );
514 if listener < 0 {
515 libc::_exit(3);
516 }
517 let header = control.as_mut_ptr().cast::<libc::cmsghdr>();
518 (*header).cmsg_len = 20;
519 (*header).cmsg_level = libc::SOL_SOCKET;
520 (*header).cmsg_type = libc::SCM_RIGHTS;
521 *control.as_mut_ptr().add(2).cast::<libc::c_int>() = listener as libc::c_int;
522 if libc::sendmsg(sender.as_raw_fd(), &message, 0) != 1 {
523 libc::_exit(4);
524 }
525 libc::_exit(0);
526 }
527 }
528 let mut status = 0;
529 assert_eq!(unsafe { libc::waitpid(child, &mut status, 0) }, child);
530 assert_eq!(libc::WEXITSTATUS(status), 0, "child status {status:#x}");
531 let mut payload = [0u8; 1];
532 let mut iov = libc::iovec {
533 iov_base: payload.as_mut_ptr().cast(),
534 iov_len: 1,
535 };
536 let mut control = [0u8; 64];
537 let mut message: libc::msghdr = unsafe { std::mem::zeroed() };
538 message.msg_iov = &mut iov;
539 message.msg_iovlen = 1;
540 message.msg_control = control.as_mut_ptr().cast();
541 #[cfg(target_env = "musl")]
544 {
545 message.msg_controllen = control.len().try_into().unwrap();
546 }
547 #[cfg(not(target_env = "musl"))]
548 {
549 message.msg_controllen = control.len();
550 }
551 let received =
552 unsafe { libc::recvmsg(receiver.as_raw_fd(), &mut message, libc::MSG_CMSG_CLOEXEC) };
553 assert_eq!(received, 1, "recvmsg: {}", std::io::Error::last_os_error());
554 #[cfg(target_env = "musl")]
555 let control_len = usize::try_from(message.msg_controllen).unwrap();
556 #[cfg(not(target_env = "musl"))]
557 let control_len = message.msg_controllen;
558 let descriptors = received_descriptors(&control[..control_len]);
559 assert_eq!(descriptors.len(), 1, "{descriptors:?}");
560 let listener = unsafe {
561 <std::os::fd::OwnedFd as std::os::fd::FromRawFd>::from_raw_fd(descriptors[0])
562 };
563 assert_eq!(
564 descriptor_exit_dependency(Path::new(&format!(
565 "/proc/self/fd/{}",
566 listener.as_raw_fd()
567 ))),
568 Some("a seccomp user-notification listener")
569 );
570 }
571
572 #[test]
573 fn a_fuse_device_descriptor_is_recognised_by_its_number() {
574 let Ok(fuse) = std::fs::File::open("/dev/fuse") else {
575 eprintln!("skipping: /dev/fuse cannot be opened on this host");
576 return;
577 };
578 assert_eq!(
579 descriptor_exit_dependency(Path::new(&format!("/proc/self/fd/{}", fuse.as_raw_fd()))),
580 Some("a FUSE device (/dev/fuse)")
581 );
582 assert_eq!(
584 held_exit_dependency(Path::new("/proc/self/fd")).unwrap(),
585 Some((fuse.as_raw_fd().to_string(), "a FUSE device (/dev/fuse)"))
586 );
587 }
588
589 #[test]
590 fn the_refusal_names_the_capability_and_ends_with_the_backend_advice() {
591 let text = refusal("a FUSE device (/dev/fuse)", 5, "opened");
592 assert!(
593 text.starts_with(
594 "hermit: refusing a guest that holds a FUSE device (/dev/fuse) (descriptor 5, \
595 opened): this backend completes process exits asynchronously"
596 ),
597 "{text}"
598 );
599 set_backend_advice("Backend X refused it; try backend Y.");
600 assert!(
601 refusal("a FUSE device (/dev/fuse)", 5, "opened")
602 .ends_with("wait forever. Backend X refused it; try backend Y."),
603 );
604 }
605
606 #[test]
607 fn the_startup_scan_refuses_an_unlistable_directory() {
608 assert!(held_exit_dependency(Path::new("/nonexistent-fd-directory")).is_err());
609 }
610}