Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion alioth/src/device/ioapic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,4 @@ impl<M: MsiSender> MmioDev for IoApic<M> {}

#[cfg(test)]
#[path = "ioapic_test.rs"]
mod tests;
pub mod tests;
60 changes: 23 additions & 37 deletions alioth/src/device/ioapic_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use std::sync::Arc;

use assert_matches::assert_matches;
use parking_lot::Mutex;

use crate::hv::tests::TestIrqFd;
use crate::hv::{Error as HvError, MsiSender};
use crate::arch::x86_64::ioapic::{IOREGSEL, IOWIN, RedirectEntry};
use crate::hv::MsiSender;
use crate::hv::tests::TestMsiSender;
use crate::mem::emulated::Mmio;

use super::{IOREGSEL, IOWIN, IoApic};

#[derive(Debug, Default)]
struct TestMsiSender {
messages: Arc<Mutex<Vec<(u64, u32)>>>,
}

impl MsiSender for TestMsiSender {
type IrqFd = TestIrqFd;

fn send(&self, addr: u64, data: u32) -> Result<(), HvError> {
self.messages.lock().push((addr, data));
Ok(())
}

fn create_irqfd(&self) -> Result<Self::IrqFd, HvError> {
Ok(TestIrqFd::default())
}
}
use super::IoApic;

