• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

boustrophedon / extrasafe / 11256497533

09 Oct 2024 01:56PM UTC coverage: 77.969% (+0.5%) from 77.506%
11256497533

Pull #48

github

web-flow
Merge 139264a4c into b56d18c8d
Pull Request #48: feat(builtins): add builtin `UserId`

75 of 75 new or added lines in 3 files covered. (100.0%)

2 existing lines in 2 files now uncovered.

952 of 1221 relevant lines covered (77.97%)

93.34 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

22.73
/src/isolate/mod.rs
1
//! Extrasafe's `Isolate` allows you to run a subprocess in a [user
2
//! namespace](https://man7.org/linux/man-pages/man7/user_namespaces.7.html), which allows you to
3
//! isolate your program in order to e.g. run 
4
//!
5
//! Specifically, you can isolate:
6
//! - The filesystem. The Isolate creates a temporary directory and mounts a tmpfs onto it, and
7
//! then makes that tmpfs the new root. The original root filesystem becomes unaccessible.
8
//! Specific directories and files may be mounted into the tmpfs if desired.
9
//! - The network. A new network namespace may also be created. In the context of extrasafe, this
10
//! is mostly useful to disable the network and make it simpler to use seccomp.
11
//! - The program itself. In addition to running in a different memory space, so the original
12
//! program's data is unaffected by the subprocess, the Isolate is executed via an in-memory
13
//! copy of the program so that the program binary itself cannot be modified.
14

15
// options:
16
// - keep network
17
// - env vars
18
// - program name for ps (it's a prctl or something to set)
19
// - args identifier to catch we're in an isolate in main
20
// - bind mount directory list
21
// - tmpfs size?
22

23
use std::path::{Path, PathBuf};
24
use std::collections::HashMap;
25
use std::os::unix::process::CommandExt;
26
use std::os::fd::AsRawFd;
27
use std::process::Output;
28

29
mod isolate_sys;
30
use isolate_sys as system;
31

32
/// Error type for errors related to `Isolate`
33
#[derive(Debug)]
34
pub enum IsolateError {
35
    /// An error that occurred during memfd operations
36
    MemFd(std::io::Error),
37
    /// An error that occurred while spawning a `std::process::Command`
38
    Command(std::io::Error),
39
    /// An error that occurred while configuring a bindmount (in the subprocess but before clone)
40
    BindmountConfig(std::io::Error),
41

42
}
43

44
/// Allows creation of subprocesses which then use Linux user namespaces to isolate the program
45
#[derive(Debug)]
46
#[must_use]
47
pub struct Isolate {
48
    /// Isolate name, checked for in `argv[0]` inside `main_hook`. Note that this must be a
49
    /// `&'static str`
50
    isolate_name: &'static str,
51
    /// The function to execute inside the isolate
52
    func: fn() -> (),
53
    /// If true, start a new network namespace. Default: true
54
    new_network: bool,
55
    /// Size in MB of the root tmpfs filesystem of the isolate. Default is 10MB.
56
    root_fs_size: u32,
57
    /// A mapping of paths in the parent filesystem to be bindmounted to paths in the child
58
    /// filesystem.
59
    bindmounts: HashMap<PathBuf, PathBuf>,
60
}
61

