xref: /aosp_15_r20/external/crosvm/e2e_tests/tests/fs.rs (revision bb4ee6a4ae7042d18b07a98463b9c8b875e44b39)
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