#[test]
fn test_ioapic_read_write() {
Expand Down Expand Up @@ -69,25 +49,31 @@ fn test_ioapic_read_write() {
assert_eq!(regs.redirtbl[0].0, 0xabcdef0012345678);
}

/// Configure redirection table entry for pin `pin` with `vector` and `dest`.
/// physical, edge triggered.
pub(crate) fn enable_pin<M: MsiSender>(io_apci: &IoApic<M>, pin: u8, vector: u8, dest: u8) {
let mut redirtbl_entry = RedirectEntry(0);
redirtbl_entry.set_vector(vector);
redirtbl_entry.set_dest_id(dest);
// IOREDTBL for pin 4 is at registers 0x10 + pin * 2
let offset = 0x10 + (pin as u64 * 2);
io_apci.write(IOREGSEL, 4, offset).unwrap();
io_apci
.write(IOWIN, 4, (redirtbl_entry.0 & 0xffffffff) as u64)
.unwrap();
io_apci.write(IOREGSEL, 4, offset + 1).unwrap();
io_apci
.write(IOWIN, 4, (redirtbl_entry.0 >> 32) as u64)
.unwrap();
}

#[test]
fn test_ioapic_service_pin() {
let msi_sender = TestMsiSender::default();
let messages = msi_sender.messages.clone();
let io_apic = IoApic::new(msi_sender);

// Configure redirection table entry for pin 4
// Vector 0x24, destination 2, physical, edge triggered
let redirtbl_entry = (2u64 << 56) | 0x24;

// IOREDTBL for pin 4 is at registers 0x10 + 4*2 = 0x18 and 0x19
io_apic.write(IOREGSEL, 4, 0x18).unwrap();
io_apic
.write(IOWIN, 4, (redirtbl_entry & 0xFFFFFFFF) as u64)
.unwrap();
io_apic.write(IOREGSEL, 4, 0x19).unwrap();
io_apic
.write(IOWIN, 4, (redirtbl_entry >> 32) as u64)
.unwrap();
enable_pin(&io_apic, 4, 0x24, 2);

// Service pin 4
io_apic.service_pin(4).unwrap();
Expand Down
32 changes: 18 additions & 14 deletions alioth/src/device/serial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,18 @@ use crate::hv::MsiSender;
use crate::mem::emulated::{Action, Mmio};
use crate::{bitflags, mem};

const TX_HOLDING_REGISTER: u16 = 0x0;
const RX_BUFFER_REGISTER: u16 = 0x0;
const DIVISOR_LATCH_LSB: u16 = 0x0;
const DIVISOR_LATCH_MSB: u16 = 0x1;
const INTERRUPT_ENABLE_REGISTER: u16 = 0x1;
const FIFO_CONTROL_REGISTER: u16 = 0x2;
const INTERRUPT_IDENTIFICATION_REGISTER: u16 = 0x2;
const LINE_CONTROL_REGISTER: u16 = 0x3;
const MODEM_CONTROL_REGISTER: u16 = 0x4;
const LINE_STATUS_REGISTER: u16 = 0x5;
const MODEM_STATUS_REGISTER: u16 = 0x6;
const SCRATCH_REGISTER: u16 = 0x7;
const TX_HOLDING_REGISTER: u64 = 0x0;
const RX_BUFFER_REGISTER: u64 = 0x0;
const DIVISOR_LATCH_LSB: u64 = 0x0;
const DIVISOR_LATCH_MSB: u64 = 0x1;
const INTERRUPT_ENABLE_REGISTER: u64 = 0x1;
const FIFO_CONTROL_REGISTER: u64 = 0x2;
const INTERRUPT_IDENTIFICATION_REGISTER: u64 = 0x2;
const LINE_CONTROL_REGISTER: u64 = 0x3;
const MODEM_CONTROL_REGISTER: u64 = 0x4;
const LINE_STATUS_REGISTER: u64 = 0x5;
const MODEM_STATUS_REGISTER: u64 = 0x6;
const SCRATCH_REGISTER: u64 = 0x7;

// offset 0x1, Interrupt Enable Register (IER)
bitflags! {
Expand Down Expand Up @@ -203,7 +203,7 @@ where

fn read(&self, offset: u64, _size: u8) -> Result<u64, mem::Error> {
let mut reg = self.reg.lock();
let ret = match offset as u16 {
let ret = match offset {
DIVISOR_LATCH_LSB if reg.line_control.divisor_latch_access() => reg.divisor as u8,
DIVISOR_LATCH_MSB if reg.line_control.divisor_latch_access() => {
(reg.divisor >> 8) as u8
Expand Down Expand Up @@ -236,7 +236,7 @@ where
fn write(&self, offset: u64, _size: u8, val: u64) -> mem::Result<Action> {
let byte = val as u8;
let mut reg = self.reg.lock();
match offset as u16 {
match offset {
DIVISOR_LATCH_LSB if reg.line_control.divisor_latch_access() => {
reg.divisor = (reg.divisor & 0xff00) | byte as u16;
}
Expand Down Expand Up @@ -365,3 +365,7 @@ where
}
}
}

#[cfg(test)]
#[path = "serial_test.rs"]
mod tests;
234 changes: 234 additions & 0 deletions alioth/src/device/serial_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::sync::Arc;
use std::thread::sleep;
use std::time::{Duration, Instant};

use assert_matches::assert_matches;
use parking_lot::Mutex;

use crate::device::console::tests::TestConsole;
use crate::device::ioapic::IoApic;
use crate::device::ioapic::tests::enable_pin;
use crate::device::serial::{
DIVISOR_LATCH_LSB, DIVISOR_LATCH_MSB, FIFO_CONTROL_REGISTER, INTERRUPT_ENABLE_REGISTER,
INTERRUPT_IDENTIFICATION_REGISTER, LINE_CONTROL_REGISTER, LINE_STATUS_REGISTER,
MODEM_CONTROL_REGISTER, MODEM_STATUS_REGISTER, RX_BUFFER_REGISTER, SCRATCH_REGISTER, Serial,
TX_HOLDING_REGISTER,
};
use crate::hv::tests::TestMsiSender;
use crate::mem::emulated::Mmio;

fn fixture_serial() -> (
Serial<TestMsiSender, TestConsole>,
Arc<IoApic<TestMsiSender>>,
Arc<Mutex<Vec<(u64, u32)>>>,
) {
let msi_sender = TestMsiSender::default();
let messages = msi_sender.messages.clone();
let ioapic = Arc::new(IoApic::new(msi_sender));
let console = TestConsole::new().unwrap();
let serial = Serial::new(0x3f8, ioapic.clone(), 4, console).unwrap();
(serial, ioapic, messages)
}

#[test]
fn test_serial_basic() {
let (serial, _, _) = fixture_serial();

assert_eq!(serial.size(), 8);

// Default LCR should be 0b00000011 (8 data bits)
assert_matches!(serial.read(LINE_CONTROL_REGISTER, 1), Ok(0x03));

// Write LCR to enable DLAB (Divisor Latch Access Bit)
assert_matches!(serial.write(LINE_CONTROL_REGISTER, 1, 0x83), Ok(_));
assert_matches!(serial.read(LINE_CONTROL_REGISTER, 1), Ok(0x83));

// Write divisor latches
assert_matches!(serial.write(DIVISOR_LATCH_LSB as u64, 1, 0x12), Ok(_));
assert_matches!(serial.write(DIVISOR_LATCH_MSB as u64, 1, 0x34), Ok(_));

// Read divisor latches
assert_matches!(serial.read(DIVISOR_LATCH_LSB as u64, 1), Ok(0x12));
assert_matches!(serial.read(DIVISOR_LATCH_MSB as u64, 1), Ok(0x34));

// Disable DLAB
assert_matches!(serial.write(LINE_CONTROL_REGISTER, 1, 0x03), Ok(_));

// Scratch register
assert_matches!(serial.write(SCRATCH_REGISTER, 1, 0x5a), Ok(_));
assert_matches!(serial.read(SCRATCH_REGISTER, 1), Ok(0x5a));

// Default IIR
assert_matches!(serial.read(INTERRUPT_IDENTIFICATION_REGISTER, 1), Ok(0x01));

// Modem Control Register
assert_matches!(serial.write(MODEM_CONTROL_REGISTER, 1, 0x1f), Ok(_));
assert_matches!(serial.read(MODEM_CONTROL_REGISTER, 1), Ok(0x1f));

// Modem Status Register (read-only in real hardware, but we just check it returns 0 as it's uninitialized default)
assert_matches!(serial.read(MODEM_STATUS_REGISTER, 1), Ok(0x00));
// Writing should be a no-op but shouldn't panic
assert_matches!(serial.write(MODEM_STATUS_REGISTER, 1, 0xff), Ok(_));

// FIFO Control Register (write-only)
assert_matches!(serial.write(FIFO_CONTROL_REGISTER, 1, 0xc7), Ok(_));

// Unreachable offsets
assert_matches!(serial.read(0x100, 1), Ok(0x00));
assert_matches!(serial.write(0x100, 1, 0x00), Ok(_));
}

#[test]
fn test_serial_tx() {
let (serial, ioapic, messages) = fixture_serial();

// Enable TX empty interrupt
assert_matches!(serial.write(INTERRUPT_ENABLE_REGISTER, 1, 0x02), Ok(_));

enable_pin(&ioapic, 4, 0x24, 2);

// Write a character
assert_matches!(serial.write(TX_HOLDING_REGISTER, 1, b'A' as u64), Ok(_));

// Check if character is pushed to outbound console
let mut outbound = serial.console.outbound.lock();
assert_eq!(outbound.pop_front(), Some(b'A'));
drop(outbound);

// TX should send an IRQ through IOAPIC
let messages_lock = messages.lock();
assert_matches!(messages_lock.as_slice(), [(0xfee02000, 0x24)]);
}

#[test]
fn test_serial_rx() {
let (serial, ioapic, messages) = fixture_serial();

// Enable RX available interrupt
assert_matches!(serial.write(INTERRUPT_ENABLE_REGISTER, 1, 0x01), Ok(_));
assert_matches!(serial.read(INTERRUPT_ENABLE_REGISTER, 4), Ok(0x01));

enable_pin(&ioapic, 4, 0x24, 2);

{
serial.console.inbound.lock().push_back(b'B');
serial.console.notifier.lock().notify().unwrap();
}

let now = Instant::now();
while !matches!(serial.read(LINE_STATUS_REGISTER, 1), Ok(s) if s & 1 == 1)
&& now.elapsed() < Duration::from_secs(5)
{
sleep(Duration::from_millis(100));
}

// Check if data is available
assert_matches!(
serial.read(LINE_STATUS_REGISTER, 1),
Ok(s) if s & 1 == 1
);

// Check IIR for RX data available
assert_matches!(serial.read(INTERRUPT_IDENTIFICATION_REGISTER, 1), Ok(0x04));

// Read the character
assert_matches!(
serial.read(RX_BUFFER_REGISTER, 1),
Ok(b) if b == b'B' as u64
);

// RX should send an IRQ through IOAPIC
let messages_lock = messages.lock();
assert_eq!(messages_lock.len(), 1);
assert_matches!(messages_lock.as_slice(), [(0xfee02000, 0x24)]);

// IIR should be cleared after read
assert_matches!(serial.read(INTERRUPT_IDENTIFICATION_REGISTER, 1), Ok(0x01));
}

#[test]
fn test_serial_rx_no_interrupt() {
let (serial, _ioapic, messages) = fixture_serial();

// Disable all interrupts
assert_matches!(serial.write(INTERRUPT_ENABLE_REGISTER, 1, 0x00), Ok(_));

{
serial.console.inbound.lock().push_back(b'B');
serial.console.notifier.lock().notify().unwrap();
}

let now = Instant::now();
while !matches!(serial.read(LINE_STATUS_REGISTER, 1), Ok(s) if s & 1 == 1)
&& now.elapsed() < Duration::from_secs(5)
{
sleep(Duration::from_millis(100));
}

// Check if data is available
assert_matches!(
serial.read(LINE_STATUS_REGISTER, 1),
Ok(s) if s & 1 == 1
);

// Read the character
assert_matches!(
serial.read(RX_BUFFER_REGISTER, 1),
Ok(b) if b == b'B' as u64
);

// No IRQ should have been sent
let messages_lock = messages.lock();
assert_eq!(messages_lock.len(), 0);
}

#[test]
fn test_serial_loopback() {
let (serial, ioapic, messages) = fixture_serial();

// Enable RX available interrupt
assert_matches!(serial.write(INTERRUPT_ENABLE_REGISTER, 1, 0x01), Ok(_));

enable_pin(&ioapic, 4, 0x24, 2);

// Enable loopback mode (bit 4)
assert_matches!(serial.write(MODEM_CONTROL_REGISTER, 1, 0x10), Ok(_));

// Write a character
assert_matches!(serial.write(TX_HOLDING_REGISTER, 1, b'C' as u64), Ok(_));

// The character should be looped back into RX
assert_matches!(
serial.read(LINE_STATUS_REGISTER, 1),
Ok(s) if s & 1 == 1
);
assert_matches!(
serial.read(RX_BUFFER_REGISTER, 1),
Ok(b) if b == b'C' as u64
);

let messages_lock = messages.lock();
assert_matches!(messages_lock.as_slice(), [(0xfee02000, 0x24)]);
}

#[test]
fn test_serial_pause_resume() {
use crate::device::Pause;
let (serial, _, _) = fixture_serial();
assert_matches!(serial.pause(), Ok(()));
assert_matches!(serial.resume(), Ok(()));
}
Loading