Skip to main content

proot/seccomp/
notify.rs

1//! The listener pool: leader/followers over one seccomp listener descriptor.
2//!
3//! The kernel hands a notification to the longest-waiting receiver, which on
4//! an idle host means a cold core.  So exactly one thread (the leader) waits
5//! at a time, spinning on `poll` for a short while before sleeping.  When it
6//! receives it releases the token and serves the request itself; whichever
7//! spinning follower grabs the token becomes the next leader.  Idle threads
8//! park after `spin_us`.
9
10use crate::state::Ctx;
11use crate::sys::*;
12use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
13use std::sync::{Arc, Condvar, Mutex};
14use std::time::{Duration, Instant};
15
16/// The listener pool; see [`start`].
17pub struct Pool {
18    listener: i32,
19    token: Mutex<()>,
20    park: Mutex<()>,
21    cv: Condvar,
22    parked: AtomicUsize,
23    closed: AtomicBool,
24    /// Current spin budget in microseconds, 0..=spin_us.
25    budget: AtomicU64,
26}
27
28/// Longest a follower spins for the token before parking, in microseconds.
29const FOLLOWER_SPIN_US: u64 = 20;
30
31/// Blocking receive; `None` once no process uses the filter any more.
32///
33/// `ENOENT` from `RECV` has two meanings: the filter has no users left, or
34/// the notification just queued was withdrawn by a signal (a `SIGCHLD`
35/// landing on a shell in a `stat`) before it was received.  Only the former
36/// ends the pool, and only it raises `POLLHUP` on the listener, which is how
37/// the two are told apart.
38fn recv(listener: i32) -> Option<SeccompNotif> {
39    loop {
40        let mut req = SeccompNotif::default();
41        if unsafe { libc::ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV as _, &mut req as *mut _) } == 0 {
42            return Some(req);
43        }
44        match errno() {
45            libc::EINTR => continue,
46            libc::ENOENT => {
47                let mut pfd = libc::pollfd {
48                    fd: listener,
49                    events: libc::POLLIN,
50                    revents: 0,
51                };
52                if unsafe { libc::poll(&mut pfd, 1, 0) } > 0 && pfd.revents & libc::POLLHUP != 0 {
53                    return None;
54                }
55                continue;
56            }
57            _ => return None,
58        }
59    }
60}
61
62/// Spins on `poll(0)` for up to `budget` microseconds, then blocks until readable.
63///
64/// # Returns
65/// True when a request was found while spinning, false when we had to block.
66fn wait_ready(listener: i32, budget: u64) -> bool {
67    let mut pfd = libc::pollfd {
68        fd: listener,
69        events: libc::POLLIN,
70        revents: 0,
71    };
72    if budget > 0 {
73        let start = Instant::now();
74        loop {
75            pfd.revents = 0;
76            if unsafe { libc::poll(&mut pfd, 1, 0) } > 0 {
77                return true;
78            }
79            if start.elapsed().as_micros() >= budget as u128 {
80                break;
81            }
82            std::hint::spin_loop();
83        }
84    }
85    unsafe { libc::poll(&mut pfd, 1, -1) };
86    false
87}
88
89fn thread_main(listener: i32, pool: Arc<Pool>, ctx: Arc<Ctx>, spin_us: u64) {
90    loop {
91        if pool.closed.load(Ordering::Acquire) {
92            return;
93        }
94        let guard = 'tok: loop {
95            let start = Instant::now();
96            let follower_spin = FOLLOWER_SPIN_US.min(pool.budget.load(Ordering::Relaxed));
97            loop {
98                if let Ok(g) = pool.token.try_lock() {
99                    break 'tok g;
100                }
101                if start.elapsed().as_micros() >= follower_spin as u128 {
102                    break;
103                }
104                std::hint::spin_loop();
105            }
106            // Park until the leader steps down.  Re-check the token under the
107            // park lock so a notify sent just before we parked is not lost.
108            let pg = pool.park.lock().unwrap();
109            if let Ok(g) = pool.token.try_lock() {
110                break 'tok g;
111            }
112            pool.parked.fetch_add(1, Ordering::AcqRel);
113            let _ = pool.cv.wait_timeout(pg, Duration::from_secs(1)).unwrap();
114            pool.parked.fetch_sub(1, Ordering::AcqRel);
115            if pool.closed.load(Ordering::Acquire) {
116                return;
117            }
118        };
119        let budget = pool.budget.load(Ordering::Relaxed);
120        let hit = wait_ready(listener, budget);
121        pool.budget
122            .store(if hit { spin_us } else { budget / 2 }, Ordering::Relaxed);
123        let req = recv(listener);
124        drop(guard);
125        if pool.parked.load(Ordering::Acquire) > 0 {
126            let _pg = pool.park.lock().unwrap();
127            pool.cv.notify_one();
128        }
129        match req {
130            Some(r) => crate::syscall::notify::handle(&ctx, listener, &r),
131            None => {
132                pool.closed.store(true, Ordering::Release);
133                pool.cv.notify_all();
134                return;
135            }
136        }
137    }
138}
139
140impl Pool {
141    /// Blocks until the pool has shut down, i.e. no process uses the filter any more.
142    ///
143    /// The wait is on the listener's `POLLHUP`, which closes the pool too: a thread already
144    /// blocked in `RECV` when the last process left the filter is not woken by the hangup on
145    /// every kernel (6.8 wakes a receiver only for `EPOLLIN` or `EPOLLERR`), and would hold
146    /// the supervisor up for good.  A `poll` for no events wakes for the hangup alone.
147    pub fn wait_closed(&self) {
148        while !self.closed.load(Ordering::Acquire) {
149            let mut pfd = libc::pollfd {
150                fd: self.listener,
151                events: 0,
152                revents: 0,
153            };
154            if unsafe { libc::poll(&mut pfd, 1, 200) } > 0 && pfd.revents & libc::POLLHUP != 0 {
155                self.closed.store(true, Ordering::Release);
156                self.cv.notify_all();
157                break;
158            }
159        }
160    }
161}
162
163/// Starts the supervisor threads.  They run until the listener closes.
164///
165/// # Arguments
166/// * `listener` - Seccomp listener descriptor received from the first tracee.
167/// * `ctx` - Shared supervisor state.
168///
169/// # Returns
170/// The pool, to wait for its shutdown.
171pub fn start(listener: i32, ctx: Arc<Ctx>) -> Arc<Pool> {
172    let pool = Arc::new(Pool {
173        listener,
174        token: Mutex::new(()),
175        park: Mutex::new(()),
176        cv: Condvar::new(),
177        parked: AtomicUsize::new(0),
178        closed: AtomicBool::new(false),
179        budget: AtomicU64::new(ctx.config.spin_us),
180    });
181    let threads = ctx.config.threads.max(1);
182    let spin_us = ctx.config.spin_us;
183    for i in 0..threads {
184        let (pool, ctx) = (pool.clone(), ctx.clone());
185        std::thread::Builder::new()
186            .name(format!("notify-{i}"))
187            .spawn(move || thread_main(listener, pool, ctx, spin_us))
188            .expect("spawn supervisor thread");
189    }
190    pool
191}
192
193/// Answers a notification with a plain result or an error.
194///
195/// # Arguments
196/// * `listener` - Listener descriptor.
197/// * `id` - Notification id.
198/// * `result` - `Ok(value)` becomes the syscall's return value, `Err(errno)` its error.
199pub fn reply(listener: i32, id: u64, result: Result<i64>) {
200    let resp = match result {
201        Ok(v) => SeccompNotifResp {
202            id,
203            val: v,
204            error: 0,
205            flags: 0,
206        },
207        Err(e) => SeccompNotifResp {
208            id,
209            val: 0,
210            error: -e,
211            flags: 0,
212        },
213    };
214    unsafe { libc::ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND as _, &resp as *const _) };
215}
216
217/// Lets the kernel execute the syscall unchanged.
218pub fn reply_continue(listener: i32, id: u64) {
219    let resp = SeccompNotifResp {
220        id,
221        val: 0,
222        error: 0,
223        flags: SECCOMP_USER_NOTIF_FLAG_CONTINUE,
224    };
225    unsafe { libc::ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND as _, &resp as *const _) };
226}
227
228/// Installs `fd` into the tracee and completes the syscall with its number.
229///
230/// Closes the supervisor's copy of `fd` afterwards.
231///
232/// # Arguments
233/// * `listener` - Listener descriptor.
234/// * `id` - Notification id.
235/// * `fd` - Supervisor-side descriptor to hand over.
236/// * `cloexec` - Set `O_CLOEXEC` on the tracee's copy.
237pub fn reply_fd(listener: i32, id: u64, fd: i32, cloexec: bool) {
238    let add = SeccompNotifAddfd {
239        id,
240        flags: SECCOMP_ADDFD_FLAG_SEND,
241        srcfd: fd as u32,
242        newfd: 0,
243        newfd_flags: if cloexec { libc::O_CLOEXEC as u32 } else { 0 },
244    };
245    let rc = unsafe { libc::ioctl(listener, SECCOMP_IOCTL_NOTIF_ADDFD as _, &add as *const _) };
246    if rc < 0 {
247        let e = errno();
248        if e == libc::EINVAL {
249            // Kernel older than 5.14: install first, then answer with the number.
250            let add2 = SeccompNotifAddfd { flags: 0, ..add };
251            let newfd = unsafe { libc::ioctl(listener, SECCOMP_IOCTL_NOTIF_ADDFD as _, &add2 as *const _) };
252            reply(
253                listener,
254                id,
255                if newfd < 0 { Err(errno()) } else { Ok(newfd as i64) },
256            );
257        } else if e != libc::ENOENT {
258            // The tracee must never be left waiting: EBADF here means the
259            // descriptor cannot be injected (e.g. O_PATH), report it as such.
260            reply(
261                listener,
262                id,
263                Err(if e == libc::EBADF { libc::EOPNOTSUPP } else { e }),
264            );
265        }
266    }
267    unsafe { libc::close(fd) };
268}
269
270/// Installs `fd` into the tracee at descriptor number `newfd`, closing what
271/// was there, without answering the notification.
272///
273/// Used before a `CONTINUE` so the continued syscall finds a different
274/// object behind one of its descriptor arguments.  Closes the supervisor's
275/// copy of `fd` afterwards.
276///
277/// # Arguments
278/// * `listener` - Listener descriptor.
279/// * `id` - Notification id.
280/// * `fd` - Supervisor-side descriptor to hand over.
281/// * `newfd` - Descriptor number in the tracee to install it at.
282/// * `cloexec` - Set `O_CLOEXEC` on the tracee's copy.
283///
284/// # Errors
285/// Errno from the ioctl.
286pub fn addfd_at(listener: i32, id: u64, fd: i32, newfd: i32, cloexec: bool) -> Result<()> {
287    let add = SeccompNotifAddfd {
288        id,
289        flags: SECCOMP_ADDFD_FLAG_SETFD,
290        srcfd: fd as u32,
291        newfd: newfd as u32,
292        newfd_flags: if cloexec { libc::O_CLOEXEC as u32 } else { 0 },
293    };
294    let rc = unsafe { libc::ioctl(listener, SECCOMP_IOCTL_NOTIF_ADDFD as _, &add as *const _) };
295    let e = errno();
296    unsafe { libc::close(fd) };
297    if rc < 0 { Err(e) } else { Ok(()) }
298}
299
300/// Installs `fd` into the tracee at the lowest free descriptor number,
301/// without answering the notification.  Closes the supervisor's copy.
302///
303/// # Arguments
304/// * `listener` - Listener descriptor.
305/// * `id` - Notification id.
306/// * `fd` - Supervisor-side descriptor to hand over.
307/// * `cloexec` - Set `O_CLOEXEC` on the tracee's copy.
308///
309/// # Returns
310/// The descriptor number in the tracee.
311///
312/// # Errors
313/// Errno from the ioctl.
314pub fn addfd(listener: i32, id: u64, fd: i32, cloexec: bool) -> Result<i32> {
315    let add = SeccompNotifAddfd {
316        id,
317        flags: 0,
318        srcfd: fd as u32,
319        newfd: 0,
320        newfd_flags: if cloexec { libc::O_CLOEXEC as u32 } else { 0 },
321    };
322    let rc = unsafe { libc::ioctl(listener, SECCOMP_IOCTL_NOTIF_ADDFD as _, &add as *const _) };
323    let e = errno();
324    unsafe { libc::close(fd) };
325    if rc < 0 { Err(e) } else { Ok(rc) }
326}
327
328/// Checks that the tracee is still blocked in this notification.
329///
330/// Call after reading tracee memory and before acting on it.
331pub fn still_valid(listener: i32, id: u64) -> bool {
332    unsafe { libc::ioctl(listener, SECCOMP_IOCTL_NOTIF_ID_VALID as _, &id as *const _) == 0 }
333}