Skip to main content

squib_virtio/devices/
console.rs

1//! virtio-console — serial frontend.
2//!
3//! Per [14-virtio-and-devices.md §
4//! 4.6](../../../specs/14-virtio-and-devices.md#46-virtio-console-serial): the UART backend writes
5//! to a regular file, a FIFO (`mkfifo` works on macOS), or `stdout` per the `/serial` PUT config.
6//! Same shape as Firecracker.
7//!
8//! Only the basic two-port flavour is implemented for 1.0 — receive and
9//! transmit queues for port 0. The multi-port extension behind
10//! `VIRTIO_CONSOLE_F_MULTIPORT` is documented as future work.
11
12use std::{io::Write, sync::Arc};
13
14use parking_lot::Mutex;
15use squib_core::GuestMemory;
16
17use crate::{
18    device::{ActivateError, VirtioDevice},
19    device_id::VirtioDeviceType,
20    interrupt::IrqLine,
21    queue::Queue,
22};
23
24/// `VIRTIO_CONSOLE_F_SIZE` — driver may read terminal columns/rows from
25/// config-space.
26pub const F_SIZE: u64 = 1 << 0;
27/// `VIRTIO_CONSOLE_F_MULTIPORT` — multi-port mode (not implemented in 1.0).
28pub const F_MULTIPORT: u64 = 1 << 1;
29
30/// Receive-queue index (host → guest).
31pub const RX_QUEUE: usize = 0;
32/// Transmit-queue index (guest → host).
33pub const TX_QUEUE: usize = 1;
34
35const QUEUE_MAX_SIZE: u16 = 64;
36
37/// Where the console output goes.
38pub enum ConsoleSink {
39    /// Discard all output.
40    Discard,
41    /// Append to a host-side `Write` (`stdout`, file, FIFO).
42    Writer(Box<dyn Write + Send>),
43}
44
45impl std::fmt::Debug for ConsoleSink {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        match self {
48            Self::Discard => f.write_str("ConsoleSink::Discard"),
49            Self::Writer(_) => f.write_str("ConsoleSink::Writer(<dyn Write + Send>)"),
50        }
51    }
52}
53
54/// virtio-console device.
55#[derive(Debug)]
56pub struct ConsoleDevice {
57    avail: u64,
58    acked: u64,
59    queues: Vec<Queue>,
60    sink: Arc<Mutex<ConsoleSink>>,
61    state: Arc<Mutex<ActiveState>>,
62}
63
64#[derive(Debug, Default)]
65struct ActiveState {
66    mem: Option<Arc<dyn GuestMemory>>,
67    irq: Option<IrqLine>,
68    activated: bool,
69}
70
71impl ConsoleDevice {
72    /// Build a console with a custom sink.
73    #[must_use]
74    pub fn new(sink: ConsoleSink) -> Self {
75        Self {
76            avail: 0,
77            acked: 0,
78            queues: vec![Queue::new(QUEUE_MAX_SIZE), Queue::new(QUEUE_MAX_SIZE)],
79            sink: Arc::new(Mutex::new(sink)),
80            state: Arc::new(Mutex::new(ActiveState::default())),
81        }
82    }
83
84    /// Build a console that discards output. Useful for tests and headless
85    /// boots.
86    #[must_use]
87    pub fn discard() -> Self {
88        Self::new(ConsoleSink::Discard)
89    }
90
91    fn drain_tx(&mut self) {
92        let (mem, irq) = {
93            let state = self.state.lock();
94            match (state.mem.clone(), state.irq.clone()) {
95                (Some(m), Some(i)) => (m, i),
96                _ => return,
97            }
98        };
99        let queue = &mut self.queues[TX_QUEUE];
100        let mut completed = false;
101        loop {
102            let chain = match queue.pop_avail(mem.as_ref()) {
103                Ok(Some(c)) => c,
104                Ok(None) => break,
105                Err(err) => {
106                    tracing::warn!(error = %err, "console: tx walk failed");
107                    break;
108                }
109            };
110            let head = chain.head_index();
111            let descs = match chain.collect(mem.as_ref()) {
112                Ok(d) => d,
113                Err(err) => {
114                    tracing::warn!(error = %err, "console: tx collect failed");
115                    break;
116                }
117            };
118            let mut written: u32 = 0;
119            for desc in descs {
120                if desc.is_write_only() {
121                    continue;
122                }
123                let len = desc.len as usize;
124                if len == 0 {
125                    continue;
126                }
127                let mut buf = vec![0u8; len];
128                if let Err(err) = mem.read(desc.addr, &mut buf) {
129                    tracing::warn!(error = %err, "console: tx read from guest failed");
130                    continue;
131                }
132                let mut sink = self.sink.lock();
133                match &mut *sink {
134                    ConsoleSink::Discard => {}
135                    ConsoleSink::Writer(w) => {
136                        if let Err(err) = w.write_all(&buf) {
137                            tracing::warn!(error = %err, "console: tx host write failed");
138                        }
139                    }
140                }
141                written = written.saturating_add(desc.len);
142            }
143            if let Err(err) = queue.push_used(mem.as_ref(), head, written) {
144                tracing::warn!(error = %err, "console: push_used failed");
145                break;
146            }
147            completed = true;
148        }
149        if completed {
150            let _ = irq.trigger_queue();
151        }
152    }
153}
154
155impl VirtioDevice for ConsoleDevice {
156    fn device_type(&self) -> VirtioDeviceType {
157        VirtioDeviceType::Console
158    }
159    fn avail_features(&self) -> u64 {
160        self.avail
161    }
162    fn acked_features(&self) -> u64 {
163        self.acked
164    }
165    fn set_acked_features(&mut self, value: u64) {
166        self.acked = value;
167    }
168    fn queue_max_sizes(&self) -> &[u16] {
169        const SIZES: &[u16] = &[QUEUE_MAX_SIZE, QUEUE_MAX_SIZE];
170        SIZES
171    }
172    fn queues(&self) -> &[Queue] {
173        &self.queues
174    }
175    fn queues_mut(&mut self) -> &mut [Queue] {
176        &mut self.queues
177    }
178    fn read_config(&self, _offset: u64, data: &mut [u8]) {
179        for b in data.iter_mut() {
180            *b = 0;
181        }
182    }
183    fn write_config(&mut self, _offset: u64, _data: &[u8]) {}
184    fn activate(&mut self, mem: Arc<dyn GuestMemory>, irq: IrqLine) -> Result<(), ActivateError> {
185        let mut state = self.state.lock();
186        state.mem = Some(mem);
187        state.irq = Some(irq);
188        state.activated = true;
189        Ok(())
190    }
191    fn is_activated(&self) -> bool {
192        self.state.lock().activated
193    }
194    fn process_queue(&mut self, queue_index: u16) {
195        if queue_index as usize == TX_QUEUE {
196            self.drain_tx();
197        }
198        // RX queue: host-side input wakes it up; that path lives in the VMM
199        // event loop, not here. The driver may still post empty descriptors
200        // to RX which we leave parked until input arrives.
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use std::io::Cursor;
207
208    use squib_arch::IntId;
209    use squib_core::{GuestAddress, SliceGuestMemory};
210    use squib_gic::Gic;
211
212    use super::*;
213
214    /// Sink wrapper that exposes the captured bytes for assertions.
215    #[derive(Debug, Default)]
216    struct CapturedSink(Arc<Mutex<Vec<u8>>>);
217    impl Write for CapturedSink {
218        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
219            self.0.lock().extend_from_slice(buf);
220            Ok(buf.len())
221        }
222        fn flush(&mut self) -> std::io::Result<()> {
223            Ok(())
224        }
225    }
226
227    #[derive(Debug, Default)]
228    struct StubGic;
229    impl Gic for StubGic {
230        fn pulse_spi(&self, _: IntId) -> Result<(), squib_gic::GicError> {
231            Ok(())
232        }
233        fn set_spi_level(&self, _: IntId, _: bool) -> Result<(), squib_gic::GicError> {
234            Ok(())
235        }
236        fn save_state(&self) -> Result<Vec<u8>, squib_gic::GicError> {
237            Ok(Vec::new())
238        }
239        fn restore_state(&self, _data: &[u8]) -> Result<(), squib_gic::GicError> {
240            Ok(())
241        }
242    }
243
244    fn line() -> IrqLine {
245        let gic: Arc<dyn Gic + Send + Sync> = Arc::new(StubGic);
246        IrqLine::new(gic, IntId::from_spi_cell(16).unwrap())
247    }
248
249    #[test]
250    fn test_should_offer_two_queues_one_each_direction() {
251        let dev = ConsoleDevice::discard();
252        assert_eq!(dev.queue_max_sizes().len(), 2);
253    }
254
255    #[test]
256    fn test_should_forward_tx_descriptors_to_host_sink() {
257        let buf = Arc::new(Mutex::new(Vec::new()));
258        let sink = CapturedSink(buf.clone());
259        let mut dev = ConsoleDevice::new(ConsoleSink::Writer(Box::new(sink)));
260        let mem = Arc::new(SliceGuestMemory::new(GuestAddress(0x4000_0000), 0x4000));
261        let q = &mut dev.queues_mut()[TX_QUEUE];
262        q.size = 8;
263        q.desc_table_addr = GuestAddress(0x4000_0000);
264        q.avail_ring_addr = GuestAddress(0x4000_0800);
265        q.used_ring_addr = GuestAddress(0x4000_1000);
266        q.ready = true;
267        // Buffer "hi\n" at 0x4000_2000.
268        mem.write(GuestAddress(0x4000_2000), b"hi\n").unwrap();
269        let base = 0x4000_0000u64;
270        mem.write_u32_le(GuestAddress(base), 0x4000_2000).unwrap();
271        mem.write_u32_le(GuestAddress(base + 4), 0).unwrap();
272        mem.write_u32_le(GuestAddress(base + 8), 3).unwrap();
273        mem.write_u16_le(GuestAddress(base + 12), 0).unwrap();
274        mem.write_u16_le(GuestAddress(base + 14), 0).unwrap();
275        mem.write_u16_le(GuestAddress(0x4000_0804), 0).unwrap();
276        mem.write_u16_le(GuestAddress(0x4000_0802), 1).unwrap();
277        dev.activate(mem.clone(), line()).unwrap();
278        dev.process_queue(TX_QUEUE as u16);
279        assert_eq!(buf.lock().as_slice(), b"hi\n");
280    }
281
282    #[test]
283    fn test_discard_sink_is_a_noop() {
284        let mut dev = ConsoleDevice::discard();
285        let mem = Arc::new(SliceGuestMemory::new(GuestAddress(0x4000_0000), 0x4000));
286        dev.activate(mem.clone(), line()).unwrap();
287        dev.process_queue(TX_QUEUE as u16); // no panic with empty queue setup.
288    }
289
290    /// Keep imports referenced.
291    #[test]
292    fn test_constants_referenced() {
293        let _ = F_SIZE;
294        let _ = F_MULTIPORT;
295        let _ = Cursor::new(Vec::<u8>::new());
296    }
297}