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}