1use std::fs::File;
13use std::io::{self, Read, Seek, SeekFrom};
14use std::os::fd::{AsRawFd, OwnedFd, RawFd};
15
16use evalbox_sys::seccomp_notify::{
17 SECCOMP_ADDFD_FLAG_SEND, SECCOMP_USER_NOTIF_FLAG_CONTINUE, SeccompNotif, SeccompNotifAddfd,
18 SeccompNotifResp, notif_addfd, notif_id_valid, notif_recv, notif_send,
19};
20
21use super::virtual_fs::VirtualFs;
22use crate::plan::NotifyMode;
23
24#[derive(Debug)]
26pub enum NotifyEvent {
27 SyscallHandled {
29 pid: u32,
31 syscall_nr: i32,
33 allowed: bool,
35 },
36}
37
38pub struct Supervisor {
40 listener_fd: OwnedFd,
41 mode: NotifyMode,
42 vfs: VirtualFs,
43}
44
45impl Supervisor {
46 pub fn new(listener_fd: OwnedFd, mode: NotifyMode, vfs: VirtualFs) -> Self {
48 Self {
49 listener_fd,
50 mode,
51 vfs,
52 }
53 }
54
55 pub fn fd(&self) -> RawFd {
57 self.listener_fd.as_raw_fd()
58 }
59
60 pub fn handle_event(&self) -> io::Result<Option<NotifyEvent>> {
65 let mut notif = SeccompNotif::default();
66
67 if let Err(e) = notif_recv(self.listener_fd.as_raw_fd(), &mut notif) {
68 if e == rustix::io::Errno::NOENT {
70 return Ok(None);
71 }
72 return Err(io::Error::from_raw_os_error(e.raw_os_error()));
73 }
74
75 match self.mode {
76 NotifyMode::Disabled => {
77 debug_assert!(
78 false,
79 "supervisor received notification with NotifyMode::Disabled"
80 );
81 self.respond_continue(¬if)?;
82 Ok(None)
83 }
84 NotifyMode::Monitor => self.handle_monitor(¬if),
85 NotifyMode::Virtualize => self.handle_virtualize(¬if),
86 }
87 }
88
89 fn handle_monitor(&self, notif: &SeccompNotif) -> io::Result<Option<NotifyEvent>> {
90 let syscall_name = syscall_name(notif.data.nr);
91 eprintln!(
92 "[notify] pid={} syscall={}({}) args=[{:#x}, {:#x}, {:#x}]",
93 notif.pid,
94 syscall_name,
95 notif.data.nr,
96 notif.data.args[0],
97 notif.data.args[1],
98 notif.data.args[2],
99 );
100
101 self.respond_continue(notif)?;
102
103 Ok(Some(NotifyEvent::SyscallHandled {
104 pid: notif.pid,
105 syscall_nr: notif.data.nr,
106 allowed: true,
107 }))
108 }
109
110 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
112 fn handle_virtualize(&self, notif: &SeccompNotif) -> io::Result<Option<NotifyEvent>> {
113 let syscall_nr = notif.data.nr;
114
115 let path_addr = if is_at_syscall(syscall_nr) {
118 notif.data.args[1]
119 } else {
120 notif.data.args[0]
121 };
122
123 let path = match self.read_child_string(notif.pid, path_addr) {
125 Ok(p) => p,
126 Err(_) => {
127 self.respond_continue(notif)?;
129 return Ok(None);
130 }
131 };
132
133 if notif_id_valid(self.listener_fd.as_raw_fd(), notif.id).is_err() {
135 return Ok(None); }
137
138 if let Some(real_path) = self.vfs.translate(&path) {
140 if is_open_syscall(syscall_nr) {
142 let flags = if syscall_nr == libc::SYS_openat as i32 {
143 notif.data.args[2] as i32
144 } else {
145 notif.data.args[1] as i32
146 };
147
148 match self.open_and_inject(notif, &real_path, flags) {
149 Ok(()) => {
150 return Ok(Some(NotifyEvent::SyscallHandled {
151 pid: notif.pid,
152 syscall_nr,
153 allowed: true,
154 }));
155 }
156 Err(_) => {
157 }
159 }
160 }
161 }
162
163 self.respond_continue(notif)?;
165 Ok(Some(NotifyEvent::SyscallHandled {
166 pid: notif.pid,
167 syscall_nr,
168 allowed: true,
169 }))
170 }
171
172 fn respond_continue(&self, notif: &SeccompNotif) -> io::Result<()> {
173 let resp = SeccompNotifResp {
174 id: notif.id,
175 val: 0,
176 error: 0,
177 flags: SECCOMP_USER_NOTIF_FLAG_CONTINUE,
178 };
179 notif_send(self.listener_fd.as_raw_fd(), &resp)
180 .map_err(|e| io::Error::from_raw_os_error(e.raw_os_error()))
181 }
182
183 #[allow(clippy::cast_sign_loss)]
185 fn open_and_inject(
186 &self,
187 notif: &SeccompNotif,
188 real_path: &std::path::Path,
189 flags: i32,
190 ) -> io::Result<()> {
191 use std::ffi::CString;
192 use std::os::unix::ffi::OsStrExt;
193
194 let path_c = CString::new(real_path.as_os_str().as_bytes())
195 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid path"))?;
196
197 let fd = unsafe { libc::open(path_c.as_ptr(), flags & !libc::O_CLOEXEC, 0o666) };
199 if fd < 0 {
200 return Err(io::Error::last_os_error());
201 }
202
203 let addfd = SeccompNotifAddfd {
205 id: notif.id,
206 flags: SECCOMP_ADDFD_FLAG_SEND,
207 srcfd: fd as u32,
208 newfd: 0,
209 newfd_flags: 0,
210 };
211
212 let result = notif_addfd(self.listener_fd.as_raw_fd(), &addfd)
213 .map_err(|e| io::Error::from_raw_os_error(e.raw_os_error()));
214
215 unsafe { libc::close(fd) };
217
218 result.map(|_| ())
219 }
220
221 fn read_child_string(&self, pid: u32, addr: u64) -> io::Result<String> {
223 let mem_path = format!("/proc/{pid}/mem");
224 let mut file = File::open(&mem_path)?;
225 file.seek(SeekFrom::Start(addr))?;
226
227 let mut buf = vec![0u8; 4096];
228 let n = file.read(&mut buf)?;
229 buf.truncate(n);
230
231 if let Some(nul_pos) = buf.iter().position(|&b| b == 0) {
233 buf.truncate(nul_pos);
234 }
235
236 String::from_utf8(buf)
237 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid UTF-8 in path"))
238 }
239}
240
241#[allow(clippy::cast_possible_truncation)]
243fn is_at_syscall(nr: i32) -> bool {
244 let nr = nr as i64;
245 nr == libc::SYS_openat
246 || nr == libc::SYS_newfstatat
247 || nr == libc::SYS_faccessat
248 || nr == libc::SYS_faccessat2
249 || nr == libc::SYS_readlinkat
250}
251
252#[allow(clippy::cast_possible_truncation)]
254fn is_open_syscall(nr: i32) -> bool {
255 let nr = nr as i64;
256 if nr == libc::SYS_openat {
257 return true;
258 }
259 #[cfg(target_arch = "x86_64")]
260 if nr == libc::SYS_open || nr == libc::SYS_creat {
261 return true;
262 }
263 false
264}
265
266#[allow(clippy::cast_possible_truncation)]
269fn syscall_name(nr: i32) -> &'static str {
270 match nr as i64 {
271 libc::SYS_openat => "openat",
272 libc::SYS_faccessat => "faccessat",
273 libc::SYS_faccessat2 => "faccessat2",
274 libc::SYS_newfstatat => "newfstatat",
275 libc::SYS_statx => "statx",
276 libc::SYS_readlinkat => "readlinkat",
277 #[cfg(target_arch = "x86_64")]
278 libc::SYS_open => "open",
279 #[cfg(target_arch = "x86_64")]
280 libc::SYS_creat => "creat",
281 #[cfg(target_arch = "x86_64")]
282 libc::SYS_access => "access",
283 #[cfg(target_arch = "x86_64")]
284 libc::SYS_stat => "stat",
285 #[cfg(target_arch = "x86_64")]
286 libc::SYS_lstat => "lstat",
287 #[cfg(target_arch = "x86_64")]
288 libc::SYS_readlink => "readlink",
289 _ => "unknown",
290 }
291}
292
293#[cfg(test)]
294#[allow(clippy::cast_possible_truncation)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn syscall_names() {
300 assert_eq!(syscall_name(libc::SYS_openat as i32), "openat");
301 assert_eq!(syscall_name(libc::SYS_newfstatat as i32), "newfstatat");
302 assert_eq!(syscall_name(9999), "unknown");
303 }
304}