summary refs log tree commit diff
diff options
context:
space:
mode:
authorChirantan Ekbote <chirantan@chromium.org>2018-04-16 19:32:04 -0700
committerchrome-bot <chrome-bot@chromium.org>2018-08-08 13:45:44 -0700
commitebd56813e189de4a2d2bad2699bc07b56e580035 (patch)
treea890b6c34c5c3cc36864560cc22f0ab84c3f5ecb
parenta79073ad7d244b45bca5ba0c5ddf92b04827fc24 (diff)
downloadcrosvm-ebd56813e189de4a2d2bad2699bc07b56e580035.tar
crosvm-ebd56813e189de4a2d2bad2699bc07b56e580035.tar.gz
crosvm-ebd56813e189de4a2d2bad2699bc07b56e580035.tar.bz2
crosvm-ebd56813e189de4a2d2bad2699bc07b56e580035.tar.lz
crosvm-ebd56813e189de4a2d2bad2699bc07b56e580035.tar.xz
crosvm-ebd56813e189de4a2d2bad2699bc07b56e580035.tar.zst
crosvm-ebd56813e189de4a2d2bad2699bc07b56e580035.zip
virtio: Implement the 9P device
Implement a new virtio_9p device to be used for sharing directories with
the VM.

BUG=chromium:703939
TEST=mount inside a VM and run `bonnie++ -r 256`
Append the shared directory to the crosvm command line:
--shared-dir /path/to/dir:test_9p
Then mount in the guest:
mkdir /tmp/9p
mount -t 9p -o trans=virtio test_9p /tmp/9p -oversion=9p2000.L
Or for a 9p root:
run --shared-dir /mnt/vm_root:/dev/root -p 'root=/dev/root ro rootflags=ro,trans=virtio,version=9p2000.L,cache=loose rootfstype=9p' vmlinux.bin

CQ-DEPEND=CL:1065170

