From 359e7de9a19bf82ffd54cc383c47ad5ef5e936f1 Mon Sep 17 00:00:00 2001 From: "Jorge E. Moreira" Date: Wed, 2 Dec 2020 18:25:53 -0800 Subject: [PATCH] Add the VioS audio backend It only supports playback streams, with capture streams to be added in a different change. BUG=b/171602855 Change-Id: Id9a5a560506f8fd026ef3ed83f8d14b29389e329 Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/crosvm/+/2574813 Tested-by: Jorge Moreira Broche Commit-Queue: Jorge Moreira Broche Auto-Submit: Jorge Moreira Broche Reviewed-by: Dylan Reid Reviewed-by: Chih-Yang Hsia Reviewed-by: Zach Reizner --- Cargo.lock | 3 +- devices/Cargo.toml | 2 + devices/src/pci/ac97.rs | 29 +- devices/src/pci/pci_device.rs | 6 + devices/src/virtio/snd/constants.rs | 5 + devices/src/virtio/snd/layout.rs | 6 +- devices/src/virtio/snd/mod.rs | 2 + devices/src/virtio/snd/vios_backend/mod.rs | 11 + .../virtio/snd/vios_backend/shm_streams.rs | 259 +++++++++ .../src/virtio/snd/vios_backend/shm_vios.rs | 548 ++++++++++++++++++ seccomp/aarch64/vios_audio_device.policy | 13 + seccomp/arm/vios_audio_device.policy | 14 + seccomp/x86_64/vios_audio_device.policy | 14 + src/argument.rs | 2 +- src/main.rs | 46 +- 15 files changed, 950 insertions(+), 10 deletions(-) create mode 100644 devices/src/virtio/snd/vios_backend/mod.rs create mode 100644 devices/src/virtio/snd/vios_backend/shm_streams.rs create mode 100644 devices/src/virtio/snd/vios_backend/shm_vios.rs create mode 100644 seccomp/aarch64/vios_audio_device.policy create mode 100644 seccomp/arm/vios_audio_device.policy create mode 100644 seccomp/x86_64/vios_audio_device.policy diff --git a/Cargo.lock b/Cargo.lock index 7ec0f66a3c..b0be54f713 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -275,8 +275,10 @@ dependencies = [ "resources", "rutabaga_gfx", "sync", + "sys_util", "syscall_defines", "tempfile", + "thiserror", "tpm2", "usb_util", "vfio_sys", @@ -566,7 +568,6 @@ dependencies = [ "libc", "log", "protobuf", - "thiserror", ] [[package]] diff --git a/devices/Cargo.toml b/devices/Cargo.toml index 8c35682f4d..bd38b124cc 100644 --- a/devices/Cargo.toml +++ b/devices/Cargo.toml @@ -45,8 +45,10 @@ rand_ish = { path = "../rand_ish" } remain = "*" resources = { path = "../resources" } sync = { path = "../sync" } +sys_util = { path = "../sys_util" } base = { path = "../base" } syscall_defines = { path = "../syscall_defines" } +thiserror = "1.0.20" tpm2 = { path = "../tpm2", optional = true } usb_util = { path = "../usb_util" } vfio_sys = { path = "../vfio_sys" } diff --git a/devices/src/pci/ac97.rs b/devices/src/pci/ac97.rs index 8aa43acc29..a3762bb63a 100644 --- a/devices/src/pci/ac97.rs +++ b/devices/src/pci/ac97.rs @@ -5,6 +5,7 @@ use std::default::Default; use std::error; use std::fmt::{self, Display}; +use std::path::PathBuf; use std::str::FromStr; use audio_streams::shm_streams::{NullShmStreamSource, ShmStreamSource}; @@ -13,6 +14,10 @@ use libcras::{CrasClient, CrasClientType, CrasSocketType}; use resources::{Alloc, MmioType, SystemAllocator}; use vm_memory::GuestMemory; +#[cfg(target_os = "linux")] +use crate::virtio::snd::vios_backend::VioSShmStreamSource; +#[cfg(not(target_os = "linux"))] +use crate::virtio::snd::vios_backend::Error as VioSError; use crate::pci::ac97_bus_master::Ac97BusMaster; use crate::pci::ac97_mixer::Ac97Mixer; use crate::pci::ac97_regs::*; @@ -34,6 +39,7 @@ const PCI_DEVICE_ID_INTEL_82801AA_5: u16 = 0x2415; pub enum Ac97Backend { NULL, CRAS, + VIOS, } impl Default for Ac97Backend { @@ -46,6 +52,7 @@ impl Default for Ac97Backend { #[derive(Debug)] pub enum Ac97Error { InvalidBackend, + MissingServerPath, } impl error::Error for Ac97Error {} @@ -53,7 +60,8 @@ impl error::Error for Ac97Error {} impl Display for Ac97Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Ac97Error::InvalidBackend => write!(f, "Must be cras or null"), + Ac97Error::InvalidBackend => write!(f, "Must be cras, vios or null"), + Ac97Error::MissingServerPath => write!(f, "server must be provided for vios backend"), } } } @@ -63,6 +71,7 @@ impl FromStr for Ac97Backend { fn from_str(s: &str) -> std::result::Result { match s { "cras" => Ok(Ac97Backend::CRAS), + "vios" => Ok(Ac97Backend::VIOS), "null" => Ok(Ac97Backend::NULL), _ => Err(Ac97Error::InvalidBackend), } @@ -74,6 +83,7 @@ impl FromStr for Ac97Backend { pub struct Ac97Parameters { pub backend: Ac97Backend, pub capture: bool, + pub vios_server_path: Option, } pub struct Ac97Dev { @@ -129,6 +139,7 @@ impl Ac97Dev { ); Self::create_null_audio_device(mem) }), + Ac97Backend::VIOS => Self::create_vios_audio_device(mem, param), Ac97Backend::NULL => Self::create_null_audio_device(mem), } } @@ -137,6 +148,7 @@ impl Ac97Dev { pub fn minijail_policy(&self) -> &'static str { match self.backend { Ac97Backend::CRAS => "cras_audio_device", + Ac97Backend::VIOS => "vios_audio_device", Ac97Backend::NULL => "null_audio_device", } } @@ -155,6 +167,21 @@ impl Ac97Dev { Ok(cras_audio) } + fn create_vios_audio_device(mem: GuestMemory, param: Ac97Parameters) -> Result { + #[cfg(target_os = "linux")] + { + let server = Box::new( + // The presence of vios_server_path is checked during argument parsing + VioSShmStreamSource::new(param.vios_server_path.expect("Missing server path")) + .map_err(|e| pci_device::Error::CreateViosClientFailed(e))?, + ); + let vios_audio = Self::new(mem, Ac97Backend::VIOS, server); + return Ok(vios_audio); + } + #[cfg(not(target_os = "linux"))] + Err(pci_device::Error::CreateViosClientFailed(VioSError::PlatformNotSupported)) + } + fn create_null_audio_device(mem: GuestMemory) -> Result { let server = Box::new(NullShmStreamSource::new()); let null_audio = Self::new(mem, Ac97Backend::NULL, server); diff --git a/devices/src/pci/pci_device.rs b/devices/src/pci/pci_device.rs index be336e7eca..151c051153 100644 --- a/devices/src/pci/pci_device.rs +++ b/devices/src/pci/pci_device.rs @@ -11,6 +11,7 @@ use resources::{Error as SystemAllocatorFaliure, SystemAllocator}; use crate::pci::pci_configuration; use crate::pci::{PciAddress, PciInterruptPin}; use crate::{BusAccessInfo, BusDevice}; +use crate::virtio::snd::vios_backend::Error as VioSError; #[derive(Debug)] pub enum Error { @@ -23,6 +24,9 @@ pub enum Error { /// Create cras client failed. #[cfg(feature = "audio")] CreateCrasClientFailed(libcras::Error), + /// Create VioS client failed. + #[cfg(feature = "audio")] + CreateViosClientFailed(VioSError), } pub type Result = std::result::Result; @@ -34,6 +38,8 @@ impl Display for Error { CapabilitiesSetup(e) => write!(f, "failed to add capability {}", e), #[cfg(feature = "audio")] CreateCrasClientFailed(e) => write!(f, "failed to create CRAS Client: {}", e), + #[cfg(feature = "audio")] + CreateViosClientFailed(e) => write!(f, "failed to create VioS Client: {}", e), IoAllocationFailed(size, e) => write!( f, "failed to allocate space for an IO BAR, size={}: {}", diff --git a/devices/src/virtio/snd/constants.rs b/devices/src/virtio/snd/constants.rs index 176d81f0d3..38b06c4968 100644 --- a/devices/src/virtio/snd/constants.rs +++ b/devices/src/virtio/snd/constants.rs @@ -13,6 +13,11 @@ pub const STREAM_STOP: u32 = 0x0100 + 5; pub const CHANNEL_MAP_INFO: u32 = 0x0200; +pub const VIRTIO_SND_S_OK: u32 = 0x8000; +pub const VIRTIO_SND_S_BAD_MSG: u32 = 0x8001; +pub const VIRTIO_SND_S_NOT_SUPP: u32 = 0x8002; +pub const VIRTIO_SND_S_IO_ERR: u32 = 0x8003; + pub const VIRTIO_SND_D_OUTPUT: u8 = 0; pub const VIRTIO_SND_D_INPUT: u8 = 1; diff --git a/devices/src/virtio/snd/layout.rs b/devices/src/virtio/snd/layout.rs index 38e7779853..c78a9d7a41 100644 --- a/devices/src/virtio/snd/layout.rs +++ b/devices/src/virtio/snd/layout.rs @@ -4,7 +4,7 @@ use data_model::{DataInit, Le32, Le64}; -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Default)] #[repr(C)] pub struct virtio_snd_hdr { pub code: Le32, @@ -23,7 +23,7 @@ pub struct virtio_snd_query_info { // Safe because it only has data and has no implicit padding. unsafe impl DataInit for virtio_snd_query_info {} -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Default)] #[repr(C)] pub struct virtio_snd_info { pub hda_fn_nid: Le32, @@ -31,7 +31,7 @@ pub struct virtio_snd_info { // Safe because it only has data and has no implicit padding. unsafe impl DataInit for virtio_snd_info {} -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Default)] #[repr(C)] pub struct virtio_snd_pcm_info { pub hdr: virtio_snd_info, diff --git a/devices/src/virtio/snd/mod.rs b/devices/src/virtio/snd/mod.rs index d4d4c71325..5a772aed1c 100644 --- a/devices/src/virtio/snd/mod.rs +++ b/devices/src/virtio/snd/mod.rs @@ -5,3 +5,5 @@ pub mod constants; pub mod layout; + +pub mod vios_backend; diff --git a/devices/src/virtio/snd/vios_backend/mod.rs b/devices/src/virtio/snd/vios_backend/mod.rs new file mode 100644 index 0000000000..416fe77259 --- /dev/null +++ b/devices/src/virtio/snd/vios_backend/mod.rs @@ -0,0 +1,11 @@ +// Copyright 2020 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. + +mod shm_streams; +mod shm_vios; + +#[cfg(target_os = "linux")] +pub use self::shm_streams::*; + +pub use self::shm_vios::*; diff --git a/devices/src/virtio/snd/vios_backend/shm_streams.rs b/devices/src/virtio/snd/vios_backend/shm_streams.rs new file mode 100644 index 0000000000..c83ae86ef3 --- /dev/null +++ b/devices/src/virtio/snd/vios_backend/shm_streams.rs @@ -0,0 +1,259 @@ +// Copyright 2020 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. + +//! Provides an implementation of the audio_streams::shm_streams::ShmStream trait using the VioS +//! client. +//! Given that the VioS server doesn't emit an event when the next buffer is expected, this +//! implementation uses thread::sleep to drive the frame timings. + +use super::shm_vios::{VioSClient, VioSStreamParams}; + +use crate::virtio::snd::constants::*; + +use audio_streams::shm_streams::{BufferSet, ServerRequest, ShmStream, ShmStreamSource}; +use audio_streams::{BoxError, SampleFormat, StreamDirection, StreamEffect}; + +use base::{error, SharedMemory, SharedMemoryUnix}; + +use std::fs::File; +use std::os::unix::io::{AsRawFd, FromRawFd, RawFd}; +use std::path::Path; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use sync::Mutex; +use sys_util::{Error as SysError, SharedMemory as SysSharedMemory}; + +use super::shm_vios::{Error, Result}; + +// This is the error type used in audio_streams::shm_streams. Unfortunately, it's not declared +// public there so it needs to be re-declared here. It also prevents the usage of anyhow::Error. +type GenericResult = std::result::Result; + +/// Adapter that provides the ShmStreamSource trait around the VioS backend. +pub struct VioSShmStreamSource { + // Reference counting is needed because the streams also need a reference to the client to push + // buffers and release the stream on drop and it has to implement Send because ShmStreamSource + // requires it, so the only possibility is Arc. + vios_client: Arc>, +} + +impl VioSShmStreamSource { + /// Creates a new stream source given the path to the audio server's socket. + pub fn new>(server: P) -> Result { + Ok(Self { + vios_client: Arc::new(Mutex::new(VioSClient::try_new(server)?)), + }) + } +} + +fn from_sample_format(format: SampleFormat) -> u8 { + match format { + SampleFormat::U8 => VIRTIO_SND_PCM_FMT_U8, + SampleFormat::S16LE => VIRTIO_SND_PCM_FMT_U16, + SampleFormat::S24LE => VIRTIO_SND_PCM_FMT_U24, + SampleFormat::S32LE => VIRTIO_SND_PCM_FMT_U32, + } +} + +fn virtio_frame_rate(frame_rate: u32) -> GenericResult { + Ok(match frame_rate { + 5512u32 => VIRTIO_SND_PCM_RATE_5512, + 8000u32 => VIRTIO_SND_PCM_RATE_8000, + 11025u32 => VIRTIO_SND_PCM_RATE_11025, + 16000u32 => VIRTIO_SND_PCM_RATE_16000, + 22050u32 => VIRTIO_SND_PCM_RATE_22050, + 32000u32 => VIRTIO_SND_PCM_RATE_32000, + 44100u32 => VIRTIO_SND_PCM_RATE_44100, + 48000u32 => VIRTIO_SND_PCM_RATE_48000, + 64000u32 => VIRTIO_SND_PCM_RATE_64000, + 88200u32 => VIRTIO_SND_PCM_RATE_88200, + 96000u32 => VIRTIO_SND_PCM_RATE_96000, + 176400u32 => VIRTIO_SND_PCM_RATE_176400, + 192000u32 => VIRTIO_SND_PCM_RATE_192000, + 384000u32 => VIRTIO_SND_PCM_RATE_384000, + _ => { + return Err(Box::new(Error::UnsupportedFrameRate(frame_rate))); + } + }) +} + +impl ShmStreamSource for VioSShmStreamSource { + /// Creates a new stream + #[allow(clippy::too_many_arguments)] + fn new_stream( + &mut self, + direction: StreamDirection, + num_channels: usize, + format: SampleFormat, + frame_rate: u32, + buffer_size: usize, + _effects: &[StreamEffect], + client_shm: &SysSharedMemory, + _buffer_offsets: [u64; 2], + ) -> GenericResult> { + let mut vios_client = self.vios_client.lock(); + match direction { + StreamDirection::Playback => { + match vios_client.get_unused_stream_id(VIRTIO_SND_D_OUTPUT) { + Some(stream_id) => { + vios_client.prepare_stream(stream_id)?; + let frame_size = num_channels * format.sample_bytes(); + let period_bytes = (frame_size * buffer_size) as u32; + let params = VioSStreamParams { + buffer_bytes: 2 * period_bytes, + period_bytes, + features: 0u32, + channels: num_channels as u8, + format: from_sample_format(format), + rate: virtio_frame_rate(frame_rate)?, + }; + vios_client.set_stream_parameters(stream_id, params)?; + vios_client.start_stream(stream_id)?; + VioSndShmStream::new( + buffer_size, + num_channels, + format, + frame_rate, + stream_id, + self.vios_client.clone(), + client_shm, + ) + } + None => Err(Box::new(Error::NoStreamsAvailable)), + } + } + StreamDirection::Capture => panic!("Capture not yet supported"), + } + } + + /// Get a list of file descriptors used by the implementation. + /// + /// Returns any open file descriptors needed by the implementation. + /// This list helps users of the ShmStreamSource enter Linux jails without + /// closing needed file descriptors. + fn keep_fds(&self) -> Vec { + self.vios_client.lock().keep_fds() + } +} + +/// Adapter around a VioS stream that implements the ShmStream trait. +pub struct VioSndShmStream { + num_channels: usize, + frame_rate: u32, + buffer_size: usize, + frame_size: usize, + interval: Duration, + next_frame: Duration, + start_time: Instant, + stream_id: u32, + vios_client: Arc>, + client_shm: SharedMemory, +} + +impl VioSndShmStream { + /// Creates a new shm stream. + fn new( + buffer_size: usize, + num_channels: usize, + format: SampleFormat, + frame_rate: u32, + stream_id: u32, + vios_client: Arc>, + client_shm: &SysSharedMemory, + ) -> GenericResult> { + let interval = Duration::from_millis(buffer_size as u64 * 1000 / frame_rate as u64); + + let dup_fd = unsafe { + // Safe because dup doesn't affect memory and client_shm should wrap a known valid file + // descriptor + libc::dup(client_shm.as_raw_fd()) + }; + if dup_fd < 0 { + return Err(Box::new(Error::DupError(SysError::last()))); + } + let file = unsafe { + // safe because we checked the result of libc::dup() + File::from_raw_fd(dup_fd) + }; + let client_shm_clone = + SharedMemory::from_file(file).map_err(|e| Error::BaseMmapError(e))?; + + Ok(Box::new(Self { + num_channels, + frame_rate, + buffer_size, + frame_size: format.sample_bytes() * num_channels, + interval, + next_frame: interval, + start_time: Instant::now(), + stream_id, + vios_client, + client_shm: client_shm_clone, + })) + } +} + +impl ShmStream for VioSndShmStream { + fn frame_size(&self) -> usize { + self.frame_size + } + + fn num_channels(&self) -> usize { + self.num_channels + } + + fn frame_rate(&self) -> u32 { + self.frame_rate + } + + /// Waits until the next time a frame should be sent to the server. The server may release the + /// previous buffer much sooner than it needs the next one, so this function may sleep to wait + /// for the right time. + fn wait_for_next_action_with_timeout<'b>( + &'b mut self, + timeout: Duration, + ) -> GenericResult>> { + let elapsed = self.start_time.elapsed(); + if elapsed < self.next_frame { + if timeout < self.next_frame - elapsed { + std::thread::sleep(timeout); + return Ok(None); + } else { + std::thread::sleep(self.next_frame - elapsed); + } + } + self.next_frame += self.interval; + Ok(Some(ServerRequest::new(self.buffer_size, self))) + } +} + +impl BufferSet for VioSndShmStream { + fn callback(&mut self, offset: usize, frames: usize) -> GenericResult<()> { + self.vios_client.lock().inject_audio_data( + self.stream_id, + &mut self.client_shm, + offset, + frames * self.frame_size, + )?; + Ok(()) + } + + fn ignore(&mut self) -> GenericResult<()> { + Ok(()) + } +} + +impl Drop for VioSndShmStream { + fn drop(&mut self) { + let mut client = self.vios_client.lock(); + let stream_id = self.stream_id; + if let Err(e) = client + .stop_stream(stream_id) + .and_then(|_| client.release_stream(stream_id)) + { + error!("Failed to stop and release stream {}: {}", stream_id, e); + } + } +} diff --git a/devices/src/virtio/snd/vios_backend/shm_vios.rs b/devices/src/virtio/snd/vios_backend/shm_vios.rs new file mode 100644 index 0000000000..d6372e68cc --- /dev/null +++ b/devices/src/virtio/snd/vios_backend/shm_vios.rs @@ -0,0 +1,548 @@ +// Copyright 2020 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 crate::virtio::snd::constants::*; +use crate::virtio::snd::layout::*; + +use base::{ + net::UnixSeqpacket, Error as BaseError, FromRawDescriptor, IntoRawDescriptor, MemoryMapping, + MemoryMappingBuilder, MmapError, SafeDescriptor, ScmSocket, SharedMemory, +}; +use data_model::{DataInit, VolatileMemory, VolatileMemoryError}; + +use std::fs::File; +use std::io::{Error as IOError, ErrorKind as IOErrorKind, Seek, SeekFrom}; +use std::os::unix::io::{AsRawFd, FromRawFd, RawFd}; +use std::path::Path; + +use thiserror::Error as ThisError; + +pub type Result = std::result::Result; + +#[derive(ThisError, Debug)] +pub enum Error { + #[error("Failed to connect to VioS server: {0:?}")] + ServerConnectionError(IOError), + #[error("Failed to communicate with VioS server: {0:?}")] + ServerError(BaseError), + #[error("Failed to communicate with VioS server: {0:?}")] + ServerIOError(IOError), + #[error("Failed to get size of tx shared memory: {0}")] + FileSizeError(IOError), + #[error("Error duplicating file descriptor: {0}")] + DupError(BaseError), + #[error("Error accessing VioS server's shared memory: {0}")] + ServerMmapError(MmapError), + #[error("Error accessing guest's shared memory: {0}")] + GuestMmapError(MmapError), + #[error("Error memory mapping client_shm: {0}")] + BaseMmapError(BaseError), + #[error("Error accessing volatile memory: {0}")] + VolatileMemoryError(VolatileMemoryError), + #[error("{0}")] + ProtocolError(ProtocolErrorKind), + #[error("No PCM streams available")] + NoStreamsAvailable, + #[error("No stream with id {0}")] + InvalidStreamId(u32), + #[error("Stream is unexpected state: {0:?}")] + UnexpectedState(StreamState), + #[error("Invalid operation for stream direction: {0}")] + WrongDirection(u8), + #[error("Insuficient space for the new buffer in the queue's buffer area")] + OutOfSpace, + #[error("Unsupported frame rate: {0}")] + UnsupportedFrameRate(u32), + #[error("Platform not supported")] + PlatformNotSupported, + #[error("Command failed with status {0}")] + CommandFailed(u32), +} + +#[derive(ThisError, Debug)] +pub enum ProtocolErrorKind { + #[error("The server sent a config of the wrong size: {0}")] + UnexpectedConfigSize(usize), + #[error("Received {1} file descriptors from the server, expected {0}")] + UnexpectedNumberOfFileDescriptors(usize, usize), // expected, received + #[error("Server's version ({0}) doesn't match client's")] + VersionMismatch(u32), + #[error("Received a msg with an unexpected size: expected {0}, received {1}")] + UnexpectedMessageSize(usize, usize), // expected, received +} + +/// The client for the VioS backend +/// +/// Uses a protocol equivalent to virtio-snd over a shared memory file and a unix socket for +/// notifications. +pub struct VioSClient { + config: VioSConfig, + streams: Vec, + control_socket: UnixSeqpacket, + event_socket: UnixSeqpacket, + tx: IoBufferQueue, + rx: IoBufferQueue, +} + +impl VioSClient { + /// Create a new client given the path to the audio server's socket. + pub fn try_new>(server: P) -> Result { + let client_socket = + UnixSeqpacket::connect(server).map_err(|e| Error::ServerConnectionError(e))?; + let mut config: VioSConfig = Default::default(); + let mut fds: Vec = Vec::new(); + const NUM_FDS: usize = 5; + fds.resize(NUM_FDS, 0 as RawFd); + let (recv_size, fd_count) = client_socket + .recv_with_fds(config.as_mut_slice(), &mut fds) + .map_err(|e| Error::ServerError(e))?; + + // Resize the vector to the actual number of file descriptors received and wrap them in + // SafeDescriptors to prevent leaks + fds.resize(fd_count, -1 as RawFd); + let mut safe_fds: Vec = fds + .into_iter() + .map(|fd| unsafe { + // safe because the SafeDescriptor object completely assumes ownership of the fd. + SafeDescriptor::from_raw_descriptor(fd) + }) + .collect(); + + if recv_size != std::mem::size_of::() { + return Err(Error::ProtocolError( + ProtocolErrorKind::UnexpectedConfigSize(recv_size), + )); + } + + if config.version != VIOS_VERSION { + return Err(Error::ProtocolError(ProtocolErrorKind::VersionMismatch( + config.version, + ))); + } + + fn pop( + safe_fds: &mut Vec, + expected: usize, + received: usize, + ) -> Result { + unsafe { + // Safe because we transfer ownership from the SafeDescriptor to T + Ok(T::from_raw_fd( + safe_fds + .pop() + .ok_or(Error::ProtocolError( + ProtocolErrorKind::UnexpectedNumberOfFileDescriptors( + expected, received, + ), + ))? + .into_raw_descriptor(), + )) + } + } + + let rx_shm_file = pop::(&mut safe_fds, NUM_FDS, fd_count)?; + let tx_shm_file = pop::(&mut safe_fds, NUM_FDS, fd_count)?; + let rx_socket = pop::(&mut safe_fds, NUM_FDS, fd_count)?; + let tx_socket = pop::(&mut safe_fds, NUM_FDS, fd_count)?; + let event_socket = pop::(&mut safe_fds, NUM_FDS, fd_count)?; + + if !safe_fds.is_empty() { + return Err(Error::ProtocolError( + ProtocolErrorKind::UnexpectedNumberOfFileDescriptors(NUM_FDS, fd_count), + )); + } + + let control_socket = client_socket; + + let mut client = VioSClient { + config, + streams: Vec::new(), + control_socket, + event_socket, + tx: IoBufferQueue::new(tx_socket, tx_shm_file)?, + rx: IoBufferQueue::new(rx_socket, rx_shm_file)?, + }; + client.request_and_cache_streams_info()?; + + Ok(client) + } + + /// Get a description of the available sound streams. + pub fn streams(&self) -> &Vec { + &self.streams + } + + /// Gets an unused stream id of the specified direction. `direction` must be one of + /// VIRTIO_SND_D_INPUT OR VIRTIO_SND_D_OUTPUT. + pub fn get_unused_stream_id(&self, direction: u8) -> Option { + self.streams + .iter() + .filter(|s| s.state == StreamState::Available && s.direction == direction as u8) + .map(|s| s.id) + .next() + } + + /// Configures a stream with the given parameters. + pub fn set_stream_parameters( + &mut self, + stream_id: u32, + params: VioSStreamParams, + ) -> Result<()> { + self.validate_stream_id(stream_id, &[StreamState::Available, StreamState::Acquired])?; + let raw_params: virtio_snd_pcm_set_params = (stream_id, params).into(); + seq_socket_send(&self.control_socket, raw_params)?; + self.recv_cmd_status()?; + self.streams[stream_id as usize].state = StreamState::Acquired; + Ok(()) + } + + /// Send the PREPARE_STREAM command to the server. + pub fn prepare_stream(&mut self, stream_id: u32) -> Result<()> { + self.common_stream_op( + stream_id, + &[StreamState::Available, StreamState::Acquired], + StreamState::Acquired, + STREAM_PREPARE, + ) + } + + /// Send the RELEASE_STREAM command to the server. + pub fn release_stream(&mut self, stream_id: u32) -> Result<()> { + self.common_stream_op( + stream_id, + &[StreamState::Acquired], + StreamState::Available, + STREAM_RELEASE, + ) + } + + /// Send the START_STREAM command to the server. + pub fn start_stream(&mut self, stream_id: u32) -> Result<()> { + self.common_stream_op( + stream_id, + &[StreamState::Acquired], + StreamState::Active, + STREAM_START, + ) + } + + /// Send the STOP_STREAM command to the server. + pub fn stop_stream(&mut self, stream_id: u32) -> Result<()> { + self.common_stream_op( + stream_id, + &[StreamState::Active], + StreamState::Acquired, + STREAM_STOP, + ) + } + + /// Send audio frames to the server. The audio data is taken from a shared memory resource. + pub fn inject_audio_data( + &mut self, + stream_id: u32, + buffer: &mut SharedMemory, + src_offset: usize, + size: usize, + ) -> Result<()> { + self.validate_stream_id(stream_id, &[StreamState::Active])?; + if self.streams[stream_id as usize].direction != VIRTIO_SND_D_OUTPUT { + return Err(Error::WrongDirection( + self.streams[stream_id as usize].direction, + )); + } + let dst_offset = self.tx.push_buffer(buffer, src_offset, size)?; + let msg = IoTransferMsg::new(stream_id, dst_offset, size); + seq_socket_send(&self.tx.socket, msg) + } + + /// Get a list of file descriptors used by the implementation. + pub fn keep_fds(&self) -> Vec { + vec![ + self.control_socket.as_raw_fd(), + self.event_socket.as_raw_fd(), + self.tx.socket.as_raw_fd(), + self.rx.socket.as_raw_fd(), + self.tx.file.as_raw_fd(), + self.rx.file.as_raw_fd(), + ] + } + + fn validate_stream_id(&self, stream_id: u32, permitted_states: &[StreamState]) -> Result<()> { + if stream_id >= self.streams.len() as u32 { + return Err(Error::InvalidStreamId(stream_id)); + } + if !permitted_states.contains(&self.streams[stream_id as usize].state) { + return Err(Error::UnexpectedState( + self.streams[stream_id as usize].state, + )); + } + Ok(()) + } + + fn common_stream_op( + &mut self, + stream_id: u32, + expected_states: &[StreamState], + new_state: StreamState, + op: u32, + ) -> Result<()> { + self.validate_stream_id(stream_id, expected_states)?; + let msg = virtio_snd_pcm_hdr { + hdr: virtio_snd_hdr { code: op.into() }, + stream_id: stream_id.into(), + }; + seq_socket_send(&self.control_socket, msg)?; + self.recv_cmd_status()?; + self.streams[stream_id as usize].state = new_state; + Ok(()) + } + + fn recv_cmd_status(&mut self) -> Result<()> { + let mut status: virtio_snd_hdr = Default::default(); + self.control_socket + .recv(status.as_mut_slice()) + .map_err(|e| Error::ServerIOError(e))?; + if status.code.to_native() == VIRTIO_SND_S_OK { + Ok(()) + } else { + Err(Error::CommandFailed(status.code.to_native())) + } + } + + fn request_and_cache_streams_info(&mut self) -> Result<()> { + let num_streams = self.config.streams as usize; + let info_size = std::mem::size_of::(); + let req = virtio_snd_query_info { + hdr: virtio_snd_hdr { + code: STREAM_INFO.into(), + }, + start_id: 0u32.into(), + count: (num_streams as u32).into(), + size: (std::mem::size_of::() as u32).into(), + }; + seq_socket_send(&self.control_socket, req)?; + self.recv_cmd_status()?; + let info_vec = self + .control_socket + .recv_as_vec() + .map_err(|e| Error::ServerIOError(e))?; + if info_vec.len() != num_streams * info_size { + return Err(Error::ProtocolError( + ProtocolErrorKind::UnexpectedMessageSize(num_streams * info_size, info_vec.len()), + )); + } + self.streams = info_vec + .chunks(info_size) + .enumerate() + .map(|(id, info_buffer)| { + // unwrap is safe because we checked the size of the vector above + let virtio_stream_info = virtio_snd_pcm_info::from_slice(&info_buffer).unwrap(); + VioSStreamInfo::new(id as u32, &virtio_stream_info) + }) + .collect(); + Ok(()) + } +} + +struct IoBufferQueue { + socket: UnixSeqpacket, + file: File, + mmap: MemoryMapping, + size: usize, + next: usize, +} + +impl IoBufferQueue { + fn new(socket: UnixSeqpacket, mut file: File) -> Result { + let size = file + .seek(SeekFrom::End(0)) + .map_err(|e| Error::FileSizeError(e))? as usize; + + let mmap = MemoryMappingBuilder::new(size) + .from_descriptor(&file) + .build() + .map_err(|e| Error::ServerMmapError(e))?; + + Ok(IoBufferQueue { + socket, + file, + mmap, + size, + next: 0, + }) + } + + fn push_buffer(&mut self, src: &mut SharedMemory, offset: usize, size: usize) -> Result { + if size > self.size { + return Err(Error::OutOfSpace); + } + let shm_offset = if size > self.size - self.next { + // Can't fit the new buffer at the end of the area, so put it at the beginning + 0 + } else { + self.next + }; + + let (src_mmap, mmap_offset) = mmap_buffer(src, offset, size)?; + let src_slice = src_mmap + .get_slice(mmap_offset, size) + .map_err(|e| Error::VolatileMemoryError(e))?; + let dst_slice = self + .mmap + .get_slice(shm_offset, size) + .map_err(|e| Error::VolatileMemoryError(e))?; + src_slice.copy_to_volatile_slice(dst_slice); + self.next = shm_offset + size; + Ok(shm_offset) + } +} + +/// Description of a stream made available by the server. +pub struct VioSStreamInfo { + pub id: u32, + pub hda_fn_nid: u32, + pub features: u32, + pub formats: u64, + pub rates: u64, + pub direction: u8, + pub channels_min: u8, + pub channels_max: u8, + state: StreamState, +} + +impl VioSStreamInfo { + fn new(id: u32, info: &virtio_snd_pcm_info) -> VioSStreamInfo { + VioSStreamInfo { + id, + hda_fn_nid: info.hdr.hda_fn_nid.to_native(), + features: info.features.to_native(), + formats: info.formats.to_native(), + rates: info.rates.to_native(), + direction: info.direction, + channels_min: info.channels_min, + channels_max: info.channels_max, + state: StreamState::Available, + } + } +} + +#[derive(PartialEq, Debug, Copy, Clone)] +pub enum StreamState { + Available, + Acquired, + Active, +} + +/// Groups the parameters used to configure a stream prior to using it. +pub struct VioSStreamParams { + pub buffer_bytes: u32, + pub period_bytes: u32, + pub features: u32, + pub channels: u8, + pub format: u8, + pub rate: u8, +} + +impl Into for (u32, VioSStreamParams) { + fn into(self) -> virtio_snd_pcm_set_params { + virtio_snd_pcm_set_params { + hdr: virtio_snd_pcm_hdr { + hdr: virtio_snd_hdr { + code: STREAM_SET_PARAMS.into(), + }, + stream_id: self.0.into(), + }, + buffer_bytes: self.1.buffer_bytes.into(), + period_bytes: self.1.period_bytes.into(), + features: self.1.features.into(), + channels: self.1.channels, + format: self.1.format, + rate: self.1.rate, + padding: 0u8, + } + } +} + +/// Memory map a shared memory object to access an audio buffer. The buffer may not be located at an +/// offset aligned to page size, so the offset within the mapped region is returned along with the +/// MemoryMapping struct. +fn mmap_buffer( + src: &mut SharedMemory, + offset: usize, + size: usize, +) -> Result<(MemoryMapping, usize)> { + // If the buffer is not aligned to page size a bigger region needs to be mapped. + let aligned_offset = offset & !(base::pagesize() - 1); + let offset_from_mapping_start = offset - aligned_offset; + let extended_size = size + offset_from_mapping_start; + + let mmap = MemoryMappingBuilder::new(extended_size) + .offset(aligned_offset as u64) + .from_descriptor(src) + .build() + .map_err(|e| Error::GuestMmapError(e))?; + + Ok((mmap, offset_from_mapping_start)) +} + +fn seq_socket_send(socket: &UnixSeqpacket, data: T) -> Result<()> { + loop { + let send_res = socket.send(data.as_slice()); + if let Err(e) = send_res { + match e.kind() { + // Retry if interrupted + IOErrorKind::Interrupted => continue, + _ => return Err(Error::ServerIOError(e)), + } + } + // Success + break; + } + Ok(()) +} + +const VIOS_VERSION: u32 = 1; + +#[repr(C)] +#[derive(Copy, Clone, Default)] +struct VioSConfig { + version: u32, + jacks: u32, + streams: u32, + chmaps: u32, +} +// Safe because it only has data and has no implicit padding. +unsafe impl DataInit for VioSConfig {} + +#[repr(C)] +#[derive(Copy, Clone)] +struct IoTransferMsg { + io_xfer: virtio_snd_pcm_xfer, + buffer_offset: u32, + buffer_len: u32, +} +// Safe because it only has data and has no implicit padding. +unsafe impl DataInit for IoTransferMsg {} + +impl IoTransferMsg { + fn new(stream_id: u32, buffer_offset: usize, buffer_len: usize) -> IoTransferMsg { + IoTransferMsg { + io_xfer: virtio_snd_pcm_xfer { + stream_id: stream_id.into(), + }, + buffer_offset: buffer_offset as u32, + buffer_len: buffer_len as u32, + } + } +} + +#[repr(C)] +#[derive(Copy, Clone)] +struct IoStatusMsg { + status: virtio_snd_pcm_status, + buffer_offset: u32, + consumed_len: u32, +} +// Safe because it only has data and has no implicit padding. +unsafe impl DataInit for IoStatusMsg {} diff --git a/seccomp/aarch64/vios_audio_device.policy b/seccomp/aarch64/vios_audio_device.policy new file mode 100644 index 0000000000..df54139f51 --- /dev/null +++ b/seccomp/aarch64/vios_audio_device.policy @@ -0,0 +1,13 @@ +# Copyright 2021 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. + +@include /usr/share/policy/crosvm/common_device.policy + +clock_gettime: 1 +clock_nanosleep: 1 +lseek: 1 +openat: return ENOENT +prlimit64: 1 +sched_setscheduler: 1 +setrlimit: 1 diff --git a/seccomp/arm/vios_audio_device.policy b/seccomp/arm/vios_audio_device.policy new file mode 100644 index 0000000000..ad27b0e362 --- /dev/null +++ b/seccomp/arm/vios_audio_device.policy @@ -0,0 +1,14 @@ +# Copyright 2021 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. + +@include /usr/share/policy/crosvm/common_device.policy + +clock_gettime: 1 +clock_nanosleep: 1 +lseek: 1 +open: return ENOENT +openat: return ENOENT +prlimit64: 1 +sched_setscheduler: 1 +setrlimit: 1 diff --git a/seccomp/x86_64/vios_audio_device.policy b/seccomp/x86_64/vios_audio_device.policy new file mode 100644 index 0000000000..ad27b0e362 --- /dev/null +++ b/seccomp/x86_64/vios_audio_device.policy @@ -0,0 +1,14 @@ +# Copyright 2021 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. + +@include /usr/share/policy/crosvm/common_device.policy + +clock_gettime: 1 +clock_nanosleep: 1 +lseek: 1 +open: return ENOENT +openat: return ENOENT +prlimit64: 1 +sched_setscheduler: 1 +setrlimit: 1 diff --git a/src/argument.rs b/src/argument.rs index abe94c2902..525506df74 100644 --- a/src/argument.rs +++ b/src/argument.rs @@ -51,7 +51,7 @@ use std::result; pub enum Error { /// There was a syntax error with the argument. Syntax(String), - /// The argumen's name is unused. + /// The argument's name is unused. UnknownArgument(String), /// The argument was required. ExpectedArgument(String), diff --git a/src/main.rs b/src/main.rs index 6954f6ad11..4eb7411a3b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ use std::fs::{File, OpenOptions}; use std::io::{BufRead, BufReader}; use std::num::ParseIntError; use std::path::{Path, PathBuf}; +use std::str::FromStr; use std::string::String; use std::thread::sleep; use std::time::Duration; @@ -396,6 +397,16 @@ fn parse_ac97_options(s: &str) -> argument::Result { argument::Error::Syntax(format!("invalid capture option: {}", e)) })?; } + #[cfg(target_os = "linux")] + "server" => { + ac97_params.vios_server_path = + Some( + PathBuf::from_str(v).map_err(|e| argument::Error::InvalidValue { + value: v.to_string(), + expected: e.to_string(), + })?, + ); + } _ => { return Err(argument::Error::UnknownArgument(format!( "unknown ac97 parameter {}", @@ -405,6 +416,25 @@ fn parse_ac97_options(s: &str) -> argument::Result { } } + // server is required for and exclusive to vios backend + #[cfg(target_os = "linux")] + match ac97_params.backend { + Ac97Backend::VIOS => { + if ac97_params.vios_server_path.is_none() { + return Err(argument::Error::ExpectedArgument(String::from( + "server argument is required for VIOS backend", + ))); + } + } + _ => { + if ac97_params.vios_server_path.is_some() { + return Err(argument::Error::UnexpectedValue(String::from( + "server argument is exclusive to the VIOS backend", + ))); + } + } + } + Ok(ac97_params) } @@ -1605,13 +1635,14 @@ fn run_vm(args: std::env::Args) -> std::result::Result<(), ()> { Argument::value("net-vq-pairs", "N", "virtio net virtual queue paris. (default: 1)"), #[cfg(feature = "audio")] Argument::value("ac97", - "[backend=BACKEND,capture=true,capture_effect=EFFECT]", + "[backend=BACKEND,capture=true,capture_effect=EFFECT,shm-fd=FD,client-fd=FD,server-fd=FD]", "Comma separated key=value pairs for setting up Ac97 devices. Can be given more than once . Possible key values: - backend=(null, cras) - Where to route the audio device. If not provided, backend will default to null. - `null` for /dev/null, and cras for CRAS server. + backend=(null, cras, vios) - Where to route the audio device. If not provided, backend will default to null. + `null` for /dev/null, cras for CRAS server and vios for VioS server. capture - Enable audio capture - capture_effects - | separated effects to be enabled for recording. The only supported effect value now is EchoCancellation or aec."), + capture_effects - | separated effects to be enabled for recording. The only supported effect value now is EchoCancellation or aec. + server - The to the VIOS server (unix socket)."), Argument::value("serial", "type=TYPE,[hardware=HW,num=NUM,path=PATH,input=PATH,console,earlycon,stdin]", "Comma separated key=value pairs for setting up serial devices. Can be given more than once. @@ -2401,6 +2432,13 @@ mod tests { parse_ac97_options("backend=cras,capture=true").expect("parse should have succeded"); } + #[cfg(feature = "audio")] + #[test] + fn parse_ac97_vios_valid() { + parse_ac97_options("backend=vios,server=/path/to/server") + .expect("parse should have succeded"); + } + #[test] fn parse_serial_vaild() { parse_serial_options("type=syslog,num=1,console=true,stdin=true")