Skip to main content

proot/
guest_stub.rs

1//! The supervisor's side of the guest-stub restricted mode.
2//!
3//! Where neither ptrace nor a user namespace lets the supervisor reach a
4//! guest's memory (see `access`), a stub inside every guest process moves
5//! the memory each served syscall uses into a mailbox both sides map.  The
6//! stub is part of the exec shim (`exec/shim/src/stub.rs`), which loads
7//! every program in place instead of exec'ing it so the stub stays armed;
8//! [`layout`] is the contract between the two, compiled into both.
9//!
10//! Here the mailbox is created and mapped into the supervisor, where
11//! `tracee::mem` serves guest addresses inside it and refuses every other,
12//! so the handlers in `syscall::notify` and `syscall::direct` read and write
13//! the staged copies as if they were the guest's memory.  The first tracee
14//! maps the gate page and execs the shim through the allow gate, the one
15//! exec the filter lets through without the stub.
16
17#[path = "exec/shim/src/layout.rs"]
18#[allow(dead_code)]
19pub mod layout;
20
21use crate::sys::errno;
22
23/// The mailbox memfd.  The supervisor's mapping lives as long as the process.
24#[derive(Debug)]
25pub struct Mailbox {
26    /// The memfd, close-on-exec in the supervisor.
27    pub fd: i32,
28}
29
30impl Mailbox {
31    /// Creates the mailbox and maps it into the supervisor, registering the
32    /// mapping with `tracee::mem`.
33    ///
34    /// # Returns
35    /// The mailbox.
36    ///
37    /// # Errors
38    /// A message naming the step that failed.
39    pub fn create() -> Result<Mailbox, String> {
40        let fd = unsafe {
41            libc::syscall(
42                libc::SYS_memfd_create,
43                c"proot-mailbox".as_ptr(),
44                libc::MFD_CLOEXEC,
45            )
46        };
47        if fd < 0 {
48            return Err(format!("memfd_create: {}", crate::sys::strerror(errno())));
49        }
50        let fd = fd as i32;
51        if unsafe { libc::ftruncate(fd, layout::MAIL_SIZE as libc::off_t) } < 0 {
52            let e = errno();
53            unsafe { libc::close(fd) };
54            return Err(format!("sizing the mailbox: {}", crate::sys::strerror(e)));
55        }
56        let local = unsafe {
57            libc::mmap(
58                std::ptr::null_mut(),
59                layout::MAIL_SIZE,
60                libc::PROT_READ | libc::PROT_WRITE,
61                libc::MAP_SHARED,
62                fd,
63                0,
64            )
65        };
66        if local == libc::MAP_FAILED {
67            let e = errno();
68            unsafe { libc::close(fd) };
69            return Err(format!("mapping the mailbox: {}", crate::sys::strerror(e)));
70        }
71        crate::tracee::mem::set_window(
72            &[layout::MAIL64, layout::MAIL32],
73            layout::MAIL_SIZE,
74            local as *mut u8,
75        );
76        Ok(Mailbox { fd })
77    }
78}
79
80/// Execs the trampoline in the first tracee, through the allow gate, with
81/// the mailbox descriptor it inherited kept open for it.  Allocation free,
82/// for use between `fork` and `execve`.
83///
84/// # Arguments
85/// * `mail` - The mailbox descriptor.
86/// * `shim` - Path of the trampoline from the kernel working directory.
87/// * `argv` - The command's argument vector.
88/// * `envp` - Its environment.
89///
90/// # Safety
91/// The pointers must be valid for `execve`.  Exits the process, with 127
92/// when the trampoline is missing and 126 for any other failure.
93pub unsafe fn exec_trampoline(
94    mail: i32,
95    shim: *const libc::c_char,
96    argv: *const *const libc::c_char,
97    envp: *const *const libc::c_char,
98) -> ! {
99    #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
100    unsafe {
101        libc::fcntl(mail, libc::F_SETFD, 0);
102        if map_gates() && exec_through_gate(shim, argv, envp) == -(libc::ENOENT as i64) {
103            libc::_exit(127);
104        }
105    }
106    #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
107    let _ = (mail, shim, argv, envp);
108    unsafe { libc::_exit(126) }
109}
110
111/// Maps the gate page in the calling process.  Allocation free, for the
112/// first tracee between `fork` and its exec of the shim.
113///
114/// # Returns
115/// `true` once the gates can be called.
116#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
117fn map_gates() -> bool {
118    unsafe {
119        let page = libc::mmap(
120            layout::GATE_ADDR as *mut libc::c_void,
121            layout::PAGE,
122            libc::PROT_READ | libc::PROT_WRITE,
123            libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_FIXED_NOREPLACE,
124            -1,
125            0,
126        );
127        if page as usize != layout::GATE_ADDR {
128            return false;
129        }
130        for at in [layout::ALLOW_GATE, layout::NOTIFY_GATE] {
131            std::ptr::copy_nonoverlapping(layout::GATE_CODE.as_ptr(), at as *mut u8, layout::GATE_CODE.len());
132        }
133        #[cfg(target_arch = "aarch64")]
134        sync_code(layout::GATE_ADDR, layout::PAGE);
135        libc::mprotect(page, layout::PAGE, libc::PROT_READ | libc::PROT_EXEC) == 0
136    }
137}
138
139/// Makes code just written at `start` visible to instruction fetch: cleans
140/// the data cache and invalidates the instruction cache over it, line by
141/// line, as `__clear_cache` does.  Allocation free.
142///
143/// # Arguments
144/// * `start` - First byte written.
145/// * `len` - Bytes written.
146#[cfg(target_arch = "aarch64")]
147fn sync_code(start: usize, len: usize) {
148    let ctr: usize;
149    unsafe { std::arch::asm!("mrs {}, ctr_el0", out(reg) ctr, options(nomem, nostack)) };
150    let dline = 4 << ((ctr >> 16) & 0xf);
151    let iline = 4 << (ctr & 0xf);
152    let mut a = start & !(dline - 1);
153    while a < start + len {
154        unsafe { std::arch::asm!("dc cvau, {}", in(reg) a, options(nostack)) };
155        a += dline;
156    }
157    unsafe { std::arch::asm!("dsb ish", options(nostack)) };
158    let mut a = start & !(iline - 1);
159    while a < start + len {
160        unsafe { std::arch::asm!("ic ivau, {}", in(reg) a, options(nostack)) };
161        a += iline;
162    }
163    unsafe { std::arch::asm!("dsb ish", "isb", options(nostack)) };
164}
165
166/// `execve` through the allow gate.  Allocation free.
167///
168/// # Arguments
169/// * `path` - Program to exec.
170/// * `argv` - Its argument vector.
171/// * `envp` - Its environment.
172///
173/// # Returns
174/// The negative errno of a failed exec; a successful one does not return.
175///
176/// # Safety
177/// [`map_gates`] must have succeeded, and the pointers be valid for `execve`.
178#[cfg(target_arch = "x86_64")]
179unsafe fn exec_through_gate(
180    path: *const libc::c_char,
181    argv: *const *const libc::c_char,
182    envp: *const *const libc::c_char,
183) -> i64 {
184    let ret: i64;
185    unsafe {
186        std::arch::asm!(
187            "lea r12, [rip + 2f]",
188            "jmp {at}",
189            "2:",
190            at = in(reg) layout::ALLOW_GATE,
191            inlateout("rax") libc::SYS_execve => ret,
192            in("rdi") path,
193            in("rsi") argv,
194            in("rdx") envp,
195            out("r12") _,
196            lateout("rcx") _,
197            lateout("r11") _,
198            options(nostack),
199        );
200    }
201    ret
202}
203
204/// `execve` through the allow gate.  Allocation free.
205///
206/// # Arguments
207/// * `path` - Program to exec.
208/// * `argv` - Its argument vector.
209/// * `envp` - Its environment.
210///
211/// # Returns
212/// The negative errno of a failed exec; a successful one does not return.
213///
214/// # Safety
215/// [`map_gates`] must have succeeded, and the pointers be valid for `execve`.
216#[cfg(target_arch = "aarch64")]
217unsafe fn exec_through_gate(
218    path: *const libc::c_char,
219    argv: *const *const libc::c_char,
220    envp: *const *const libc::c_char,
221) -> i64 {
222    let ret: i64;
223    unsafe {
224        std::arch::asm!(
225            "adr x16, 2f",
226            "br {at}",
227            "2:",
228            at = in(reg) layout::ALLOW_GATE,
229            inlateout("x0") path => ret,
230            in("x1") argv,
231            in("x2") envp,
232            in("x8") libc::SYS_execve,
233            out("x16") _,
234            options(nostack),
235        );
236    }
237    ret
238}
239
240#[cfg(test)]
241mod tests {
242    use super::layout::*;
243
244    /// The last slot ends where the mailbox does.
245    #[test]
246    fn slots_fill_the_mailbox() {
247        assert_eq!(slot_addr(0), MAIL_ADDR + PAGE);
248        assert_eq!(slot_addr(SLOTS - 1) + SLOT_SIZE, MAIL_ADDR + MAIL_SIZE);
249    }
250
251    /// Every path syscall the supervisor serves is staged or handled by the
252    /// stub, on all four ABIs, and the calls that need only registers are
253    /// not trapped.
254    #[test]
255    fn served_path_syscalls_are_trapped() {
256        use crate::abi::Abi;
257        use crate::syscall::{NO_PTRACE_SYSCALLS, NOTIFY_SYSCALLS, nr};
258        let registers_only = [nr::CLONE, nr::FORK, nr::VFORK, nr::FCHDIR, nr::CLONE3];
259        type Trapped = fn(i64) -> bool;
260        let tables: [(Abi, Trapped); 4] = [
261            (Abi::X86_64, trapped_x86_64),
262            (Abi::I386, trapped_i386),
263            (Abi::Aarch64, trapped_aarch64),
264            (Abi::Arm, trapped_arm),
265        ];
266        for (abi, trapped) in tables {
267            for nr in abi.numbers_for(NOTIFY_SYSCALLS) {
268                assert!(trapped(nr), "{abi:?} {nr} is served but not staged");
269            }
270            for nr in abi.numbers_for(NO_PTRACE_SYSCALLS) {
271                let canonical = abi.to_canonical(nr).unwrap().0;
272                assert_eq!(trapped(nr), !registers_only.contains(&canonical), "{abi:?} {nr}");
273            }
274        }
275        for nr in [nr::GETUID, nr::FCHOWN, nr::SETUID] {
276            assert!(!trapped_x86_64(nr), "{nr}");
277        }
278    }
279
280    /// Paths and outputs are staged at the arguments the kernel reads them
281    /// from, in each ABI's layout.
282    #[test]
283    fn staging_names_the_right_arguments() {
284        assert_eq!(staging_x86_64(262).unwrap()[0], (1, Stage::Path));
285        assert_eq!(
286            staging_x86_64(262).unwrap()[1],
287            (2, Stage::Out(Len::Fixed(144), Produced::Whole))
288        );
289        assert_eq!(
290            staging_x86_64(332).unwrap()[1],
291            (4, Stage::Out(Len::Fixed(256), Produced::Whole))
292        );
293        assert_eq!(
294            staging_x86_64(266).unwrap(),
295            &[(0, Stage::Path), (2, Stage::Path)]
296        );
297        assert_eq!(
298            staging_x86_64(267).unwrap()[1],
299            (2, Stage::Out(Len::Arg(3), Produced::Ret))
300        );
301        assert_eq!(
302            staging_x86_64(79).unwrap()[0],
303            (0, Stage::Out(Len::Arg(1), Produced::Ret))
304        );
305        assert!(staging_x86_64(0).is_none());
306        assert_eq!(
307            staging_i386(195).unwrap()[1],
308            (1, Stage::Out(Len::Fixed(96), Produced::Whole))
309        );
310        assert_eq!(
311            staging_i386(106).unwrap()[1],
312            (1, Stage::Out(Len::Fixed(64), Produced::Whole))
313        );
314        assert_eq!(
315            staging_i386(268).unwrap()[1],
316            (2, Stage::Out(Len::Fixed(84), Produced::Whole))
317        );
318        assert_eq!(
319            staging_i386(80).unwrap()[0],
320            (1, Stage::Out(Len::Elems(0, 2), Produced::RetElems(2)))
321        );
322        assert_eq!(staging_i386(304).unwrap(), &[(0, Stage::Path), (2, Stage::Path)]);
323        assert!(trapped_i386(11) && trapped_i386(174) && !trapped_i386(120));
324        assert_eq!(
325            staging_aarch64(79).unwrap()[1],
326            (2, Stage::Out(Len::Fixed(128), Produced::Whole))
327        );
328        assert_eq!(
329            staging_aarch64(36).unwrap(),
330            &[(0, Stage::Path), (2, Stage::Path)]
331        );
332        assert!(trapped_aarch64(221) && trapped_aarch64(134) && !trapped_aarch64(220));
333        assert_eq!(
334            staging_arm(327).unwrap()[1],
335            (2, Stage::Out(Len::Fixed(104), Produced::Whole))
336        );
337        assert_eq!(
338            staging_arm(266).unwrap()[1],
339            (2, Stage::Out(Len::Fixed(84), Produced::Whole))
340        );
341        assert!(trapped_arm(387) && trapped_arm(174) && !trapped_arm(120));
342    }
343}