supermachine 0.7.70

Run any OCI/Docker image as a hardware-isolated microVM on macOS HVF (Linux KVM and Windows WHP in progress). Single library API, zero flags for the common case, sub-100 ms cold-restore from snapshot.
Documentation
//! 16550A UART (COM1) — the x86 serial console, driven by `VcpuExit::Io` on
//! ports `0x3f8..=0x3ff`.
//!
//! This is the x86 counterpart to `devices::serial` (the aarch64 PL011, which
//! is MMIO): same role (the kernel's `console=ttyS0`), different register map
//! and access path (port I/O, not MMIO). Like the rest of the device plane it
//! is pure logic — no host I/O. The run loop reads transmitted bytes out of
//! [`Com1::write`]'s return, feeds host→guest bytes via [`Com1::push_rx`], and
//! mirrors [`Com1::irq_line`] onto the COM1 interrupt line (IRQ 4).
//!
//! Productionized from the validated `spikes/kvm-boot` Com1 (which proved the
//! kernel TX path on real /dev/kvm), extended with a level-correct interrupt
//! model and a receive path (RX data + RX interrupt) for an interactive shell.
//!
//! Register map (offset from the 0x3f8 base; DLAB in LCR bit 7 switches 0/1):
//! ```text
//!   0  THR (w) / RBR (r)   transmit / receive          DLL when DLAB
//!   1  IER                 interrupt enable            DLM when DLAB
//!   2  IIR (r) / FCR (w)   interrupt id / FIFO control
//!   3  LCR                 line control (DLAB is bit 7)
//!   4  MCR                 modem control (LOOP is bit 4)
//!   5  LSR (r)             line status (DR, THRE, TEMT)
//!   6  MSR (r)             modem status
//!   7  SCR                 scratch
//! ```

use std::collections::VecDeque;

/// COM1 port base (`ttyS0`).
pub const COM1_BASE: u16 = 0x3f8;
/// COM1 interrupt line on the legacy 8259/IOAPIC.
pub const COM1_IRQ: u32 = 4;

// IER (interrupt-enable) bits.
const IER_ERBFI: u8 = 0x01; // received-data-available interrupt
const IER_ETBEI: u8 = 0x02; // transmit-holding-register-empty interrupt

// IIR (interrupt-identification) values, highest priority first.
const IIR_NONE: u8 = 0x01; // bit0=1 → no interrupt pending
const IIR_THRE: u8 = 0x02; // transmit holding register empty
const IIR_RDA: u8 = 0x04; // received data available

// LSR (line-status) bits.
const LSR_DR: u8 = 0x01; // data ready (RX byte available)
const LSR_THRE: u8 = 0x20; // transmit holding register empty
const LSR_TEMT: u8 = 0x40; // transmitter empty

// LCR / MCR bits.
const LCR_DLAB: u8 = 0x80; // divisor-latch access
const MCR_LOOP: u8 = 0x10; // local loopback (used by the 8250 probe)

/// A minimal but spec-faithful 16550A. Transmit is instantaneous (bytes are
/// returned to the caller immediately), so the transmitter is always empty.
#[derive(Default)]
pub struct Com1 {
    ier: u8,
    lcr: u8,
    mcr: u8,
    scr: u8,
    dll: u8,
    dlm: u8,
    /// Host→guest bytes waiting to be read from RBR.
    rx: VecDeque<u8>,
    /// THRE interrupt source: latched when the transmitter goes empty (i.e.
    /// after every THR write, since we transmit instantly) while the THRE
    /// interrupt is enabled; cleared when the guest reads IIR and sees it.
    thre_pending: bool,
}

/// Snapshot of a [`Com1`]'s programmable register state (for VM snapshot). The
/// host→guest RX queue is intentionally excluded: it's transient host input,
/// not guest-visible state to preserve across a snapshot.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Com1State {
    pub ier: u8,
    pub lcr: u8,
    pub mcr: u8,
    pub scr: u8,
    pub dll: u8,
    pub dlm: u8,
}

impl Com1 {
    pub fn new() -> Self {
        Self::default()
    }

    /// Capture the register state for a VM snapshot.
    pub fn snapshot(&self) -> Com1State {
        Com1State {
            ier: self.ier,
            lcr: self.lcr,
            mcr: self.mcr,
            scr: self.scr,
            dll: self.dll,
            dlm: self.dlm,
        }
    }

