1 // Copyright 2023 The ChromiumOS Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 //! Testing virtio-fs.
6
7 #![cfg(any(target_os = "android", target_os = "linux"))]
8
9 use std::path::Path;
10
11 use fixture::vhost_user::CmdType;
12 use fixture::vhost_user::Config as VuConfig;
13 use fixture::vhost_user::VhostUserBackend;
14 use fixture::vm::Config;
15 use fixture::vm::TestVm;
16 use tempfile::NamedTempFile;
17 use tempfile::TempDir;
18
19 /// Tests file copy
20 ///
21 /// 1. Create `original.txt` on a temporal directory.
22 /// 2. Start a VM with a virtiofs device for the temporal directory.
23 /// 3. Copy `original.txt` to `new.txt` in the guest.
24 /// 4. Check that `new.txt` is created in the host.
copy_file(mut vm: TestVm, tag: &str, dir: TempDir)25 fn copy_file(mut vm: TestVm, tag: &str, dir: TempDir) {
26 const ORIGINAL_FILE_NAME: &str = "original.txt";
27 const NEW_FILE_NAME: &str = "new.txt";
28 const TEST_DATA: &str = "virtiofs works!";
29
30 let orig_file = dir.path().join(ORIGINAL_FILE_NAME);
31
32 std::fs::write(orig_file, TEST_DATA).unwrap();
33
34 // TODO(b/269137600): Split this into multiple lines instead of connecting commands with `&&`.
35 vm.exec_in_guest(&format!(
36 "mount -t virtiofs {tag} /mnt && cp /mnt/{} /mnt/{} && sync",
37 ORIGINAL_FILE_NAME, NEW_FILE_NAME,
38 ))
39 .unwrap();
40
41 let new_file = dir.path().join(NEW_FILE_NAME);
42 let contents = std::fs::read(new_file).unwrap();
43 assert_eq!(TEST_DATA.as_bytes(), &contents);
44 }
45
46 /// Tests mount/read/create/write
47 /// 1. Create `read_file.txt` with test data in host's temporal directory.
48 /// 2. Start a VM with a virtiofs device for the temporal directory.
49 /// 3. Guest reads read_file.txt file & verify the content is test data
50 /// 4. Guest creates a write_file.txt file in shared directory
51 /// 5. Host reads file from host's temporal directory & verify content is test data
mount_rw(mut vm: TestVm, tag: &str, dir: TempDir)52 fn mount_rw(mut vm: TestVm, tag: &str, dir: TempDir) {
53 const READ_FILE_NAME: &str = "read_test.txt";
54 const WRITE_FILE_NAME: &str = "write_test.txt";
55 const TEST_DATA: &str = "hello world";
56
57 let read_test_file = dir.path().join(READ_FILE_NAME);
58 let write_test_file = dir.path().join(WRITE_FILE_NAME);
59 std::fs::write(read_test_file, TEST_DATA).unwrap();
60
61 assert_eq!(
62 vm.exec_in_guest(&format!(
63 "mount -t virtiofs {tag} /mnt && cat /mnt/read_test.txt"
64 ))
65 .unwrap()
66 .stdout
67 .trim(),
68 TEST_DATA
69 );
70
71 const IN_FS_WRITE_FILE_PATH: &str = "/mnt/write_test.txt";
72 let _ = vm.exec_in_guest(&format!("echo -n {TEST_DATA} > {IN_FS_WRITE_FILE_PATH}"));
73 let read_contents = std::fs::read(write_test_file).unwrap();
74 assert_eq!(TEST_DATA.as_bytes(), &read_contents);
75 }
76
77 #[test]
fs_copy_file()78 fn fs_copy_file() {
79 let tag = "mtdtest";
80 let temp_dir = tempfile::tempdir().unwrap();
81
82 let config = Config::new().extra_args(vec![
83 "--shared-dir".to_string(),
84 format!(
85 "{}:{tag}:type=fs:cache=auto",
86 temp_dir.path().to_str().unwrap()
87 ),
88 ]);
89
90 let vm = TestVm::new(config).unwrap();
91 copy_file(vm, tag, temp_dir)
92 }
93
94 #[test]
fs_mount_rw()95 fn fs_mount_rw() {
96 let tag = "mtdtest";
97 let temp_dir = tempfile::tempdir().unwrap();
98
99 let config = Config::new().extra_args(vec![
100 "--shared-dir".to_string(),
101 format!(
102 "{}:{tag}:type=fs:cache=auto",
103 temp_dir.path().to_str().unwrap()
104 ),
105 ]);
106
107 let vm = TestVm::new(config).unwrap();
108 mount_rw(vm, tag, temp_dir)
109 }
110
111 /// Tests file ownership seen by the VM.
112 ///
113 /// 1. Create `user_file.txt` owned by the current user of the host on a temporal directory.
114 /// 2. Set virtiofs options: uidmap=<mapped-uid> <current-uid> 1, uid=<mapped-uid>.
115 /// 3. Start a VM with a virtiofs device for the temporal directory.
116 /// 4. Check that `user_file.txt`'s uid is <mapped-uid> in the VM.
117 /// 5. Verify gid similarly.
118 #[test]
file_ugid()119 fn file_ugid() {
120 const FILE_NAME: &str = "user_file.txt";
121 let uid = base::geteuid();
122 let gid = base::getegid();
123 let mapped_uid: u32 = rand::random();
124 let mapped_gid: u32 = rand::random();
125 let uid_map: String = format!("{} {} 1", mapped_uid, uid);
126 let gid_map = format!("{} {} 1", mapped_gid, gid);
127
128 let temp_dir = tempfile::tempdir().unwrap();
129 let orig_file = temp_dir.path().join(FILE_NAME);
130
131 std::fs::write(orig_file, "").unwrap();
132
133 let tag = "mtdtest";
134
135 let config = Config::new().extra_args(vec![
136 "--shared-dir".to_string(),
137 format!(
138 "{}:{tag}:type=fs:uidmap={}:gidmap={}:uid={}:gid={}",
139 temp_dir.path().to_str().unwrap(),
140 uid_map,
141 gid_map,
142 mapped_uid,
143 mapped_gid
144 ),
145 ]);
146
147 let mut vm = TestVm::new(config).unwrap();
148 vm.exec_in_guest(&format!("mount -t virtiofs {tag} /mnt"))
149 .unwrap();
150 let output = vm
151 .exec_in_guest(&format!("stat /mnt/{}", FILE_NAME,))
152 .unwrap();
153 // stat output example:
154 // File: /mnt/user_file.txt
155 // Size: 0 Blocks: 0 IO Block: 4096 regular empty file
156 // Device: 0,11 Inode: 11666031 Links: 1
157 // Access: (0640/-rw-r-----) Uid: (2350626183/ UNKNOWN) Gid: (949179291/ UNKNOWN)
158 // Access: 2023-04-05 03:06:27.110144457 +0000
159 // Modify: 2023-04-05 03:06:27.110144457 +0000
160 // Change: 2023-04-05 03:06:27.110144457 +0000
161 assert!(output.stdout.contains(&format!("Uid: ({}/", mapped_uid)));
162 assert!(output.stdout.contains(&format!("Gid: ({}/", mapped_gid)));
163 }
164
create_vu_fs_config(socket: &Path, shared_dir: &Path, tag: &str) -> VuConfig165 pub fn create_vu_fs_config(socket: &Path, shared_dir: &Path, tag: &str) -> VuConfig {
166 let uid = base::geteuid();
167 let gid = base::getegid();
168 let socket_path = socket.to_str().unwrap();
169 let shared_dir_path = shared_dir.to_str().unwrap();
170 println!("socket={socket_path}, tag={tag}, shared_dir={shared_dir_path}");
171 VuConfig::new(CmdType::Device, "vhost-user-fs").extra_args(vec![
172 "fs".to_string(),
173 format!("--socket-path={socket_path}"),
174 format!("--shared-dir={shared_dir_path}"),
175 format!("--tag={tag}"),
176 format!("--uid-map=0 {uid} 1"),
177 format!("--gid-map=0 {gid} 1"),
178 ])
179 }
180
181 /// Tests vhost-user fs device copy file.
182 #[test]
vhost_user_fs_copy_file()183 fn vhost_user_fs_copy_file() {
184 let socket = NamedTempFile::new().unwrap();
185 let temp_dir = tempfile::tempdir().unwrap();
186
187 let config = Config::new();
188 let tag = "mtdtest";
189
190 let vu_config = create_vu_fs_config(socket.path(), temp_dir.path(), tag);
191 let _vu_device = VhostUserBackend::new(vu_config).unwrap();
192
193 let config = config.with_vhost_user_fs(socket.path(), tag);
194 let vm = TestVm::new(config).unwrap();
195
196 copy_file(vm, tag, temp_dir);
197 }
198
199 /// Tests vhost-user fs device mount and read write.
200 #[test]
vhost_user_fs_mount_rw()201 fn vhost_user_fs_mount_rw() {
202 let socket = NamedTempFile::new().unwrap();
203 let temp_dir = tempfile::tempdir().unwrap();
204
205 let config = Config::new();
206 let tag = "mtdtest";
207
208 let vu_config = create_vu_fs_config(socket.path(), temp_dir.path(), tag);
209 let _vu_device = VhostUserBackend::new(vu_config).unwrap();
210
211 let config = config.with_vhost_user_fs(socket.path(), tag);
212 let vm = TestVm::new(config).unwrap();
213
214 mount_rw(vm, tag, temp_dir);
215 }
216
copy_file_validate_ugid_mapping( mut vm: TestVm, tag: &str, dir: TempDir, mapped_uid: u32, mapped_gid: u32, )217 fn copy_file_validate_ugid_mapping(
218 mut vm: TestVm,
219 tag: &str,
220 dir: TempDir,
221 mapped_uid: u32,
222 mapped_gid: u32,
223 ) {
224 use std::os::linux::fs::MetadataExt;
225 const ORIGINAL_FILE_NAME: &str = "original.txt";
226 const NEW_FILE_NAME: &str = "new.txt";
227 const TEST_DATA: &str = "Hello world!";
228
229 let orig_file = dir.path().join(ORIGINAL_FILE_NAME);
230
231 std::fs::write(orig_file, TEST_DATA).unwrap();
232
233 vm.exec_in_guest(&format!(
234 "mount -t virtiofs {tag} /mnt && cp /mnt/{} /mnt/{} && sync",
235 ORIGINAL_FILE_NAME, NEW_FILE_NAME,
236 ))
237 .unwrap();
238
239 let output = vm
240 .exec_in_guest(&format!("stat /mnt/{}", ORIGINAL_FILE_NAME,))
241 .unwrap();
242
243 assert!(output.stdout.contains(&format!("Uid: ({}/", mapped_uid)));
244 assert!(output.stdout.contains(&format!("Gid: ({}/", mapped_gid)));
245
246 let new_file = dir.path().join(NEW_FILE_NAME);
247 let output_stat = std::fs::metadata(new_file.clone());
248
249 assert_eq!(
250 output_stat
251 .as_ref()
252 .expect("stat of new_file failed")
253 .st_uid(),
254 base::geteuid()
255 );
256 assert_eq!(
257 output_stat
258 .as_ref()
259 .expect("stat of new_file failed")
260 .st_gid(),
261 base::getegid()
262 );
263
264 let contents = std::fs::read(new_file).unwrap();
265 assert_eq!(TEST_DATA.as_bytes(), &contents);
266 }
267
create_ugid_map_config( socket: &Path, shared_dir: &Path, tag: &str, mapped_uid: u32, mapped_gid: u32, ) -> VuConfig268 pub fn create_ugid_map_config(
269 socket: &Path,
270 shared_dir: &Path,
271 tag: &str,
272 mapped_uid: u32,
273 mapped_gid: u32,
274 ) -> VuConfig {
275 let socket_path = socket.to_str().unwrap();
276 let shared_dir_path = shared_dir.to_str().unwrap();
277
278 let uid = base::geteuid();
279 let gid = base::getegid();
280 let ugid_map_value = format!("{} {} {} {} 7 /", mapped_uid, mapped_gid, uid, gid,);
281
282 let cfg_arg = format!("writeback=true,ugid_map='{}'", ugid_map_value);
283
284 println!("socket={socket_path}, tag={tag}, shared_dir={shared_dir_path}");
285
286 VuConfig::new(CmdType::Device, "vhost-user-fs").extra_args(vec![
287 "fs".to_string(),
288 format!("--socket-path={socket_path}"),
289 format!("--shared-dir={shared_dir_path}"),
290 format!("--tag={tag}"),
291 format!("--cfg={cfg_arg}"),
292 format!("--disable-sandbox"),
293 format!("--skip-pivot-root=true"),
294 ])
295 }
296
297 /// Tests file copy with disabled sandbox
298 ///
299 /// 1. Create `original.txt` on a temporal directory.
300 /// 2. Setup ugid_map for vhost-user-fs backend
301 /// 3. Start a VM with a virtiofs device for the temporal directory.
302 /// 4. Copy `original.txt` to `new.txt` in the guest.
303 /// 5. Check that `new.txt` is created in the host.
304 /// 6. Verify the UID/GID of the files both in the guest and the host.
305 #[test]
vhost_user_fs_without_sandbox_and_pivot_root()306 fn vhost_user_fs_without_sandbox_and_pivot_root() {
307 let socket = NamedTempFile::new().unwrap();
308 let temp_dir = tempfile::tempdir().unwrap();
309
310 let config = Config::new();
311 let tag = "android";
312
313 let mapped_uid = 123456;
314 let mapped_gid = 12345;
315 let vu_config =
316 create_ugid_map_config(socket.path(), temp_dir.path(), tag, mapped_uid, mapped_gid);
317
318 let _vu_device = VhostUserBackend::new(vu_config).unwrap();
319
320 let config = config.with_vhost_user_fs(socket.path(), tag);
321 let vm = TestVm::new(config).unwrap();
322
323 copy_file_validate_ugid_mapping(vm, tag, temp_dir, mapped_uid, mapped_gid);
324 }
325