Change-Id: I41fc21306ab5fa318a271f172d7057b767b29f31
Signed-off-by: Chirantan Ekbote <chirantan@chromium.org>
Reviewed-on: https://chromium-review.googlesource.com/1065173
Commit-Ready: ChromeOS CL Exonerator Bot <chromiumos-cl-exonerator@appspot.gserviceaccount.com>
Reviewed-by: Dylan Reid <dgreid@chromium.org>
-rw-r--r--Cargo.lock1
-rw-r--r--devices/Cargo.toml1
-rw-r--r--devices/src/lib.rs1
-rw-r--r--devices/src/virtio/mod.rs3
-rw-r--r--devices/src/virtio/p9.rs468
-rw-r--r--seccomp/aarch64/9p_device.policy54
-rw-r--r--seccomp/x86_64/9p_device.policy54
-rw-r--r--src/linux.rs50
-rw-r--r--src/main.rs32
9 files changed, 663 insertions, 1 deletions
diff --git a/Cargo.lock b/Cargo.lock
index b8d822b..4eb57b9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -133,6 +133,7 @@ dependencies = [
  "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
  "net_sys 0.1.0",
  "net_util 0.1.0",
+ "p9 0.1.0",
  "resources 0.1.0",
  "sys_util 0.1.0",
  "vhost 0.1.0",
diff --git a/devices/Cargo.toml b/devices/Cargo.toml
index 03f1a8a..404efb0 100644
--- a/devices/Cargo.toml
+++ b/devices/Cargo.toml
@@ -17,6 +17,7 @@ libc = "*"
 io_jail = { path = "../io_jail" }
 net_sys = { path = "../net_sys" }
 net_util = { path = "../net_util" }
+p9 = { path = "../p9" }
 resources = { path = "../resources" }
 sys_util = { path = "../sys_util" }
 vhost = { path = "../vhost" }
diff --git a/devices/src/lib.rs b/devices/src/lib.rs
index eaf6b4e..1478849 100644
--- a/devices/src/lib.rs
+++ b/devices/src/lib.rs
@@ -10,6 +10,7 @@ extern crate io_jail;
 extern crate libc;
 extern crate net_sys;
 extern crate net_util;
+extern crate p9;
 extern crate resources;
 #[macro_use]
 extern crate sys_util;
diff --git a/devices/src/virtio/mod.rs b/devices/src/virtio/mod.rs
index 8e8e4b6..91a2c23 100644
--- a/devices/src/virtio/mod.rs
+++ b/devices/src/virtio/mod.rs
@@ -12,6 +12,7 @@ mod rng;
 mod net;
 #[cfg(feature = "gpu")]
 mod gpu;
+mod p9;
 mod wl;
 
 pub mod vhost;
@@ -24,6 +25,7 @@ pub use self::rng::*;
 pub use self::net::*;
 #[cfg(feature = "gpu")]
 pub use self::gpu::*;
+pub use self::p9::*;
 pub use self::wl::*;
 
 const DEVICE_ACKNOWLEDGE: u32 = 0x01;
@@ -39,6 +41,7 @@ const TYPE_RNG: u32 = 4;
 const TYPE_BALLOON: u32 = 5;
 #[allow(dead_code)]
 const TYPE_GPU: u32 = 16;
+const TYPE_9P: u32 = 9;
 const TYPE_VSOCK: u32 = 19;
 const TYPE_WL: u32 = 30;
 
diff --git a/devices/src/virtio/p9.rs b/devices/src/virtio/p9.rs
new file mode 100644
index 0000000..0ff4de1
--- /dev/null
+++ b/devices/src/virtio/p9.rs
@@ -0,0 +1,468 @@
+// Copyright 2018 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use std::cmp::min;
+use std::error;
+use std::fmt;
+use std::io::{self, Read, Write};
+use std::iter::Peekable;
+use std::mem;
+use std::os::unix::io::RawFd;
+use std::path::{Path, PathBuf};
+use std::result;
+use std::sync::Arc;
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::thread;
+
+use p9;
+use sys_util::{Error as SysError, EventFd, GuestAddress, GuestMemory, PollContext, PollToken};
+use virtio_sys::vhost::VIRTIO_F_VERSION_1;
+
+use super::{DescriptorChain, Queue, TYPE_9P, VirtioDevice, INTERRUPT_STATUS_USED_RING};
+
+const QUEUE_SIZE: u16 = 128;
+const QUEUE_SIZES: &'static [u16] = &[QUEUE_SIZE];
+
+// The only virtio_9p feature.
+const VIRTIO_9P_MOUNT_TAG: u8 = 0;
+
+/// Errors that occur during operation of a virtio 9P device.
+#[derive(Debug)]
+pub enum P9Error {
+    /// The tag for the 9P device was too large to fit in the config space.
+    TagTooLong(usize),
+    /// The root directory for the 9P server is not absolute.
+    RootNotAbsolute(PathBuf),
+    /// Creating PollContext failed.
+    CreatePollContext(SysError),
+    /// Error while polling for events.
+    PollError(SysError),
+    /// Error while reading from the virtio queue's EventFd.
+    ReadQueueEventFd(SysError),
+    /// A request is missing readable descriptors.
+    NoReadableDescriptors,
+    /// A request is missing writable descriptors.
+    NoWritableDescriptors,
+    /// A descriptor contained an invalid guest address range.
+    InvalidGuestAddress(GuestAddress, u32),
+    /// Failed to signal the virio used queue.
+    SignalUsedQueue(SysError),
+    /// An internal I/O error occurred.
+    Internal(io::Error),
+}
+
+impl error::Error for P9Error {
+    fn description(&self) -> &str {
+        "An error occurred in the virtio 9P device"
+    }
+}
+
+impl fmt::Display for P9Error {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            &P9Error::TagTooLong(len) => write!(
+                f,
+                "P9 device tag is too long: len = {}, max = {}",
+                len,
+                ::std::u16::MAX
+            ),
+            &P9Error::RootNotAbsolute(ref buf) => write!(
+                f,
+                "P9 root directory is not absolute: root = {}",
+                buf.display()
+            ),
+            &P9Error::CreatePollContext(ref err) => {
+                write!(f, "failed to create PollContext: {:?}", err)
+            }
+            &P9Error::PollError(ref err) => write!(f, "failed to poll events: {:?}", err),
+            &P9Error::ReadQueueEventFd(ref err) => {
+                write!(f, "failed to read from virtio queue EventFd: {:?}", err)
+            }
+            &P9Error::NoReadableDescriptors => {
+                write!(f, "request does not have any readable descriptors")
+            }
+            &P9Error::NoWritableDescriptors => {
+                write!(f, "request does not have any writable descriptors")
+            }
+            &P9Error::InvalidGuestAddress(addr, len) => write!(
+                f,
+                "descriptor contained invalid guest address range: address = {:?}, len = {}",
+                addr, len
+            ),
+            &P9Error::SignalUsedQueue(ref err) => {
+                write!(f, "failed to signal used queue: {:?}", err)
+            }
+            &P9Error::Internal(ref err) => write!(f, "P9 internal server error: {}", err),
+        }
+    }
+}
+
+pub type P9Result<T> = result::Result<T, P9Error>;
+
+struct Reader<'a, I>
+where
+    I: Iterator<Item = DescriptorChain<'a>>,
+{
+    mem: &'a GuestMemory,
+    offset: u32,
+    iter: Peekable<I>,
+}
+
+impl<'a, I> Read for Reader<'a, I>
+where
+    I: Iterator<Item = DescriptorChain<'a>>,
+{
+    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+        let needs_advance = if let Some(current) = self.iter.peek() {
+            self.offset >= current.len
+        } else {
+            false
+        };
+
+        if needs_advance {
+            self.offset = 0;
+            self.iter.next();
+        }
+
+        if let Some(current) = self.iter.peek() {
+            debug_assert!(current.is_read_only());
+            let addr = current
+                .addr
+                .checked_add(self.offset as u64)
+                .ok_or_else(|| {
+                    io::Error::new(
+                        io::ErrorKind::InvalidData,
+                        P9Error::InvalidGuestAddress(current.addr, current.len),
+                    )
+                })?;
+            let len = min(buf.len(), (current.len - self.offset) as usize);
+            let count = self.mem
+                .read_slice_at_addr(&mut buf[..len], addr)
+                .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
+
+            // |count| has to fit into a u32 because it must be less than or equal to
+            // |current.len|, which does fit into a u32.
+            self.offset += count as u32;
+
+            Ok(count)
+        } else {
+            // Nothing left to read.
+            Ok(0)
+        }
+    }
+}
+
+struct Writer<'a, I>
+where
+    I: Iterator<Item = DescriptorChain<'a>>,
+{
+    mem: &'a GuestMemory,
+    bytes_written: u32,
+    offset: u32,
+    iter: Peekable<I>,
+}
+
+impl<'a, I> Write for Writer<'a, I>
+where
+    I: Iterator<Item = DescriptorChain<'a>>,
+{
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        let needs_advance = if let Some(current) = self.iter.peek() {
+            self.offset >= current.len
+        } else {
+            false
+        };
+
+        if needs_advance {
+            self.offset = 0;
+            self.iter.next();
+        }
+
+        if let Some(current) = self.iter.peek() {
+            debug_assert!(current.is_write_only());
+            let addr = current
+                .addr
+                .checked_add(self.offset as u64)
+                .ok_or_else(|| {
+                    io::Error::new(
+                        io::ErrorKind::InvalidData,
+                        P9Error::InvalidGuestAddress(current.addr, current.len),
+                    )
+                })?;
+
+            let len = min(buf.len(), (current.len - self.offset) as usize);
+            let count = self.mem
+                .write_slice_at_addr(&buf[..len], addr)
+                .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
+
+            // |count| has to fit into a u32 because it must be less than or equal to
+            // |current.len|, which does fit into a u32.
+            self.offset += count as u32;
+            self.bytes_written += count as u32;
+
+            Ok(count)
+        } else {
+            // No more room in the descriptor chain.
+            Ok(0)
+        }
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        // Nothing to flush since the writes go straight into the buffer.
+        Ok(())
+    }
+}
+
+struct Worker {
+    mem: GuestMemory,
+    queue: Queue,
+    server: p9::Server,
+    irq_status: Arc<AtomicUsize>,
+    irq_evt: EventFd,
+}
+
+impl Worker {
+    fn signal_used_queue(&self) -> P9Result<()> {
+        self.irq_status
+            .fetch_or(INTERRUPT_STATUS_USED_RING as usize, Ordering::SeqCst);
+        self.irq_evt.write(1).map_err(P9Error::SignalUsedQueue)
+    }
+
+    fn process_queue(&mut self) -> P9Result<()> {
+        let mut used_desc_heads = [(0, 0); QUEUE_SIZE as usize];
+        let mut used_count = 0;
+
+        for avail_desc in self.queue.iter(&self.mem) {
+            let mut reader = Reader {
+                mem: &self.mem,
+                offset: 0,
+                iter: avail_desc
+                    .clone()
+                    .into_iter()
+                    .readable()
+                    .peekable(),
+            };
+            let mut writer = Writer {
+                mem: &self.mem,
+                bytes_written: 0,
+                offset: 0,
+                iter: avail_desc
+                    .clone()
+                    .into_iter()
+                    .writable()
+                    .peekable(),
+            };
+
+            self.server
+                .handle_message(&mut reader, &mut writer)
+                .map_err(P9Error::Internal)?;
+
+            used_desc_heads[used_count] = (avail_desc.index, writer.bytes_written);
+            used_count += 1;
+        }
+
+        for &(idx, count) in &used_desc_heads[..used_count] {
+            self.queue.add_used(&self.mem, idx, count);
+        }
+
+        self.signal_used_queue()?;
+
+        Ok(())
+    }
+
+    fn run(&mut self, queue_evt: EventFd, kill_evt: EventFd) -> P9Result<()> {
+        #[derive(PollToken)]
+        enum Token {
+            // A request is ready on the queue.
+            QueueReady,
+            // The parent thread requested an exit.
+            Kill,
+        }
+
+        let poll_ctx: PollContext<Token> = PollContext::new()
+            .and_then(|pc| pc.add(&queue_evt, Token::QueueReady).and(Ok(pc)))
+            .and_then(|pc| pc.add(&kill_evt, Token::Kill).and(Ok(pc)))
+            .map_err(P9Error::CreatePollContext)?;
+
+        loop {
+            let events = poll_ctx.wait().map_err(P9Error::PollError)?;
+            for event in events.iter_readable() {
+                match event.token() {
+                    Token::QueueReady => {
+                        queue_evt.read().map_err(P9Error::ReadQueueEventFd)?;
+                        self.process_queue()?;
+                    }
+                    Token::Kill => return Ok(()),
+                }
+            }
+        }
+    }
+}
+
+/// Virtio device for sharing specific directories on the host system with the guest VM.
+pub struct P9 {
+    config: Vec<u8>,
+    server: Option<p9::Server>,
+    kill_evt: Option<EventFd>,
+    avail_features: u64,
+    acked_features: u64,
+    worker: Option<thread::JoinHandle<P9Result<()>>>,
+}
+
+impl P9 {
+    pub fn new<P: AsRef<Path>>(root: P, tag: &str) -> P9Result<P9> {
+        if tag.len() > ::std::u16::MAX as usize {
+            return Err(P9Error::TagTooLong(tag.len()));
+        }
+
+        if !root.as_ref().is_absolute() {
+            return Err(P9Error::RootNotAbsolute(root.as_ref().into()));
+        }
+
+        let len = tag.len() as u16;
+        let mut cfg = Vec::with_capacity(tag.len() + mem::size_of::<u16>());
+        cfg.push(len as u8);
+        cfg.push((len >> 8) as u8);
+
+        cfg.write_all(tag.as_bytes()).map_err(P9Error::Internal)?;
+
+        Ok(P9 {
+            config: cfg,
+            server: Some(p9::Server::new(root)),
+            kill_evt: None,
+            avail_features: 1 << VIRTIO_9P_MOUNT_TAG | 1 << VIRTIO_F_VERSION_1,
+            acked_features: 0,
+            worker: None,
+        })
+    }
+}
+
+impl VirtioDevice for P9 {
+    fn keep_fds(&self) -> Vec<RawFd> {
+        Vec::new()
+    }
+
+    fn device_type(&self) -> u32 {
+        TYPE_9P
+    }
+
+    fn queue_max_sizes(&self) -> &[u16] {
+        QUEUE_SIZES
+    }
+
+    fn features(&self, page: u32) -> u32 {
+        match page {
+            0 => self.avail_features as u32,
+            1 => (self.avail_features >> 32) as u32,
+            _ => {
+                warn!("virtio_9p got request for features page: {}", page);
+                0u32
+            }
+        }
+    }
+
+    fn ack_features(&mut self, page: u32, value: u32) {
+        let mut v = match page {
+            0 => value as u64,
+            1 => (value as u64) << 32,
+            _ => {
+                warn!("virtio_9p device cannot ack unknown feature page: {}", page);
+                0u64
+            }
+        };
+
+        // Check if the guest is ACK'ing a feature that we didn't claim to have.
+        let unrequested_features = v & !self.avail_features;
+        if unrequested_features != 0 {
+            warn!(
+                "virtio_9p got unknown feature ack: {:x}, page = {}, value = {:x}",
+                v, page, value
+            );
+
+            // Don't count these features as acked.
+            v &= !unrequested_features;
+        }
+        self.acked_features |= v;
+    }
+
+    fn read_config(&self, offset: u64, data: &mut [u8]) {
+        if offset >= self.config.len() as u64 {
+            // Nothing to see here.
+            return;
+        }
+
+        // The config length cannot be more than ::std::u16::MAX + mem::size_of::<u16>(), which
+        // is significantly smaller than ::std::usize::MAX on the architectures we care about so
+        // if we reach this point then we know that `offset` will fit into a usize.
+        let offset = offset as usize;
+        let len = min(data.len(), self.config.len() - offset);
+        data[..len].copy_from_slice(&self.config[offset..offset + len])
+    }
+
+    fn activate(
+        &mut self,
+        guest_mem: GuestMemory,
+        interrupt_evt: EventFd,
+        status: Arc<AtomicUsize>,
+        mut queues: Vec<Queue>,
+        mut queue_evts: Vec<EventFd>,
+    ) {
+        if queues.len() != 1 || queue_evts.len() != 1 {
+            return;
+        }
+
+        let (self_kill_evt, kill_evt) = match EventFd::new().and_then(|e| Ok((e.try_clone()?, e))) {
+            Ok(v) => v,
+            Err(e) => {
+                error!("failed creating kill EventFd pair: {:?}", e);
+                return;
+            }
+        };
+        self.kill_evt = Some(self_kill_evt);
+
+        if let Some(server) = self.server.take() {
+            let worker_result = thread::Builder::new().name("virtio_9p".to_string()).spawn(
+                move || {
+                    let mut worker = Worker {
+                        mem: guest_mem,
+                        queue: queues.remove(0),
+                        server: server,
+                        irq_status: status,
+                        irq_evt: interrupt_evt,
+                    };
+
+                    worker.run(queue_evts.remove(0), kill_evt)
+                },
+            );
+
+            match worker_result {
+                Ok(worker) => self.worker = Some(worker),
+                Err(e) => error!("failed to spawn virtio_9p worker: {:?}", e),
+            }
+        }
+    }
+}
+
+impl Drop for P9 {
+    fn drop(&mut self) {
+        if let Some(kill_evt) = self.kill_evt.take() {
+            if let Err(e) = kill_evt.write(1) {
+                error!("failed to kill virtio_9p worker thread: {:?}", e);
+                return;
+            }
+
+            // Only wait on the child thread if we were able to send it a kill event.
+            if let Some(worker) = self.worker.take() {
+                match worker.join() {
+                    Ok(r) => {
+                        if let Err(e) = r {
+                            error!("virtio_9p worker thread exited with error: {}", e)
+                        }
+                    }
+                    Err(e) => error!("virtio_9p worker thread panicked: {:?}", e),
+                }
+            }
+        }
+    }
+}
diff --git a/seccomp/aarch64/9p_device.policy b/seccomp/aarch64/9p_device.policy
new file mode 100644
index 0000000..238fb87
--- /dev/null
+++ b/seccomp/aarch64/9p_device.policy
@@ -0,0 +1,54 @@
+# Copyright 2018 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+write: 1
+recv: 1
+read: 1
+epoll_wait: 1
+pread64: 1
+pwrite64: 1
+lstat64: 1
+stat64: 1
+close: 1
+prctl: arg0 == PR_SET_NAME
+open: 1
+fstat64: 1
+# ioctl(fd, FIOCLEX, 0) is equivalent to fcntl(fd, F_SETFD, FD_CLOEXEC).
+ioctl: arg1 == FIOCLEX
+getdents64: 1
+fdatasync: 1
+fsync: 1
+# Disallow mmap with PROT_EXEC set.  The syntax here doesn't allow bit
+# negation, thus the manually negated mask constant.
+mmap2: arg2 in 0xfffffffb
+mprotect: arg2 in 0xfffffffb
+sigaltstack: 1
+munmap: 1
+mkdir: 1
+rmdir: 1
+epoll_ctl: 1
+rename: 1
+writev: 1
+link: 1
+unlink: 1
+restart_syscall: 1
+exit: 1
+rt_sigreturn: 1
+epoll_create1: 1
+sched_getaffinity: 1
+dup: 1
+# Disallow clone's other than new threads.
+clone: arg0 & 0x00010000
+set_robust_list: 1
+exit_group: 1
+socket: arg0 == AF_UNIX
+futex: 1
+eventfd2: 1
+mremap: 1
+# Allow MADV_DONTDUMP and MADV_DONTNEED only.
+madvise: arg2 == 0x00000010 || arg2 == 0x00000004
+utimensat: 1
+ftruncate64: 1
+fchown: arg1 == 0xffffffff && arg2 == 0xffffffff
+statfs64: 1
diff --git a/seccomp/x86_64/9p_device.policy b/seccomp/x86_64/9p_device.policy
new file mode 100644
index 0000000..d161b5c
--- /dev/null
+++ b/seccomp/x86_64/9p_device.policy
@@ -0,0 +1,54 @@
+# Copyright 2018 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+write: 1
+writev: 1
+recvfrom: 1
+epoll_wait: 1
+read: 1
+pwrite64: 1
+stat: 1
+lstat: 1
+close: 1
+open: 1
+fstat: 1
+# ioctl(fd, FIOCLEX, 0) is equivalent to fcntl(fd, F_SETFD, FD_CLOEXEC).
+ioctl: arg1 == FIOCLEX
+link: 1
+unlink: 1
+rename: 1
+pread64: 1
+getdents: 1
+# Disallow mmap with PROT_EXEC set.  The syntax here doesn't allow bit
+# negation, thus the manually negated mask constant.
+mmap: arg2 in 0xfffffffb
+mprotect: arg2 in 0xfffffffb
+munmap: 1
+mkdir: 1
+sigaltstack: 1
+epoll_ctl: 1
+mremap: 1
+rmdir: 1
+fsync: 1
+fdatasync: 1
+restart_syscall: 1
+exit: 1
+rt_sigreturn: 1
+epoll_create1: 1
+prctl: arg0 == PR_SET_NAME
+eventfd2: 1
+sched_getaffinity: 1
+dup: 1
+getpid: 1
+# Disallow clone's other than new threads.
+clone: arg0 & 0x00010000
+set_robust_list: 1
+exit_group: 1
+# Allow MADV_DONTDUMP and MADV_DONTNEED only.
+madvise: arg2 == 0x00000010 || arg2 == 0x00000004
+futex: 1
+utimensat: 1
+ftruncate: 1
+fchown: arg1 == 0xffffffff && arg2 == 0xffffffff
+statfs: 1
diff --git a/src/linux.rs b/src/linux.rs
index 191de34..98d0c5e 100644
--- a/src/linux.rs
+++ b/src/linux.rs
@@ -74,6 +74,7 @@ pub enum Error {
     NetDeviceNew(devices::virtio::NetError),
     NoVarEmpty,
     OpenKernel(PathBuf, io::Error),
+    P9DeviceNew(devices::virtio::P9Error),
     PollContextAdd(sys_util::Error),
     PollContextDelete(sys_util::Error),
     QcowDeviceCreate(qcow::Error),
@@ -83,6 +84,7 @@ pub enum Error {
     RegisterBlock(MmioRegisterError),
     RegisterGpu(MmioRegisterError),
     RegisterNet(MmioRegisterError),
+    RegisterP9(MmioRegisterError),
     RegisterRng(MmioRegisterError),
     RegisterSignalHandler(sys_util::Error),
     RegisterVsock(MmioRegisterError),
@@ -139,6 +141,7 @@ impl fmt::Display for Error {
             &Error::OpenKernel(ref p, ref e) => {
                 write!(f, "failed to open kernel image {:?}: {}", p, e)
             }
+            &Error::P9DeviceNew(ref e) => write!(f, "failed to create 9p device: {}", e),
             &Error::PollContextAdd(ref e) => write!(f, "failed to add fd to poll context: {:?}", e),
             &Error::PollContextDelete(ref e) => {
                 write!(f, "failed to remove fd from poll context: {:?}", e)
@@ -158,6 +161,7 @@ impl fmt::Display for Error {
             &Error::RegisterBlock(ref e) => write!(f, "error registering block device: {:?}", e),
             &Error::RegisterGpu(ref e) => write!(f, "error registering gpu device: {:?}", e),
             &Error::RegisterNet(ref e) => write!(f, "error registering net device: {:?}", e),
+            &Error::RegisterP9(ref e) => write!(f, "error registering 9p device: {:?}", e),
             &Error::RegisterRng(ref e) => write!(f, "error registering rng device: {:?}", e),
             &Error::RegisterSignalHandler(ref e) => {
                 write!(f, "error registering signal handler: {:?}", e)
@@ -602,6 +606,52 @@ fn setup_mmio_bus(cfg: &Config,
         }
     }
 
+    let chronos_user_group = CStr::from_bytes_with_nul(b"chronos\0").unwrap();
+    let chronos_uid = match get_user_id(&chronos_user_group) {
+        Ok(u) => u,
+        Err(e) => {
+            warn!("falling back to current user id for 9p: {:?}", e);
+            geteuid()
+        }
+    };
+    let chronos_gid = match get_group_id(&chronos_user_group) {
+        Ok(u) => u,
+        Err(e) => {
+            warn!("falling back to current group id for 9p: {:?}", e);
+            getegid()
+        }
+    };
+
+    for &(ref src, ref tag) in &cfg.shared_dirs {
+        let (jail, root) = if cfg.multiprocess {
+            let policy_path: PathBuf = cfg.seccomp_policy_dir.join("9p_device.policy");
+            let mut jail = create_base_minijail(empty_root_path, &policy_path)?;
+
+            //  The shared directory becomes the root of the device's file system.
+            let root = Path::new("/");
+            jail.mount_bind(&src, root, true).unwrap();
+
+            // Set the uid/gid for the jailed process, and give a basic id map. This
+            // is required for the above bind mount to work.
+            jail.change_uid(chronos_uid);
+            jail.change_gid(chronos_gid);
+            jail.uidmap(&format!("{0} {0} 1", chronos_uid))
+                .map_err(Error::SettingUidMap)?;
+            jail.gidmap(&format!("{0} {0} 1", chronos_gid))
+                .map_err(Error::SettingGidMap)?;
+
+            (Some(jail), root)
+        } else {
+            // There's no bind mount so we tell the server to treat the source directory as the
+            // root.  The double deref here converts |src| from a &PathBuf into a &Path.
+            (None, &**src)
+        };
+
+        let p9_box = Box::new(devices::virtio::P9::new(root, tag).map_err(Error::P9DeviceNew)?);
+
+        register_mmio(&mut bus, vm, p9_box, jail, resources, cmdline).map_err(Error::RegisterP9)?;
+    }
+
     Ok(bus)
 }
 
diff --git a/src/main.rs b/src/main.rs
index c85d720..d1de672 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -80,6 +80,7 @@ pub struct Config {
     wayland_socket_path: Option<PathBuf>,
     wayland_dmabuf: bool,
     socket_path: Option<PathBuf>,
+    shared_dirs: Vec<(PathBuf, String)>,
     multiprocess: bool,
     seccomp_policy_dir: PathBuf,
     cid: Option<u64>,
@@ -105,6 +106,7 @@ impl Default for Config {
             wayland_dmabuf: false,
             socket_path: None,
             multiprocess: !cfg!(feature = "default-no-sandbox"),
+            shared_dirs: Vec::new(),
             seccomp_policy_dir: PathBuf::from(SECCOMP_POLICY_DIR),
             cid: None,
             gpu: false,
@@ -318,6 +320,32 @@ fn set_argument(cfg: &mut Config, name: &str, value: Option<&str>) -> argument::
                 }
             })?);
         }
+        "shared-dir" => {
+            // Formatted as <src:tag>.
+            let param = value.unwrap();
+            let mut components = param.splitn(2, ':');
+            let src = PathBuf::from(components.next().ok_or_else(|| {
+                argument::Error::InvalidValue {
+                    value: param.to_owned(),
+                    expected: "missing source path for `shared-dir`",
+                }
+            })?);
+            let tag = components.next().ok_or_else(|| {
+                argument::Error::InvalidValue {
+                    value: param.to_owned(),
+                    expected: "missing tag for `shared-dir`",
+                }
+            })?.to_owned();
+
+            if !src.is_dir() {
+                return Err(argument::Error::InvalidValue {
+                    value: param.to_owned(),
+                    expected: "source path for `shared-dir` must be a directory",
+                });
+            }
+
+            cfg.shared_dirs.push((src, tag));
+        }
         "seccomp-policy-dir" => {
             // `value` is Some because we are in this match so it's safe to unwrap.
             cfg.seccomp_policy_dir = PathBuf::from(value.unwrap());
@@ -401,7 +429,9 @@ fn run_vm(args: std::env::Args) -> std::result::Result<(), ()> {
                                 "Path to put the control socket. If PATH is a directory, a name will be generated."),
           Argument::short_flag('u', "multiprocess", "Run each device in a child process(default)."),
           Argument::flag("disable-sandbox", "Run all devices in one, non-sandboxed process."),
-          Argument::value("cid", "CID", "Context ID for virtual sockets"),
+          Argument::value("cid", "CID", "Context ID for virtual sockets."),
+          Argument::value("shared-dir", "PATH:TAG",
+                          "Directory to be shared with a VM as a source:tag pair. Can be given more than once."),
           Argument::value("seccomp-policy-dir", "PATH", "Path to seccomp .policy files."),
           #[cfg(feature = "plugin")]
           Argument::value("plugin", "PATH", "Absolute path to plugin process to run under crosvm."),