devices/virtio/vhost_user_backend/
fs.rs

1// Copyright 2021 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
5mod sys;
6
7use std::collections::BTreeMap;
8use std::path::PathBuf;
9use std::sync::Arc;
10use std::sync::RwLock;
11
12use anyhow::bail;
13use argh::FromArgs;
14use base::error;
15use base::info;
16use base::warn;
17use base::AsRawDescriptor;
18use base::FromRawDescriptor;
19use base::IntoRawDescriptor;
20use base::RawDescriptor;
21use base::SafeDescriptor;
22use base::Tube;
23use base::UnixSeqpacketListener;
24use base::WorkerThread;
25use data_model::Le32;
26use fuse::Server;
27use hypervisor::ProtectionType;
28use snapshot::AnySnapshot;
29use sync::Mutex;
30pub use sys::start_device as run_fs_device;
31use virtio_sys::virtio_fs::virtio_fs_config;
32use vm_control::FsAllowlistCommand;
33use vm_control::FsAllowlistResponse;
34use vm_memory::GuestMemory;
35use vmm_vhost::message::VhostUserProtocolFeatures;
36use vmm_vhost::VHOST_USER_F_PROTOCOL_FEATURES;
37use zerocopy::IntoBytes;
38
39use crate::virtio;
40use crate::virtio::copy_config;
41use crate::virtio::device_constants::fs::FS_MAX_TAG_LEN;
42use crate::virtio::fs::passthrough::PassthroughFs;
43use crate::virtio::fs::Config;
44use crate::virtio::fs::PathAllowlist;
45use crate::virtio::fs::Worker;
46use crate::virtio::vhost_user_backend::handler::Error as DeviceError;
47use crate::virtio::vhost_user_backend::handler::VhostUserDevice;
48use crate::virtio::Queue;
49
50const MAX_QUEUE_NUM: usize = 2; /* worker queue and high priority queue */
51
52pub(crate) struct FsBackend {
53    server: Arc<fuse::Server<PassthroughFs>>,
54    tag: String,
55    avail_features: u64,
56    workers: BTreeMap<usize, WorkerThread<Queue>>,
57    keep_rds: Vec<RawDescriptor>,
58    unmap_guest_memory_on_fork: bool,
59    allowlist_socket_fd: Option<SafeDescriptor>,
60    allowlist: Option<Arc<RwLock<PathAllowlist>>>,
61}
62
63/// Runs a listener thread that processes allowlist control commands from a Unix socket.
64///
65/// # Protocol Specification
66///
67/// The control socket communicates using JSON-serialized structured messages over `SOCK_SEQPACKET`
68/// (wrapped in a `crosvm::base::Tube`).
69///
70/// * **Request Format (`FsAllowlistCommand`)**:
71///   - `{"AddPaths": {"paths": ["/absolute/path"]}}` (to add paths to the allowlist)
72///   - `{"RemovePaths": {"paths": ["/absolute/path"]}}` (to remove paths from the allowlist)
73/// * **Response Format (`FsAllowlistResponse`)**:
74///   - `"Ok"` (success)
75///   - `{"Err": "error message details"}` (error)
76fn handle_client_session(tube: Tube, allowlist: &Arc<RwLock<PathAllowlist>>) {
77    loop {
78        match tube.recv::<FsAllowlistCommand>() {
79            Ok(cmd) => {
80                let result = match cmd {
81                    FsAllowlistCommand::AddPaths { paths } => {
82                        info!("Allowlist socket: Add paths {:?}", paths);
83                        let mut al_guard = allowlist.write().expect(
84                            "Allowlist lock poisoned during write (add_paths). Terminating.",
85                        );
86                        let mut al_clone = al_guard.clone();
87                        let mut success = true;
88                        for path in &paths {
89                            if !al_clone.add_path(path) {
90                                error!("Allowlist socket: Failed to add invalid path: {:?}", path);
91                                success = false;
92                                break;
93                            }
94                        }
95                        if success {
96                            *al_guard = al_clone;
97                            FsAllowlistResponse::Ok
98                        } else {
99                            FsAllowlistResponse::Err("Failed to add one or more paths".to_string())
100                        }
101                    }
102                    FsAllowlistCommand::RemovePaths { paths } => {
103                        info!("Allowlist socket: Remove paths {:?}", paths);
104                        let mut al_guard = allowlist.write().expect(
105                            "Allowlist lock poisoned during write (remove_paths). Terminating.",
106                        );
107                        let mut al_clone = al_guard.clone();
108                        let mut success = true;
109                        for path in &paths {
110                            if !al_clone.remove_path(path) {
111                                error!("Allowlist socket: Failed to remove path: {:?}", path);
112                                success = false;
113                                break;
114                            }
115                        }
116                        if success {
117                            *al_guard = al_clone;
118                            FsAllowlistResponse::Ok
119                        } else {
120                            FsAllowlistResponse::Err(
121                                "Failed to remove one or more paths".to_string(),
122                            )
123                        }
124                    }
125                };
126
127                if let Err(e) = tube.send(&result) {
128                    error!("Allowlist socket: Failed to send response: {}", e);
129                }
130            }
131            Err(base::TubeError::Disconnected) => {
132                info!("Allowlist socket: Client disconnected");
133                break;
134            }
135            Err(e) => {
136                error!("Allowlist socket: Error reading from control socket: {}", e);
137                break;
138            }
139        }
140    }
141}
142
143fn run_allowlist_listener(fd: SafeDescriptor, allowlist: Arc<RwLock<PathAllowlist>>) {
144    // Release ownership of the fd so it can be re-acquired by UnixSeqpacketListener::bind
145    // via the /proc/self/fd/ path.
146    // Since UnixSeqpacketListener::bind will recreate a SafeDescriptor from the fd number,
147    // we must ensure the original SafeDescriptor (fd) is NOT dropped (which would close it).
148    // We do this by converting fd into a raw descriptor (losing ownership).
149    let raw_fd = fd.into_raw_descriptor();
150    let path = format!("/proc/self/fd/{raw_fd}");
151    let listener = match UnixSeqpacketListener::bind(&path) {
152        Ok(l) => l,
153        Err(e) => {
154            error!(
155                "Allowlist socket: Failed to re-create listener from fd: {}",
156                e
157            );
158            // Re-wrap the raw fd to ensure it gets closed since bind failed to take ownership.
159            // SAFETY: safe because we owned it before and nobody else is using it.
160            let _ = unsafe { SafeDescriptor::from_raw_descriptor(raw_fd) };
161            return;
162        }
163    };
164
165    loop {
166        match listener.accept() {
167            Ok(seqpacket) => {
168                let tube = match Tube::try_from(seqpacket) {
169                    Ok(t) => t,
170                    Err(e) => {
171                        error!("Allowlist socket: Failed to create Tube: {}", e);
172                        continue;
173                    }
174                };
175                handle_client_session(tube, &allowlist);
176            }
177            Err(e) => {
178                error!("Allowlist socket: Accept failed: {}", e);
179                break;
180            }
181        }
182    }
183}
184
185impl FsBackend {
186    #[allow(unused_variables)]
187    pub fn new(
188        tag: &str,
189        shared_dir: &str,
190        skip_pivot_root: bool,
191        cfg: Option<Config>,
192        allowlist_socket_fd: Option<RawDescriptor>,
193    ) -> anyhow::Result<Self> {
194        if tag.len() > FS_MAX_TAG_LEN {
195            bail!(
196                "fs tag is too long: {} (max supported: {})",
197                tag.len(),
198                FS_MAX_TAG_LEN
199            );
200        }
201
202        let avail_features = virtio::base_features(ProtectionType::Unprotected)
203            | 1 << VHOST_USER_F_PROTOCOL_FEATURES;
204
205        let cfg = cfg.unwrap_or_default();
206
207        #[cfg(any(target_os = "android", target_os = "linux"))]
208        let unmap_guest_memory_on_fork = cfg.unmap_guest_memory_on_fork;
209        #[cfg(not(any(target_os = "android", target_os = "linux")))]
210        let unmap_guest_memory_on_fork = false;
211
212        // Use default passthroughfs config
213        #[allow(unused_mut)]
214        let mut fs = PassthroughFs::new(tag, cfg)?;
215        #[cfg(feature = "fs_runtime_ugid_map")]
216        if skip_pivot_root {
217            fs.set_root_dir(shared_dir.to_string())?;
218        }
219
220        let allowlist_socket_fd = allowlist_socket_fd.map(|fd| {
221            // SAFETY: safe because we own the file descriptor and nobody else is using it.
222            unsafe { SafeDescriptor::from_raw_descriptor(fd) }
223        });
224
225        let allowlist = if allowlist_socket_fd.is_some() {
226            let al = Arc::new(RwLock::new(PathAllowlist::new()));
227            fs.set_allowlist(Some(al.clone()));
228            Some(al)
229        } else {
230            None
231        };
232
233        let mut keep_rds: Vec<RawDescriptor> = [0, 1, 2].to_vec();
234        keep_rds.append(&mut fs.keep_rds());
235        if let Some(ref fd) = allowlist_socket_fd {
236            keep_rds.push(fd.as_raw_descriptor());
237        }
238
239        let server = Arc::new(Server::new(fs));
240
241        Ok(FsBackend {
242            server,
243            tag: tag.to_owned(),
244            avail_features,
245            workers: Default::default(),
246            keep_rds,
247            unmap_guest_memory_on_fork,
248            allowlist_socket_fd,
249            allowlist,
250        })
251    }
252
253    pub fn start_allowlist_listener(&mut self) {
254        if let Some(fd) = self.allowlist_socket_fd.take() {
255            if let Some(allowlist) = &self.allowlist {
256                let allowlist = allowlist.clone();
257                let result = std::thread::Builder::new()
258                    .name("fs_allowlist_listener".to_string())
259                    .spawn(move || {
260                        run_allowlist_listener(fd, allowlist);
261                    });
262                if let Err(e) = result {
263                    error!("Failed to spawn allowlist listener thread: {}", e);
264                }
265            }
266        }
267    }
268}
269
270impl VhostUserDevice for FsBackend {
271    fn max_queue_num(&self) -> usize {
272        MAX_QUEUE_NUM
273    }
274
275    fn features(&self) -> u64 {
276        self.avail_features
277    }
278
279    fn protocol_features(&self) -> VhostUserProtocolFeatures {
280        VhostUserProtocolFeatures::CONFIG | VhostUserProtocolFeatures::MQ
281    }
282
283    fn read_config(&self, offset: u64, data: &mut [u8]) {
284        let mut config = virtio_fs_config {
285            tag: [0; FS_MAX_TAG_LEN],
286            num_request_queues: Le32::from(1),
287        };
288        config.tag[..self.tag.len()].copy_from_slice(self.tag.as_bytes());
289        copy_config(data, 0, config.as_bytes(), offset);
290    }
291
292    fn reset(&mut self) {
293        for worker in std::mem::take(&mut self.workers).into_values() {
294            let _ = worker.stop();
295        }
296    }
297
298    fn start_queue(
299        &mut self,
300        idx: usize,
301        queue: virtio::Queue,
302        _mem: GuestMemory,
303    ) -> anyhow::Result<()> {
304        if self.workers.contains_key(&idx) {
305            warn!("Starting new queue handler without stopping old handler");
306            self.stop_queue(idx)?;
307        }
308
309        let (_, fs_device_tube) = Tube::pair()?;
310        let tube = Arc::new(Mutex::new(fs_device_tube));
311
312        let server = self.server.clone();
313
314        // Slot is always going to be 0 because we do not support DAX
315        let slot: u32 = 0;
316
317        let worker = WorkerThread::start(format!("v_fs:{}:{}", self.tag, idx), move |kill_evt| {
318            let mut worker = Worker::new(queue, server, tube, slot);
319            if let Err(e) = worker.run(kill_evt) {
320                error!("vhost-user-fs worker failed: {e:#}");
321            }
322            worker.queue
323        });
324        self.workers.insert(idx, worker);
325
326        Ok(())
327    }
328
329    fn stop_queue(&mut self, idx: usize) -> anyhow::Result<virtio::Queue> {
330        // TODO(b/440937769): Remove debug logs once the issue is resolved.
331        info!("Stopping vhost-user fs queue [{idx}]");
332        if let Some(worker) = self.workers.remove(&idx) {
333            let queue = worker.stop();
334            Ok(queue)
335        } else {
336            Err(anyhow::Error::new(DeviceError::WorkerNotFound))
337        }
338    }
339
340    fn unmap_guest_memory_on_fork(&self) -> bool {
341        self.unmap_guest_memory_on_fork
342    }
343
344    fn enter_suspended_state(&mut self) -> anyhow::Result<()> {
345        // No non-queue workers.
346        Ok(())
347    }
348
349    fn snapshot(&mut self) -> anyhow::Result<AnySnapshot> {
350        bail!("snapshot not implemented for vhost-user fs");
351    }
352
353    fn restore(&mut self, _data: AnySnapshot) -> anyhow::Result<()> {
354        bail!("snapshot not implemented for vhost-user fs");
355    }
356}
357
358#[derive(FromArgs)]
359#[argh(subcommand, name = "fs")]
360/// FS Device
361pub struct Options {
362    #[argh(option, arg_name = "PATH", hidden_help)]
363    /// deprecated - please use --socket-path instead
364    socket: Option<String>,
365    #[argh(option, arg_name = "PATH")]
366    /// path to the vhost-user socket to bind to.
367    /// If this flag is set, --fd cannot be specified.
368    socket_path: Option<String>,
369    #[argh(option, arg_name = "FD")]
370    /// file descriptor of a connected vhost-user socket.
371    /// If this flag is set, --socket-path cannot be specified.
372    fd: Option<RawDescriptor>,
373    #[argh(option, arg_name = "PATH")]
374    /// path to the Unix domain socket for dynamically controlling the path allowlist.
375    /// Communicates over SOCK_SEQPACKET using JSON. See crosvm book (devices/fs.html) for
376    /// protocol.
377    allowlist_socket_path: Option<PathBuf>,
378
379    #[argh(option, arg_name = "TAG")]
380    /// the virtio-fs tag
381    tag: String,
382    #[argh(option, arg_name = "DIR")]
383    /// path to a directory to share
384    shared_dir: PathBuf,
385    #[argh(option, arg_name = "UIDMAP")]
386    /// uid map to use
387    uid_map: Option<String>,
388    #[argh(option, arg_name = "GIDMAP")]
389    /// gid map to use
390    gid_map: Option<String>,
391    #[argh(option, arg_name = "CFG")]
392    /// colon-separated options for configuring a directory to be
393    /// shared with the VM through virtio-fs. The format is the same as
394    /// `crosvm run --shared-dir` flag except only the keys related to virtio-fs
395    /// are valid here.
396    cfg: Option<Config>,
397    #[argh(option, arg_name = "UID", default = "0")]
398    /// uid of the device process in the new user namespace created by minijail.
399    /// These two options (uid/gid) are useful when the crosvm process cannot
400    /// get CAP_SETGID/CAP_SETUID but an identity mapping of the current
401    /// user/group between the VM and the host is required.
402    /// Say the current user and the crosvm process has uid 5000, a user can use
403    /// "uid=5000" and "uidmap=5000 5000 1" such that files owned by user 5000
404    /// still appear to be owned by user 5000 in the VM. These 2 options are
405    /// useful only when there is 1 user in the VM accessing shared files.
406    /// If multiple users want to access the shared file, gid/uid options are
407    /// useless. It'd be better to create a new user namespace and give
408    /// CAP_SETUID/CAP_SETGID to the crosvm.
409    /// Default: 0.
410    uid: u32,
411    #[argh(option, arg_name = "GID", default = "0")]
412    /// gid of the device process in the new user namespace created by minijail.
413    /// Default: 0.
414    gid: u32,
415    #[argh(switch)]
416    /// disable-sandbox controls whether vhost-user-fs device uses minijail sandbox.
417    /// By default, it is false, the vhost-user-fs will enter new mnt/user/pid/net
418    /// namespace. If the this option is true, the vhost-user-fs device only create
419    /// a new mount namespace and run without seccomp filter.
420    /// Default: false.
421    disable_sandbox: bool,
422    #[argh(option, arg_name = "skip_pivot_root", default = "false")]
423    /// disable pivot_root when process is jailed.
424    ///
425    /// virtio-fs typically uses mount namespaces and pivot_root for file system isolation,
426    /// making the jailed process's root directory "/".
427    ///
428    /// Android's security model restricts crosvm's access to certain system capabilities,
429    /// specifically those related to managing mount namespaces and using pivot_root.
430    /// These capabilities are typically associated with the SYS_ADMIN capability.
431    /// To maintain a secure environment, Android relies on mechanisms like SELinux to
432    /// enforce isolation and control access to directories.
433    #[allow(dead_code)]
434    skip_pivot_root: bool,
435}
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440
441    #[test]
442    fn test_run_allowlist_listener() {
443        let temp_dir = tempfile::TempDir::new().unwrap();
444        let socket_path = temp_dir.path().join("test.sock");
445        let listener = UnixSeqpacketListener::bind(&socket_path).unwrap();
446        let allowlist = Arc::new(RwLock::new(PathAllowlist::default()));
447
448        use std::os::fd::OwnedFd;
449        let fd = SafeDescriptor::from(OwnedFd::from(listener));
450        let fd_clone = fd.try_clone().unwrap();
451
452        let allowlist_clone = allowlist.clone();
453        let handle = std::thread::spawn(move || {
454            run_allowlist_listener(fd, allowlist_clone);
455        });
456
457        use base::UnixSeqpacket;
458        let client_socket = UnixSeqpacket::connect(&socket_path).unwrap();
459        let client_tube = Tube::try_from(client_socket).unwrap();
460
461        // 1. Send a valid AddPaths command (multiple paths)
462        client_tube
463            .send(&FsAllowlistCommand::AddPaths {
464                paths: vec!["/allowed_path1".into(), "/allowed_path2".into()],
465            })
466            .unwrap();
467        let resp: FsAllowlistResponse = client_tube.recv().unwrap();
468        assert!(matches!(resp, FsAllowlistResponse::Ok));
469
470        // Verify paths are added
471        {
472            let al = allowlist.read().unwrap();
473            assert!(al.is_accessible("/allowed_path1"));
474            assert!(al.is_accessible("/allowed_path2"));
475        }
476
477        // 2. Send a valid RemovePaths command (multiple paths)
478        client_tube
479            .send(&FsAllowlistCommand::RemovePaths {
480                paths: vec!["/allowed_path1".into(), "/allowed_path2".into()],
481            })
482            .unwrap();
483        let resp: FsAllowlistResponse = client_tube.recv().unwrap();
484        assert!(matches!(resp, FsAllowlistResponse::Ok));
485
486        // Verify paths are removed
487        {
488            let al = allowlist.read().unwrap();
489            assert!(!al.is_accessible("/allowed_path1"));
490            assert!(!al.is_accessible("/allowed_path2"));
491        }
492
493        // 3. Send AddPaths with one invalid path (atomic rollback test)
494        client_tube
495            .send(&FsAllowlistCommand::AddPaths {
496                paths: vec!["/valid_but_rolled_back".into(), "/a/../../..".into()],
497            })
498            .unwrap();
499        let resp: FsAllowlistResponse = client_tube.recv().unwrap();
500        assert!(matches!(resp, FsAllowlistResponse::Err(_)));
501
502        // Verify that even the valid path was NOT added due to rollback
503        {
504            let al = allowlist.read().unwrap();
505            assert!(!al.is_accessible("/valid_but_rolled_back"));
506        }
507
508        // Close client tube to terminate listener
509        drop(client_tube);
510
511        // Force close the listener socket to break the accept loop
512        // SAFETY: safe because we own fd_clone and we are just shutting down the socket
513        unsafe {
514            libc::shutdown(fd_clone.as_raw_descriptor(), libc::SHUT_RDWR);
515        }
516
517        let join_res = handle.join();
518        assert!(join_res.is_ok(), "Listener thread panicked!");
519    }
520}