diff --git a/.gitignore b/.gitignore index 9ca2162b..bb358dbf 100644 --- a/.gitignore +++ b/.gitignore @@ -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: diff --git a/Cargo.lock b/Cargo.lock index 950afdc4..fbbc4a49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -855,7 +855,7 @@ dependencies = [ ] [[package]] -name = "rbos_cookbook" +name = "redbear_cookbook" version = "0.1.0" dependencies = [ "ansi-to-tui", diff --git a/config/redbear-desktop.toml b/config/redbear-desktop.toml index 0882a1a3..aa50e3ad 100644 --- a/config/redbear-desktop.toml +++ b/config/redbear-desktop.toml @@ -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" +""" diff --git a/config/redbear-full.toml b/config/redbear-full.toml index db5fa856..e7caebf3 100644 --- a/config/redbear-full.toml +++ b/config/redbear-full.toml @@ -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 " — 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" +""" diff --git a/config/redbear-kde.toml b/config/redbear-kde.toml new file mode 100644 index 00000000..275a99d3 --- /dev/null +++ b/config/redbear-kde.toml @@ -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 " 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 & +""" diff --git a/config/redbear-minimal.toml b/config/redbear-minimal.toml index 6f89f035..5dd2a505 100644 --- a/config/redbear-minimal.toml +++ b/config/redbear-minimal.toml @@ -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 " 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" +""" diff --git a/local/config/my-amd-desktop.toml b/local/config/my-amd-desktop.toml index f2ae2d15..de5303a8 100644 --- a/local/config/my-amd-desktop.toml +++ b/local/config/my-amd-desktop.toml @@ -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 """ diff --git a/local/config/my-baremetal-desktop.toml b/local/config/my-baremetal-desktop.toml index fe435e96..736d8dde 100644 --- a/local/config/my-baremetal-desktop.toml +++ b/local/config/my-baremetal-desktop.toml @@ -68,5 +68,5 @@ nowait evdevd path = "/usr/lib/init.d/11_udev" data = """ requires_weak 00_drivers -nowait udev-shim +nowait udev """ diff --git a/local/config/my-intel-desktop.toml b/local/config/my-intel-desktop.toml index c7a70568..dc9189a1 100644 --- a/local/config/my-intel-desktop.toml +++ b/local/config/my-intel-desktop.toml @@ -54,5 +54,5 @@ nowait evdevd path = "/usr/lib/init.d/11_udev" data = """ requires_weak 00_drivers -nowait udev-shim +nowait udev """ diff --git a/local/recipes/system/evdevd/source/Cargo.toml b/local/recipes/system/evdevd/source/Cargo.toml index c119acac..849f11c0 100644 --- a/local/recipes/system/evdevd/source/Cargo.toml +++ b/local/recipes/system/evdevd/source/Cargo.toml @@ -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" diff --git a/local/recipes/system/evdevd/source/src/device.rs b/local/recipes/system/evdevd/source/src/device.rs index 39ef85e8..ac3aa084 100644 --- a/local/recipes/system/evdevd/source/src/device.rs +++ b/local/recipes/system/evdevd/source/src/device.rs @@ -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, + pub key_state: [u8; KEY_MAX / 8 + 1], + pub led_state: [u8; LED_MAX / 8 + 1], + pub custom_abs: BTreeMap, } 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 { + 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 { + 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 { + match self.kind { + DeviceKind::Mouse => bitmap_from_codes(&[REL_X, REL_Y, REL_WHEEL, REL_HWHEEL]), + _ => Vec::new(), + } + } + + pub fn supported_abs(&self) -> Vec { + 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 { + match self.kind { + DeviceKind::Keyboard => bitmap_from_codes(&[MSC_SCAN]), + _ => Vec::new(), + } + } + + pub fn supported_leds(&self) -> Vec { + match self.kind { + DeviceKind::Keyboard => bitmap_from_codes(&[LED_NUML, LED_CAPSL, LED_SCROLLL]), + _ => Vec::new(), + } + } + + pub fn supported_rep(&self) -> Vec { + match self.kind { + DeviceKind::Keyboard => bitmap_from_codes(&[REP_DELAY, REP_PERIOD]), + _ => Vec::new(), + } + } + + pub fn supported_props(&self) -> Vec { + 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 { + 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 +} diff --git a/local/recipes/system/evdevd/source/src/main.rs b/local/recipes/system/evdevd/source/src/main.rs index 661c993e..f67942ca 100644 --- a/local/recipes/system/evdevd/source/src/main.rs +++ b/local/recipes/system/evdevd/source/src/main.rs @@ -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, +} - 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 { + 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 { + let event_size = size_of::(); + 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::::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, + socket: &Socket, +) -> Result { + 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, + socket: &Socket, +) -> Result { + 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, +) -> Result { + 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, +) -> 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, +) -> 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() { diff --git a/local/recipes/system/evdevd/source/src/scheme.rs b/local/recipes/system/evdevd/source/src/scheme.rs index 7d1d6af3..f03768be 100644 --- a/local/recipes/system/evdevd/source/src/scheme.rs +++ b/local/recipes/system/evdevd/source/src/scheme.rs @@ -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, + }, } pub struct EvdevScheme { next_id: usize, handles: BTreeMap, devices: Vec, + grabbed_by: BTreeMap, + 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 { + 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, 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 { + 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 { + 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 { + 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(arg: usize, value: &T) -> Result { + if arg == 0 { + return Err(Error::new(EFAULT)); } + + ptr::copy_nonoverlapping( + value as *const T as *const u8, + arg as *mut u8, + size_of::(), + ); + Ok(size_of::()) + } + + unsafe fn write_bytes_to_user(arg: usize, bytes: &[u8]) -> Result { + 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(arg: usize) -> Result { + if arg == 0 { + return Err(Error::new(EFAULT)); + } + + let mut value = MaybeUninit::::uninit(); + ptr::copy_nonoverlapping( + arg as *const u8, + value.as_mut_ptr() as *mut u8, + size_of::(), + ); + Ok(value.assume_init()) + } + + fn ioctl_abs_set_axis(cmd: u64) -> Option { + 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> { 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> { + 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> { - 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> { + 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::(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::(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> { - 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 { + 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); + } } } diff --git a/local/recipes/system/evdevd/source/src/translate.rs b/local/recipes/system/evdevd/source/src/translate.rs index 72e9a4a3..75aa7476 100644 --- a/local/recipes/system/evdevd/source/src/translate.rs +++ b/local/recipes/system/evdevd/source/src/translate.rs @@ -1,35 +1,226 @@ use crate::types::*; -fn orb_key_to_evdev(orb_key: u8) -> Option { - 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 { + 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 { +pub fn translate_keyboard(scancode: u8, pressed: bool) -> Vec { 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 { } } -pub fn translate_mouse_dx(dx: i32) -> Vec { - vec![InputEvent::new(EV_REL, REL_X, dx), InputEvent::syn_report()] +pub fn translate_mouse_motion(dx: i32, dy: i32) -> Vec { + 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 { - vec![InputEvent::new(EV_REL, REL_Y, dy), InputEvent::syn_report()] -} - -pub fn translate_mouse_scroll(y: i32) -> Vec { - vec![ - InputEvent::new(EV_REL, REL_WHEEL, y), - InputEvent::syn_report(), - ] +pub fn translate_mouse_scroll(x: i32, y: i32) -> Vec { + 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 { @@ -68,10 +272,206 @@ pub fn translate_mouse_button(button: usize, pressed: bool) -> Vec { ] } -pub fn translate_touch(x: i32, y: i32, touching: bool) -> Vec { - 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 { + 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 { + 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 { + 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)); + } } diff --git a/local/recipes/system/evdevd/source/src/types.rs b/local/recipes/system/evdevd/source/src/types.rs index 77b51298..6baff135 100644 --- a/local/recipes/system/evdevd/source/src/types.rs +++ b/local/recipes/system/evdevd/source/src/types.rs @@ -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::() 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() diff --git a/local/recipes/system/firmware-loader/recipe.toml b/local/recipes/system/firmware-loader/recipe.toml index 90242b90..c5792f52 100644 --- a/local/recipes/system/firmware-loader/recipe.toml +++ b/local/recipes/system/firmware-loader/recipe.toml @@ -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" diff --git a/local/recipes/system/firmware-loader/source/Cargo.toml b/local/recipes/system/firmware-loader/source/Cargo.toml index b6011bbc..0e273efc 100644 --- a/local/recipes/system/firmware-loader/source/Cargo.toml +++ b/local/recipes/system/firmware-loader/source/Cargo.toml @@ -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" diff --git a/local/recipes/system/firmware-loader/source/src/blob.rs b/local/recipes/system/firmware-loader/source/src/blob.rs index 3afa2456..a4e170d7 100644 --- a/local/recipes/system/firmware-loader/source/src/blob.rs +++ b/local/recipes/system/firmware-loader/source/src/blob.rs @@ -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() diff --git a/local/recipes/system/firmware-loader/source/src/main.rs b/local/recipes/system/firmware-loader/source/src/main.rs index eb459519..980b4907 100644 --- a/local/recipes/system/firmware-loader/source/src/main.rs +++ b/local/recipes/system/firmware-loader/source/src/main.rs @@ -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); } diff --git a/local/recipes/system/firmware-loader/source/src/scheme.rs b/local/recipes/system/firmware-loader/source/src/scheme.rs index 3e065f0f..3bc60096 100644 --- a/local/recipes/system/firmware-loader/source/src/scheme.rs +++ b/local/recipes/system/firmware-loader/source/src/scheme.rs @@ -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>, - 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 { @@ -65,8 +75,23 @@ fn resolve_key(path: &str) -> Option { Some(key) } -impl SchemeBlockMut for FirmwareScheme { - fn open(&mut self, path: &str, _flags: usize, _uid: u32, _gid: u32) -> Result> { +impl SchemeSync for FirmwareScheme { + fn scheme_root(&mut self) -> Result { + Ok(SCHEME_ROOT_ID) + } + + fn openat( + &mut self, + dirfd: usize, + path: &str, + _flags: usize, + _fcntl_flags: u32, + _ctx: &CallerCtx, + ) -> Result { + 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> { - 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> { - 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 { + 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> { - 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 { + let _ = self.handle(id)?; Err(Error::new(EROFS)) } - fn fpath(&mut self, id: usize, buf: &mut [u8]) -> Result> { - let handle = self.handles.get(&id).ok_or(Error::new(EBADF))?; + fn fpath(&mut self, id: usize, buf: &mut [u8], _ctx: &CallerCtx) -> Result { + 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> { - 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> { - 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> { - 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 { + let _ = self.handle(id)?; + Ok(0) } - fn close(&mut self, id: usize) -> Result> { - 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 { + 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 { + 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> { - let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?; + _flags: syscall::MapFlags, + _ctx: &CallerCtx, + ) -> Result { + 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> { - 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); + } + } } } diff --git a/local/recipes/system/iommu/recipe.toml b/local/recipes/system/iommu/recipe.toml new file mode 100644 index 00000000..31378216 --- /dev/null +++ b/local/recipes/system/iommu/recipe.toml @@ -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" diff --git a/local/recipes/system/iommu/source/Cargo.toml b/local/recipes/system/iommu/source/Cargo.toml new file mode 100644 index 00000000..5acfaa79 --- /dev/null +++ b/local/recipes/system/iommu/source/Cargo.toml @@ -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" } diff --git a/local/recipes/system/iommu/source/src/acpi.rs b/local/recipes/system/iommu/source/src/acpi.rs new file mode 100644 index 00000000..a2828694 --- /dev/null +++ b/local/recipes/system/iommu/source/src/acpi.rs @@ -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 { + 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::()) + .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 }, +} + +#[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, +} + +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 = 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, +} + +#[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 { + 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 { + 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 { + bytes + .get(offset..offset + 2)? + .try_into() + .ok() + .map(u16::from_le_bytes) +} + +fn read_u32(bytes: &[u8], offset: usize) -> Option { + bytes + .get(offset..offset + 4)? + .try_into() + .ok() + .map(u32::from_le_bytes) +} + +fn read_u64(bytes: &[u8], offset: usize) -> Option { + 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]) -> Vec { + let length = (IVRS_HEADER_BYTES + units.iter().map(Vec::len).sum::()) 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 { + 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))); + } +} diff --git a/local/recipes/system/iommu/source/src/amd_vi.rs b/local/recipes/system/iommu/source/src/amd_vi.rs new file mode 100644 index 00000000..e202f8c7 --- /dev/null +++ b/local/recipes/system/iommu/source/src/amd_vi.rs @@ -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, + device_table: Option, + command_buffer: Option, + event_log: Option, + interrupt_table: Option, + completion_store: Option, + 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, 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, 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 { + 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 { + 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))); + } +} diff --git a/local/recipes/system/iommu/source/src/command_buffer.rs b/local/recipes/system/iommu/source/src/command_buffer.rs new file mode 100644 index 00000000..1a93e9c2 --- /dev/null +++ b/local/recipes/system/iommu/source/src/command_buffer.rs @@ -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::() == COMMAND_ENTRY_SIZE); + +pub struct CommandBuffer { + buffer: DmaBuffer, + capacity: usize, +} + +impl CommandBuffer { + pub fn new(entry_count: usize) -> Result { + 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::() == EVENT_LOG_ENTRY_SIZE); + +pub struct EventLog { + buffer: DmaBuffer, + capacity: usize, +} + +impl EventLog { + pub fn new(entry_count: usize) -> Result { + 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); + } +} diff --git a/local/recipes/system/iommu/source/src/device_table.rs b/local/recipes/system/iommu/source/src/device_table.rs new file mode 100644 index 00000000..9e1d0db0 --- /dev/null +++ b/local/recipes/system/iommu/source/src/device_table.rs @@ -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::() == 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 { + 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 { + 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(()) + } +} diff --git a/local/recipes/system/iommu/source/src/interrupt.rs b/local/recipes/system/iommu/source/src/interrupt.rs new file mode 100644 index 00000000..a472b66c --- /dev/null +++ b/local/recipes/system/iommu/source/src/interrupt.rs @@ -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::() == 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 { + 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::(), self.capacity) } + } + + fn entries_mut(&mut self) -> &mut [AmdIrte] { + unsafe { + slice::from_raw_parts_mut(self.buffer.as_mut_ptr().cast::(), 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); + } +} diff --git a/local/recipes/system/iommu/source/src/lib.rs b/local/recipes/system/iommu/source/src/lib.rs new file mode 100644 index 00000000..4b92ebff --- /dev/null +++ b/local/recipes/system/iommu/source/src/lib.rs @@ -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 { + 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 { + 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, +} + +pub struct IommuScheme { + units: Vec, + next_id: usize, + handles: BTreeMap, + domains: BTreeMap, + device_assignments: BTreeMap, +} + +impl IommuScheme { + pub fn new() -> Self { + Self::with_units(Vec::new()) + } + + pub fn with_units(units: Vec) -> 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 { + (1..u16::MAX).find(|domain_id| !self.domains.contains_key(domain_id)) + } + + fn root_listing(&self) -> Vec { + 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 { + let trimmed = path.trim(); + trimmed + .strip_prefix("0x") + .and_then(|hex| u16::from_str_radix(hex, 16).ok()) + .or_else(|| trimmed.parse::().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, + ) -> core::result::Result { + 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> { + 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> { + 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> { + 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> { + 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> { + 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> { + 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> { + let _ = self.handles.get(&id).ok_or(Error::new(EBADF))?; + Ok(Some(EventFlags::empty())) + } + + fn close(&mut self, id: usize) -> Result> { + 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); + } +} diff --git a/local/recipes/system/iommu/source/src/main.rs b/local/recipes/system/iommu/source/src/main.rs new file mode 100644 index 00000000..6ca49a9b --- /dev/null +++ b/local/recipes/system/iommu/source/src/main.rs @@ -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, 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); + } +} diff --git a/local/recipes/system/iommu/source/src/mmio.rs b/local/recipes/system/iommu/source/src/mmio.rs new file mode 100644 index 00000000..01e91a17 --- /dev/null +++ b/local/recipes/system/iommu/source/src/mmio.rs @@ -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::() == 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::::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::(); + let control_ptr = byte_base.add(offsets::CONTROL).cast::(); + let head_ptr = byte_base.add(offsets::CMD_BUF_HEAD).cast::(); + + assert_eq!(core::ptr::read_volatile(control_ptr), 0xdead_beef); + assert_eq!(core::ptr::read_volatile(head_ptr), 0x1122_3344_5566_7788); + } + } +} diff --git a/local/recipes/system/iommu/source/src/page_table.rs b/local/recipes/system/iommu/source/src/page_table.rs new file mode 100644 index 00000000..bfcfde75 --- /dev/null +++ b/local/recipes/system/iommu/source/src/page_table.rs @@ -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::() == 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, + layout: Layout, + len: usize, + }, +} + +struct PageBuffer { + storage: PageStorage, + phys_addr: usize, +} + +impl PageBuffer { + fn allocate(len: usize, align: usize) -> Result { + 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 { + 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::(), PTES_PER_PAGE) } + } + + fn entries_mut(&mut self) -> &mut [AmdPte] { + unsafe { + slice::from_raw_parts_mut(self.buffer.as_mut_ptr().cast::(), PTES_PER_PAGE) + } + } +} + +struct PageTableNode { + page: PageTablePage, + children: BTreeMap>, +} + +impl PageTableNode { + fn new() -> Result { + Ok(Self { + page: PageTablePage::new()?, + children: BTreeMap::new(), + }) + } + + fn phys_addr(&self) -> u64 { + self.page.physical_address() + } +} + +pub struct PageTable { + levels: u8, + root: Box, +} + +impl PageTable { + pub fn new(levels: u8) -> Result { + 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 { + 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, +} + +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 { + 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 { + 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, +} + +impl DomainPageTables { + pub fn new(domain_id: u16) -> Result { + Self::with_range(domain_id, DEFAULT_IOVA_BASE, DEFAULT_IOVA_LIMIT) + } + + pub fn with_range(domain_id: u16, base: u64, limit: u64) -> Result { + 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, + ) -> Result { + 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 { + 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 { + 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()); + } +} diff --git a/local/recipes/system/rbos-info b/local/recipes/system/rbos-info new file mode 120000 index 00000000..2b423222 --- /dev/null +++ b/local/recipes/system/rbos-info @@ -0,0 +1 @@ +redbear-info \ No newline at end of file diff --git a/local/recipes/system/redbear-hwutils/recipe.toml b/local/recipes/system/redbear-hwutils/recipe.toml new file mode 100644 index 00000000..53a29a53 --- /dev/null +++ b/local/recipes/system/redbear-hwutils/recipe.toml @@ -0,0 +1,9 @@ +[source] +path = "source" + +[build] +template = "cargo" + +[package.files] +"/usr/bin/lspci" = "lspci" +"/usr/bin/lsusb" = "lsusb" diff --git a/local/recipes/system/redbear-hwutils/source/Cargo.toml b/local/recipes/system/redbear-hwutils/source/Cargo.toml new file mode 100644 index 00000000..ec71962b --- /dev/null +++ b/local/recipes/system/redbear-hwutils/source/Cargo.toml @@ -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" } diff --git a/local/recipes/system/redbear-hwutils/source/src/bin/lspci.rs b/local/recipes/system/redbear-hwutils/source/src/bin/lspci.rs new file mode 100644 index 00000000..b3c1a255 --- /dev/null +++ b/local/recipes/system/redbear-hwutils/source/src/bin/lspci.rs @@ -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, 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) +} diff --git a/local/recipes/system/redbear-hwutils/source/src/bin/lsusb.rs b/local/recipes/system/redbear-hwutils/source/src/bin/lsusb.rs new file mode 100644 index 00000000..bbf9edba --- /dev/null +++ b/local/recipes/system/redbear-hwutils/source/src/bin/lsusb.rs @@ -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, Vec), 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::() 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)) +} diff --git a/local/recipes/system/redbear-hwutils/source/src/lib.rs b/local/recipes/system/redbear-hwutils/source/src/lib.rs new file mode 100644 index 00000000..4125771c --- /dev/null +++ b/local/recipes/system/redbear-hwutils/source/src/lib.rs @@ -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 { + 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, +) -> Result<(), String> { + let extras: Vec = 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(" ") + } +} diff --git a/local/recipes/system/redbear-info/source/src/main.rs b/local/recipes/system/redbear-info/source/src/main.rs index 76596744..c35fb194 100644 --- a/local/recipes/system/redbear-info/source/src/main.rs +++ b/local/recipes/system/redbear-info/source/src/main.rs @@ -1,24 +1,21 @@ use std::env; use std::fs; -use std::path::Path; +use std::path::PathBuf; use std::process; +#[cfg(test)] +use std::path::Path; +#[cfg(test)] +use std::time::{SystemTime, UNIX_EPOCH}; + const RESET: &str = "\x1b[0m"; const GREEN: &str = "\x1b[32m"; const YELLOW: &str = "\x1b[33m"; const RED: &str = "\x1b[31m"; +const BLUE: &str = "\x1b[34m"; const DIVIDER: &str = "═══════════════════════════════════════════════════════════════════"; -const REDBEAR_META_README: &str = "/usr/share/doc/redbear-meta/README"; - -struct Component { - name: &'static str, - description: &'static str, - category: &'static str, - scheme_path: &'static str, - binary_path: &'static str, - test_hint: &'static str, - dependencies: &'static [&'static str], -} +const RTL8125_VENDOR_ID: u16 = 0x10ec; +const RTL8125_DEVICE_ID: u16 = 0x8125; #[derive(Clone, Copy, PartialEq, Eq)] enum OutputMode { @@ -33,113 +30,213 @@ struct Options { verbose: bool, } -#[derive(Clone, Copy, PartialEq, Eq)] -enum AvailabilityState { - Available, - Unavailable, - BuiltIn, +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ProbeState { + Absent, + Present, + Active, + Functional, + Unobservable, } -struct ComponentStatus<'a> { - component: &'a Component, - state: AvailabilityState, - available: bool, - status_text: &'static str, - scheme_exists: Option, - binary_exists: Option, +struct Runtime { + root: Option, } -const COMPONENTS: &[Component] = &[ - Component { - name: "redbear-release", - description: "OS identity (hostname, os-release, motd, banner)", - category: "Branding", - scheme_path: "", - binary_path: "", - test_hint: "cat /usr/lib/os-release", - dependencies: &[], +struct IdentityReport { + pretty_name: Option, + version_id: Option, + hostname: Option, +} + +struct NetworkReport { + state: ProbeState, + connected: bool, + interface: Option, + mac: Option, + address: Option, + dns: Option, + default_route: Option, + active_profile: Option, + network_schemes: Vec, + claim_limit: &'static str, +} + +struct HardwareReport { + pci_devices: usize, + usb_controllers: usize, + drm_cards: usize, + rtl8125_present: bool, +} + +struct IntegrationCheck { + name: &'static str, + category: &'static str, + description: &'static str, + artifact_path: Option<&'static str>, + control_path: Option<&'static str>, + test_hint: &'static str, + note: &'static str, + functional_probe: + Option Option>, +} + +struct IntegrationStatus<'a> { + check: &'a IntegrationCheck, + state: ProbeState, + artifact_present: Option, + control_present: Option, + evidence: Vec, + claim_limit: &'static str, +} + +struct Report<'a> { + identity: IdentityReport, + network: NetworkReport, + hardware: HardwareReport, + integrations: Vec>, +} + +const INTEGRATIONS: &[IntegrationCheck] = &[ + IntegrationCheck { + name: "redbear-info", + category: "Tool", + description: "Runtime integration status utility", + artifact_path: Some("/usr/bin/redbear-info"), + control_path: None, + test_hint: "redbear-info --json", + note: "Binary presence proves the tool is installed, not that every integration is healthy.", + functional_probe: None, }, - Component { + IntegrationCheck { + name: "lspci", + category: "Tool", + description: "Native PCI inventory command", + artifact_path: Some("/usr/bin/lspci"), + control_path: Some("/scheme/pci"), + test_hint: "lspci", + note: "Functional when the PCI scheme is enumerable.", + functional_probe: Some(probe_directory_readable), + }, + IntegrationCheck { + name: "lsusb", + category: "Tool", + description: "Native USB inventory command", + artifact_path: Some("/usr/bin/lsusb"), + control_path: Some("/scheme"), + test_hint: "lsusb", + note: "Functional when at least one usb.* controller scheme is readable.", + functional_probe: Some(probe_usb_surface), + }, + IntegrationCheck { + name: "netctl", + category: "Tool", + description: "Redox-native network profile manager", + artifact_path: Some("/usr/bin/netctl"), + control_path: Some("/etc/netctl"), + test_hint: "netctl status", + note: "Profiles and active profile tracking are readable; profile application remains a separate runtime action.", + functional_probe: Some(probe_netctl_surface), + }, + IntegrationCheck { + name: "pcid-spawner", + category: "Core", + description: "PCI driver autoload daemon", + artifact_path: Some("/usr/bin/pcid-spawner"), + control_path: Some("/scheme/pci"), + test_hint: "lspci", + note: "The PCI scheme proves discovery is live, but not which driver handled each device.", + functional_probe: Some(probe_directory_readable), + }, + IntegrationCheck { + name: "smolnetd", + category: "Networking", + description: "Native TCP/IP stack daemon", + artifact_path: Some("/usr/bin/smolnetd"), + control_path: Some("/scheme/netcfg"), + test_hint: "redbear-info --verbose", + note: "Functional when the netcfg scheme answers read-only queries.", + functional_probe: Some(probe_smolnetd_surface), + }, + IntegrationCheck { + name: "dhcpd", + category: "Networking", + description: "DHCP client daemon", + artifact_path: Some("/usr/bin/dhcpd"), + control_path: None, + test_hint: "netctl start ", + note: "Binary presence is observable; passive probing cannot prove the DHCP client is currently driving configuration.", + functional_probe: None, + }, + IntegrationCheck { name: "ext4d", - description: "ext4 scheme daemon", category: "Filesystem", - scheme_path: "/scheme/ext4d", - binary_path: "/usr/bin/ext4d", + description: "ext4 scheme daemon", + artifact_path: Some("/usr/bin/ext4d"), + control_path: Some("/scheme/ext4d"), test_hint: "ls /scheme/ext4d/", - dependencies: &[], + note: "Functional when the ext4 scheme directory can be enumerated.", + functional_probe: Some(probe_directory_readable), }, - Component { - name: "redox-driver-sys", - description: "Safe Rust wrappers for scheme:memory, scheme:irq, scheme:pci", - category: "Driver", - scheme_path: "", - binary_path: "", - test_hint: "pkg list | grep redox-driver-sys", - dependencies: &[], - }, - Component { - name: "linux-kpi", - description: - "Linux Kernel Programming Interface compatibility layer (C headers + Rust impl)", - category: "Driver", - scheme_path: "", - binary_path: "", - test_hint: "pkg list | grep linux-kpi", - dependencies: &["redox-driver-sys"], - }, - Component { + IntegrationCheck { name: "firmware-loader", - description: "Loads GPU firmware blobs via scheme:firmware", category: "System", - scheme_path: "/scheme/firmware", - binary_path: "/usr/lib/drivers/firmware-loader", - test_hint: "ls /scheme/firmware/amdgpu/", - dependencies: &[], + description: "Firmware indexing and serving daemon", + artifact_path: Some("/usr/bin/firmware-loader"), + control_path: Some("/scheme/firmware"), + test_hint: "ls /scheme/firmware/", + note: "Functional when the firmware scheme is enumerable.", + functional_probe: Some(probe_directory_readable), }, - Component { - name: "redox-drm", - description: "DRM display driver for AMD and Intel GPUs", - category: "GPU", - scheme_path: "/scheme/drm", - binary_path: "/usr/bin/redox-drm", - test_hint: "ls /scheme/drm/card0/", - dependencies: &["redox-driver-sys", "linux-kpi"], - }, - Component { - name: "amdgpu", - description: "AMD GPU driver (Display Core modesetting) via LinuxKPI", - category: "GPU", - scheme_path: "", - binary_path: "/usr/lib/redox/drivers/libamdgpu_dc_redox.so", - test_hint: "ls -la /usr/lib/redox/drivers/libamdgpu_dc_redox.so", - dependencies: &["redox-driver-sys", "linux-kpi", "firmware-loader"], - }, - Component { - name: "evdevd", - description: "Translates Redox input events to evdev protocol", - category: "Input", - scheme_path: "/scheme/evdev", - binary_path: "/usr/lib/drivers/evdevd", - test_hint: "ls /scheme/evdev/", - dependencies: &[], - }, - Component { + IntegrationCheck { name: "udev-shim", - description: "udev-compatible device enumeration shim (PCI scanning)", category: "System", - scheme_path: "/scheme/udev", - binary_path: "/usr/lib/drivers/udev-shim", + description: "udev-compatible device enumeration shim", + artifact_path: Some("/usr/bin/udev"), + control_path: Some("/scheme/udev"), test_hint: "ls /scheme/udev/", - dependencies: &[], + note: "Functional when the udev scheme can be listed.", + functional_probe: Some(probe_directory_readable), }, - Component { - name: "redbear-meta", - description: "Umbrella meta-package depending on all Red Bear OS components", - category: "System", - scheme_path: "", - binary_path: "", - test_hint: "cat /usr/share/doc/redbear-meta/README", - dependencies: &["redbear-release", "firmware-loader", "evdevd", "udev-shim"], + IntegrationCheck { + name: "evdevd", + category: "Input", + description: "Event-device translation daemon", + artifact_path: Some("/usr/lib/drivers/evdevd"), + control_path: Some("/scheme/evdev"), + test_hint: "ls /scheme/evdev/", + note: "Functional when event nodes are enumerable through the evdev scheme.", + functional_probe: Some(probe_directory_readable), + }, + IntegrationCheck { + name: "redox-drm", + category: "GPU", + description: "DRM/KMS scheme daemon", + artifact_path: None, + control_path: Some("/scheme/drm"), + test_hint: "ls /scheme/drm/", + note: "A live DRM scheme proves the daemon is running, not that hardware display is fully validated.", + functional_probe: Some(probe_directory_readable), + }, + IntegrationCheck { + name: "amdgpu", + category: "GPU", + description: "AMD GPU userspace driver library", + artifact_path: Some("/usr/lib/redox/drivers/libamdgpu_dc_redox.so"), + control_path: None, + test_hint: "redbear-info --verbose", + note: "Library presence proves packaging; runtime GPU validation still depends on actual hardware and redox-drm activity.", + functional_probe: None, + }, + IntegrationCheck { + name: "rtl8125-native-path", + category: "Networking", + description: "Native Realtek RTL8125 support through the rtl8168d autoload path", + artifact_path: Some("/usr/lib/drivers/rtl8168d"), + control_path: Some("/scheme/pci"), + test_hint: "redbear-info --verbose", + note: "This only becomes functional when 10ec:8125 hardware is present and a network.* scheme is live.", + functional_probe: Some(probe_rtl8125_path), }, ]; @@ -152,25 +249,269 @@ fn main() { fn run() -> Result<(), String> { let options = parse_args(env::args())?; - if options.mode == OutputMode::Help { print_help(); return Ok(()); } - let branding_available = has_red_bear_branding(); - let statuses = collect_statuses(branding_available); + let runtime = Runtime::from_env(); + let report = collect_report(&runtime); match options.mode { - OutputMode::Table => print_table(&statuses, options.verbose), - OutputMode::Json => print_json(&statuses), - OutputMode::Test => print_tests(&statuses, options.verbose), + OutputMode::Table => print_table(&report, options.verbose), + OutputMode::Json => print_json(&report), + OutputMode::Test => print_tests(&report, options.verbose), OutputMode::Help => {} } Ok(()) } +impl Runtime { + fn from_env() -> Self { + Self { + root: env::var_os("REDBEAR_INFO_ROOT").map(PathBuf::from), + } + } + + #[cfg(test)] + fn from_root(root: PathBuf) -> Self { + Self { root: Some(root) } + } + + fn resolve(&self, absolute: &str) -> PathBuf { + let trimmed = absolute.trim_start_matches('/'); + match &self.root { + Some(root) => root.join(trimmed), + None => PathBuf::from(absolute), + } + } + + fn exists(&self, absolute: &str) -> bool { + self.resolve(absolute).exists() + } + + fn is_dir(&self, absolute: &str) -> bool { + self.resolve(absolute).is_dir() + } + + fn read_to_string(&self, absolute: &str) -> Option { + fs::read_to_string(self.resolve(absolute)).ok() + } + + fn read_dir_names(&self, absolute: &str) -> Option> { + let mut names = Vec::new(); + for entry in fs::read_dir(self.resolve(absolute)).ok()? { + let entry = entry.ok()?; + let name = entry.file_name(); + let name = name.to_str()?.to_string(); + names.push(name); + } + names.sort(); + Some(names) + } +} + +fn collect_report<'a>(runtime: &Runtime) -> Report<'a> { + let identity = collect_identity(runtime); + let network = collect_network(runtime); + let hardware = collect_hardware(runtime, &network); + let integrations = INTEGRATIONS + .iter() + .map(|check| inspect_integration(runtime, &network, &hardware, check)) + .collect(); + + Report { + identity, + network, + hardware, + integrations, + } +} + +fn collect_identity(runtime: &Runtime) -> IdentityReport { + let os_release = runtime.read_to_string("/usr/lib/os-release"); + IdentityReport { + pretty_name: os_release + .as_deref() + .and_then(|content| parse_os_release_value(content, "PRETTY_NAME")), + version_id: os_release + .as_deref() + .and_then(|content| parse_os_release_value(content, "VERSION_ID")), + hostname: runtime + .read_to_string("/etc/hostname") + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()), + } +} + +fn collect_network(runtime: &Runtime) -> NetworkReport { + let network_schemes = runtime + .read_dir_names("/scheme") + .unwrap_or_default() + .into_iter() + .filter(|name| name.starts_with("network.")) + .collect::>(); + + let mac = read_trimmed(runtime, "/scheme/netcfg/ifaces/eth0/mac") + .filter(|value| !matches!(value.as_str(), "Not configured" | "Device not found")); + + let address = read_trimmed(runtime, "/scheme/netcfg/ifaces/eth0/addr/list") + .filter(|value| !matches!(value.as_str(), "Not configured" | "Device not found")); + + let dns = read_trimmed(runtime, "/scheme/netcfg/resolv/nameserver") + .or_else(|| read_trimmed(runtime, "/etc/net/dns")) + .filter(|value| !value.is_empty()); + + let default_route = read_trimmed(runtime, "/scheme/netcfg/route/list") + .and_then(|routes| parse_default_route(&routes)); + + let active_profile = + read_trimmed(runtime, "/etc/netctl/active").filter(|value| !value.is_empty()); + + let state = if runtime.exists("/scheme/netcfg") { + if address.is_some() { + ProbeState::Functional + } else { + ProbeState::Active + } + } else if runtime.exists("/usr/bin/smolnetd") { + ProbeState::Present + } else { + ProbeState::Absent + }; + + NetworkReport { + state, + connected: address.is_some(), + interface: runtime.exists("/scheme/netcfg/ifaces/eth0/mac").then_some("eth0".to_string()), + mac, + address, + dns, + default_route, + active_profile, + network_schemes, + claim_limit: "Connected means the local stack exposes a configured address; this does not prove external reachability.", + } +} + +fn collect_hardware(runtime: &Runtime, network: &NetworkReport) -> HardwareReport { + let pci_entries = runtime.read_dir_names("/scheme/pci").unwrap_or_default(); + let pci_devices = pci_entries + .iter() + .filter(|entry| entry.contains("--") && entry.contains('.')) + .count(); + + let usb_controllers = runtime + .read_dir_names("/scheme") + .unwrap_or_default() + .into_iter() + .filter(|name| name.starts_with("usb.")) + .count(); + + let drm_cards = runtime + .read_dir_names("/scheme/drm") + .unwrap_or_default() + .into_iter() + .filter(|name| name.starts_with("card")) + .count(); + + let rtl8125_present = pci_entries.into_iter().any(|entry| { + let config_path = format!("/scheme/pci/{entry}/config"); + let Some(bytes) = read_bytes(runtime, &config_path) else { + return false; + }; + if bytes.len() < 4 { + return false; + } + let vendor = u16::from_le_bytes([bytes[0], bytes[1]]); + let device = u16::from_le_bytes([bytes[2], bytes[3]]); + vendor == RTL8125_VENDOR_ID && device == RTL8125_DEVICE_ID + }) || network + .network_schemes + .iter() + .any(|name| name.contains("rtl8125")); + + HardwareReport { + pci_devices, + usb_controllers, + drm_cards, + rtl8125_present, + } +} + +fn inspect_integration<'a>( + runtime: &Runtime, + network: &NetworkReport, + hardware: &HardwareReport, + check: &'a IntegrationCheck, +) -> IntegrationStatus<'a> { + let artifact_present = check.artifact_path.map(|path| runtime.exists(path)); + let control_present = check.control_path.map(|path| runtime.exists(path)); + + let mut evidence = Vec::new(); + + if let Some(path) = check.artifact_path { + evidence.push(format!( + "artifact {} {}", + path, + if artifact_present == Some(true) { + "present" + } else { + "missing" + } + )); + } + if let Some(path) = check.control_path { + evidence.push(format!( + "control {} {}", + path, + if control_present == Some(true) { + "present" + } else { + "missing" + } + )); + } + + let state = if let Some(probe) = check.functional_probe { + match probe(runtime, network, hardware, check) { + Some(message) => { + evidence.push(message); + if artifact_present == Some(false) { + ProbeState::Active + } else { + ProbeState::Functional + } + } + None => derive_state(artifact_present, control_present), + } + } else { + derive_state(artifact_present, control_present) + }; + + IntegrationStatus { + check, + state, + artifact_present, + control_present, + evidence, + claim_limit: check.note, + } +} + +fn derive_state(artifact_present: Option, control_present: Option) -> ProbeState { + if control_present == Some(true) { + ProbeState::Active + } else if artifact_present == Some(true) { + ProbeState::Present + } else if artifact_present.is_none() && control_present.is_none() { + ProbeState::Unobservable + } else { + ProbeState::Absent + } +} + fn parse_args(args: I) -> Result where I: IntoIterator, @@ -201,259 +542,357 @@ where Ok(Options { mode, verbose }) } -fn has_red_bear_branding() -> bool { - match fs::read_to_string("/usr/lib/os-release") { - Ok(contents) => contents.contains("Red Bear OS"), - Err(_) => false, - } -} - -fn collect_statuses(branding_available: bool) -> Vec> { - COMPONENTS - .iter() - .map(|component| inspect_component(component, branding_available)) - .collect() -} - -fn inspect_component( - component: &'static Component, - branding_available: bool, -) -> ComponentStatus<'static> { - let scheme_exists = if component.scheme_path.is_empty() { - None - } else { - Some(Path::new(component.scheme_path).exists()) - }; - - let binary_exists = if component.binary_path.is_empty() { - None - } else { - Some(Path::new(component.binary_path).exists()) - }; - - let (state, available, status_text) = if component.name == "redbear-release" { - if branding_available { - (AvailabilityState::Available, true, "available") - } else { - (AvailabilityState::Unavailable, false, "not configured") - } - } else if component.name == "redbear-meta" { - if Path::new(REDBEAR_META_README).exists() { - (AvailabilityState::Available, true, "available") - } else { - (AvailabilityState::Unavailable, false, "missing") - } - } else if let Some(exists) = scheme_exists { - if exists { - (AvailabilityState::Available, true, "available") - } else { - (AvailabilityState::Unavailable, false, "not running") - } - } else if let Some(exists) = binary_exists { - if exists { - (AvailabilityState::Available, true, "available") - } else { - (AvailabilityState::Unavailable, false, "missing") - } - } else { - (AvailabilityState::BuiltIn, true, "built-in") - }; - - ComponentStatus { - component, - state, - available, - status_text, - scheme_exists, - binary_exists, - } -} - -fn print_table(statuses: &[ComponentStatus<'_>], verbose: bool) { - let name_width = statuses - .iter() - .map(|status| status.component.name.len()) - .max() - .unwrap_or(0); - let category_width = statuses - .iter() - .map(|status| status.component.category.len()) - .max() - .unwrap_or(0); - - println!("Red Bear OS Component Status"); +fn print_table(report: &Report<'_>, verbose: bool) { + println!("Red Bear OS Runtime Integration Report"); println!("{DIVIDER}"); println!(); - for status in statuses { - println!( - " {} {:name_width$} [{:category_width$}] {}", - colorize(marker_for(status), marker_color(status)), - status.component.name, - status.component.category, - colorize(status.status_text, status_color(status)), - name_width = name_width, - category_width = category_width, - ); - println!(" {}", status.component.description); - println!(" Test: {}", status.component.test_hint); + print_section_header("Identity"); + println!( + " OS: {}", + display_or_unknown(report.identity.pretty_name.as_deref()) + ); + println!( + " Version: {}", + display_or_unknown(report.identity.version_id.as_deref()) + ); + println!( + " Hostname: {}", + display_or_unknown(report.identity.hostname.as_deref()) + ); + println!(); - if verbose { - println!( - " Dependencies: {}", - format_dependencies(status.component.dependencies) - ); + print_section_header("Networking"); + println!( + " Stack: {} {}", + colorize( + state_marker(report.network.state), + state_color(report.network.state) + ), + state_label(report.network.state) + ); + println!( + " Connected: {}", + if report.network.connected { + "yes" + } else { + "no" } + ); + println!( + " Interface: {}", + display_or_unknown(report.network.interface.as_deref()) + ); + println!( + " MAC: {}", + display_or_unknown(report.network.mac.as_deref()) + ); + println!( + " Address: {}", + display_or_unknown(report.network.address.as_deref()) + ); + println!( + " DNS: {}", + display_or_unknown(report.network.dns.as_deref()) + ); + println!( + " Default route: {}", + display_or_unknown(report.network.default_route.as_deref()) + ); + println!( + " Active profile: {}", + display_or_unknown(report.network.active_profile.as_deref()) + ); + println!( + " Network schemes: {}", + if report.network.network_schemes.is_empty() { + "none".to_string() + } else { + report.network.network_schemes.join(", ") + } + ); + if verbose { + println!(" Note: {}", report.network.claim_limit); + } + println!(); + print_section_header("Hardware"); + println!(" PCI devices: {}", report.hardware.pci_devices); + println!(" USB controllers: {}", report.hardware.usb_controllers); + println!(" DRM cards: {}", report.hardware.drm_cards); + println!( + " RTL8125 device seen: {}", + if report.hardware.rtl8125_present { + "yes" + } else { + "no" + } + ); + println!(); + + print_section_header("Integrations"); + for integration in &report.integrations { + println!( + " {} {:<18} [{}] {}", + colorize( + state_marker(integration.state), + state_color(integration.state) + ), + integration.check.name, + integration.check.category, + state_label(integration.state) + ); + println!(" {}", integration.check.description); + println!(" Test: {}", integration.check.test_hint); + if verbose { + for line in &integration.evidence { + println!(" Evidence: {line}"); + } + println!(" Claim limit: {}", integration.claim_limit); + } println!(); } println!("{DIVIDER}"); println!( - "{}/{} components available", - available_count(statuses), - statuses.len() + "functional={} active={} present={} absent={} total={}", + count_state(&report.integrations, ProbeState::Functional), + count_state(&report.integrations, ProbeState::Active), + count_state(&report.integrations, ProbeState::Present), + count_state(&report.integrations, ProbeState::Absent), + report.integrations.len() ); } -fn print_tests(statuses: &[ComponentStatus<'_>], verbose: bool) { +fn print_tests(report: &Report<'_>, verbose: bool) { println!("Red Bear OS Runtime Test Hints"); println!("{DIVIDER}"); println!(); + println!(" redbear-info --json"); + println!(" redbear-info --verbose"); + println!(" netctl status"); + println!(" lspci"); + println!(" lsusb"); + println!(); - let mut printed = 0usize; - - for status in statuses.iter().filter(|status| status.available) { + for integration in report + .integrations + .iter() + .filter(|integration| integration.state != ProbeState::Absent) + { println!( - " {} {:<16} {}", - colorize("●", GREEN), - status.component.name, - status.component.test_hint, + " {:<18} {}", + integration.check.name, integration.check.test_hint ); - if verbose { - println!( - " Dependencies: {}", - format_dependencies(status.component.dependencies) - ); + println!(" {}", integration.claim_limit); } - - printed += 1; - } - - if printed == 0 { - println!(" No available Red Bear OS components detected."); } println!(); - println!("{DIVIDER}"); - println!("{} test command(s) ready", printed); + println!("Network interpretation: {}", report.network.claim_limit); } -fn print_json(statuses: &[ComponentStatus<'_>]) { - let mut output = String::new(); +fn print_json(report: &Report<'_>) { + let mut out = String::new(); + out.push_str("{\n"); - output.push_str("{\n"); - output.push_str(" \"summary\": {\n"); - output.push_str(&format!( - " \"available\": {},\n \"total\": {}\n", - available_count(statuses), - statuses.len() - )); - output.push_str(" },\n"); - output.push_str(" \"components\": [\n"); + out.push_str(" \"identity\": {\n"); + push_json_field( + &mut out, + "pretty_name", + report.identity.pretty_name.as_deref(), + true, + 4, + ); + push_json_field( + &mut out, + "version_id", + report.identity.version_id.as_deref(), + true, + 4, + ); + push_json_field( + &mut out, + "hostname", + report.identity.hostname.as_deref(), + false, + 4, + ); + out.push_str(" },\n"); - for (index, status) in statuses.iter().enumerate() { - output.push_str(" {\n"); - push_json_field(&mut output, "name", status.component.name, true); - push_json_field( - &mut output, + out.push_str(" \"network\": {\n"); + push_json_string_field( + &mut out, + "state", + state_label(report.network.state), + true, + 4, + ); + push_json_bool_field(&mut out, "connected", report.network.connected, true, 4); + push_json_field( + &mut out, + "interface", + report.network.interface.as_deref(), + true, + 4, + ); + push_json_field(&mut out, "mac", report.network.mac.as_deref(), true, 4); + push_json_field( + &mut out, + "address", + report.network.address.as_deref(), + true, + 4, + ); + push_json_field(&mut out, "dns", report.network.dns.as_deref(), true, 4); + push_json_field( + &mut out, + "default_route", + report.network.default_route.as_deref(), + true, + 4, + ); + push_json_field( + &mut out, + "active_profile", + report.network.active_profile.as_deref(), + true, + 4, + ); + push_json_string_array_field( + &mut out, + "network_schemes", + &report.network.network_schemes, + true, + 4, + ); + push_json_string_field( + &mut out, + "claim_limit", + report.network.claim_limit, + false, + 4, + ); + out.push_str(" },\n"); + + out.push_str(" \"hardware\": {\n"); + push_json_number_field( + &mut out, + "pci_devices", + report.hardware.pci_devices, + true, + 4, + ); + push_json_number_field( + &mut out, + "usb_controllers", + report.hardware.usb_controllers, + true, + 4, + ); + push_json_number_field(&mut out, "drm_cards", report.hardware.drm_cards, true, 4); + push_json_bool_field( + &mut out, + "rtl8125_present", + report.hardware.rtl8125_present, + false, + 4, + ); + out.push_str(" },\n"); + + out.push_str(" \"integrations\": [\n"); + for (index, integration) in report.integrations.iter().enumerate() { + out.push_str(" {\n"); + push_json_string_field(&mut out, "name", integration.check.name, true, 6); + push_json_string_field(&mut out, "category", integration.check.category, true, 6); + push_json_string_field( + &mut out, "description", - status.component.description, + integration.check.description, true, + 6, ); - push_json_field(&mut output, "category", status.component.category, true); - push_json_field( - &mut output, - "scheme_path", - status.component.scheme_path, + push_json_string_field(&mut out, "state", state_label(integration.state), true, 6); + push_json_optional_bool_field( + &mut out, + "artifact_present", + integration.artifact_present, true, + 6, ); - push_json_field( - &mut output, - "binary_path", - status.component.binary_path, + push_json_optional_bool_field( + &mut out, + "control_present", + integration.control_present, true, + 6, ); - push_json_field(&mut output, "test_hint", status.component.test_hint, true); - output.push_str(" \"dependencies\": "); - push_json_array(&mut output, status.component.dependencies); - output.push_str(",\n"); - output.push_str(&format!( - " \"available\": {},\n", - bool_to_json(status.available) - )); - push_json_field(&mut output, "status", status.status_text, true); - push_json_optional_bool(&mut output, "scheme_exists", status.scheme_exists, true); - push_json_optional_bool(&mut output, "binary_exists", status.binary_exists, false); - output.push_str("\n }"); - - if index + 1 != statuses.len() { - output.push(','); + push_json_string_field(&mut out, "test_hint", integration.check.test_hint, true, 6); + push_json_string_array_field(&mut out, "evidence", &integration.evidence, true, 6); + push_json_string_field(&mut out, "claim_limit", integration.claim_limit, false, 6); + out.push_str(" }"); + if index + 1 != report.integrations.len() { + out.push(','); } - - output.push('\n'); + out.push('\n'); } - - output.push_str(" ]\n"); - output.push('}'); - println!("{output}"); + out.push_str(" ]\n"); + out.push('}'); + println!("{out}"); } fn print_help() { println!("Usage: redbear-info [--verbose|-v] [--json|--test]"); println!(); - println!("Enumerate Red Bear OS custom components and report runtime availability."); + println!("Passive runtime integration report for Red Bear OS."); + println!(); + println!("This tool distinguishes:"); + println!(" present artifact or config exists"); + println!(" active live runtime surface exists"); + println!(" functional read-only runtime probe succeeded"); + println!(); + println!("Connected means the local networking stack has a configured address."); + println!("It does not prove internet reachability."); println!(); println!("Options:"); - println!(" -v, --verbose Show component dependencies"); - println!(" --json Print machine-readable JSON"); - println!(" --test Print runtime test commands for available components"); + println!(" -v, --verbose Show evidence and claim limits"); + println!(" --json Print structured JSON"); + println!(" --test Print suggested diagnostic commands"); println!(" -h, --help Show this help message"); } -fn available_count(statuses: &[ComponentStatus<'_>]) -> usize { - statuses.iter().filter(|status| status.available).count() +fn print_section_header(title: &str) { + println!("{}", colorize(title, BLUE)); } -fn format_dependencies(dependencies: &[&str]) -> String { - if dependencies.is_empty() { - "none".to_string() - } else { - dependencies.join(", ") +fn state_marker(state: ProbeState) -> &'static str { + match state { + ProbeState::Functional => "●", + ProbeState::Active => "◉", + ProbeState::Present => "◌", + ProbeState::Absent => "○", + ProbeState::Unobservable => "?", } } -fn marker_for(status: &ComponentStatus<'_>) -> &'static str { - if status.available { - "●" - } else { - "○" +fn state_label(state: ProbeState) -> &'static str { + match state { + ProbeState::Functional => "functional", + ProbeState::Active => "active", + ProbeState::Present => "present", + ProbeState::Absent => "absent", + ProbeState::Unobservable => "unobservable", } } -fn marker_color(status: &ComponentStatus<'_>) -> &'static str { - match status.state { - AvailabilityState::Available | AvailabilityState::BuiltIn => GREEN, - AvailabilityState::Unavailable => status_color(status), - } -} - -fn status_color(status: &ComponentStatus<'_>) -> &'static str { - match status.state { - AvailabilityState::Available | AvailabilityState::BuiltIn => GREEN, - AvailabilityState::Unavailable if status.status_text == "not running" => YELLOW, - AvailabilityState::Unavailable => RED, +fn state_color(state: ProbeState) -> &'static str { + match state { + ProbeState::Functional => GREEN, + ProbeState::Active => YELLOW, + ProbeState::Present => BLUE, + ProbeState::Absent => RED, + ProbeState::Unobservable => YELLOW, } } @@ -461,65 +900,139 @@ fn colorize(text: &str, color: &str) -> String { format!("{color}{text}{RESET}") } -fn bool_to_json(value: bool) -> &'static str { - if value { - "true" - } else { - "false" - } +fn count_state(items: &[IntegrationStatus<'_>], state: ProbeState) -> usize { + items.iter().filter(|item| item.state == state).count() } -fn push_json_field(output: &mut String, key: &str, value: &str, trailing_comma: bool) { - output.push_str(" "); - push_json_string(output, key); - output.push_str(": "); - push_json_string(output, value); - - if trailing_comma { - output.push(','); - } - - output.push('\n'); +fn display_or_unknown(value: Option<&str>) -> &str { + value.unwrap_or("unknown") } -fn push_json_array(output: &mut String, values: &[&str]) { - output.push('['); - - for (index, value) in values.iter().enumerate() { - if index > 0 { - output.push_str(", "); +fn parse_os_release_value(contents: &str, key: &str) -> Option { + contents.lines().find_map(|line| { + let (found_key, raw) = line.split_once('=')?; + if found_key == key { + Some(raw.trim().trim_matches('"').to_string()) + } else { + None } - push_json_string(output, value); - } - - output.push(']'); + }) } -fn push_json_optional_bool( - output: &mut String, - key: &str, - value: Option, - trailing_comma: bool, -) { - output.push_str(" "); - push_json_string(output, key); - output.push_str(": "); +fn read_trimmed(runtime: &Runtime, path: &str) -> Option { + runtime + .read_to_string(path) + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) +} - match value { - Some(flag) => output.push_str(bool_to_json(flag)), - None => output.push_str("null"), +fn read_bytes(runtime: &Runtime, path: &str) -> Option> { + fs::read(runtime.resolve(path)).ok() +} + +fn parse_default_route(routes: &str) -> Option { + routes.lines().find_map(|line| { + let trimmed = line.trim(); + if trimmed.starts_with("default via ") || trimmed.starts_with("0.0.0.0/0 via ") { + Some(trimmed.to_string()) + } else { + None + } + }) +} + +fn probe_directory_readable( + runtime: &Runtime, + _network: &NetworkReport, + _hardware: &HardwareReport, + check: &IntegrationCheck, +) -> Option { + let path = check.control_path?; + let entries = runtime.read_dir_names(path)?; + Some(format!( + "read-only probe succeeded on {path} ({} entrie(s))", + entries.len() + )) +} + +fn probe_usb_surface( + runtime: &Runtime, + _network: &NetworkReport, + hardware: &HardwareReport, + _check: &IntegrationCheck, +) -> Option { + (hardware.usb_controllers > 0 || runtime.is_dir("/scheme")).then(|| { + format!( + "usb scheme scan sees {} controller(s)", + hardware.usb_controllers + ) + }) +} + +fn probe_netctl_surface( + runtime: &Runtime, + network: &NetworkReport, + _hardware: &HardwareReport, + _check: &IntegrationCheck, +) -> Option { + let profiles = runtime.read_dir_names("/etc/netctl")?; + let profile_count = profiles + .iter() + .filter(|name| *name != "active" && !name.starts_with('.')) + .count(); + Some(match &network.active_profile { + Some(active) => format!( + "{} profile(s) visible, active profile {}", + profile_count, active + ), + None => format!( + "{} profile(s) visible, no active profile recorded", + profile_count + ), + }) +} + +fn probe_smolnetd_surface( + runtime: &Runtime, + network: &NetworkReport, + _hardware: &HardwareReport, + _check: &IntegrationCheck, +) -> Option { + let _ = runtime.read_dir_names("/scheme/netcfg")?; + Some(match &network.address { + Some(address) => format!("netcfg readable, active address {address}"), + None => "netcfg readable, no configured address".to_string(), + }) +} + +fn probe_rtl8125_path( + _runtime: &Runtime, + network: &NetworkReport, + hardware: &HardwareReport, + _check: &IntegrationCheck, +) -> Option { + if !hardware.rtl8125_present { + return None; } - if trailing_comma { - output.push(','); - } - - output.push('\n'); + Some( + if network + .network_schemes + .iter() + .any(|name| name.contains("rtl8125")) + { + "RTL8125 PCI device seen and network.rtl8125 scheme visible".to_string() + } else if network.connected { + "RTL8125 PCI device seen and native network stack reports a configured address" + .to_string() + } else { + "RTL8125 PCI device seen through /scheme/pci".to_string() + }, + ) } fn push_json_string(output: &mut String, value: &str) { output.push('"'); - for ch in value.chars() { match ch { '"' => output.push_str("\\\""), @@ -530,6 +1043,266 @@ fn push_json_string(output: &mut String, value: &str) { _ => output.push(ch), } } - output.push('"'); } + +fn push_json_indent(output: &mut String, indent: usize) { + for _ in 0..indent { + output.push(' '); + } +} + +fn push_json_string_field( + output: &mut String, + key: &str, + value: &str, + trailing_comma: bool, + indent: usize, +) { + push_json_indent(output, indent); + push_json_string(output, key); + output.push_str(": "); + push_json_string(output, value); + if trailing_comma { + output.push(','); + } + output.push('\n'); +} + +fn push_json_field( + output: &mut String, + key: &str, + value: Option<&str>, + trailing_comma: bool, + indent: usize, +) { + push_json_indent(output, indent); + push_json_string(output, key); + output.push_str(": "); + match value { + Some(value) => push_json_string(output, value), + None => output.push_str("null"), + } + if trailing_comma { + output.push(','); + } + output.push('\n'); +} + +fn push_json_bool_field( + output: &mut String, + key: &str, + value: bool, + trailing_comma: bool, + indent: usize, +) { + push_json_indent(output, indent); + push_json_string(output, key); + output.push_str(": "); + output.push_str(if value { "true" } else { "false" }); + if trailing_comma { + output.push(','); + } + output.push('\n'); +} + +fn push_json_optional_bool_field( + output: &mut String, + key: &str, + value: Option, + trailing_comma: bool, + indent: usize, +) { + push_json_indent(output, indent); + push_json_string(output, key); + output.push_str(": "); + match value { + Some(value) => output.push_str(if value { "true" } else { "false" }), + None => output.push_str("null"), + } + if trailing_comma { + output.push(','); + } + output.push('\n'); +} + +fn push_json_number_field( + output: &mut String, + key: &str, + value: usize, + trailing_comma: bool, + indent: usize, +) { + push_json_indent(output, indent); + push_json_string(output, key); + output.push_str(": "); + output.push_str(&value.to_string()); + if trailing_comma { + output.push(','); + } + output.push('\n'); +} + +fn push_json_string_array_field( + output: &mut String, + key: &str, + values: &[String], + trailing_comma: bool, + indent: usize, +) { + push_json_indent(output, indent); + push_json_string(output, key); + output.push_str(": ["); + for (index, value) in values.iter().enumerate() { + if index > 0 { + output.push_str(", "); + } + push_json_string(output, value); + } + output.push(']'); + if trailing_comma { + output.push(','); + } + output.push('\n'); +} + +#[cfg(test)] +mod tests { + use super::*; + + fn temp_root() -> PathBuf { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = env::temp_dir().join(format!("redbear-info-test-{}-{}", process::id(), unique)); + fs::create_dir_all(&path).unwrap(); + path + } + + fn write_file(root: &Path, path: &str, content: &str) { + let full = root.join(path.trim_start_matches('/')); + if let Some(parent) = full.parent() { + fs::create_dir_all(parent).unwrap(); + } + fs::write(full, content).unwrap(); + } + + fn create_dir(root: &Path, path: &str) { + fs::create_dir_all(root.join(path.trim_start_matches('/'))).unwrap(); + } + + #[test] + fn network_report_uses_live_netcfg_surfaces() { + let root = temp_root(); + write_file( + &root, + "/usr/lib/os-release", + "PRETTY_NAME=\"Red Bear OS\"\nVERSION_ID=\"0.1.0\"\n", + ); + write_file(&root, "/etc/hostname", "redbear\n"); + create_dir(&root, "/scheme/netcfg/ifaces/eth0/addr"); + write_file( + &root, + "/scheme/netcfg/ifaces/eth0/addr/list", + "192.168.10.20/24\n", + ); + write_file( + &root, + "/scheme/netcfg/ifaces/eth0/mac", + "02:00:00:00:00:01\n", + ); + write_file(&root, "/scheme/netcfg/resolv/nameserver", "1.1.1.1\n"); + write_file( + &root, + "/scheme/netcfg/route/list", + "default via 192.168.10.1\n", + ); + create_dir(&root, "/scheme/network.eth0_rtl8125"); + create_dir(&root, "/etc/netctl"); + write_file(&root, "/etc/netctl/active", "wired-static\n"); + + let report = collect_report(&Runtime::from_root(root.clone())); + assert_eq!(report.network.state, ProbeState::Functional); + assert!(report.network.connected); + assert_eq!(report.network.address.as_deref(), Some("192.168.10.20/24")); + assert_eq!(report.network.dns.as_deref(), Some("1.1.1.1")); + assert_eq!( + report.network.default_route.as_deref(), + Some("default via 192.168.10.1") + ); + assert_eq!( + report.network.active_profile.as_deref(), + Some("wired-static") + ); + assert!(report + .network + .network_schemes + .iter() + .any(|name| name.contains("rtl8125"))); + + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn rtl8125_hardware_detection_parses_pci_config() { + let root = temp_root(); + create_dir(&root, "/scheme/pci/0000--02--00.0"); + let config = [ + (RTL8125_VENDOR_ID & 0xff) as u8, + (RTL8125_VENDOR_ID >> 8) as u8, + (RTL8125_DEVICE_ID & 0xff) as u8, + (RTL8125_DEVICE_ID >> 8) as u8, + 0, + 0, + 0, + 0, + ]; + let path = root.join("scheme/pci/0000--02--00.0/config"); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).unwrap(); + } + fs::write(&path, config).unwrap(); + + let network = collect_network(&Runtime::from_root(root.clone())); + let hardware = collect_hardware(&Runtime::from_root(root.clone()), &network); + assert!(hardware.rtl8125_present); + + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn json_output_contains_network_and_integration_state() { + let root = temp_root(); + write_file( + &root, + "/usr/lib/os-release", + "PRETTY_NAME=\"Red Bear OS\"\nVERSION_ID=\"0.1.0\"\n", + ); + write_file(&root, "/usr/bin/redbear-info", ""); + create_dir(&root, "/scheme/netcfg"); + write_file( + &root, + "/scheme/netcfg/ifaces/eth0/addr/list", + "Not configured\n", + ); + + let report = collect_report(&Runtime::from_root(root.clone())); + let mut output = String::new(); + output.push_str("{"); + push_json_string_field( + &mut output, + "state", + state_label(report.network.state), + false, + 0, + ); + assert!(output.contains("state")); + assert!(report + .integrations + .iter() + .any(|item| item.check.name == "redbear-info")); + + fs::remove_dir_all(root).unwrap(); + } +} diff --git a/local/recipes/system/redbear-meta/recipe.toml b/local/recipes/system/redbear-meta/recipe.toml index 443a9f7f..3093e44d 100644 --- a/local/recipes/system/redbear-meta/recipe.toml +++ b/local/recipes/system/redbear-meta/recipe.toml @@ -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", diff --git a/local/recipes/system/redbear-netctl/recipe.toml b/local/recipes/system/redbear-netctl/recipe.toml new file mode 100644 index 00000000..4e47e6bb --- /dev/null +++ b/local/recipes/system/redbear-netctl/recipe.toml @@ -0,0 +1,5 @@ +[source] +path = "source" + +[build] +template = "cargo" diff --git a/local/recipes/system/redbear-netctl/source/Cargo.toml b/local/recipes/system/redbear-netctl/source/Cargo.toml new file mode 100644 index 00000000..ba6c3f54 --- /dev/null +++ b/local/recipes/system/redbear-netctl/source/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "redbear-netctl" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "netctl" +path = "src/main.rs" diff --git a/local/recipes/system/redbear-netctl/source/src/main.rs b/local/recipes/system/redbear-netctl/source/src/main.rs new file mode 100644 index 00000000..d08017b8 --- /dev/null +++ b/local/recipes/system/redbear-netctl/source/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 |stop |enable |disable [profile]|is-enabled [profile]]"; + +#[derive(Clone, Debug)] +enum ProfileIpMode { + Dhcp, + Static { + address: String, + gateway: Option, + dns: Option, + }, +} + +#[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) -> Result { + 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 { + 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, 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, 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 { + 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 { + 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 { + 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) + } +} diff --git a/local/recipes/system/udev-shim/recipe.toml b/local/recipes/system/udev-shim/recipe.toml index a41d788d..23cfb918 100644 --- a/local/recipes/system/udev-shim/recipe.toml +++ b/local/recipes/system/udev-shim/recipe.toml @@ -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" diff --git a/local/recipes/system/udev-shim/source/Cargo.toml b/local/recipes/system/udev-shim/source/Cargo.toml index 7822505f..399533ba 100644 --- a/local/recipes/system/udev-shim/source/Cargo.toml +++ b/local/recipes/system/udev-shim/source/Cargo.toml @@ -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" diff --git a/local/recipes/system/udev-shim/source/src/device_db.rs b/local/recipes/system/udev-shim/source/src/device_db.rs index abec5b9a..c5a0abaa 100644 --- a/local/recipes/system/udev-shim/source/src/device_db.rs +++ b/local/recipes/system/udev-shim/source/src/device_db.rs @@ -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, pub name: String, - pub path: String, + pub devpath: String, + pub devnode: String, + pub scheme_target: String, + pub symlinks: Vec, +} + +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, + scheme_target: impl Into, + symlinks: Vec, + ) { + 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 { + 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, +) -> 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 } diff --git a/local/recipes/system/udev-shim/source/src/main.rs b/local/recipes/system/udev-shim/source/src/main.rs index 3f633d9e..b448a061 100644 --- a/local/recipes/system/udev-shim/source/src/main.rs +++ b/local/recipes/system/udev-shim/source/src/main.rs @@ -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); } diff --git a/local/recipes/system/udev-shim/source/src/scheme.rs b/local/recipes/system/udev-shim/source/src/scheme.rs index ae52b43f..4d86819c 100644 --- a/local/recipes/system/udev-shim/source/src/scheme.rs +++ b/local/recipes/system/udev-shim/source/src/scheme.rs @@ -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, + handles: BTreeMap, devices: Vec, } 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 { - 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 = 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 = 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 { + 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 { + 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 { + 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 { + 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 { + 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(&self, entries: I) -> String + where + I: IntoIterator, + { + 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 { + 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 = 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 { + 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 { + 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::().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 { + 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> { - 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 { + Ok(SCHEME_ROOT_ID) } - fn read(&mut self, id: usize, buf: &mut [u8]) -> Result> { - 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 { + 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 { + 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> { - 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 { + let _kind = self.kind_for_id(id)?; Err(Error::new(EROFS)) } - fn seek(&mut self, id: usize, pos: isize, whence: usize) -> Result> { - 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 { + 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 { + let _kind = self.kind_for_id(id)?; + Ok(0) + } + + fn fsize(&mut self, id: usize, _ctx: &CallerCtx) -> Result { + 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 { + 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::().ok()?; + let dev = parts.next()?.parse::().ok()?; + let func = parts.next()?.parse::().ok()?; + if parts.next().is_some() { + return None; + } + Some((bus, dev, func)) +} + +fn basename(path: &str) -> Option { + 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> { - 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> { - let _ = self.handles.get(&id).ok_or(Error::new(EBADF))?; - Ok(Some(EventFlags::empty())) - } - - fn close(&mut self, id: usize) -> Result> { - self.handles.remove(&id); - Ok(Some(0)) } } diff --git a/local/scripts/apply-patches.sh b/local/scripts/apply-patches.sh index 2e9e7336..11c70135 100755 --- a/local/scripts/apply-patches.sh +++ b/local/scripts/apply-patches.sh @@ -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" diff --git a/local/scripts/integrate-redbear.sh b/local/scripts/integrate-redbear.sh index de8cbc82..0e774eb3 100755 --- a/local/scripts/integrate-redbear.sh +++ b/local/scripts/integrate-redbear.sh @@ -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" diff --git a/local/scripts/test-iommu-qemu.sh b/local/scripts/test-iommu-qemu.sh new file mode 100755 index 00000000..48d71140 --- /dev/null +++ b/local/scripts/test-iommu-qemu.sh @@ -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" diff --git a/recipes/core/base-initfs/recipe.toml b/recipes/core/base-initfs/recipe.toml index eb1353c4..237dfe0c 100644 --- a/recipes/core/base-initfs/recipe.toml +++ b/recipes/core/base-initfs/recipe.toml @@ -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" diff --git a/recipes/core/base/recipe.toml b/recipes/core/base/recipe.toml index da6805d9..2b895506 100644 --- a/recipes/core/base/recipe.toml +++ b/recipes/core/base/recipe.toml @@ -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/" """ diff --git a/recipes/core/installer/recipe.toml b/recipes/core/installer/recipe.toml index 02353821..ecde0315 100644 --- a/recipes/core/installer/recipe.toml +++ b/recipes/core/installer/recipe.toml @@ -1,5 +1,6 @@ [source] git = "https://gitlab.redox-os.org/redox-os/installer.git" +patches = ["redox.patch"] [build] template = "cargo" diff --git a/recipes/core/kernel/recipe.toml b/recipes/core/kernel/recipe.toml index 15cbc83b..1c3f9817 100644 --- a/recipes/core/kernel/recipe.toml +++ b/recipes/core/kernel/recipe.toml @@ -1,5 +1,6 @@ [source] git = "https://gitlab.redox-os.org/redox-os/kernel.git" +patches = ["redox.patch"] [build] template = "custom" diff --git a/recipes/shells/bash/recipe.toml b/recipes/shells/bash/recipe.toml index fd0fdca4..e15d4e52 100644 --- a/recipes/shells/bash/recipe.toml +++ b/recipes/shells/bash/recipe.toml @@ -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" diff --git a/recipes/system/iommu b/recipes/system/iommu new file mode 120000 index 00000000..6c243dd8 --- /dev/null +++ b/recipes/system/iommu @@ -0,0 +1 @@ +../../local/recipes/system/iommu \ No newline at end of file diff --git a/recipes/system/rbos-info b/recipes/system/rbos-info new file mode 120000 index 00000000..8b7e25f6 --- /dev/null +++ b/recipes/system/rbos-info @@ -0,0 +1 @@ +../../local/recipes/system/rbos-info \ No newline at end of file diff --git a/recipes/system/redbear-hwutils b/recipes/system/redbear-hwutils new file mode 120000 index 00000000..61cb40aa --- /dev/null +++ b/recipes/system/redbear-hwutils @@ -0,0 +1 @@ +../../local/recipes/system/redbear-hwutils \ No newline at end of file diff --git a/recipes/system/redbear-netctl b/recipes/system/redbear-netctl new file mode 120000 index 00000000..c533290e --- /dev/null +++ b/recipes/system/redbear-netctl @@ -0,0 +1 @@ +../../local/recipes/system/redbear-netctl \ No newline at end of file diff --git a/scripts/build-iso.sh b/scripts/build-iso.sh new file mode 100755 index 00000000..498e6e3c --- /dev/null +++ b/scripts/build-iso.sh @@ -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" diff --git a/scripts/fetch-all-sources.sh b/scripts/fetch-all-sources.sh index 0d2e51c8..1df1ed9b 100755 --- a/scripts/fetch-all-sources.sh +++ b/scripts/fetch-all-sources.sh @@ -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///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 diff --git a/scripts/run.sh b/scripts/run.sh new file mode 100755 index 00000000..8e1bde9c --- /dev/null +++ b/scripts/run.sh @@ -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 < 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[@]}"