summaryrefslogtreecommitdiff
path: root/test/test_mount.rs
blob: d2e08bc42855d60a72c1cf80366b47f13225fd10 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
// Impelmentation note: to allow unprivileged users to run it, this test makes
// use of user and mount namespaces. On systems that allow unprivileged user
// namespaces (Linux >= 3.8 compiled with CONFIG_USER_NS), the test should run
// without root.

extern crate libc;
extern crate nix;
extern crate tempfile;

#[cfg(target_os = "linux")]
mod test_mount {
    use std::fs::{self, File};
    use std::io::{self, Read, Write};
    use std::os::unix::fs::OpenOptionsExt;
    use std::os::unix::fs::PermissionsExt;
    use std::process::{self, Command};

    use libc::{EACCES, EROFS};

    use nix::errno::Errno;
    use nix::mount::{mount, umount, MsFlags};
    use nix::sched::{unshare, CloneFlags};
    use nix::sys::stat::{self, Mode};
    use nix::unistd::getuid;

    use tempfile;

    static SCRIPT_CONTENTS: &'static [u8] = b"#!/bin/sh
exit 23";

    const EXPECTED_STATUS: i32 = 23;

    const NONE: Option<&'static [u8]> = None;
    pub fn test_mount_tmpfs_without_flags_allows_rwx() {
        let tempdir = tempfile::tempdir().unwrap();

        mount(NONE,
              tempdir.path(),
              Some(b"tmpfs".as_ref()),
              MsFlags::empty(),
              NONE)
            .unwrap_or_else(|e| panic!("mount failed: {}", e));

        let test_path = tempdir.path().join("test");

        // Verify write.
        fs::OpenOptions::new()
            .create(true)
            .write(true)
            .mode((Mode::S_IRWXU | Mode::S_IRWXG | Mode::S_IRWXO).bits())
            .open(&test_path)
            .or_else(|e|
                if Errno::from_i32(e.raw_os_error().unwrap()) == Errno::EOVERFLOW {
                    // Skip tests on certain Linux kernels which have a bug
                    // regarding tmpfs in namespaces.
                    // Ubuntu 14.04 and 16.04 are known to be affected; 16.10 is
                    // not.  There is no legitimate reason for open(2) to return
                    // EOVERFLOW here.
                    // https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1659087
                    let stderr = io::stderr();
                    let mut handle = stderr.lock();
                    writeln!(handle, "Buggy Linux kernel detected.  Skipping test.")
                    .unwrap();
                    process::exit(0);
               } else {
                   panic!("open failed: {}", e);
               }
            )
            .and_then(|mut f| f.write(SCRIPT_CONTENTS))
            .unwrap_or_else(|e| panic!("write failed: {}", e));

        // Verify read.
        let mut buf = Vec::new();
        File::open(&test_path)
            .and_then(|mut f| f.read_to_end(&mut buf))
            .unwrap_or_else(|e| panic!("read failed: {}", e));
        assert_eq!(buf, SCRIPT_CONTENTS);

        // Verify execute.
        assert_eq!(EXPECTED_STATUS,
                   Command::new(&test_path)
                       .status()
                       .unwrap_or_else(|e| panic!("exec failed: {}", e))
                       .code()
                       .unwrap_or_else(|| panic!("child killed by signal")));

        umount(tempdir.path()).unwrap_or_else(|e| panic!("umount failed: {}", e));
    }

    pub fn test_mount_rdonly_disallows_write() {
        let tempdir = tempfile::tempdir().unwrap();

        mount(NONE,
              tempdir.path(),
              Some(b"tmpfs".as_ref()),
              MsFlags::MS_RDONLY,
              NONE)
            .unwrap_or_else(|e| panic!("mount failed: {}", e));

        // EROFS: Read-only file system
        assert_eq!(EROFS as i32,
                   File::create(tempdir.path().join("test")).unwrap_err().raw_os_error().unwrap());

        umount(tempdir.path()).unwrap_or_else(|e| panic!("umount failed: {}", e));
    }

