1use std::collections::{HashMap, HashSet};
6use std::future::Future;
7use std::io;
8use std::net::IpAddr;
9use std::os::unix::io::{AsRawFd, OwnedFd, RawFd};
10use std::pin::Pin;
11use std::sync::Arc;
12
13use crate::error::NotifError;
14use crate::arch;
15use crate::sys::structs::{
16 SeccompNotif, SeccompNotifAddfd, SeccompNotifResp,
17 SECCOMP_ADDFD_FLAG_SEND, SECCOMP_IOCTL_NOTIF_ADDFD, SECCOMP_IOCTL_NOTIF_ID_VALID, SECCOMP_IOCTL_NOTIF_RECV,
18 SECCOMP_IOCTL_NOTIF_SEND, SECCOMP_IOCTL_NOTIF_SET_FLAGS,
19 SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP, SECCOMP_USER_NOTIF_FLAG_CONTINUE,
20 ENOMEM,
21};
22
23pub struct OnInjectSuccess(pub Box<dyn FnOnce(i32) + Send + Sync>);
34
35impl std::fmt::Debug for OnInjectSuccess {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 f.write_str("OnInjectSuccess(<callback>)")
38 }
39}
40
41impl OnInjectSuccess {
42 pub fn new<F: FnOnce(i32) + Send + Sync + 'static>(f: F) -> Self {
43 Self(Box::new(f))
44 }
45}
46
47pub struct Deferred(Pin<Box<dyn Future<Output = NotifAction> + Send + 'static>>);
64
65unsafe impl Sync for Deferred {}
75
76impl std::fmt::Debug for Deferred {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 f.write_str("Deferred(<future>)")
79 }
80}
81
82impl Deferred {
83 pub fn new<F: Future<Output = NotifAction> + Send + 'static>(f: F) -> Self {
84 Self(Box::pin(f))
85 }
86
87 pub async fn run(self) -> NotifAction {
90 self.0.await
91 }
92}
93
94#[derive(Debug)]
96pub enum NotifAction {
97 Continue,
99 Errno(i32),
101 InjectFd { srcfd: RawFd, targetfd: i32 },
103 InjectFdSend { srcfd: OwnedFd, newfd_flags: u32 },
108 InjectFdSendTracked {
113 srcfd: OwnedFd,
114 newfd_flags: u32,
115 on_success: OnInjectSuccess,
116 },
117 ReturnValue(i64),
119 Hold,
121 Kill { sig: i32, pgid: i32 },
124 Defer(Deferred),
129}
130
131impl NotifAction {
132 pub fn defer<F: Future<Output = NotifAction> + Send + 'static>(fut: F) -> Self {
135 NotifAction::Defer(Deferred::new(fut))
136 }
137
138 pub fn inject_bytes(content: &[u8]) -> NotifAction {
153 match content_memfd(content, true) {
154 Ok(fd) => NotifAction::InjectFdSend {
155 srcfd: fd,
156 newfd_flags: libc::O_CLOEXEC as u32,
157 },
158 Err(_) => NotifAction::Errno(libc::EIO),
159 }
160 }
161}
162
163pub fn content_memfd(content: &[u8], seal: bool) -> io::Result<OwnedFd> {
177 use std::io::{Seek, SeekFrom, Write};
178 use std::os::unix::io::FromRawFd;
179
180 let flags = if seal {
181 (libc::MFD_CLOEXEC | libc::MFD_ALLOW_SEALING) as u32
182 } else {
183 libc::MFD_CLOEXEC as u32
184 };
185 let memfd = crate::sys::syscall::memfd_create("sandlock-content", flags)?;
186
187 {
190 let raw = memfd.as_raw_fd();
191 let mut file = unsafe { std::fs::File::from_raw_fd(raw) };
192 let res = file
193 .write_all(content)
194 .and_then(|()| file.seek(SeekFrom::Start(0)).map(|_| ()));
195 std::mem::forget(file); res?;
197 }
198
199 if seal {
200 let seals =
202 libc::F_SEAL_SEAL | libc::F_SEAL_WRITE | libc::F_SEAL_GROW | libc::F_SEAL_SHRINK;
203 unsafe { libc::fcntl(memfd.as_raw_fd(), libc::F_ADD_SEALS, seals) };
204 }
205
206 Ok(memfd)
207}
208
209fn finalize_deferred(action: NotifAction) -> NotifAction {
214 match action {
215 NotifAction::Defer(_) => NotifAction::Errno(libc::EIO),
216 other => other,
217 }
218}
219
220#[derive(Debug, Clone)]
228pub enum PortAllow {
229 Any,
231 Specific(HashSet<u16>),
233}
234
235#[derive(Debug, Clone)]
237pub enum NetworkPolicy {
238 Unrestricted,
245 AllowList {
248 per_ip: HashMap<IpAddr, PortAllow>,
251 cidrs: Vec<(crate::network::IpCidr, PortAllow)>,
255 any_ip_ports: HashSet<u16>,
258 },
259 DenyList {
262 cidrs: Vec<(crate::network::IpCidr, PortAllow)>,
265 any_ip_ports: HashSet<u16>,
267 deny_all: bool,
270 },
271}
272
273impl NetworkPolicy {
274 pub fn allows(&self, ip: IpAddr, port: u16) -> bool {
276 let ip = ip.to_canonical();
281 match self {
282 NetworkPolicy::Unrestricted => true,
283 NetworkPolicy::AllowList { per_ip, cidrs, any_ip_ports } => {
284 if any_ip_ports.contains(&port) {
285 return true;
286 }
287 match per_ip.get(&ip) {
288 Some(PortAllow::Any) => return true,
289 Some(PortAllow::Specific(s)) if s.contains(&port) => return true,
290 _ => {}
291 }
292 for (net, allowed) in cidrs {
293 if net.contains(ip) {
294 match allowed {
295 PortAllow::Any => return true,
296 PortAllow::Specific(s) => {
297 if s.contains(&port) {
298 return true;
299 }
300 }
301 }
302 }
303 }
304 false
305 }
306 NetworkPolicy::DenyList { cidrs, any_ip_ports, deny_all } => {
307 if *deny_all {
308 return false;
309 }
310 if any_ip_ports.contains(&port) {
311 return false;
312 }
313 for (net, denied) in cidrs {
314 if net.contains(ip) {
315 match denied {
316 PortAllow::Any => return false,
317 PortAllow::Specific(s) => {
318 if s.contains(&port) {
319 return false;
320 }
321 }
322 }
323 }
324 }
325 true
326 }
327 }
328 }
329}
330
331pub(crate) fn is_path_denied_for_notif(
342 policy_fn_state: &super::state::PolicyFnState,
343 notif: &SeccompNotif,
344 notif_fd: RawFd,
345) -> bool {
346 if let Some(path) = resolve_path_for_notif(notif, notif_fd) {
347 if is_denied_with_symlink_resolve(policy_fn_state, &path) {
348 return true;
349 }
350 }
351 if let Some(path) = resolve_second_path_for_notif(notif, notif_fd) {
353 if is_denied_with_symlink_resolve(policy_fn_state, &path) {
354 return true;
355 }
356 }
357 false
358}
359
360fn is_denied_with_symlink_resolve(
366 policy_fn_state: &super::state::PolicyFnState,
367 path: &str,
368) -> bool {
369 if policy_fn_state.is_path_denied(path) {
371 return true;
372 }
373 if let Ok(real) = std::fs::canonicalize(path) {
375 if policy_fn_state.is_path_denied(&real.to_string_lossy()) {
376 return true;
377 }
378 if let Some(id) = super::state::file_id_of_path(&real.to_string_lossy()) {
384 if policy_fn_state.is_id_denied(&id) {
385 return true;
386 }
387 }
388 }
389 false
390}
391
392const RESOLVE_NO_MAGICLINKS: u64 = 0x02;
396
397#[repr(C)]
399struct OpenHow {
400 flags: u64,
401 mode: u64,
402 resolve: u64,
403}
404
405fn last_errno(fallback: i32) -> i32 {
406 io::Error::last_os_error().raw_os_error().unwrap_or(fallback)
407}
408
409fn openat2_at(dirfd: RawFd, path: &std::ffi::CStr, flags: u64, mode: u64, resolve: u64)
411 -> Result<OwnedFd, i32>
412{
413 use std::os::unix::io::FromRawFd;
414 let how = OpenHow { flags, mode, resolve };
415 let fd = unsafe {
416 libc::syscall(
417 arch::SYS_OPENAT2,
418 dirfd,
419 path.as_ptr(),
420 &how as *const OpenHow,
421 std::mem::size_of::<OpenHow>(),
422 )
423 } as i32;
424 if fd < 0 {
425 Err(last_errno(libc::ENOENT))
426 } else {
427 Ok(unsafe { OwnedFd::from_raw_fd(fd) })
428 }
429}
430
431fn open_base_dir(pid: u32, dirfd: i64) -> Result<OwnedFd, i32> {
435 use std::os::unix::io::FromRawFd;
436 if dirfd as i32 == libc::AT_FDCWD {
437 let cwd = std::ffi::CString::new(format!("/proc/{}/cwd", pid)).map_err(|_| libc::EINVAL)?;
438 let fd = unsafe {
439 libc::open(cwd.as_ptr(), libc::O_PATH | libc::O_DIRECTORY | libc::O_CLOEXEC)
440 };
441 if fd < 0 {
442 return Err(last_errno(libc::EACCES));
443 }
444 Ok(unsafe { OwnedFd::from_raw_fd(fd) })
445 } else {
446 dup_fd_from_pid(pid, dirfd as i32).map_err(|_| libc::EBADF)
447 }
448}
449
450fn realpath_of_fd(fd: RawFd) -> Option<std::path::PathBuf> {
452 std::fs::read_link(format!("/proc/self/fd/{}", fd)).ok()
453}
454
455
456fn path_under_any(path: &std::path::Path, list: &[std::path::PathBuf]) -> bool {
457 list.iter().any(|p| path.starts_with(p))
458}
459
460fn deny_open_verdict(
467 realpath: &std::path::Path,
468 flags: u64,
469 policy: &NotifPolicy,
470 pfs: &super::state::PolicyFnState,
471) -> Option<i32> {
472 if pfs.is_path_denied(&realpath.to_string_lossy()) {
473 return Some(libc::EACCES);
474 }
475 let acc = flags as i32 & libc::O_ACCMODE;
476 let is_write = acc == libc::O_WRONLY
477 || acc == libc::O_RDWR
478 || (flags & libc::O_TRUNC as u64) != 0
479 || (flags & libc::O_CREAT as u64) != 0;
480 let allowed = if is_write {
481 path_under_any(realpath, &policy.chroot_writable)
482 } else {
483 path_under_any(realpath, &policy.chroot_readable)
484 || path_under_any(realpath, &policy.chroot_writable)
485 };
486 if allowed { None } else { Some(libc::EACCES) }
487}
488
489struct OpenArgs {
491 dirfd: i64,
492 path_ptr: u64,
493 flags: u64,
494 mode: u64,
495 resolve: u64,
497}
498
499fn decode_open_args(notif: &SeccompNotif, notif_fd: RawFd) -> Option<OpenArgs> {
504 let a = ¬if.data.args;
505 let nr = notif.data.nr as i64;
506 if nr == libc::SYS_openat {
507 Some(OpenArgs { dirfd: a[0] as i64, path_ptr: a[1], flags: a[2], mode: a[3], resolve: 0 })
508 } else if nr == arch::SYS_OPENAT2 {
509 let how_ptr = a[2];
512 let want = (a[3] as usize).min(std::mem::size_of::<OpenHow>());
513 if how_ptr == 0 || want < 16 {
514 return None; }
516 let bytes = read_child_mem(notif_fd, notif.id, notif.pid, how_ptr, want).ok()?;
517 if bytes.len() < 16 {
518 return None;
519 }
520 let flags = u64::from_ne_bytes(bytes[0..8].try_into().ok()?);
521 let mode = u64::from_ne_bytes(bytes[8..16].try_into().ok()?);
522 let resolve = if bytes.len() >= 24 {
523 u64::from_ne_bytes(bytes[16..24].try_into().ok()?)
524 } else {
525 0
526 };
527 Some(OpenArgs { dirfd: a[0] as i64, path_ptr: a[1], flags, mode, resolve })
528 } else {
529 Some(OpenArgs { dirfd: libc::AT_FDCWD as i64, path_ptr: a[0], flags: a[1], mode: a[2], resolve: 0 })
531 }
532}
533
534fn inject_open_result(raw_fd: i32, flags: u64) -> NotifAction {
537 use std::os::unix::io::FromRawFd;
538 if raw_fd < 0 {
539 return NotifAction::Errno(last_errno(libc::EACCES));
540 }
541 let owned = unsafe { OwnedFd::from_raw_fd(raw_fd) };
542 let newfd_flags = if flags & libc::O_CLOEXEC as u64 != 0 {
543 libc::O_CLOEXEC as u32
544 } else {
545 0
546 };
547 NotifAction::InjectFdSend { srcfd: owned, newfd_flags }
548}
549
550fn reopen_existing_on_behalf(
554 probe: OwnedFd,
555 flags: u64,
556 policy: &NotifPolicy,
557 pfs: &super::state::PolicyFnState,
558) -> NotifAction {
559 if (flags & libc::O_CREAT as u64) != 0 && (flags & libc::O_EXCL as u64) != 0 {
561 return NotifAction::Errno(libc::EEXIST);
562 }
563 let realpath = match realpath_of_fd(probe.as_raw_fd()) {
564 Some(p) => p,
565 None => return NotifAction::Errno(libc::EACCES),
566 };
567 if let Some(errno) = deny_open_verdict(&realpath, flags, policy, pfs) {
568 return NotifAction::Errno(errno);
569 }
570 if let Some(id) = super::state::file_id_of_fd(probe.as_raw_fd()) {
575 if pfs.is_id_denied(&id) {
576 return NotifAction::Errno(libc::EACCES);
577 }
578 }
579 let reopen_flags =
581 flags as i32 & !(libc::O_CREAT | libc::O_EXCL | libc::O_PATH | libc::O_NOFOLLOW);
582 let proc_path = match std::ffi::CString::new(format!("/proc/self/fd/{}", probe.as_raw_fd())) {
583 Ok(c) => c,
584 Err(_) => return NotifAction::Errno(libc::EIO),
585 };
586 let fd = unsafe { libc::open(proc_path.as_ptr(), reopen_flags) };
587 inject_open_result(fd, flags)
588}
589
590fn create_new_on_behalf(
594 base: &OwnedFd,
595 path: &str,
596 flags: u64,
597 mode: u64,
598 resolve: u64,
599 policy: &NotifPolicy,
600 pfs: &super::state::PolicyFnState,
601) -> NotifAction {
602 let p = std::path::Path::new(path);
603 let file_name = match p.file_name() {
604 Some(f) => f,
605 None => return NotifAction::Errno(libc::ENOENT),
606 };
607 let parent = p.parent().unwrap_or(std::path::Path::new("."));
608 let parent_str = match parent.to_str() {
609 Some("") | None => ".",
610 Some(s) => s,
611 };
612 let c_parent = match std::ffi::CString::new(parent_str) {
613 Ok(c) => c,
614 Err(_) => return NotifAction::Errno(libc::EINVAL),
615 };
616 let parent_fd = match openat2_at(
617 base.as_raw_fd(),
618 &c_parent,
619 (libc::O_PATH | libc::O_DIRECTORY | libc::O_CLOEXEC) as u64,
620 0,
621 RESOLVE_NO_MAGICLINKS | resolve,
622 ) {
623 Ok(f) => f,
624 Err(e) => return NotifAction::Errno(e),
625 };
626 let parent_real = match realpath_of_fd(parent_fd.as_raw_fd()) {
627 Some(p) => p,
628 None => return NotifAction::Errno(libc::EACCES),
629 };
630 if let Some(errno) = deny_open_verdict(&parent_real.join(file_name), flags, policy, pfs) {
631 return NotifAction::Errno(errno);
632 }
633 let c_name = match std::ffi::CString::new(file_name.as_encoded_bytes()) {
634 Ok(c) => c,
635 Err(_) => return NotifAction::Errno(libc::EINVAL),
636 };
637 let create_flags = flags as i32 & !(libc::O_PATH | libc::O_NOFOLLOW);
638 let fd = unsafe { libc::openat(parent_fd.as_raw_fd(), c_name.as_ptr(), create_flags, mode) };
639 inject_open_result(fd, flags)
640}
641
642fn on_behalf_open_for_deny(
649 notif: &SeccompNotif,
650 policy: &NotifPolicy,
651 pfs: &super::state::PolicyFnState,
652 notif_fd: RawFd,
653) -> NotifAction {
654 if policy.chroot_readable.is_empty() && policy.chroot_writable.is_empty() {
658 return NotifAction::Continue;
659 }
660
661 let OpenArgs { dirfd, path_ptr, flags, mode, resolve } =
662 match decode_open_args(notif, notif_fd) {
663 Some(a) => a,
664 None => return NotifAction::Continue, };
666
667 let path = match read_child_cstr(notif_fd, notif.id, notif.pid, path_ptr, 4096) {
668 Some(p) => p,
669 None => return NotifAction::Continue, };
671 let c_path = match std::ffi::CString::new(path.clone()) {
672 Ok(c) => c,
673 Err(_) => return NotifAction::Errno(libc::EINVAL),
674 };
675 let base = match open_base_dir(notif.pid, dirfd) {
676 Ok(b) => b,
677 Err(e) => return NotifAction::Errno(e),
678 };
679
680 let probe_flags = (libc::O_PATH | libc::O_CLOEXEC) as u64 | (flags & libc::O_NOFOLLOW as u64);
683 match openat2_at(base.as_raw_fd(), &c_path, probe_flags, 0, RESOLVE_NO_MAGICLINKS | resolve) {
684 Ok(probe) => reopen_existing_on_behalf(probe, flags, policy, pfs),
685 Err(errno) if errno == libc::ENOENT && (flags & libc::O_CREAT as u64) != 0 => {
686 create_new_on_behalf(&base, &path, flags, mode, resolve, policy, pfs)
687 }
688 Err(errno) => NotifAction::Errno(errno),
689 }
690}
691
692fn tgid_of(tid: u32) -> Option<u32> {
694 let status = std::fs::read_to_string(format!("/proc/{}/status", tid)).ok()?;
695 status
696 .lines()
697 .find_map(|l| l.strip_prefix("Tgid:").and_then(|r| r.trim().parse().ok()))
698}
699
700pub(crate) fn dup_fd_from_pid(pid: u32, target_fd: i32) -> io::Result<OwnedFd> {
709 use crate::sys::syscall::{pidfd_getfd, pidfd_open};
710 let pidfd = pidfd_open(pid, 0).or_else(|e| match tgid_of(pid) {
711 Some(tgid) if tgid != pid => pidfd_open(tgid, 0),
712 _ => Err(e),
713 })?;
714 pidfd_getfd(&pidfd, target_fd, 0)
715}
716
717pub struct NotifPolicy {
723 pub max_memory_bytes: u64,
724 pub max_processes: u32,
725 pub has_memory_limit: bool,
726 pub has_net_destination_policy: bool,
735 pub has_bind_denylist: bool,
739 pub has_unix_fs_gate: bool,
745 pub has_random_seed: bool,
746 pub has_time_start: bool,
747 pub argv_safety_required: bool,
754 pub time_offset: i64,
755 pub num_cpus: Option<u32>,
756 pub port_remap: bool,
757 pub cow_enabled: bool,
758 pub chroot_root: Option<std::path::PathBuf>,
759 pub chroot_readable: Vec<std::path::PathBuf>,
761 pub chroot_writable: Vec<std::path::PathBuf>,
763 pub chroot_denied: Vec<std::path::PathBuf>,
765 pub chroot_mounts: Vec<(std::path::PathBuf, std::path::PathBuf)>,
767 pub chroot_mount_ro: Vec<std::path::PathBuf>,
772 pub deterministic_dirs: bool,
773 pub virtual_hostname: Option<String>,
774 pub has_http_acl: bool,
775 pub virtual_etc_hosts: String,
780 pub ca_inject_paths: Vec<std::path::PathBuf>,
782 pub ca_inject_pem: Option<std::sync::Arc<Vec<u8>>>,
785}
786
787impl NotifPolicy {
788 pub(crate) fn ip_connect_supervised(&self, dest_is_loopback: bool) -> bool {
804 self.has_net_destination_policy || (self.port_remap && dest_is_loopback)
805 }
806}
807
808fn recv_notif(fd: RawFd) -> io::Result<SeccompNotif> {
815 let mut notif: SeccompNotif = unsafe { std::mem::zeroed() };
816 let ret = unsafe {
817 libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_RECV as libc::c_ulong, &mut notif as *mut _)
818 };
819 if ret < 0 {
820 Err(io::Error::last_os_error())
821 } else {
822 Ok(notif)
823 }
824}
825
826enum NotifFdState {
828 Pending,
831 Empty,
834 Terminal,
839}
840
841fn probe_notif_fd(fd: RawFd) -> NotifFdState {
852 let mut pfd = libc::pollfd {
853 fd,
854 events: libc::POLLIN,
855 revents: 0,
856 };
857 let r = unsafe { libc::poll(&mut pfd, 1, 0) };
858 if r > 0 && (pfd.revents & libc::POLLIN) != 0 {
859 return NotifFdState::Pending;
860 }
861 if r < 0 || (pfd.revents & (libc::POLLHUP | libc::POLLERR | libc::POLLNVAL)) != 0 {
862 return NotifFdState::Terminal;
863 }
864 NotifFdState::Empty
865}
866
867fn respond_continue(fd: RawFd, id: u64) -> io::Result<()> {
869 let resp = SeccompNotifResp {
870 id,
871 val: 0,
872 error: 0,
873 flags: SECCOMP_USER_NOTIF_FLAG_CONTINUE,
874 };
875 send_resp_raw(fd, &resp)
876}
877
878fn respond_errno(fd: RawFd, id: u64, errno: i32) -> io::Result<()> {
880 let resp = SeccompNotifResp {
881 id,
882 val: 0,
883 error: -errno,
884 flags: 0,
885 };
886 send_resp_raw(fd, &resp)
887}
888
889fn respond_value(fd: RawFd, id: u64, val: i64) -> io::Result<()> {
891 let resp = SeccompNotifResp {
892 id,
893 val,
894 error: 0,
895 flags: 0,
896 };
897 send_resp_raw(fd, &resp)
898}
899
900fn inject_failure_resp(id: u64) -> SeccompNotifResp {
908 SeccompNotifResp {
909 id,
910 val: 0,
911 error: -libc::EACCES,
912 flags: 0,
913 }
914}
915
916fn inject_fd_and_send(fd: RawFd, id: u64, srcfd: RawFd, newfd_flags: u32) -> io::Result<i32> {
922 let addfd = SeccompNotifAddfd {
923 id,
924 flags: SECCOMP_ADDFD_FLAG_SEND,
925 srcfd: srcfd as u32,
926 newfd: 0, newfd_flags,
928 };
929 let ret = unsafe {
930 libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_ADDFD as libc::c_ulong, &addfd as *const _)
931 };
932 if ret < 0 {
933 Err(io::Error::last_os_error())
934 } else {
935 Ok(ret as i32)
936 }
937}
938
939fn inject_fd(fd: RawFd, id: u64, srcfd: RawFd, targetfd: i32) -> io::Result<()> {
942 let addfd = SeccompNotifAddfd {
943 id,
944 flags: 0,
945 srcfd: srcfd as u32,
946 newfd: targetfd as u32,
947 newfd_flags: 0,
948 };
949 let ret = unsafe {
950 libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_ADDFD as libc::c_ulong, &addfd as *const _)
951 };
952 if ret < 0 {
953 Err(io::Error::last_os_error())
954 } else {
955 Ok(())
956 }
957}
958
959fn send_resp_raw(fd: RawFd, resp: &SeccompNotifResp) -> io::Result<()> {
961 let ret = unsafe {
962 libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_SEND as libc::c_ulong, resp as *const _)
963 };
964 if ret < 0 {
965 Err(io::Error::last_os_error())
966 } else {
967 Ok(())
968 }
969}
970
971pub(crate) fn id_valid(fd: RawFd, id: u64) -> io::Result<()> {
974 let ret = unsafe {
975 libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_ID_VALID as libc::c_ulong, &id as *const _)
976 };
977 if ret < 0 {
978 Err(io::Error::last_os_error())
979 } else {
980 Ok(())
981 }
982}
983
984fn try_set_sync_wakeup(fd: RawFd) {
986 let flags: u64 = SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP as u64;
987 unsafe {
988 libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_SET_FLAGS as libc::c_ulong, &flags as *const _);
989 }
990}
991
992fn read_child_mem_vm(pid: u32, addr: u64, len: usize) -> Result<Vec<u8>, NotifError> {
998 let mut buf = vec![0u8; len];
999 let local_iov = libc::iovec {
1000 iov_base: buf.as_mut_ptr() as *mut libc::c_void,
1001 iov_len: len,
1002 };
1003 let remote_iov = libc::iovec {
1004 iov_base: addr as *mut libc::c_void,
1005 iov_len: len,
1006 };
1007 let ret = unsafe {
1008 libc::process_vm_readv(pid as i32, &local_iov, 1, &remote_iov, 1, 0)
1009 };
1010 if ret < 0 {
1011 Err(NotifError::ChildMemoryRead(io::Error::last_os_error()))
1012 } else {
1013 buf.truncate(ret as usize);
1014 Ok(buf)
1015 }
1016}
1017
1018fn write_child_mem_vm(pid: u32, addr: u64, data: &[u8]) -> Result<(), NotifError> {
1020 let local_iov = libc::iovec {
1021 iov_base: data.as_ptr() as *mut libc::c_void,
1022 iov_len: data.len(),
1023 };
1024 let remote_iov = libc::iovec {
1025 iov_base: addr as *mut libc::c_void,
1026 iov_len: data.len(),
1027 };
1028 let ret = unsafe {
1029 libc::process_vm_writev(pid as i32, &local_iov, 1, &remote_iov, 1, 0)
1030 };
1031 if ret < 0 {
1032 Err(NotifError::ChildMemoryRead(io::Error::last_os_error()))
1033 } else if (ret as usize) < data.len() {
1034 Err(NotifError::ChildMemoryRead(io::Error::new(
1035 io::ErrorKind::WriteZero,
1036 format!("short write: {} of {} bytes", ret, data.len()),
1037 )))
1038 } else {
1039 Ok(())
1040 }
1041}
1042
1043pub fn read_child_mem(
1053 notif_fd: RawFd,
1054 id: u64,
1055 pid: u32,
1056 addr: u64,
1057 len: usize,
1058) -> Result<Vec<u8>, NotifError> {
1059 id_valid(notif_fd, id).map_err(NotifError::Ioctl)?;
1060 let result = read_child_mem_vm(pid, addr, len)?;
1061 id_valid(notif_fd, id).map_err(NotifError::Ioctl)?;
1062 Ok(result)
1063}
1064
1065pub fn read_child_cstr(
1080 notif_fd: RawFd,
1081 id: u64,
1082 pid: u32,
1083 addr: u64,
1084 max_len: usize,
1085) -> Option<String> {
1086 if addr == 0 || max_len == 0 {
1087 return None;
1088 }
1089
1090 const PAGE_SIZE: u64 = 4096;
1091 let mut result = Vec::with_capacity(max_len.min(256));
1092 let mut cur = addr;
1093 while result.len() < max_len {
1094 let page_remaining = PAGE_SIZE - (cur % PAGE_SIZE);
1095 let remaining = max_len - result.len();
1096 let to_read = page_remaining.min(remaining as u64) as usize;
1097 let bytes = read_child_mem(notif_fd, id, pid, cur, to_read).ok()?;
1098 if let Some(nul) = bytes.iter().position(|&b| b == 0) {
1099 result.extend_from_slice(&bytes[..nul]);
1100 return String::from_utf8(result).ok();
1101 }
1102 result.extend_from_slice(&bytes);
1103 cur += to_read as u64;
1104 }
1105
1106 String::from_utf8(result).ok()
1107}
1108
1109pub fn write_child_mem(
1116 notif_fd: RawFd,
1117 id: u64,
1118 pid: u32,
1119 addr: u64,
1120 data: &[u8],
1121) -> Result<(), NotifError> {
1122 id_valid(notif_fd, id).map_err(NotifError::Ioctl)?;
1123 write_child_mem_vm(pid, addr, data)?;
1124 id_valid(notif_fd, id).map_err(NotifError::Ioctl)?;
1125 Ok(())
1126}
1127
1128pub fn write_child_mem_force(
1155 notif_fd: RawFd,
1156 id: u64,
1157 pid: u32,
1158 addr: u64,
1159 data: &[u8],
1160) -> Result<(), NotifError> {
1161 id_valid(notif_fd, id).map_err(NotifError::Ioctl)?;
1162 write_child_mem_proc(pid, addr, data)?;
1163 id_valid(notif_fd, id).map_err(NotifError::Ioctl)?;
1164 Ok(())
1165}
1166
1167fn write_child_mem_proc(pid: u32, addr: u64, data: &[u8]) -> Result<(), NotifError> {
1174 use std::os::unix::fs::FileExt;
1175 let mem = std::fs::OpenOptions::new()
1176 .write(true)
1177 .open(format!("/proc/{}/mem", pid))
1178 .map_err(NotifError::ChildMemoryWrite)?;
1179 mem.write_all_at(data, addr)
1180 .map_err(NotifError::ChildMemoryWrite)?;
1181 Ok(())
1182}
1183
1184const EXEC_MAX_ARG_STRLEN: usize = 32 * 4096;
1188
1189const EXEC_MAX_PTR_ENTRIES: usize = 1 << 20;
1194
1195struct ExecRewritePlan {
1198 buf: Vec<u8>,
1201 patches: Vec<(u64, u64)>,
1204}
1205
1206fn read_exec_ptr_array(
1210 read: &mut impl FnMut(u64, usize) -> Result<Vec<u8>, NotifError>,
1211 base: u64,
1212) -> Result<Vec<u64>, NotifError> {
1213 if base == 0 {
1214 return Ok(Vec::new());
1215 }
1216 const PAGE: u64 = 4096;
1217 let mut ptrs = Vec::new();
1218 let mut pending: Vec<u8> = Vec::new();
1219 let mut cur = base;
1220 loop {
1221 let chunk = (PAGE - cur % PAGE) as usize;
1222 let bytes = read(cur, chunk)?;
1223 cur += chunk as u64;
1224 pending.extend_from_slice(&bytes);
1225 let mut consumed = 0;
1226 while pending.len() - consumed >= 8 {
1227 let ptr = u64::from_ne_bytes(pending[consumed..consumed + 8].try_into().unwrap());
1228 consumed += 8;
1229 if ptr == 0 {
1230 return Ok(ptrs);
1231 }
1232 ptrs.push(ptr);
1233 if ptrs.len() >= EXEC_MAX_PTR_ENTRIES {
1234 return Ok(ptrs);
1235 }
1236 }
1237 pending.drain(..consumed);
1238 }
1239}
1240
1241fn read_exec_range(
1243 read: &mut impl FnMut(u64, usize) -> Result<Vec<u8>, NotifError>,
1244 addr: u64,
1245 len: usize,
1246) -> Result<Vec<u8>, NotifError> {
1247 let mut out = Vec::with_capacity(len);
1248 let mut cur = addr;
1249 while out.len() < len {
1250 let chunk = ((4096 - cur % 4096) as usize).min(len - out.len());
1251 out.extend_from_slice(&read(cur, chunk)?);
1252 cur += chunk as u64;
1253 }
1254 Ok(out)
1255}
1256
1257fn read_exec_cstr(
1260 read: &mut impl FnMut(u64, usize) -> Result<Vec<u8>, NotifError>,
1261 addr: u64,
1262) -> Result<Vec<u8>, NotifError> {
1263 let mut out = Vec::new();
1264 let mut cur = addr;
1265 while out.len() < EXEC_MAX_ARG_STRLEN {
1266 let chunk = ((4096 - cur % 4096) as usize).min(EXEC_MAX_ARG_STRLEN - out.len());
1267 let bytes = read(cur, chunk)?;
1268 if let Some(n) = bytes.iter().position(|&b| b == 0) {
1269 out.extend_from_slice(&bytes[..n]);
1270 return Ok(out);
1271 }
1272 out.extend_from_slice(&bytes);
1273 cur += chunk as u64;
1274 }
1275 Err(NotifError::Supervisor(format!(
1276 "exec arg string at {addr:#x} exceeds MAX_ARG_STRLEN"
1277 )))
1278}
1279
1280fn plan_exec_rewrite(
1302 read: &mut impl FnMut(u64, usize) -> Result<Vec<u8>, NotifError>,
1303 path_ptr: u64,
1304 fd_path: &[u8],
1305 argv_ptr: u64,
1306 envp_ptr: u64,
1307) -> Result<ExecRewritePlan, NotifError> {
1308 let argv = read_exec_ptr_array(read, argv_ptr)?;
1309 let envp = read_exec_ptr_array(read, envp_ptr)?;
1310 let slots: Vec<(u64, u64)> = argv
1311 .iter()
1312 .enumerate()
1313 .map(|(i, &p)| (argv_ptr + 8 * i as u64, p))
1314 .chain(envp.iter().enumerate().map(|(i, &p)| (envp_ptr + 8 * i as u64, p)))
1315 .collect();
1316
1317 let mut buf = fd_path.to_vec();
1318 let mut relocated: std::collections::BTreeMap<u64, u64> = std::collections::BTreeMap::new();
1319 let relocate = |buf: &mut Vec<u8>,
1320 relocated: &mut std::collections::BTreeMap<u64, u64>,
1321 read: &mut dyn FnMut(u64, usize) -> Result<Vec<u8>, NotifError>,
1322 ptr: u64|
1323 -> Result<(), NotifError> {
1324 let s = read_exec_cstr(&mut |a, l| read(a, l), ptr)?;
1325 relocated.insert(ptr, path_ptr + buf.len() as u64);
1326 buf.extend_from_slice(&s);
1327 buf.push(0);
1328 Ok(())
1329 };
1330
1331 let mut below: Vec<u64> = slots
1335 .iter()
1336 .map(|&(_, p)| p)
1337 .filter(|&p| p < path_ptr && path_ptr - p < EXEC_MAX_ARG_STRLEN as u64)
1338 .collect();
1339 below.sort_unstable();
1340 below.dedup();
1341 let mut nul_free_from = path_ptr;
1342 for &p in below.iter().rev() {
1343 let seg = read_exec_range(read, p, (nul_free_from - p) as usize)?;
1344 if seg.contains(&0) {
1345 break;
1346 }
1347 relocate(&mut buf, &mut relocated, read, p)?;
1348 nul_free_from = p;
1349 }
1350
1351 loop {
1353 let end = path_ptr + buf.len() as u64;
1354 let mut grew = false;
1355 for &(_, p) in &slots {
1356 if !relocated.contains_key(&p) && p >= path_ptr && p < end {
1357 relocate(&mut buf, &mut relocated, read, p)?;
1358 grew = true;
1359 }
1360 }
1361 if !grew {
1362 break;
1363 }
1364 }
1365
1366 let end = path_ptr + buf.len() as u64;
1367 for (base, n) in [(argv_ptr, argv.len()), (envp_ptr, envp.len())] {
1368 if base != 0 && base < end && base + 8 * (n as u64 + 1) > path_ptr {
1369 return Err(NotifError::Supervisor(
1370 "execve argv/envp pointer array overlaps the path rewrite window".into(),
1371 ));
1372 }
1373 }
1374
1375 let patches = slots
1376 .iter()
1377 .filter_map(|&(slot, p)| relocated.get(&p).map(|&np| (slot, np)))
1378 .collect();
1379 Ok(ExecRewritePlan { buf, patches })
1380}
1381
1382pub(crate) fn rewrite_exec_path_to_fd(
1411 notif_fd: RawFd,
1412 id: u64,
1413 pid: u32,
1414 path_ptr: u64,
1415 argv_ptr: u64,
1416 envp_ptr: u64,
1417 child_fd: i32,
1418) -> Result<(), NotifError> {
1419 let fd_path = format!("/proc/self/fd/{}\0", child_fd);
1420 let mut read = |addr: u64, len: usize| read_child_mem(notif_fd, id, pid, addr, len);
1421 let plan = plan_exec_rewrite(&mut read, path_ptr, fd_path.as_bytes(), argv_ptr, envp_ptr)?;
1422 write_child_mem_force(notif_fd, id, pid, path_ptr, &plan.buf)?;
1423 for (slot, new_ptr) in plan.patches {
1424 write_child_mem_force(notif_fd, id, pid, slot, &new_ptr.to_ne_bytes())?;
1425 }
1426 Ok(())
1427}
1428
1429fn send_response(fd: RawFd, id: u64, action: NotifAction) -> io::Result<()> {
1435 match action {
1436 NotifAction::Continue => respond_continue(fd, id),
1437 NotifAction::Errno(errno) => respond_errno(fd, id, errno),
1438 NotifAction::InjectFd { srcfd, targetfd } => {
1439 inject_fd(fd, id, srcfd, targetfd)?;
1440 respond_continue(fd, id)
1441 }
1442 NotifAction::InjectFdSend { srcfd, newfd_flags } => {
1443 match inject_fd_and_send(fd, id, srcfd.as_raw_fd(), newfd_flags) {
1449 Ok(_new_fd) => Ok(()),
1450 Err(_) => send_resp_raw(fd, &inject_failure_resp(id)),
1451 }
1452 }
1453 NotifAction::InjectFdSendTracked { srcfd, newfd_flags, on_success } => {
1454 match inject_fd_and_send(fd, id, srcfd.as_raw_fd(), newfd_flags) {
1455 Ok(new_fd) => {
1456 (on_success.0)(new_fd);
1457 Ok(())
1458 }
1459 Err(_) => send_resp_raw(fd, &inject_failure_resp(id)),
1460 }
1461 }
1462 NotifAction::ReturnValue(val) => respond_value(fd, id, val),
1463 NotifAction::Hold => Ok(()), NotifAction::Defer(_) => {
1465 debug_assert!(false, "Defer reached send_response; should be intercepted earlier");
1469 respond_errno(fd, id, libc::EIO)
1470 }
1471 NotifAction::Kill { sig, pgid } => {
1472 unsafe { libc::killpg(pgid, sig) };
1475 respond_errno(fd, id, ENOMEM)
1476 }
1477 }
1478}
1479
1480fn maybe_patch_vdso(pid: i32, procfs: &mut super::state::ProcfsState, policy: &NotifPolicy) {
1486 let base = match crate::vdso::find_vdso_base(pid) {
1487 Ok(addr) => addr,
1488 Err(_) => return,
1489 };
1490 if base == procfs.vdso_patched_addr {
1491 return; }
1493 let time_offset = if policy.has_time_start { Some(policy.time_offset) } else { None };
1494 if crate::vdso::patch(pid, time_offset, policy.has_random_seed).is_ok() {
1495 procfs.vdso_patched_addr = base;
1496 }
1497}
1498
1499fn syscall_name(nr: i64) -> &'static str {
1505 match nr {
1506 n if n == libc::SYS_openat => "openat",
1507 n if n == libc::SYS_connect => "connect",
1508 n if n == libc::SYS_sendto => "sendto",
1509 n if n == libc::SYS_sendmsg => "sendmsg",
1510 n if n == libc::SYS_sendmmsg => "sendmmsg",
1511 n if n == libc::SYS_bind => "bind",
1512 n if n == libc::SYS_clone => "clone",
1513 n if n == libc::SYS_clone3 => "clone3",
1514 n if Some(n) == arch::sys_vfork() => "vfork",
1515 n if Some(n) == arch::sys_fork() => "fork",
1516 n if n == libc::SYS_execve => "execve",
1517 n if n == libc::SYS_execveat => "execveat",
1518 n if n == libc::SYS_mmap => "mmap",
1519 n if n == libc::SYS_munmap => "munmap",
1520 n if n == libc::SYS_brk => "brk",
1521 n if n == libc::SYS_getrandom => "getrandom",
1522 n if n == libc::SYS_unlinkat => "unlinkat",
1523 n if n == libc::SYS_mkdirat => "mkdirat",
1524 _ => "unknown",
1525 }
1526}
1527
1528fn syscall_category(nr: i64) -> crate::policy_fn::SyscallCategory {
1530 use crate::policy_fn::SyscallCategory;
1531 match nr {
1532 n if n == libc::SYS_openat || n == libc::SYS_unlinkat
1533 || n == libc::SYS_mkdirat || n == libc::SYS_renameat2
1534 || n == libc::SYS_symlinkat || n == libc::SYS_linkat
1535 || n == libc::SYS_fchmodat || n == libc::SYS_fchownat
1536 || n == libc::SYS_truncate || n == libc::SYS_readlinkat
1537 || n == libc::SYS_newfstatat || n == libc::SYS_statx
1538 || n == libc::SYS_faccessat || n == libc::SYS_getdents64
1539 || Some(n) == arch::sys_getdents() => SyscallCategory::File,
1540 n if n == libc::SYS_connect || n == libc::SYS_sendto
1541 || n == libc::SYS_sendmsg || n == libc::SYS_sendmmsg
1542 || n == libc::SYS_bind
1543 || n == libc::SYS_getsockname => SyscallCategory::Network,
1544 n if n == libc::SYS_clone || n == libc::SYS_clone3
1545 || Some(n) == arch::sys_vfork() || Some(n) == arch::sys_fork()
1546 || n == libc::SYS_execve || n == libc::SYS_execveat => SyscallCategory::Process,
1547 n if n == libc::SYS_mmap || n == libc::SYS_munmap
1548 || n == libc::SYS_brk || n == libc::SYS_mremap
1549 => SyscallCategory::Memory,
1550 _ => SyscallCategory::File, }
1552}
1553
1554fn read_ppid(pid: u32) -> Option<u32> {
1556 let stat = std::fs::read_to_string(format!("/proc/{}/stat", pid)).ok()?;
1557 let close_paren = stat.rfind(')')?;
1560 let rest = &stat[close_paren + 2..]; let fields: Vec<&str> = rest.split_whitespace().collect();
1562 fields.get(1)?.parse().ok()
1564}
1565
1566fn read_path_for_event(notif: &SeccompNotif, addr: u64, notif_fd: RawFd) -> Option<String> {
1568 if addr == 0 { return None; }
1569 let bytes = read_child_mem(notif_fd, notif.id, notif.pid, addr, 256).ok()?;
1570 let nul = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
1571 String::from_utf8(bytes[..nul].to_vec()).ok()
1572}
1573
1574fn normalize_path(path: &std::path::Path) -> String {
1575 use std::path::{Component, PathBuf};
1576
1577 let mut normalized = PathBuf::new();
1578 let absolute = path.is_absolute();
1579 if absolute {
1580 normalized.push("/");
1581 }
1582
1583 for component in path.components() {
1584 match component {
1585 Component::RootDir | Component::CurDir => {}
1586 Component::ParentDir => {
1587 normalized.pop();
1588 }
1589 Component::Normal(part) => normalized.push(part),
1590 Component::Prefix(_) => {}
1591 }
1592 }
1593
1594 if normalized.as_os_str().is_empty() {
1595 if absolute { "/".into() } else { ".".into() }
1596 } else {
1597 normalized.to_string_lossy().into_owned()
1598 }
1599}
1600
1601fn resolve_at_path_for_event(notif: &SeccompNotif, dirfd: i64, path: &str) -> Option<String> {
1602 use std::path::Path;
1603
1604 if Path::new(path).is_absolute() {
1605 return Some(normalize_path(Path::new(path)));
1606 }
1607
1608 let dirfd32 = dirfd as i32;
1609 let base = if dirfd32 == libc::AT_FDCWD {
1610 std::fs::read_link(format!("/proc/{}/cwd", notif.pid)).ok()?
1611 } else {
1612 std::fs::read_link(format!("/proc/{}/fd/{}", notif.pid, dirfd32)).ok()?
1613 };
1614
1615 Some(normalize_path(&base.join(path)))
1616}
1617
1618fn resolve_path_for_notif(notif: &SeccompNotif, notif_fd: RawFd) -> Option<String> {
1619 let nr = notif.data.nr as i64;
1620 match nr {
1621 n if n == libc::SYS_openat || n == arch::SYS_OPENAT2 => {
1622 let path = read_path_for_event(notif, notif.data.args[1], notif_fd)?;
1625 resolve_at_path_for_event(notif, notif.data.args[0] as i64, &path)
1626 }
1627 n if Some(n) == arch::sys_open() || n == libc::SYS_execve => {
1628 let path = read_path_for_event(notif, notif.data.args[0], notif_fd)?;
1629 resolve_at_path_for_event(notif, libc::AT_FDCWD as i64, &path)
1630 }
1631 n if n == libc::SYS_execveat => {
1632 let path = read_path_for_event(notif, notif.data.args[1], notif_fd)?;
1633 resolve_at_path_for_event(notif, notif.data.args[0] as i64, &path)
1634 }
1635 n if n == libc::SYS_linkat => {
1638 let path = read_path_for_event(notif, notif.data.args[1], notif_fd)?;
1639 resolve_at_path_for_event(notif, notif.data.args[0] as i64, &path)
1640 }
1641 n if n == libc::SYS_renameat2 => {
1644 let path = read_path_for_event(notif, notif.data.args[1], notif_fd)?;
1645 resolve_at_path_for_event(notif, notif.data.args[0] as i64, &path)
1646 }
1647 n if Some(n) == arch::sys_link() => {
1653 let path = read_path_for_event(notif, notif.data.args[0], notif_fd)?;
1654 resolve_at_path_for_event(notif, libc::AT_FDCWD as i64, &path)
1655 }
1656 n if Some(n) == arch::sys_rename() => {
1658 let path = read_path_for_event(notif, notif.data.args[0], notif_fd)?;
1659 resolve_at_path_for_event(notif, libc::AT_FDCWD as i64, &path)
1660 }
1661 _ => None,
1662 }
1663}
1664
1665fn resolve_second_path_for_notif(notif: &SeccompNotif, notif_fd: RawFd) -> Option<String> {
1669 let nr = notif.data.nr as i64;
1670 match nr {
1671 n if n == libc::SYS_renameat2 => {
1673 let path = read_path_for_event(notif, notif.data.args[3], notif_fd)?;
1674 resolve_at_path_for_event(notif, notif.data.args[2] as i64, &path)
1675 }
1676 n if n == libc::SYS_linkat => {
1680 let path = read_path_for_event(notif, notif.data.args[3], notif_fd)?;
1681 resolve_at_path_for_event(notif, notif.data.args[2] as i64, &path)
1682 }
1683 n if Some(n) == arch::sys_rename() => {
1685 let path = read_path_for_event(notif, notif.data.args[1], notif_fd)?;
1686 resolve_at_path_for_event(notif, libc::AT_FDCWD as i64, &path)
1687 }
1688 n if Some(n) == arch::sys_link() => {
1690 let path = read_path_for_event(notif, notif.data.args[1], notif_fd)?;
1691 resolve_at_path_for_event(notif, libc::AT_FDCWD as i64, &path)
1692 }
1693 _ => None,
1694 }
1695}
1696
1697fn read_sockaddr_for_event(notif: &SeccompNotif, addr: u64, len: usize, notif_fd: RawFd)
1701 -> (Option<std::net::IpAddr>, Option<u16>)
1702{
1703 if addr == 0 || len < 4 { return (None, None); }
1704 let bytes = match read_child_mem(notif_fd, notif.id, notif.pid, addr, len.min(128)) {
1705 Ok(b) => b,
1706 Err(_) => return (None, None),
1707 };
1708 let ip = crate::network::materialize::parse_ip_from_sockaddr(&bytes);
1709 let port = crate::network::materialize::parse_port_from_sockaddr(&bytes);
1710 (ip, port.filter(|&p| p > 0))
1711}
1712
1713fn read_argv_for_event(notif: &SeccompNotif, argv_ptr: u64, notif_fd: RawFd) -> Option<Vec<String>> {
1716 if argv_ptr == 0 { return None; }
1717 let mut args = Vec::new();
1718 let ptr_size = std::mem::size_of::<u64>();
1719
1720 for i in 0..64u64 {
1721 let ptr_addr = argv_ptr + i * ptr_size as u64;
1722 let ptr_bytes = read_child_mem(notif_fd, notif.id, notif.pid, ptr_addr, ptr_size).ok()?;
1723 let str_ptr = u64::from_ne_bytes(ptr_bytes[..8].try_into().ok()?);
1724 if str_ptr == 0 { break; } if let Some(s) = read_path_for_event(notif, str_ptr, notif_fd) {
1727 args.push(s);
1728 } else {
1729 break;
1730 }
1731 }
1732
1733 if args.is_empty() { None } else { Some(args) }
1734}
1735
1736fn resolve_held_gate(
1743 received: Option<crate::policy_fn::Verdict>,
1744) -> Option<crate::policy_fn::Verdict> {
1745 match received {
1746 Some(v) => Some(v),
1747 None => Some(crate::policy_fn::Verdict::Deny),
1748 }
1749}
1750
1751async fn emit_policy_event(
1754 notif: &SeccompNotif,
1755 action: &NotifAction,
1756 policy_fn_state: &Arc<tokio::sync::Mutex<super::state::PolicyFnState>>,
1757 notif_fd: RawFd,
1758) -> Option<crate::policy_fn::Verdict> {
1759 let pfs = policy_fn_state.lock().await;
1760 let tx = match pfs.event_tx.as_ref() {
1761 Some(tx) => tx.clone(),
1762 None => return None,
1763 };
1764 drop(pfs);
1765
1766 let nr = notif.data.nr as i64;
1767 let denied = matches!(action, NotifAction::Errno(_));
1768 let name = syscall_name(nr);
1769 let category = syscall_category(nr);
1770 let parent_pid = read_ppid(notif.pid);
1771
1772 let mut host = None;
1789 let mut port = None;
1790 let mut size = None;
1791 let mut argv = None;
1792
1793 if !denied && (nr == libc::SYS_execve || nr == libc::SYS_execveat) {
1794 let argv_ptr = if nr == libc::SYS_execveat {
1797 notif.data.args[2]
1798 } else {
1799 notif.data.args[1]
1800 };
1801 argv = read_argv_for_event(notif, argv_ptr, notif_fd);
1802 }
1803
1804 if nr == libc::SYS_connect || nr == libc::SYS_sendto || nr == libc::SYS_bind {
1805 let addr_ptr = notif.data.args[1];
1807 let addr_len = notif.data.args[2] as usize;
1808 let (h, p) = read_sockaddr_for_event(notif, addr_ptr, addr_len, notif_fd);
1809 host = h;
1810 port = p;
1811 }
1812
1813 if nr == libc::SYS_mmap {
1814 size = Some(notif.data.args[1]);
1816 }
1817
1818 let event = crate::policy_fn::SyscallEvent {
1819 syscall: name.to_string(),
1820 category,
1821 pid: notif.pid,
1822 parent_pid,
1823 host,
1824 port,
1825 size,
1826 argv,
1827 denied,
1828 };
1829
1830 let is_held = nr == libc::SYS_execve || nr == libc::SYS_execveat
1833 || nr == libc::SYS_connect || nr == libc::SYS_sendto
1834 || nr == libc::SYS_bind || nr == libc::SYS_openat;
1835
1836 if is_held {
1837 let (gate_tx, gate_rx) = tokio::sync::oneshot::channel();
1838 let _ = tx.send(crate::policy_fn::PolicyEvent {
1839 event,
1840 gate: Some(gate_tx),
1841 });
1842 let received = match tokio::time::timeout(std::time::Duration::from_secs(5), gate_rx).await {
1843 Ok(Ok(verdict)) => Some(verdict),
1844 _ => None, };
1846 resolve_held_gate(received)
1847 } else {
1848 let _ = tx.send(crate::policy_fn::PolicyEvent {
1849 event,
1850 gate: None,
1851 });
1852 None
1853 }
1854}
1855
1856const DEFER_MAX_INFLIGHT: usize = 64;
1866
1867const DEFER_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
1872
1873async fn run_deferred_within(deferred: Deferred, limit: std::time::Duration) -> NotifAction {
1879 match tokio::time::timeout(limit, deferred.run()).await {
1880 Ok(action) => finalize_deferred(action),
1881 Err(_) => {
1882 eprintln!(
1883 "sandlock: deferred handler exceeded {:?}; failing syscall with EIO",
1884 limit
1885 );
1886 NotifAction::Errno(libc::EIO)
1887 }
1888 }
1889}
1890
1891fn spawn_deferred(
1897 fd: RawFd,
1898 id: u64,
1899 deferred: Deferred,
1900 permit: tokio::sync::OwnedSemaphorePermit,
1901) {
1902 tokio::spawn(async move {
1903 let _permit = permit; let action = run_deferred_within(deferred, DEFER_TIMEOUT).await;
1905 let _ = send_response(fd, id, action);
1906 });
1907}
1908
1909async fn handle_notification(
1910 notif: SeccompNotif,
1911 ctx: &Arc<super::ctx::SupervisorCtx>,
1912 dispatch_table: &super::dispatch::DispatchTable,
1913 fd: RawFd,
1914 defer_sem: &Arc<tokio::sync::Semaphore>,
1915) {
1916 let policy = &ctx.policy;
1917
1918 crate::resource::register_child_if_new(ctx, notif.pid as i32).await;
1924
1925 if policy.has_time_start || policy.has_random_seed {
1927 let mut pfs = ctx.procfs.lock().await;
1928 maybe_patch_vdso(notif.pid as i32, &mut pfs, policy);
1929 }
1930
1931 let mut action = {
1933 let nr = notif.data.nr as i64;
1934 let mut path_check_nrs = vec![
1938 libc::SYS_openat, arch::SYS_OPENAT2, libc::SYS_execve, libc::SYS_execveat,
1939 libc::SYS_linkat, libc::SYS_renameat2,
1940 ];
1941 path_check_nrs.extend([
1942 arch::sys_open(), arch::sys_link(), arch::sys_rename(),
1943 ].into_iter().flatten());
1944 let should_precheck_denied = policy.chroot_root.is_none()
1945 && path_check_nrs.contains(&nr);
1946 if should_precheck_denied {
1947 let pfs = ctx.policy_fn.lock().await;
1948 if is_path_denied_for_notif(&pfs, ¬if, fd) {
1949 NotifAction::Errno(libc::EACCES)
1950 } else {
1951 let has_denied = pfs.has_denied_paths();
1952 drop(pfs);
1953 let action = dispatch_table.dispatch(notif, fd).await;
1956 let is_openat_family = nr == libc::SYS_openat
1965 || nr == arch::SYS_OPENAT2
1966 || Some(nr) == arch::sys_open();
1967 if matches!(action, NotifAction::Continue) && is_openat_family && has_denied {
1968 let pfs = ctx.policy_fn.lock().await;
1969 on_behalf_open_for_deny(¬if, policy, &pfs, fd)
1970 } else {
1971 action
1972 }
1973 }
1974 } else {
1975 dispatch_table.dispatch(notif, fd).await
1976 }
1977 };
1978
1979 let nr = notif.data.nr as i64;
1980 let fork_counted = matches!(action, NotifAction::Continue)
1981 && crate::resource::fork_counted_on_continue(¬if, fd);
1982
1983 let mut exec_freeze = None;
1997 if matches!(action, NotifAction::Continue)
1998 && policy.argv_safety_required
1999 && crate::freeze::requires_freeze_on_continue(nr)
2000 {
2001 match crate::freeze::freeze_sandbox_for_execve(
2002 &ctx.processes,
2003 notif.pid as i32,
2004 ) {
2005 Ok(outcome) => {
2006 exec_freeze = Some(outcome);
2007 }
2008 Err(e) => {
2009 eprintln!(
2010 "sandlock: argv-safety freeze failed for pid {}: {} \
2011 — denying execve to preserve TOCTOU invariant",
2012 notif.pid, e
2013 );
2014 action = NotifAction::Errno(libc::EPERM);
2015 }
2016 }
2017 }
2018
2019 if let Some(verdict) = emit_policy_event(¬if, &action, &ctx.policy_fn, fd).await {
2023 use crate::policy_fn::Verdict;
2024 match verdict {
2025 Verdict::Deny => { action = NotifAction::Errno(libc::EPERM); }
2026 Verdict::DenyWith(errno) => { action = NotifAction::Errno(errno); }
2027 Verdict::Audit => { }
2028 Verdict::Allow => {}
2029 }
2030 }
2031
2032 if fork_counted && !matches!(action, NotifAction::Continue) {
2033 crate::resource::rollback_fork_count(&ctx.resource).await;
2034 }
2035
2036 let mut creation_trace = None;
2041 if matches!(action, NotifAction::Continue)
2042 && crate::resource::requires_process_creation_tracking(¬if, fd, policy)
2043 {
2044 match crate::resource::prepare_process_creation_tracking(ctx, notif.pid as i32).await {
2045 Ok(trace) => {
2046 creation_trace = Some(trace);
2047 }
2048 Err(e) => {
2049 eprintln!(
2050 "sandlock: process-creation tracking failed for pid {}: {} \
2051 — denying fork-like syscall to preserve argv TOCTOU invariant",
2052 notif.pid, e
2053 );
2054 if fork_counted {
2055 crate::resource::rollback_fork_count(&ctx.resource).await;
2056 }
2057 action = NotifAction::Errno(libc::EPERM);
2058 }
2059 }
2060 }
2061
2062 if let NotifAction::Defer(deferred) = action {
2073 if crate::freeze::requires_freeze_on_continue(nr)
2074 || crate::resource::requires_process_creation_tracking(¬if, fd, policy)
2075 {
2076 let _ = send_response(fd, notif.id, NotifAction::Errno(libc::EPERM));
2077 return;
2078 }
2079 match Arc::clone(defer_sem).try_acquire_owned() {
2080 Ok(permit) => spawn_deferred(fd, notif.id, deferred, permit),
2081 Err(_) => {
2084 let _ = send_response(fd, notif.id, NotifAction::Errno(libc::EAGAIN));
2085 }
2086 }
2087 return;
2088 }
2089
2090 let exec_continued = exec_freeze.is_some() && matches!(action, NotifAction::Continue);
2092 let send_result = send_response(fd, notif.id, action);
2093
2094 if let Some(trace) = creation_trace {
2095 if send_result.is_ok() {
2096 match crate::resource::finish_process_creation_tracking(trace).await {
2097 Ok(true) => {}
2098 Ok(false) => {
2099 crate::resource::rollback_fork_count(&ctx.resource).await;
2100 }
2101 Err(e) => {
2102 crate::resource::rollback_fork_count(&ctx.resource).await;
2103 eprintln!(
2104 "sandlock: process-creation tracking completion failed for pid {}: {}",
2105 notif.pid, e
2106 );
2107 }
2108 }
2109 } else {
2110 crate::resource::rollback_fork_count(&ctx.resource).await;
2111 crate::resource::abort_process_creation_tracking(trace).await;
2112 }
2113 }
2114
2115 if let Some(freeze) = exec_freeze {
2116 if exec_continued && send_result.is_ok() {
2117 crate::freeze::detach_peers(&freeze.peer_tids);
2118 } else {
2119 crate::freeze::detach_all(&freeze);
2120 }
2121 }
2122}
2123
2124pub async fn supervisor(
2136 notif_fd: OwnedFd,
2137 ctx: Arc<super::ctx::SupervisorCtx>,
2138 pending_handlers: Vec<(i64, std::sync::Arc<dyn super::dispatch::Handler>)>,
2139 startup: tokio::sync::oneshot::Sender<io::Result<()>>,
2140) {
2141 let async_fd = match tokio::io::unix::AsyncFd::with_interest(
2144 notif_fd,
2145 tokio::io::Interest::READABLE,
2146 ) {
2147 Ok(fd) => fd,
2148 Err(err) => {
2149 let _ = startup.send(Err(err));
2150 return;
2151 }
2152 };
2153 let fd = async_fd.get_ref().as_raw_fd();
2154
2155 let dispatch_table = Arc::new(super::dispatch::build_dispatch_table(
2157 &ctx.policy,
2158 &ctx.resource,
2159 &ctx,
2160 pending_handlers,
2161 ));
2162
2163 try_set_sync_wakeup(fd);
2165
2166 let _ = startup.send(Ok(()));
2170
2171 let gc = tokio::spawn(process_index_gc(Arc::clone(&ctx.processes)));
2177
2178 let defer_sem = Arc::new(tokio::sync::Semaphore::new(DEFER_MAX_INFLIGHT));
2182
2183 'outer: loop {
2193 let mut ready = match async_fd.readable().await {
2194 Ok(r) => r,
2195 Err(_) => break 'outer,
2196 };
2197 ready.clear_ready();
2198 drop(ready);
2199
2200 loop {
2201 match probe_notif_fd(fd) {
2202 NotifFdState::Pending => {
2203 let notif = match recv_notif(fd) {
2204 Ok(n) => n,
2205 Err(e) if e.raw_os_error() == Some(libc::EINTR) => continue,
2206 Err(_) => break 'outer,
2207 };
2208 handle_notification(notif, &ctx, &dispatch_table, fd, &defer_sem).await;
2209 }
2210 NotifFdState::Empty => break,
2211 NotifFdState::Terminal => break 'outer,
2212 }
2213 }
2214 }
2215
2216 gc.abort();
2217}
2218
2219async fn process_index_gc(processes: Arc<super::state::ProcessIndex>) {
2223 let interval = std::time::Duration::from_secs(300);
2224 loop {
2225 tokio::time::sleep(interval).await;
2226 if processes.len() == 0 {
2227 continue;
2228 }
2229 processes.prune_dead();
2230 }
2231}
2232
2233pub(crate) fn spawn_pid_watcher(
2243 ctx: Arc<super::ctx::SupervisorCtx>,
2244 key: super::state::PidKey,
2245 pidfd: std::os::unix::io::OwnedFd,
2246) {
2247 tokio::spawn(async move {
2248 let async_fd = match tokio::io::unix::AsyncFd::with_interest(
2249 pidfd,
2250 tokio::io::Interest::READABLE,
2251 ) {
2252 Ok(f) => f,
2253 Err(_) => {
2254 cleanup_pid(&ctx, key).await;
2260 return;
2261 }
2262 };
2263 let _ = async_fd.readable().await;
2266 cleanup_pid(&ctx, key).await;
2267 });
2269}
2270
2271pub(crate) async fn cleanup_pid(ctx: &super::ctx::SupervisorCtx, key: super::state::PidKey) {
2277 ctx.processes.unregister(key);
2278}
2279
2280#[cfg(test)]
2285mod tests {
2286 use super::*;
2287 use std::os::unix::io::FromRawFd;
2288
2289 fn gettid() -> u32 {
2290 (unsafe { libc::syscall(libc::SYS_gettid) }) as u32
2291 }
2292
2293 const FAKE_BASE: u64 = 0x10000;
2299 const FD: &[u8] = b"/proc/self/fd/3\0"; fn put(mem: &mut [u8], addr: u64, bytes: &[u8]) {
2302 let off = (addr - FAKE_BASE) as usize;
2303 mem[off..off + bytes.len()].copy_from_slice(bytes);
2304 }
2305
2306 fn put_ptrs(mem: &mut [u8], addr: u64, ptrs: &[u64]) {
2307 for (i, &p) in ptrs.iter().enumerate() {
2308 put(mem, addr + 8 * i as u64, &p.to_ne_bytes());
2309 }
2310 }
2311
2312 fn plan(
2313 mem: &[u8],
2314 path_ptr: u64,
2315 argv_ptr: u64,
2316 envp_ptr: u64,
2317 ) -> Result<ExecRewritePlan, NotifError> {
2318 let mut read = |addr: u64, len: usize| {
2319 let off = addr
2320 .checked_sub(FAKE_BASE)
2321 .ok_or_else(|| NotifError::Supervisor("read below fake memory".into()))?
2322 as usize;
2323 mem.get(off..off + len)
2324 .map(|s| s.to_vec())
2325 .ok_or_else(|| NotifError::Supervisor("read past fake memory".into()))
2326 };
2327 plan_exec_rewrite(&mut read, path_ptr, FD, argv_ptr, envp_ptr)
2328 }
2329
2330 #[test]
2331 fn exec_rewrite_plain_when_no_string_overlaps() {
2332 let mut mem = vec![0u8; 4096];
2333 put(&mut mem, FAKE_BASE, b"./m\0");
2334 put(&mut mem, FAKE_BASE + 0x200, b"prog\0");
2335 put_ptrs(&mut mem, FAKE_BASE + 0x800, &[FAKE_BASE + 0x200]);
2336 let p = plan(&mem, FAKE_BASE, FAKE_BASE + 0x800, 0).unwrap();
2337 assert_eq!(p.buf, FD);
2338 assert!(p.patches.is_empty());
2339 }
2340
2341 #[test]
2342 fn exec_rewrite_relocates_aliased_argv0() {
2343 let mut mem = vec![0u8; 4096];
2345 put(&mut mem, FAKE_BASE, b"./m\0");
2346 put_ptrs(&mut mem, FAKE_BASE + 0x800, &[FAKE_BASE]);
2347 let p = plan(&mem, FAKE_BASE, FAKE_BASE + 0x800, 0).unwrap();
2348 assert_eq!(p.buf, [FD, b"./m\0".as_slice()].concat());
2349 assert_eq!(p.patches, vec![(FAKE_BASE + 0x800, FAKE_BASE + 16)]);
2350 }
2351
2352 #[test]
2353 fn exec_rewrite_relocates_packed_argv1() {
2354 let mut mem = vec![0u8; 4096];
2357 put(&mut mem, FAKE_BASE, b"./echo\0EXEC_OK\0");
2358 put_ptrs(&mut mem, FAKE_BASE + 0x800, &[FAKE_BASE, FAKE_BASE + 7]);
2359 let p = plan(&mem, FAKE_BASE, FAKE_BASE + 0x800, 0).unwrap();
2360 assert_eq!(p.buf, [FD, b"./echo\0EXEC_OK\0".as_slice()].concat());
2361 assert_eq!(
2362 p.patches,
2363 vec![
2364 (FAKE_BASE + 0x800, FAKE_BASE + 16),
2365 (FAKE_BASE + 0x808, FAKE_BASE + 23),
2366 ]
2367 );
2368 }
2369
2370 #[test]
2371 fn exec_rewrite_relocates_string_reaching_window_from_below() {
2372 let mut mem = vec![0u8; 4096];
2375 put(&mut mem, FAKE_BASE, b"/bin/echo\0");
2376 put_ptrs(&mut mem, FAKE_BASE + 0x800, &[FAKE_BASE]);
2377 let p = plan(&mem, FAKE_BASE + 5, FAKE_BASE + 0x800, 0).unwrap();
2378 assert_eq!(p.buf, [FD, b"/bin/echo\0".as_slice()].concat());
2379 assert_eq!(p.patches, vec![(FAKE_BASE + 0x800, FAKE_BASE + 5 + 16)]);
2380 }
2381
2382 #[test]
2383 fn exec_rewrite_skips_terminated_string_below_window() {
2384 let mut mem = vec![0u8; 4096];
2385 put(&mut mem, FAKE_BASE, b"a\0");
2386 put(&mut mem, FAKE_BASE + 8, b"./m\0");
2387 put_ptrs(&mut mem, FAKE_BASE + 0x800, &[FAKE_BASE]);
2388 let p = plan(&mem, FAKE_BASE + 8, FAKE_BASE + 0x800, 0).unwrap();
2389 assert_eq!(p.buf, FD);
2390 assert!(p.patches.is_empty());
2391 }
2392
2393 #[test]
2394 fn exec_rewrite_relocates_envp_string() {
2395 let mut mem = vec![0u8; 4096];
2396 put(&mut mem, FAKE_BASE, b"./m\0K=V\0");
2397 put(&mut mem, FAKE_BASE + 0x200, b"prog\0");
2398 put_ptrs(&mut mem, FAKE_BASE + 0x800, &[FAKE_BASE + 0x200]);
2399 put_ptrs(&mut mem, FAKE_BASE + 0x900, &[FAKE_BASE + 4]);
2400 let p = plan(&mem, FAKE_BASE, FAKE_BASE + 0x800, FAKE_BASE + 0x900).unwrap();
2401 assert_eq!(p.buf, [FD, b"K=V\0".as_slice()].concat());
2402 assert_eq!(p.patches, vec![(FAKE_BASE + 0x900, FAKE_BASE + 16)]);
2403 }
2404
2405 #[test]
2406 fn exec_rewrite_window_growth_pulls_in_later_string() {
2407 let mut mem = vec![0u8; 4096];
2410 put(&mut mem, FAKE_BASE, b"./m\0");
2411 put(&mut mem, FAKE_BASE + 18, b"Z\0");
2412 put_ptrs(&mut mem, FAKE_BASE + 0x800, &[FAKE_BASE, FAKE_BASE + 18]);
2413 let p = plan(&mem, FAKE_BASE, FAKE_BASE + 0x800, 0).unwrap();
2414 assert_eq!(p.buf, [FD, b"./m\0".as_slice(), b"Z\0".as_slice()].concat());
2415 assert_eq!(
2416 p.patches,
2417 vec![
2418 (FAKE_BASE + 0x800, FAKE_BASE + 16),
2419 (FAKE_BASE + 0x808, FAKE_BASE + 20),
2420 ]
2421 );
2422 }
2423
2424 #[test]
2425 fn exec_rewrite_rejects_pointer_array_inside_window() {
2426 let mut mem = vec![0u8; 4096];
2429 put(&mut mem, FAKE_BASE, b"./m\0");
2430 put_ptrs(&mut mem, FAKE_BASE + 8, &[FAKE_BASE + 0x200]);
2431 put(&mut mem, FAKE_BASE + 0x200, b"prog\0");
2432 assert!(plan(&mem, FAKE_BASE, FAKE_BASE + 8, 0).is_err());
2433 }
2434
2435 #[test]
2436 fn exec_rewrite_null_arrays() {
2437 let mut mem = vec![0u8; 4096];
2438 put(&mut mem, FAKE_BASE, b"./m\0");
2439 let p = plan(&mem, FAKE_BASE, 0, 0).unwrap();
2440 assert_eq!(p.buf, FD);
2441 assert!(p.patches.is_empty());
2442 }
2443
2444 #[test]
2445 fn inject_failure_response_denies_not_continues() {
2446 let resp = inject_failure_resp(123);
2450 assert_eq!(resp.id, 123);
2451 assert_eq!(
2452 resp.flags & SECCOMP_USER_NOTIF_FLAG_CONTINUE,
2453 0,
2454 "fd-injection failure must not respond with CONTINUE"
2455 );
2456 assert_ne!(resp.error, 0, "fd-injection failure must be a denial");
2457 assert_eq!(resp.error, -libc::EACCES);
2458 }
2459
2460 #[test]
2461 fn held_gate_no_decision_denies() {
2462 use crate::policy_fn::Verdict;
2463 assert!(matches!(resolve_held_gate(None), Some(Verdict::Deny)));
2466 }
2467
2468 #[test]
2469 fn held_gate_passes_through_callback_verdict() {
2470 use crate::policy_fn::Verdict;
2471 assert!(matches!(
2473 resolve_held_gate(Some(Verdict::Allow)),
2474 Some(Verdict::Allow)
2475 ));
2476 assert!(matches!(
2477 resolve_held_gate(Some(Verdict::Deny)),
2478 Some(Verdict::Deny)
2479 ));
2480 assert!(matches!(
2481 resolve_held_gate(Some(Verdict::DenyWith(13))),
2482 Some(Verdict::DenyWith(13))
2483 ));
2484 }
2485
2486 #[test]
2487 fn tgid_of_main_thread_is_own_pid() {
2488 assert_eq!(tgid_of(gettid()), Some(std::process::id()));
2490 }
2491
2492 #[test]
2493 fn tgid_of_worker_thread_resolves_to_process() {
2494 let (tid_tx, tid_rx) = std::sync::mpsc::channel();
2496 let (done_tx, done_rx) = std::sync::mpsc::channel::<()>();
2497 let h = std::thread::spawn(move || {
2498 tid_tx.send(gettid()).unwrap();
2499 done_rx.recv().ok(); });
2501 let worker_tid = tid_rx.recv().unwrap();
2502 let pid = std::process::id();
2503 assert_ne!(worker_tid, pid, "worker tid must differ from pid");
2504 assert_eq!(tgid_of(worker_tid), Some(pid));
2505 done_tx.send(()).ok();
2506 h.join().unwrap();
2507 }
2508
2509 #[test]
2510 fn dup_fd_from_pid_handles_worker_thread_fd() {
2511 use std::os::unix::io::AsRawFd;
2512 let (info_tx, info_rx) = std::sync::mpsc::channel();
2516 let (done_tx, done_rx) = std::sync::mpsc::channel::<()>();
2517 let h = std::thread::spawn(move || {
2518 let f = std::fs::File::open("/dev/null").unwrap();
2519 info_tx.send((gettid(), f.as_raw_fd())).unwrap();
2520 done_rx.recv().ok();
2521 drop(f);
2522 });
2523 let (worker_tid, fd) = info_rx.recv().unwrap();
2524 let dup = dup_fd_from_pid(worker_tid, fd);
2525 done_tx.send(()).ok();
2526 h.join().unwrap();
2527 assert!(dup.is_ok(), "dup_fd_from_pid for a worker-thread fd failed: {:?}", dup.err());
2528 }
2529
2530 #[test]
2531 fn read_child_cstr_returns_none_for_null_addr_or_zero_max_len() {
2532 assert!(read_child_cstr(-1, 0, 0, 0, 4096).is_none());
2534 assert!(read_child_cstr(-1, 0, 0, 0xdeadbeef, 0).is_none());
2536 }
2537
2538 #[test]
2539 fn test_notif_action_debug() {
2540 let _ = format!("{:?}", NotifAction::Continue);
2542 let _ = format!("{:?}", NotifAction::Errno(1));
2543 let _ = format!("{:?}", NotifAction::InjectFd { srcfd: 3, targetfd: 4 });
2544 let test_fd = unsafe { OwnedFd::from_raw_fd(libc::dup(2)) };
2546 let _ = format!("{:?}", NotifAction::InjectFdSend { srcfd: test_fd, newfd_flags: 0 });
2547 let _ = format!("{:?}", NotifAction::ReturnValue(42));
2548 let _ = format!("{:?}", NotifAction::Hold);
2549 let _ = format!("{:?}", NotifAction::Kill { sig: 9, pgid: 1 });
2550 let _ = format!("{:?}", NotifAction::defer(async { NotifAction::Continue }));
2551 }
2552
2553 #[tokio::test]
2554 async fn deferred_future_need_not_be_sync() {
2555 use std::cell::Cell;
2560 let action = NotifAction::defer(async move {
2561 let counter = Cell::new(0);
2562 counter.set(counter.get() + 41);
2563 tokio::task::yield_now().await; NotifAction::ReturnValue(counter.get() + 1)
2565 });
2566 let NotifAction::Defer(d) = action else { panic!("expected Defer") };
2567 assert!(matches!(d.run().await, NotifAction::ReturnValue(42)));
2568 }
2569
2570 #[tokio::test]
2571 async fn deferred_runs_to_its_terminal_action() {
2572 let action = NotifAction::defer(async { NotifAction::ReturnValue(7) });
2574 let NotifAction::Defer(deferred) = action else {
2575 panic!("defer() must construct a NotifAction::Defer");
2576 };
2577 assert!(matches!(deferred.run().await, NotifAction::ReturnValue(7)));
2578 }
2579
2580 #[tokio::test(start_paused = true)]
2581 async fn deferred_times_out_to_eio() {
2582 let slow = Deferred::new(async {
2586 tokio::time::sleep(std::time::Duration::from_secs(60)).await;
2587 NotifAction::ReturnValue(7)
2588 });
2589 let action = run_deferred_within(slow, std::time::Duration::from_secs(1)).await;
2590 assert!(matches!(action, NotifAction::Errno(e) if e == libc::EIO));
2591 }
2592
2593 #[tokio::test(start_paused = true)]
2594 async fn deferred_within_limit_passes_through() {
2595 let fast = Deferred::new(async { NotifAction::ReturnValue(7) });
2597 let action = run_deferred_within(fast, std::time::Duration::from_secs(1)).await;
2598 assert!(matches!(action, NotifAction::ReturnValue(7)));
2599 }
2600
2601 #[test]
2602 fn finalize_deferred_collapses_nested_defer_to_eio() {
2603 let nested = NotifAction::defer(async { NotifAction::Continue });
2606 assert!(matches!(finalize_deferred(nested), NotifAction::Errno(e) if e == libc::EIO));
2607 assert!(matches!(finalize_deferred(NotifAction::Continue), NotifAction::Continue));
2609 assert!(matches!(
2610 finalize_deferred(NotifAction::ReturnValue(3)),
2611 NotifAction::ReturnValue(3)
2612 ));
2613 }
2614
2615 #[test]
2616 fn content_memfd_roundtrips_content() {
2617 use std::io::Read;
2618 let fd = content_memfd(b"hello world", true).expect("content_memfd");
2619 let mut f = std::fs::File::from(fd);
2621 let mut buf = String::new();
2622 f.read_to_string(&mut buf).unwrap();
2623 assert_eq!(buf, "hello world");
2624 }
2625
2626 #[test]
2627 fn content_memfd_sealed_applies_write_seal() {
2628 let fd = content_memfd(b"data", true).expect("content_memfd");
2629 let seals = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GET_SEALS) };
2630 assert!(seals >= 0, "F_GET_SEALS failed");
2631 assert!(
2632 seals & libc::F_SEAL_WRITE != 0,
2633 "expected F_SEAL_WRITE on a sealed memfd, got {seals:#x}"
2634 );
2635 }
2636
2637 #[test]
2638 fn content_memfd_unsealed_has_no_write_seal() {
2639 let fd = content_memfd(b"data", false).expect("content_memfd");
2640 let seals = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GET_SEALS) };
2641 assert!(seals >= 0, "F_GET_SEALS failed");
2642 assert_eq!(
2643 seals & libc::F_SEAL_WRITE,
2644 0,
2645 "unsealed memfd must not carry a write seal, got {seals:#x}"
2646 );
2647 }
2648
2649 #[test]
2650 fn inject_bytes_produces_sealed_cloexec_injectfdsend() {
2651 use std::io::Read;
2652 match NotifAction::inject_bytes(b"payload") {
2653 NotifAction::InjectFdSend { srcfd, newfd_flags } => {
2654 assert_eq!(newfd_flags, libc::O_CLOEXEC as u32);
2655 let seals = unsafe { libc::fcntl(srcfd.as_raw_fd(), libc::F_GET_SEALS) };
2656 assert!(seals & libc::F_SEAL_WRITE != 0, "inject_bytes must seal");
2657 let mut f = std::fs::File::from(srcfd);
2658 let mut buf = String::new();
2659 f.read_to_string(&mut buf).unwrap();
2660 assert_eq!(buf, "payload");
2661 }
2662 other => panic!("expected InjectFdSend, got {other:?}"),
2663 }
2664 }
2665
2666 #[test]
2667 fn test_network_state_new() {
2668 let ns = super::super::state::NetworkState::new();
2669 assert!(matches!(ns.tcp_policy, NetworkPolicy::Unrestricted));
2670 assert!(matches!(ns.udp_policy, NetworkPolicy::Unrestricted));
2671 assert!(matches!(ns.icmp_policy, NetworkPolicy::Unrestricted));
2672 assert!(ns.port_map.bound_ports.is_empty());
2673 }
2674
2675 #[test]
2676 fn test_time_random_state_new() {
2677 let tr = super::super::state::TimeRandomState::new(None, None);
2678 assert!(tr.time_offset.is_none());
2679 assert!(tr.random_state.is_none());
2680 }
2681
2682 #[test]
2683 fn test_resource_state_new() {
2684 let rs = super::super::state::ResourceState::new(1024 * 1024, 10);
2685 assert_eq!(rs.mem_used, 0);
2686 assert_eq!(rs.max_memory_bytes, 1024 * 1024);
2687 assert_eq!(rs.max_processes, 10);
2688 assert!(!rs.hold_forks);
2689 assert!(rs.held_notif_ids.is_empty());
2690 }
2691
2692 #[test]
2693 fn test_process_vm_readv_self() {
2694 let data: u64 = 0xDEADBEEF_CAFEBABE;
2695 let addr = &data as *const u64 as u64;
2696 let pid = std::process::id();
2697 let result = read_child_mem_vm(pid, addr, 8);
2698 assert!(result.is_ok());
2699 let bytes = result.unwrap();
2700 let read_val = u64::from_ne_bytes(bytes[..8].try_into().unwrap());
2701 assert_eq!(read_val, 0xDEADBEEF_CAFEBABE);
2702 }
2703
2704 #[test]
2705 fn test_process_vm_writev_self() {
2706 let mut data: u64 = 0;
2707 let addr = &mut data as *mut u64 as u64;
2708 let pid = std::process::id();
2709 let payload = 0x1234567890ABCDEFu64.to_ne_bytes();
2710 let result = write_child_mem_vm(pid, addr, &payload);
2711 assert!(result.is_ok());
2712 assert_eq!(data, 0x1234567890ABCDEF);
2713 }
2714
2715 #[test]
2720 fn write_child_mem_proc_forces_past_readonly_page() {
2721 const PAGE: usize = 4096;
2722 let orig = b"/some/rodata/path\0";
2723 let newb = b"/proc/self/fd/7\0\0"; let addr = unsafe {
2726 libc::mmap(
2727 std::ptr::null_mut(),
2728 PAGE,
2729 libc::PROT_READ | libc::PROT_WRITE,
2730 libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
2731 -1,
2732 0,
2733 )
2734 };
2735 assert_ne!(addr, libc::MAP_FAILED, "mmap failed");
2736 unsafe { std::ptr::copy_nonoverlapping(orig.as_ptr(), addr as *mut u8, orig.len()) };
2737
2738 assert_eq!(
2741 unsafe { libc::mprotect(addr, PAGE, libc::PROT_READ) },
2742 0,
2743 "mprotect PROT_READ failed"
2744 );
2745
2746 let pid = std::process::id();
2747 let uaddr = addr as u64;
2748
2749 assert!(
2750 write_child_mem_vm(pid, uaddr, newb).is_err(),
2751 "process_vm_writev must fail on a read-only page"
2752 );
2753
2754 write_child_mem_proc(pid, uaddr, newb)
2756 .expect("force-write through /proc/pid/mem must succeed on a read-only page");
2757
2758 let got = unsafe { std::slice::from_raw_parts(addr as *const u8, newb.len()) };
2761 assert_eq!(got, newb, "forced write must be visible at the target address");
2762
2763 unsafe { libc::munmap(addr, PAGE) };
2764 }
2765
2766 #[test]
2767 fn denylist_blocks_matching_cidr_allows_rest() {
2768 use crate::network::IpCidr;
2769 let policy = NetworkPolicy::DenyList {
2770 cidrs: vec![(IpCidr::parse("10.0.0.0/8").unwrap(), PortAllow::Any)],
2771 any_ip_ports: HashSet::new(),
2772 deny_all: false,
2773 };
2774 assert!(!policy.allows("10.1.2.3".parse().unwrap(), 443)); assert!(policy.allows("8.8.8.8".parse().unwrap(), 443)); }
2777
2778 #[test]
2779 fn denylist_blocks_any_ip_port() {
2780 let mut ports = HashSet::new();
2781 ports.insert(25u16);
2782 let policy = NetworkPolicy::DenyList {
2783 cidrs: Vec::new(),
2784 any_ip_ports: ports,
2785 deny_all: false,
2786 };
2787 assert!(!policy.allows("8.8.8.8".parse().unwrap(), 25)); assert!(policy.allows("8.8.8.8".parse().unwrap(), 80)); }
2790
2791 #[test]
2792 fn denylist_specific_ports_on_cidr() {
2793 use crate::network::IpCidr;
2794 let mut ports = HashSet::new();
2795 ports.insert(443u16);
2796 let policy = NetworkPolicy::DenyList {
2797 cidrs: vec![(IpCidr::parse("1.2.3.4/32").unwrap(), PortAllow::Specific(ports))],
2798 any_ip_ports: HashSet::new(),
2799 deny_all: false,
2800 };
2801 assert!(!policy.allows("1.2.3.4".parse().unwrap(), 443)); assert!(policy.allows("1.2.3.4".parse().unwrap(), 80)); }
2804
2805 #[test]
2806 fn allowlist_permits_matching_cidr_only() {
2807 use crate::network::IpCidr;
2808 let mut ports = HashSet::new();
2809 ports.insert(80u16);
2810 let policy = NetworkPolicy::AllowList {
2811 per_ip: HashMap::new(),
2812 cidrs: vec![(IpCidr::parse("10.0.0.0/8").unwrap(), PortAllow::Specific(ports))],
2813 any_ip_ports: HashSet::new(),
2814 };
2815 assert!(policy.allows("10.1.2.3".parse().unwrap(), 80)); assert!(!policy.allows("10.1.2.3".parse().unwrap(), 443)); assert!(!policy.allows("8.8.8.8".parse().unwrap(), 80)); }
2819
2820 #[test]
2821 fn allowlist_cidr_all_ports() {
2822 use crate::network::IpCidr;
2823 let policy = NetworkPolicy::AllowList {
2824 per_ip: HashMap::new(),
2825 cidrs: vec![(IpCidr::parse("192.168.0.0/16").unwrap(), PortAllow::Any)],
2826 any_ip_ports: HashSet::new(),
2827 };
2828 assert!(policy.allows("192.168.5.5".parse().unwrap(), 9999)); assert!(!policy.allows("10.0.0.1".parse().unwrap(), 9999)); }
2831
2832 #[test]
2833 fn denylist_blocks_v4_mapped_ipv6_form_of_denied_ip() {
2834 use crate::network::IpCidr;
2838 let policy = NetworkPolicy::DenyList {
2839 cidrs: vec![(IpCidr::parse("169.254.169.254").unwrap(), PortAllow::Any)],
2840 any_ip_ports: HashSet::new(),
2841 deny_all: false,
2842 };
2843 assert!(!policy.allows("::ffff:169.254.169.254".parse().unwrap(), 80));
2844 }
2845
2846 #[test]
2847 fn allowlist_accepts_v4_mapped_ipv6_form_of_allowed_ip() {
2848 use crate::network::IpCidr;
2851 let mut per_ip = HashMap::new();
2852 per_ip.insert("1.2.3.4".parse().unwrap(), PortAllow::Any);
2853 let policy = NetworkPolicy::AllowList {
2854 per_ip,
2855 cidrs: vec![(IpCidr::parse("10.0.0.0/8").unwrap(), PortAllow::Any)],
2856 any_ip_ports: HashSet::new(),
2857 };
2858 assert!(policy.allows("::ffff:1.2.3.4".parse().unwrap(), 443));
2859 assert!(policy.allows("::ffff:10.1.2.3".parse().unwrap(), 443));
2860 assert!(!policy.allows("::ffff:8.8.8.8".parse().unwrap(), 443));
2861 }
2862}