    /// Restore register state captured by [`Com1::snapshot`].
    pub fn restore(&mut self, s: &Com1State) {
        self.ier = s.ier;
        self.lcr = s.lcr;
        self.mcr = s.mcr;
        self.scr = s.scr;
        self.dll = s.dll;
        self.dlm = s.dlm;
    }

    fn dlab(&self) -> bool {
        self.lcr & LCR_DLAB != 0
    }

    /// Queue a host→guest byte (e.g. a keystroke). Sets DR and, if the RX
    /// interrupt is enabled, raises the COM1 interrupt (observe via
    /// [`Com1::irq_line`]). In loopback mode the guest's own transmitted bytes
    /// arrive here instead of being emitted.
    pub fn push_rx(&mut self, byte: u8) {
        self.rx.push_back(byte);
    }

    /// Write a UART register. `port` is the absolute I/O port (0x3f8..=0x3ff).
    /// Returns `Some(byte)` when the write transmits a data byte (THR write,
    /// not in loopback) that the caller should emit to the console.
    pub fn write(&mut self, port: u16, v: u8) -> Option<u8> {
        match port - COM1_BASE {
            0 if self.dlab() => self.dll = v, // divisor latch low
            0 => {
                // THR: transmit. In loopback the byte feeds our own RX.
                if self.mcr & MCR_LOOP != 0 {
                    self.rx.push_back(v);
                } else {
                    // Transmit completes instantly → THR empty → if TX
                    // interrupt enabled, a THRE interrupt becomes pending.
                    if self.ier & IER_ETBEI != 0 {
                        self.thre_pending = true;
                    }
                    return Some(v);
                }
            }
            1 if self.dlab() => self.dlm = v, // divisor latch high
            1 => {
                self.ier = v;
                // Enabling the TX interrupt while THR is empty (always) latches
                // a THRE interrupt so the kernel's interrupt-mode tty starts.
                if v & IER_ETBEI != 0 {
                    self.thre_pending = true;
                }
            }
            2 => {} // FCR (FIFO control) — accept and ignore (we model no FIFO depth)
            3 => self.lcr = v,
            4 => self.mcr = v,
            5 | 6 => {} // LSR/MSR are read-only
            7 => self.scr = v,
            _ => {}
        }
        None
    }

    /// Read a UART register. `port` is the absolute I/O port.
    pub fn read(&mut self, port: u16) -> u8 {
        match port - COM1_BASE {
            0 if self.dlab() => self.dll,
            0 => self.rx.pop_front().unwrap_or(0), // RBR
            1 if self.dlab() => self.dlm,
            1 => self.ier,
            2 => self.iir(), // reading IIR clears a pending THRE
            3 => self.lcr,
            4 => self.mcr,
            5 => self.lsr(),
            6 => 0xb0, // MSR: DCD | DSR | CTS asserted
            7 => self.scr,
            _ => 0,
        }
    }

    /// Interrupt-identification register. Reports the highest-priority pending
    /// source; reading it clears a pending THRE (per the 16550 spec — the THRE
    /// interrupt is cleared by reading IIR or writing THR).
    fn iir(&mut self) -> u8 {
        if self.ier & IER_ERBFI != 0 && !self.rx.is_empty() {
            // RDA is cleared by reading RBR, not by reading IIR.
            IIR_RDA
        } else if self.ier & IER_ETBEI != 0 && self.thre_pending {
            self.thre_pending = false;
            IIR_THRE
        } else {
            IIR_NONE
        }
    }

    /// Line-status register: DR reflects pending RX; the transmitter is always
    /// empty (THRE | TEMT), so writes never block.
    fn lsr(&self) -> u8 {
        let mut s = LSR_THRE | LSR_TEMT;
        if !self.rx.is_empty() {
            s |= LSR_DR;
        }
        s
    }

