syd/libseccomp/notify.rs
1// SPDX-License-Identifier: Apache-2.0 or MIT
2//
3// Copyright 2021 Sony Group Corporation
4//
5
6use std::{ops::BitOr, os::unix::io::RawFd};
7
8use bitflags::bitflags;
9
10use super::cvt;
11use crate::{
12 libseccomp::{
13 api::ensure_supported_api,
14 error::{Result, SeccompError},
15 ScmpArch, ScmpFilterContext, ScmpSyscall, ScmpVersion,
16 },
17 libseccomp_sys::*,
18};
19
20fn get_errno() -> i32 {
21 std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
22}
23
24/// Checks the libseccomp API level and the libseccomp version for
25/// the userspace notification.
26///
27/// This function succeeds if the libseccomp API level and the libseccomp
28/// version being used are equal to or greater than 6 and 2.5.0.
29///
30/// # Errors
31///
32/// If both the libseccomp API level and the libseccomp version being used are
33/// less than 6 and 2.5.0, an error will be returned.
34fn notify_supported() -> Result<()> {
35 ensure_supported_api("seccomp notification", 6, ScmpVersion::from((2, 5, 0)))?;
36
37 Ok(())
38}
39
40/// Represents a file descriptor used for the userspace notification.
41pub type ScmpFd = RawFd;
42
43bitflags! {
44 /// Userspace notification response flags
45 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
46 pub struct ScmpNotifRespFlags: u32 {
47 /// Tells the kernel to continue executing the system call that triggered the
48 /// notification. Must only be used when the notification response's error and value is 0.
49 const CONTINUE = SECCOMP_USER_NOTIF_FLAG_CONTINUE;
50 }
51}
52
53/// Userspace notification response flag
54///
55/// Tells the kernel to continue executing the system call that triggered the
56/// notification. Must only be used when the notification response's error is 0.
57#[deprecated(
58 since = "0.3.0",
59 note = "Use ScmpNotifRespFlags::CONTINUE or ScmpNotifRespFlags::CONTINUE.bits()"
60)]
61pub const NOTIF_FLAG_CONTINUE: u32 = ScmpNotifRespFlags::CONTINUE.bits();
62
63impl ScmpFilterContext {
64 /// Gets a file descriptor for the userspace notification associated with the
65 /// given filter context.
66 ///
67 /// Such a file descriptor is only valid after the filter has been loaded
68 /// and only when the filter uses the [`crate::ScmpAction::Notify`] action.
69 /// The file descriptor can be used to retrieve and respond to notifications
70 /// associated with the filter (see [`ScmpNotifReq::receive()`],
71 /// [`ScmpNotifResp::respond()`], and [`notify_id_valid()`]).
72 ///
73 /// **Note**: This file descriptor is shared between all threads.
74 ///
75 /// This function returns a raw file descriptor for the userspace notification.
76 /// The ownership of this file descriptor is not moved and you must not close it.
77 ///
78 /// This function corresponds to
79 /// [`seccomp_notify_fd`](https://man7.org/linux/man-pages/man3/seccomp_notify_fd.3.html).
80 ///
81 /// # Errors
82 ///
83 /// If an issue is encountered getting the file descriptor,
84 /// an error will be returned.
85 /// # Examples
86 ///
87 /// ```
88 /// # use syd::libseccomp::*;
89 /// let mut ctx = ScmpFilterContext::new(ScmpAction::Allow)?;
90 /// ctx.add_rule(ScmpAction::Notify, ScmpSyscall::from_name("getpid")?)?;
91 /// ctx.load()?;
92 /// let fd = ctx.get_notify_fd()?;
93 /// # Ok::<(), Box<dyn std::error::Error>>(())
94 /// ```
95 pub fn get_notify_fd(&self) -> Result<ScmpFd> {
96 notify_supported()?;
97
98 let ret = unsafe { seccomp_notify_fd(self.as_ptr()) };
99 if ret < 0 {
100 return Err(SeccompError::from_errno(ret));
101 }
102
103 Ok(ret)
104 }
105}
106
107/// Describes the system call context that triggered a notification.
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
109#[non_exhaustive]
110pub struct ScmpNotifData {
111 /// The syscall number
112 pub syscall: ScmpSyscall,
113 /// The filter architecture
114 pub arch: ScmpArch,
115 /// Address of the instruction that triggered a notification
116 pub instr_pointer: u64,
117 /// Arguments (up to 6) for the syscall
118 pub args: [u64; 6],
119}
120
121impl ScmpNotifData {
122 fn from_sys(data: seccomp_data) -> Result<Self> {
123 Ok(Self {
124 syscall: ScmpSyscall::from(data.nr),
125 arch: ScmpArch::from_sys(data.arch)?,
126 instr_pointer: data.instruction_pointer,
127 args: data.args,
128 })
129 }
130}
131
132/// Represents a userspace notification request.
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
134#[non_exhaustive]
135pub struct ScmpNotifReq {
136 /// Notification ID
137 pub id: u64,
138 /// Process that triggered the notification event
139 pub pid: u32,
140 /// Filter flags (see seccomp(2))
141 pub flags: u32,
142 /// System call context that triggered the notification
143 pub data: ScmpNotifData,
144}
145
146impl ScmpNotifReq {
147 fn from_sys(req: seccomp_notif) -> Result<Self> {
148 Ok(Self {
149 id: req.id,
150 pid: req.pid,
151 flags: req.flags,
152 data: ScmpNotifData::from_sys(req.data)?,
153 })
154 }
155
156 /// Retrieves a userspace notification from a filter whose
157 /// [`crate::ScmpAction::Notify`] action has triggered.
158 ///
159 /// The caller is expected to process the notification and return a
160 /// response via [`ScmpNotifResp::respond()`]. Each invocation of
161 /// this function returns one notification.
162 /// As multiple notifications may be pending at any time, this function is
163 /// normally called within a polling loop.
164 ///
165 /// This function returns a userspace notification request.
166 ///
167 /// This function corresponds to
168 /// [`seccomp_notify_receive`](https://man7.org/linux/man-pages/man3/seccomp_notify_receive.3.html).
169 ///
170 /// # Arguments
171 ///
172 /// * `fd` - A file descriptor for the userspace notification
173 ///
174 /// # Errors
175 ///
176 /// If an issue is encountered getting a notification request,
177 /// an error will be returned.
178 ///
179 /// # Examples
180 ///
181 /// ```
182 /// # use syd::libseccomp::*;
183 /// # use std::process::exit;
184 /// // Get the current process ID
185 /// let parent_pid = std::process::id();
186 /// let mut ctx = ScmpFilterContext::new(ScmpAction::Allow)?;
187 /// ctx.add_rule(ScmpAction::Notify, ScmpSyscall::from_name("getpid")?)?;
188 /// ctx.load()?;
189 /// let fd = ctx.get_notify_fd()?;
190 /// let pid = unsafe { libc::fork() };
191 /// if pid == 0 {
192 /// // Child process
193 ///
194 /// let child_pid = std::process::id();
195 /// // The child PID will be same as the parent PID by the user notification.
196 /// assert_eq!(child_pid, parent_pid);
197 /// exit(0);
198 /// }
199 /// let req = ScmpNotifReq::receive(fd)?;
200 /// let resp = ScmpNotifResp::new_val(req.id, parent_pid as i64, ScmpNotifRespFlags::empty());
201 /// resp.respond(fd);
202 /// # Ok::<(), Box<dyn std::error::Error>>(())
203 /// ```
204 pub fn receive(fd: ScmpFd) -> Result<Self> {
205 notify_supported()?;
206
207 let mut req_ptr: *mut seccomp_notif = std::ptr::null_mut();
208
209 // We only use the request here; the response is unused.
210 cvt(unsafe { seccomp_notify_alloc(&mut req_ptr, std::ptr::null_mut()) })?;
211
212 loop {
213 let ret = unsafe { seccomp_notify_receive(fd, req_ptr) };
214 let errno = get_errno();
215
216 if ret == 0 {
217 break;
218 } else if errno == libc::EINTR {
219 continue;
220 } else {
221 unsafe { seccomp_notify_free(req_ptr, std::ptr::null_mut()) };
222 return Err(SeccompError::from_errno(ret));
223 }
224 }
225
226 // Copy notify request before freeing the memory.
227 let req = seccomp_notif {
228 id: unsafe { (*req_ptr).id },
229 pid: unsafe { (*req_ptr).pid },
230 flags: unsafe { (*req_ptr).flags },
231 data: unsafe { (*req_ptr).data },
232 };
233
234 unsafe { seccomp_notify_free(req_ptr, std::ptr::null_mut()) };
235
236 Self::from_sys(req)
237 }
238}
239
240/// Represents a userspace notification response.
241#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
242#[non_exhaustive]
243pub struct ScmpNotifResp {
244 /// Notification ID (must match the corresponding `ScmpNotifReq` ID)
245 pub id: u64,
246 /// Return value for the syscall that created the notification.
247 /// Only relevant if the error is 0.
248 pub val: i64,
249 /// An error code.
250 /// Must be 0 if no error occurred, or an error constant from package
251 /// syscall (e.g., `libc::EPERM`, etc). In the latter case, it's used
252 /// as an error return from the syscall that created the notification.
253 pub error: i32,
254 /// Userspace notification response flag
255 pub flags: u32,
256}
257
258impl ScmpNotifResp {
259 unsafe fn to_sys(self, resp: *mut seccomp_notif_resp) {
260 unsafe {
261 (*resp).id = self.id;
262 (*resp).val = self.val;
263 (*resp).error = self.error;
264 (*resp).flags = self.flags;
265 }
266 }
267
268 /// Creates `ScmpNotifResp` from the specified arguments.
269 ///
270 /// It is recommended to use the convenient functions [`new_val`](ScmpNotifResp::new_val),
271 /// [`new_error`](ScmpNotifResp::new_error) and [`new_continue`](ScmpNotifResp::new_continue)
272 /// rather than this function.
273 ///
274 /// # Arguments
275 ///
276 /// * `id` - Notification ID
277 /// * `val` - Return value for the syscall that created the notification
278 /// * `error` - An error code
279 /// * `flags` - Userspace notification response flag
280 ///
281 /// # Examples
282 ///
283 /// ```ignore
284 /// let resp = ScmpNotifResp::new(req.id, val, error, flags);
285 /// ```
286 #[must_use]
287 pub fn new(id: u64, val: i64, error: i32, flags: u32) -> Self {
288 Self {
289 id,
290 val,
291 error,
292 flags,
293 }
294 }
295
296 /// Creates `ScmpNotifResp` for a spoofed success response.
297 ///
298 /// # Arguments
299 ///
300 /// * `id` - Notification ID
301 /// * `val` - Return value for the syscall that created the notification
302 /// * `flags` - Reserved for future use, specify as `ScmpNotifRespFlags::empty()`.
303 ///
304 /// # Examples
305 ///
306 /// ```ignore
307 /// let resp = ScmpNotifResp::new_val(req.id, val, ScmpNotifRespFlags::empty());
308 /// ```
309 #[must_use]
310 pub fn new_val(id: u64, val: i64, flags: ScmpNotifRespFlags) -> Self {
311 Self {
312 id,
313 val,
314 error: 0,
315 flags: flags.bits(),
316 }
317 }
318
319 /// Creates `ScmpNotifResp` for a spoofed error response.
320 ///
321 /// # Arguments
322 ///
323 /// * `id` - Notification ID
324 /// * `error` - An *negative* error code
325 /// * `flags` - Reserved for future use, specify as `ScmpNotifRespFlags::empty()`.
326 ///
327 /// # Examples
328 ///
329 /// ```ignore
330 /// let resp = ScmpNotifResp::new_error(req.id, error, ScmpNotifRespFlags::empty());
331 /// ```
332 #[must_use]
333 pub fn new_error(id: u64, error: i32, flags: ScmpNotifRespFlags) -> Self {
334 debug_assert!(error.is_negative());
335 Self {
336 id,
337 val: 0,
338 error,
339 flags: flags.bits(),
340 }
341 }
342
343 /// Creates `ScmpNotifResp` which continues the syscall execution.
344 ///
345 /// # Arguments
346 ///
347 /// * `id` - Notification ID
348 /// * `flags` - Reserved for future use, specify as `ScmpNotifRespFlags::empty()`.
349 ///
350 /// # Examples
351 ///
352 /// ```ignore
353 /// let resp = ScmpNotifResp::new_continue(req.id, ScmpNotifRespFlags::empty());
354 /// ```
355 #[must_use]
356 pub fn new_continue(id: u64, flags: ScmpNotifRespFlags) -> Self {
357 Self {
358 id,
359 val: 0,
360 error: 0,
361 flags: ScmpNotifRespFlags::CONTINUE.bitor(flags).bits(),
362 }
363 }
364
365 /// Responds to a userspace notification retrieved via [`ScmpNotifReq::receive()`].
366 ///
367 /// The response ID must match that of the corresponding notification retrieved
368 /// via [`ScmpNotifReq::receive()`].
369 ///
370 /// This function corresponds to
371 /// [`seccomp_notify_respond`](https://man7.org/linux/man-pages/man3/seccomp_notify_respond.3.html).
372 ///
373 /// # Arguments
374 ///
375 /// * `fd` - A file descriptor for the userspace notification
376 ///
377 /// # Errors
378 ///
379 /// If an issue is encountered responding a notification,
380 /// an error will be returned.
381 ///
382 /// # Examples
383 ///
384 /// ```
385 /// # use syd::libseccomp::*;
386 /// # use std::process::exit;
387 /// // Get the current process ID
388 /// let parent_pid = std::process::id();
389 /// let mut ctx = ScmpFilterContext::new(ScmpAction::Allow)?;
390 /// ctx.add_rule(ScmpAction::Notify, ScmpSyscall::from_name("getpid")?)?;
391 /// ctx.load()?;
392 /// let fd = ctx.get_notify_fd()?;
393 /// let pid = unsafe { libc::fork() };
394 /// if pid == 0 {
395 /// // Child process
396 ///
397 /// let child_pid = std::process::id();
398 /// // The child PID will be same as the parent PID by the user notification.
399 /// assert_eq!(child_pid, parent_pid);
400 /// exit(0);
401 /// }
402 /// let req = ScmpNotifReq::receive(fd)?;
403 /// let resp = ScmpNotifResp::new_val(req.id, parent_pid as i64, ScmpNotifRespFlags::empty());
404 /// resp.respond(fd);
405 /// # Ok::<(), Box<dyn std::error::Error>>(())
406 /// ```
407 pub fn respond(&self, fd: ScmpFd) -> Result<()> {
408 notify_supported()?;
409
410 let mut resp_ptr: *mut seccomp_notif_resp = std::ptr::null_mut();
411
412 // We only use the response here; the request is unused.
413 cvt(unsafe { seccomp_notify_alloc(std::ptr::null_mut(), &mut resp_ptr) })?;
414
415 unsafe { self.to_sys(resp_ptr) };
416
417 loop {
418 let ret = unsafe { seccomp_notify_respond(fd, resp_ptr) };
419 let errno = get_errno();
420
421 if ret == 0 {
422 break;
423 } else if errno == libc::EINTR {
424 continue;
425 } else {
426 unsafe { seccomp_notify_free(std::ptr::null_mut(), resp_ptr) };
427 return Err(SeccompError::from_errno(ret));
428 }
429 }
430
431 unsafe { seccomp_notify_free(std::ptr::null_mut(), resp_ptr) };
432
433 Ok(())
434 }
435}
436
437/// Checks if a userspace notification is still valid.
438///
439/// A return value of `Ok` means the notification is still valid.
440/// Otherwise the notification is not valid. This can be used to mitigate
441/// time-of-check-time-of-use (TOCTOU) attacks as described in [`seccomp_notify_id_valid(2)`].
442///
443/// [`seccomp_notify_id_valid(2)`]: https://man7.org/linux/man-pages/man3/seccomp_notify_id_valid.3.html
444///
445/// This function corresponds to
446/// [`seccomp_notify_id_valid`](https://man7.org/linux/man-pages/man3/seccomp_notify_id_valid.3.html).
447///
448/// # Arguments
449///
450/// * `fd` - A file descriptor for the userspace notification
451/// * `id` - Notification ID
452///
453/// # Errors
454///
455/// If the notification ID is invalid, an error will be returned.
456///
457/// # Examples
458///
459/// ```ignore
460/// # use syd::libseccomp::*;
461/// # use std::{fs::File, io::Read, process::Command, thread};
462/// let notify_fd = thread::spawn(|| -> Result<ScmpFd, error::SeccompError> {
463/// let mut ctx = ScmpFilterContext::new(ScmpAction::Allow)?;
464/// ctx.add_rule(ScmpAction::Notify, ScmpSyscall::from_name("openat")?)?
465/// .load()?;
466/// let notify_fd = ctx.get_notify_fd()?;
467///
468/// /// Example command execution
469/// let _ = Command::new("ls").spawn();
470/// Ok(notify_fd)
471/// })
472/// .join()
473/// .unwrap()?;
474///
475/// let req = ScmpNotifReq::receive(notify_fd)?;
476/// assert_eq!(req.data.arch, ScmpArch::native());
477/// assert_eq!(req.data.syscall, ScmpSyscall::from_name("openat")?);
478///
479/// let mut stat = File::open(&format!("/proc/{}/stat", req.pid))?;
480///
481/// // Checks if a notification is still valid.
482/// notify_id_valid(notify_fd, req.id)?;
483///
484/// let mut stat_data = String::new();
485/// let _ = stat.read_to_string(&mut stat_data)?;
486/// println!("{}", stat_data);
487///
488/// // Checks if a notification is still valid.
489/// notify_id_valid(notify_fd, req.id)?;
490///
491/// let resp = ScmpNotifResp::new_continue(req.id, ScmpNotifRespFlags::empty());
492/// resp.respond(notify_fd)?;
493/// # Ok::<(), Box<dyn std::error::Error>>(())
494/// ```
495pub fn notify_id_valid(fd: ScmpFd, id: u64) -> Result<()> {
496 notify_supported()?;
497
498 loop {
499 let ret = unsafe { seccomp_notify_id_valid(fd, id) };
500 let errno = get_errno();
501
502 if ret == 0 {
503 break;
504 } else if errno == libc::EINTR {
505 continue;
506 } else {
507 return Err(SeccompError::from_errno(ret));
508 }
509 }
510
511 Ok(())
512}