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 | |