62
impl Isolate {
63
    /// Create a new isolate that will execute the given function when called in a subprocess with
64
    /// `argv[0]` equal to `isolate_name`. func must not be a closure.
65
    pub fn new(isolate_name: &'static str, func: fn() -> ()) -> Isolate {
×
66
        Isolate {
×
67
            isolate_name,
×
68
            func,
×
69
            new_network: true,
×
70
            root_fs_size: 10,
×
71
            bindmounts: HashMap::new(),
×
72
        }
×
73
    }
×
74

75
    /// Bind mount the file or path in src to the file or path in dst. If `dst` is relative, it is
76
    /// treated as relative to the root of the isolate's tmpfs. If `dst` is absolute, it will still
77
    /// be joined as if it were relative to the isolate's tmpfs root. `dst` will be created if it does not
78
    /// exist, including all intermediate directories.
79
    ///
80
    /// Bindmounts to files and symlinks are allowed, but if a symlink points outside the current
81
    /// filesystem it will not function.
82
    ///
83
    /// Bind mounts are not created until `Isolate::main_hook` executes.
84
    pub fn add_bind_mount(mut self, src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Isolate {
×
85
        let src = src.as_ref();
×
86
        let dst = dst.as_ref();
×
87
        
×
88
        let _old = self.bindmounts.insert(src.into(), dst.into());
×
89
        self
×
90
    }
×
91

92
    /// Set the maximum size of the filesystem, in MiB.
93
    pub fn set_rootfs_size(mut self, size: u32) -> Self {
×
94
        self.root_fs_size = size;
×
95
        self
×
96
    }
×
97

98
    /// If true is passed, a new network namespace is created which is detached from all existing
99
    /// interfaces.
100
    pub fn new_network(mut self, new_network: bool) -> Self {
×
101
        self.new_network = new_network;
×
102
        self
×
103
    }
×
104

105
    /// Start a subprocess that will activate an Isolate with the given `isolate_name` and
106
    /// environment variables. Only the provided environment variables will be present in the
107
    /// subprocess and nothing else from the current process. The subprocess's `argv` will only
108
    /// contain `isolate_name`.
109
    ///
110
    /// # Errors
111
    /// Will return an error if starting the Command fails.
112
    pub fn run(isolate_name: &'static str, envs: &HashMap<String, String>) -> Result<Output, IsolateError> {
12✔
113
        let memfd = system::create_memfd_from_self_exe()?;
12✔
114
        std::process::Command::new(format!("/proc/self/fd/{}", memfd.as_raw_fd()))
12✔
115
            .arg0(isolate_name)
12✔
116
            .env_clear()
12✔
117
            .envs(envs)
12✔
118
            .output()
12✔
119
            .map_err(IsolateError::Command)
12✔
120
    }
12✔
121

122
    /// At the start of the program, call this function to check whether we are in an isolate
123
    /// subprocess and should run `func` after setting up the user namespace.
124
    ///
125
    /// # Panics
126
    /// If there is an error with starting the isolate, there's no way to recover so in all cases
127
    /// we panic.
128
    pub fn main_hook<F: Fn(&'static str) -> Isolate>(isolate_name: &'static str, builder: F) {
12✔
129
        /// drop wrapper for the tempdir so that even if there's a panic it should get cleaned up
130
        struct TempDirCleanup(pub PathBuf);
131
        impl Drop for TempDirCleanup {
UNCOV
132
            fn drop(&mut self) {
×
133
                std::fs::remove_dir(&self.0)
×
134
                    .expect("tmpfs dir was not empty");
×
135
            }
×
136
        }
137

138
        let mut args = std::env::args();
12✔
139
        // Check if we're supposed to be running as the specified isolate
140
        if let Some(arg0) = args.next() {
12✔
141
            if arg0 == isolate_name {
12✔
142
                let isolate = builder(isolate_name);
×
143

×
144
                let tempdir = system::make_tempdir(isolate_name);
×
145
                let tempdir_clean = TempDirCleanup(tempdir.clone());
×
146

×
147
                let mut child_stack = Vec::with_capacity(system::CHILD_STACK_SIZE);
×
148

×
149
                let (_child_pid, child_pidfd) = isolate.isolate_and_run(&mut child_stack, tempdir.clone());
×
150
                let child_ret = system::wait_for_child(child_pidfd);
×
151

×
152
                drop(tempdir_clean);
×
153
                std::process::exit(child_ret);
×
154
            }
12✔
155
        }
×
156
        // If we're not, just continue with the rest of the program
157
    }
12✔
158

159
    /// `clone`s into a new namespace, creates a tmpfs at `tempdir`, bindmounts the relevant
160
    /// directories into it, `pivot_root`s into the tmpfs, and runs `self.func`
161
    fn isolate_and_run(&self, child_stack: &mut [u8], tempdir: PathBuf) -> (libc::pid_t, libc::id_t) {
×
162
        let new_network = self.new_network;
×
163
        let data = system::IsolateConfigData::new(
×
164
            self.isolate_name,
×
165
            self.bindmounts.clone(),
×
166
            self.func,
×
167
            self.root_fs_size,
×
168
            tempdir);
×
169
        system::clone_into_namespace(child_stack, data, new_network)
×
170
    }
×
171
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc