| 1 | mod common; |
| 2 | |
| 3 | // Implementation note: to allow unprivileged users to run it, this test makes |
| 4 | // use of user and mount namespaces. On systems that allow unprivileged user |
| 5 | // namespaces (Linux >= 3.8 compiled with CONFIG_USER_NS), the test should run |
| 6 | // without root. |
| 7 | |
| 8 | #[cfg (target_os = "linux" )] |
| 9 | mod test_mount { |
| 10 | use std::fs::{self, File}; |
| 11 | use std::io::{self, Read, Write}; |
| 12 | use std::os::unix::fs::OpenOptionsExt; |
| 13 | use std::os::unix::fs::PermissionsExt; |
| 14 | use std::process::{self, Command}; |
| 15 | |
| 16 | use libc::{EACCES, EROFS}; |
| 17 | |
| 18 | use nix::errno::Errno; |
| 19 | use nix::mount::{mount, umount, MsFlags}; |
| 20 | use nix::sched::{unshare, CloneFlags}; |
| 21 | use nix::sys::stat::{self, Mode}; |
| 22 | use nix::unistd::getuid; |
| 23 | |
| 24 | static SCRIPT_CONTENTS: &[u8] = b"#!/bin/sh |
| 25 | exit 23" ; |
| 26 | |
| 27 | const EXPECTED_STATUS: i32 = 23; |
| 28 | |
| 29 | const NONE: Option<&'static [u8]> = None; |
| 30 | #[allow (clippy::bind_instead_of_map)] // False positive |
| 31 | pub fn test_mount_tmpfs_without_flags_allows_rwx() { |
| 32 | let tempdir = tempfile::tempdir().unwrap(); |
| 33 | |
| 34 | mount( |
| 35 | NONE, |
| 36 | tempdir.path(), |
| 37 | Some(b"tmpfs" .as_ref()), |
| 38 | MsFlags::empty(), |
| 39 | NONE, |
| 40 | ) |
| 41 | .unwrap_or_else(|e| panic!("mount failed: {e}" )); |
| 42 | |
| 43 | let test_path = tempdir.path().join("test" ); |
| 44 | |
| 45 | // Verify write. |
| 46 | fs::OpenOptions::new() |
| 47 | .create(true) |
| 48 | .write(true) |
| 49 | .mode((Mode::S_IRWXU | Mode::S_IRWXG | Mode::S_IRWXO).bits()) |
| 50 | .open(&test_path) |
| 51 | .or_else(|e| { |
| 52 | if Errno::from_i32(e.raw_os_error().unwrap()) |
| 53 | == Errno::EOVERFLOW |
| 54 | { |
| 55 | // Skip tests on certain Linux kernels which have a bug |
| 56 | // regarding tmpfs in namespaces. |
| 57 | // Ubuntu 14.04 and 16.04 are known to be affected; 16.10 is |
| 58 | // not. There is no legitimate reason for open(2) to return |
| 59 | // EOVERFLOW here. |
| 60 | // https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1659087 |
| 61 | let stderr = io::stderr(); |
| 62 | let mut handle = stderr.lock(); |
| 63 | writeln!( |
| 64 | handle, |
| 65 | "Buggy Linux kernel detected. Skipping test." |
| 66 | ) |
| 67 | .unwrap(); |
| 68 | process::exit(0); |
| 69 | } else { |
| 70 | panic!("open failed: {e}" ); |
| 71 | } |
| 72 | }) |
| 73 | .and_then(|mut f| f.write(SCRIPT_CONTENTS)) |
| 74 | .unwrap_or_else(|e| panic!("write failed: {e}" )); |
| 75 | |
| 76 | // Verify read. |
| 77 | let mut buf = Vec::new(); |
| 78 | File::open(&test_path) |
| 79 | .and_then(|mut f| f.read_to_end(&mut buf)) |
| 80 | .unwrap_or_else(|e| panic!("read failed: {e}" )); |
| 81 | assert_eq!(buf, SCRIPT_CONTENTS); |
| 82 | |
| 83 | // Verify execute. |
| 84 | assert_eq!( |
| 85 | EXPECTED_STATUS, |
| 86 | Command::new(&test_path) |
| 87 | .status() |
| 88 | .unwrap_or_else(|e| panic!("exec failed: {e}" )) |
| 89 | .code() |
| 90 | .unwrap_or_else(|| panic!("child killed by signal" )) |
| 91 | ); |
| 92 | |
| 93 | umount(tempdir.path()).unwrap_or_else(|e| panic!("umount failed: {e}" )); |
| 94 | } |
| 95 | |
| 96 | pub fn test_mount_rdonly_disallows_write() { |
| 97 | let tempdir = tempfile::tempdir().unwrap(); |
| 98 | |
| 99 | mount( |
| 100 | NONE, |
| 101 | tempdir.path(), |
| 102 | Some(b"tmpfs" .as_ref()), |
| 103 | MsFlags::MS_RDONLY, |
| 104 | NONE, |
| 105 | ) |
| 106 | .unwrap_or_else(|e| panic!("mount failed: {e}" )); |
| 107 | |
| 108 | // EROFS: Read-only file system |
| 109 | assert_eq!( |
| 110 | EROFS, |
| 111 | File::create(tempdir.path().join("test" )) |
| 112 | .unwrap_err() |
| 113 | .raw_os_error() |
| 114 | .unwrap() |
| 115 | ); |
| 116 | |
| 117 | umount(tempdir.path()).unwrap_or_else(|e| panic!("umount failed: {e}" )); |
| 118 | } |
| 119 | |
| 120 | pub fn test_mount_noexec_disallows_exec() { |
| 121 | let tempdir = tempfile::tempdir().unwrap(); |
| 122 | |
| 123 | mount( |
| 124 | NONE, |
| 125 | tempdir.path(), |
| 126 | Some(b"tmpfs" .as_ref()), |
| 127 | MsFlags::MS_NOEXEC, |
| 128 | NONE, |
| 129 | ) |
| 130 | .unwrap_or_else(|e| panic!("mount failed: {e}" )); |
| 131 | |
| 132 | let test_path = tempdir.path().join("test" ); |
| 133 | |
| 134 | fs::OpenOptions::new() |
| 135 | .create(true) |
| 136 | .write(true) |
| 137 | .mode((Mode::S_IRWXU | Mode::S_IRWXG | Mode::S_IRWXO).bits()) |
| 138 | .open(&test_path) |
| 139 | .and_then(|mut f| f.write(SCRIPT_CONTENTS)) |
| 140 | .unwrap_or_else(|e| panic!("write failed: {e}" )); |
| 141 | |
| 142 | // Verify that we cannot execute despite a+x permissions being set. |
| 143 | let mode = stat::Mode::from_bits_truncate( |
| 144 | fs::metadata(&test_path) |
| 145 | .map(|md| md.permissions().mode()) |
| 146 | .unwrap_or_else(|e| panic!("metadata failed: {e}" )), |
| 147 | ); |
| 148 | |
| 149 | assert!( |
| 150 | mode.contains(Mode::S_IXUSR | Mode::S_IXGRP | Mode::S_IXOTH), |
| 151 | "{:?} did not have execute permissions" , |
| 152 | &test_path |
| 153 | ); |
| 154 | |
| 155 | // EACCES: Permission denied |
| 156 | assert_eq!( |
| 157 | EACCES, |
| 158 | Command::new(&test_path) |
| 159 | .status() |
| 160 | .unwrap_err() |
| 161 | .raw_os_error() |
| 162 | .unwrap() |
| 163 | ); |
| 164 | |
| 165 | umount(tempdir.path()).unwrap_or_else(|e| panic!("umount failed: {e}" )); |
| 166 | } |
| 167 | |
| 168 | pub fn test_mount_bind() { |
| 169 | let tempdir = tempfile::tempdir().unwrap(); |
| 170 | let file_name = "test" ; |
| 171 | |
| 172 | { |
| 173 | let mount_point = tempfile::tempdir().unwrap(); |
| 174 | |
| 175 | mount( |
| 176 | Some(tempdir.path()), |
| 177 | mount_point.path(), |
| 178 | NONE, |
| 179 | MsFlags::MS_BIND, |
| 180 | NONE, |
| 181 | ) |
| 182 | .unwrap_or_else(|e| panic!("mount failed: {e}" )); |
| 183 | |
| 184 | fs::OpenOptions::new() |
| 185 | .create(true) |
| 186 | .write(true) |
| 187 | .mode((Mode::S_IRWXU | Mode::S_IRWXG | Mode::S_IRWXO).bits()) |
| 188 | .open(mount_point.path().join(file_name)) |
| 189 | .and_then(|mut f| f.write(SCRIPT_CONTENTS)) |
| 190 | .unwrap_or_else(|e| panic!("write failed: {e}" )); |
| 191 | |
| 192 | umount(mount_point.path()) |
| 193 | .unwrap_or_else(|e| panic!("umount failed: {e}" )); |
| 194 | } |
| 195 | |
| 196 | // Verify the file written in the mount shows up in source directory, even |
| 197 | // after unmounting. |
| 198 | |
| 199 | let mut buf = Vec::new(); |
| 200 | File::open(tempdir.path().join(file_name)) |
| 201 | .and_then(|mut f| f.read_to_end(&mut buf)) |
| 202 | .unwrap_or_else(|e| panic!("read failed: {e}" )); |
| 203 | assert_eq!(buf, SCRIPT_CONTENTS); |
| 204 | } |
| 205 | |
| 206 | pub fn setup_namespaces() { |
| 207 | // Hold on to the uid in the parent namespace. |
| 208 | let uid = getuid(); |
| 209 | |
| 210 | unshare(CloneFlags::CLONE_NEWNS | CloneFlags::CLONE_NEWUSER).unwrap_or_else(|e| { |
| 211 | let stderr = io::stderr(); |
| 212 | let mut handle = stderr.lock(); |
| 213 | writeln!(handle, |
| 214 | "unshare failed: {e}. Are unprivileged user namespaces available?" ).unwrap(); |
| 215 | writeln!(handle, "mount is not being tested" ).unwrap(); |
| 216 | // Exit with success because not all systems support unprivileged user namespaces, and |
| 217 | // that's not what we're testing for. |
| 218 | process::exit(0); |
| 219 | }); |
| 220 | |
| 221 | // Map user as uid 1000. |
| 222 | fs::OpenOptions::new() |
| 223 | .write(true) |
| 224 | .open("/proc/self/uid_map" ) |
| 225 | .and_then(|mut f| f.write(format!("1000 {uid} 1 \n" ).as_bytes())) |
| 226 | .unwrap_or_else(|e| panic!("could not write uid map: {e}" )); |
| 227 | } |
| 228 | } |
| 229 | |
| 230 | // Test runner |
| 231 | |
| 232 | /// Mimic normal test output (hackishly). |
| 233 | #[cfg (target_os = "linux" )] |
| 234 | macro_rules! run_tests { |
| 235 | ( $($test_fn:ident),* ) => {{ |
| 236 | println!(); |
| 237 | |
| 238 | $( |
| 239 | print!("test test_mount::{} ... " , stringify!($test_fn)); |
| 240 | $test_fn(); |
| 241 | println!("ok" ); |
| 242 | )* |
| 243 | |
| 244 | println!(); |
| 245 | }} |
| 246 | } |
| 247 | |
| 248 | #[cfg (target_os = "linux" )] |
| 249 | fn main() { |
| 250 | use test_mount::{ |
| 251 | setup_namespaces, test_mount_bind, test_mount_noexec_disallows_exec, |
| 252 | test_mount_rdonly_disallows_write, |
| 253 | test_mount_tmpfs_without_flags_allows_rwx, |
| 254 | }; |
| 255 | skip_if_cirrus!("Fails for an unknown reason Cirrus CI. Bug #1351" ); |
| 256 | setup_namespaces(); |
| 257 | |
| 258 | run_tests!( |
| 259 | test_mount_tmpfs_without_flags_allows_rwx, |
| 260 | test_mount_rdonly_disallows_write, |
| 261 | test_mount_noexec_disallows_exec, |
| 262 | test_mount_bind |
| 263 | ); |
| 264 | } |
| 265 | |
| 266 | #[cfg (not(target_os = "linux" ))] |
| 267 | fn main() {} |
| 268 | |