virtio_media/
lib.rs

1// Copyright 2024 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//! This crate contains host-side helpers to write virtio-media devices and full devices
6//! implementations.
7//!
8//! Both helpers and devices are VMM-independent and rely on a handful of traits being implemented
9//! to operate on a given VMM. This means that implementing a specific device, and adding support
10//! for all virtio-media devices on a given VMM, are two completely orthogonal tasks. Adding
11//! support for a VMM makes all the devices relying on this crate available. Conversely, writing a
12//! new device using this crate makes it available to all supported VMMs.
13//!
14//! # Traits to implement by the VMM
15//!
16//! * Descriptor chains must implement `Read` and `Write` on their device-readable and
17//!   device-writable parts, respectively. This allows devices to read commands and writes
18//!   responses.
19//! * The event queue must implement the `VirtioMediaEventQueue` trait to allow devices to send
20//!   events to the guest.
21//! * The guest memory must be made accessible through an implementation of
22//!   `VirtioMediaGuestMemoryMapper`.
23//! * Optionally, .... can be implemented if the host supports mapping MMAP buffers into the guest
24//!   address space.
25//!
26//! These traits allow any device that implements `VirtioMediaDevice` to run on any VMM that
27//! implements them.
28//!
29//! # Anatomy of a device
30//!
31//! Devices implement `VirtioMediaDevice` to provide ways to create and close sessions, and to make
32//! MMAP buffers visible to the guest (if supported). They also typically implement
33//! `VirtioMediaIoctlHandler` and make use of `virtio_media_dispatch_ioctl` to handle ioctls
34//! simply.
35//!
36//! The VMM then uses `VirtioMediaDeviceRunner` in order to ask it to process a command whenever
37//! one arrives on the command queue.
38//!
39//! By following this pattern, devices never need to care about deserializing and validating the
40//! virtio-media protocol. Instead, their relevant methods are invoked when needed, on validated
41//! input, while protocol errors are handled upstream in a way that is consistent for all devices.
42//!
43//! The devices currently in this crate are:
44//!
45//! * A device that proxies any host V4L2 device into the guest, in the `crate::v4l2_device_proxy`
46//!   module.
47
48pub mod devices;
49pub mod io;
50pub mod ioctl;
51pub mod memfd;
52pub mod mmap;
53pub mod poll;
54pub mod protocol;
55
56use io::ReadFromDescriptorChain;
57use io::WriteToDescriptorChain;
58use poll::SessionPoller;
59pub use v4l2r;
60
61use std::collections::HashMap;
62use std::io::Result as IoResult;
63use std::os::fd::BorrowedFd;
64
65use anyhow::Context;
66use log::error;
67
68use protocol::*;
69
70/// Trait for sending V4L2 events to the driver.
71pub trait VirtioMediaEventQueue {
72    /// Wait until an event descriptor becomes available and send `event` to the guest.
73    fn send_event(&mut self, event: V4l2Event);
74
75    /// Wait until an event descriptor becomes available and send `errno` as an error event to the
76    /// guest.
77    fn send_error(&mut self, session_id: u32, errno: i32) {
78        self.send_event(V4l2Event::Error(ErrorEvent::new(session_id, errno)));
79    }
80}
81
82/// Trait for representing a range of guest memory that has been mapped linearly into the host's
83/// address space.
84pub trait GuestMemoryRange {
85    fn as_ptr(&self) -> *const u8;
86    fn as_mut_ptr(&mut self) -> *mut u8;
87}
88
89/// Trait enabling guest memory linear access for the device.
90///
91/// Although the host can access the guest memory, it sometimes need to have a linear view of
92/// sparse areas. This trait provides a way to perform such mappings.
93///
94/// Note to devices: [`VirtioMediaGuestMemoryMapper::GuestMemoryMapping`] instances must be held
95/// for as long as the device might access the memory to avoid race conditions, as some
96/// implementations might e.g. write back into the guest memory at destruction time.
97pub trait VirtioMediaGuestMemoryMapper {
98    /// Host-side linear mapping of sparse guest memory.
99    type GuestMemoryMapping: GuestMemoryRange;
100
101    /// Maps `sgs`, which contains a list of guest-physical SG entries into a linear mapping on the
102    /// host.
103    fn new_mapping(&self, sgs: Vec<SgEntry>) -> anyhow::Result<Self::GuestMemoryMapping>;
104}
105
106/// Trait for mapping host buffers into the guest physical address space.
107///
108/// An VMM-side implementation of this trait is needed in order to map `MMAP` buffers into the
109/// guest.
110///
111/// If the functionality is not needed, `()` can be passed in place of an implementor of this
112/// trait. It will return `ENOTTY` to each `mmap` attempt, effectively disabling the ability to
113/// map `MMAP` buffers into the guest.
114pub trait VirtioMediaHostMemoryMapper {
115    /// Maps `length` bytes of host memory starting at `offset` and backed by `buffer` into the
116    /// guest's shared memory region.
117    ///
118    /// Returns the offset in the guest shared memory region of the start of the mapped memory on
119    /// success, or a `libc` error code in case of failure.
120    fn add_mapping(
121        &mut self,
122        buffer: BorrowedFd,
123        length: u64,
124        offset: u64,
125        rw: bool,
126    ) -> Result<u64, i32>;
127
128    /// Removes a guest mapping previously created at shared memory region offset `shm_offset`.
129    fn remove_mapping(&mut self, shm_offset: u64) -> Result<(), i32>;
130}
131
132/// No-op implementation of `VirtioMediaHostMemoryMapper`. Can be used for testing purposes or when
133/// it is not needed to map `MMAP` buffers into the guest.
134impl VirtioMediaHostMemoryMapper for () {
135    fn add_mapping(&mut self, _: BorrowedFd, _: u64, _: u64, _: bool) -> Result<u64, i32> {
136        Err(libc::ENOTTY)
137    }
138
139    fn remove_mapping(&mut self, _: u64) -> Result<(), i32> {
140        Err(libc::ENOTTY)
141    }
142}
143
144pub trait VirtioMediaDeviceSession {
145    /// Returns the file descriptor that the client can listen to in order to know when a session
146    /// event has occurred. The FD signals that it is readable when the device's `process_events`
147    /// should be called.
148    ///
149    /// If this method returns `None`, then the session does not need to be polled by the client,
150    /// and `process_events` does not need to be called either.
151    fn poll_fd(&self) -> Option<BorrowedFd>;
152}
153
154/// Trait for implementing virtio-media devices.
155///
156/// The preferred way to use this trait is to wrap implementations in a
157/// [`VirtioMediaDeviceRunner`], which takes care of reading and dispatching commands. In addition,
158/// [`ioctl::VirtioMediaIoctlHandler`] should also be used to automatically parse and dispatch
159/// ioctls.
160pub trait VirtioMediaDevice<Reader: ReadFromDescriptorChain, Writer: WriteToDescriptorChain> {
161    type Session: VirtioMediaDeviceSession;
162
163    /// Create a new session which ID is `session_id`.
164    ///
165    /// The error value returned is the error code to send back to the guest.
166    fn new_session(&mut self, session_id: u32) -> Result<Self::Session, i32>;
167    /// Close the passed session.
168    fn close_session(&mut self, session: Self::Session);
169
170    /// Perform the IOCTL command and write the response into `writer`.
171    ///
172    /// The flow for performing a given `ioctl` is to read the parameters from `reader`, perform
173    /// the operation, and then write the result on `writer`. Events triggered by a given ioctl can
174    /// be queued on `evt_queue`.
175    ///
176    /// Only returns an error if the response could not be properly written ; all other errors are
177    /// propagated to the guest and considered normal operation from the host's point of view.
178    ///
179    /// The recommended implementation of this method is to just invoke
180    /// `virtio_media_dispatch_ioctl` on an implementation of `VirtioMediaIoctlHandler`, so all the
181    /// details of ioctl parsing and validation are taken care of by this crate.
182    fn do_ioctl(
183        &mut self,
184        session: &mut Self::Session,
185        ioctl: V4l2Ioctl,
186        reader: &mut Reader,
187        writer: &mut Writer,
188    ) -> IoResult<()>;
189
190    /// Performs the MMAP command.
191    ///
192    /// Only returns an error if the response could not be properly written ; all other errors are
193    /// propagated to the guest.
194    //
195    // TODO: flags should be a dedicated enum?
196    fn do_mmap(
197        &mut self,
198        session: &mut Self::Session,
199        flags: u32,
200        offset: u32,
201    ) -> Result<(u64, u64), i32>;
202    /// Performs the MUNMAP command.
203    ///
204    /// Only returns an error if the response could not be properly written ; all other errors are
205    /// propagated to the guest.
206    fn do_munmap(&mut self, guest_addr: u64) -> Result<(), i32>;
207
208    fn process_events(&mut self, _session: &mut Self::Session) -> Result<(), i32> {
209        panic!("process_events needs to be implemented")
210    }
211}
212
213/// Wrapping structure for a `VirtioMediaDevice` managing its sessions and providing methods for
214/// processing its commands.
215pub struct VirtioMediaDeviceRunner<Reader, Writer, Device, Poller>
216where
217    Reader: ReadFromDescriptorChain,
218    Writer: WriteToDescriptorChain,
219    Device: VirtioMediaDevice<Reader, Writer>,
220    Poller: SessionPoller,
221{
222    pub device: Device,
223    poller: Poller,
224    pub sessions: HashMap<u32, Device::Session>,
225    // TODO: recycle session ids...
226    session_id_counter: u32,
227}
228
229impl<Reader, Writer, Device, Poller> VirtioMediaDeviceRunner<Reader, Writer, Device, Poller>
230where
231    Reader: ReadFromDescriptorChain,
232    Writer: WriteToDescriptorChain,
233    Device: VirtioMediaDevice<Reader, Writer>,
234    Poller: SessionPoller,
235{
236    pub fn new(device: Device, poller: Poller) -> Self {
237        Self {
238            device,
239            poller,
240            sessions: Default::default(),
241            session_id_counter: 0,
242        }
243    }
244}
245
246impl<Reader, Writer, Device, Poller> VirtioMediaDeviceRunner<Reader, Writer, Device, Poller>
247where
248    Reader: ReadFromDescriptorChain,
249    Writer: WriteToDescriptorChain,
250    Device: VirtioMediaDevice<Reader, Writer>,
251    Poller: SessionPoller,
252{
253    /// Handle a single command from the virtio queue.
254    ///
255    /// `reader` and `writer` are the device-readable and device-writable sections of the
256    /// descriptor chain containing the command. After this method has returned, the caller is
257    /// responsible for returning the used descriptor chain to the guest.
258    ///
259    /// This method never returns an error, as doing so would halt the worker thread. All errors
260    /// are propagated to the guest, with the exception of errors triggered while writing the
261    /// response which are logged on the host side.
262    pub fn handle_command(&mut self, reader: &mut Reader, writer: &mut Writer) {
263        let hdr = match reader.read_obj::<CmdHeader>() {
264            Ok(hdr) => hdr,
265            Err(e) => {
266                error!("error while reading command header: {:#}", e);
267                let _ = writer.write_err_response(libc::EINVAL);
268                return;
269            }
270        };
271
272        let res = match hdr.cmd {
273            VIRTIO_MEDIA_CMD_OPEN => {
274                let session_id = self.session_id_counter;
275
276                match self.device.new_session(session_id) {
277                    Ok(session) => {
278                        if let Some(fd) = session.poll_fd() {
279                            match self.poller.add_session(fd, session_id) {
280                                Ok(()) => {
281                                    self.sessions.insert(session_id, session);
282                                    self.session_id_counter += 1;
283                                    writer.write_response(OpenResp::ok(session_id))
284                                }
285                                Err(e) => {
286                                    log::error!(
287                                        "failed to register poll FD for new session: {}",
288                                        e
289                                    );
290                                    self.device.close_session(session);
291                                    writer.write_err_response(e)
292                                }
293                            }
294                        } else {
295                            self.sessions.insert(session_id, session);
296                            self.session_id_counter += 1;
297                            writer.write_response(OpenResp::ok(session_id))
298                        }
299                    }
300                    Err(e) => writer.write_err_response(e),
301                }
302                .context("while writing response for OPEN command")
303            }
304            .context("while writing response for OPEN command"),
305            VIRTIO_MEDIA_CMD_CLOSE => reader
306                .read_obj()
307                .context("while reading CLOSE command")
308                .map(|CloseCmd { session_id, .. }| {
309                    if let Some(session) = self.sessions.remove(&session_id) {
310                        if let Some(fd) = session.poll_fd() {
311                            self.poller.remove_session(fd);
312                        }
313                        self.device.close_session(session);
314                    }
315                }),
316            VIRTIO_MEDIA_CMD_IOCTL => reader
317                .read_obj()
318                .context("while reading IOCTL command")
319                .and_then(|IoctlCmd { session_id, code }| {
320                    match self.sessions.get_mut(&session_id) {
321                        Some(session) => match V4l2Ioctl::n(code) {
322                            Some(ioctl) => self.device.do_ioctl(session, ioctl, reader, writer),
323                            None => {
324                                error!("unknown ioctl code {}", code);
325                                writer.write_err_response(libc::ENOTTY)
326                            }
327                        },
328                        None => writer.write_err_response(libc::EINVAL),
329                    }
330                    .context("while writing response for IOCTL command")
331                }),
332            VIRTIO_MEDIA_CMD_MMAP => reader
333                .read_obj()
334                .context("while reading MMAP command")
335                .and_then(
336                    |MmapCmd {
337                         session_id,
338                         flags,
339                         offset,
340                     }| {
341                        match self
342                            .sessions
343                            .get_mut(&session_id)
344                            .ok_or(libc::EINVAL)
345                            .and_then(|session| self.device.do_mmap(session, flags, offset))
346                        {
347                            Ok((guest_addr, size)) => {
348                                writer.write_response(MmapResp::ok(guest_addr, size))
349                            }
350                            Err(e) => writer.write_err_response(e),
351                        }
352                        .context("while writing response for MMAP command")
353                    },
354                ),
355            VIRTIO_MEDIA_CMD_MUNMAP => reader
356                .read_obj()
357                .context("while reading UNMMAP command")
358                .and_then(
359                    |MunmapCmd {
360                         driver_addr: guest_addr,
361                     }| {
362                        match self.device.do_munmap(guest_addr) {
363                            Ok(()) => writer.write_response(MunmapResp::ok()),
364                            Err(e) => writer.write_err_response(e),
365                        }
366                        .context("while writing response for MUNMAP command")
367                    },
368                ),
369            _ => writer
370                .write_err_response(libc::ENOTTY)
371                .context("while writing error response for invalid command"),
372        };
373
374        if let Err(e) = res {
375            error!("error while processing command: {:#}", e);
376            let _ = writer.write_err_response(libc::EINVAL);
377        }
378    }
379
380    /// Returns the device this runner has been created from.
381    pub fn into_device(self) -> Device {
382        self.device
383    }
384}