Add runtime tools and Red Bear service wiring

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-14 10:50:42 +01:00
parent fd60edc823
commit 51f3c21121
62 changed files with 9613 additions and 881 deletions
+1
View File
@@ -26,6 +26,7 @@ target
wget-log
/sysroot/
local/docs/*.log
local/docs/qt6-build-log.txt
# Explicitly track our OWN source code (recipes with path="source" where we wrote the code)
# Only recipes under these categories contain our hand-written source:
Generated
+1 -1
View File
@@ -855,7 +855,7 @@ dependencies = [
]
[[package]]
name = "rbos_cookbook"
name = "redbear_cookbook"
version = "0.1.0"
dependencies = [
"ansi-to-tui",
+55
View File
@@ -13,8 +13,63 @@ filesystem_size = 10240
# Red Bear OS branding (os-release, hostname, motd)
redbear-release = {}
# Native Redox PCI/USB listing tools (lspci, lsusb)
redbear-hwutils = {}
# Redox-native netctl compatibility command
redbear-netctl = {}
# Terminal file manager (Midnight Commander port)
mc = {}
# Package builder (cub -S/-B/-G CLI)
cub = {}
[[files]]
path = "/etc/netctl"
data = ""
directory = true
mode = 0o755
[[files]]
path = "/etc/netctl/examples"
data = ""
directory = true
mode = 0o755
[[files]]
path = "/etc/netctl/examples/wired-dhcp"
data = """
Description='Red Bear wired DHCP profile'
Interface=eth0
Connection=ethernet
IP=dhcp
"""
[[files]]
path = "/etc/netctl/examples/wired-static"
data = """
Description='Red Bear wired static profile'
Interface=eth0
Connection=ethernet
IP=static
Address=('192.168.1.10/24')
Gateway='192.168.1.1'
DNS=('1.1.1.1')
"""
[[files]]
path = "/usr/lib/init.d/12_netctl.service"
data = """
[unit]
description = "Network profile application"
requires_weak = [
"10_smolnetd.service",
"10_dhcpd.service",
]
[service]
cmd = "netctl"
args = ["--boot"]
type = "oneshot"
"""
+229 -10
View File
@@ -1,22 +1,29 @@
# Red Bear OS Full Configuration
# Complete desktop + RBOS branding + ext4 + input drivers
# Note: GPU drivers (redox-driver-sys, linux-kpi, redox-drm, amdgpu)
# are not included because they need custom build templates.
# Build them separately with: ./local/scripts/build-amd.sh
#
# Desktop + RBOS branding + ext4 + input + Wayland + Qt6
# Build: make all CONFIG_NAME=redbear-full
# Live: make live CONFIG_NAME=redbear-full
#
# GPU driver stack (redox-driver-sys, linux-kpi, redox-drm, amdgpu): WIP crates
# need custom template (library-only, cargo install fails). Re-enable when fixed.
# KDE Frameworks + KWin: depend on qtdeclarative/qtwayland. Re-enable when ported.
# libinput/libevdev: WIP meson builds, not yet validated. Re-enable when tested.
# seatd: now builds; DRM lease/runtime validation is still open before enabling broadly.
include = ["desktop.toml"]
[general]
# 2GB filesystem — plenty for full desktop + headroom
filesystem_size = 2048
[packages]
# Red Bear OS branding (os-release, hostname, motd)
redbear-release = {}
# Native Redox PCI/USB listing tools (lspci, lsusb)
redbear-hwutils = {}
# Redox-native netctl compatibility command
redbear-netctl = {}
# Terminal file manager (Midnight Commander port)
mc = {}
@@ -30,12 +37,224 @@ firmware-loader = {}
evdevd = {}
udev-shim = {}
# Package builder (cub -S/-B/-G CLI)
cub = {}
# Diagnostic tool
redbear-info = {}
# RBOS meta-package (dependencies, default config)
redbear-meta = {}
# Process monitor
htop = {}
# Wayland protocol
libwayland = {}
# Keyboard support
libxkbcommon = {}
# Qt6 base (Core+Concurrent+Xml+Gui+Widgets, software rendering)
qtbase = {}
# RBOS meta-package — temporarily disabled (depends on GPU stack via redox-driver-sys)
# redbear-meta = {}
# Workaround: bash fails to cross-compile (upstream mkbuiltins.c issue)
# ion (from minimal) is the default shell anyway
bash = "ignore"
# Firmware directory for AMD/Intel GPU blobs
[[files]]
path = "/usr/firmware/amdgpu"
data = ""
directory = true
mode = 0o755
# ── Neutralize broken legacy scripts from upstream configs ─────────
#
# base.toml and desktop-minimal.toml ship legacy init scripts that use
# "notify <service>" — but "notify" is NOT a keyword in the legacy script
# parser (only "requires_weak" and "nowait" exist). This causes init to
# try executing a binary called "notify" which doesn't exist.
#
# The base source package already ships proper .service/.target files in
# its init.d/ directory. These get installed to /usr/lib/init.d/ during
# the build. We just need to neutralize the conflicting legacy scripts
# so only the proper .service/.target files remain active.
#
# Override each broken legacy script with an empty file. Init will still
# find the .target and .service files from the base package.
# base.toml: "notify ipcd", "notify ptyd", "nowait sudo --daemon"
# → Replaced by 00_base.target → 00_ipcd.service + 00_ptyd.service + 00_sudo.service
[[files]]
path = "/usr/lib/init.d/00_base"
data = ""
# base.toml: "pcid-spawner" (blocking, no keyword)
# → Replaced by 00_pcid-spawner.service (oneshot)
[[files]]
path = "/usr/lib/init.d/00_drivers"
data = ""
# base.toml: "notify smolnetd", "nowait dhcpd"
# → Replaced by 10_net.target → 10_smolnetd.service + 10_dhcpd.service
[[files]]
path = "/usr/lib/init.d/10_net"
data = ""
# desktop-minimal.toml: "notify audiod", "nowait orbital orblogin launcher"
# → audiod: 20_audiod.service (from base package, type=scheme)
# → orbital: needs its own .service file since base doesn't ship one
[[files]]
path = "/usr/lib/init.d/20_orbital"
data = ""
# ── Desktop services (not provided by base package) ────────────────
# Orbital display server + login + launcher
# desktop-minimal.toml had "nowait VT=3 orbital orblogin launcher"
[[files]]
path = "/usr/lib/init.d/20_orbital.service"
data = """
[unit]
description = "Orbital display server"
requires_weak = [
"10_net.target",
]
[service]
cmd = "orbital"
args = ["orblogin", "launcher"]
envs = { VT = "3" }
type = "oneshot_async"
"""
# ── Red Bear OS custom service .service files ──────────────────────
# firmware-loader: scheme daemon serving /scheme/firmware
# Uses SchemeDaemon which requires init to read the pipe (ServiceType::Scheme).
[[files]]
path = "/usr/lib/init.d/05_firmware-loader.service"
data = """
[unit]
description = "Firmware loading scheme"
requires_weak = [
"00_pcid-spawner.service",
]
[service]
cmd = "firmware-loader"
type = { scheme = "firmware" }
"""
# udev-shim: scheme daemon serving /scheme/udev
[[files]]
path = "/usr/lib/init.d/11_udev.service"
data = """
[unit]
description = "udev compatibility shim"
requires_weak = [
"00_pcid-spawner.service",
]
[service]
cmd = "udev-shim"
type = { scheme = "udev" }
"""
# evdevd: self-registers scheme, no INIT_NOTIFY handshake needed
[[files]]
path = "/usr/lib/init.d/10_evdevd.service"
data = """
[unit]
description = "Evdev input daemon"
requires_weak = [
"00_pcid-spawner.service",
]
[service]
cmd = "evdevd"
type = "oneshot_async"
"""
[[files]]
path = "/etc/netctl"
data = ""
directory = true
mode = 0o755
[[files]]
path = "/etc/netctl/examples"
data = ""
directory = true
mode = 0o755
[[files]]
path = "/etc/netctl/examples/wired-dhcp"
data = """
Description='Red Bear wired DHCP profile'
Interface=eth0
Connection=ethernet
IP=dhcp
"""
[[files]]
path = "/etc/netctl/examples/wired-static"
data = """
Description='Red Bear wired static profile'
Interface=eth0
Connection=ethernet
IP=static
Address=('192.168.1.10/24')
Gateway='192.168.1.1'
DNS=('1.1.1.1')
"""
[[files]]
path = "/usr/lib/init.d/12_netctl.service"
data = """
[unit]
description = "Network profile application"
requires_weak = [
"10_smolnetd.service",
"10_dhcpd.service",
]
[service]
cmd = "netctl"
args = ["--boot"]
type = "oneshot"
"""
# desktop-minimal.toml: "inputd -A 2", "nowait getty 2", "nowait getty /scheme/debug/no-preserve -J"
# Neutralize and replace with proper service files
[[files]]
path = "/usr/lib/init.d/30_console"
data = ""
[[files]]
path = "/usr/lib/init.d/30_console.service"
data = """
[unit]
description = "Console terminals"
requires_weak = [
"20_orbital.service",
]
[service]
cmd = "getty"
args = ["2"]
type = "oneshot_async"
"""
[[files]]
path = "/usr/lib/init.d/31_debug_console.service"
data = """
[unit]
description = "Debug console"
requires_weak = [
"20_orbital.service",
]
[service]
cmd = "getty"
args = ["/scheme/debug/no-preserve", "-J"]
type = "oneshot_async"
"""
+294
View File
@@ -0,0 +1,294 @@
# Red Bear OS KDE Plasma Desktop Configuration
# Build: make all CONFIG_NAME=redbear-kde
# Live: make live CONFIG_NAME=redbear-kde
#
# KDE Plasma 6 session with Wayland compositor
# Requires: D-Bus, libinput, Mesa, Qt6, KF6, KWin, plasma-workspace
include = ["desktop.toml"]
[general]
filesystem_size = 4096
[packages]
# Red Bear OS branding
redbear-release = {}
# ext4 filesystem support
ext4d = {}
# Firmware loading
firmware-loader = {}
# Input layer
evdevd = {}
udev-shim = {}
# D-Bus (session + system bus)
dbus = {}
# Wayland protocol
libwayland = {}
wayland-protocols = {}
# Input
libxkbcommon = {}
libevdev = {}
libinput = {}
# Seat management
seatd = {}
# Qt6 stack
qtbase = {}
qtdeclarative = {}
qtsvg = {}
qtwayland = {}
# KF6 Frameworks — Tier 1 (no special deps)
kf6-extra-cmake-modules = {}
kf6-kcoreaddons = {}
kf6-kwidgetsaddons = {}
kf6-kconfig = {}
kf6-ki18n = {}
kf6-kcodecs = {}
kf6-kguiaddons = {}
kf6-kcolorscheme = {}
kf6-kauth = {}
kf6-kitemmodels = {}
kf6-kitemviews = {}
# KF6 Frameworks — Tier 2
kf6-karchive = {}
kf6-kwindowsystem = {}
kf6-knotifications = {}
kf6-kjobwidgets = {}
kf6-kconfigwidgets = {}
# KF6 Frameworks — Tier 3 (needs D-Bus)
kf6-kcrash = {}
kf6-kdbusaddons = {}
kf6-kglobalaccel = {}
kf6-kservice = {}
kf6-kpackage = {}
kf6-kiconthemes = {}
kf6-kxmlgui = {}
kf6-ktextwidgets = {}
kf6-kirigami = {}
kf6-solid = {}
kf6-sonnet = {}
# KF6 Frameworks — Tier 4 (needs kio + kxmlgui)
kf6-kio = {}
kf6-kbookmarks = {}
kf6-kcompletion = {}
kf6-kdeclarative = {}
kf6-kcmutils = {}
plasma-framework = {}
# KDE Plasma
kwin = {}
plasma-workspace = {}
plasma-desktop = {}
breeze = {}
kde-cli-tools = {}
# Graphics
mesa = {}
libdrm = {}
# Workaround: bash fails to cross-compile
bash = "ignore"
# Firmware directory for AMD/Intel GPU blobs
[[files]]
path = "/usr/firmware/amdgpu"
data = ""
directory = true
mode = 0o755
# ── Neutralize broken legacy scripts from upstream configs ─────────
# base.toml uses "notify <service>" which is not a keyword in the legacy
# script parser. Base source package ships proper .service/.target files.
[[files]]
path = "/usr/lib/init.d/00_base"
data = ""
[[files]]
path = "/usr/lib/init.d/00_drivers"
data = ""
[[files]]
path = "/usr/lib/init.d/10_net"
data = ""
[[files]]
path = "/usr/lib/init.d/20_orbital"
data = ""
[[files]]
path = "/usr/lib/init.d/30_console"
data = ""
# ── Red Bear OS custom services ─────────────────────────────────────
[[files]]
path = "/usr/lib/init.d/05_firmware-loader.service"
data = """
[unit]
description = "Firmware loading scheme"
requires_weak = [
"00_pcid-spawner.service",
]
[service]
cmd = "firmware-loader"
type = { scheme = "firmware" }
"""
[[files]]
path = "/usr/lib/init.d/10_evdevd.service"
data = """
[unit]
description = "Evdev input daemon"
requires_weak = [
"00_pcid-spawner.service",
]
[service]
cmd = "evdevd"
type = "oneshot_async"
"""
[[files]]
path = "/usr/lib/init.d/11_udev.service"
data = """
[unit]
description = "udev compatibility shim"
requires_weak = [
"00_pcid-spawner.service",
]
[service]
cmd = "udev-shim"
type = { scheme = "udev" }
"""
[[files]]
path = "/usr/lib/init.d/12_dbus.service"
data = """
[unit]
description = "D-Bus system bus"
requires_weak = [
"00_base.target",
]
[service]
cmd = "dbus-daemon"
args = ["--system"]
type = "oneshot_async"
"""
[[files]]
path = "/usr/lib/init.d/13_seatd.service"
data = """
[unit]
description = "seatd seat management daemon"
requires_weak = [
"12_dbus.service",
]
[service]
cmd = "seatd"
args = ["-l", "info"]
type = "oneshot_async"
"""
[[files]]
path = "/usr/lib/init.d/20_orbital.service"
data = """
[unit]
description = "Orbital display server (KDE session)"
requires_weak = [
"10_net.target",
"12_dbus.service",
"13_seatd.service",
]
[service]
cmd = "orbital"
args = ["orbital-kde"]
envs = { VT = "3" }
type = "oneshot_async"
"""
[[files]]
path = "/usr/lib/init.d/30_console.service"
data = """
[unit]
description = "Console terminals"
requires_weak = [
"20_orbital.service",
]
[service]
cmd = "getty"
args = ["2"]
type = "oneshot_async"
"""
[[files]]
path = "/usr/lib/init.d/31_debug_console.service"
data = """
[unit]
description = "Debug console"
requires_weak = [
"20_orbital.service",
]
[service]
cmd = "getty"
args = ["/scheme/debug/no-preserve", "-J"]
type = "oneshot_async"
"""
# KDE session launcher
[[files]]
path = "/usr/bin/orbital-kde"
mode = 0o755
data = """
#!/usr/bin/env bash
set -ex
export DISPLAY=""
export WAYLAND_DISPLAY=wayland-0
export XDG_RUNTIME_DIR=/tmp/run/user/0
export XDG_SESSION_TYPE=wayland
export KDE_FULL_SESSION=true
export XDG_CURRENT_DESKTOP=KDE
export HOME=/root
export LIBSEAT_BACKEND=seatd
export SEATD_SOCK=/run/seatd.sock
mkdir -p /tmp/run/user/0
mkdir -p /var/lib/dbus
mkdir -p /run/dbus
# Start D-Bus system bus (if not already running)
if [ ! -S /run/dbus/system_bus_socket ]; then
dbus-uuidgen --ensure
dbus-daemon --system --fork
fi
# Start D-Bus session bus
eval $(dbus-launch --sh-syntax)
# Start KWin Wayland compositor
kwin_wayland --replace &
sleep 2
# Start Plasma Shell
plasmashell &
"""
+151 -7
View File
@@ -12,15 +12,159 @@ filesystem_size = 512
# Red Bear OS branding
redbear-release = {}
# Native Redox PCI/USB listing tools (lspci, lsusb)
redbear-hwutils = {}
# Redox-native netctl compatibility command
redbear-netctl = {}
# Terminal file manager
mc = {}
# Package builder
cub = {}
# Diagnostic tool
redbear-info = {}
# Firmware loading
firmware-loader = {}
# ── Neutralize broken legacy scripts from base.toml ─────────────────
# base.toml uses "notify <service>" which is not a keyword in the legacy
# script parser. Base source package already ships proper .service/.target
# files — we just need to suppress the conflicting legacy scripts.
# Input event handling
evdevd = {}
udev-shim = {}
[[files]]
path = "/usr/lib/init.d/00_base"
data = ""
[[files]]
path = "/usr/lib/init.d/00_drivers"
data = ""
[[files]]
path = "/usr/lib/init.d/10_net"
data = ""
# minimal.toml: "inputd -A 2", "nowait getty 2", "nowait getty /scheme/debug/no-preserve -J"
[[files]]
path = "/usr/lib/init.d/30_console"
data = ""
[[files]]
path = "/usr/lib/init.d/30_console.service"
data = """
[unit]
description = "Console terminals"
requires_weak = [
"10_net.target",
]
[service]
cmd = "getty"
args = ["2"]
type = "oneshot_async"
"""
[[files]]
path = "/usr/lib/init.d/31_debug_console.service"
data = """
[unit]
description = "Debug console"
requires_weak = [
"10_net.target",
]
[service]
cmd = "getty"
args = ["/scheme/debug/no-preserve", "-J"]
type = "oneshot_async"
"""
# ── Red Bear OS custom services ─────────────────────────────────────
[[files]]
path = "/usr/lib/init.d/05_firmware-loader.service"
data = """
[unit]
description = "Firmware loading scheme"
requires_weak = [
"00_pcid-spawner.service",
]
[service]
cmd = "firmware-loader"
type = { scheme = "firmware" }
"""
[[files]]
path = "/usr/lib/init.d/11_udev.service"
data = """
[unit]
description = "udev compatibility shim"
requires_weak = [
"00_pcid-spawner.service",
]
[service]
cmd = "udev-shim"
type = { scheme = "udev" }
"""
[[files]]
path = "/usr/lib/init.d/10_evdevd.service"
data = """
[unit]
description = "Evdev input daemon"
requires_weak = [
"00_pcid-spawner.service",
]
[service]
cmd = "evdevd"
type = "oneshot_async"
"""
[[files]]
path = "/etc/netctl"
data = ""
directory = true
mode = 0o755
[[files]]
path = "/etc/netctl/examples"
data = ""
directory = true
mode = 0o755
[[files]]
path = "/etc/netctl/examples/wired-dhcp"
data = """
Description='Red Bear wired DHCP profile'
Interface=eth0
Connection=ethernet
IP=dhcp
"""
[[files]]
path = "/etc/netctl/examples/wired-static"
data = """
Description='Red Bear wired static profile'
Interface=eth0
Connection=ethernet
IP=static
Address=('192.168.1.10/24')
Gateway='192.168.1.1'
DNS=('1.1.1.1')
"""
[[files]]
path = "/usr/lib/init.d/12_netctl.service"
data = """
[unit]
description = "Network profile application"
requires_weak = [
"10_smolnetd.service",
"10_dhcpd.service",
]
[service]
cmd = "netctl"
args = ["--boot"]
type = "oneshot"
"""
+3 -3
View File
@@ -21,8 +21,8 @@ redox-drm = {}
amdgpu = {}
# Input (Phase 3)
evdevd = { path = "../../local/recipes/system/evdevd" }
udev-shim = { path = "../../local/recipes/system/udev-shim" }
evdevd = {}
udev-shim = {}
# Wayland (Phase 4 — depends on P2+P3)
# libwayland = {}
@@ -62,5 +62,5 @@ nowait evdevd
path = "/usr/lib/init.d/11_udev"
data = """
requires_weak 00_drivers
nowait udev-shim
nowait udev
"""
+1 -1
View File
@@ -68,5 +68,5 @@ nowait evdevd
path = "/usr/lib/init.d/11_udev"
data = """
requires_weak 00_drivers
nowait udev-shim
nowait udev
"""
+1 -1
View File
@@ -54,5 +54,5 @@ nowait evdevd
path = "/usr/lib/init.d/11_udev"
data = """
requires_weak 00_drivers
nowait udev-shim
nowait udev
"""
@@ -8,3 +8,7 @@ redox-scheme = "0.1"
syscall = { package = "redox_syscall", version = "0.4" }
log = { version = "0.4", features = ["std"] }
thiserror = "2"
orbclient = { version = "=0.3.47", default-features = false }
[target.'cfg(target_os = "redox")'.dependencies]
redox_event = "0.4"
+168 -31
View File
@@ -1,6 +1,12 @@
use std::collections::VecDeque;
use std::collections::BTreeMap;
use crate::types::{InputEvent, InputId, BUS_VIRTUAL};
use crate::translate::{KEYBOARD_KEY_CODES, MOUSE_BUTTON_CODES, TOUCHPAD_KEY_CODES};
use crate::types::{
AbsInfo, InputId, ABS_MT_POSITION_X, ABS_MT_POSITION_Y, ABS_MT_SLOT, ABS_MT_TOUCH_MAJOR,
ABS_MT_TRACKING_ID, ABS_PRESSURE, ABS_X, ABS_Y, BUS_VIRTUAL, EV_ABS, EV_KEY, EV_LED, EV_MSC,
EV_REL, EV_REP, EV_SYN, INPUT_PROP_POINTER, KEY_MAX, LED_CAPSL, LED_MAX, LED_NUML, LED_SCROLLL,
MSC_SCAN, REL_HWHEEL, REL_WHEEL, REL_X, REL_Y, REP_DELAY, REP_PERIOD,
};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum DeviceKind {
@@ -10,17 +16,17 @@ pub enum DeviceKind {
}
pub struct InputDevice {
pub id: usize,
pub kind: DeviceKind,
pub name: String,
pub input_id: InputId,
pub event_buf: VecDeque<InputEvent>,
pub key_state: [u8; KEY_MAX / 8 + 1],
pub led_state: [u8; LED_MAX / 8 + 1],
pub custom_abs: BTreeMap<u16, AbsInfo>,
}
impl InputDevice {
pub fn new_keyboard(id: usize) -> Self {
InputDevice {
id,
kind: DeviceKind::Keyboard,
name: format!("Redox Keyboard {}", id),
input_id: InputId {
@@ -29,13 +35,14 @@ impl InputDevice {
product: id as u16,
version: 1,
},
event_buf: VecDeque::new(),
key_state: [0u8; KEY_MAX / 8 + 1],
led_state: [0u8; LED_MAX / 8 + 1],
custom_abs: BTreeMap::new(),
}
}
pub fn new_mouse(id: usize) -> Self {
InputDevice {
id,
kind: DeviceKind::Mouse,
name: format!("Redox Mouse {}", id),
input_id: InputId {
@@ -44,13 +51,14 @@ impl InputDevice {
product: (id + 0x10) as u16,
version: 1,
},
event_buf: VecDeque::new(),
key_state: [0u8; KEY_MAX / 8 + 1],
led_state: [0u8; LED_MAX / 8 + 1],
custom_abs: BTreeMap::new(),
}
}
pub fn new_touchpad(id: usize) -> Self {
InputDevice {
id,
kind: DeviceKind::Touchpad,
name: format!("Redox Touchpad {}", id),
input_id: InputId {
@@ -59,37 +67,166 @@ impl InputDevice {
product: (id + 0x20) as u16,
version: 1,
},
event_buf: VecDeque::new(),
key_state: [0u8; KEY_MAX / 8 + 1],
led_state: [0u8; LED_MAX / 8 + 1],
custom_abs: BTreeMap::new(),
}
}
pub fn push_event(&mut self, event: InputEvent) {
self.event_buf.push_back(event);
}
pub fn push_events(&mut self, events: &[InputEvent]) {
for &ev in events {
self.event_buf.push_back(ev);
pub fn supported_event_types(&self) -> Vec<u8> {
match self.kind {
DeviceKind::Keyboard => bitmap_from_codes(&[EV_SYN, EV_KEY, EV_MSC, EV_LED, EV_REP]),
DeviceKind::Mouse => bitmap_from_codes(&[EV_SYN, EV_KEY, EV_REL]),
DeviceKind::Touchpad => bitmap_from_codes(&[EV_SYN, EV_KEY, EV_ABS]),
}
}
pub fn pop_bytes(&mut self, buf: &mut [u8]) -> usize {
let event_count = buf.len() / InputEvent::SIZE;
let mut written = 0;
for _ in 0..event_count {
match self.event_buf.pop_front() {
Some(ev) => {
let bytes = ev.to_bytes();
buf[written..written + InputEvent::SIZE].copy_from_slice(&bytes);
written += InputEvent::SIZE;
}
None => break,
pub fn supported_keys(&self) -> Vec<u8> {
match self.kind {
DeviceKind::Keyboard => bitmap_from_codes(KEYBOARD_KEY_CODES),
DeviceKind::Mouse => bitmap_from_codes(MOUSE_BUTTON_CODES),
DeviceKind::Touchpad => bitmap_from_codes(TOUCHPAD_KEY_CODES),
}
}
pub fn supported_rel(&self) -> Vec<u8> {
match self.kind {
DeviceKind::Mouse => bitmap_from_codes(&[REL_X, REL_Y, REL_WHEEL, REL_HWHEEL]),
_ => Vec::new(),
}
}
pub fn supported_abs(&self) -> Vec<u8> {
match self.kind {
DeviceKind::Touchpad => bitmap_from_codes(&[
ABS_X,
ABS_Y,
ABS_PRESSURE,
ABS_MT_SLOT,
ABS_MT_TOUCH_MAJOR,
ABS_MT_POSITION_X,
ABS_MT_POSITION_Y,
ABS_MT_TRACKING_ID,
]),
_ => Vec::new(),
}
}
pub fn supported_msc(&self) -> Vec<u8> {
match self.kind {
DeviceKind::Keyboard => bitmap_from_codes(&[MSC_SCAN]),
_ => Vec::new(),
}
}
pub fn supported_leds(&self) -> Vec<u8> {
match self.kind {
DeviceKind::Keyboard => bitmap_from_codes(&[LED_NUML, LED_CAPSL, LED_SCROLLL]),
_ => Vec::new(),
}
}
pub fn supported_rep(&self) -> Vec<u8> {
match self.kind {
DeviceKind::Keyboard => bitmap_from_codes(&[REP_DELAY, REP_PERIOD]),
_ => Vec::new(),
}
}
pub fn supported_props(&self) -> Vec<u8> {
match self.kind {
DeviceKind::Mouse | DeviceKind::Touchpad => bitmap_from_codes(&[INPUT_PROP_POINTER]),
DeviceKind::Keyboard => Vec::new(),
}
}
pub fn abs_info(&self, axis: u16) -> AbsInfo {
if let Some(abs_info) = self.custom_abs.get(&axis) {
return *abs_info;
}
if self.kind != DeviceKind::Touchpad {
return AbsInfo::default();
}
match axis {
ABS_X | ABS_MT_POSITION_X => AbsInfo {
minimum: 0,
maximum: 65_535,
resolution: 1,
..AbsInfo::default()
},
ABS_Y | ABS_MT_POSITION_Y => AbsInfo {
minimum: 0,
maximum: 65_535,
resolution: 1,
..AbsInfo::default()
},
ABS_PRESSURE => AbsInfo {
minimum: 0,
maximum: 255,
resolution: 1,
..AbsInfo::default()
},
ABS_MT_TOUCH_MAJOR => AbsInfo {
minimum: 0,
maximum: 255,
resolution: 1,
..AbsInfo::default()
},
ABS_MT_SLOT => AbsInfo {
minimum: 0,
maximum: 9,
..AbsInfo::default()
},
ABS_MT_TRACKING_ID => AbsInfo {
minimum: 0,
maximum: i32::MAX,
..AbsInfo::default()
},
_ => AbsInfo::default(),
}
}
pub fn set_abs_info(&mut self, axis: u16, abs_info: AbsInfo) {
self.custom_abs.insert(axis, abs_info);
}
pub fn update_key_state(&mut self, code: u16, pressed: bool) {
let byte = (code / 8) as usize;
let bit = code % 8;
if byte < self.key_state.len() {
if pressed {
self.key_state[byte] |= 1 << bit;
} else {
self.key_state[byte] &= !(1 << bit);
}
}
written
}
pub fn has_events(&self) -> bool {
!self.event_buf.is_empty()
pub fn update_led_state(&mut self, code: u16, lit: bool) {
let byte = (code / 8) as usize;
let bit = code % 8;
if byte < self.led_state.len() {
if lit {
self.led_state[byte] |= 1 << bit;
} else {
self.led_state[byte] &= !(1 << bit);
}
}
}
}
fn bitmap_from_codes(codes: &[u16]) -> Vec<u8> {
let Some(max) = codes.iter().copied().max() else {
return Vec::new();
};
let mut bitmap = vec![0u8; (usize::from(max) / 8) + 1];
for &code in codes {
let index = usize::from(code / 8);
let bit = 1u8 << (code % 8);
bitmap[index] |= bit;
}
bitmap
}
+257 -44
View File
@@ -3,16 +3,37 @@ mod scheme;
mod translate;
mod types;
use std::collections::VecDeque;
use std::env;
use std::fs::File;
use std::io::Read;
use std::fs::OpenOptions;
use std::io::{ErrorKind, Read};
use std::mem::{size_of, MaybeUninit};
#[cfg(target_os = "redox")]
use std::os::fd::AsRawFd;
use std::os::unix::fs::OpenOptionsExt;
use std::process;
#[cfg(not(target_os = "redox"))]
use std::thread;
#[cfg(not(target_os = "redox"))]
use std::time::Duration;
use log::{error, info, LevelFilter, Metadata, Record};
use redox_scheme::{SignalBehavior, Socket};
use orbclient::{Event, EventOption};
use redox_scheme::{Request, SignalBehavior, Socket};
use syscall::error::EAGAIN;
use syscall::flag::O_NONBLOCK;
use scheme::EvdevScheme;
#[cfg(target_os = "redox")]
use event::{EventFlags as QueueEventFlags, RawEventQueue};
#[cfg(target_os = "redox")]
const SCHEME_QUEUE_TOKEN: usize = 1;
#[cfg(target_os = "redox")]
const INPUT_QUEUE_TOKEN: usize = 2;
struct StderrLogger {
level: LevelFilter,
}
@@ -29,64 +50,256 @@ impl log::Log for StderrLogger {
fn flush(&self) {}
}
fn read_input_events(scheme: &mut EvdevScheme) -> Result<(), String> {
let mut input_file =
File::open("/scheme/input").map_err(|e| format!("failed to open /scheme/input: {}", e))?;
struct InputConsumer {
file: File,
partial: Vec<u8>,
}
let mut buf = [0u8; 256];
match input_file.read(&mut buf) {
Ok(n) if n > 0 => {
let data = &buf[..n];
for &byte in data {
let pressed = (byte & 0x80) == 0;
let key = byte & 0x7F;
scheme.feed_keyboard_event(key, pressed);
impl InputConsumer {
fn open() -> Result<Self, String> {
let file = OpenOptions::new()
.read(true)
.custom_flags(O_NONBLOCK as i32)
.open("/scheme/input/consumer")
.map_err(|e| format!("failed to open /scheme/input/consumer: {e}"))?;
Ok(Self {
file,
partial: Vec::new(),
})
}
#[cfg(target_os = "redox")]
fn fd(&self) -> usize {
self.file.as_raw_fd() as usize
}
fn read_available(&mut self, scheme: &mut EvdevScheme) -> Result<bool, String> {
let event_size = size_of::<Event>();
let mut buf = vec![0u8; event_size * 32];
let mut progress = false;
loop {
match self.file.read(&mut buf) {
Ok(0) => break,
Ok(count) => {
progress = true;
self.partial.extend_from_slice(&buf[..count]);
while self.partial.len() >= event_size {
let event = read_event_from_bytes(&self.partial[..event_size]);
self.partial.drain(..event_size);
dispatch_input_event(event, scheme);
}
}
Err(err) if err.kind() == ErrorKind::WouldBlock => break,
Err(err) => return Err(format!("failed to read /scheme/input/consumer: {err}")),
}
}
Ok(_) => {}
Err(e) => {
error!("evdevd: failed to read input: {}", e);
Ok(progress)
}
}
#[derive(Clone, Copy, Debug, Default)]
struct SchemePoll {
mounted: bool,
progress: bool,
}
fn read_event_from_bytes(bytes: &[u8]) -> Event {
let mut event = MaybeUninit::<Event>::uninit();
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), event.as_mut_ptr() as *mut u8, bytes.len());
event.assume_init()
}
}
fn dispatch_input_event(event: Event, scheme: &mut EvdevScheme) {
match event.to_option() {
EventOption::Key(key) => scheme.feed_keyboard_event(key.scancode, key.pressed),
EventOption::Mouse(mouse) => scheme.feed_touchpad_position(mouse.x, mouse.y),
EventOption::MouseRelative(mouse) => scheme.feed_mouse_move(mouse.dx, mouse.dy),
EventOption::Button(button) => {
scheme.feed_mouse_buttons(button.left, button.middle, button.right)
}
EventOption::Scroll(scroll) => scheme.feed_mouse_scroll(scroll.x, scroll.y),
_ => {}
}
}
fn is_would_block_socket(err: &syscall::Error) -> bool {
err.errno == EAGAIN
}
fn write_scheme_response(socket: &Socket, response: redox_scheme::Response) -> Result<(), String> {
socket
.write_response(response, SignalBehavior::Restart)
.map_err(|e| format!("failed to write response: {e}"))?;
Ok(())
}
fn handle_request(
request: Request,
scheme: &mut EvdevScheme,
pending_requests: &mut VecDeque<Request>,
socket: &Socket,
) -> Result<bool, String> {
match request.handle_scheme_block_mut(scheme) {
Ok(response) => {
write_scheme_response(socket, response)?;
Ok(true)
}
Err(request) => {
pending_requests.push_back(request);
Ok(true)
}
}
}
fn flush_pending_requests(
scheme: &mut EvdevScheme,
pending_requests: &mut VecDeque<Request>,
socket: &Socket,
) -> Result<bool, String> {
let mut progress = false;
let pending_len = pending_requests.len();
for _ in 0..pending_len {
let Some(request) = pending_requests.pop_front() else {
break;
};
match request.handle_scheme_block_mut(scheme) {
Ok(response) => {
write_scheme_response(socket, response)?;
progress = true;
}
Err(request) => pending_requests.push_back(request),
}
}
Ok(progress)
}
fn read_scheme_requests(
socket: &Socket,
scheme: &mut EvdevScheme,
pending_requests: &mut VecDeque<Request>,
) -> Result<SchemePoll, String> {
let mut poll = SchemePoll {
mounted: true,
progress: false,
};
loop {
match socket.next_request(SignalBehavior::Restart) {
Ok(Some(request)) => {
poll.progress =
handle_request(request, scheme, pending_requests, socket)? || poll.progress;
}
Ok(None) => {
poll.mounted = false;
break;
}
Err(err) if is_would_block_socket(&err) => break,
Err(err) => return Err(format!("failed to read scheme request: {err}")),
}
}
Ok(poll)
}
#[cfg(target_os = "redox")]
fn run_redox_event_loop(
socket: &Socket,
scheme: &mut EvdevScheme,
input: &mut InputConsumer,
pending_requests: &mut VecDeque<Request>,
) -> Result<(), String> {
let event_queue =
RawEventQueue::new().map_err(|e| format!("failed to create event queue: {e}"))?;
event_queue
.subscribe(
socket.inner().raw(),
SCHEME_QUEUE_TOKEN,
QueueEventFlags::READ,
)
.map_err(|e| format!("failed to subscribe scheme socket: {e}"))?;
event_queue
.subscribe(input.fd(), INPUT_QUEUE_TOKEN, QueueEventFlags::READ)
.map_err(|e| format!("failed to subscribe input consumer: {e}"))?;
loop {
let raw_event = event_queue
.next_event()
.map_err(|e| format!("failed to wait for events: {e}"))?;
match raw_event.user_data {
SCHEME_QUEUE_TOKEN => {
let poll = read_scheme_requests(socket, scheme, pending_requests)?;
if !poll.mounted {
info!("evdevd: scheme unmounted, exiting");
break;
}
}
INPUT_QUEUE_TOKEN => {
let _ = input.read_available(scheme)?;
}
_ => {}
}
let _ = flush_pending_requests(scheme, pending_requests, socket)?;
}
Ok(())
}
#[cfg(not(target_os = "redox"))]
fn run_host_event_loop(
socket: &Socket,
scheme: &mut EvdevScheme,
input: &mut InputConsumer,
pending_requests: &mut VecDeque<Request>,
) -> Result<(), String> {
loop {
let mut progress = input.read_available(scheme)?;
let poll = read_scheme_requests(socket, scheme, pending_requests)?;
if !poll.mounted {
info!("evdevd: scheme unmounted, exiting");
break;
}
progress |= poll.progress;
progress |= flush_pending_requests(scheme, pending_requests, socket)?;
if !progress {
thread::sleep(Duration::from_millis(10));
}
}
Ok(())
}
fn run() -> Result<(), String> {
let mut scheme = EvdevScheme::new();
let mut input = InputConsumer::open()?;
let mut pending_requests = VecDeque::new();
let socket =
Socket::create("evdev").map_err(|e| format!("failed to register evdev scheme: {}", e))?;
Socket::nonblock("evdev").map_err(|e| format!("failed to register evdev scheme: {}", e))?;
info!("evdevd: registered scheme:evdev");
info!("evdevd: consuming orbclient::Event from /scheme/input/consumer");
loop {
let request = match socket.next_request(SignalBehavior::Restart) {
Ok(Some(r)) => r,
Ok(None) => {
info!("evdevd: scheme unmounted, exiting");
break;
}
Err(e) => {
error!("evdevd: failed to read scheme request: {}", e);
continue;
}
};
let response = match request.handle_scheme_block_mut(&mut scheme) {
Ok(r) => r,
Err(_req) => {
error!("evdevd: failed to handle request");
continue;
}
};
if let Err(e) = socket.write_response(response, SignalBehavior::Restart) {
error!("evdevd: failed to write response: {}", e);
}
let _ = read_input_events(&mut scheme);
#[cfg(target_os = "redox")]
{
run_redox_event_loop(&socket, &mut scheme, &mut input, &mut pending_requests)
}
Ok(())
#[cfg(not(target_os = "redox"))]
{
run_host_event_loop(&socket, &mut scheme, &mut input, &mut pending_requests)
}
}
fn main() {
+594 -46
View File
@@ -1,11 +1,23 @@
use std::collections::BTreeMap;
use std::collections::{BTreeMap, VecDeque};
use std::mem::size_of;
use std::mem::MaybeUninit;
use std::ptr;
use syscall::data::Stat;
use syscall::error::{Error, Result, EBADF, EINVAL, ENOENT, EROFS};
use syscall::flag::{EventFlags, MODE_DIR, MODE_FILE, SEEK_CUR, SEEK_END, SEEK_SET};
use syscall::error::{Error, Result, EBADF, EBUSY, EFAULT, EINVAL, ENOENT, ENOTTY, EROFS};
use syscall::flag::{
EventFlags, F_GETFD, F_GETFL, F_SETFD, F_SETFL, MODE_DIR, MODE_FILE, O_RDONLY, SEEK_CUR,
SEEK_END, SEEK_SET,
};
use crate::device::{DeviceKind, InputDevice};
use crate::translate;
use crate::types::{
ioc_dir, ioc_nr, ioc_size, ioc_type, is_evdev_ioctl, AbsInfo, InputEvent, InputId,
EVDEV_IOCTL_TYPE, EVIOCGABS, EVIOCGEFFECTS, EVIOCGID, EVIOCGKEY, EVIOCGLED, EVIOCGNAME,
EVIOCGPROP, EVIOCGRAB, EVIOCGVERSION, EVIOCRMFF, EVIOCSABS, EVIOCSCLOCKID, EVIOCSFF, EV_ABS,
EV_KEY, EV_LED, EV_MSC, EV_REL, EV_REP, EV_VERSION, IOC_READ,
};
struct Handle {
kind: HandleKind,
@@ -14,13 +26,22 @@ struct Handle {
enum HandleKind {
Root,
Device(usize),
Device {
device_idx: usize,
events: VecDeque<InputEvent>,
},
}
pub struct EvdevScheme {
next_id: usize,
handles: BTreeMap<usize, Handle>,
devices: Vec<InputDevice>,
grabbed_by: BTreeMap<usize, usize>,
mouse_buttons: [bool; 3],
touchpad_position: (i32, i32),
touchpad_touching: bool,
next_tracking_id: i32,
current_tracking_id: i32,
}
impl EvdevScheme {
@@ -29,54 +50,241 @@ impl EvdevScheme {
next_id: 0,
handles: BTreeMap::new(),
devices: Vec::new(),
grabbed_by: BTreeMap::new(),
mouse_buttons: [false; 3],
touchpad_position: (0, 0),
touchpad_touching: false,
next_tracking_id: 1,
current_tracking_id: -1,
};
scheme.devices.push(InputDevice::new_keyboard(0));
scheme.devices.push(InputDevice::new_mouse(0));
scheme.devices.push(InputDevice::new_mouse(1));
scheme.devices.push(InputDevice::new_touchpad(2));
scheme
}
pub fn feed_keyboard_event(&mut self, key: u8, pressed: bool) {
let events = translate::translate_keyboard(key, pressed);
if !events.is_empty() {
if let Some(dev) = self
.devices
.iter_mut()
.find(|d| d.kind == DeviceKind::Keyboard)
fn device_index(&self, kind: DeviceKind) -> Option<usize> {
self.devices.iter().position(|d| d.kind == kind)
}
fn current_tracking_id(&self) -> i32 {
if self.touchpad_touching {
self.current_tracking_id
} else {
-1
}
}
fn queue_device_events(&mut self, kind: DeviceKind, events: &[InputEvent]) {
if events.is_empty() {
return;
}
let Some(device_idx) = self.device_index(kind) else {
return;
};
for event in events {
if event.event_type == EV_KEY {
self.devices[device_idx].update_key_state(event.code, event.value != 0);
} else if event.event_type == EV_LED {
self.devices[device_idx].update_led_state(event.code, event.value != 0);
}
}
let grabbed_handle = self.grabbed_by.get(&device_idx).copied();
for (handle_id, handle) in self.handles.iter_mut() {
if let HandleKind::Device {
device_idx: handle_device_idx,
events: handle_events,
} = &mut handle.kind
{
dev.push_events(&events);
if *handle_device_idx == device_idx {
if let Some(grabbed_id) = grabbed_handle {
if *handle_id != grabbed_id {
continue;
}
}
handle_events.extend(events.iter().copied());
}
}
}
}
fn pop_handle_bytes(events: &mut VecDeque<InputEvent>, buf: &mut [u8]) -> usize {
let event_count = buf.len() / InputEvent::SIZE;
let mut written = 0;
for _ in 0..event_count {
let Some(event) = events.pop_front() else {
break;
};
let bytes = event.to_bytes();
buf[written..written + InputEvent::SIZE].copy_from_slice(&bytes);
written += InputEvent::SIZE;
}
written
}
pub fn feed_keyboard_event(&mut self, scancode: u8, pressed: bool) {
let events = translate::translate_keyboard(scancode, pressed);
self.queue_device_events(DeviceKind::Keyboard, &events);
}
pub fn feed_mouse_move(&mut self, dx: i32, dy: i32) {
if let Some(dev) = self
.devices
.iter_mut()
.find(|d| d.kind == DeviceKind::Mouse)
{
dev.push_events(&translate::translate_mouse_dx(dx));
dev.push_events(&translate::translate_mouse_dy(dy));
let events = translate::translate_mouse_motion(dx, dy);
self.queue_device_events(DeviceKind::Mouse, &events);
}
pub fn feed_mouse_scroll(&mut self, x: i32, y: i32) {
let events = translate::translate_mouse_scroll(x, y);
self.queue_device_events(DeviceKind::Mouse, &events);
}
pub fn feed_mouse_buttons(&mut self, left: bool, middle: bool, right: bool) {
let old_buttons = self.mouse_buttons;
let new_buttons = [left, middle, right];
for (index, (&old, &new)) in old_buttons.iter().zip(new_buttons.iter()).enumerate() {
if old != new {
let events = translate::translate_mouse_button(index, new);
self.queue_device_events(DeviceKind::Mouse, &events);
}
}
self.mouse_buttons = new_buttons;
let touching = left;
if touching != self.touchpad_touching {
if touching {
self.current_tracking_id = self.next_tracking_id;
self.next_tracking_id = self.next_tracking_id.saturating_add(1);
}
self.touchpad_touching = touching;
let (x, y) = self.touchpad_position;
let tracking_id = self.current_tracking_id();
let events = translate::translate_touchpad_contact(x, y, touching, tracking_id);
self.queue_device_events(DeviceKind::Touchpad, &events);
}
}
pub fn feed_mouse_scroll(&mut self, y: i32) {
if let Some(dev) = self
.devices
.iter_mut()
.find(|d| d.kind == DeviceKind::Mouse)
{
dev.push_events(&translate::translate_mouse_scroll(y));
pub fn feed_touchpad_position(&mut self, x: i32, y: i32) {
self.touchpad_position = (x, y);
let touching = self.touchpad_touching;
let tracking_id = self.current_tracking_id();
let events = translate::translate_touchpad_motion(x, y, touching, tracking_id);
self.queue_device_events(DeviceKind::Touchpad, &events);
}
fn ioctl_name_len(cmd: u64) -> Option<usize> {
if cmd == EVIOCGNAME || (ioc_type(cmd) == EVDEV_IOCTL_TYPE && ioc_nr(cmd) == 0x06) {
let size = ioc_size(cmd);
return Some(if size == 0 { 256 } else { size });
}
None
}
fn ioctl_bit_ev_and_len(cmd: u64) -> Option<(u8, usize)> {
if !is_evdev_ioctl(cmd) {
return None;
}
let nr = ioc_nr(cmd);
if !(0x20..0x40).contains(&nr) {
return None;
}
let size = ioc_size(cmd);
Some(((nr - 0x20) as u8, size))
}
fn ioctl_abs_axis(cmd: u64) -> Option<u16> {
if !is_evdev_ioctl(cmd) {
return None;
}
let nr = ioc_nr(cmd);
if !(0x40..0x80).contains(&nr) {
return None;
}
Some((nr - 0x40) as u16)
}
fn device_bitmap(device: &InputDevice, ev: u8) -> Vec<u8> {
match u16::from(ev) {
0 => device.supported_event_types(),
EV_KEY => device.supported_keys(),
EV_REL => device.supported_rel(),
EV_ABS => device.supported_abs(),
EV_MSC => device.supported_msc(),
EV_LED => device.supported_leds(),
EV_REP => device.supported_rep(),
_ => Vec::new(),
}
}
pub fn feed_mouse_button(&mut self, button: usize, pressed: bool) {
if let Some(dev) = self
.devices
.iter_mut()
.find(|d| d.kind == DeviceKind::Mouse)
{
dev.push_events(&translate::translate_mouse_button(button, pressed));
unsafe fn write_value_to_user<T: Copy>(arg: usize, value: &T) -> Result<usize> {
if arg == 0 {
return Err(Error::new(EFAULT));
}
ptr::copy_nonoverlapping(
value as *const T as *const u8,
arg as *mut u8,
size_of::<T>(),
);
Ok(size_of::<T>())
}
unsafe fn write_bytes_to_user(arg: usize, bytes: &[u8]) -> Result<usize> {
if arg == 0 {
return Err(Error::new(EFAULT));
}
if !bytes.is_empty() {
ptr::copy_nonoverlapping(bytes.as_ptr(), arg as *mut u8, bytes.len());
}
Ok(bytes.len())
}
unsafe fn read_value_from_user<T: Copy>(arg: usize) -> Result<T> {
if arg == 0 {
return Err(Error::new(EFAULT));
}
let mut value = MaybeUninit::<T>::uninit();
ptr::copy_nonoverlapping(
arg as *const u8,
value.as_mut_ptr() as *mut u8,
size_of::<T>(),
);
Ok(value.assume_init())
}
fn ioctl_abs_set_axis(cmd: u64) -> Option<u16> {
if !is_evdev_ioctl(cmd) {
return None;
}
let nr = ioc_nr(cmd);
if !(0xc0..0x100).contains(&nr) {
return None;
}
Some((nr - 0xc0) as u16)
}
fn device_prop_bitmap(device: &InputDevice) -> [u8; 64] {
let bitmap = device.supported_props();
let mut bytes = [0u8; 64];
let copy_len = bitmap.len().min(bytes.len());
if copy_len > 0 {
bytes[..copy_len].copy_from_slice(&bitmap[..copy_len]);
}
bytes
}
}
@@ -94,7 +302,10 @@ impl redox_scheme::SchemeBlockMut for EvdevScheme {
if idx >= self.devices.len() {
return Err(Error::new(ENOENT));
}
HandleKind::Device(idx)
HandleKind::Device {
device_idx: idx,
events: VecDeque::new(),
}
} else {
return Err(Error::new(ENOENT));
};
@@ -108,22 +319,28 @@ impl redox_scheme::SchemeBlockMut for EvdevScheme {
fn read(&mut self, id: usize, buf: &mut [u8]) -> Result<Option<usize>> {
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
match &handle.kind {
match &mut handle.kind {
HandleKind::Root => {
let mut listing = String::new();
for (i, _dev) in self.devices.iter().enumerate() {
listing.push_str(&format!("event{}\n", i));
}
let bytes = listing.as_bytes();
if handle.offset >= bytes.len() {
return Ok(Some(0));
}
let remaining = &bytes[handle.offset..];
let to_copy = remaining.len().min(buf.len());
buf[..to_copy].copy_from_slice(&remaining[..to_copy]);
handle.offset += to_copy;
Ok(Some(to_copy))
}
HandleKind::Device(idx) => {
let dev = &mut self.devices[*idx];
let written = dev.pop_bytes(buf);
HandleKind::Device { events, .. } => {
if !events.is_empty() && buf.len() < InputEvent::SIZE {
return Err(Error::new(EINVAL));
}
let written = Self::pop_handle_bytes(events, buf);
handle.offset += written;
Ok(if written == 0 { None } else { Some(written) })
}
@@ -156,7 +373,7 @@ impl redox_scheme::SchemeBlockMut for EvdevScheme {
HandleKind::Root => {
stat.st_mode = MODE_DIR | 0o555;
}
HandleKind::Device(_) => {
HandleKind::Device { .. } => {
stat.st_mode = MODE_FILE | 0o444;
}
}
@@ -164,6 +381,7 @@ impl redox_scheme::SchemeBlockMut for EvdevScheme {
}
fn close(&mut self, id: usize) -> Result<Option<usize>> {
self.grabbed_by.retain(|_, grabbed_id| *grabbed_id != id);
self.handles.remove(&id);
Ok(Some(0))
}
@@ -172,7 +390,7 @@ impl redox_scheme::SchemeBlockMut for EvdevScheme {
let handle = self.handles.get(&id).ok_or(Error::new(EBADF))?;
let path = match &handle.kind {
HandleKind::Root => "evdev:".to_string(),
HandleKind::Device(idx) => format!("evdev:event{}", idx),
HandleKind::Device { device_idx, .. } => format!("evdev:event{}", device_idx),
};
let bytes = path.as_bytes();
let to_copy = bytes.len().min(buf.len());
@@ -180,13 +398,343 @@ impl redox_scheme::SchemeBlockMut for EvdevScheme {
Ok(Some(to_copy))
}
fn fcntl(&mut self, id: usize, _cmd: usize, _arg: usize) -> Result<Option<usize>> {
let _ = self.handles.get(&id).ok_or(Error::new(EBADF))?;
Ok(Some(0))
fn fcntl(&mut self, id: usize, cmd_raw: usize, arg: usize) -> Result<Option<usize>> {
let device_idx = match &self.handles.get(&id).ok_or(Error::new(EBADF))?.kind {
HandleKind::Root => None,
HandleKind::Device { device_idx, .. } => Some(*device_idx),
};
match cmd_raw {
F_GETFL => return Ok(Some(O_RDONLY)),
F_GETFD => return Ok(Some(0)),
F_SETFL | F_SETFD => {
return Ok(Some(0));
}
_ => {}
}
let Some(idx) = device_idx else {
return Err(Error::new(EINVAL));
};
if cmd_raw == EVIOCGRAB as usize {
let grab = unsafe { Self::read_value_from_user::<i32>(arg)? };
return match grab {
0 => {
if self.grabbed_by.get(&idx) == Some(&id) {
self.grabbed_by.remove(&idx);
}
Ok(Some(0))
}
1 => match self.grabbed_by.get(&idx).copied() {
Some(grabbed_id) if grabbed_id != id => Err(Error::new(EBUSY)),
_ => {
self.grabbed_by.insert(idx, id);
Ok(Some(0))
}
},
_ => Err(Error::new(EINVAL)),
};
}
if cmd_raw == EVIOCSCLOCKID as usize {
return Ok(Some(0));
}
let cmd = cmd_raw as u64;
if matches!(cmd, EVIOCSFF | EVIOCRMFF | EVIOCGEFFECTS) {
return Err(Error::new(ENOTTY));
}
if cmd == EVIOCSABS || Self::ioctl_abs_set_axis(cmd).is_some() {
let axis = Self::ioctl_abs_set_axis(cmd).unwrap_or(0);
let abs_info = unsafe { Self::read_value_from_user::<AbsInfo>(arg)? };
self.devices[idx].set_abs_info(axis, abs_info);
return Ok(Some(0));
}
let device = &self.devices[idx];
if cmd == EVIOCGVERSION {
let version = EV_VERSION;
return unsafe { Self::write_value_to_user(arg, &version).map(Some) };
}
if cmd == EVIOCGID {
let input_id: InputId = device.input_id;
return unsafe { Self::write_value_to_user(arg, &input_id).map(Some) };
}
if cmd == EVIOCGKEY {
let key_state = device.key_state;
return unsafe { Self::write_bytes_to_user(arg, &key_state).map(Some) };
}
if cmd == EVIOCGLED {
let led_state = device.led_state;
return unsafe { Self::write_bytes_to_user(arg, &led_state).map(Some) };
}
if cmd == EVIOCGPROP {
let props = Self::device_prop_bitmap(device);
return unsafe { Self::write_bytes_to_user(arg, &props).map(Some) };
}
if let Some(name_len) = Self::ioctl_name_len(cmd) {
let mut bytes = vec![0u8; name_len];
let name = device.name.as_bytes();
let copy_len = name.len().min(bytes.len().saturating_sub(1));
if copy_len > 0 {
bytes[..copy_len].copy_from_slice(&name[..copy_len]);
}
return unsafe { Self::write_bytes_to_user(arg, &bytes).map(Some) };
}
if let Some((ev, len)) = Self::ioctl_bit_ev_and_len(cmd) {
let bitmap = Self::device_bitmap(device, ev);
let out_len = if len == 0 {
bitmap.len()
} else {
len.max(bitmap.len()).min(len)
};
let mut bytes = vec![0u8; out_len];
let copy_len = bitmap.len().min(bytes.len());
if copy_len > 0 {
bytes[..copy_len].copy_from_slice(&bitmap[..copy_len]);
}
return unsafe { Self::write_bytes_to_user(arg, &bytes).map(Some) };
}
if cmd == EVIOCGABS || Self::ioctl_abs_axis(cmd).is_some() {
let axis = Self::ioctl_abs_axis(cmd).unwrap_or(0);
let abs_info: AbsInfo = device.abs_info(axis);
return unsafe { Self::write_value_to_user(arg, &abs_info).map(Some) };
}
if is_evdev_ioctl(cmd) && ioc_dir(cmd) == IOC_READ {
return Err(Error::new(EINVAL));
}
Err(Error::new(EINVAL))
}
fn fevent(&mut self, id: usize, flags: EventFlags) -> Result<Option<EventFlags>> {
let _ = self.handles.get(&id).ok_or(Error::new(EBADF))?;
Ok(Some(flags))
let handle = self.handles.get(&id).ok_or(Error::new(EBADF))?;
let readiness = match &handle.kind {
HandleKind::Root => flags,
HandleKind::Device { events, .. } if !events.is_empty() => {
flags & EventFlags::EVENT_READ
}
HandleKind::Device { .. } => EventFlags::empty(),
};
Ok(Some(readiness))
}
}
#[cfg(test)]
mod tests {
use redox_scheme::SchemeBlockMut;
use super::EvdevScheme;
use crate::types::{
AbsInfo, InputEvent, ABS_MT_SLOT, EVIOCGEFFECTS, EVIOCGPROP, EVIOCGRAB, EVIOCRMFF,
EVIOCSABS, EVIOCSFF, INPUT_PROP_POINTER,
};
fn open_device(scheme: &mut EvdevScheme, index: usize) -> usize {
scheme
.open(&format!("event{index}"), 0, 0, 0)
.expect("open should succeed")
.expect("device handle id")
}
fn read_events(scheme: &mut EvdevScheme, id: usize) -> Option<usize> {
let mut buf = vec![0u8; InputEvent::SIZE * 8];
scheme.read(id, &mut buf).expect("read should succeed")
}
#[test]
fn eviocgrab_routes_events_only_to_grabbing_handle() {
let mut scheme = EvdevScheme::new();
let first = open_device(&mut scheme, 0);
let second = open_device(&mut scheme, 0);
let grab = 1i32;
scheme
.fcntl(first, EVIOCGRAB as usize, (&grab as *const i32) as usize)
.expect("grab should succeed");
let err = scheme
.fcntl(second, EVIOCGRAB as usize, (&grab as *const i32) as usize)
.expect_err("second grab should fail");
assert_eq!(err.errno, syscall::error::EBUSY);
scheme.feed_keyboard_event(0x1E, true);
assert!(read_events(&mut scheme, first).is_some());
assert_eq!(read_events(&mut scheme, second), None);
let release = 0i32;
scheme
.fcntl(first, EVIOCGRAB as usize, (&release as *const i32) as usize)
.expect("release should succeed");
scheme.feed_keyboard_event(0x30, true);
assert!(read_events(&mut scheme, first).is_some());
assert!(read_events(&mut scheme, second).is_some());
}
#[test]
fn closing_grabbed_handle_releases_grab() {
let mut scheme = EvdevScheme::new();
let first = open_device(&mut scheme, 0);
let second = open_device(&mut scheme, 0);
let grab = 1i32;
scheme
.fcntl(first, EVIOCGRAB as usize, (&grab as *const i32) as usize)
.expect("grab should succeed");
scheme.close(first).expect("close should succeed");
scheme.feed_keyboard_event(0x1E, true);
assert!(read_events(&mut scheme, second).is_some());
}
#[test]
fn eviocgrab_is_scoped_to_each_device() {
let mut scheme = EvdevScheme::new();
let keyboard = open_device(&mut scheme, 0);
let mouse = open_device(&mut scheme, 1);
let grab = 1i32;
scheme
.fcntl(keyboard, EVIOCGRAB as usize, (&grab as *const i32) as usize)
.expect("keyboard grab should succeed");
scheme
.fcntl(mouse, EVIOCGRAB as usize, (&grab as *const i32) as usize)
.expect("mouse grab should also succeed");
}
#[test]
fn eviocgprop_reports_pointer_capability_for_pointer_devices() {
let mut scheme = EvdevScheme::new();
let keyboard = open_device(&mut scheme, 0);
let mouse = open_device(&mut scheme, 1);
let touchpad = open_device(&mut scheme, 2);
let mut keyboard_props = [0u8; 64];
let mut mouse_props = [0u8; 64];
let mut touchpad_props = [0u8; 64];
let keyboard_len = scheme
.fcntl(
keyboard,
EVIOCGPROP as usize,
keyboard_props.as_mut_ptr() as usize,
)
.expect("keyboard props ioctl should succeed")
.expect("keyboard props length");
let mouse_len = scheme
.fcntl(
mouse,
EVIOCGPROP as usize,
mouse_props.as_mut_ptr() as usize,
)
.expect("mouse props ioctl should succeed")
.expect("mouse props length");
let touchpad_len = scheme
.fcntl(
touchpad,
EVIOCGPROP as usize,
touchpad_props.as_mut_ptr() as usize,
)
.expect("touchpad props ioctl should succeed")
.expect("touchpad props length");
let pointer_mask = 1u8 << INPUT_PROP_POINTER;
assert_eq!(keyboard_len, 64);
assert_eq!(mouse_len, 64);
assert_eq!(touchpad_len, 64);
assert_eq!(keyboard_props[0] & pointer_mask, 0);
assert_ne!(mouse_props[0] & pointer_mask, 0);
assert_ne!(touchpad_props[0] & pointer_mask, 0);
}
#[test]
fn eviocsabs_overrides_default_abs_info() {
let mut scheme = EvdevScheme::new();
let touchpad = open_device(&mut scheme, 2);
let abs_info = AbsInfo {
value: 7,
minimum: -10,
maximum: 1234,
fuzz: 2,
flat: 3,
resolution: 4,
};
let mut reported = AbsInfo::default();
scheme
.fcntl(
touchpad,
EVIOCSABS as usize,
(&abs_info as *const AbsInfo) as usize,
)
.expect("set abs info should succeed");
scheme
.fcntl(
touchpad,
crate::types::eviocgabs(crate::types::ABS_X as u8) as usize,
(&mut reported as *mut AbsInfo) as usize,
)
.expect("get abs info should succeed");
assert_eq!(reported.value, abs_info.value);
assert_eq!(reported.minimum, abs_info.minimum);
assert_eq!(reported.maximum, abs_info.maximum);
assert_eq!(reported.fuzz, abs_info.fuzz);
assert_eq!(reported.flat, abs_info.flat);
assert_eq!(reported.resolution, abs_info.resolution);
}
#[test]
fn multitouch_slot_abs_info_reports_nine_slots() {
let mut scheme = EvdevScheme::new();
let touchpad = open_device(&mut scheme, 2);
let mut reported = AbsInfo::default();
scheme
.fcntl(
touchpad,
crate::types::eviocgabs(ABS_MT_SLOT as u8) as usize,
(&mut reported as *mut AbsInfo) as usize,
)
.expect("get mt slot abs info should succeed");
assert_eq!(reported.minimum, 0);
assert_eq!(reported.maximum, 9);
}
#[test]
fn force_feedback_ioctls_return_enotty() {
let mut scheme = EvdevScheme::new();
let mouse = open_device(&mut scheme, 1);
for cmd in [
EVIOCSFF as usize,
EVIOCRMFF as usize,
EVIOCGEFFECTS as usize,
] {
let err = scheme
.fcntl(mouse, cmd, 0)
.expect_err("force feedback ioctl should fail");
assert_eq!(err.errno, syscall::error::ENOTTY);
}
}
}
@@ -1,35 +1,226 @@
use crate::types::*;
fn orb_key_to_evdev(orb_key: u8) -> Option<u16> {
let mapped = match orb_key {
b'1'..=b'9' => KEY_1 + (orb_key - b'1') as u16,
b'0' => KEY_0,
b'a'..=b'z' => KEY_A + (orb_key - b'a') as u16,
b'\n' | b'\r' => KEY_ENTER,
b'\t' => KEY_TAB,
b' ' => KEY_SPACE,
b'\x08' => KEY_BACKSPACE,
b'\x1b' => KEY_ESC,
b'-' => KEY_MINUS,
b'=' => KEY_EQUAL,
b'[' => KEY_LEFTBRACE,
b']' => KEY_RIGHTBRACE,
b'\\' => KEY_BACKSLASH,
b';' => KEY_SEMICOLON,
b'\'' => KEY_APOSTROPHE,
b'`' => KEY_GRAVE,
b',' => KEY_COMMA,
b'.' => KEY_DOT,
b'/' => KEY_SLASH,
pub const KEYBOARD_KEY_CODES: &[u16] = &[
KEY_ESC,
KEY_1,
KEY_2,
KEY_3,
KEY_4,
KEY_5,
KEY_6,
KEY_7,
KEY_8,
KEY_9,
KEY_0,
KEY_MINUS,
KEY_EQUAL,
KEY_BACKSPACE,
KEY_TAB,
KEY_Q,
KEY_W,
KEY_E,
KEY_R,
KEY_T,
KEY_Y,
KEY_U,
KEY_I,
KEY_O,
KEY_P,
KEY_LEFTBRACE,
KEY_RIGHTBRACE,
KEY_ENTER,
KEY_LEFTCTRL,
KEY_A,
KEY_S,
KEY_D,
KEY_F,
KEY_G,
KEY_H,
KEY_J,
KEY_K,
KEY_L,
KEY_SEMICOLON,
KEY_APOSTROPHE,
KEY_GRAVE,
KEY_LEFTSHIFT,
KEY_BACKSLASH,
KEY_Z,
KEY_X,
KEY_C,
KEY_V,
KEY_B,
KEY_N,
KEY_M,
KEY_COMMA,
KEY_DOT,
KEY_SLASH,
KEY_RIGHTSHIFT,
KEY_KPASTERISK,
KEY_LEFTALT,
KEY_SPACE,
KEY_CAPSLOCK,
KEY_F1,
KEY_F2,
KEY_F3,
KEY_F4,
KEY_F5,
KEY_F6,
KEY_F7,
KEY_F8,
KEY_F9,
KEY_F10,
KEY_NUMLOCK,
KEY_SCROLLLOCK,
KEY_KP7,
KEY_KP8,
KEY_KP9,
KEY_KPMINUS,
KEY_KP4,
KEY_KP5,
KEY_KP6,
KEY_KPPLUS,
KEY_KP1,
KEY_KP2,
KEY_KP3,
KEY_KP0,
KEY_KPDOT,
KEY_F11,
KEY_F12,
KEY_KPENTER,
KEY_RIGHTCTRL,
KEY_KPSLASH,
KEY_RIGHTALT,
KEY_HOME,
KEY_UP,
KEY_PAGEUP,
KEY_LEFT,
KEY_RIGHT,
KEY_END,
KEY_DOWN,
KEY_PAGEDOWN,
KEY_INSERT,
KEY_DELETE,
KEY_LEFTMETA,
KEY_RIGHTMETA,
KEY_MENU,
];
pub const MOUSE_BUTTON_CODES: &[u16] = &[BTN_LEFT, BTN_RIGHT, BTN_MIDDLE];
pub const TOUCHPAD_KEY_CODES: &[u16] = &[BTN_TOUCH, BTN_TOOL_FINGER];
fn orb_key_to_evdev(scancode: u8) -> Option<u16> {
Some(match scancode {
0x01 => KEY_ESC,
0x02 => KEY_1,
0x03 => KEY_2,
0x04 => KEY_3,
0x05 => KEY_4,
0x06 => KEY_5,
0x07 => KEY_6,
0x08 => KEY_7,
0x09 => KEY_8,
0x0A => KEY_9,
0x0B => KEY_0,
0x0C => KEY_MINUS,
0x0D => KEY_EQUAL,
0x0E => KEY_BACKSPACE,
0x0F => KEY_TAB,
0x10 => KEY_Q,
0x11 => KEY_W,
0x12 => KEY_E,
0x13 => KEY_R,
0x14 => KEY_T,
0x15 => KEY_Y,
0x16 => KEY_U,
0x17 => KEY_I,
0x18 => KEY_O,
0x19 => KEY_P,
0x1A => KEY_LEFTBRACE,
0x1B => KEY_RIGHTBRACE,
0x1C => KEY_ENTER,
0x1D => KEY_LEFTCTRL,
0x1E => KEY_A,
0x1F => KEY_S,
0x20 => KEY_D,
0x21 => KEY_F,
0x22 => KEY_G,
0x23 => KEY_H,
0x24 => KEY_J,
0x25 => KEY_K,
0x26 => KEY_L,
0x27 => KEY_SEMICOLON,
0x28 => KEY_APOSTROPHE,
0x29 => KEY_GRAVE,
0x2A => KEY_LEFTSHIFT,
0x2B => KEY_BACKSLASH,
0x2C => KEY_Z,
0x2D => KEY_X,
0x2E => KEY_C,
0x2F => KEY_V,
0x30 => KEY_B,
0x31 => KEY_N,
0x32 => KEY_M,
0x33 => KEY_COMMA,
0x34 => KEY_DOT,
0x35 => KEY_SLASH,
0x36 => KEY_RIGHTSHIFT,
0x37 => KEY_KPASTERISK,
0x38 => KEY_LEFTALT,
0x39 => KEY_SPACE,
0x3A => KEY_CAPSLOCK,
0x3B => KEY_F1,
0x3C => KEY_F2,
0x3D => KEY_F3,
0x3E => KEY_F4,
0x3F => KEY_F5,
0x40 => KEY_F6,
0x41 => KEY_F7,
0x42 => KEY_F8,
0x43 => KEY_F9,
0x44 => KEY_F10,
0x45 => KEY_NUMLOCK,
0x46 => KEY_SCROLLLOCK,
0x47 => KEY_HOME,
0x48 => KEY_UP,
0x49 => KEY_PAGEUP,
0x4B => KEY_LEFT,
0x4D => KEY_RIGHT,
0x4F => KEY_END,
0x50 => KEY_DOWN,
0x51 => KEY_PAGEDOWN,
0x52 => KEY_INSERT,
0x53 => KEY_DELETE,
0x57 => KEY_F11,
0x58 => KEY_F12,
0x5B => KEY_LEFTMETA,
0x5C => KEY_RIGHTMETA,
0x5D => KEY_MENU,
0x64 => KEY_RIGHTCTRL,
0x70 => KEY_KP0,
0x71 => KEY_KP1,
0x72 => KEY_KP2,
0x73 => KEY_KP3,
0x74 => KEY_KP4,
0x75 => KEY_KP5,
0x76 => KEY_KP6,
0x77 => KEY_KP7,
0x78 => KEY_KP8,
0x79 => KEY_KP9,
0x7A => KEY_KPDOT,
0x7B => KEY_KPMINUS,
0x7C => KEY_KPPLUS,
0x7D => KEY_KPASTERISK,
0x7E => KEY_KPSLASH,
0x7F => KEY_KPENTER,
_ => return None,
};
Some(mapped)
})
}
pub fn translate_keyboard(orb_key: u8, pressed: bool) -> Vec<InputEvent> {
pub fn translate_keyboard(scancode: u8, pressed: bool) -> Vec<InputEvent> {
let value = if pressed { 1 } else { 0 };
match orb_key_to_evdev(orb_key) {
match orb_key_to_evdev(scancode) {
Some(code) => vec![
InputEvent::new(EV_MSC, MSC_SCAN, i32::from(scancode)),
InputEvent::new(EV_KEY, code, value),
InputEvent::syn_report(),
],
@@ -37,19 +228,32 @@ pub fn translate_keyboard(orb_key: u8, pressed: bool) -> Vec<InputEvent> {
}
}
pub fn translate_mouse_dx(dx: i32) -> Vec<InputEvent> {
vec![InputEvent::new(EV_REL, REL_X, dx), InputEvent::syn_report()]
pub fn translate_mouse_motion(dx: i32, dy: i32) -> Vec<InputEvent> {
let mut events = Vec::new();
if dx != 0 {
events.push(InputEvent::new(EV_REL, REL_X, dx));
}
if dy != 0 {
events.push(InputEvent::new(EV_REL, REL_Y, dy));
}
if !events.is_empty() {
events.push(InputEvent::syn_report());
}
events
}
pub fn translate_mouse_dy(dy: i32) -> Vec<InputEvent> {
vec![InputEvent::new(EV_REL, REL_Y, dy), InputEvent::syn_report()]
}
pub fn translate_mouse_scroll(y: i32) -> Vec<InputEvent> {
vec![
InputEvent::new(EV_REL, REL_WHEEL, y),
InputEvent::syn_report(),
]
pub fn translate_mouse_scroll(x: i32, y: i32) -> Vec<InputEvent> {
let mut events = Vec::new();
if x != 0 {
events.push(InputEvent::new(EV_REL, REL_HWHEEL, x));
}
if y != 0 {
events.push(InputEvent::new(EV_REL, REL_WHEEL, y));
}
if !events.is_empty() {
events.push(InputEvent::syn_report());
}
events
}
pub fn translate_mouse_button(button: usize, pressed: bool) -> Vec<InputEvent> {
@@ -68,10 +272,206 @@ pub fn translate_mouse_button(button: usize, pressed: bool) -> Vec<InputEvent> {
]
}
pub fn translate_touch(x: i32, y: i32, touching: bool) -> Vec<InputEvent> {
let btn = InputEvent::new(EV_KEY, BTN_TOUCH, if touching { 1 } else { 0 });
let abs_x = InputEvent::new(EV_ABS, ABS_X, x);
let abs_y = InputEvent::new(EV_ABS, ABS_Y, y);
let syn = InputEvent::syn_report();
vec![btn, abs_x, abs_y, syn]
pub fn translate_touchpad_motion(
x: i32,
y: i32,
touching: bool,
tracking_id: i32,
) -> Vec<InputEvent> {
let mut events = vec![
InputEvent::new(EV_ABS, ABS_X, x),
InputEvent::new(EV_ABS, ABS_Y, y),
];
if touching {
events.extend_from_slice(&[
InputEvent::new(EV_ABS, ABS_MT_SLOT, 0),
InputEvent::new(EV_ABS, ABS_MT_TRACKING_ID, tracking_id),
InputEvent::new(EV_ABS, ABS_MT_POSITION_X, x),
InputEvent::new(EV_ABS, ABS_MT_POSITION_Y, y),
InputEvent::new(EV_ABS, ABS_PRESSURE, 255),
InputEvent::new(EV_ABS, ABS_MT_TOUCH_MAJOR, 1),
]);
}
events.push(InputEvent::syn_report());
events
}
pub fn translate_touchpad_contact(
x: i32,
y: i32,
touching: bool,
tracking_id: i32,
) -> Vec<InputEvent> {
let mut events = vec![
InputEvent::new(EV_ABS, ABS_X, x),
InputEvent::new(EV_ABS, ABS_Y, y),
InputEvent::new(EV_ABS, ABS_MT_SLOT, 0),
];
if touching {
events.extend_from_slice(&[
InputEvent::new(EV_KEY, BTN_TOUCH, 1),
InputEvent::new(EV_KEY, BTN_TOOL_FINGER, 1),
InputEvent::new(EV_ABS, ABS_MT_TRACKING_ID, tracking_id),
InputEvent::new(EV_ABS, ABS_MT_POSITION_X, x),
InputEvent::new(EV_ABS, ABS_MT_POSITION_Y, y),
InputEvent::new(EV_ABS, ABS_PRESSURE, 255),
InputEvent::new(EV_ABS, ABS_MT_TOUCH_MAJOR, 1),
]);
} else {
events.extend_from_slice(&[
InputEvent::new(EV_ABS, ABS_MT_TRACKING_ID, -1),
InputEvent::new(EV_ABS, ABS_PRESSURE, 0),
InputEvent::new(EV_ABS, ABS_MT_TOUCH_MAJOR, 0),
InputEvent::new(EV_KEY, BTN_TOUCH, 0),
InputEvent::new(EV_KEY, BTN_TOOL_FINGER, 0),
]);
}
events.push(InputEvent::syn_report());
events
}
#[cfg(test)]
mod tests {
use super::{
translate_keyboard, translate_mouse_button, translate_mouse_motion, translate_mouse_scroll,
translate_touchpad_motion,
};
use crate::types::*;
fn has_event(events: &[InputEvent], event_type: u16, code: u16, value: i32) -> bool {
events.iter().any(|event| {
event.event_type == event_type && event.code == code && event.value == value
})
}
fn has_event_code(events: &[InputEvent], event_type: u16, code: u16) -> bool {
events
.iter()
.any(|event| event.event_type == event_type && event.code == code)
}
fn event_index(events: &[InputEvent], event_type: u16, code: u16, value: i32) -> Option<usize> {
events.iter().position(|event| {
event.event_type == event_type && event.code == code && event.value == value
})
}
#[test]
fn keyboard_press_translates_to_key_a_down() {
let events = translate_keyboard(0x1E, true);
assert!(has_event(&events, EV_KEY, KEY_A, 1));
}
#[test]
fn keyboard_release_translates_to_key_a_up() {
let events = translate_keyboard(0x1E, false);
assert!(has_event(&events, EV_KEY, KEY_A, 0));
}
#[test]
fn keyboard_events_include_scan_before_key() {
let events = translate_keyboard(0x1E, true);
let scan_index = event_index(&events, EV_MSC, MSC_SCAN, 0x1E).unwrap();
let key_index = event_index(&events, EV_KEY, KEY_A, 1).unwrap();
assert!(scan_index < key_index);
}
#[test]
fn keyboard_events_end_with_syn_report() {
let events = translate_keyboard(0x1E, true);
let last = events
.last()
.expect("keyboard translation should emit events");
assert_eq!(last.event_type, EV_SYN);
assert_eq!(last.code, SYN_REPORT);
assert_eq!(last.value, 0);
}
#[test]
fn unknown_keyboard_scancode_returns_empty_events() {
let events = translate_keyboard(0xFF, true);
assert!(events.is_empty());
}
#[test]
fn mouse_motion_x_only_emits_x_and_syn() {
let events = translate_mouse_motion(10, 0);
assert!(has_event(&events, EV_REL, REL_X, 10));
assert!(!has_event_code(&events, EV_REL, REL_Y));
assert_eq!(
events
.last()
.map(|event| (event.event_type, event.code, event.value)),
Some((EV_SYN, SYN_REPORT, 0))
);
}
#[test]
fn mouse_motion_x_and_y_emits_both_axes() {
let events = translate_mouse_motion(5, -3);
assert!(has_event(&events, EV_REL, REL_X, 5));
assert!(has_event(&events, EV_REL, REL_Y, -3));
}
#[test]
fn mouse_motion_zero_returns_empty_events() {
let events = translate_mouse_motion(0, 0);
assert!(events.is_empty());
}
#[test]
fn mouse_scroll_up_emits_vertical_wheel() {
let events = translate_mouse_scroll(0, 1);
assert!(has_event(&events, EV_REL, REL_WHEEL, 1));
}
#[test]
fn mouse_scroll_horizontal_emits_horizontal_wheel() {
let events = translate_mouse_scroll(2, 0);
assert!(has_event(&events, EV_REL, REL_HWHEEL, 2));
}
#[test]
fn mouse_button_left_press_emits_btn_left_down() {
let events = translate_mouse_button(0, true);
assert!(has_event(&events, EV_KEY, BTN_LEFT, 1));
}
#[test]
fn mouse_button_right_release_emits_btn_right_up() {
let events = translate_mouse_button(2, false);
assert!(has_event(&events, EV_KEY, BTN_RIGHT, 0));
}
#[test]
fn unknown_mouse_button_returns_empty_events() {
let events = translate_mouse_button(10, true);
assert!(events.is_empty());
}
#[test]
fn touchpad_motion_emits_absolute_contact_details() {
let events = translate_touchpad_motion(100, 200, true, 1);
assert!(has_event(&events, EV_ABS, ABS_X, 100));
assert!(has_event(&events, EV_ABS, ABS_Y, 200));
assert!(has_event(&events, EV_ABS, ABS_MT_TRACKING_ID, 1));
}
}
@@ -1,7 +1,10 @@
#![allow(dead_code)]
/// Linux-compatible evdev event types and constants.
///
/// These mirror the Linux kernel's `include/uapi/linux/input.h` definitions
/// so that clients expecting evdev semantics can work on Redox.
use std::mem::size_of;
// Event types
pub const EV_SYN: u16 = 0x00;
@@ -9,14 +12,51 @@ pub const EV_KEY: u16 = 0x01;
pub const EV_REL: u16 = 0x02;
pub const EV_ABS: u16 = 0x03;
pub const EV_MSC: u16 = 0x04;
pub const EV_SW: u16 = 0x05;
pub const EV_LED: u16 = 0x11;
pub const EV_SND: u16 = 0x12;
pub const EV_REP: u16 = 0x14;
pub const EV_FF: u16 = 0x15;
// Synchronization events
pub const SYN_REPORT: u16 = 0;
pub const SYN_CONFIG: u16 = 1;
// Misc events
pub const MSC_SCAN: u16 = 0x04;
// Switch events
pub const SW_LID: u16 = 0x00;
pub const SW_TABLET_MODE: u16 = 0x01;
pub const SW_HEADPHONE_INSERT: u16 = 0x02;
pub const SW_RFKILL_ALL: u16 = 0x03;
pub const SW_MICROPHONE_INSERT: u16 = 0x04;
pub const SW_DOCK: u16 = 0x05;
pub const SW_LINEOUT_INSERT: u16 = 0x06;
pub const SW_JACK_PHYSICAL_INSERT: u16 = 0x07;
pub const SW_VIDEOOUT_INSERT: u16 = 0x08;
pub const SW_CAMERA_LENS_COVER: u16 = 0x09;
pub const SW_KEYPAD_SLIDE: u16 = 0x0a;
pub const SW_FRONT_PROXIMITY: u16 = 0x0b;
pub const SW_ROTATE_LOCK: u16 = 0x0c;
pub const SW_LINEIN_INSERT: u16 = 0x0d;
pub const SW_MUTE_DEVICE: u16 = 0x0e;
pub const SW_PEN_INSERTED: u16 = 0x0f;
pub const SW_MACHINE_COVER: u16 = 0x10;
// Input properties
pub const INPUT_PROP_POINTER: u16 = 0x00;
pub const INPUT_PROP_DIRECT: u16 = 0x01;
// LEDs
pub const LED_NUML: u16 = 0x00;
pub const LED_CAPSL: u16 = 0x01;
pub const LED_SCROLLL: u16 = 0x02;
// Repeat settings
pub const REP_DELAY: u16 = 0x00;
pub const REP_PERIOD: u16 = 0x01;
// Relative axes
pub const REL_X: u16 = 0x00;
pub const REL_Y: u16 = 0x01;
@@ -107,8 +147,22 @@ pub const KEY_F9: u16 = 67;
pub const KEY_F10: u16 = 68;
pub const KEY_NUMLOCK: u16 = 69;
pub const KEY_SCROLLLOCK: u16 = 70;
pub const KEY_KP7: u16 = 71;
pub const KEY_KP8: u16 = 72;
pub const KEY_KP9: u16 = 73;
pub const KEY_KPMINUS: u16 = 74;
pub const KEY_KP4: u16 = 75;
pub const KEY_KP5: u16 = 76;
pub const KEY_KP6: u16 = 77;
pub const KEY_KPPLUS: u16 = 78;
pub const KEY_KP1: u16 = 79;
pub const KEY_KP2: u16 = 80;
pub const KEY_KP3: u16 = 81;
pub const KEY_KP0: u16 = 82;
pub const KEY_KPDOT: u16 = 83;
pub const KEY_F11: u16 = 87;
pub const KEY_F12: u16 = 88;
pub const KEY_KPENTER: u16 = 96;
pub const KEY_HOME: u16 = 102;
pub const KEY_UP: u16 = 103;
@@ -120,6 +174,8 @@ pub const KEY_DOWN: u16 = 108;
pub const KEY_PAGEDOWN: u16 = 109;
pub const KEY_INSERT: u16 = 110;
pub const KEY_DELETE: u16 = 111;
pub const KEY_KPSLASH: u16 = 98;
pub const KEY_MENU: u16 = 139;
pub const KEY_LEFTMETA: u16 = 125;
pub const KEY_RIGHTMETA: u16 = 126;
@@ -145,6 +201,95 @@ pub const BUS_VIRTUAL: u16 = 0x06;
// Evdev version
pub const EV_VERSION: i32 = 0x010001;
// ioctl constants
pub const EVIOCGVERSION: u64 = 0x80044501;
pub const EVIOCGID: u64 = 0x80084502;
pub const EVIOCGNAME: u64 = 0x80000000 | 0x45 << 8;
pub const EVIOCGBIT: u64 = 0x80000000 | 0x45 << 8;
pub const EVIOCGABS: u64 = 0x80184540;
pub const EVIOCSABS: u64 = 0x401845c0;
pub const EVIOCGRAB: u64 = 0x40044590;
pub const EVIOCSCLOCKID: u64 = 0x400445a0;
pub const EVIOCGPROP: u64 = 0x804045a0;
pub const EVIOCSFF: u64 = 0x402c4580;
pub const EVIOCRMFF: u64 = 0x40044581;
pub const EVIOCGEFFECTS: u64 = 0x80044584;
// EVIOCGKEY returns the current state of all keys (bitmask of pressed keys)
pub const EVIOCGKEY: u64 = (IOC_READ << IOC_DIRSHIFT)
| ((KEY_MAX / 8 + 1) as u64) << IOC_SIZESHIFT
| (EVDEV_IOCTL_TYPE << IOC_TYPESHIFT)
| 0x18;
// EVIOCGLED returns the current state of all LEDs (bitmask of lit LEDs)
pub const EVIOCGLED: u64 = (IOC_READ << IOC_DIRSHIFT)
| ((LED_MAX / 8 + 1) as u64) << IOC_SIZESHIFT
| (EVDEV_IOCTL_TYPE << IOC_TYPESHIFT)
| 0x19;
// Key and LED state bitmaps
pub const KEY_MAX: usize = 0x2FF; // 767 bits = 96 bytes
pub const LED_MAX: usize = 0x0F; // 15 bits = 2 bytes
pub const IOC_NRBITS: u64 = 8;
pub const IOC_TYPEBITS: u64 = 8;
pub const IOC_SIZEBITS: u64 = 14;
pub const IOC_NRMASK: u64 = (1 << IOC_NRBITS) - 1;
pub const IOC_TYPEMASK: u64 = (1 << IOC_TYPEBITS) - 1;
pub const IOC_SIZEMASK: u64 = (1 << IOC_SIZEBITS) - 1;
pub const IOC_NRSHIFT: u64 = 0;
pub const IOC_TYPESHIFT: u64 = IOC_NRSHIFT + IOC_NRBITS;
pub const IOC_SIZESHIFT: u64 = IOC_TYPESHIFT + IOC_TYPEBITS;
pub const IOC_DIRSHIFT: u64 = IOC_SIZESHIFT + IOC_SIZEBITS;
pub const IOC_NONE: u64 = 0;
pub const IOC_WRITE: u64 = 1;
pub const IOC_READ: u64 = 2;
pub const EVDEV_IOCTL_TYPE: u64 = 0x45;
pub const fn ioc_dir(cmd: u64) -> u64 {
cmd >> IOC_DIRSHIFT
}
pub const fn ioc_type(cmd: u64) -> u64 {
(cmd >> IOC_TYPESHIFT) & IOC_TYPEMASK
}
pub const fn ioc_nr(cmd: u64) -> u64 {
(cmd >> IOC_NRSHIFT) & IOC_NRMASK
}
pub const fn ioc_size(cmd: u64) -> usize {
((cmd >> IOC_SIZESHIFT) & IOC_SIZEMASK) as usize
}
pub const fn is_evdev_ioctl(cmd: u64) -> bool {
ioc_type(cmd) == EVDEV_IOCTL_TYPE
}
pub const fn eviocgname(len: usize) -> u64 {
(IOC_READ << IOC_DIRSHIFT)
| ((len as u64) << IOC_SIZESHIFT)
| (EVDEV_IOCTL_TYPE << IOC_TYPESHIFT)
| 0x06
}
pub const fn eviocgbit(ev: u8, len: usize) -> u64 {
(IOC_READ << IOC_DIRSHIFT)
| ((len as u64) << IOC_SIZESHIFT)
| (EVDEV_IOCTL_TYPE << IOC_TYPESHIFT)
| ((0x20 + ev as u64) << IOC_NRSHIFT)
}
pub const fn eviocgabs(axis: u8) -> u64 {
(IOC_READ << IOC_DIRSHIFT)
| ((size_of::<AbsInfo>() as u64) << IOC_SIZESHIFT)
| (EVDEV_IOCTL_TYPE << IOC_TYPESHIFT)
| ((0x40 + axis as u64) << IOC_NRSHIFT)
}
/// Linux `struct input_event` layout (24 bytes).
///
/// Matches the kernel binary layout:
@@ -203,6 +348,17 @@ pub struct InputId {
pub version: u16,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
pub struct AbsInfo {
pub value: i32,
pub minimum: i32,
pub maximum: i32,
pub fuzz: i32,
pub flat: i32,
pub resolution: i32,
}
fn now_timestamp() -> (u64, u64) {
use std::time::SystemTime;
let dur = SystemTime::now()
@@ -4,21 +4,7 @@
path = "source"
[build]
template = "custom"
script = """
# Build the firmware-loader daemon
COOKBOOK_CARGO_PATH=. cookbook_cargo
# Stage firmware blobs (copied by integrate-redbear.sh from local/firmware/amdgpu/)
if [ -d "${COOKBOOK_SOURCE}/firmware/amdgpu" ]; then
AMD_FW_COUNT=$(ls "${COOKBOOK_SOURCE}/firmware/amdgpu/"*.bin 2>/dev/null | wc -l)
if [ "${AMD_FW_COUNT}" -gt 0 ]; then
mkdir -p "${COOKBOOK_STAGE}/usr/lib/firmware/amdgpu"
cp "${COOKBOOK_SOURCE}/firmware/amdgpu/"*.bin "${COOKBOOK_STAGE}/usr/lib/firmware/amdgpu/"
echo "Staged ${AMD_FW_COUNT} AMD firmware blobs"
fi
fi
"""
template = "cargo"
[package.files]
"/usr/lib/drivers/firmware-loader" = "firmware-loader"
"/usr/bin/firmware-loader" = "firmware-loader"
@@ -4,8 +4,9 @@ version = "0.1.0"
edition = "2021"
[dependencies]
redox_syscall = { version = "0.7", features = ["std"] }
syscall04 = { package = "redox_syscall", version = "0.4" }
redox_scheme = { package = "redox-scheme", version = "0.1" }
libc = "0.2"
syscall = { package = "redox_syscall", version = "0.7", features = ["std"] }
redox_scheme = { package = "redox-scheme", version = "0.11" }
libredox = "0.1"
log = { version = "0.4", features = ["std"] }
thiserror = "2"
@@ -103,6 +103,14 @@ impl FirmwareRegistry {
Ok(data)
}
pub fn len(&self) -> usize {
self.blobs.len()
}
pub fn is_empty(&self) -> bool {
self.blobs.is_empty()
}
#[allow(dead_code)]
pub fn list_keys(&self) -> Vec<&str> {
self.blobs.keys().map(|s| s.as_str()).collect()
@@ -2,11 +2,12 @@ mod blob;
mod scheme;
use std::env;
use std::os::fd::{AsRawFd, FromRawFd, RawFd};
use std::path::PathBuf;
use std::process;
use log::{error, info, LevelFilter, Metadata, Record};
use redox_scheme::{SignalBehavior, Socket};
use redox_scheme::{scheme::SchemeSync, SignalBehavior, Socket};
use blob::FirmwareRegistry;
use scheme::FirmwareScheme;
@@ -40,52 +41,59 @@ fn default_firmware_dir() -> PathBuf {
PathBuf::from("/usr/firmware/")
}
fn run() -> Result<(), String> {
let firmware_dir = env::var("FIRMWARE_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| default_firmware_dir());
unsafe fn get_init_notify_fd() -> RawFd {
let fd: RawFd = env::var("INIT_NOTIFY")
.expect("firmware-loader: INIT_NOTIFY not set")
.parse()
.expect("firmware-loader: INIT_NOTIFY is not a valid fd");
libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC);
fd
}
info!(
"firmware-loader: starting with directory {}",
firmware_dir.display()
);
fn notify_scheme_ready(notify_fd: RawFd, socket: &Socket, scheme: &mut FirmwareScheme) {
let cap_id = scheme
.scheme_root()
.expect("firmware-loader: scheme_root failed");
let cap_fd = socket
.create_this_scheme_fd(0, cap_id, 0, 0)
.expect("firmware-loader: create_this_scheme_fd failed");
let registry = FirmwareRegistry::new(&firmware_dir)
.map_err(|e| format!("failed to initialize firmware registry: {e}"))?;
syscall::call_wo(
notify_fd as usize,
&libredox::Fd::new(cap_fd).into_raw().to_ne_bytes(),
syscall::CallFlags::FD,
&[],
)
.expect("firmware-loader: failed to notify init that scheme is ready");
}
fn run_daemon(notify_fd: RawFd, registry: FirmwareRegistry) -> ! {
let socket = Socket::create().expect("firmware-loader: failed to create scheme socket");
let mut scheme = FirmwareScheme::new(registry);
notify_scheme_ready(notify_fd, &socket, &mut scheme);
let socket = Socket::create("firmware")
.map_err(|e| format!("failed to register firmware scheme: {e}"))?;
info!("firmware-loader: registered scheme:firmware");
let mut firmware_scheme = FirmwareScheme::new(registry);
libredox::call::setrens(0, 0).expect("firmware-loader: failed to enter null namespace");
loop {
let request = match socket.next_request(SignalBehavior::Restart) {
Ok(Some(request)) => request,
Ok(None) => {
info!("firmware-loader: scheme unmounted, exiting");
break;
while let Some(request) = socket
.next_request(SignalBehavior::Restart)
.expect("firmware-loader: failed to read scheme request")
{
match request.kind() {
redox_scheme::RequestKind::Call(request) => {
let mut state = redox_scheme::scheme::SchemeState::new();
let response = request.handle_sync(&mut scheme, &mut state);
socket
.write_response(response, SignalBehavior::Restart)
.expect("firmware-loader: failed to write response");
}
Err(e) => {
error!("firmware-loader: failed to read scheme request: {}", e);
continue;
}
};
let response = match request.handle_scheme_block_mut(&mut firmware_scheme) {
Ok(response) => response,
Err(_request) => {
error!("firmware-loader: failed to handle request");
continue;
}
};
if let Err(e) = socket.write_response(response, SignalBehavior::Restart) {
error!("firmware-loader: failed to write response: {}", e);
_ => (),
}
}
Ok(())
process::exit(0);
}
fn main() {
@@ -99,8 +107,26 @@ fn main() {
init_logging(log_level);
if let Err(e) = run() {
error!("firmware-loader: fatal error: {}", e);
let firmware_dir = env::var("FIRMWARE_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| default_firmware_dir());
info!(
"firmware-loader: starting with directory {}",
firmware_dir.display()
);
let registry = FirmwareRegistry::new(&firmware_dir).unwrap_or_else(|e| {
error!("firmware-loader: fatal error: failed to initialize firmware registry: {e}");
process::exit(1);
}
});
info!(
"firmware-loader: indexed {} firmware blob(s) from {}",
registry.len(),
firmware_dir.display()
);
let notify_fd = unsafe { get_init_notify_fd() };
run_daemon(notify_fd, registry);
}
@@ -2,17 +2,19 @@ use std::collections::BTreeMap;
use std::sync::Arc;
use log::warn;
use redox_scheme::SchemeBlockMut;
use syscall04::data::Stat;
use syscall04::error::{Error, Result, EBADF, EINVAL, EISDIR, ENOENT, EROFS};
use syscall04::flag::{EventFlags, MapFlags, MunmapFlags, MODE_FILE, SEEK_CUR, SEEK_END, SEEK_SET};
use redox_scheme::scheme::SchemeSync;
use redox_scheme::{CallerCtx, OpenResult};
use syscall::error::*;
use syscall::schemev2::NewFdFlags;
use syscall::{EventFlags, Stat, MODE_FILE};
use crate::blob::FirmwareRegistry;
const SCHEME_ROOT_ID: usize = 1;
struct Handle {
blob_key: String,
data: Arc<Vec<u8>>,
offset: u64,
map_count: usize,
closed: bool,
}
@@ -27,10 +29,18 @@ impl FirmwareScheme {
pub fn new(registry: FirmwareRegistry) -> Self {
FirmwareScheme {
registry,
next_id: 0,
next_id: SCHEME_ROOT_ID + 1,
handles: BTreeMap::new(),
}
}
fn handle(&self, id: usize) -> Result<&Handle> {
self.handles.get(&id).ok_or(Error::new(EBADF))
}
fn handle_mut(&mut self, id: usize) -> Result<&mut Handle> {
self.handles.get_mut(&id).ok_or(Error::new(EBADF))
}
}
fn resolve_key(path: &str) -> Option<String> {
@@ -65,8 +75,23 @@ fn resolve_key(path: &str) -> Option<String> {
Some(key)
}
impl SchemeBlockMut for FirmwareScheme {
fn open(&mut self, path: &str, _flags: usize, _uid: u32, _gid: u32) -> Result<Option<usize>> {
impl SchemeSync for FirmwareScheme {
fn scheme_root(&mut self) -> Result<usize> {
Ok(SCHEME_ROOT_ID)
}
fn openat(
&mut self,
dirfd: usize,
path: &str,
_flags: usize,
_fcntl_flags: u32,
_ctx: &CallerCtx,
) -> Result<OpenResult> {
if dirfd != SCHEME_ROOT_ID {
return Err(Error::new(EACCES));
}
let key = resolve_key(path).ok_or(Error::new(EISDIR))?;
if !self.registry.contains(&key) {
@@ -87,94 +112,95 @@ impl SchemeBlockMut for FirmwareScheme {
Handle {
blob_key: key,
data,
offset: 0,
map_count: 0,
closed: false,
},
);
Ok(Some(id))
Ok(OpenResult::ThisScheme {
number: id,
flags: NewFdFlags::empty(),
})
}
fn seek(&mut self, id: usize, pos: isize, whence: usize) -> Result<Option<isize>> {
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
let len = handle.data.len() as i64;
let new_offset = match whence {
SEEK_SET => pos as i64,
SEEK_CUR => handle.offset as i64 + pos as i64,
SEEK_END => len + pos as i64,
_ => return Err(Error::new(EINVAL)),
};
if new_offset < 0 {
return Err(Error::new(EINVAL));
}
handle.offset = new_offset as u64;
let new_offset = isize::try_from(new_offset).map_err(|_| Error::new(EINVAL))?;
Ok(Some(new_offset))
}
fn read(&mut self, id: usize, buf: &mut [u8]) -> Result<Option<usize>> {
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
let offset = handle.offset as usize;
fn read(
&mut self,
id: usize,
buf: &mut [u8],
offset: u64,
_flags: u32,
_ctx: &CallerCtx,
) -> Result<usize> {
let handle = self.handle(id)?;
let offset = usize::try_from(offset).map_err(|_| Error::new(EINVAL))?;
let data = &handle.data;
if offset >= data.len() {
return Ok(Some(0));
return Ok(0);
}
let available = data.len() - offset;
let to_copy = available.min(buf.len());
buf[..to_copy].copy_from_slice(&data[offset..offset + to_copy]);
handle.offset += to_copy as u64;
Ok(Some(to_copy))
Ok(to_copy)
}
fn write(&mut self, id: usize, _buf: &[u8]) -> Result<Option<usize>> {
let _ = self.handles.get(&id).ok_or(Error::new(EBADF))?;
fn write(
&mut self,
id: usize,
_buf: &[u8],
_offset: u64,
_flags: u32,
_ctx: &CallerCtx,
) -> Result<usize> {
let _ = self.handle(id)?;
Err(Error::new(EROFS))
}
fn fpath(&mut self, id: usize, buf: &mut [u8]) -> Result<Option<usize>> {
let handle = self.handles.get(&id).ok_or(Error::new(EBADF))?;
fn fpath(&mut self, id: usize, buf: &mut [u8], _ctx: &CallerCtx) -> Result<usize> {
let handle = self.handle(id)?;
let path = format!("firmware:/{}.bin", handle.blob_key);
let bytes = path.as_bytes();
let len = bytes.len().min(buf.len());
buf[..len].copy_from_slice(&bytes[..len]);
Ok(Some(len))
Ok(len)
}
fn fstat(&mut self, id: usize, stat: &mut Stat) -> Result<Option<usize>> {
let handle = self.handles.get(&id).ok_or(Error::new(EBADF))?;
fn fstat(&mut self, id: usize, stat: &mut Stat, _ctx: &CallerCtx) -> Result<()> {
let handle = self.handle(id)?;
stat.st_mode = MODE_FILE | 0o444;
stat.st_size = handle.data.len() as u64;
stat.st_blksize = 4096;
stat.st_blocks = (handle.data.len() as u64 + 511) / 512;
Ok(Some(0))
stat.st_nlink = 1;
Ok(())
}
fn fsync(&mut self, id: usize) -> Result<Option<usize>> {
if !self.handles.contains_key(&id) {
return Err(Error::new(EBADF));
}
Ok(Some(0))
fn fsync(&mut self, id: usize, _ctx: &CallerCtx) -> Result<()> {
let _ = self.handle(id)?;
Ok(())
}
fn fevent(&mut self, id: usize, _flags: EventFlags) -> Result<Option<EventFlags>> {
if !self.handles.contains_key(&id) {
return Err(Error::new(EBADF));
}
Ok(Some(EventFlags::empty()))
fn fcntl(&mut self, id: usize, _cmd: usize, _arg: usize, _ctx: &CallerCtx) -> Result<usize> {
let _ = self.handle(id)?;
Ok(0)
}
fn close(&mut self, id: usize) -> Result<Option<usize>> {
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
handle.closed = true;
let should_remove = handle.map_count == 0;
if should_remove {
self.handles.remove(&id);
}
Ok(Some(0))
fn fsize(&mut self, id: usize, _ctx: &CallerCtx) -> Result<u64> {
let handle = self.handle(id)?;
Ok(handle.data.len() as u64)
}
fn ftruncate(&mut self, id: usize, _len: u64, _ctx: &CallerCtx) -> Result<()> {
let _ = self.handle(id)?;
Err(Error::new(EROFS))
}
fn fevent(&mut self, id: usize, _flags: EventFlags, _ctx: &CallerCtx) -> Result<EventFlags> {
let _ = self.handle(id)?;
Ok(EventFlags::empty())
}
fn mmap_prep(
@@ -182,9 +208,10 @@ impl SchemeBlockMut for FirmwareScheme {
id: usize,
offset: u64,
size: usize,
_flags: MapFlags,
) -> Result<Option<usize>> {
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
_flags: syscall::MapFlags,
_ctx: &CallerCtx,
) -> Result<usize> {
let handle = self.handle_mut(id)?;
let data_len = handle.data.len() as u64;
if offset > data_len {
@@ -196,7 +223,7 @@ impl SchemeBlockMut for FirmwareScheme {
let ptr = &handle.data[offset as usize] as *const u8;
handle.map_count += 1;
Ok(Some(ptr as usize))
Ok(ptr as usize)
}
fn munmap(
@@ -204,9 +231,10 @@ impl SchemeBlockMut for FirmwareScheme {
id: usize,
_offset: u64,
_size: usize,
_flags: MunmapFlags,
) -> Result<Option<usize>> {
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
_flags: syscall::MunmapFlags,
_ctx: &CallerCtx,
) -> Result<()> {
let handle = self.handle_mut(id)?;
if handle.map_count > 0 {
handle.map_count -= 1;
}
@@ -214,6 +242,20 @@ impl SchemeBlockMut for FirmwareScheme {
if should_cleanup {
self.handles.remove(&id);
}
Ok(Some(0))
Ok(())
}
fn on_close(&mut self, id: usize) {
if id == SCHEME_ROOT_ID {
return;
}
if let Some(handle) = self.handles.get_mut(&id) {
handle.closed = true;
let should_remove = handle.map_count == 0;
if should_remove {
self.handles.remove(&id);
}
}
}
}
+14
View File
@@ -0,0 +1,14 @@
#TODO: IOMMU daemon — needs hardware validation with QEMU amd-iommu device
# Provides scheme:iommu for DMA remapping and device isolation.
[source]
path = "source"
[build]
template = "cargo"
dependencies = [
"redox-driver-sys",
]
[package.files]
"/usr/lib/drivers/iommu" = "iommu"
@@ -0,0 +1,13 @@
[package]
name = "iommu"
version = "0.1.0"
edition = "2021"
[dependencies]
redox-driver-sys = { version = "0.1", path = "../../../drivers/redox-driver-sys/source" }
redox_scheme = { package = "redox-scheme", version = "0.1" }
syscall = { package = "redox_syscall", version = "0.4" }
log = { version = "0.4", features = ["std"] }
[patch.crates-io]
redox-driver-sys = { path = "../../../drivers/redox-driver-sys/source" }
@@ -0,0 +1,524 @@
use std::error::Error as StdError;
use std::fmt;
const ACPI_HEADER_BYTES: usize = 36;
const IVRS_HEADER_BYTES: usize = ACPI_HEADER_BYTES + 4;
const IVHD_HEADER_BYTES: usize = 0x18;
const IVHD_TYPE_10: u8 = 0x10;
const IVHD_TYPE_11: u8 = 0x11;
const IVMD_TYPE_20: u8 = 0x20;
const IVMD_TYPE_21: u8 = 0x21;
const IVHD_ALL: u8 = 0x00;
const IVHD_SEL: u8 = 0x01;
const IVHD_SOR: u8 = 0x02;
const IVHD_EOR: u8 = 0x03;
const IVHD_PAD4: u8 = 0x42;
const IVHD_PAD8: u8 = 0x43;
const IVHD_VAR: u8 = 0x44;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Bdf(pub u16);
impl Bdf {
pub const fn new(bus: u8, device: u8, function: u8) -> Self {
Self(((bus as u16) << 8) | (((device as u16) & 0x1F) << 3) | ((function as u16) & 0x7))
}
pub const fn raw(self) -> u16 {
self.0
}
pub const fn bus(self) -> u8 {
(self.0 >> 8) as u8
}
pub const fn device(self) -> u8 {
((self.0 >> 3) & 0x1F) as u8
}
pub const fn function(self) -> u8 {
(self.0 & 0x7) as u8
}
}
impl fmt::Display for Bdf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:02x}:{:02x}.{}",
self.bus(),
self.device(),
self.function()
)
}
}
pub fn parse_bdf(text: &str) -> Option<Bdf> {
let trimmed = text.trim();
if trimmed.is_empty() {
return None;
}
if let Some(raw) = trimmed.strip_prefix("0x") {
return u16::from_str_radix(raw, 16).ok().map(Bdf);
}
if trimmed.contains('.') {
let (head, function) = trimmed.rsplit_once('.')?;
let function = u8::from_str_radix(function, 16)
.or_else(|_| function.parse::<u8>())
.ok()?;
let parts: Vec<&str> = head.split(':').collect();
let (bus, device) = match parts.as_slice() {
[bus, device] => (*bus, *device),
[_, bus, device] => (*bus, *device),
_ => return None,
};
let bus = u8::from_str_radix(bus, 16).ok()?;
let device = u8::from_str_radix(device, 16).ok()?;
return Some(Bdf::new(bus, device, function));
}
u16::from_str_radix(trimmed, 16).ok().map(Bdf)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum IvhdEntry {
All { flags: u8 },
Select { bdf: Bdf, flags: u8 },
StartRange { bdf: Bdf, flags: u8 },
EndRange { bdf: Bdf },
Padding { kind: u8, length: usize },
Variable { kind: u8, payload: Vec<u8> },
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IommuUnitInfo {
pub entry_type: u8,
pub flags: u8,
pub length: u16,
pub iommu_bdf: Bdf,
pub capability_offset: u16,
pub mmio_base: u64,
pub pci_segment_group: u16,
pub iommu_info: u16,
pub iommu_efr: u32,
pub device_entries: Vec<IvhdEntry>,
}
impl IommuUnitInfo {
pub fn unit_id(&self) -> u8 {
((self.iommu_info >> 6) & 0x7F) as u8
}
pub fn msi_number(&self) -> u8 {
(self.iommu_info & 0x3F) as u8
}
pub fn handles_device(&self, bdf: Bdf) -> bool {
let mut all = false;
let mut range_start: Option<u16> = None;
for entry in &self.device_entries {
match *entry {
IvhdEntry::All { .. } => all = true,
IvhdEntry::Select { bdf: selected, .. } if selected == bdf => return true,
IvhdEntry::StartRange { bdf: start, .. } => range_start = Some(start.raw()),
IvhdEntry::EndRange { bdf: end } => {
if let Some(start) = range_start.take() {
let raw = bdf.raw();
if (start..=end.raw()).contains(&raw) {
return true;
}
}
}
_ => {}
}
}
all
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IvrsInfo {
pub revision: u8,
pub iv_info: u32,
pub units: Vec<IommuUnitInfo>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum IvrsError {
TooShort,
InvalidSignature([u8; 4]),
InvalidLength(u32),
InvalidChecksum,
TruncatedEntry { offset: usize },
InvalidEntryLength { offset: usize, length: usize },
InvalidIvhdLength { offset: usize, length: usize },
InvalidVariableLength { offset: usize, length: usize },
}
impl fmt::Display for IvrsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooShort => write!(f, "IVRS table is shorter than the ACPI header"),
Self::InvalidSignature(sig) => write!(
f,
"invalid IVRS signature {:?}",
String::from_utf8_lossy(sig)
),
Self::InvalidLength(length) => write!(f, "invalid IVRS table length {length}"),
Self::InvalidChecksum => write!(f, "IVRS checksum validation failed"),
Self::TruncatedEntry { offset } => {
write!(f, "truncated IVRS entry at offset {offset:#x}")
}
Self::InvalidEntryLength { offset, length } => {
write!(
f,
"invalid IVRS entry length {length} at offset {offset:#x}"
)
}
Self::InvalidIvhdLength { offset, length } => {
write!(
f,
"invalid IVHD entry length {length} at offset {offset:#x}"
)
}
Self::InvalidVariableLength { offset, length } => {
write!(
f,
"invalid IVHD variable-length entry {length} at offset {offset:#x}"
)
}
}
}
}
impl StdError for IvrsError {}
pub fn parse_ivrs(bytes: &[u8]) -> Result<IvrsInfo, IvrsError> {
if bytes.len() < IVRS_HEADER_BYTES {
return Err(IvrsError::TooShort);
}
let signature = bytes[0..4].try_into().map_err(|_| IvrsError::TooShort)?;
if signature != *b"IVRS" {
return Err(IvrsError::InvalidSignature(signature));
}
let length = read_u32(bytes, 4).ok_or(IvrsError::TooShort)?;
if length < IVRS_HEADER_BYTES as u32 {
return Err(IvrsError::InvalidLength(length));
}
if bytes.len() < length as usize {
return Err(IvrsError::TooShort);
}
let table = &bytes[..length as usize];
if table.iter().fold(0u8, |sum, byte| sum.wrapping_add(*byte)) != 0 {
return Err(IvrsError::InvalidChecksum);
}
let revision = table[8];
let iv_info = read_u32(table, ACPI_HEADER_BYTES).ok_or(IvrsError::TooShort)?;
let mut units = Vec::new();
let mut offset = IVRS_HEADER_BYTES;
while offset < table.len() {
if offset + 4 > table.len() {
return Err(IvrsError::TruncatedEntry { offset });
}
let entry_type = table[offset];
let entry_length =
read_u16(table, offset + 2).ok_or(IvrsError::TruncatedEntry { offset })? as usize;
if entry_length < 4 {
return Err(IvrsError::InvalidEntryLength {
offset,
length: entry_length,
});
}
if offset + entry_length > table.len() {
return Err(IvrsError::TruncatedEntry { offset });
}
let entry = &table[offset..offset + entry_length];
if matches!(entry_type, IVHD_TYPE_10 | IVHD_TYPE_11) {
units.push(parse_ivhd(entry, offset)?);
}
if matches!(entry_type, IVMD_TYPE_20 | IVMD_TYPE_21) {
offset += entry_length;
continue;
}
offset += entry_length;
}
Ok(IvrsInfo {
revision,
iv_info,
units,
})
}
fn parse_ivhd(entry: &[u8], table_offset: usize) -> Result<IommuUnitInfo, IvrsError> {
if entry.len() < IVHD_HEADER_BYTES {
return Err(IvrsError::InvalidIvhdLength {
offset: table_offset,
length: entry.len(),
});
}
let mut device_entries = Vec::new();
let mut offset = IVHD_HEADER_BYTES;
while offset < entry.len() {
let kind = entry[offset];
match kind {
IVHD_ALL => {
ensure_remaining(entry, offset, 4, table_offset)?;
device_entries.push(IvhdEntry::All {
flags: entry[offset + 1],
});
offset += 4;
}
IVHD_SEL => {
ensure_remaining(entry, offset, 4, table_offset)?;
device_entries.push(IvhdEntry::Select {
bdf: Bdf(
read_u16(entry, offset + 2).ok_or(IvrsError::TruncatedEntry {
offset: table_offset + offset,
})?,
),
flags: entry[offset + 1],
});
offset += 4;
}
IVHD_SOR => {
ensure_remaining(entry, offset, 4, table_offset)?;
device_entries.push(IvhdEntry::StartRange {
bdf: Bdf(
read_u16(entry, offset + 2).ok_or(IvrsError::TruncatedEntry {
offset: table_offset + offset,
})?,
),
flags: entry[offset + 1],
});
offset += 4;
}
IVHD_EOR => {
ensure_remaining(entry, offset, 4, table_offset)?;
device_entries.push(IvhdEntry::EndRange {
bdf: Bdf(
read_u16(entry, offset + 2).ok_or(IvrsError::TruncatedEntry {
offset: table_offset + offset,
})?,
),
});
offset += 4;
}
IVHD_PAD4 => {
ensure_remaining(entry, offset, 8, table_offset)?;
device_entries.push(IvhdEntry::Padding { kind, length: 8 });
offset += 8;
}
IVHD_PAD8 => {
ensure_remaining(entry, offset, 12, table_offset)?;
device_entries.push(IvhdEntry::Padding { kind, length: 12 });
offset += 12;
}
IVHD_VAR => {
ensure_remaining(entry, offset, 2, table_offset)?;
let variable_length = entry[offset + 1] as usize;
if variable_length < 2 {
return Err(IvrsError::InvalidVariableLength {
offset: table_offset + offset,
length: variable_length,
});
}
ensure_remaining(entry, offset, variable_length, table_offset)?;
device_entries.push(IvhdEntry::Variable {
kind,
payload: entry[offset + 2..offset + variable_length].to_vec(),
});
offset += variable_length;
}
_ => {
ensure_remaining(entry, offset, 4, table_offset)?;
device_entries.push(IvhdEntry::Variable {
kind,
payload: entry[offset + 1..offset + 4].to_vec(),
});
offset += 4;
}
}
}
Ok(IommuUnitInfo {
entry_type: entry[0],
flags: entry[1],
length: read_u16(entry, 2).ok_or(IvrsError::TruncatedEntry {
offset: table_offset,
})?,
iommu_bdf: Bdf(read_u16(entry, 4).ok_or(IvrsError::TruncatedEntry {
offset: table_offset,
})?),
capability_offset: read_u16(entry, 6).ok_or(IvrsError::TruncatedEntry {
offset: table_offset,
})?,
mmio_base: read_u64(entry, 8).ok_or(IvrsError::TruncatedEntry {
offset: table_offset,
})?,
pci_segment_group: read_u16(entry, 16).ok_or(IvrsError::TruncatedEntry {
offset: table_offset,
})?,
iommu_info: read_u16(entry, 18).ok_or(IvrsError::TruncatedEntry {
offset: table_offset,
})?,
iommu_efr: read_u32(entry, 20).ok_or(IvrsError::TruncatedEntry {
offset: table_offset,
})?,
device_entries,
})
}
fn ensure_remaining(
entry: &[u8],
offset: usize,
length: usize,
table_offset: usize,
) -> Result<(), IvrsError> {
if offset + length > entry.len() {
return Err(IvrsError::TruncatedEntry {
offset: table_offset + offset,
});
}
Ok(())
}
fn read_u16(bytes: &[u8], offset: usize) -> Option<u16> {
bytes
.get(offset..offset + 2)?
.try_into()
.ok()
.map(u16::from_le_bytes)
}
fn read_u32(bytes: &[u8], offset: usize) -> Option<u32> {
bytes
.get(offset..offset + 4)?
.try_into()
.ok()
.map(u32::from_le_bytes)
}
fn read_u64(bytes: &[u8], offset: usize) -> Option<u64> {
bytes
.get(offset..offset + 8)?
.try_into()
.ok()
.map(u64::from_le_bytes)
}
#[cfg(test)]
mod tests {
use super::{parse_bdf, parse_ivrs, Bdf, IommuUnitInfo, IvhdEntry, IVRS_HEADER_BYTES};
fn build_ivrs(units: &[Vec<u8>]) -> Vec<u8> {
let length = (IVRS_HEADER_BYTES + units.iter().map(Vec::len).sum::<usize>()) as u32;
let mut bytes = vec![0u8; length as usize];
bytes[0..4].copy_from_slice(b"IVRS");
bytes[4..8].copy_from_slice(&length.to_le_bytes());
bytes[8] = 3;
bytes[10..16].copy_from_slice(b"RDBEAR");
bytes[16..24].copy_from_slice(b"AMDVI ");
bytes[36..40].copy_from_slice(&0x0123_4567u32.to_le_bytes());
let mut offset = IVRS_HEADER_BYTES;
for unit in units {
bytes[offset..offset + unit.len()].copy_from_slice(unit);
offset += unit.len();
}
let checksum =
(!bytes.iter().fold(0u8, |sum, byte| sum.wrapping_add(*byte))).wrapping_add(1);
bytes[9] = checksum;
bytes
}
fn build_ivhd(mmio_base: u64, iommu_bdf: Bdf, entries: &[u8]) -> Vec<u8> {
let length = (0x18 + entries.len()) as u16;
let mut bytes = vec![0u8; length as usize];
bytes[0] = 0x11;
bytes[1] = 0xA0;
bytes[2..4].copy_from_slice(&length.to_le_bytes());
bytes[4..6].copy_from_slice(&iommu_bdf.raw().to_le_bytes());
bytes[6..8].copy_from_slice(&0x0040u16.to_le_bytes());
bytes[8..16].copy_from_slice(&mmio_base.to_le_bytes());
bytes[16..18].copy_from_slice(&0u16.to_le_bytes());
bytes[18..20].copy_from_slice(&0x01c2u16.to_le_bytes());
bytes[20..24].copy_from_slice(&0x00aa_5500u32.to_le_bytes());
bytes[24..].copy_from_slice(entries);
bytes
}
#[test]
fn parses_bdf_text_forms() {
assert_eq!(parse_bdf("00:14.0"), Some(Bdf::new(0x00, 0x14, 0x0)));
assert_eq!(parse_bdf("0000:02:00.1"), Some(Bdf::new(0x02, 0x00, 0x1)));
assert_eq!(parse_bdf("0x1234"), Some(Bdf(0x1234)));
assert_eq!(parse_bdf("zz:zz.z"), None);
}
#[test]
fn parses_ivrs_with_multiple_units() {
let unit0_entries = [
0x01, 0x11, 0x08, 0x00, // select 00:01.0
0x02, 0x22, 0x10, 0x00, // start range 00:02.0
0x03, 0x00, 0x17, 0x00, // end range 00:02.7
];
let unit1_entries = [0x00, 0x00, 0x00, 0x00];
let table = build_ivrs(&[
build_ivhd(0xfee0_0000, Bdf::new(0, 0x18, 2), &unit0_entries),
build_ivhd(0xfee1_0000, Bdf::new(0, 0x18, 3), &unit1_entries),
]);
let parsed = parse_ivrs(&table).unwrap_or_else(|err| panic!("IVRS parse failed: {err}"));
assert_eq!(parsed.units.len(), 2);
assert_eq!(parsed.units[0].mmio_base, 0xfee0_0000);
assert_eq!(parsed.units[1].iommu_bdf, Bdf::new(0, 0x18, 3));
let unit = &parsed.units[0];
assert!(unit.handles_device(Bdf::new(0, 1, 0)));
assert!(unit.handles_device(Bdf::new(0, 2, 3)));
assert!(!unit.handles_device(Bdf::new(0, 3, 0)));
assert_eq!(unit.unit_id(), 7);
assert_eq!(unit.msi_number(), 2);
}
#[test]
fn all_entry_covers_entire_bus_space() {
let unit = IommuUnitInfo {
entry_type: 0x11,
flags: 0,
length: 0x1c,
iommu_bdf: Bdf::new(0, 0x18, 2),
capability_offset: 0x40,
mmio_base: 0xfee0_0000,
pci_segment_group: 0,
iommu_info: 0,
iommu_efr: 0,
device_entries: vec![IvhdEntry::All { flags: 0 }],
};
assert!(unit.handles_device(Bdf::new(0x80, 0x1f, 7)));
}
}
@@ -0,0 +1,416 @@
use core::ptr::{read_volatile, write_volatile};
use log::{debug, warn};
use redox_driver_sys::memory::{CacheType, MmioProt, MmioRegion};
use crate::acpi::{parse_ivrs, Bdf, IommuUnitInfo, IvrsError};
use crate::command_buffer::{CommandBuffer, CommandEntry, EventLog, EventLogEntry};
use crate::device_table::{DeviceTable, DeviceTableEntry, DEVICE_TABLE_ENTRIES};
use crate::interrupt::InterruptRemapTable;
use crate::mmio::{control, ext_feature, status, AmdViMmio, AMD_VI_MMIO_BYTES};
use crate::page_table::DomainPageTables;
const CMD_BUF_LEN_ENCODING: u64 = 0x09;
const EVT_LOG_LEN_ENCODING: u64 = 0x09;
const DEV_TABLE_SIZE_ENCODING: u64 = 0x0F;
const DEFAULT_CMD_ENTRIES: usize = 512;
const DEFAULT_EVT_ENTRIES: usize = 512;
const DEFAULT_IRT_ENTRIES: usize = 4096;
const COMPLETION_TOKEN: u32 = 0xA11D_F00D;
struct MmioMapping {
region: MmioRegion,
base: *mut AmdViMmio,
}
pub struct AmdViUnit {
info: IommuUnitInfo,
mmio: Option<MmioMapping>,
device_table: Option<DeviceTable>,
command_buffer: Option<CommandBuffer>,
event_log: Option<EventLog>,
interrupt_table: Option<InterruptRemapTable>,
completion_store: Option<redox_driver_sys::dma::DmaBuffer>,
command_tail: usize,
event_head: usize,
initialized: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AmdViEvent {
pub unit_id: u8,
pub event_code: u16,
pub event_flags: u16,
pub device_id: Bdf,
pub address: u64,
}
impl AmdViUnit {
pub fn detect(ivrs: &[u8]) -> Result<Vec<Self>, IvrsError> {
let parsed = parse_ivrs(ivrs)?;
Ok(parsed.units.into_iter().map(Self::from_info).collect())
}
pub fn from_info(info: IommuUnitInfo) -> Self {
Self {
info,
mmio: None,
device_table: None,
command_buffer: None,
event_log: None,
interrupt_table: None,
completion_store: None,
command_tail: 0,
event_head: 0,
initialized: false,
}
}
pub fn info(&self) -> &IommuUnitInfo {
&self.info
}
pub fn initialized(&self) -> bool {
self.initialized
}
pub fn handles_device(&self, bdf: Bdf) -> bool {
self.info.handles_device(bdf)
}
pub fn init(&mut self) -> Result<(), String> {
if self.initialized {
return Ok(());
}
let region = MmioRegion::map(
self.info.mmio_base,
AMD_VI_MMIO_BYTES,
CacheType::DeviceMemory,
MmioProt::READ_WRITE,
)
.map_err(|err| {
format!(
"failed to map AMD-Vi MMIO {:#x}: {err}",
self.info.mmio_base
)
})?;
let base = region.as_ptr() as *mut AmdViMmio;
self.mmio = Some(MmioMapping { region, base });
self.disable_unit()?;
let device_table = DeviceTable::new().map_err(|err| err.to_string())?;
let command_buffer =
CommandBuffer::new(DEFAULT_CMD_ENTRIES).map_err(|err| err.to_string())?;
let event_log = EventLog::new(DEFAULT_EVT_ENTRIES).map_err(|err| err.to_string())?;
let interrupt_table =
InterruptRemapTable::new(DEFAULT_IRT_ENTRIES).map_err(|err| err.to_string())?;
self.program_bars(&device_table, &command_buffer, &event_log)?;
self.reset_ring_pointers()?;
self.device_table = Some(device_table);
self.command_buffer = Some(command_buffer);
self.event_log = Some(event_log);
self.interrupt_table = Some(interrupt_table);
let ext = self.mmio_read_extended_feature()?;
let mut control_value = control::EVENT_LOG_EN | control::CMD_BUF_EN;
if ext & ext_feature::XT_SUP != 0 {
control_value |= control::XT_EN;
}
if ext & ext_feature::NX_SUP != 0 {
control_value |= control::NX_EN;
}
unsafe {
AmdViMmio::write_control(self.mmio_base()?, control_value);
}
self.flush_configuration()?;
unsafe {
AmdViMmio::write_control(self.mmio_base()?, control_value | control::IOMMU_ENABLE);
}
self.wait_for_running(true)?;
self.initialized = true;
Ok(())
}
pub fn assign_device(&mut self, bdf: Bdf, domain: &DomainPageTables) -> Result<(), String> {
if !self.initialized {
return Err("AMD-Vi unit is not initialized".to_string());
}
if !self.handles_device(bdf) {
return Err(format!(
"AMD-Vi unit {} does not cover device {bdf}",
self.info.unit_id()
));
}
let interrupt_table = self
.interrupt_table
.as_ref()
.ok_or_else(|| "interrupt remap table not initialized".to_string())?;
let device_table = self
.device_table
.as_mut()
.ok_or_else(|| "device table not initialized".to_string())?;
let mut entry = DeviceTableEntry::new();
entry.set_valid(true);
entry.set_translation_valid(true);
entry.set_read_permission(true);
entry.set_write_permission(true);
entry.set_mode(domain.levels());
entry.set_page_table_root(domain.root_address());
entry.set_interrupt_remap(true);
entry.set_interrupt_write(true);
entry.set_interrupt_control(0x02);
entry.set_int_table_len(interrupt_table.len_encoding());
entry.set_int_remap_table_ptr(interrupt_table.physical_address() as u64);
device_table.set_entry(bdf.raw(), &entry);
self.submit_command(CommandEntry::invalidate_devtab_entry(bdf.raw()))?;
self.submit_command(CommandEntry::invalidate_interrupt_table(bdf.raw()))?;
self.wait_for_completion()?;
Ok(())
}
pub fn drain_events(&mut self) -> Result<Vec<AmdViEvent>, String> {
let mut drained = Vec::new();
if !self.initialized {
return Ok(drained);
}
let base = self.mmio_base()?;
let event_log = self
.event_log
.as_ref()
.ok_or_else(|| "event log not initialized".to_string())?;
let tail = unsafe { AmdViMmio::read_evt_log_tail(base) as usize % event_log.capacity() };
while self.event_head != tail {
let event = event_log.read_entry(self.event_head);
drained.push(self.decode_event(event));
self.event_head = (self.event_head + 1) % event_log.capacity();
}
unsafe {
AmdViMmio::write_evt_log_head(base, self.event_head as u64);
}
Ok(drained)
}
fn decode_event(&self, event: EventLogEntry) -> AmdViEvent {
AmdViEvent {
unit_id: self.info.unit_id(),
event_code: event.event_type() as u16,
event_flags: event.event_flags(),
device_id: Bdf(event.device_id()),
address: event.virtual_address(),
}
}
fn disable_unit(&mut self) -> Result<(), String> {
let base = self.mmio_base()?;
unsafe {
AmdViMmio::write_control(base, 0);
}
self.wait_for_running(false)
}
fn wait_for_running(&self, expected: bool) -> Result<(), String> {
let base = self.mmio_base()?;
for _ in 0..100_000 {
let running = unsafe { AmdViMmio::read_status(base) } & status::IOMMU_RUNNING != 0;
if running == expected {
return Ok(());
}
std::hint::spin_loop();
}
Err(format!(
"timed out waiting for AMD-Vi unit {} running={expected}",
self.info.unit_id()
))
}
fn program_bars(
&mut self,
device_table: &DeviceTable,
command_buffer: &CommandBuffer,
event_log: &EventLog,
) -> Result<(), String> {
let base = self.mmio_base()?;
unsafe {
AmdViMmio::write_dev_table_bar(
base,
(device_table.physical_address() as u64 & !0xFFF) | DEV_TABLE_SIZE_ENCODING,
);
AmdViMmio::write_cmd_buf_bar(
base,
(command_buffer.physical_address() as u64 & !0xFFF) | CMD_BUF_LEN_ENCODING,
);
AmdViMmio::write_evt_log_bar(
base,
(event_log.physical_address() as u64 & !0xFFF) | EVT_LOG_LEN_ENCODING,
);
AmdViMmio::write_exclusion_base(base, 0);
AmdViMmio::write_exclusion_limit(base, 0);
}
Ok(())
}
fn reset_ring_pointers(&mut self) -> Result<(), String> {
let base = self.mmio_base()?;
unsafe {
AmdViMmio::write_cmd_buf_head(base, 0);
AmdViMmio::write_cmd_buf_tail(base, 0);
AmdViMmio::write_evt_log_head(base, 0);
}
self.command_tail = 0;
self.event_head = 0;
Ok(())
}
fn flush_configuration(&mut self) -> Result<(), String> {
let ext = self.mmio_read_extended_feature()?;
if ext & ext_feature::IA_SUP != 0 {
self.submit_command(CommandEntry::invalidate_all())?;
} else if let Some(table) = self.device_table.as_ref() {
let mut pending_invalidations = Vec::new();
for device_id in 0..DEVICE_TABLE_ENTRIES {
let entry = table.get_entry(device_id as u16);
if entry.valid() {
pending_invalidations.push(device_id as u16);
}
}
for device_id in pending_invalidations {
self.submit_command(CommandEntry::invalidate_devtab_entry(device_id))?;
}
} else {
warn!("amd-vi: device table not yet allocated while flushing configuration");
}
self.wait_for_completion()
}
fn submit_command(&mut self, command: CommandEntry) -> Result<(), String> {
let base = self.mmio_base()?;
let command_buffer = self
.command_buffer
.as_mut()
.ok_or_else(|| "command buffer not initialized".to_string())?;
let head =
unsafe { AmdViMmio::read_cmd_buf_head(base) as usize % command_buffer.capacity() };
let next_tail = (self.command_tail + 1) % command_buffer.capacity();
if next_tail == head {
return Err("AMD-Vi command buffer is full".to_string());
}
command_buffer.write_command(self.command_tail, &command);
self.command_tail = next_tail;
unsafe {
AmdViMmio::write_cmd_buf_tail(base, self.command_tail as u64);
}
Ok(())
}
fn wait_for_completion(&mut self) -> Result<(), String> {
let completion_store = match self.completion_store.take() {
Some(buffer) => buffer,
None => redox_driver_sys::dma::DmaBuffer::allocate(8, 8)
.map_err(|err| format!("failed to allocate completion wait store: {err}"))?,
};
let completion_ptr = completion_store.as_ptr() as *const u32;
let completion_mut = completion_store.as_ptr() as *mut u32;
unsafe {
write_volatile(completion_mut, 0);
}
let completion_phys = completion_store.physical_address() as u64;
self.submit_command(CommandEntry::completion_wait(
completion_phys,
COMPLETION_TOKEN,
))?;
for _ in 0..100_000 {
if unsafe { read_volatile(completion_ptr) } == COMPLETION_TOKEN {
self.completion_store = Some(completion_store);
return Ok(());
}
std::hint::spin_loop();
}
self.completion_store = Some(completion_store);
Err("timed out waiting for AMD-Vi command completion".to_string())
}
fn mmio_read_extended_feature(&self) -> Result<u64, String> {
let base = self.mmio_base()?;
Ok(unsafe { AmdViMmio::read_extended_feature(base) })
}
fn mmio_base(&self) -> Result<*mut AmdViMmio, String> {
self.mmio
.as_ref()
.map(|mapping| mapping.base)
.ok_or_else(|| "AMD-Vi MMIO is not mapped".to_string())
}
}
impl Drop for AmdViUnit {
fn drop(&mut self) {
if let Some(mapping) = &self.mmio {
debug!(
"amd-vi: dropping unit {} mapped at {:#x} ({:#x} bytes)",
self.info.unit_id(),
self.info.mmio_base,
mapping.region.size()
);
}
}
}
#[cfg(test)]
mod tests {
use crate::acpi::Bdf;
use super::AmdViUnit;
fn build_ivrs_with_unit() -> Vec<u8> {
let mut table = vec![0u8; 40 + 28];
table[0..4].copy_from_slice(b"IVRS");
table[4..8].copy_from_slice(&(68u32).to_le_bytes());
table[8] = 3;
table[10..16].copy_from_slice(b"RDBEAR");
table[16..24].copy_from_slice(b"AMDVI ");
let offset = 40;
table[offset] = 0x11;
table[offset + 1] = 0x20;
table[offset + 2..offset + 4].copy_from_slice(&(28u16).to_le_bytes());
table[offset + 4..offset + 6].copy_from_slice(&Bdf::new(0, 0x18, 2).raw().to_le_bytes());
table[offset + 6..offset + 8].copy_from_slice(&0x40u16.to_le_bytes());
table[offset + 8..offset + 16].copy_from_slice(&0xfee0_0000u64.to_le_bytes());
table[offset + 16..offset + 18].copy_from_slice(&0u16.to_le_bytes());
table[offset + 18..offset + 20].copy_from_slice(&0x0081u16.to_le_bytes());
table[offset + 20..offset + 24].copy_from_slice(&0u32.to_le_bytes());
table[offset + 24..offset + 28].copy_from_slice(&[0x00, 0, 0, 0]);
let checksum =
(!table.iter().fold(0u8, |sum, byte| sum.wrapping_add(*byte))).wrapping_add(1);
table[9] = checksum;
table
}
#[test]
fn detect_builds_units_from_ivrs() {
let units = AmdViUnit::detect(&build_ivrs_with_unit())
.unwrap_or_else(|err| panic!("amd-vi detect failed: {err}"));
assert_eq!(units.len(), 1);
assert_eq!(units[0].info().mmio_base, 0xfee0_0000);
assert!(units[0].handles_device(Bdf::new(0x80, 0x1f, 7)));
}
}
@@ -0,0 +1,371 @@
use core::mem::size_of;
use core::slice;
use redox_driver_sys::dma::DmaBuffer;
pub const COMMAND_ENTRY_SIZE: usize = 16;
pub const EVENT_LOG_ENTRY_SIZE: usize = 16;
const DMA_ALIGNMENT: usize = 4096;
pub const CMD_COMPLETION_WAIT: u32 = 0x01;
pub const CMD_INVALIDATE_DEVTAB_ENTRY: u32 = 0x02;
pub const CMD_INVALIDATE_IOMMU_PAGES: u32 = 0x03;
pub const CMD_INVALIDATE_INTERRUPT_TABLE: u32 = 0x04;
pub const CMD_INVALIDATE_IOMMU_ALL: u32 = 0x05;
pub const EVENT_IO_PAGE_FAULT: u32 = 0x01;
pub const EVENT_INVALIDATE_DEVICE_TABLE: u32 = 0x02;
const COMPLETION_WAIT_STORE_BIT: u32 = 1 << 4;
const COMPLETION_WAIT_INTERRUPT_BIT: u32 = 1 << 5;
const INVALIDATE_PAGES_PDE_BIT: u32 = 1 << 12;
const INVALIDATE_PAGES_SIZE_BIT: u32 = 1 << 13;
/// Command buffer entry (128 bits = 16 bytes = 4 × u32).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[repr(C)]
pub struct CommandEntry {
words: [u32; 4],
}
impl CommandEntry {
pub const fn new() -> Self {
Self { words: [0; 4] }
}
pub const fn from_words(words: [u32; 4]) -> Self {
Self { words }
}
pub fn words(&self) -> [u32; 4] {
self.words
}
pub fn opcode(&self) -> u32 {
self.words[0] & 0xF
}
/// COMPLETION_WAIT (opcode 0x01).
pub fn completion_wait(store_addr: u64, store_data: u32) -> Self {
debug_assert_eq!(
store_addr & 0x7,
0,
"completion wait store address must be 8-byte aligned"
);
Self {
words: [
CMD_COMPLETION_WAIT | COMPLETION_WAIT_STORE_BIT,
store_addr as u32,
(store_addr >> 32) as u32,
store_data,
],
}
}
/// INVALIDATE_DEVTAB_ENTRY (opcode 0x02).
pub fn invalidate_devtab_entry(device_id: u16) -> Self {
Self {
words: [CMD_INVALIDATE_DEVTAB_ENTRY, device_id as u32, 0, 0],
}
}
pub fn invalidate_pages(domain_id: u16, addr: u64) -> Self {
Self::invalidate_pages_with_flags(domain_id, addr, false, false)
}
pub fn invalidate_pages_with_flags(domain_id: u16, addr: u64, pde: bool, size: bool) -> Self {
let mut word0 = CMD_INVALIDATE_IOMMU_PAGES;
if pde {
word0 |= INVALIDATE_PAGES_PDE_BIT;
}
if size {
word0 |= INVALIDATE_PAGES_SIZE_BIT;
}
Self {
words: [word0, domain_id as u32, addr as u32, (addr >> 32) as u32],
}
}
pub fn invalidate_interrupt_table(device_id: u16) -> Self {
Self {
words: [CMD_INVALIDATE_INTERRUPT_TABLE, device_id as u32, 0, 0],
}
}
/// INVALIDATE_IOMMU_ALL (opcode 0x05).
pub fn invalidate_all() -> Self {
Self {
words: [CMD_INVALIDATE_IOMMU_ALL, 0, 0, 0],
}
}
pub fn completion_wait_store(&self) -> bool {
self.words[0] & COMPLETION_WAIT_STORE_BIT != 0
}
pub fn completion_wait_interrupt(&self) -> bool {
self.words[0] & COMPLETION_WAIT_INTERRUPT_BIT != 0
}
pub fn completion_wait_store_address(&self) -> u64 {
(self.words[1] as u64) | ((self.words[2] as u64) << 32)
}
pub fn completion_wait_store_data(&self) -> u32 {
self.words[3]
}
pub fn invalidate_device_id(&self) -> u16 {
self.words[1] as u16
}
pub fn invalidate_pages_pde(&self) -> bool {
self.words[0] & INVALIDATE_PAGES_PDE_BIT != 0
}
pub fn invalidate_pages_size(&self) -> bool {
self.words[0] & INVALIDATE_PAGES_SIZE_BIT != 0
}
pub fn invalidate_pages_address(&self) -> u64 {
(self.words[2] as u64) | ((self.words[3] as u64) << 32)
}
}
const _: () = assert!(size_of::<CommandEntry>() == COMMAND_ENTRY_SIZE);
pub struct CommandBuffer {
buffer: DmaBuffer,
capacity: usize,
}
impl CommandBuffer {
pub fn new(entry_count: usize) -> Result<Self, &'static str> {
if entry_count == 0 {
return Err("IOMMU command buffer entry count must be non-zero");
}
let byte_len = entry_count
.checked_mul(COMMAND_ENTRY_SIZE)
.ok_or("IOMMU command buffer size overflow")?;
let buffer = DmaBuffer::allocate(byte_len, DMA_ALIGNMENT)
.map_err(|_| "failed to allocate IOMMU command buffer")?;
if buffer.len() < byte_len {
return Err("IOMMU command buffer allocation was smaller than requested");
}
if !buffer.is_physically_contiguous() {
return Err("IOMMU command buffer allocation is not physically contiguous");
}
if buffer.physical_address() & (DMA_ALIGNMENT - 1) != 0 {
return Err("IOMMU command buffer allocation is not 4KiB-aligned");
}
Ok(Self {
buffer,
capacity: entry_count,
})
}
pub fn physical_address(&self) -> usize {
self.buffer.physical_address()
}
/// Write a command at the given index.
pub fn write_command(&mut self, index: usize, cmd: &CommandEntry) {
assert!(index < self.capacity, "IOMMU command index out of bounds");
self.commands_mut()[index] = *cmd;
}
pub fn capacity(&self) -> usize {
self.capacity
}
fn commands_mut(&mut self) -> &mut [CommandEntry] {
unsafe {
slice::from_raw_parts_mut(self.buffer.as_mut_ptr() as *mut CommandEntry, self.capacity)
}
}
}
/// Event log entry (128 bits = 16 bytes).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[repr(C)]
pub struct EventLogEntry {
words: [u32; 4],
}
impl EventLogEntry {
pub const fn new() -> Self {
Self { words: [0; 4] }
}
pub const fn from_words(words: [u32; 4]) -> Self {
Self { words }
}
pub fn words(&self) -> [u32; 4] {
self.words
}
pub fn event_type(&self) -> u32 {
self.words[0] & 0xFFFF
}
pub fn event_flags(&self) -> u16 {
((self.words[0] >> 16) & 0xFFFF) as u16
}
pub fn device_id(&self) -> u16 {
self.words[1] as u16
}
pub fn virtual_address(&self) -> u64 {
((self.words[3] as u64) << 32) | (self.words[2] as u64)
}
}
const _: () = assert!(size_of::<EventLogEntry>() == EVENT_LOG_ENTRY_SIZE);
pub struct EventLog {
buffer: DmaBuffer,
capacity: usize,
}
impl EventLog {
pub fn new(entry_count: usize) -> Result<Self, &'static str> {
if entry_count == 0 {
return Err("IOMMU event log entry count must be non-zero");
}
let byte_len = entry_count
.checked_mul(EVENT_LOG_ENTRY_SIZE)
.ok_or("IOMMU event log size overflow")?;
let buffer = DmaBuffer::allocate(byte_len, DMA_ALIGNMENT)
.map_err(|_| "failed to allocate IOMMU event log")?;
if buffer.len() < byte_len {
return Err("IOMMU event log allocation was smaller than requested");
}
if !buffer.is_physically_contiguous() {
return Err("IOMMU event log allocation is not physically contiguous");
}
if buffer.physical_address() & (DMA_ALIGNMENT - 1) != 0 {
return Err("IOMMU event log allocation is not 4KiB-aligned");
}
Ok(Self {
buffer,
capacity: entry_count,
})
}
pub fn physical_address(&self) -> usize {
self.buffer.physical_address()
}
pub fn read_entry(&self, index: usize) -> EventLogEntry {
assert!(index < self.capacity, "IOMMU event log index out of bounds");
self.entries()[index]
}
pub fn capacity(&self) -> usize {
self.capacity
}
fn entries(&self) -> &[EventLogEntry] {
unsafe {
slice::from_raw_parts(self.buffer.as_ptr() as *const EventLogEntry, self.capacity)
}
}
}
#[cfg(test)]
mod tests {
use super::{
CommandEntry, EventLogEntry, CMD_COMPLETION_WAIT, CMD_INVALIDATE_DEVTAB_ENTRY,
CMD_INVALIDATE_IOMMU_ALL, CMD_INVALIDATE_IOMMU_PAGES, EVENT_IO_PAGE_FAULT,
};
#[test]
fn test_completion_wait_command() {
let store_addr = 0x1234_5000_0000_1000;
let store_data = 0xabcdefff;
let cmd = CommandEntry::completion_wait(store_addr, store_data);
let words = cmd.words();
assert_eq!(cmd.opcode(), CMD_COMPLETION_WAIT);
assert!(cmd.completion_wait_store());
assert!(!cmd.completion_wait_interrupt());
assert_eq!(words[1], store_addr as u32);
assert_eq!(words[2], (store_addr >> 32) as u32);
assert_eq!(words[3], store_data);
assert_eq!(cmd.completion_wait_store_address(), store_addr);
assert_eq!(cmd.completion_wait_store_data(), store_data);
}
#[test]
fn test_invalidate_devtab_command() {
let device_id = 0x1234;
let cmd = CommandEntry::invalidate_devtab_entry(device_id);
let words = cmd.words();
assert_eq!(cmd.opcode(), CMD_INVALIDATE_DEVTAB_ENTRY);
assert_eq!(cmd.invalidate_device_id(), device_id);
assert_eq!(words[1], device_id as u32);
assert_eq!(words[2], 0);
assert_eq!(words[3], 0);
}
#[test]
fn test_invalidate_pages_command() {
let device_id = 0x4321;
let addr = 0xfeed_cafe_b000;
let cmd = CommandEntry::invalidate_pages(device_id, addr);
let words = cmd.words();
assert_eq!(cmd.opcode(), CMD_INVALIDATE_IOMMU_PAGES);
assert_eq!(cmd.invalidate_device_id(), device_id);
assert!(!cmd.invalidate_pages_pde());
assert!(!cmd.invalidate_pages_size());
assert_eq!(words[1], device_id as u32);
assert_eq!(cmd.invalidate_pages_address(), addr);
}
#[test]
fn test_invalidate_all_command() {
let cmd = CommandEntry::invalidate_all();
let words = cmd.words();
assert_eq!(cmd.opcode(), CMD_INVALIDATE_IOMMU_ALL);
assert_eq!(words[1], 0);
assert_eq!(words[2], 0);
assert_eq!(words[3], 0);
}
#[test]
fn test_event_entry_parsing() {
let device_id = 0x2468;
let address = 0x0123_4567_89ab_cdef;
let entry = EventLogEntry::from_words([
EVENT_IO_PAGE_FAULT | ((0x5a as u32) << 16),
device_id as u32,
address as u32,
(address >> 32) as u32,
]);
assert_eq!(entry.event_type(), EVENT_IO_PAGE_FAULT);
assert_eq!(entry.event_flags(), 0x5a);
assert_eq!(entry.device_id(), device_id);
assert_eq!(entry.virtual_address(), address);
}
}
@@ -0,0 +1,337 @@
use core::mem::size_of;
use core::slice;
use redox_driver_sys::dma::DmaBuffer;
/// AMD-Vi Device Table: 65536 entries × 32 bytes = 2 MiB.
pub const DEVICE_TABLE_ENTRIES: usize = 65_536;
pub const DTE_SIZE: usize = 32;
const DEVICE_TABLE_BYTES: usize = DEVICE_TABLE_ENTRIES * DTE_SIZE;
const DTE_VALID_BIT: u64 = 1 << 0;
const DTE_TRANSLATION_VALID_BIT: u64 = 1 << 1;
const DTE_WRITE_PERMISSION_BIT: u64 = 1 << 4;
const DTE_READ_PERMISSION_BIT: u64 = 1 << 5;
const DTE_SNOOP_ENABLE_BIT: u64 = 1 << 8;
const DTE_MODE_SHIFT: u32 = 9;
const DTE_MODE_MASK: u64 = 0x7 << DTE_MODE_SHIFT;
const DTE_PAGE_TABLE_ROOT_MASK: u64 = ((1u64 << 40) - 1) << 12;
const DTE_INTERRUPT_REMAP_BIT: u64 = 1 << 61;
const DTE_INTERRUPT_WRITE_BIT: u64 = 1 << 62;
const DTE_INT_TABLE_LEN_MASK: u64 = 0xF;
const DTE_INT_CONTROL_SHIFT: u32 = 4;
const DTE_INT_CONTROL_MASK: u64 = 0x3 << DTE_INT_CONTROL_SHIFT;
const DTE_INT_REMAP_TABLE_PTR_SHIFT: u32 = 6;
const DTE_INT_REMAP_TABLE_PTR_MASK: u64 = ((1u64 << 46) - 1) << DTE_INT_REMAP_TABLE_PTR_SHIFT;
/// Device Table Entry (DTE) — 256 bits (32 bytes = 4 × u64).
///
/// Layout follows AMD IOMMU Spec 48882 Rev 3.10, Section 3.2.2.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[repr(C)]
pub struct DeviceTableEntry {
data: [u64; 4],
}
impl DeviceTableEntry {
pub const fn new() -> Self {
Self { data: [0; 4] }
}
pub fn valid(&self) -> bool {
self.data[0] & DTE_VALID_BIT != 0
}
pub fn set_valid(&mut self, value: bool) {
if value {
self.data[0] |= DTE_VALID_BIT;
} else {
self.data[0] &= !DTE_VALID_BIT;
}
}
pub fn translation_valid(&self) -> bool {
self.data[0] & DTE_TRANSLATION_VALID_BIT != 0
}
pub fn set_translation_valid(&mut self, value: bool) {
if value {
self.data[0] |= DTE_TRANSLATION_VALID_BIT;
} else {
self.data[0] &= !DTE_TRANSLATION_VALID_BIT;
}
}
pub fn write_permission(&self) -> bool {
self.data[0] & DTE_WRITE_PERMISSION_BIT != 0
}
pub fn set_write_permission(&mut self, value: bool) {
if value {
self.data[0] |= DTE_WRITE_PERMISSION_BIT;
} else {
self.data[0] &= !DTE_WRITE_PERMISSION_BIT;
}
}
pub fn read_permission(&self) -> bool {
self.data[0] & DTE_READ_PERMISSION_BIT != 0
}
pub fn set_read_permission(&mut self, value: bool) {
if value {
self.data[0] |= DTE_READ_PERMISSION_BIT;
} else {
self.data[0] &= !DTE_READ_PERMISSION_BIT;
}
}
pub fn snoop_enable(&self) -> bool {
self.data[0] & DTE_SNOOP_ENABLE_BIT != 0
}
pub fn set_snoop_enable(&mut self, value: bool) {
if value {
self.data[0] |= DTE_SNOOP_ENABLE_BIT;
} else {
self.data[0] &= !DTE_SNOOP_ENABLE_BIT;
}
}
pub fn mode(&self) -> u8 {
((self.data[0] & DTE_MODE_MASK) >> DTE_MODE_SHIFT) as u8
}
pub fn set_mode(&mut self, mode: u8) {
self.data[0] = (self.data[0] & !DTE_MODE_MASK) | (((mode as u64) & 0x7) << DTE_MODE_SHIFT);
}
/// Returns the full, 4KiB-aligned physical address stored in bits 12:51.
pub fn page_table_root(&self) -> u64 {
self.data[0] & DTE_PAGE_TABLE_ROOT_MASK
}
pub fn set_page_table_root(&mut self, phys: u64) {
self.data[0] =
(self.data[0] & !DTE_PAGE_TABLE_ROOT_MASK) | (phys & DTE_PAGE_TABLE_ROOT_MASK);
}
/// Interrupt remapping enable (bit 61 of word 0 in the AMD-Vi DTE).
pub fn interrupt_remap(&self) -> bool {
self.data[0] & DTE_INTERRUPT_REMAP_BIT != 0
}
pub fn set_interrupt_remap(&mut self, value: bool) {
if value {
self.data[0] |= DTE_INTERRUPT_REMAP_BIT;
} else {
self.data[0] &= !DTE_INTERRUPT_REMAP_BIT;
}
}
/// Interrupt write permission (bit 62 of word 0 in the AMD-Vi DTE).
pub fn interrupt_write(&self) -> bool {
self.data[0] & DTE_INTERRUPT_WRITE_BIT != 0
}
pub fn set_interrupt_write(&mut self, value: bool) {
if value {
self.data[0] |= DTE_INTERRUPT_WRITE_BIT;
} else {
self.data[0] &= !DTE_INTERRUPT_WRITE_BIT;
}
}
pub fn int_table_len(&self) -> u8 {
(self.data[1] & DTE_INT_TABLE_LEN_MASK) as u8
}
pub fn set_int_table_len(&mut self, len: u8) {
self.data[1] =
(self.data[1] & !DTE_INT_TABLE_LEN_MASK) | ((len as u64) & DTE_INT_TABLE_LEN_MASK);
}
pub fn interrupt_control(&self) -> u8 {
((self.data[1] & DTE_INT_CONTROL_MASK) >> DTE_INT_CONTROL_SHIFT) as u8
}
pub fn set_interrupt_control(&mut self, control: u8) {
self.data[1] = (self.data[1] & !DTE_INT_CONTROL_MASK)
| (((control as u64) & 0x3) << DTE_INT_CONTROL_SHIFT);
}
/// Returns the interrupt remap table pointer bits stored in word 1.
pub fn int_remap_table_ptr(&self) -> u64 {
self.data[1] & DTE_INT_REMAP_TABLE_PTR_MASK
}
pub fn set_int_remap_table_ptr(&mut self, phys: u64) {
self.data[1] =
(self.data[1] & !DTE_INT_REMAP_TABLE_PTR_MASK) | (phys & DTE_INT_REMAP_TABLE_PTR_MASK);
}
}
const _: () = assert!(size_of::<DeviceTableEntry>() == DTE_SIZE);
/// Device Table — manages the 65536-entry device table.
pub struct DeviceTable {
buffer: DmaBuffer,
}
impl DeviceTable {
/// Allocate a new device table (65536 × 32 bytes = 2 MiB).
pub fn new() -> Result<Self, &'static str> {
let buffer = DmaBuffer::allocate(DEVICE_TABLE_BYTES, 4096)
.map_err(|_| "failed to allocate IOMMU device table")?;
if buffer.len() < DEVICE_TABLE_BYTES {
return Err("IOMMU device table allocation was smaller than requested");
}
if !buffer.is_physically_contiguous() {
return Err("IOMMU device table allocation is not physically contiguous");
}
Ok(Self { buffer })
}
pub fn get_entry(&self, device_id: u16) -> DeviceTableEntry {
self.entries()[device_id as usize]
}
pub fn set_entry(&mut self, device_id: u16, entry: &DeviceTableEntry) {
self.entries_mut()[device_id as usize] = *entry;
}
pub fn clear_entry(&mut self, device_id: u16) {
self.entries_mut()[device_id as usize] = DeviceTableEntry::new();
}
pub fn physical_address(&self) -> usize {
self.buffer.physical_address()
}
/// Convert PCI BDF to device ID.
/// Bus: bits 8:15, Device: bits 3:7, Function: bits 0:2.
pub fn bdf_to_device_id(bus: u8, device: u8, function: u8) -> u16 {
((bus as u16) << 8) | ((device as u16) << 3) | (function as u16)
}
fn entries(&self) -> &[DeviceTableEntry] {
unsafe {
slice::from_raw_parts(
self.buffer.as_ptr() as *const DeviceTableEntry,
DEVICE_TABLE_ENTRIES,
)
}
}
fn entries_mut(&mut self) -> &mut [DeviceTableEntry] {
unsafe {
slice::from_raw_parts_mut(
self.buffer.as_mut_ptr() as *mut DeviceTableEntry,
DEVICE_TABLE_ENTRIES,
)
}
}
}
#[cfg(test)]
mod tests {
use super::{DeviceTable, DeviceTableEntry, DTE_PAGE_TABLE_ROOT_MASK};
fn try_allocate_table() -> Option<DeviceTable> {
match DeviceTable::new() {
Ok(table) => Some(table),
Err(err) => {
eprintln!("skipping DeviceTable allocation-dependent test: {err}");
None
}
}
}
#[test]
fn test_dte_valid_bit() {
let mut entry = DeviceTableEntry::new();
assert!(!entry.valid());
entry.set_valid(true);
assert!(entry.valid());
entry.set_valid(false);
assert!(!entry.valid());
}
#[test]
fn test_dte_translation_valid() {
let mut entry = DeviceTableEntry::new();
assert!(!entry.translation_valid());
entry.set_translation_valid(true);
assert!(entry.translation_valid());
entry.set_translation_valid(false);
assert!(!entry.translation_valid());
}
#[test]
fn test_dte_mode_4level() {
let mut entry = DeviceTableEntry::new();
entry.set_mode(4);
assert_eq!(entry.mode(), 4);
}
#[test]
fn test_dte_permissions_and_interrupt_control() {
let mut entry = DeviceTableEntry::new();
entry.set_read_permission(true);
entry.set_write_permission(true);
entry.set_snoop_enable(true);
entry.set_interrupt_control(0x02);
assert!(entry.read_permission());
assert!(entry.write_permission());
assert!(entry.snoop_enable());
assert_eq!(entry.interrupt_control(), 0x02);
}
#[test]
fn test_dte_page_table_root() {
let mut entry = DeviceTableEntry::new();
entry.set_page_table_root(0x1234_5000);
assert_eq!(entry.page_table_root(), 0x1234_5000);
assert_eq!(entry.data[0] & DTE_PAGE_TABLE_ROOT_MASK, 0x1234_5000);
}
#[test]
fn test_bdf_encoding() {
assert_eq!(DeviceTable::bdf_to_device_id(0x12, 0x05, 0x03), 0x122b);
assert_eq!(DeviceTable::bdf_to_device_id(0xff, 0x1f, 0x07), 0xffff);
}
#[test]
fn test_clear_entry() -> Result<(), &'static str> {
let Some(mut table) = try_allocate_table() else {
return Ok(());
};
let device_id = DeviceTable::bdf_to_device_id(0x02, 0x00, 0x00);
let mut entry = DeviceTableEntry::new();
entry.set_valid(true);
entry.set_translation_valid(true);
entry.set_mode(4);
entry.set_page_table_root(0x1234_5000);
table.set_entry(device_id, &entry);
assert_eq!(table.get_entry(device_id), entry);
table.clear_entry(device_id);
assert_eq!(table.get_entry(device_id), DeviceTableEntry::new());
Ok(())
}
}
@@ -0,0 +1,215 @@
use core::mem::size_of;
use core::slice;
use redox_driver_sys::dma::DmaBuffer;
pub const IRTE_SIZE: usize = 16;
pub const MAX_INTERRUPT_REMAP_ENTRIES: usize = 4096;
const DMA_ALIGNMENT: usize = 4096;
const IRTE_REMAP_ENABLE: u64 = 1 << 0;
const IRTE_SUPPRESS_IOPF: u64 = 1 << 1;
const IRTE_INT_TYPE_SHIFT: u64 = 2;
const IRTE_INT_TYPE_MASK: u64 = 0x7 << IRTE_INT_TYPE_SHIFT;
const IRTE_DEST_MODE: u64 = 1 << 8;
const IRTE_DEST_LOW_SHIFT: u64 = 16;
const IRTE_DEST_LOW_MASK: u64 = 0xFFFF << IRTE_DEST_LOW_SHIFT;
const IRTE_VECTOR_SHIFT: u64 = 32;
const IRTE_VECTOR_MASK: u64 = 0xFF << IRTE_VECTOR_SHIFT;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[repr(C)]
pub struct AmdIrte {
data: [u64; 2],
}
impl AmdIrte {
pub const fn new() -> Self {
Self { data: [0; 2] }
}
pub fn remap_enabled(&self) -> bool {
self.data[0] & IRTE_REMAP_ENABLE != 0
}
pub fn set_remap_enabled(&mut self, value: bool) {
if value {
self.data[0] |= IRTE_REMAP_ENABLE;
} else {
self.data[0] &= !IRTE_REMAP_ENABLE;
}
}
pub fn suppress_io_page_faults(&self) -> bool {
self.data[0] & IRTE_SUPPRESS_IOPF != 0
}
pub fn set_suppress_io_page_faults(&mut self, value: bool) {
if value {
self.data[0] |= IRTE_SUPPRESS_IOPF;
} else {
self.data[0] &= !IRTE_SUPPRESS_IOPF;
}
}
pub fn interrupt_type(&self) -> u8 {
((self.data[0] & IRTE_INT_TYPE_MASK) >> IRTE_INT_TYPE_SHIFT) as u8
}
pub fn set_interrupt_type(&mut self, value: u8) {
self.data[0] = (self.data[0] & !IRTE_INT_TYPE_MASK)
| ((u64::from(value) & 0x7) << IRTE_INT_TYPE_SHIFT);
}
pub fn destination_mode(&self) -> bool {
self.data[0] & IRTE_DEST_MODE != 0
}
pub fn set_destination_mode(&mut self, logical: bool) {
if logical {
self.data[0] |= IRTE_DEST_MODE;
} else {
self.data[0] &= !IRTE_DEST_MODE;
}
}
pub fn destination(&self) -> u32 {
(((self.data[1] & 0xFFFF_FFFF) as u32) << 16)
| (((self.data[0] & IRTE_DEST_LOW_MASK) >> IRTE_DEST_LOW_SHIFT) as u32)
}
pub fn set_destination(&mut self, apic_id: u32) {
self.data[0] = (self.data[0] & !IRTE_DEST_LOW_MASK)
| ((u64::from(apic_id & 0xFFFF)) << IRTE_DEST_LOW_SHIFT);
self.data[1] = (self.data[1] & !0xFFFF_FFFF) | u64::from(apic_id >> 16);
}
pub fn vector(&self) -> u8 {
((self.data[0] & IRTE_VECTOR_MASK) >> IRTE_VECTOR_SHIFT) as u8
}
pub fn set_vector(&mut self, vector: u8) {
self.data[0] =
(self.data[0] & !IRTE_VECTOR_MASK) | (u64::from(vector) << IRTE_VECTOR_SHIFT);
}
}
const _: () = assert!(size_of::<AmdIrte>() == IRTE_SIZE);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct IrteConfig {
pub vector: u8,
pub destination: u32,
pub logical_destination: bool,
pub interrupt_type: u8,
pub suppress_io_page_faults: bool,
}
pub struct InterruptRemapTable {
buffer: DmaBuffer,
capacity: usize,
}
impl InterruptRemapTable {
pub fn new(entry_count: usize) -> Result<Self, &'static str> {
if !(2..=MAX_INTERRUPT_REMAP_ENTRIES).contains(&entry_count) {
return Err("interrupt remap table entry count must be between 2 and 4096");
}
if !entry_count.is_power_of_two() {
return Err("interrupt remap table entry count must be a power of two");
}
let byte_len = entry_count
.checked_mul(IRTE_SIZE)
.ok_or("interrupt remap table size overflow")?;
let buffer = DmaBuffer::allocate(byte_len, DMA_ALIGNMENT)
.map_err(|_| "failed to allocate interrupt remap table")?;
if buffer.len() < byte_len {
return Err("interrupt remap table allocation was smaller than requested");
}
if !buffer.is_physically_contiguous() {
return Err("interrupt remap table allocation is not physically contiguous");
}
Ok(Self {
buffer,
capacity: entry_count,
})
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn len_encoding(&self) -> u8 {
self.capacity.ilog2() as u8 - 1
}
pub fn physical_address(&self) -> usize {
self.buffer.physical_address()
}
pub fn entry(&self, index: usize) -> AmdIrte {
assert!(
index < self.capacity,
"interrupt remap table index out of bounds"
);
self.entries()[index]
}
pub fn set_entry(&mut self, index: usize, entry: AmdIrte) {
assert!(
index < self.capacity,
"interrupt remap table index out of bounds"
);
self.entries_mut()[index] = entry;
}
pub fn clear_entry(&mut self, index: usize) {
self.set_entry(index, AmdIrte::new());
}
pub fn configure(&mut self, index: usize, config: IrteConfig) {
let mut entry = AmdIrte::new();
entry.set_remap_enabled(true);
entry.set_suppress_io_page_faults(config.suppress_io_page_faults);
entry.set_interrupt_type(config.interrupt_type);
entry.set_destination_mode(config.logical_destination);
entry.set_destination(config.destination);
entry.set_vector(config.vector);
self.set_entry(index, entry);
}
fn entries(&self) -> &[AmdIrte] {
unsafe { slice::from_raw_parts(self.buffer.as_ptr().cast::<AmdIrte>(), self.capacity) }
}
fn entries_mut(&mut self) -> &mut [AmdIrte] {
unsafe {
slice::from_raw_parts_mut(self.buffer.as_mut_ptr().cast::<AmdIrte>(), self.capacity)
}
}
}
#[cfg(test)]
mod tests {
use super::AmdIrte;
#[test]
fn irte_accessors_round_trip() {
let mut irte = AmdIrte::new();
irte.set_remap_enabled(true);
irte.set_suppress_io_page_faults(true);
irte.set_interrupt_type(3);
irte.set_destination_mode(true);
irte.set_destination(0x1234_5678);
irte.set_vector(0x52);
assert!(irte.remap_enabled());
assert!(irte.suppress_io_page_faults());
assert_eq!(irte.interrupt_type(), 3);
assert!(irte.destination_mode());
assert_eq!(irte.destination(), 0x1234_5678);
assert_eq!(irte.vector(), 0x52);
}
}
@@ -0,0 +1,868 @@
//! AMD-Vi-backed scheme:iommu implementation.
pub mod acpi;
pub mod amd_vi;
pub mod command_buffer;
pub mod device_table;
pub mod interrupt;
pub mod mmio;
pub mod page_table;
use std::collections::BTreeMap;
use acpi::{parse_bdf, Bdf};
use amd_vi::AmdViUnit;
use page_table::{DomainPageTables, MappingFlags};
use redox_scheme::SchemeBlockMut;
use syscall::data::Stat;
use syscall::error::{Error, Result, EBADF, EINVAL, EIO, EISDIR, ENODEV, ENOENT};
use syscall::flag::{EventFlags, MODE_DIR, MODE_FILE, SEEK_CUR, SEEK_END, SEEK_SET};
pub const IOMMU_PROTOCOL_VERSION: u16 = 1;
pub mod opcode {
pub const QUERY: u16 = 0x0000;
pub const CREATE_DOMAIN: u16 = 0x0001;
pub const DESTROY_DOMAIN: u16 = 0x0002;
pub const MAP: u16 = 0x0010;
pub const UNMAP: u16 = 0x0011;
pub const ASSIGN_DEVICE: u16 = 0x0020;
pub const UNASSIGN_DEVICE: u16 = 0x0021;
pub const DRAIN_EVENTS: u16 = 0x0030;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct IommuRequest {
pub opcode: u16,
pub version: u16,
pub arg0: u32,
pub arg1: u64,
pub arg2: u64,
pub arg3: u64,
}
impl IommuRequest {
pub const SIZE: usize = 32;
pub const fn new(opcode: u16, arg0: u32, arg1: u64, arg2: u64, arg3: u64) -> Self {
Self {
opcode,
version: IOMMU_PROTOCOL_VERSION,
arg0,
arg1,
arg2,
arg3,
}
}
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
let header = bytes.get(..Self::SIZE)?;
Some(Self {
opcode: u16::from_le_bytes(header.get(0..2)?.try_into().ok()?),
version: u16::from_le_bytes(header.get(2..4)?.try_into().ok()?),
arg0: u32::from_le_bytes(header.get(4..8)?.try_into().ok()?),
arg1: u64::from_le_bytes(header.get(8..16)?.try_into().ok()?),
arg2: u64::from_le_bytes(header.get(16..24)?.try_into().ok()?),
arg3: u64::from_le_bytes(header.get(24..32)?.try_into().ok()?),
})
}
pub fn to_bytes(self) -> [u8; Self::SIZE] {
let mut bytes = [0u8; Self::SIZE];
bytes[0..2].copy_from_slice(&self.opcode.to_le_bytes());
bytes[2..4].copy_from_slice(&self.version.to_le_bytes());
bytes[4..8].copy_from_slice(&self.arg0.to_le_bytes());
bytes[8..16].copy_from_slice(&self.arg1.to_le_bytes());
bytes[16..24].copy_from_slice(&self.arg2.to_le_bytes());
bytes[24..32].copy_from_slice(&self.arg3.to_le_bytes());
bytes
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct IommuResponse {
pub status: i32,
pub kind: u16,
pub version: u16,
pub arg0: u32,
pub arg1: u64,
pub arg2: u64,
pub arg3: u64,
}
impl IommuResponse {
pub const SIZE: usize = 36;
pub const fn success(kind: u16, arg0: u32, arg1: u64, arg2: u64, arg3: u64) -> Self {
Self {
status: 0,
kind,
version: IOMMU_PROTOCOL_VERSION,
arg0,
arg1,
arg2,
arg3,
}
}
pub const fn error(kind: u16, errno: i32) -> Self {
Self {
status: -errno,
kind,
version: IOMMU_PROTOCOL_VERSION,
arg0: 0,
arg1: 0,
arg2: 0,
arg3: 0,
}
}
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
let header = bytes.get(..Self::SIZE)?;
Some(Self {
status: i32::from_le_bytes(header.get(0..4)?.try_into().ok()?),
kind: u16::from_le_bytes(header.get(4..6)?.try_into().ok()?),
version: u16::from_le_bytes(header.get(6..8)?.try_into().ok()?),
arg0: u32::from_le_bytes(header.get(8..12)?.try_into().ok()?),
arg1: u64::from_le_bytes(header.get(12..20)?.try_into().ok()?),
arg2: u64::from_le_bytes(header.get(20..28)?.try_into().ok()?),
arg3: u64::from_le_bytes(header.get(28..36)?.try_into().ok()?),
})
}
pub fn to_bytes(self) -> [u8; Self::SIZE] {
let mut bytes = [0u8; Self::SIZE];
bytes[0..4].copy_from_slice(&self.status.to_le_bytes());
bytes[4..6].copy_from_slice(&self.kind.to_le_bytes());
bytes[6..8].copy_from_slice(&self.version.to_le_bytes());
bytes[8..12].copy_from_slice(&self.arg0.to_le_bytes());
bytes[12..20].copy_from_slice(&self.arg1.to_le_bytes());
bytes[20..28].copy_from_slice(&self.arg2.to_le_bytes());
bytes[28..36].copy_from_slice(&self.arg3.to_le_bytes());
bytes
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum HandleKind {
Root,
Control,
Domain(u16),
Device(Bdf),
}
#[derive(Clone, Debug)]
struct Handle {
kind: HandleKind,
offset: usize,
response: Vec<u8>,
}
pub struct IommuScheme {
units: Vec<AmdViUnit>,
next_id: usize,
handles: BTreeMap<usize, Handle>,
domains: BTreeMap<u16, DomainPageTables>,
device_assignments: BTreeMap<Bdf, (u16, usize)>,
}
impl IommuScheme {
pub fn new() -> Self {
Self::with_units(Vec::new())
}
pub fn with_units(units: Vec<AmdViUnit>) -> Self {
Self {
units,
next_id: 0,
handles: BTreeMap::new(),
domains: BTreeMap::new(),
device_assignments: BTreeMap::new(),
}
}
pub fn unit_count(&self) -> usize {
self.units.len()
}
fn insert_handle(&mut self, kind: HandleKind) -> usize {
let id = self.next_id;
self.next_id = self.next_id.saturating_add(1);
self.handles.insert(
id,
Handle {
kind,
offset: 0,
response: Vec::new(),
},
);
id
}
fn ensure_domain_exists(&mut self, domain_id: u16) -> core::result::Result<(), i32> {
if self.domains.contains_key(&domain_id) {
return Ok(());
}
let domain = DomainPageTables::new(domain_id).map_err(|_| EIO as i32)?;
self.domains.insert(domain_id, domain);
Ok(())
}
fn next_domain_id(&self) -> Option<u16> {
(1..u16::MAX).find(|domain_id| !self.domains.contains_key(domain_id))
}
fn root_listing(&self) -> Vec<u8> {
let mut listing = String::from("control\n");
for (index, unit) in self.units.iter().enumerate() {
let state = if unit.initialized() {
"initialized"
} else {
"detected"
};
listing.push_str(&format!(
"unit/{index} {} mmio={:#x} state={}\n",
unit.info().iommu_bdf,
unit.info().mmio_base,
state
));
}
for domain_id in self.domains.keys() {
listing.push_str(&format!("domain/{domain_id}\n"));
}
for bdf in self.device_assignments.keys() {
listing.push_str(&format!("device/{bdf}\n"));
}
listing.into_bytes()
}
fn parse_domain_id(path: &str) -> Option<u16> {
let trimmed = path.trim();
trimmed
.strip_prefix("0x")
.and_then(|hex| u16::from_str_radix(hex, 16).ok())
.or_else(|| trimmed.parse::<u16>().ok())
.or_else(|| u16::from_str_radix(trimmed, 16).ok())
}
fn map_flags(bits: u32) -> MappingFlags {
let flags = MappingFlags {
readable: bits & 0x1 != 0,
writable: bits & 0x2 != 0,
executable: bits & 0x4 != 0,
force_coherent: bits & 0x8 != 0,
user: bits & 0x10 != 0,
};
if !flags.readable
&& !flags.writable
&& !flags.executable
&& !flags.force_coherent
&& !flags.user
{
MappingFlags::read_write()
} else {
flags
}
}
fn choose_unit_for_device(
&self,
bdf: Bdf,
requested_unit: Option<usize>,
) -> core::result::Result<usize, i32> {
if let Some(index) = requested_unit {
let Some(unit) = self.units.get(index) else {
return Err(ENODEV as i32);
};
if unit.handles_device(bdf) {
return Ok(index);
}
return Err(ENODEV as i32);
}
self.units
.iter()
.position(|unit| unit.handles_device(bdf))
.ok_or(ENODEV as i32)
}
fn dispatch_request(&mut self, kind: HandleKind, request: IommuRequest) -> IommuResponse {
if request.version != IOMMU_PROTOCOL_VERSION {
return IommuResponse::error(request.opcode, EINVAL as i32);
}
match kind {
HandleKind::Root => IommuResponse::error(request.opcode, EISDIR as i32),
HandleKind::Control => self.handle_control_request(request),
HandleKind::Domain(domain_id) => self.handle_domain_request(domain_id, request),
HandleKind::Device(bdf) => self.handle_device_request(bdf, request),
}
}
fn handle_control_request(&mut self, request: IommuRequest) -> IommuResponse {
match request.opcode {
opcode::QUERY => IommuResponse::success(
request.opcode,
self.units.len() as u32,
self.domains.len() as u64,
self.device_assignments.len() as u64,
self.units.iter().filter(|unit| unit.initialized()).count() as u64,
),
opcode::CREATE_DOMAIN => {
let domain_id = if request.arg0 == 0 {
match self.next_domain_id() {
Some(domain_id) => domain_id,
None => return IommuResponse::error(request.opcode, EIO as i32),
}
} else {
request.arg0 as u16
};
if let Err(errno) = self.ensure_domain_exists(domain_id) {
return IommuResponse::error(request.opcode, errno);
}
let Some(domain) = self.domains.get(&domain_id) else {
return IommuResponse::error(request.opcode, EIO as i32);
};
IommuResponse::success(
request.opcode,
domain_id as u32,
domain.root_address(),
domain.levels() as u64,
domain.mapping_count() as u64,
)
}
opcode::DESTROY_DOMAIN => {
let domain_id = request.arg0 as u16;
if self
.device_assignments
.values()
.any(|(assigned_domain, _)| *assigned_domain == domain_id)
{
return IommuResponse::error(request.opcode, EINVAL as i32);
}
if self.domains.remove(&domain_id).is_none() {
return IommuResponse::error(request.opcode, ENOENT as i32);
}
IommuResponse::success(request.opcode, domain_id as u32, 0, 0, 0)
}
opcode::DRAIN_EVENTS => {
let requested_index = if request.arg0 == u32::MAX {
None
} else {
Some(request.arg0 as usize)
};
let mut count = 0u32;
let mut first_code = 0u64;
let mut first_device = 0u64;
let mut first_address = 0u64;
for (index, unit) in self.units.iter_mut().enumerate() {
if requested_index.is_some() && requested_index != Some(index) {
continue;
}
match unit.drain_events() {
Ok(events) => {
if let Some(event) = events.first() {
if count == 0 {
first_code = u64::from(event.event_code);
first_device = u64::from(event.device_id.raw());
first_address = event.address;
}
count = count.saturating_add(events.len() as u32);
}
}
Err(_) => return IommuResponse::error(request.opcode, EIO as i32),
}
}
IommuResponse::success(
request.opcode,
count,
first_code,
first_device,
first_address,
)
}
_ => IommuResponse::error(request.opcode, EINVAL as i32),
}
}
fn handle_domain_request(&mut self, domain_id: u16, request: IommuRequest) -> IommuResponse {
if let Err(errno) = self.ensure_domain_exists(domain_id) {
return IommuResponse::error(request.opcode, errno);
}
match request.opcode {
opcode::QUERY => {
let Some(domain) = self.domains.get(&domain_id) else {
return IommuResponse::error(request.opcode, ENOENT as i32);
};
IommuResponse::success(
request.opcode,
domain_id as u32,
domain.root_address(),
domain.levels() as u64,
domain.mapping_count() as u64,
)
}
opcode::MAP => {
let flags = Self::map_flags(request.arg0);
let preferred_iova = if request.arg3 == 0 {
None
} else {
Some(request.arg3)
};
let Some(domain) = self.domains.get_mut(&domain_id) else {
return IommuResponse::error(request.opcode, ENOENT as i32);
};
match domain.map_range(request.arg1, request.arg2, flags, preferred_iova) {
Ok(iova) => IommuResponse::success(
request.opcode,
domain_id as u32,
iova,
request.arg2,
0,
),
Err(_) => IommuResponse::error(request.opcode, EIO as i32),
}
}
opcode::UNMAP => {
let Some(domain) = self.domains.get_mut(&domain_id) else {
return IommuResponse::error(request.opcode, ENOENT as i32);
};
match domain.unmap_range(request.arg1) {
Ok(size) => IommuResponse::success(
request.opcode,
domain_id as u32,
request.arg1,
size,
0,
),
Err(_) => IommuResponse::error(request.opcode, ENOENT as i32),
}
}
_ => IommuResponse::error(request.opcode, EINVAL as i32),
}
}
fn handle_device_request(&mut self, bdf: Bdf, request: IommuRequest) -> IommuResponse {
match request.opcode {
opcode::QUERY => {
let (domain_id, unit_index) = self
.device_assignments
.get(&bdf)
.copied()
.unwrap_or((0, usize::MAX));
IommuResponse::success(
request.opcode,
domain_id as u32,
if unit_index == usize::MAX {
u64::MAX
} else {
unit_index as u64
},
u64::from(bdf.raw()),
0,
)
}
opcode::ASSIGN_DEVICE => {
let domain_id = request.arg0 as u16;
if let Err(errno) = self.ensure_domain_exists(domain_id) {
return IommuResponse::error(request.opcode, errno);
}
let requested_unit = if request.arg1 == u64::MAX {
None
} else {
Some(request.arg1 as usize)
};
let unit_index = match self.choose_unit_for_device(bdf, requested_unit) {
Ok(index) => index,
Err(errno) => return IommuResponse::error(request.opcode, errno),
};
let Some(domain) = self.domains.get(&domain_id) else {
return IommuResponse::error(request.opcode, ENOENT as i32);
};
let Some(unit) = self.units.get_mut(unit_index) else {
return IommuResponse::error(request.opcode, ENODEV as i32);
};
match unit.assign_device(bdf, domain) {
Ok(()) => {
self.device_assignments.insert(bdf, (domain_id, unit_index));
IommuResponse::success(
request.opcode,
domain_id as u32,
unit_index as u64,
u64::from(bdf.raw()),
0,
)
}
Err(_) => IommuResponse::error(request.opcode, EIO as i32),
}
}
opcode::UNASSIGN_DEVICE => {
if self.device_assignments.remove(&bdf).is_none() {
return IommuResponse::error(request.opcode, ENOENT as i32);
}
IommuResponse::success(request.opcode, 0, u64::from(bdf.raw()), 0, 0)
}
_ => IommuResponse::error(request.opcode, EINVAL as i32),
}
}
}
impl Default for IommuScheme {
fn default() -> Self {
Self::new()
}
}
impl SchemeBlockMut for IommuScheme {
fn open(&mut self, path: &str, _flags: usize, _uid: u32, _gid: u32) -> Result<Option<usize>> {
let cleaned = path.trim_matches('/');
let kind = if cleaned.is_empty() {
HandleKind::Root
} else if cleaned == "control" {
HandleKind::Control
} else if let Some(rest) = cleaned.strip_prefix("domain/") {
let domain_id = Self::parse_domain_id(rest).ok_or(Error::new(ENOENT))?;
self.ensure_domain_exists(domain_id).map_err(Error::new)?;
HandleKind::Domain(domain_id)
} else if let Some(rest) = cleaned.strip_prefix("device/") {
let bdf = parse_bdf(rest).ok_or(Error::new(ENOENT))?;
HandleKind::Device(bdf)
} else {
return Err(Error::new(ENOENT));
};
Ok(Some(self.insert_handle(kind)))
}
fn read(&mut self, id: usize, buf: &mut [u8]) -> Result<Option<usize>> {
let (kind, offset, response) = {
let handle = self.handles.get(&id).ok_or(Error::new(EBADF))?;
(handle.kind, handle.offset, handle.response.clone())
};
let content = match kind {
HandleKind::Root => self.root_listing(),
_ => response,
};
if offset >= content.len() {
return Ok(Some(0));
}
let to_copy = (content.len() - offset).min(buf.len());
buf[..to_copy].copy_from_slice(&content[offset..offset + to_copy]);
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
handle.offset = offset + to_copy;
Ok(Some(to_copy))
}
fn write(&mut self, id: usize, buf: &[u8]) -> Result<Option<usize>> {
let kind = self
.handles
.get(&id)
.map(|handle| handle.kind)
.ok_or(Error::new(EBADF))?;
if kind == HandleKind::Root {
return Err(Error::new(EISDIR));
}
let response = match IommuRequest::from_bytes(buf) {
Some(request) => self.dispatch_request(kind, request),
None => IommuResponse::error(0, EINVAL as i32),
};
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
handle.response = response.to_bytes().to_vec();
handle.offset = 0;
Ok(Some(buf.len()))
}
fn seek(&mut self, id: usize, pos: isize, whence: usize) -> Result<Option<isize>> {
let (kind, current_offset, response_len) = {
let handle = self.handles.get(&id).ok_or(Error::new(EBADF))?;
(handle.kind, handle.offset, handle.response.len())
};
let content_len = match kind {
HandleKind::Root => self.root_listing().len(),
_ => response_len,
};
let new_offset = match whence {
SEEK_SET => pos,
SEEK_CUR => current_offset as isize + pos,
SEEK_END => content_len as isize + pos,
_ => return Err(Error::new(EINVAL)),
};
if new_offset < 0 {
return Err(Error::new(EINVAL));
}
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
handle.offset = new_offset as usize;
Ok(Some(new_offset))
}
fn fpath(&mut self, id: usize, buf: &mut [u8]) -> Result<Option<usize>> {
let kind = self
.handles
.get(&id)
.map(|handle| handle.kind)
.ok_or(Error::new(EBADF))?;
let path = match kind {
HandleKind::Root => "iommu:".to_string(),
HandleKind::Control => "iommu:control".to_string(),
HandleKind::Domain(domain_id) => format!("iommu:domain/{domain_id}"),
HandleKind::Device(bdf) => format!("iommu:device/{bdf}"),
};
let bytes = path.as_bytes();
let to_copy = bytes.len().min(buf.len());
buf[..to_copy].copy_from_slice(&bytes[..to_copy]);
Ok(Some(to_copy))
}
fn fstat(&mut self, id: usize, stat: &mut Stat) -> Result<Option<usize>> {
let kind = self
.handles
.get(&id)
.map(|handle| handle.kind)
.ok_or(Error::new(EBADF))?;
match kind {
HandleKind::Root => {
stat.st_mode = MODE_DIR | 0o555;
stat.st_size = self.root_listing().len() as u64;
}
_ => {
let response_len = self
.handles
.get(&id)
.map(|handle| handle.response.len())
.ok_or(Error::new(EBADF))?;
stat.st_mode = MODE_FILE | 0o666;
stat.st_size = response_len as u64;
}
}
stat.st_blksize = 4096;
stat.st_blocks = stat.st_size.div_ceil(512);
Ok(Some(0))
}
fn fevent(&mut self, id: usize, _flags: EventFlags) -> Result<Option<EventFlags>> {
let _ = self.handles.get(&id).ok_or(Error::new(EBADF))?;
Ok(Some(EventFlags::empty()))
}
fn close(&mut self, id: usize) -> Result<Option<usize>> {
if self.handles.remove(&id).is_none() {
return Err(Error::new(EBADF));
}
Ok(Some(0))
}
}
#[cfg(all(test, not(target_os = "redox")))]
mod host_redox_stubs {
use core::ptr;
use syscall::error::{EINVAL, ENOSYS};
fn error_result(errno: i32) -> usize {
usize::wrapping_neg(errno as usize)
}
#[no_mangle]
pub extern "C" fn redox_open_v1(
_path_base: *const u8,
_path_len: usize,
_flags: u32,
_mode: u16,
) -> usize {
error_result(ENOSYS)
}
#[no_mangle]
pub extern "C" fn redox_openat_v1(
_fd: usize,
_buf: *const u8,
_path_len: usize,
_flags: u32,
_fcntl_flags: u32,
) -> usize {
error_result(ENOSYS)
}
#[no_mangle]
pub extern "C" fn redox_close_v1(_fd: usize) -> usize {
0
}
#[no_mangle]
pub extern "C" fn redox_mmap_v1(
_addr: *mut (),
_unaligned_len: usize,
_prot: u32,
_flags: u32,
_fd: usize,
_offset: u64,
) -> usize {
error_result(ENOSYS)
}
#[no_mangle]
pub extern "C" fn redox_munmap_v1(_addr: *mut (), _unaligned_len: usize) -> usize {
0
}
#[no_mangle]
pub extern "C" fn redox_sys_call_v0(
_fd: usize,
_payload: *mut u8,
_payload_len: usize,
_flags: usize,
_metadata: *const u64,
_metadata_len: usize,
) -> usize {
error_result(ENOSYS)
}
#[no_mangle]
pub extern "C" fn redox_strerror_v1(dst: *mut u8, dst_len: *mut usize, _error: u32) -> usize {
if dst.is_null() || dst_len.is_null() {
return error_result(EINVAL);
}
let message = b"host test stub";
unsafe {
let writable = *dst_len;
let count = writable.min(message.len());
ptr::copy_nonoverlapping(message.as_ptr(), dst, count);
*dst_len = count;
}
0
}
}
#[cfg(test)]
mod tests {
use super::{opcode, IommuRequest, IommuResponse, IommuScheme};
use crate::page_table::PAGE_SIZE;
use redox_scheme::SchemeBlockMut;
fn read_response(scheme: &mut IommuScheme, id: usize) -> IommuResponse {
let mut bytes = [0u8; IommuResponse::SIZE];
let count = scheme
.read(id, &mut bytes)
.unwrap_or_else(|err| panic!("read failed: {err}"))
.unwrap_or_else(|| panic!("expected response bytes"));
IommuResponse::from_bytes(&bytes[..count])
.unwrap_or_else(|| panic!("invalid response bytes"))
}
#[test]
fn request_round_trip_serialization() {
let request = IommuRequest::new(opcode::MAP, 7, 0x1000, 0x2000, 0x3000);
let encoded = request.to_bytes();
let decoded = IommuRequest::from_bytes(&encoded)
.unwrap_or_else(|| panic!("failed to deserialize request"));
assert_eq!(decoded, request);
}
#[test]
fn root_lists_control_endpoint() {
let mut scheme = IommuScheme::new();
let root = scheme
.open("", 0, 0, 0)
.unwrap_or_else(|err| panic!("open root failed: {err}"))
.unwrap_or_else(|| panic!("root open returned no handle"));
let mut bytes = [0u8; 128];
let count = scheme
.read(root, &mut bytes)
.unwrap_or_else(|err| panic!("read root failed: {err}"))
.unwrap_or_else(|| panic!("expected root bytes"));
let listing = String::from_utf8_lossy(&bytes[..count]);
assert!(listing.contains("control"));
}
#[test]
fn control_can_create_and_query_domain() {
let mut scheme = IommuScheme::new();
let control = scheme
.open("control", 0, 0, 0)
.unwrap_or_else(|err| panic!("open control failed: {err}"))
.unwrap_or_else(|| panic!("control open returned no handle"));
let request = IommuRequest::new(opcode::CREATE_DOMAIN, 7, 0, 0, 0);
scheme
.write(control, &request.to_bytes())
.unwrap_or_else(|err| panic!("create domain write failed: {err}"));
let response = read_response(&mut scheme, control);
assert_eq!(response.status, 0);
assert_eq!(response.arg0, 7);
assert_ne!(response.arg1, 0);
let query = IommuRequest::new(opcode::QUERY, 0, 0, 0, 0);
scheme
.write(control, &query.to_bytes())
.unwrap_or_else(|err| panic!("control query failed: {err}"));
let query_response = read_response(&mut scheme, control);
assert_eq!(query_response.status, 0);
assert_eq!(query_response.arg0, 0);
assert_eq!(query_response.arg1, 1);
}
#[test]
fn domain_handle_can_map_pages() {
let mut scheme = IommuScheme::new();
let domain = scheme
.open("domain/5", 0, 0, 0)
.unwrap_or_else(|err| panic!("open domain failed: {err}"))
.unwrap_or_else(|| panic!("domain open returned no handle"));
let map = IommuRequest::new(opcode::MAP, 0x3, 0x4000_0000, PAGE_SIZE * 2, 0);
scheme
.write(domain, &map.to_bytes())
.unwrap_or_else(|err| panic!("domain map write failed: {err}"));
let response = read_response(&mut scheme, domain);
assert_eq!(response.status, 0);
assert_eq!(response.arg0, 5);
assert_ne!(response.arg1, 0);
let unmap = IommuRequest::new(opcode::UNMAP, 0, response.arg1, 0, 0);
scheme
.write(domain, &unmap.to_bytes())
.unwrap_or_else(|err| panic!("domain unmap write failed: {err}"));
let unmap_response = read_response(&mut scheme, domain);
assert_eq!(unmap_response.status, 0);
assert_eq!(unmap_response.arg2, PAGE_SIZE * 2);
}
#[test]
fn assigning_without_detected_units_returns_error_response() {
let mut scheme = IommuScheme::new();
let device = scheme
.open("device/00:14.0", 0, 0, 0)
.unwrap_or_else(|err| panic!("open device failed: {err}"))
.unwrap_or_else(|| panic!("device open returned no handle"));
let assign = IommuRequest::new(opcode::ASSIGN_DEVICE, 1, u64::MAX, 0, 0);
scheme
.write(device, &assign.to_bytes())
.unwrap_or_else(|err| panic!("device assign write failed: {err}"));
let response = read_response(&mut scheme, device);
assert!(response.status < 0);
}
}
@@ -0,0 +1,135 @@
//! IOMMU daemon — provides scheme:iommu for DMA remapping.
use std::env;
use std::fs;
use std::process;
use iommu::amd_vi::AmdViUnit;
#[cfg(target_os = "redox")]
use iommu::IommuScheme;
use log::{error, info, LevelFilter, Metadata, Record};
#[cfg(target_os = "redox")]
use redox_scheme::{SignalBehavior, Socket};
struct StderrLogger {
level: LevelFilter,
}
impl log::Log for StderrLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= self.level
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
eprintln!("[{}] {}", record.level(), record.args());
}
}
fn flush(&self) {}
}
fn init_logging(level: LevelFilter) {
if log::set_boxed_logger(Box::new(StderrLogger { level })).is_err() {
return;
}
log::set_max_level(level);
}
fn detect_units_from_env() -> Result<Vec<AmdViUnit>, String> {
let Some(path) = env::var_os("IOMMU_IVRS_PATH") else {
return Ok(Vec::new());
};
let bytes = fs::read(&path).map_err(|err| {
format!(
"failed to read IVRS table from {}: {err}",
path.to_string_lossy()
)
})?;
let units = AmdViUnit::detect(&bytes).map_err(|err| format!("failed to parse IVRS: {err}"))?;
Ok(units)
}
#[cfg(target_os = "redox")]
fn run() -> Result<(), String> {
let mut units = detect_units_from_env()?;
info!("iommu: detected {} AMD-Vi unit(s)", units.len());
for (index, unit) in units.iter_mut().enumerate() {
match unit.init() {
Ok(()) => info!(
"iommu: initialized unit {} at MMIO {:#x}",
index,
unit.info().mmio_base
),
Err(err) => error!(
"iommu: failed to initialize unit {} at MMIO {:#x}: {}",
index,
unit.info().mmio_base,
err
),
}
}
let socket =
Socket::create("iommu").map_err(|e| format!("failed to register iommu scheme: {e}"))?;
info!("iommu: registered scheme:iommu");
let mut scheme = IommuScheme::with_units(units);
loop {
let request = match socket.next_request(SignalBehavior::Restart) {
Ok(Some(request)) => request,
Ok(None) => {
info!("iommu: scheme unmounted, exiting");
break;
}
Err(e) => {
error!("iommu: failed to read scheme request: {e}");
continue;
}
};
let response = match request.handle_scheme_block_mut(&mut scheme) {
Ok(response) => response,
Err(_request) => {
error!("iommu: failed to handle request");
continue;
}
};
if let Err(e) = socket.write_response(response, SignalBehavior::Restart) {
error!("iommu: failed to write response: {e}");
}
}
Ok(())
}
#[cfg(not(target_os = "redox"))]
fn run() -> Result<(), String> {
let units = detect_units_from_env()?;
info!(
"iommu: host build stub active; parsed {} AMD-Vi unit(s) from IOMMU_IVRS_PATH",
units.len()
);
Ok(())
}
fn main() {
let log_level = match env::var("IOMMU_LOG").as_deref() {
Ok("debug") => LevelFilter::Debug,
Ok("trace") => LevelFilter::Trace,
Ok("warn") => LevelFilter::Warn,
Ok("error") => LevelFilter::Error,
_ => LevelFilter::Info,
};
init_logging(log_level);
if let Err(e) = run() {
error!("iommu: fatal error: {e}");
process::exit(1);
}
}
@@ -0,0 +1,241 @@
use core::mem::{offset_of, size_of};
use core::ptr::{addr_of, addr_of_mut, read_volatile, write_volatile};
pub const AMD_VI_MMIO_BYTES: usize = 0x2038;
pub mod offsets {
pub const DEV_TABLE_BAR: usize = 0x0000;
pub const CMD_BUF_BAR: usize = 0x0008;
pub const EVT_LOG_BAR: usize = 0x0010;
pub const CONTROL: usize = 0x0018;
pub const EXCLUSION_BASE: usize = 0x0020;
pub const EXCLUSION_LIMIT: usize = 0x0028;
pub const EXTENDED_FEATURE: usize = 0x0030;
pub const PPR_LOG_BAR: usize = 0x0038;
pub const CMD_BUF_HEAD: usize = 0x2000;
pub const CMD_BUF_TAIL: usize = 0x2008;
pub const EVT_LOG_HEAD: usize = 0x2010;
pub const EVT_LOG_TAIL: usize = 0x2018;
pub const STATUS: usize = 0x2020;
pub const PPR_LOG_HEAD: usize = 0x2028;
pub const PPR_LOG_TAIL: usize = 0x2030;
}
pub mod control {
pub const IOMMU_ENABLE: u32 = 1 << 0;
pub const HT_TUN_EN: u32 = 1 << 1;
pub const EVENT_LOG_EN: u32 = 1 << 2;
pub const EVENT_INT_EN: u32 = 1 << 3;
pub const COM_WAIT_INT_EN: u32 = 1 << 4;
pub const CMD_BUF_EN: u32 = 1 << 5;
pub const PPR_LOG_EN: u32 = 1 << 6;
pub const PPR_INT_EN: u32 = 1 << 7;
pub const PPR_EN: u32 = 1 << 8;
pub const GT_EN: u32 = 1 << 9;
pub const GA_EN: u32 = 1 << 10;
pub const CRW: u32 = 1 << 12;
pub const SMIF_EN: u32 = 1 << 13;
pub const SLFW_EN: u32 = 1 << 14;
pub const SMIF_LOG_EN: u32 = 1 << 15;
pub const GAM_EN_0: u32 = 1 << 16;
pub const GAM_EN_1: u32 = 1 << 17;
pub const GAM_EN_2: u32 = 1 << 18;
pub const XT_EN: u32 = 1 << 22;
pub const NX_EN: u32 = 1 << 23;
pub const IRQ_TABLE_LEN_EN: u32 = 1 << 24;
}
pub mod status {
pub const IOMMU_RUNNING: u32 = 1 << 0;
pub const EVENT_OVERFLOW: u32 = 1 << 1;
pub const EVENT_LOG_INT: u32 = 1 << 2;
pub const COM_WAIT_INT: u32 = 1 << 3;
pub const PPR_OVERFLOW: u32 = 1 << 4;
pub const PPR_INT: u32 = 1 << 5;
}
pub mod ext_feature {
pub const PREF_SUP: u64 = 1 << 0;
pub const PPR_SUP: u64 = 1 << 1;
pub const XT_SUP: u64 = 1 << 2;
pub const NX_SUP: u64 = 1 << 3;
pub const GT_SUP: u64 = 1 << 4;
pub const IA_SUP: u64 = 1 << 6;
pub const GA_SUP: u64 = 1 << 7;
pub const HE_SUP: u64 = 1 << 8;
pub const PC_SUP: u64 = 1 << 9;
pub const GI_SUP: u64 = 1 << 57;
pub const HA_SUP: u64 = 1 << 58;
}
#[repr(C)]
pub struct AmdViMmio {
pub dev_table_bar: u64,
pub cmd_buf_bar: u64,
pub evt_log_bar: u64,
pub control: u32,
_reserved0: u32,
pub exclusion_base: u64,
pub exclusion_limit: u64,
pub extended_feature: u64,
pub ppr_log_bar: u64,
_reserved1: [u8; 0x2000 - 0x40],
pub cmd_buf_head: u64,
pub cmd_buf_tail: u64,
pub evt_log_head: u64,
pub evt_log_tail: u64,
pub status: u32,
_reserved2: u32,
pub ppr_log_head: u64,
pub ppr_log_tail: u64,
}
const _: () = assert!(size_of::<AmdViMmio>() == AMD_VI_MMIO_BYTES);
const _: () = assert!(offset_of!(AmdViMmio, dev_table_bar) == offsets::DEV_TABLE_BAR);
const _: () = assert!(offset_of!(AmdViMmio, cmd_buf_bar) == offsets::CMD_BUF_BAR);
const _: () = assert!(offset_of!(AmdViMmio, evt_log_bar) == offsets::EVT_LOG_BAR);
const _: () = assert!(offset_of!(AmdViMmio, control) == offsets::CONTROL);
const _: () = assert!(offset_of!(AmdViMmio, extended_feature) == offsets::EXTENDED_FEATURE);
const _: () = assert!(offset_of!(AmdViMmio, cmd_buf_head) == offsets::CMD_BUF_HEAD);
const _: () = assert!(offset_of!(AmdViMmio, cmd_buf_tail) == offsets::CMD_BUF_TAIL);
const _: () = assert!(offset_of!(AmdViMmio, evt_log_head) == offsets::EVT_LOG_HEAD);
const _: () = assert!(offset_of!(AmdViMmio, evt_log_tail) == offsets::EVT_LOG_TAIL);
const _: () = assert!(offset_of!(AmdViMmio, status) == offsets::STATUS);
const _: () = assert!(offset_of!(AmdViMmio, ppr_log_head) == offsets::PPR_LOG_HEAD);
const _: () = assert!(offset_of!(AmdViMmio, ppr_log_tail) == offsets::PPR_LOG_TAIL);
impl AmdViMmio {
pub unsafe fn read_dev_table_bar(base: *mut Self) -> u64 {
read_volatile(addr_of!((*base).dev_table_bar))
}
pub unsafe fn write_dev_table_bar(base: *mut Self, value: u64) {
write_volatile(addr_of_mut!((*base).dev_table_bar), value);
}
pub unsafe fn read_cmd_buf_bar(base: *mut Self) -> u64 {
read_volatile(addr_of!((*base).cmd_buf_bar))
}
pub unsafe fn write_cmd_buf_bar(base: *mut Self, value: u64) {
write_volatile(addr_of_mut!((*base).cmd_buf_bar), value);
}
pub unsafe fn read_evt_log_bar(base: *mut Self) -> u64 {
read_volatile(addr_of!((*base).evt_log_bar))
}
pub unsafe fn write_evt_log_bar(base: *mut Self, value: u64) {
write_volatile(addr_of_mut!((*base).evt_log_bar), value);
}
pub unsafe fn read_control(base: *mut Self) -> u32 {
read_volatile(addr_of!((*base).control))
}
pub unsafe fn write_control(base: *mut Self, value: u32) {
write_volatile(addr_of_mut!((*base).control), value);
}
pub unsafe fn read_exclusion_base(base: *mut Self) -> u64 {
read_volatile(addr_of!((*base).exclusion_base))
}
pub unsafe fn write_exclusion_base(base: *mut Self, value: u64) {
write_volatile(addr_of_mut!((*base).exclusion_base), value);
}
pub unsafe fn read_exclusion_limit(base: *mut Self) -> u64 {
read_volatile(addr_of!((*base).exclusion_limit))
}
pub unsafe fn write_exclusion_limit(base: *mut Self, value: u64) {
write_volatile(addr_of_mut!((*base).exclusion_limit), value);
}
pub unsafe fn read_extended_feature(base: *mut Self) -> u64 {
read_volatile(addr_of!((*base).extended_feature))
}
pub unsafe fn read_ppr_log_bar(base: *mut Self) -> u64 {
read_volatile(addr_of!((*base).ppr_log_bar))
}
pub unsafe fn write_ppr_log_bar(base: *mut Self, value: u64) {
write_volatile(addr_of_mut!((*base).ppr_log_bar), value);
}
pub unsafe fn read_cmd_buf_head(base: *mut Self) -> u64 {
read_volatile(addr_of!((*base).cmd_buf_head))
}
pub unsafe fn write_cmd_buf_head(base: *mut Self, value: u64) {
write_volatile(addr_of_mut!((*base).cmd_buf_head), value);
}
pub unsafe fn read_cmd_buf_tail(base: *mut Self) -> u64 {
read_volatile(addr_of!((*base).cmd_buf_tail))
}
pub unsafe fn write_cmd_buf_tail(base: *mut Self, value: u64) {
write_volatile(addr_of_mut!((*base).cmd_buf_tail), value);
}
pub unsafe fn read_evt_log_head(base: *mut Self) -> u64 {
read_volatile(addr_of!((*base).evt_log_head))
}
pub unsafe fn write_evt_log_head(base: *mut Self, value: u64) {
write_volatile(addr_of_mut!((*base).evt_log_head), value);
}
pub unsafe fn read_evt_log_tail(base: *mut Self) -> u64 {
read_volatile(addr_of!((*base).evt_log_tail))
}
pub unsafe fn read_status(base: *mut Self) -> u32 {
read_volatile(addr_of!((*base).status))
}
pub unsafe fn read_ppr_log_head(base: *mut Self) -> u64 {
read_volatile(addr_of!((*base).ppr_log_head))
}
pub unsafe fn write_ppr_log_head(base: *mut Self, value: u64) {
write_volatile(addr_of_mut!((*base).ppr_log_head), value);
}
pub unsafe fn read_ppr_log_tail(base: *mut Self) -> u64 {
read_volatile(addr_of!((*base).ppr_log_tail))
}
}
#[cfg(test)]
mod tests {
use core::mem::MaybeUninit;
use super::{offsets, AmdViMmio};
#[test]
fn register_accessors_use_expected_offsets() {
let mut mmio = MaybeUninit::<AmdViMmio>::zeroed();
let base = mmio.as_mut_ptr();
unsafe {
AmdViMmio::write_control(base, 0xdead_beef);
AmdViMmio::write_cmd_buf_head(base, 0x1122_3344_5566_7788);
AmdViMmio::write_dev_table_bar(base, 0x2000);
assert_eq!(AmdViMmio::read_control(base), 0xdead_beef);
assert_eq!(AmdViMmio::read_cmd_buf_head(base), 0x1122_3344_5566_7788);
assert_eq!(AmdViMmio::read_dev_table_bar(base), 0x2000);
let byte_base = base.cast::<u8>();
let control_ptr = byte_base.add(offsets::CONTROL).cast::<u32>();
let head_ptr = byte_base.add(offsets::CMD_BUF_HEAD).cast::<u64>();
assert_eq!(core::ptr::read_volatile(control_ptr), 0xdead_beef);
assert_eq!(core::ptr::read_volatile(head_ptr), 0x1122_3344_5566_7788);
}
}
}
@@ -0,0 +1,690 @@
use core::alloc::Layout;
use core::mem::size_of;
use core::ptr::NonNull;
use core::slice;
use std::collections::BTreeMap;
use redox_driver_sys::dma::DmaBuffer;
pub const PAGE_SIZE: u64 = 4096;
pub const PTES_PER_PAGE: usize = 512;
pub const DEFAULT_IOMMU_LEVELS: u8 = 4;
pub const DEFAULT_IOVA_BASE: u64 = 0x1_0000_0000;
pub const DEFAULT_IOVA_LIMIT: u64 = 0x0000_FFFF_FFFF_F000;
const PTE_PRESENT: u64 = 1 << 0;
const PTE_USER: u64 = 1 << 1;
const PTE_WRITE: u64 = 1 << 2;
const PTE_READ: u64 = 1 << 3;
const PTE_NEXT_LEVEL_SHIFT: u64 = 9;
const PTE_NEXT_LEVEL_MASK: u64 = 0x7 << PTE_NEXT_LEVEL_SHIFT;
const PTE_OUTPUT_ADDR_MASK: u64 = 0x000F_FFFF_FFFF_F000;
const PTE_FORCE_COHERENT: u64 = 1 << 59;
const PTE_IRQ_REMAP: u64 = 1 << 61;
const PTE_IRQ_WRITE: u64 = 1 << 62;
const PTE_NO_EXECUTE: u64 = 1 << 63;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[repr(transparent)]
pub struct AmdPte(pub u64);
impl AmdPte {
pub const fn new() -> Self {
Self(0)
}
pub fn present(&self) -> bool {
self.0 & PTE_PRESENT != 0
}
pub fn set_present(&mut self, value: bool) {
if value {
self.0 |= PTE_PRESENT;
} else {
self.0 &= !PTE_PRESENT;
}
}
pub fn user(&self) -> bool {
self.0 & PTE_USER != 0
}
pub fn set_user(&mut self, value: bool) {
if value {
self.0 |= PTE_USER;
} else {
self.0 &= !PTE_USER;
}
}
pub fn writable(&self) -> bool {
self.0 & PTE_WRITE != 0
}
pub fn set_writable(&mut self, value: bool) {
if value {
self.0 |= PTE_WRITE;
} else {
self.0 &= !PTE_WRITE;
}
}
pub fn readable(&self) -> bool {
self.0 & PTE_READ != 0
}
pub fn set_readable(&mut self, value: bool) {
if value {
self.0 |= PTE_READ;
} else {
self.0 &= !PTE_READ;
}
}
pub fn next_level(&self) -> u8 {
((self.0 & PTE_NEXT_LEVEL_MASK) >> PTE_NEXT_LEVEL_SHIFT) as u8
}
pub fn set_next_level(&mut self, level: u8) {
self.0 =
(self.0 & !PTE_NEXT_LEVEL_MASK) | ((u64::from(level) & 0x7) << PTE_NEXT_LEVEL_SHIFT);
}
pub fn output_addr(&self) -> u64 {
self.0 & PTE_OUTPUT_ADDR_MASK
}
pub fn set_output_addr(&mut self, addr: u64) {
self.0 = (self.0 & !PTE_OUTPUT_ADDR_MASK) | (addr & PTE_OUTPUT_ADDR_MASK);
}
pub fn force_coherent(&self) -> bool {
self.0 & PTE_FORCE_COHERENT != 0
}
pub fn set_force_coherent(&mut self, value: bool) {
if value {
self.0 |= PTE_FORCE_COHERENT;
} else {
self.0 &= !PTE_FORCE_COHERENT;
}
}
pub fn interrupt_remap(&self) -> bool {
self.0 & PTE_IRQ_REMAP != 0
}
pub fn set_interrupt_remap(&mut self, value: bool) {
if value {
self.0 |= PTE_IRQ_REMAP;
} else {
self.0 &= !PTE_IRQ_REMAP;
}
}
pub fn interrupt_write(&self) -> bool {
self.0 & PTE_IRQ_WRITE != 0
}
pub fn set_interrupt_write(&mut self, value: bool) {
if value {
self.0 |= PTE_IRQ_WRITE;
} else {
self.0 &= !PTE_IRQ_WRITE;
}
}
pub fn no_execute(&self) -> bool {
self.0 & PTE_NO_EXECUTE != 0
}
pub fn set_no_execute(&mut self, value: bool) {
if value {
self.0 |= PTE_NO_EXECUTE;
} else {
self.0 &= !PTE_NO_EXECUTE;
}
}
pub fn leaf(addr: u64, flags: MappingFlags) -> Self {
let mut entry = Self::new();
entry.set_present(true);
entry.set_output_addr(addr);
entry.set_readable(flags.readable);
entry.set_writable(flags.writable);
entry.set_user(flags.user);
entry.set_force_coherent(flags.force_coherent);
entry.set_no_execute(!flags.executable);
entry
}
pub fn pointer(addr: u64, next_level: u8) -> Self {
let mut entry = Self::new();
entry.set_present(true);
entry.set_next_level(next_level);
entry.set_output_addr(addr);
entry
}
}
const _: () = assert!(size_of::<AmdPte>() == 8);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MappingFlags {
pub readable: bool,
pub writable: bool,
pub executable: bool,
pub force_coherent: bool,
pub user: bool,
}
impl Default for MappingFlags {
fn default() -> Self {
Self::read_write()
}
}
impl MappingFlags {
pub const fn read_write() -> Self {
Self {
readable: true,
writable: true,
executable: false,
force_coherent: false,
user: false,
}
}
}
enum PageStorage {
Dma(DmaBuffer),
Host {
ptr: NonNull<u8>,
layout: Layout,
len: usize,
},
}
struct PageBuffer {
storage: PageStorage,
phys_addr: usize,
}
impl PageBuffer {
fn allocate(len: usize, align: usize) -> Result<Self, &'static str> {
match DmaBuffer::allocate(len, align) {
Ok(buffer) => Ok(Self {
phys_addr: buffer.physical_address(),
storage: PageStorage::Dma(buffer),
}),
Err(_) => {
let layout = Layout::from_size_align(len, align)
.map_err(|_| "invalid page-table allocation layout")?;
let ptr = unsafe { std::alloc::alloc_zeroed(layout) };
let ptr = NonNull::new(ptr).ok_or("failed to allocate host page-table memory")?;
Ok(Self {
phys_addr: ptr.as_ptr() as usize,
storage: PageStorage::Host { ptr, layout, len },
})
}
}
}
fn as_ptr(&self) -> *const u8 {
match &self.storage {
PageStorage::Dma(buffer) => buffer.as_ptr(),
PageStorage::Host { ptr, .. } => ptr.as_ptr(),
}
}
fn as_mut_ptr(&mut self) -> *mut u8 {
match &mut self.storage {
PageStorage::Dma(buffer) => buffer.as_mut_ptr(),
PageStorage::Host { ptr, .. } => ptr.as_ptr(),
}
}
fn physical_address(&self) -> usize {
self.phys_addr
}
fn len(&self) -> usize {
match &self.storage {
PageStorage::Dma(buffer) => buffer.len(),
PageStorage::Host { len, .. } => *len,
}
}
}
impl Drop for PageBuffer {
fn drop(&mut self) {
if let PageStorage::Host { ptr, layout, .. } = &self.storage {
unsafe {
std::alloc::dealloc(ptr.as_ptr(), *layout);
}
}
}
}
unsafe impl Send for PageBuffer {}
unsafe impl Sync for PageBuffer {}
struct PageTablePage {
buffer: PageBuffer,
}
impl PageTablePage {
fn new() -> Result<Self, &'static str> {
let buffer = PageBuffer::allocate(PAGE_SIZE as usize, PAGE_SIZE as usize)?;
if buffer.len() < PAGE_SIZE as usize {
return Err("page-table allocation smaller than one page");
}
Ok(Self { buffer })
}
fn physical_address(&self) -> u64 {
self.buffer.physical_address() as u64
}
fn entry(&self, index: usize) -> AmdPte {
self.entries()[index]
}
fn set_entry(&mut self, index: usize, entry: AmdPte) {
self.entries_mut()[index] = entry;
}
fn entries(&self) -> &[AmdPte] {
unsafe { slice::from_raw_parts(self.buffer.as_ptr().cast::<AmdPte>(), PTES_PER_PAGE) }
}
fn entries_mut(&mut self) -> &mut [AmdPte] {
unsafe {
slice::from_raw_parts_mut(self.buffer.as_mut_ptr().cast::<AmdPte>(), PTES_PER_PAGE)
}
}
}
struct PageTableNode {
page: PageTablePage,
children: BTreeMap<usize, Box<PageTableNode>>,
}
impl PageTableNode {
fn new() -> Result<Self, &'static str> {
Ok(Self {
page: PageTablePage::new()?,
children: BTreeMap::new(),
})
}
fn phys_addr(&self) -> u64 {
self.page.physical_address()
}
}
pub struct PageTable {
levels: u8,
root: Box<PageTableNode>,
}
impl PageTable {
pub fn new(levels: u8) -> Result<Self, &'static str> {
if !(1..=6).contains(&levels) {
return Err("AMD-Vi page tables support between 1 and 6 levels");
}
Ok(Self {
levels,
root: Box::new(PageTableNode::new()?),
})
}
pub fn levels(&self) -> u8 {
self.levels
}
pub fn root_address(&self) -> u64 {
self.root.phys_addr()
}
pub fn map_page(
&mut self,
iova: u64,
phys: u64,
flags: MappingFlags,
) -> Result<(), &'static str> {
if iova & (PAGE_SIZE - 1) != 0 || phys & (PAGE_SIZE - 1) != 0 {
return Err("IOMMU mappings must be 4KiB-aligned");
}
let mut node = self.root.as_mut();
for level in (2..=self.levels).rev() {
let index = page_table_index(level, iova);
if !node.children.contains_key(&index) {
let child = Box::new(PageTableNode::new()?);
let child_phys = child.phys_addr();
node.page
.set_entry(index, AmdPte::pointer(child_phys, level - 1));
node.children.insert(index, child);
}
let child = node
.children
.get_mut(&index)
.ok_or("failed to descend page table")?;
node = child.as_mut();
}
let leaf_index = page_table_index(1, iova);
node.page.set_entry(leaf_index, AmdPte::leaf(phys, flags));
Ok(())
}
pub fn unmap_page(&mut self, iova: u64) -> bool {
Self::unmap_in_node(self.root.as_mut(), self.levels, iova)
}
pub fn translate(&self, iova: u64) -> Option<u64> {
let page_base = iova & !(PAGE_SIZE - 1);
let page_offset = iova & (PAGE_SIZE - 1);
let mut node = self.root.as_ref();
for level in (2..=self.levels).rev() {
let index = page_table_index(level, page_base);
let entry = node.page.entry(index);
if !entry.present() {
return None;
}
node = node.children.get(&index)?.as_ref();
}
let leaf = node.page.entry(page_table_index(1, page_base));
if !leaf.present() {
return None;
}
Some(leaf.output_addr() + page_offset)
}
fn unmap_in_node(node: &mut PageTableNode, level: u8, iova: u64) -> bool {
if level == 1 {
let index = page_table_index(1, iova);
let present = node.page.entry(index).present();
if present {
node.page.set_entry(index, AmdPte::new());
}
return present;
}
let index = page_table_index(level, iova);
let Some(child) = node.children.get_mut(&index) else {
return false;
};
Self::unmap_in_node(child.as_mut(), level - 1, iova)
}
}
fn page_table_index(level: u8, address: u64) -> usize {
((address >> (12 + ((u64::from(level) - 1) * 9))) & 0x1FF) as usize
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DomainMapping {
pub iova: u64,
pub phys: u64,
pub size: u64,
pub flags: MappingFlags,
}
pub struct IovaAllocator {
base: u64,
limit: u64,
allocations: BTreeMap<u64, u64>,
}
impl IovaAllocator {
pub fn new(base: u64, limit: u64) -> Self {
Self {
base,
limit,
allocations: BTreeMap::new(),
}
}
pub fn allocate(&mut self, size: u64, align: u64) -> Option<u64> {
let size = align_up(size.max(PAGE_SIZE), PAGE_SIZE)?;
let align = align.max(PAGE_SIZE).next_power_of_two();
let mut cursor = align_up(self.base, align)?;
for (&start, &length) in &self.allocations {
if cursor.checked_add(size)? <= start {
self.allocations.insert(cursor, size);
return Some(cursor);
}
cursor = align_up(start.checked_add(length)?, align)?;
}
if cursor.checked_add(size)? > self.limit {
return None;
}
self.allocations.insert(cursor, size);
Some(cursor)
}
pub fn reserve(&mut self, start: u64, size: u64) -> bool {
let Some(end) = start.checked_add(size) else {
return false;
};
if start < self.base || end > self.limit {
return false;
}
let prev = self.allocations.range(..=start).next_back();
if let Some((&prev_start, &prev_len)) = prev {
let Some(prev_end) = prev_start.checked_add(prev_len) else {
return false;
};
if prev_end > start {
return false;
}
}
let next = self.allocations.range(start..).next();
if let Some((&next_start, _)) = next {
if next_start < end {
return false;
}
}
self.allocations.insert(start, size);
true
}
pub fn free(&mut self, start: u64) -> bool {
self.allocations.remove(&start).is_some()
}
pub fn allocated_size(&self, start: u64) -> Option<u64> {
self.allocations.get(&start).copied()
}
pub fn allocation_count(&self) -> usize {
self.allocations.len()
}
}
pub struct DomainPageTables {
domain_id: u16,
page_table: PageTable,
allocator: IovaAllocator,
mappings: BTreeMap<u64, DomainMapping>,
}
impl DomainPageTables {
pub fn new(domain_id: u16) -> Result<Self, &'static str> {
Self::with_range(domain_id, DEFAULT_IOVA_BASE, DEFAULT_IOVA_LIMIT)
}
pub fn with_range(domain_id: u16, base: u64, limit: u64) -> Result<Self, &'static str> {
Ok(Self {
domain_id,
page_table: PageTable::new(DEFAULT_IOMMU_LEVELS)?,
allocator: IovaAllocator::new(base, limit),
mappings: BTreeMap::new(),
})
}
pub fn domain_id(&self) -> u16 {
self.domain_id
}
pub fn root_address(&self) -> u64 {
self.page_table.root_address()
}
pub fn levels(&self) -> u8 {
self.page_table.levels()
}
pub fn map_range(
&mut self,
phys: u64,
size: u64,
flags: MappingFlags,
preferred_iova: Option<u64>,
) -> Result<u64, &'static str> {
if size == 0 {
return Err("IOMMU map size must be non-zero");
}
if phys & (PAGE_SIZE - 1) != 0 {
return Err("IOMMU physical mappings must be page-aligned");
}
let size = align_up(size, PAGE_SIZE).ok_or("IOMMU map size overflow")?;
let iova = if let Some(requested) = preferred_iova {
if requested & (PAGE_SIZE - 1) != 0 {
return Err("IOMMU IOVA mappings must be page-aligned");
}
if !self.allocator.reserve(requested, size) {
return Err("requested IOVA range is unavailable");
}
requested
} else {
self.allocator
.allocate(size, PAGE_SIZE)
.ok_or("unable to allocate an IOVA range")?
};
let mut mapped = 0u64;
while mapped < size {
if let Err(err) = self
.page_table
.map_page(iova + mapped, phys + mapped, flags)
{
let mut rollback = 0u64;
while rollback < mapped {
let _ = self.page_table.unmap_page(iova + rollback);
rollback += PAGE_SIZE;
}
let _ = self.allocator.free(iova);
return Err(err);
}
mapped += PAGE_SIZE;
}
self.mappings.insert(
iova,
DomainMapping {
iova,
phys,
size,
flags,
},
);
Ok(iova)
}
pub fn unmap_range(&mut self, iova: u64) -> Result<u64, &'static str> {
let mapping = self
.mappings
.remove(&iova)
.ok_or("IOMMU mapping does not exist")?;
let mut offset = 0u64;
while offset < mapping.size {
let _ = self.page_table.unmap_page(mapping.iova + offset);
offset += PAGE_SIZE;
}
let _ = self.allocator.free(mapping.iova);
Ok(mapping.size)
}
pub fn mapping(&self, iova: u64) -> Option<&DomainMapping> {
self.mappings.get(&iova)
}
pub fn mapping_count(&self) -> usize {
self.mappings.len()
}
}
fn align_up(value: u64, align: u64) -> Option<u64> {
let mask = align.checked_sub(1)?;
value.checked_add(mask).map(|rounded| rounded & !mask)
}
#[cfg(test)]
mod tests {
use super::{AmdPte, DomainPageTables, IovaAllocator, MappingFlags, PageTable, PAGE_SIZE};
#[test]
fn amd_pte_leaf_sets_permissions() {
let pte = AmdPte::leaf(0x1234_5000, MappingFlags::read_write());
assert!(pte.present());
assert!(pte.readable());
assert!(pte.writable());
assert!(pte.no_execute());
assert_eq!(pte.output_addr(), 0x1234_5000);
}
#[test]
fn iova_allocator_finds_gap_and_reuses_freed_ranges() {
let mut allocator = IovaAllocator::new(0x1000, 0x10_0000);
let first = allocator.allocate(PAGE_SIZE, PAGE_SIZE).unwrap_or(0);
let second = allocator.allocate(PAGE_SIZE * 2, PAGE_SIZE).unwrap_or(0);
assert_eq!(first, 0x1000);
assert_eq!(second, 0x2000);
assert!(allocator.free(first));
let reused = allocator.allocate(PAGE_SIZE, PAGE_SIZE).unwrap_or(0);
assert_eq!(reused, first);
}
#[test]
fn page_table_translate_round_trips_mapping() {
let mut table =
PageTable::new(4).unwrap_or_else(|err| panic!("page table create failed: {err}"));
table
.map_page(0x4000, 0x2000_0000, MappingFlags::read_write())
.unwrap_or_else(|err| panic!("page table map failed: {err}"));
assert_eq!(table.translate(0x4123), Some(0x2000_0123));
assert!(table.unmap_page(0x4000));
assert_eq!(table.translate(0x4123), None);
}
#[test]
fn domain_page_tables_allocate_iova_and_unmap() {
let mut domain = DomainPageTables::new(7)
.unwrap_or_else(|err| panic!("domain page table create failed: {err}"));
let iova = domain
.map_range(0x3000_0000, PAGE_SIZE * 2, MappingFlags::read_write(), None)
.unwrap_or_else(|err| panic!("domain mapping failed: {err}"));
let mapping = domain
.mapping(iova)
.unwrap_or_else(|| panic!("mapping missing"));
assert_eq!(mapping.size, PAGE_SIZE * 2);
assert!(domain.unmap_range(iova).is_ok());
assert!(domain.mapping(iova).is_none());
}
}
+1
View File
@@ -0,0 +1 @@
redbear-info
@@ -0,0 +1,9 @@
[source]
path = "source"
[build]
template = "cargo"
[package.files]
"/usr/bin/lspci" = "lspci"
"/usr/bin/lsusb" = "lsusb"
@@ -0,0 +1,15 @@
[package]
name = "redbear-hwutils"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "lspci"
path = "src/bin/lspci.rs"
[[bin]]
name = "lsusb"
path = "src/bin/lsusb.rs"
[dependencies]
xhcid = { path = "../../../../../recipes/core/base/source/drivers/usb/xhcid" }
@@ -0,0 +1,94 @@
use std::fs;
use std::process;
use redbear_hwutils::{parse_args, parse_pci_location, PciLocation};
const USAGE: &str = "Usage: lspci\nList PCI devices exposed by /scheme/pci.";
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
struct PciDeviceSummary {
location: PciLocation,
vendor_id: u16,
device_id: u16,
class_code: u8,
subclass: u8,
prog_if: u8,
revision: u8,
}
fn main() {
match run() {
Ok(()) => {}
Err(err) if err.is_empty() => {}
Err(err) => {
eprintln!("lspci: {err}");
process::exit(1);
}
}
}
fn run() -> Result<(), String> {
parse_args("lspci", USAGE, std::env::args())?;
let mut devices = collect_devices()?;
devices.sort();
for device in devices {
println!(
"{} class {:02x}:{:02x}.{:02x} vendor {:04x} device {:04x} rev {:02x}",
device.location,
device.class_code,
device.subclass,
device.prog_if,
device.vendor_id,
device.device_id,
device.revision,
);
}
Ok(())
}
fn collect_devices() -> Result<Vec<PciDeviceSummary>, String> {
let entries =
fs::read_dir("/scheme/pci").map_err(|err| format!("failed to read /scheme/pci: {err}"))?;
let mut devices = Vec::new();
for entry in entries {
let entry = match entry {
Ok(entry) => entry,
Err(_) => continue,
};
let file_name = entry.file_name();
let Some(file_name) = file_name.to_str() else {
continue;
};
let Some(location) = parse_pci_location(file_name) else {
continue;
};
let config_path = format!("{}/config", location.scheme_path());
let config = match fs::read(&config_path) {
Ok(config) => config,
Err(_) => continue,
};
if config.len() < 16 {
continue;
}
devices.push(PciDeviceSummary {
location,
vendor_id: u16::from_le_bytes([config[0x00], config[0x01]]),
device_id: u16::from_le_bytes([config[0x02], config[0x03]]),
revision: config[0x08],
prog_if: config[0x09],
subclass: config[0x0A],
class_code: config[0x0B],
});
}
Ok(devices)
}
@@ -0,0 +1,169 @@
use std::fs;
use std::process;
use redbear_hwutils::{describe_usb_device, parse_args};
use xhcid_interface::{PortId, PortState, XhciClientHandle};
const USAGE: &str = "Usage: lsusb\nList USB devices exposed by native usb.* schemes.";
#[derive(Clone, Debug, Eq, PartialEq)]
struct UsbDeviceSummary {
controller: String,
port: PortId,
vendor_id: u16,
product_id: u16,
class: u8,
subclass: u8,
protocol: u8,
usb_major: u8,
usb_minor: u8,
description: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct UsbPortStateSummary {
controller: String,
port: PortId,
state: PortState,
}
fn main() {
match run() {
Ok(()) => {}
Err(err) if err.is_empty() => {}
Err(err) => {
eprintln!("lsusb: {err}");
process::exit(1);
}
}
}
fn run() -> Result<(), String> {
parse_args("lsusb", USAGE, std::env::args())?;
let (mut devices, mut fallback_ports) = collect_usb_state()?;
devices.sort_by(|left, right| {
left.controller
.cmp(&right.controller)
.then(left.port.cmp(&right.port))
});
fallback_ports.sort_by(|left, right| {
left.controller
.cmp(&right.controller)
.then(left.port.cmp(&right.port))
});
for device in devices {
println!(
"{} {} ID {:04x}:{:04x} class {:02x}/{:02x}/{:02x} usb {}.{:02x} {}",
device.controller,
device.port,
device.vendor_id,
device.product_id,
device.class,
device.subclass,
device.protocol,
device.usb_major,
device.usb_minor,
device.description,
);
}
for fallback in fallback_ports {
println!(
"{} {} state {}",
fallback.controller,
fallback.port,
fallback.state.as_str(),
);
}
Ok(())
}
fn collect_usb_state() -> Result<(Vec<UsbDeviceSummary>, Vec<UsbPortStateSummary>), String> {
let entries =
fs::read_dir("/scheme").map_err(|err| format!("failed to read /scheme: {err}"))?;
let mut devices = Vec::new();
let mut fallback_ports = Vec::new();
for entry in entries {
let entry = match entry {
Ok(entry) => entry,
Err(_) => continue,
};
let file_name = entry.file_name();
let Some(controller) = file_name.to_str() else {
continue;
};
if !controller.starts_with("usb.") {
continue;
}
let controller_dir = format!("/scheme/{controller}");
let ports = match fs::read_dir(&controller_dir) {
Ok(ports) => ports,
Err(_) => continue,
};
for port_entry in ports {
let port_entry = match port_entry {
Ok(port_entry) => port_entry,
Err(_) => continue,
};
let port_name = port_entry.file_name();
let Some(port_name) = port_name.to_str() else {
continue;
};
let Some(raw_port_id) = port_name.strip_prefix("port") else {
continue;
};
let Ok(port) = raw_port_id.parse::<PortId>() else {
continue;
};
let handle = match XhciClientHandle::new(controller.to_string(), port) {
Ok(handle) => handle,
Err(_) => continue,
};
let state = handle.port_state().ok();
match handle.get_standard_descs() {
Ok(descriptors) => {
devices.push(UsbDeviceSummary {
controller: controller.to_string(),
port,
vendor_id: descriptors.vendor,
product_id: descriptors.product,
class: descriptors.class,
subclass: descriptors.sub_class,
protocol: descriptors.protocol,
usb_major: descriptors.major_version(),
usb_minor: descriptors.minor_version(),
description: describe_usb_device(
descriptors.manufacturer_str.as_deref(),
descriptors.product_str.as_deref(),
),
});
}
Err(_) => {
if let Some(state) =
state.filter(|state| *state != PortState::EnabledOrDisabled)
{
fallback_ports.push(UsbPortStateSummary {
controller: controller.to_string(),
port,
state,
});
}
}
}
}
}
Ok((devices, fallback_ports))
}
@@ -0,0 +1,80 @@
use std::fmt;
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct PciLocation {
pub segment: u16,
pub bus: u8,
pub device: u8,
pub function: u8,
}
impl PciLocation {
pub fn scheme_path(&self) -> String {
format!(
"/scheme/pci/{:04x}--{:02x}--{:02x}.{}",
self.segment, self.bus, self.device, self.function
)
}
}
impl fmt::Display for PciLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:04x}:{:02x}:{:02x}.{}",
self.segment, self.bus, self.device, self.function
)
}
}
pub fn parse_pci_location(name: &str) -> Option<PciLocation> {
let (segment, rest) = name.split_once("--")?;
let (bus, rest) = rest.split_once("--")?;
let (device, function) = rest.split_once('.')?;
Some(PciLocation {
segment: u16::from_str_radix(segment, 16).ok()?,
bus: u8::from_str_radix(bus, 16).ok()?,
device: u8::from_str_radix(device, 16).ok()?,
function: function.parse().ok()?,
})
}
pub fn parse_args(
program: &str,
usage: &str,
args: impl IntoIterator<Item = String>,
) -> Result<(), String> {
let extras: Vec<String> = args.into_iter().skip(1).collect();
if extras.is_empty() {
return Ok(());
}
if extras.len() == 1 && matches!(extras[0].as_str(), "-h" | "--help") {
println!("{usage}");
return Err(String::new());
}
Err(format!(
"{program}: unsupported arguments: {}",
extras.join(" ")
))
}
pub fn describe_usb_device(manufacturer: Option<&str>, product: Option<&str>) -> String {
let mut parts = Vec::new();
if let Some(manufacturer) = manufacturer.filter(|value| !value.is_empty()) {
parts.push(manufacturer);
}
if let Some(product) = product.filter(|value| !value.is_empty()) {
parts.push(product);
}
if parts.is_empty() {
"USB device".to_string()
} else {
parts.join(" ")
}
}
File diff suppressed because it is too large Load Diff
@@ -43,6 +43,9 @@ README
# will ensure all of these are built and staged before this package
dependencies = [
"redbear-release",
"redox-driver-sys",
"linux-kpi",
"redox-drm",
"firmware-loader",
"evdevd",
"udev-shim",
@@ -0,0 +1,5 @@
[source]
path = "source"
[build]
template = "cargo"
@@ -0,0 +1,8 @@
[package]
name = "redbear-netctl"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "netctl"
path = "src/main.rs"
@@ -0,0 +1,365 @@
use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::{self, Command};
const USAGE: &str = "Usage: netctl [--boot|list|status [profile]|start <profile>|stop <profile>|enable <profile>|disable [profile]|is-enabled [profile]]";
#[derive(Clone, Debug)]
enum ProfileIpMode {
Dhcp,
Static {
address: String,
gateway: Option<String>,
dns: Option<String>,
},
}
#[derive(Clone, Debug)]
struct Profile {
name: String,
interface: String,
connection: String,
ip_mode: ProfileIpMode,
}
fn main() {
if let Err(err) = run() {
eprintln!("netctl: {err}");
process::exit(1);
}
}
fn run() -> Result<(), String> {
let mut args = env::args().skip(1);
let Some(command) = args.next() else {
return Err(USAGE.into());
};
match command.as_str() {
"--boot" => run_boot_profile(),
"list" => list_profiles(),
"status" => status(args.next().as_deref()),
"start" => start_profile(&required_profile(args.next())?, false),
"stop" => stop_profile(&required_profile(args.next())?),
"enable" => enable_profile(&required_profile(args.next())?),
"disable" => disable_profile(args.next().as_deref()),
"is-enabled" => is_enabled(args.next().as_deref()),
"help" | "--help" | "-h" => {
println!("{USAGE}");
Ok(())
}
_ => Err(USAGE.into()),
}
}
fn required_profile(profile: Option<String>) -> Result<String, String> {
profile.ok_or_else(|| USAGE.to_string())
}
fn run_boot_profile() -> Result<(), String> {
let Some(active) = active_profile_name()? else {
return Ok(());
};
start_profile(&active, true)
}
fn list_profiles() -> Result<(), String> {
let mut entries = profile_names()?;
entries.sort();
for entry in entries {
println!("{entry}");
}
Ok(())
}
fn status(profile: Option<&str>) -> Result<(), String> {
let active = active_profile_name()?;
let selected = profile.map(str::to_string).or(active.clone());
let address = current_addr().unwrap_or_else(|| "unconfigured".into());
match selected {
Some(name) => {
let enabled = active.as_deref() == Some(name.as_str());
println!(
"profile={} enabled={} address={}",
name,
if enabled { "yes" } else { "no" },
address
);
}
None => {
println!("profile=none enabled=no address={address}");
}
}
Ok(())
}
fn start_profile(name: &str, boot: bool) -> Result<(), String> {
ensure_runtime_surfaces()?;
let profile = load_profile(name)?;
apply_profile(&profile, boot)?;
println!("started {}", profile.name);
Ok(())
}
fn stop_profile(name: &str) -> Result<(), String> {
if active_profile_name()?.as_deref() == Some(name) {
let _ = fs::remove_file(active_profile_path());
}
println!("stopped {}", name);
Ok(())
}
fn enable_profile(name: &str) -> Result<(), String> {
let profile = load_profile(name)?;
let active_path = active_profile_path();
fs::write(&active_path, format!("{}\n", profile.name))
.map_err(|err| format!("failed to write {}: {err}", active_path.display()))?;
println!("enabled {}", profile.name);
Ok(())
}
fn disable_profile(profile: Option<&str>) -> Result<(), String> {
if let Some(name) = profile {
if active_profile_name()?.as_deref() != Some(name) {
println!("disabled {}", name);
return Ok(());
}
}
let _ = fs::remove_file(active_profile_path());
println!("disabled {}", profile.unwrap_or("active"));
Ok(())
}
fn is_enabled(profile: Option<&str>) -> Result<(), String> {
let active = active_profile_name()?;
let enabled = match profile {
Some(profile) => active.as_deref() == Some(profile),
None => active.is_some(),
};
println!("{}", if enabled { "yes" } else { "no" });
Ok(())
}
fn apply_profile(profile: &Profile, boot: bool) -> Result<(), String> {
if profile.connection != "ethernet" {
return Err(format!(
"unsupported Connection={} (only ethernet is supported)",
profile.connection
));
}
if profile.interface != "eth0" {
return Err(format!(
"unsupported Interface={} (only eth0 is supported)",
profile.interface
));
}
match &profile.ip_mode {
ProfileIpMode::Dhcp => {
if boot
|| current_addr().as_deref() == Some("Not configured")
|| current_addr().is_none()
{
let _child = Command::new("dhcpd")
.spawn()
.map_err(|err| format!("failed to spawn dhcpd: {err}"))?;
}
}
ProfileIpMode::Static {
address,
gateway,
dns,
} => {
write_netcfg("ifaces/eth0/addr/set", address)?;
if let Some(gateway) = gateway {
write_netcfg("route/add", &format!("default via {gateway}"))?;
}
if let Some(dns) = dns {
write_netcfg("resolv/nameserver", dns)?;
}
}
}
if !boot && active_profile_name()?.as_deref() == Some(profile.name.as_str()) {
let active_path = active_profile_path();
fs::write(&active_path, format!("{}\n", profile.name))
.map_err(|err| format!("failed to update {}: {err}", active_path.display()))?;
}
Ok(())
}
fn ensure_runtime_surfaces() -> Result<(), String> {
let addr_path = format!("{}/ifaces/eth0/addr/list", netcfg_root().display());
fs::read_to_string(&addr_path)
.map(|_| ())
.map_err(|err| format!("failed to access {addr_path}: {err}"))
}
fn current_addr() -> Option<String> {
fs::read_to_string(format!("{}/ifaces/eth0/addr/list", netcfg_root().display()))
.ok()
.map(|value| value.trim().to_string())
}
fn write_netcfg(node: &str, value: &str) -> Result<(), String> {
let path = format!("{}/{node}", netcfg_root().display());
fs::write(&path, format!("{}\n", value.trim()))
.map_err(|err| format!("failed to write {path}: {err}"))
}
fn active_profile_name() -> Result<Option<String>, String> {
let active_path = active_profile_path();
match fs::read_to_string(&active_path) {
Ok(value) => {
let value = value.trim();
if value.is_empty() {
Ok(None)
} else {
Ok(Some(value.to_string()))
}
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(err) => Err(format!("failed to read {}: {err}", active_path.display())),
}
}
fn profile_names() -> Result<Vec<String>, String> {
let profile_dir = profile_dir();
let entries = fs::read_dir(&profile_dir)
.map_err(|err| format!("failed to read {}: {err}", profile_dir.display()))?;
let mut names = Vec::new();
for entry in entries {
let entry = entry.map_err(|err| format!("failed to read profile entry: {err}"))?;
let path = entry.path();
if !path.is_file() {
continue;
}
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
continue;
};
if name == "active" || name.starts_with('.') {
continue;
}
names.push(name.to_string());
}
Ok(names)
}
fn load_profile(name: &str) -> Result<Profile, String> {
let path = profile_path(name);
let content = fs::read_to_string(&path)
.map_err(|err| format!("failed to read {}: {err}", path.display()))?;
parse_profile(name, &content)
}
fn profile_path(name: &str) -> PathBuf {
profile_dir().join(name)
}
fn profile_dir() -> PathBuf {
env::var_os("REDBEAR_NETCTL_PROFILE_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("/etc/netctl"))
}
fn active_profile_path() -> PathBuf {
env::var_os("REDBEAR_NETCTL_ACTIVE")
.map(PathBuf::from)
.unwrap_or_else(|| profile_dir().join("active"))
}
fn netcfg_root() -> PathBuf {
env::var_os("REDBEAR_NETCFG_ROOT")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("/scheme/netcfg"))
}
fn parse_profile(name: &str, content: &str) -> Result<Profile, String> {
let mut interface = None;
let mut connection = None;
let mut ip = None;
let mut address = None;
let mut gateway = None;
let mut dns = None;
for raw_line in content.lines() {
let line = raw_line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let Some((key, value)) = line.split_once('=') else {
continue;
};
let key = key.trim();
let value = value.trim();
match key {
"Description" => {}
"Interface" => interface = Some(parse_scalar(value)),
"Connection" => connection = Some(parse_scalar(value)),
"IP" => ip = Some(parse_scalar(value)),
"Address" => address = parse_first_array_item(value),
"Gateway" => gateway = Some(parse_scalar(value)),
"DNS" => dns = parse_first_array_item(value),
_ => {}
}
}
let interface = interface.ok_or_else(|| format!("profile {name} is missing Interface="))?;
let connection = connection.ok_or_else(|| format!("profile {name} is missing Connection="))?;
let ip_mode = match ip
.ok_or_else(|| format!("profile {name} is missing IP="))?
.to_ascii_lowercase()
.as_str()
{
"dhcp" => ProfileIpMode::Dhcp,
"static" => ProfileIpMode::Static {
address: address.ok_or_else(|| format!("profile {name} is missing Address="))?,
gateway,
dns,
},
other => return Err(format!("unsupported IP={other}")),
};
Ok(Profile {
name: name.to_string(),
interface,
connection: connection.to_ascii_lowercase(),
ip_mode,
})
}
fn parse_scalar(value: &str) -> String {
let trimmed = value.trim();
trimmed
.trim_start_matches('(')
.trim_end_matches(')')
.trim()
.trim_matches('"')
.trim_matches('\'')
.to_string()
}
fn parse_first_array_item(value: &str) -> Option<String> {
let trimmed = value.trim();
if trimmed.starts_with('(') && trimmed.ends_with(')') {
let inner = &trimmed[1..trimmed.len().saturating_sub(1)];
inner
.split_whitespace()
.next()
.map(parse_scalar)
.filter(|value| !value.is_empty())
} else {
let value = parse_scalar(trimmed);
(!value.is_empty()).then_some(value)
}
}
+2 -1
View File
@@ -5,4 +5,5 @@ path = "source"
template = "cargo"
[package.files]
"/usr/lib/drivers/udev-shim" = "udev-shim"
"/usr/bin/udev" = "udev-shim"
"/usr/lib/drivers/udev" = "udev-shim"
@@ -4,7 +4,9 @@ version = "0.1.0"
edition = "2021"
[dependencies]
redox-scheme = "0.1"
syscall = { package = "redox_syscall", version = "0.4" }
libc = "0.2"
libredox = "0.1"
redox-scheme = "0.11"
syscall = { package = "redox_syscall", version = "0.7", features = ["std"] }
log = { version = "0.4", features = ["std"] }
thiserror = "2"
@@ -1,4 +1,4 @@
#[derive(Clone, Debug)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Subsystem {
Gpu,
Network,
@@ -9,8 +9,16 @@ pub enum Subsystem {
Unknown,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum InputKind {
Keyboard,
Mouse,
Generic,
}
#[derive(Clone, Debug)]
pub struct DeviceInfo {
pub is_pci: bool,
pub bus: u8,
pub dev: u8,
pub func: u8,
@@ -19,15 +27,89 @@ pub struct DeviceInfo {
pub class_code: u8,
pub subclass: u8,
pub subsystem: Subsystem,
pub input_kind: Option<InputKind>,
pub name: String,
pub path: String,
pub devpath: String,
pub devnode: String,
pub scheme_target: String,
pub symlinks: Vec<String>,
}
impl DeviceInfo {
pub fn new_platform_input(
name: &str,
devpath: &str,
input_kind: InputKind,
devnode: &str,
scheme_target: &str,
) -> Self {
Self {
is_pci: false,
bus: 0,
dev: 0,
func: 0,
vendor_id: 0,
device_id: 0,
class_code: 0,
subclass: 0,
subsystem: Subsystem::Input,
input_kind: Some(input_kind),
name: name.to_string(),
devpath: devpath.to_string(),
devnode: devnode.to_string(),
scheme_target: scheme_target.to_string(),
symlinks: Vec::new(),
}
}
pub fn set_node_metadata(
&mut self,
devnode: impl Into<String>,
scheme_target: impl Into<String>,
symlinks: Vec<String>,
) {
self.devnode = devnode.into();
self.scheme_target = scheme_target.into();
self.symlinks = symlinks;
}
pub fn subsystem_name(&self) -> &'static str {
match self.subsystem {
Subsystem::Gpu => "drm",
Subsystem::Network => "net",
Subsystem::Storage => "block",
Subsystem::Audio => "sound",
Subsystem::Usb => "usb",
Subsystem::Input => "input",
Subsystem::Unknown => "unknown",
}
}
pub fn id_path(&self) -> String {
if let Some(slot) = self.devpath.strip_prefix("/devices/pci/") {
return format!("pci-{slot}");
}
self.devpath
.trim_start_matches("/devices/")
.replace('/', "-")
}
pub fn is_input_keyboard(&self) -> bool {
self.input_kind == Some(InputKind::Keyboard)
}
pub fn is_input_mouse(&self) -> bool {
self.input_kind == Some(InputKind::Mouse)
}
}
pub fn classify_pci_device(bus: u8, dev: u8, func: u8) -> DeviceInfo {
let path = format!("/devices/pci/{:04x}:{:02x}:{:02x}.{}", bus, 0, dev, func);
let devpath = format!("/devices/pci/{:04x}:{:02x}:{:02x}.{}", bus, 0, dev, func);
let config_path = format!("/scheme/pci/{}.{}.{}", bus, dev, func);
let (vendor_id, device_id, class_code, subclass) = read_pci_config(&config_path);
let input_kind = detect_input_kind(class_code, subclass);
let subsystem = match class_code {
0x03 => Subsystem::Gpu,
@@ -39,9 +121,10 @@ pub fn classify_pci_device(bus: u8, dev: u8, func: u8) -> DeviceInfo {
_ => Subsystem::Unknown,
};
let name = format_device_name(vendor_id, device_id, class_code);
let name = format_device_name(vendor_id, device_id, class_code, subclass, input_kind);
DeviceInfo {
is_pci: true,
bus,
dev,
func,
@@ -50,8 +133,12 @@ pub fn classify_pci_device(bus: u8, dev: u8, func: u8) -> DeviceInfo {
class_code,
subclass,
subsystem,
input_kind,
name,
path,
devpath,
devnode: String::new(),
scheme_target: String::new(),
symlinks: Vec::new(),
}
}
@@ -68,7 +155,40 @@ fn read_pci_config(path: &str) -> (u16, u16, u8, u8) {
}
}
fn format_device_name(vendor_id: u16, device_id: u16, class_code: u8) -> String {
fn detect_input_kind(class_code: u8, subclass: u8) -> Option<InputKind> {
if class_code != 0x09 {
return None;
}
match subclass {
0x00 => Some(InputKind::Keyboard),
0x04 => Some(InputKind::Generic),
_ => Some(InputKind::Generic),
}
}
fn format_device_name(
vendor_id: u16,
device_id: u16,
class_code: u8,
subclass: u8,
input_kind: Option<InputKind>,
) -> String {
if class_code == 0x03 {
if let Some(name) = gpu_device_name(vendor_id, device_id) {
return format!("{name} [{vendor_id:04x}:{device_id:04x}]");
}
}
if class_code == 0x09 {
let name = match (subclass, input_kind) {
(0x00, Some(InputKind::Keyboard)) => "PS/2 Keyboard Controller",
(0x04, _) => "USB HID Controller",
_ => "Input Device",
};
return format!("{name} [{vendor_id:04x}:{device_id:04x}]");
}
let vendor_name = match vendor_id {
0x8086 => "Intel",
0x1002 => "AMD",
@@ -95,19 +215,93 @@ fn format_device_name(vendor_id: u16, device_id: u16, class_code: u8) -> String
)
}
pub fn format_device_info(dev: &DeviceInfo) -> String {
let subsystem = match dev.subsystem {
Subsystem::Gpu => "gpu",
Subsystem::Network => "net",
Subsystem::Storage => "block",
Subsystem::Audio => "sound",
Subsystem::Usb => "usb",
Subsystem::Input => "input",
Subsystem::Unknown => "unknown",
};
format!(
"P={}\nE=SUBSYSTEM={}\nE=PCI_VENDOR_ID={:#06x}\nE=PCI_DEVICE_ID={:#06x}\nE=PCI_CLASS={:#04x}{:02x}\nE=DEVNAME={}\n",
dev.path, subsystem, dev.vendor_id, dev.device_id, dev.class_code, dev.subclass, dev.name
)
fn gpu_device_name(vendor_id: u16, device_id: u16) -> Option<&'static str> {
match vendor_id {
0x1002 => match device_id {
0x73A3 => Some("AMD Radeon RX 6600 XT / 6650 XT (RDNA2)"),
0x73BF => Some("AMD Radeon RX 6800 XT / 6900 XT (RDNA2)"),
0x73DF => Some("AMD Radeon RX 6700 XT / 6750 XT (RDNA2)"),
0x73EF => Some("AMD Radeon RX 6800 / 6850M XT (RDNA2)"),
0x7422 => Some("AMD Radeon 780M (RDNA3)"),
0x7448 => Some("AMD Radeon RX 7900 XT (RDNA3)"),
0x744C => Some("AMD Radeon RX 7900 XTX (RDNA3)"),
0x7480 => Some("AMD Radeon RX 7800 XT / 7700 XT (RDNA3)"),
_ => Some("AMD Radeon GPU"),
},
0x8086 => match device_id {
0x3E92 => Some("Intel UHD Graphics 630"),
0x5912 => Some("Intel HD Graphics 630"),
0x9A49 => Some("Intel Iris Xe Graphics (Tiger Lake)"),
0x46A6 => Some("Intel Iris Xe Graphics (Alder Lake-P)"),
0x56A0 => Some("Intel Arc Graphics (DG2)"),
0x56A1 => Some("Intel Arc A380 (DG2)"),
_ => Some("Intel Graphics"),
},
_ => None,
}
}
pub fn device_properties(dev: &DeviceInfo) -> Vec<(String, String)> {
let mut props = Vec::new();
props.push(("DEVPATH".to_string(), dev.devpath.clone()));
props.push(("SUBSYSTEM".to_string(), dev.subsystem_name().to_string()));
props.push(("ID_MODEL_FROM_DATABASE".to_string(), dev.name.clone()));
if !dev.devnode.is_empty() {
props.push(("DEVNAME".to_string(), dev.devnode.clone()));
}
let id_path = dev.id_path();
if !id_path.is_empty() {
props.push(("ID_PATH".to_string(), id_path));
}
if dev.is_pci {
props.push((
"PCI_VENDOR_ID".to_string(),
format!("0x{:04x}", dev.vendor_id),
));
props.push((
"PCI_DEVICE_ID".to_string(),
format!("0x{:04x}", dev.device_id),
));
props.push((
"PCI_CLASS".to_string(),
format!("0x{:02x}{:02x}", dev.class_code, dev.subclass),
));
}
if dev.subsystem == Subsystem::Input {
props.push(("ID_INPUT".to_string(), "1".to_string()));
match dev.input_kind {
Some(InputKind::Keyboard) => {
props.push(("ID_INPUT_KEYBOARD".to_string(), "1".to_string()));
}
Some(InputKind::Mouse) => {
props.push(("ID_INPUT_MOUSE".to_string(), "1".to_string()));
}
_ => {}
}
}
props
}
pub fn format_device_info(dev: &DeviceInfo) -> String {
let mut info = format!("P={}\n", dev.devpath);
for (key, value) in device_properties(dev) {
info.push_str(&format!("E={key}={value}\n"));
}
for link in &dev.symlinks {
info.push_str(&format!("S={}\n", link.trim_start_matches('/')));
}
info
}
pub fn format_uevent_info(dev: &DeviceInfo) -> String {
let mut info = String::from("ACTION=add\n");
for (key, value) in device_properties(dev) {
info.push_str(&format!("{key}={value}\n"));
}
info
}
@@ -2,10 +2,13 @@ mod device_db;
mod scheme;
use std::env;
use std::process;
use std::os::fd::{AsRawFd, FromRawFd, RawFd};
use log::{error, info, LevelFilter, Metadata, Record};
use redox_scheme::{SignalBehavior, Socket};
use redox_scheme::{
scheme::{SchemeState, SchemeSync},
SignalBehavior, Socket,
};
use scheme::UdevScheme;
@@ -25,45 +28,35 @@ impl log::Log for StderrLogger {
fn flush(&self) {}
}
fn run() -> Result<(), String> {
let mut scheme = UdevScheme::new();
match scheme.scan_pci_devices() {
Ok(n) => info!("udev-shim: enumerated {} PCI device(s)", n),
Err(e) => error!("udev-shim: PCI scan failed: {}", e),
fn init_logging(level: LevelFilter) {
if log::set_boxed_logger(Box::new(StderrLogger { level })).is_err() {
return;
}
log::set_max_level(level);
}
let socket =
Socket::create("udev").map_err(|e| format!("failed to register udev scheme: {}", e))?;
info!("udev-shim: registered scheme:udev");
unsafe fn get_init_notify_fd() -> RawFd {
let fd: RawFd = env::var("INIT_NOTIFY")
.expect("udev-shim: INIT_NOTIFY not set")
.parse()
.expect("udev-shim: INIT_NOTIFY is not a valid fd");
libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC);
fd
}
loop {
let request = match socket.next_request(SignalBehavior::Restart) {
Ok(Some(r)) => r,
Ok(None) => {
info!("udev-shim: scheme unmounted, exiting");
break;
}
Err(e) => {
error!("udev-shim: failed to read scheme request: {}", e);
continue;
}
};
fn notify_scheme_ready(notify_fd: RawFd, socket: &Socket, scheme: &mut UdevScheme) {
let cap_id = scheme.scheme_root().expect("udev-shim: scheme_root failed");
let cap_fd = socket
.create_this_scheme_fd(0, cap_id, 0, 0)
.expect("udev-shim: create_this_scheme_fd failed");
let response = match request.handle_scheme_block_mut(&mut scheme) {
Ok(r) => r,
Err(_req) => {
error!("udev-shim: failed to handle request");
continue;
}
};
if let Err(e) = socket.write_response(response, SignalBehavior::Restart) {
error!("udev-shim: failed to write response: {}", e);
}
}
Ok(())
syscall::call_wo(
notify_fd as usize,
&libredox::Fd::new(cap_fd).into_raw().to_ne_bytes(),
syscall::CallFlags::FD,
&[],
)
.expect("udev-shim: failed to notify init that scheme is ready");
}
fn main() {
@@ -72,11 +65,40 @@ fn main() {
Ok("trace") => LevelFilter::Trace,
_ => LevelFilter::Info,
};
let _ = log::set_boxed_logger(Box::new(StderrLogger { level: log_level }));
log::set_max_level(log_level);
if let Err(e) = run() {
error!("udev-shim: fatal error: {}", e);
process::exit(1);
init_logging(log_level);
let mut scheme = UdevScheme::new();
match scheme.scan_pci_devices() {
Ok(n) => info!("udev-shim: enumerated {} PCI device(s)", n),
Err(e) => error!("udev-shim: PCI scan failed: {}", e),
}
let notify_fd = unsafe { get_init_notify_fd() };
let socket = Socket::create().expect("udev-shim: failed to create udev scheme");
let mut state = SchemeState::new();
notify_scheme_ready(notify_fd, &socket, &mut scheme);
libredox::call::setrens(0, 0).expect("udev-shim: failed to enter null namespace");
info!("udev-shim: registered scheme:udev");
while let Some(request) = socket
.next_request(SignalBehavior::Restart)
.expect("udev-shim: failed to read scheme request")
{
match request.kind() {
redox_scheme::RequestKind::Call(request) => {
let response = request.handle_sync(&mut scheme, &mut state);
socket
.write_response(response, SignalBehavior::Restart)
.expect("udev-shim: failed to write response");
}
_ => (),
}
}
std::process::exit(0);
}
@@ -1,170 +1,633 @@
use std::collections::BTreeMap;
use syscall::data::Stat;
use syscall::error::{Error, Result, EBADF, EINVAL, ENOENT, EROFS};
use syscall::flag::{EventFlags, MODE_DIR, MODE_FILE, SEEK_CUR, SEEK_END, SEEK_SET};
use redox_scheme::scheme::SchemeSync;
use redox_scheme::{CallerCtx, OpenResult};
use syscall::error::{Error, Result, EACCES, EBADF, ENOENT, EROFS};
use syscall::flag::{EventFlags, MODE_DIR, MODE_FILE};
use syscall::schemev2::NewFdFlags;
use crate::device_db::{classify_pci_device, format_device_info, DeviceInfo, Subsystem};
use crate::device_db::{
classify_pci_device, format_device_info, format_uevent_info, DeviceInfo, InputKind, Subsystem,
};
struct Handle {
kind: HandleKind,
offset: usize,
}
const SCHEME_ROOT_ID: usize = 1;
#[derive(Clone)]
enum HandleKind {
Root,
Devices,
Device(usize),
Dev,
DevInputDir,
DevInput(usize),
DevInputMice,
DevDriDir,
DevDri(usize),
DevLinks,
LinksInputDir,
LinksInputByPathDir,
LinksDriDir,
LinksDriByPathDir,
Link(usize),
Uevent,
}
pub struct UdevScheme {
next_id: usize,
handles: BTreeMap<usize, Handle>,
handles: BTreeMap<usize, HandleKind>,
devices: Vec<DeviceInfo>,
}
impl UdevScheme {
pub fn new() -> Self {
UdevScheme {
next_id: 0,
Self {
next_id: SCHEME_ROOT_ID + 1,
handles: BTreeMap::new(),
devices: Vec::new(),
}
}
pub fn scan_pci_devices(&mut self) -> Result<usize> {
let dir = match std::fs::read_dir("/scheme/pci") {
Ok(d) => d,
Err(e) => {
log::warn!("udev-shim: failed to read /scheme/pci: {e}");
return Ok(0);
self.devices.clear();
let mut pci_slots = Vec::new();
match std::fs::read_dir("/scheme/pci") {
Ok(dir) => {
for entry in dir {
let entry = match entry {
Ok(entry) => entry,
Err(_) => continue,
};
let name = match entry.file_name().to_str() {
Some(name) => name.to_string(),
None => continue,
};
if let Some(slot) = parse_pci_slot(&name) {
pci_slots.push(slot);
}
}
}
};
let mut count = 0;
for entry in dir {
let entry = match entry {
Ok(e) => e,
Err(_) => continue,
};
let name = match entry.file_name().to_str() {
Some(n) => n.to_string(),
None => continue,
};
let parts: Vec<&str> = name.split('.').collect();
if parts.len() < 3 {
continue;
Err(err) => {
log::warn!("udev-shim: failed to read /scheme/pci: {err}");
}
let bus: u8 = parts[0].parse().unwrap_or(0);
let dev: u8 = parts[1].parse().unwrap_or(0);
let func: u8 = parts[2].parse().unwrap_or(0);
let info = classify_pci_device(bus, dev, func);
self.devices.push(info);
count += 1;
}
Ok(count)
pci_slots.sort_unstable();
for (bus, dev, func) in pci_slots {
self.devices.push(classify_pci_device(bus, dev, func));
}
if path_exists("/scheme/input") {
self.devices.push(DeviceInfo::new_platform_input(
"Redox Keyboard Input",
"/devices/platform/keyboard0",
InputKind::Keyboard,
"",
"",
));
}
if path_exists("/scheme/pointer") || path_exists("/scheme/mouse") {
self.devices.push(DeviceInfo::new_platform_input(
"Redox Mouse Input",
"/devices/platform/mouse0",
InputKind::Mouse,
"",
"",
));
}
self.assign_virtual_nodes();
Ok(self.devices.len())
}
fn assign_virtual_nodes(&mut self) {
for dev in &mut self.devices {
if dev.subsystem == Subsystem::Gpu || (dev.subsystem == Subsystem::Input && !dev.is_pci)
{
dev.set_node_metadata("", "", Vec::new());
} else {
dev.symlinks.clear();
}
}
let mut gpu_indices: Vec<usize> = self
.devices
.iter()
.enumerate()
.filter_map(|(idx, dev)| (dev.subsystem == Subsystem::Gpu).then_some(idx))
.collect();
gpu_indices.sort_by_key(|idx| {
let dev = &self.devices[*idx];
(gpu_priority(dev), dev.bus, dev.dev, dev.func)
});
for (card_idx, device_idx) in gpu_indices.into_iter().enumerate() {
let devnode = format!("/dev/dri/card{card_idx}");
let scheme_target = format!("drm/card{card_idx}");
let symlink = format!(
"/links/dri/by-path/{}-card",
self.devices[device_idx].id_path()
);
self.devices[device_idx].set_node_metadata(devnode, scheme_target, vec![symlink]);
}
let mut input_indices: Vec<usize> = self
.devices
.iter()
.enumerate()
.filter_map(|(idx, dev)| {
(dev.subsystem == Subsystem::Input && !dev.is_pci).then_some(idx)
})
.collect();
input_indices.sort_by_key(|idx| {
let dev = &self.devices[*idx];
(input_priority(dev), dev.devpath.clone())
});
for (event_idx, device_idx) in input_indices.into_iter().enumerate() {
let devnode = format!("/dev/input/event{event_idx}");
let scheme_target = format!("evdev/event{event_idx}");
let suffix = match self.devices[device_idx].input_kind {
Some(InputKind::Keyboard) => "event-kbd",
Some(InputKind::Mouse) => "event-mouse",
Some(InputKind::Generic) | None => "event",
};
let symlink = format!(
"/links/input/by-path/{}-{}",
self.devices[device_idx].id_path(),
suffix
);
self.devices[device_idx].set_node_metadata(devnode, scheme_target, vec![symlink]);
}
}
fn find_device_by_devnode(&self, devnode: &str) -> Option<usize> {
self.devices
.iter()
.enumerate()
.find_map(|(idx, dev)| (dev.devnode == devnode).then_some(idx))
}
fn find_device_by_link(&self, prefix: &str, tail: &str) -> Option<usize> {
let expected = format!("{prefix}{tail}");
self.devices.iter().enumerate().find_map(|(idx, dev)| {
dev.symlinks
.iter()
.any(|link| link == &expected)
.then_some(idx)
})
}
fn mouse_device_index(&self) -> Option<usize> {
self.devices.iter().enumerate().find_map(|(idx, dev)| {
(dev.is_input_mouse() && !dev.scheme_target.is_empty()).then_some(idx)
})
}
fn input_event_indices(&self) -> Vec<usize> {
self.devices
.iter()
.enumerate()
.filter_map(|(idx, dev)| {
(dev.devnode.starts_with("/dev/input/event") && !dev.scheme_target.is_empty())
.then_some(idx)
})
.collect()
}
fn dri_card_indices(&self) -> Vec<usize> {
self.devices
.iter()
.enumerate()
.filter_map(|(idx, dev)| {
(dev.devnode.starts_with("/dev/dri/card") && !dev.scheme_target.is_empty())
.then_some(idx)
})
.collect()
}
fn directory_listing<I>(&self, entries: I) -> String
where
I: IntoIterator<Item = String>,
{
let mut listing = String::new();
for entry in entries {
listing.push_str(&entry);
listing.push('\n');
}
listing
}
fn link_listing(&self, prefix: &str) -> String {
self.directory_listing(
self.devices
.iter()
.flat_map(|dev| dev.symlinks.iter())
.filter_map(|link| {
link.strip_prefix(prefix).and_then(|tail| {
(!tail.is_empty() && !tail.contains('/')).then(|| tail.to_string())
})
}),
)
}
fn uevent_content(&self) -> String {
let mut content = String::new();
for (idx, dev) in self.devices.iter().enumerate() {
if idx > 0 {
content.push('\n');
}
content.push_str(&format_uevent_info(dev));
}
content
}
fn content_for_handle(&self, kind: &HandleKind) -> Result<String> {
match kind {
HandleKind::Root => Ok(self.directory_listing(
["devices", "dev", "links", "uevent"]
.into_iter()
.map(String::from),
)),
HandleKind::Devices => {
Ok(self.directory_listing((0..self.devices.len()).map(|idx| idx.to_string())))
}
HandleKind::Device(idx) => self
.devices
.get(*idx)
.map(format_device_info)
.ok_or_else(|| Error::new(ENOENT)),
HandleKind::Dev => {
Ok(self.directory_listing(["input", "dri"].into_iter().map(String::from)))
}
HandleKind::DevInputDir => {
let mut entries: Vec<String> = self
.input_event_indices()
.into_iter()
.filter_map(|idx| basename(&self.devices[idx].devnode))
.collect();
if self.mouse_device_index().is_some() {
entries.push("mice".to_string());
}
Ok(self.directory_listing(entries))
}
HandleKind::DevInput(idx) => {
let dev = self.devices.get(*idx).ok_or_else(|| Error::new(ENOENT))?;
if dev.scheme_target.is_empty() {
return Err(Error::new(ENOENT));
}
let mut info = format_device_info(dev);
info.push_str(&format!("SCHEME_TARGET={}\n", dev.scheme_target));
Ok(info)
}
HandleKind::DevDri(idx) => {
let dev = self.devices.get(*idx).ok_or_else(|| Error::new(ENOENT))?;
if dev.scheme_target.is_empty() {
return Err(Error::new(ENOENT));
}
let mut info = format_device_info(dev);
info.push_str(&format!("SCHEME_TARGET={}\n", dev.scheme_target));
Ok(info)
}
HandleKind::Link(idx) => {
let dev = self.devices.get(*idx).ok_or_else(|| Error::new(ENOENT))?;
if dev.scheme_target.is_empty() {
return Err(Error::new(ENOENT));
}
let mut info = format_device_info(dev);
info.push_str(&format!("SCHEME_TARGET={}\n", dev.scheme_target));
Ok(info)
}
HandleKind::DevInputMice => {
let idx = self
.mouse_device_index()
.ok_or_else(|| Error::new(ENOENT))?;
let dev = &self.devices[idx];
let mut info = format_device_info(dev);
info.push_str(&format!("SCHEME_TARGET={}\n", dev.scheme_target));
Ok(info)
}
HandleKind::DevDriDir => Ok(self.directory_listing(
self.dri_card_indices()
.into_iter()
.filter_map(|idx| basename(&self.devices[idx].devnode)),
)),
HandleKind::DevLinks => {
Ok(self.directory_listing(["input", "dri"].into_iter().map(String::from)))
}
HandleKind::LinksInputDir => {
Ok(self.directory_listing(["by-path"].into_iter().map(String::from)))
}
HandleKind::LinksInputByPathDir => Ok(self.link_listing("/links/input/by-path/")),
HandleKind::LinksDriDir => {
Ok(self.directory_listing(["by-path"].into_iter().map(String::from)))
}
HandleKind::LinksDriByPathDir => Ok(self.link_listing("/links/dri/by-path/")),
HandleKind::Uevent => Ok(self.uevent_content()),
}
}
fn is_directory(kind: &HandleKind) -> bool {
matches!(
kind,
HandleKind::Root
| HandleKind::Devices
| HandleKind::Dev
| HandleKind::DevInputDir
| HandleKind::DevDriDir
| HandleKind::DevLinks
| HandleKind::LinksInputDir
| HandleKind::LinksInputByPathDir
| HandleKind::LinksDriDir
| HandleKind::LinksDriByPathDir
)
}
fn kind_for_id(&self, id: usize) -> Result<HandleKind> {
if id == SCHEME_ROOT_ID {
return Ok(HandleKind::Root);
}
self.handles
.get(&id)
.cloned()
.ok_or_else(|| Error::new(EBADF))
}
fn kind_for_path(&self, path: &str) -> Result<HandleKind> {
let cleaned = path.trim_matches('/');
match cleaned {
"" => Ok(HandleKind::Root),
"devices" => Ok(HandleKind::Devices),
"dev" => Ok(HandleKind::Dev),
"dev/input" => Ok(HandleKind::DevInputDir),
"dev/input/mice" => {
if self.mouse_device_index().is_none() {
return Err(Error::new(ENOENT));
}
Ok(HandleKind::DevInputMice)
}
"dev/dri" => Ok(HandleKind::DevDriDir),
"links" => Ok(HandleKind::DevLinks),
"links/input" => Ok(HandleKind::LinksInputDir),
"links/input/by-path" => Ok(HandleKind::LinksInputByPathDir),
"links/dri" => Ok(HandleKind::LinksDriDir),
"links/dri/by-path" => Ok(HandleKind::LinksDriByPathDir),
"uevent" => Ok(HandleKind::Uevent),
_ => {
if let Some(rest) = cleaned.strip_prefix("devices/") {
let idx = rest.parse::<usize>().map_err(|_| Error::new(ENOENT))?;
if idx >= self.devices.len() {
return Err(Error::new(ENOENT));
}
Ok(HandleKind::Device(idx))
} else if let Some(rest) = cleaned.strip_prefix("dev/input/") {
let devnode = format!("/dev/input/{rest}");
let idx = self
.find_device_by_devnode(&devnode)
.ok_or_else(|| Error::new(ENOENT))?;
Ok(HandleKind::DevInput(idx))
} else if let Some(rest) = cleaned.strip_prefix("dev/dri/") {
let devnode = format!("/dev/dri/{rest}");
let idx = self
.find_device_by_devnode(&devnode)
.ok_or_else(|| Error::new(ENOENT))?;
Ok(HandleKind::DevDri(idx))
} else if let Some(rest) = cleaned.strip_prefix("links/input/by-path/") {
let idx = self
.find_device_by_link("/links/input/by-path/", rest)
.ok_or_else(|| Error::new(ENOENT))?;
Ok(HandleKind::Link(idx))
} else if let Some(rest) = cleaned.strip_prefix("links/dri/by-path/") {
let idx = self
.find_device_by_link("/links/dri/by-path/", rest)
.ok_or_else(|| Error::new(ENOENT))?;
Ok(HandleKind::Link(idx))
} else {
Err(Error::new(ENOENT))
}
}
}
}
fn allocate_handle(&mut self, kind: HandleKind) -> usize {
let id = self.next_id;
self.next_id = self.next_id.saturating_add(1);
self.handles.insert(id, kind);
id
}
fn path_for_handle(&self, kind: &HandleKind) -> Result<String> {
match kind {
HandleKind::Root => Ok("/scheme/udev".to_string()),
HandleKind::Devices => Ok("/scheme/udev/devices".to_string()),
HandleKind::Device(idx) => {
if *idx >= self.devices.len() {
return Err(Error::new(ENOENT));
}
Ok(format!("/scheme/udev/devices/{idx}"))
}
HandleKind::Dev => Ok("/scheme/udev/dev".to_string()),
HandleKind::DevInputDir => Ok("/scheme/udev/dev/input".to_string()),
HandleKind::DevInput(idx) => self
.devices
.get(*idx)
.filter(|dev| !dev.devnode.is_empty())
.map(|dev| format!("/scheme/udev{}", dev.devnode))
.ok_or_else(|| Error::new(ENOENT)),
HandleKind::DevInputMice => Ok("/scheme/udev/dev/input/mice".to_string()),
HandleKind::DevDriDir => Ok("/scheme/udev/dev/dri".to_string()),
HandleKind::DevDri(idx) => self
.devices
.get(*idx)
.filter(|dev| !dev.devnode.is_empty())
.map(|dev| format!("/scheme/udev{}", dev.devnode))
.ok_or_else(|| Error::new(ENOENT)),
HandleKind::DevLinks => Ok("/scheme/udev/links".to_string()),
HandleKind::LinksInputDir => Ok("/scheme/udev/links/input".to_string()),
HandleKind::LinksInputByPathDir => Ok("/scheme/udev/links/input/by-path".to_string()),
HandleKind::LinksDriDir => Ok("/scheme/udev/links/dri".to_string()),
HandleKind::LinksDriByPathDir => Ok("/scheme/udev/links/dri/by-path".to_string()),
HandleKind::Link(idx) => self
.devices
.get(*idx)
.and_then(|dev| dev.symlinks.first())
.map(|link| format!("/scheme/udev{link}"))
.ok_or_else(|| Error::new(ENOENT)),
HandleKind::Uevent => Ok("/scheme/udev/uevent".to_string()),
}
}
}
impl redox_scheme::SchemeBlockMut for UdevScheme {
fn open(&mut self, path: &str, _flags: usize, _uid: u32, _gid: u32) -> Result<Option<usize>> {
let cleaned = path.trim_matches('/');
let kind = if cleaned.is_empty() {
HandleKind::Root
} else if cleaned == "devices" || cleaned == "devices/" {
HandleKind::Root
} else if let Some(rest) = cleaned.strip_prefix("devices/") {
let idx: usize = rest
.trim_end_matches('/')
.parse()
.map_err(|_| Error::new(ENOENT))?;
if idx >= self.devices.len() {
return Err(Error::new(ENOENT));
}
HandleKind::Device(idx)
} else {
return Err(Error::new(ENOENT));
};
let id = self.next_id;
self.next_id += 1;
self.handles.insert(id, Handle { kind, offset: 0 });
Ok(Some(id))
impl SchemeSync for UdevScheme {
fn scheme_root(&mut self) -> Result<usize> {
Ok(SCHEME_ROOT_ID)
}
fn read(&mut self, id: usize, buf: &mut [u8]) -> Result<Option<usize>> {
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
fn openat(
&mut self,
dirfd: usize,
path: &str,
_flags: usize,
_fcntl_flags: u32,
_ctx: &CallerCtx,
) -> Result<OpenResult> {
if dirfd != SCHEME_ROOT_ID {
return Err(Error::new(EACCES));
}
let content = match &handle.kind {
HandleKind::Root => {
let mut listing = String::new();
for (i, dev) in self.devices.iter().enumerate() {
listing.push_str(&format!("devices/{}\n", i));
}
listing
}
HandleKind::Device(idx) => {
let dev = &self.devices[*idx];
format_device_info(dev)
}
let kind = self.kind_for_path(path)?;
let id = if matches!(kind, HandleKind::Root) {
SCHEME_ROOT_ID
} else {
self.allocate_handle(kind)
};
Ok(OpenResult::ThisScheme {
number: id,
flags: NewFdFlags::empty(),
})
}
fn read(
&mut self,
id: usize,
buf: &mut [u8],
offset: u64,
_flags: u32,
_ctx: &CallerCtx,
) -> Result<usize> {
let kind = self.kind_for_id(id)?;
let content = self.content_for_handle(&kind)?;
let bytes = content.as_bytes();
let remaining = &bytes[handle.offset..];
if offset >= bytes.len() as u64 {
return Ok(0);
}
let start = offset as usize;
let remaining = &bytes[start..];
let to_copy = remaining.len().min(buf.len());
buf[..to_copy].copy_from_slice(&remaining[..to_copy]);
handle.offset += to_copy;
Ok(Some(to_copy))
Ok(to_copy)
}
fn write(&mut self, id: usize, _buf: &[u8]) -> Result<Option<usize>> {
let _ = self.handles.get(&id).ok_or(Error::new(EBADF))?;
fn write(
&mut self,
id: usize,
_buf: &[u8],
_offset: u64,
_flags: u32,
_ctx: &CallerCtx,
) -> Result<usize> {
let _kind = self.kind_for_id(id)?;
Err(Error::new(EROFS))
}
fn seek(&mut self, id: usize, pos: isize, whence: usize) -> Result<Option<isize>> {
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
let len = match &handle.kind {
HandleKind::Root => self.devices.len() * 20,
HandleKind::Device(idx) => format_device_info(&self.devices[*idx]).len(),
fn fpath(&mut self, id: usize, buf: &mut [u8], _ctx: &CallerCtx) -> Result<usize> {
let kind = self.kind_for_id(id)?;
let path = self.path_for_handle(&kind)?;
let bytes = path.as_bytes();
let to_copy = bytes.len().min(buf.len());
buf[..to_copy].copy_from_slice(&bytes[..to_copy]);
Ok(to_copy)
}
fn fstat(&mut self, id: usize, stat: &mut syscall::Stat, _ctx: &CallerCtx) -> Result<()> {
let kind = self.kind_for_id(id)?;
let size = self.content_for_handle(&kind)?.len() as u64;
stat.st_mode = if Self::is_directory(&kind) {
MODE_DIR | 0o555
} else {
MODE_FILE | 0o444
};
let new_offset = match whence {
SEEK_SET => pos as isize,
SEEK_CUR => handle.offset as isize + pos,
SEEK_END => len as isize + pos,
_ => return Err(Error::new(EINVAL)),
};
if new_offset < 0 {
return Err(Error::new(EINVAL));
stat.st_size = size;
stat.st_blocks = size.div_ceil(512);
stat.st_blksize = 4096;
stat.st_nlink = 1;
Ok(())
}
fn fsync(&mut self, id: usize, _ctx: &CallerCtx) -> Result<()> {
let _kind = self.kind_for_id(id)?;
Ok(())
}
fn fcntl(&mut self, id: usize, _cmd: usize, _arg: usize, _ctx: &CallerCtx) -> Result<usize> {
let _kind = self.kind_for_id(id)?;
Ok(0)
}
fn fsize(&mut self, id: usize, _ctx: &CallerCtx) -> Result<u64> {
let kind = self.kind_for_id(id)?;
Ok(self.content_for_handle(&kind)?.len() as u64)
}
fn ftruncate(&mut self, id: usize, _len: u64, _ctx: &CallerCtx) -> Result<()> {
let _kind = self.kind_for_id(id)?;
Err(Error::new(EROFS))
}
fn fevent(&mut self, id: usize, _flags: EventFlags, _ctx: &CallerCtx) -> Result<EventFlags> {
let _kind = self.kind_for_id(id)?;
Ok(EventFlags::empty())
}
fn on_close(&mut self, id: usize) {
if id != SCHEME_ROOT_ID {
self.handles.remove(&id);
}
}
}
fn path_exists(path: &str) -> bool {
std::fs::metadata(path).is_ok()
}
fn parse_pci_slot(name: &str) -> Option<(u8, u8, u8)> {
let mut parts = name.split('.');
let bus = parts.next()?.parse::<u8>().ok()?;
let dev = parts.next()?.parse::<u8>().ok()?;
let func = parts.next()?.parse::<u8>().ok()?;
if parts.next().is_some() {
return None;
}
Some((bus, dev, func))
}
fn basename(path: &str) -> Option<String> {
path.rsplit('/').next().and_then(|part| {
if part.is_empty() {
None
} else {
Some(part.to_string())
}
})
}
fn gpu_priority(dev: &DeviceInfo) -> u8 {
match dev.vendor_id {
0x1002 => 0,
0x8086 => 1,
_ => 2,
}
}
fn input_priority(dev: &DeviceInfo) -> u8 {
if dev.is_input_keyboard() {
0
} else {
match dev.input_kind {
Some(InputKind::Mouse) => 1,
Some(InputKind::Generic) | None => 2,
Some(InputKind::Keyboard) => 0,
}
handle.offset = new_offset as usize;
Ok(Some(new_offset))
}
fn fstat(&mut self, id: usize, stat: &mut Stat) -> Result<Option<usize>> {
let handle = self.handles.get(&id).ok_or(Error::new(EBADF))?;
match &handle.kind {
HandleKind::Root => {
stat.st_mode = MODE_DIR | 0o555;
}
HandleKind::Device(_) => {
stat.st_mode = MODE_FILE | 0o444;
}
}
Ok(Some(0))
}
fn fevent(&mut self, id: usize, _flags: EventFlags) -> Result<Option<EventFlags>> {
let _ = self.handles.get(&id).ok_or(Error::new(EBADF))?;
Ok(Some(EventFlags::empty()))
}
fn close(&mut self, id: usize) -> Result<Option<usize>> {
self.handles.remove(&id);
Ok(Some(0))
}
}
+12
View File
@@ -83,10 +83,22 @@ mkdir -p recipes/gpu
symlink "../../local/recipes/gpu/amdgpu" "recipes/gpu/amdgpu"
symlink "../../local/recipes/gpu/redox-drm" "recipes/gpu/redox-drm"
# Library stubs / custom libs
mkdir -p recipes/libs
symlink "../../local/recipes/libs/libepoxy-stub" "recipes/libs/libepoxy-stub"
symlink "../../local/recipes/libs/libudev-stub" "recipes/libs/libudev-stub"
symlink "../../local/recipes/libs/lcms2-stub" "recipes/libs/lcms2-stub"
symlink "../../local/recipes/libs/libdisplay-info-stub" "recipes/libs/libdisplay-info-stub"
symlink "../../local/recipes/libs/libxcvt-stub" "recipes/libs/libxcvt-stub"
# System
mkdir -p recipes/system
symlink "../../local/recipes/system/cub" "recipes/system/cub"
symlink "../../local/recipes/system/evdevd" "recipes/system/evdevd"
symlink "../../local/recipes/system/firmware-loader" "recipes/system/firmware-loader"
symlink "../../local/recipes/system/iommu" "recipes/system/iommu"
symlink "../../local/recipes/system/redbear-hwutils" "recipes/system/redbear-hwutils"
symlink "../../local/recipes/system/redbear-netctl" "recipes/system/redbear-netctl"
symlink "../../local/recipes/system/redbear-meta" "recipes/system/redbear-meta"
symlink "../../local/recipes/system/udev-shim" "recipes/system/udev-shim"
+2 -8
View File
@@ -133,13 +133,6 @@ echo "Root: ${PROJECT_ROOT##*/}"
echo "Tag: $REDBEAR_TAG"
echo ""
section "Ensuring local recipe aliases..."
if [ ! -e "local/recipes/system/rbos-info" ] && [ -d "local/recipes/system/redbear-info" ]; then
symlink "redbear-info" "local/recipes/system/rbos-info"
fi
status "Local recipe aliases ready"
echo ""
section "Ensuring custom recipe symlinks..."
symlink "../../local/recipes/branding/redbear-release" "recipes/branding/redbear-release"
symlink "../../local/recipes/drivers/linux-kpi" "recipes/drivers/linux-kpi"
@@ -148,8 +141,9 @@ symlink "../../local/recipes/gpu/amdgpu" "recipes/gpu/amdgpu"
symlink "../../local/recipes/gpu/redox-drm" "recipes/gpu/redox-drm"
symlink "../../local/recipes/system/evdevd" "recipes/system/evdevd"
symlink "../../local/recipes/system/firmware-loader" "recipes/system/firmware-loader"
symlink "../../local/recipes/system/rbos-info" "recipes/system/rbos-info"
symlink "../../local/recipes/system/redbear-info" "recipes/system/redbear-info"
symlink "../../local/recipes/system/redbear-hwutils" "recipes/system/redbear-hwutils"
symlink "../../local/recipes/system/redbear-netctl" "recipes/system/redbear-netctl"
symlink "../../local/recipes/system/redbear-meta" "recipes/system/redbear-meta"
symlink "../../local/recipes/system/udev-shim" "recipes/system/udev-shim"
symlink "../../local/recipes/core/ext4d" "recipes/core/ext4d"
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env bash
#
# test-iommu-qemu.sh - Launch QEMU with AMD IOMMU device for hardware testing
#
# This wrapper adds the AMD IOMMU device to QEMU for testing IOMMU/vIOMMU
# functionality on AMD hardware. It forwards any additional QEMU flags to the
# make qemu invocation.
#
# Usage:
# ./local/scripts/test-iommu-qemu.sh [--help]
# ./local/scripts/test-iommu-qemu.sh [extra QEMU flags...]
#
# Examples:
# ./local/scripts/test-iommu-qemu.sh # Basic IOMMU test
# ./local/scripts/test-iommu-qemu.sh -display sdl # With SDL display
# ./local/scripts/test-iommu-qemu.sh -m 4G # With 4GB RAM
set -e
# Print usage information
usage() {
cat << USAGE
Usage: $(basename "$0") [options]
Launch QEMU with AMD IOMMU device for hardware testing.
Options:
--help Show this help message
Any additional arguments are passed as extra QEMU flags.
Environment:
QEMUFLAGS Additional flags (prepended to device amd-iommu)
Examples:
$(basename "$0")
$(basename "$0") -display sdl -m 4G
QEMUFLAGS="-smp 8" $(basename "$0")
USAGE
exit 0
}
# Parse --help before anything else
for arg in "$@"; do
case "$arg" in
--help|-h|help)
usage
;;
esac
done
# Trap to handle Ctrl+C gracefully
# Kill any background QEMU process if interrupted
cleanup() {
echo "Interrupted, cleaning up..."
# The make qemu process will be killed by the signal
exit 130
}
trap cleanup SIGINT SIGTERM
# Build QEMUFLAGS with AMD IOMMU device
# Prepend user QEMUFLAGS if set, then add the amd-iommu device
IOMMU_FLAGS="-device amd-iommu"
if [[ -n "${QEMUFLAGS:-}" ]]; then
QEMUFLAGS="${QEMUFLAGS} ${IOMMU_FLAGS} $@"
else
QEMUFLAGS="${IOMMU_FLAGS} $@"
fi
# Launch QEMU via make
exec make qemu QEMUFLAGS="$QEMUFLAGS"
+1 -1
View File
@@ -68,7 +68,7 @@ esac
rm -rf "${COOKBOOK_BUILD}/initfs"
mkdir -p "${COOKBOOK_BUILD}/initfs/lib/init.d"
cp "${COOKBOOK_SOURCE}/init.d"/* "${COOKBOOK_BUILD}/initfs/lib/init.d/"
cp "${COOKBOOK_SOURCE}/init.initfs.d"/* "${COOKBOOK_BUILD}/initfs/lib/init.d/"
mkdir -pv "${COOKBOOK_BUILD}/initfs/lib/pcid.d"
cp -v "${COOKBOOK_SOURCE}/drivers/initfs.toml" "${COOKBOOK_BUILD}/initfs/lib/pcid.d/initfs.toml"
+5 -1
View File
@@ -1,11 +1,12 @@
[source]
git = "https://gitlab.redox-os.org/redox-os/base.git"
patches = ["redox.patch"]
[build]
template = "custom"
script = """
mkdir -pv "${COOKBOOK_STAGE}/usr/bin"
for package in audiod ipcd ptyd; do
for package in audiod ipcd ptyd dhcpd; do
"${COOKBOOK_CARGO}" build \
--manifest-path "${COOKBOOK_SOURCE}/${package}/Cargo.toml" \
${build_flags}
@@ -73,4 +74,7 @@ do
driver="$(basename "$(dirname "$conf")")"
cp -v "$conf" "${COOKBOOK_STAGE}/lib/pcid.d/$driver.toml"
done
mkdir -pv "${COOKBOOK_STAGE}/usr/lib/init.d"
cp -v "${COOKBOOK_SOURCE}/init.d"/* "${COOKBOOK_STAGE}/usr/lib/init.d/"
"""
+1
View File
@@ -1,5 +1,6 @@
[source]
git = "https://gitlab.redox-os.org/redox-os/installer.git"
patches = ["redox.patch"]
[build]
template = "cargo"
+1
View File
@@ -1,5 +1,6 @@
[source]
git = "https://gitlab.redox-os.org/redox-os/kernel.git"
patches = ["redox.patch"]
[build]
template = "custom"
+6
View File
@@ -18,6 +18,12 @@ COOKBOOK_CONFIGURE_FLAGS+=(
--enable-static-link # This ensures loadables are not built, which will fail
)
COOKBOOK_MAKE_JOBS=1 # workaround for parallel make bugs
# mkbuiltins.c uses K&R-style empty parameter declarations (e.g. `static char *xmalloc ();`).
# GCC 15+ defaults to C23 where () means (void), breaking calls with arguments.
# Force C17 to restore the old behavior for the host-built code generator.
export CFLAGS_FOR_BUILD="-g -O2 -std=gnu17"
cookbook_configure
ln -s "bash" "${COOKBOOK_STAGE}/usr/bin/sh"
cp -r "${COOKBOOK_RECIPE}/etc" "${COOKBOOK_STAGE}/etc"
+1
View File
@@ -0,0 +1 @@
../../local/recipes/system/iommu
+1
View File
@@ -0,0 +1 @@
../../local/recipes/system/rbos-info
+1
View File
@@ -0,0 +1 @@
../../local/recipes/system/redbear-hwutils
+1
View File
@@ -0,0 +1 @@
../../local/recipes/system/redbear-netctl
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Build Red Bear OS live ISO
# Usage: ./scripts/build-iso.sh [CONFIG_NAME] [ARCH]
# CONFIG_NAME - build config (default: redbear-full)
# ARCH - target architecture (default: x86_64)
set -euo pipefail
CONFIG_NAME="${1:-redbear-full}"
ARCH="${2:-x86_64}"
echo "Building Red Bear OS ISO"
echo " config: ${CONFIG_NAME}"
echo " arch: ${ARCH}"
make live CONFIG_NAME="${CONFIG_NAME}" ARCH="${ARCH}"
echo ""
echo "Done: redbear-live.iso"
+296 -18
View File
@@ -1,6 +1,9 @@
#!/usr/bin/env bash
# fetch-all-sources.sh — Download ALL Redox OS + Red Bear OS package sources.
#
# Smart re-download: skips sources whose local checksum matches the recipe's
# blake3. Falls back to file-size comparison when no blake3 is recorded.
#
# Usage:
# ./scripts/fetch-all-sources.sh # Fetch for default desktop config
# ./scripts/fetch-all-sources.sh redbear-full # Fetch for a specific config
@@ -8,9 +11,11 @@
# ./scripts/fetch-all-sources.sh --recipe kernel # Fetch a single recipe
# ./scripts/fetch-all-sources.sh --list # List recipes that would be fetched
# ./scripts/fetch-all-sources.sh --status # Show which sources already exist
# ./scripts/fetch-all-sources.sh --preflight # Smart checksum/size check (no download)
#
# Prerequisites: rustup + nightly, git, wget, tar. The script builds the
# cookbook `repo` binary if not already built.
# Prerequisites: rustup + nightly, git, wget, tar, curl, b3sum.
# The script builds the cookbook `repo` binary if not already built.
# If b3sum is not installed, it will be installed via cargo.
#
# Sources are placed in recipes/<category>/<name>/source/ for git/tar recipes,
# and are left in-place for local/recipes/ (path-based sources).
@@ -25,6 +30,14 @@ REPO_BIN="./target/release/repo"
CONFIG_NAME="${1:-desktop}"
ACTION="fetch"
# ── Colors (disabled when not a terminal) ───────────────────────────
if [ -t 1 ]; then
C_GREEN="\033[0;32m" C_YELLOW="\033[0;33m" C_RED="\033[0;31m"
C_CYAN="\033[0;36m" C_BOLD="\033[1m" C_RESET="\033[0m"
else
C_GREEN="" C_YELLOW="" C_RED="" C_CYAN="" C_BOLD="" C_RESET=""
fi
# ── Argument parsing ────────────────────────────────────────────────
usage() {
echo "Usage: $0 [OPTIONS] [CONFIG_NAME]"
@@ -36,6 +49,8 @@ usage() {
echo " --recipe NAME Fetch a single recipe by name"
echo " --list List recipes that would be fetched (no download)"
echo " --status Show which sources already exist locally"
echo " --preflight Smart blake3/size check — show what needs updating"
echo " --force Force re-download even if checksums match"
echo " --help Show this help"
echo ""
echo "Configs: desktop, redbear-full, redbear-minimal, server, minimal, wayland, x11"
@@ -44,6 +59,7 @@ usage() {
ALL_CONFIGS=0
SINGLE_RECIPE=""
FORCE_FETCH=0
while [[ $# -gt 0 ]]; do
case "$1" in
--all-configs)
@@ -62,6 +78,14 @@ while [[ $# -gt 0 ]]; do
ACTION="status"
shift
;;
--preflight)
ACTION="preflight"
shift
;;
--force)
FORCE_FETCH=1
shift
;;
--help|-h)
usage
exit 0
@@ -99,6 +123,203 @@ resolve_config() {
fi
}
# ── Checksum / size utilities ───────────────────────────────────────
# Ensure b3sum is available
ensure_b3sum() {
if ! command -v b3sum &>/dev/null; then
echo " Installing b3sum (blake3 CLI tool)..."
cargo install b3sum 2>&1 | tail -1
if ! command -v b3sum &>/dev/null; then
echo " WARNING: b3sum not available. Size-based fallback only."
fi
fi
}
# Compute blake3 of a file (returns empty string if b3sum unavailable)
compute_blake3() {
local file="$1"
if command -v b3sum &>/dev/null && [ -f "$file" ]; then
b3sum --no-names "$file" | awk '{print $1}'
fi
}
# Get remote file size via HTTP HEAD (follows redirects)
get_remote_size() {
local url="$1"
# -sI: silent, HEAD only, -L: follow redirects, --max-time: timeout
curl -sI -L --max-time 15 "$url" 2>/dev/null \
| grep -i '^content-length:' \
| tail -1 \
| awk '{print $2}' \
| tr -d '\r\n'
}
# Get local file size (portable across Linux/macOS)
get_local_size() {
local file="$1"
if [ -f "$file" ]; then
stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null || echo ""
fi
}
# ── TOML field extraction (simple, no dependencies) ─────────────────
# Extract a quoted string field from recipe.toml: field = "value"
recipe_str_field() {
local file="$1" field="$2"
grep "^${field} *= *\"" "$file" 2>/dev/null | head -1 | sed 's/^[^"]*"\([^"]*\)".*/\1/'
}
# ── Per-recipe smart check ──────────────────────────────────────────
#
# Returns one of: "cached" | "missing" | "mismatch" | "no-checksum"
# Prints reason to stdout.
check_recipe_source() {
local recipe_dir="$1"
local recipe_toml="$recipe_dir/recipe.toml"
local source_dir="$recipe_dir/source"
local source_tar="$recipe_dir/source.tar"
# No recipe file
[ -f "$recipe_toml" ] || { echo "no-recipe"; return; }
# Path-based sources — always cached
if grep -q '^path *= *"source"' "$recipe_toml" 2>/dev/null; then
echo "cached:path"
return
fi
# ── Tar source ──────────────────────────────────────────────
local tar_url
tar_url=$(recipe_str_field "$recipe_toml" "tar")
if [ -n "$tar_url" ]; then
# No local tar at all
if [ ! -f "$source_tar" ]; then
# source dir might exist from a previous extract — check blake3 of
# the recipe against nothing: we just need to download
echo "missing"
return
fi
# Tar exists — check blake3
local blake3_expected
blake3_expected=$(recipe_str_field "$recipe_toml" "blake3")
if [ -n "$blake3_expected" ]; then
local blake3_local
blake3_local=$(compute_blake3 "$source_tar")
if [ -n "$blake3_local" ] && [ "$blake3_local" = "$blake3_expected" ]; then
echo "cached:blake3"
return
else
echo "mismatch:blake3"
return
fi
fi
# No blake3 in recipe — fall back to size comparison
local local_size remote_size
local_size=$(get_local_size "$source_tar")
remote_size=$(get_remote_size "$tar_url")
if [ -n "$remote_size" ] && [ -n "$local_size" ] && [ "$local_size" = "$remote_size" ]; then
echo "cached:size"
return
else
echo "mismatch:size"
return
fi
fi
# ── Git source ──────────────────────────────────────────────
if grep -q '^git *= *"' "$recipe_toml" 2>/dev/null; then
if [ -d "$source_dir/.git" ]; then
echo "cached:git"
return
elif [ -d "$source_dir" ]; then
echo "cached:git-dir"
return
else
echo "missing"
return
fi
fi
# ── same_as source ──────────────────────────────────────────
if grep -q '^same_as *= *"' "$recipe_toml" 2>/dev/null; then
echo "cached:same_as"
return
fi
# Unknown — let repo handle it
echo "missing"
}
# ── Preflight: scan all recipes and report status ───────────────────
preflight_scan() {
local label="${1:-all recipes}"
local total=0 cached=0 missing=0 mismatch=0 no_checksum=0
local missing_list=() mismatch_list=()
echo ""
printf "${C_BOLD}==> Smart preflight scan: %s${C_RESET}\n" "$label"
echo " Checking blake3 checksums and file sizes..."
echo ""
while IFS= read -r recipe_toml; do
local recipe_dir recipe_name category
recipe_dir="$(dirname "$recipe_toml")"
recipe_name="$(basename "$recipe_dir")"
category="$(basename "$(dirname "$recipe_dir")")"
# Skip recipes without a [source] section
grep -q '^\[source\]' "$recipe_toml" 2>/dev/null || continue
total=$((total + 1))
local status reason
status=$(check_recipe_source "$recipe_dir")
reason="${status#*:}"
status="${status%%:*}"
case "$status" in
cached)
cached=$((cached + 1))
;;
missing)
missing=$((missing + 1))
printf " ${C_YELLOW}MISSING %-30s %s${C_RESET}\n" "$category/$recipe_name" "$reason"
missing_list+=("$category/$recipe_name")
;;
mismatch)
mismatch=$((mismatch + 1))
printf " ${C_RED}CHANGED %-30s %s${C_RESET}\n" "$category/$recipe_name" "$reason"
mismatch_list+=("$category/$recipe_name")
;;
*)
# no-recipe, same_as, etc. — skip
;;
esac
done < <(find recipes local/recipes -name "recipe.toml" -not -path "*/source/*" 2>/dev/null | sort)
echo ""
printf " ${C_BOLD}Total recipes:${C_RESET} %3d\n" "$total"
printf " ${C_GREEN}Cached (skip):${C_RESET} %3d\n" "$cached"
printf " ${C_YELLOW}Missing:${C_RESET} %3d\n" "$missing"
printf " ${C_RED}Changed:${C_RESET} %3d\n" "$mismatch"
echo ""
if [ "$((missing + mismatch))" -eq 0 ]; then
printf " ${C_GREEN}✓ All sources are up to date.${C_RESET}\n"
return 1 # nothing to do
else
printf " ${C_BOLD}%d source(s) need downloading.${C_RESET}\n" "$((missing + mismatch))"
return 0
fi
}
# ── Fetch sources for a config ──────────────────────────────────────
fetch_for_config() {
local config_name="$1"
@@ -120,6 +341,21 @@ fetch_for_config() {
# ── Fetch a single recipe ──────────────────────────────────────────
fetch_single_recipe() {
local recipe_name="$1"
# Find recipe directory
local recipe_dir=""
for d in $(find recipes local/recipes -maxdepth 2 -name "$recipe_name" -type d 2>/dev/null); do
if [ -f "$d/recipe.toml" ]; then
recipe_dir="$d"
break
fi
done
if [ -z "$recipe_dir" ]; then
echo "ERROR: recipe '$recipe_name' not found" >&2
return 1
fi
echo ""
echo "==> Fetching single recipe: $recipe_name"
echo ""
@@ -150,12 +386,25 @@ list_for_config() {
# ── Status: show which sources exist ────────────────────────────────
show_status() {
echo "==> Source status for all recipes"
local config_filter="${1:-}"
echo "==> Source status${config_filter:+ for config: $config_filter}"
echo ""
local total=0 fetched=0 local_src=0 missing=0
while IFS= read -r recipe_toml; do
local recipe_list
if [ -n "$config_filter" ] && [ -x "$REPO_BIN" ]; then
local config_file
config_file="$(resolve_config "$config_filter")" 2>/dev/null || {
config_filter=""
}
if [ -n "$config_filter" ]; then
recipe_list=$("$REPO_BIN" cook-tree "--filesystem=$config_file" --with-package-deps 2>/dev/null | grep -v '^=' | grep -v '^$')
fi
fi
check_one_recipe() {
local recipe_toml="$1"
recipe_dir="$(dirname "$recipe_toml")"
recipe_name="$(basename "$recipe_dir")"
category="$(basename "$(dirname "$recipe_dir")")"
@@ -163,28 +412,44 @@ show_status() {
total=$((total + 1))
if [ -d "$recipe_dir/source" ]; then
# Check if it's a symlink (local recipe)
if [ -L "$recipe_dir/source" ] || grep -q '^path = "source"' "$recipe_toml" 2>/dev/null; then
if [ -L "$recipe_dir/source" ] || grep -q '^path *= *"source"' "$recipe_toml" 2>/dev/null; then
local_src=$((local_src + 1))
else
fetched=$((fetched + 1))
fi
else
# Check if source section exists
if grep -q '^\[source\]' "$recipe_toml" 2>/dev/null; then
missing=$((missing + 1))
echo " MISSING $category/$recipe_name"
fi
fi
done < <(find recipes -name "recipe.toml" -not -path "*/source/*" | sort)
}
# Also check local recipes
while IFS= read -r recipe_toml; do
recipe_dir="$(dirname "$recipe_toml")"
recipe_name="$(basename "$recipe_dir")"
total=$((total + 1))
local_src=$((local_src + 1))
done < <(find local/recipes -name "recipe.toml" -not -path "*/source/*" 2>/dev/null | sort)
if [ -n "${recipe_list:-}" ]; then
while IFS= read -r recipe_name; do
local found=0
while IFS= read -r recipe_toml; do
check_one_recipe "$recipe_toml"
found=1
break
done < <(find recipes local/recipes -path "*/${recipe_name}/recipe.toml" -not -path "*/source/*" 2>/dev/null | head -1)
if [ "$found" -eq 0 ]; then
total=$((total + 1))
missing=$((missing + 1))
echo " MISSING $recipe_name (no recipe.toml found)"
fi
done <<< "$recipe_list"
else
while IFS= read -r recipe_toml; do
check_one_recipe "$recipe_toml"
done < <(find recipes -name "recipe.toml" -not -path "*/source/*" | sort)
while IFS= read -r recipe_toml; do
recipe_dir="$(dirname "$recipe_toml")"
total=$((total + 1))
local_src=$((local_src + 1))
done < <(find local/recipes -name "recipe.toml" -not -path "*/source/*" 2>/dev/null | sort)
fi
echo ""
echo "Total recipes: $total"
@@ -201,9 +466,22 @@ show_status() {
# ── Main ────────────────────────────────────────────────────────────
# Ensure b3sum is available for checksum-based checking
ensure_b3sum
case "$ACTION" in
status)
show_status
show_status ""
;;
preflight)
build_repo
if [ "$ALL_CONFIGS" -eq 1 ]; then
for cfg in desktop redbear-full redbear-minimal server minimal wayland x11; do
preflight_scan "$cfg" || true
done
else
preflight_scan "$CONFIG_NAME"
fi
;;
list)
build_repo
@@ -231,11 +509,11 @@ case "$ACTION" in
done
echo ""
echo "==> All sources fetched. Summary:"
show_status
show_status ""
else
fetch_for_config "$CONFIG_NAME"
echo ""
show_status
show_status "$CONFIG_NAME"
fi
;;
esac
Executable
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
CONFIG_NAME="redbear-full"
ARCH="$(uname -m)"
BUILD=0
QEMU_EXTRA_ARGS=()
usage() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS]
Red Bear OS — build and run in QEMU.
Options:
-b, --build Build full OS before running
-c, --config NAME Config name (default: redbear-full)
-a, --arch ARCH Target architecture (default: host arch)
-- ARGS Pass remaining args to make qemu (e.g. -- QEMUFLAGS="-m 8G")
-h, --help Show this help
Examples:
$(basename "$0") # Run existing image
$(basename "$0") --build # Build + run
$(basename "$0") -b -c redbear-minimal # Build minimal + run
$(basename "$0") -- QEMUFLAGS="-m 8G" # Run with 8G RAM
$(basename "$0") -b -- serial=yes # Build + run with serial console
$(basename "$0") -b -- gpu=virtio kvm=no # Build + run with virtio GPU, no KVM
EOF
exit 0
}
while [ $# -gt 0 ]; do
case "$1" in
-b|--build) BUILD=1 ;;
-c|--config) CONFIG_NAME="$2"; shift ;;
-a|--arch) ARCH="$2"; shift ;;
-h|--help) usage ;;
--) shift; QEMU_EXTRA_ARGS=("$@"); break ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
shift
done
cd "$PROJECT_ROOT"
if [ "$BUILD" -eq 1 ]; then
echo "==> Ensuring .config is set for native build..."
if ! grep -q 'PODMAN_BUILD?=0' .config 2>/dev/null; then
echo 'PODMAN_BUILD?=0' > .config
fi
echo "==> Applying Red Bear OS patches..."
if [ -f local/scripts/apply-patches.sh ]; then
bash local/scripts/apply-patches.sh
fi
echo "==> Building cookbook..."
cargo build --release
echo "==> Building Red Bear OS ($CONFIG_NAME, $ARCH)..."
CI=1 make all "CONFIG_NAME=$CONFIG_NAME" ARCH="$ARCH"
echo "==> Build complete."
fi
BUILD_DIR="build/$ARCH/$CONFIG_NAME"
if [ ! -f "$BUILD_DIR/harddrive.img" ]; then
echo "ERROR: $BUILD_DIR/harddrive.img not found. Run with --build first."
exit 1
fi
echo "==> Launching Red Bear OS in QEMU ($CONFIG_NAME, $ARCH)..."
echo ""
exec make qemu "CONFIG_NAME=$CONFIG_NAME" ARCH="$ARCH" "${QEMU_EXTRA_ARGS[@]}"