    pub fn test_mount_noexec_disallows_exec() {
        let tempdir = tempfile::tempdir().unwrap();

        mount(NONE,
              tempdir.path(),
              Some(b"tmpfs".as_ref()),
              MsFlags::MS_NOEXEC,
              NONE)
            .unwrap_or_else(|e| panic!("mount failed: {}", e));

        let test_path = tempdir.path().join("test");

        fs::OpenOptions::new()
            .create(true)
            .write(true)
            .mode((Mode::S_IRWXU | Mode::S_IRWXG | Mode::S_IRWXO).bits())
            .open(&test_path)
            .and_then(|mut f| f.write(SCRIPT_CONTENTS))
            .unwrap_or_else(|e| panic!("write failed: {}", e));

        // Verify that we cannot execute despite a+x permissions being set.
        let mode = stat::Mode::from_bits_truncate(fs::metadata(&test_path)
                                                      .map(|md| md.permissions().mode())
                                                      .unwrap_or_else(|e| {
                                                          panic!("metadata failed: {}", e)
                                                      }));

        assert!(mode.contains(Mode::S_IXUSR | Mode::S_IXGRP | Mode::S_IXOTH),
                "{:?} did not have execute permissions",
                &test_path);

        // EACCES: Permission denied
        assert_eq!(EACCES as i32,
                   Command::new(&test_path).status().unwrap_err().raw_os_error().unwrap());

        umount(tempdir.path()).unwrap_or_else(|e| panic!("umount failed: {}", e));
    }

    pub fn test_mount_bind() {
        let tempdir = tempfile::tempdir().unwrap();
        let file_name = "test";

        {
            let mount_point = tempfile::tempdir().unwrap();

            mount(Some(tempdir.path()),
                  mount_point.path(),
                  NONE,
                  MsFlags::MS_BIND,
                  NONE)
                .unwrap_or_else(|e| panic!("mount failed: {}", e));

            fs::OpenOptions::new()
                .create(true)
                .write(true)
                .mode((Mode::S_IRWXU | Mode::S_IRWXG | Mode::S_IRWXO).bits())
                .open(mount_point.path().join(file_name))
                .and_then(|mut f| f.write(SCRIPT_CONTENTS))
                .unwrap_or_else(|e| panic!("write failed: {}", e));

            umount(mount_point.path()).unwrap_or_else(|e| panic!("umount failed: {}", e));
        }

        // Verify the file written in the mount shows up in source directory, even
        // after unmounting.

        let mut buf = Vec::new();
        File::open(tempdir.path().join(file_name))
            .and_then(|mut f| f.read_to_end(&mut buf))
            .unwrap_or_else(|e| panic!("read failed: {}", e));
        assert_eq!(buf, SCRIPT_CONTENTS);
    }

    pub fn setup_namespaces() {
        // Hold on to the uid in the parent namespace.
        let uid = getuid();

        unshare(CloneFlags::CLONE_NEWNS | CloneFlags::CLONE_NEWUSER).unwrap_or_else(|e| {
            let stderr = io::stderr();
            let mut handle = stderr.lock();
            writeln!(handle,
                     "unshare failed: {}. Are unprivileged user namespaces available?",
                     e).unwrap();
            writeln!(handle, "mount is not being tested").unwrap();
            // Exit with success because not all systems support unprivileged user namespaces, and
            // that's not what we're testing for.
            process::exit(0);
        });

        // Map user as uid 1000.
        fs::OpenOptions::new()
            .write(true)
            .open("/proc/self/uid_map")
            .and_then(|mut f| f.write(format!("1000 {} 1\n", uid).as_bytes()))
            .unwrap_or_else(|e| panic!("could not write uid map: {}", e));
    }
}


// Test runner

/// Mimic normal test output (hackishly).
#[cfg(target_os = "linux")]
macro_rules! run_tests {
    ( $($test_fn:ident),* ) => {{
        println!();

        $(
            print!("test test_mount::{} ... ", stringify!($test_fn));
            $test_fn();
            println!("ok");
        )*

        println!();
    }}
}

#[cfg(target_os = "linux")]
fn main() {
    use test_mount::{setup_namespaces, test_mount_tmpfs_without_flags_allows_rwx,
                     test_mount_rdonly_disallows_write, test_mount_noexec_disallows_exec,
                     test_mount_bind};
    setup_namespaces();

    run_tests!(test_mount_tmpfs_without_flags_allows_rwx,
               test_mount_rdonly_disallows_write,
               test_mount_noexec_disallows_exec,
               test_mount_bind);
}

#[cfg(not(target_os = "linux"))]
fn main() {}