use alloc::{boxed::Box, sync::Arc};
use axdevice::{
DeviceBuildContext, DeviceBundle, DeviceFactory, DeviceManagerError, DeviceManagerResult,
DeviceRegistration,
};
use axdevice_base::{
AccessWidth, BusAccess, BusKind, BusResponse, Device, DeviceAccess, DeviceError, DeviceResult,
Port, Resource,
};
use axvm_types::{EmulatedDeviceConfig, EmulatedDeviceType, PassThroughPortConfig};
use crate::{AxVmResult, ax_err};
pub(crate) struct HostPortPassthrough {
base: Port,
length: u16,
resources: Box<[Resource]>,
}
impl HostPortPassthrough {
pub(crate) fn new(base: u16, length: u16) -> AxVmResult<Self> {
if length == 0 {
return ax_err!(InvalidInput, "host port passthrough range is empty");
}
if base.checked_add(length - 1).is_none() {
return ax_err!(InvalidInput, "host port passthrough range overflows");
}
Ok(Self {
base: Port::new(base),
length,
resources: alloc::vec![Resource::PortRange { base, size: length }].into_boxed_slice(),
})
}
fn end(&self) -> Port {
Port::new(self.base.number() + self.length - 1)
}
fn contains(&self, port: Port) -> bool {
(self.base.number()..=self.end().number()).contains(&port.number())
}
fn read_port(&self, port: Port, width: AccessWidth) -> DeviceResult<usize> {
if !self.contains(port) {
return Err(DeviceError::OutOfRange {
addr: port.number() as u64,
});
}
match width {
AccessWidth::Byte => Ok(unsafe { inb(port.number()) } as usize),
AccessWidth::Word => Ok(unsafe { inw(port.number()) } as usize),
AccessWidth::Dword => Ok(unsafe { inl(port.number()) } as usize),
AccessWidth::Qword => Err(DeviceError::Unsupported {
operation: "read host I/O port",
detail: "x86 port I/O does not support 64-bit accesses".into(),
}),
}
}
fn write_port(&self, port: Port, width: AccessWidth, value: usize) -> DeviceResult {
if !self.contains(port) {
return Err(DeviceError::OutOfRange {
addr: port.number() as u64,
});
}
match width {
AccessWidth::Byte => unsafe { outb(port.number(), value as u8) },
AccessWidth::Word => unsafe { outw(port.number(), value as u16) },
AccessWidth::Dword => unsafe { outl(port.number(), value as u32) },
AccessWidth::Qword => {
return Err(DeviceError::Unsupported {
operation: "write host I/O port",
detail: "x86 port I/O does not support 64-bit accesses".into(),
});
}
}
Ok(())
}
}
pub(crate) struct HostPortPassthroughFactory {
config: PassThroughPortConfig,
}
impl HostPortPassthroughFactory {
pub(crate) const fn new(config: PassThroughPortConfig) -> Self {
Self { config }
}
pub(crate) fn build(&self) -> AxVmResult<DeviceBundle> {
let passthrough = Arc::new(HostPortPassthrough::new(
self.config.base,
self.config.length,
)?);
let device: Arc<dyn Device> = passthrough;
Ok(DeviceBundle::from_registration(DeviceRegistration::Device(
device,
)))
}
}
pub(crate) struct HostPortPassthroughDeviceFactory;
impl DeviceFactory for HostPortPassthroughDeviceFactory {
fn device_type(&self) -> EmulatedDeviceType {
EmulatedDeviceType::X86PortPassthrough
}
fn build(
&self,
config: &EmulatedDeviceConfig,
_context: &DeviceBuildContext<'_>,
) -> DeviceManagerResult<DeviceBundle> {
let base =
u16::try_from(config.base_gpa).map_err(|_| DeviceManagerError::InvalidConfig {
operation: "build host port passthrough",
detail: "base port does not fit in u16".into(),
})?;
let length =
u16::try_from(config.length).map_err(|_| DeviceManagerError::InvalidConfig {
operation: "build host port passthrough",
detail: "port range length does not fit in u16".into(),
})?;
HostPortPassthroughFactory::new(PassThroughPortConfig { base, length })
.build()
.map_err(|error| DeviceManagerError::InvalidConfig {
operation: "build host port passthrough",
detail: alloc::format!("{error}"),
})
}
}
impl Device for HostPortPassthrough {
fn name(&self) -> &str {
"x86-host-port-passthrough"
}
fn resources(&self) -> &[Resource] {
&self.resources
}
fn access(
&self,
access: &BusAccess,
_context: &mut dyn DeviceAccess,
) -> Result<BusResponse, DeviceError> {
if access.kind != BusKind::Port || access.addr > u16::MAX as u64 {
return Err(DeviceError::OutOfRange { addr: access.addr });
}
let port = Port::new(access.addr as u16);
if access.is_read {
self.read_port(port, access.width)
.map(|value| BusResponse::Read {
value: value as u64,
})
} else {
self.write_port(port, access.width, access.data as usize)
.map(|_| BusResponse::Write)
}
}
}
unsafe fn inb(port: u16) -> u8 {
let value: u8;
unsafe {
core::arch::asm!("in al, dx", in("dx") port, out("al") value, options(nomem, nostack));
}
value
}
unsafe fn inw(port: u16) -> u16 {
let value: u16;
unsafe {
core::arch::asm!("in ax, dx", in("dx") port, out("ax") value, options(nomem, nostack));
}
value
}
unsafe fn inl(port: u16) -> u32 {
let value: u32;
unsafe {
core::arch::asm!("in eax, dx", in("dx") port, out("eax") value, options(nomem, nostack));
}
value
}
unsafe fn outb(port: u16, value: u8) {
unsafe {
core::arch::asm!("out dx, al", in("dx") port, in("al") value, options(nomem, nostack));
}
}
unsafe fn outw(port: u16, value: u16) {
unsafe {
core::arch::asm!("out dx, ax", in("dx") port, in("ax") value, options(nomem, nostack));
}
}
unsafe fn outl(port: u16, value: u32) {
unsafe {
core::arch::asm!("out dx, eax", in("dx") port, in("eax") value, options(nomem, nostack));
}
}
#[cfg(test)]
mod tests {
use axdevice::DeviceRuntime;
use super::*;
#[test]
fn passthrough_port_range_is_inclusive() {
let dev = HostPortPassthrough::new(0x6000, 0x80).unwrap();
assert_eq!(
dev.resources(),
&[Resource::PortRange {
base: 0x6000,
size: 0x80
}]
);
assert_eq!(dev.end(), Port::new(0x607f));
}
#[test]
fn passthrough_port_range_rejects_empty_and_overflowing_ranges() {
assert!(HostPortPassthrough::new(0x6000, 0).is_err());
assert!(HostPortPassthrough::new(0xfff0, 0x20).is_err());
}
#[test]
fn passthrough_port_rejects_qword_without_touching_hardware() {
let dev = HostPortPassthrough::new(0x6000, 0x80).unwrap();
assert!(
dev.read_port(Port::new(0x6000), AccessWidth::Qword)
.is_err()
);
assert!(
dev.write_port(Port::new(0x6000), AccessWidth::Qword, 0)
.is_err()
);
}
#[test]
fn passthrough_port_bundle_registers_through_device_runtime() {
let bundle = HostPortPassthroughFactory::new(PassThroughPortConfig {
base: 0x6000,
length: 0x80,
})
.build()
.unwrap();
let mut devices = DeviceRuntime::default();
devices.register_bundle(bundle).unwrap();
assert!(devices.find_port_dev(Port::new(0x6000)).is_some());
assert!(devices.find_port_dev(Port::new(0x607f)).is_some());
assert!(devices.find_port_dev(Port::new(0x6080)).is_none());
}
}