    /// The level of the COM1 interrupt line: asserted while an enabled source
    /// is pending (RX data with ERBFI, or THR-empty with ETBEI). The run loop
    /// mirrors this onto the irqchip after each access.
    pub fn irq_line(&self) -> bool {
        (self.ier & IER_ERBFI != 0 && !self.rx.is_empty())
            || (self.ier & IER_ETBEI != 0 && self.thre_pending)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // Convenience: write/read by register offset.
    fn wr(c: &mut Com1, off: u16, v: u8) -> Option<u8> {
        c.write(COM1_BASE + off, v)
    }
    fn rd(c: &mut Com1, off: u16) -> u8 {
        c.read(COM1_BASE + off)
    }

    #[test]
    fn thr_write_transmits_byte() {
        let mut c = Com1::new();
        assert_eq!(wr(&mut c, 0, b'K'), Some(b'K'));
        assert_eq!(wr(&mut c, 0, b'\n'), Some(b'\n'));
    }

    #[test]
    fn dlab_routes_thr_ier_to_divisor_latches() {
        let mut c = Com1::new();
        wr(&mut c, 3, LCR_DLAB); // set DLAB
        assert_eq!(wr(&mut c, 0, 0x01), None); // DLL, not a transmit
        assert_eq!(wr(&mut c, 1, 0x00), None); // DLM, not IER
        assert_eq!(rd(&mut c, 0), 0x01); // reads back DLL
        assert_eq!(rd(&mut c, 1), 0x00); // reads back DLM
        wr(&mut c, 3, 0); // clear DLAB
        assert_eq!(wr(&mut c, 0, b'A'), Some(b'A')); // now a transmit again
    }

    #[test]
    fn lsr_reports_thre_always_and_dr_with_rx() {
        let mut c = Com1::new();
        assert_eq!(rd(&mut c, 5) & (LSR_THRE | LSR_TEMT), LSR_THRE | LSR_TEMT);
        assert_eq!(rd(&mut c, 5) & LSR_DR, 0);
        c.push_rx(b'x');
        assert_eq!(rd(&mut c, 5) & LSR_DR, LSR_DR);
    }

    #[test]
    fn rbr_pops_rx_in_order_and_clears_dr() {
        let mut c = Com1::new();
        c.push_rx(b'h');
        c.push_rx(b'i');
        assert_eq!(rd(&mut c, 0), b'h');
        assert_eq!(rd(&mut c, 0), b'i');
        assert_eq!(rd(&mut c, 5) & LSR_DR, 0); // DR clears when drained
        assert_eq!(rd(&mut c, 0), 0); // empty RBR reads 0
    }

    #[test]
    fn tx_interrupt_asserts_and_iir_reports_then_clears_thre() {
        let mut c = Com1::new();
        // No interrupt until enabled.
        assert!(!c.irq_line());
        wr(&mut c, 1, IER_ETBEI); // enable TX interrupt while THR empty
        assert!(c.irq_line(), "THRE int should latch when ETBEI enabled");
        assert_eq!(rd(&mut c, 2), IIR_THRE); // IIR reports THRE...
        assert!(!c.irq_line(), "...and reading IIR clears it");
        // Writing THR re-arms THRE.
        assert_eq!(wr(&mut c, 0, b'z'), Some(b'z'));
        assert!(c.irq_line());
        assert_eq!(rd(&mut c, 2), IIR_THRE);
        assert!(!c.irq_line());
    }

    #[test]
    fn rx_interrupt_asserts_while_data_and_enabled() {
        let mut c = Com1::new();
        wr(&mut c, 1, IER_ERBFI); // enable RX interrupt
        assert!(!c.irq_line()); // nothing queued yet
        c.push_rx(b'q');
        assert!(c.irq_line());
        assert_eq!(rd(&mut c, 2), IIR_RDA); // IIR shows RDA
        assert!(c.irq_line(), "RDA stays asserted until RBR is read");
        assert_eq!(rd(&mut c, 0), b'q'); // read the byte
        assert!(!c.irq_line()); // line drops when RX drains
    }

    #[test]
    fn rda_outranks_thre_in_iir() {
        let mut c = Com1::new();
        wr(&mut c, 1, IER_ERBFI | IER_ETBEI); // both enabled (THRE latches now)
        c.push_rx(b'r');
        assert_eq!(rd(&mut c, 2), IIR_RDA, "RX has priority over THRE");
    }

    #[test]
    fn loopback_feeds_tx_into_rx() {
        let mut c = Com1::new();
        wr(&mut c, 4, MCR_LOOP); // enable loopback
        assert_eq!(wr(&mut c, 0, b'L'), None, "loopback byte is not emitted");
        assert_eq!(rd(&mut c, 5) & LSR_DR, LSR_DR);
        assert_eq!(rd(&mut c, 0), b'L'); // it arrives on RX
    }

    #[test]
    fn scratch_register_round_trips() {
        let mut c = Com1::new();
        wr(&mut c, 7, 0xa5);
        assert_eq!(rd(&mut c, 7), 0xa5);
    }
}