driver-manager: QEMU gate passed — thread::scope hang fix, initfs scheme skip, graphics split

Runtime validation of the cutover in QEMU (q35, e1000 + AHCI):

- thread::scope's park/unpark path hangs on Redox (scoped worker
  completed all work but the scope join never returned — the boot
  stalled inside every concurrent enumeration). The concurrent probe
  pool now uses plain thread::spawn + JoinHandle::join (all captures
  were already owned/Arc); the counting semaphore is Arc-based so
  guards are 'static. bound map is Mutex (RwLock write was unproven
  on target).
- initfs driver-manager no longer registers scheme:driver-manager —
  the transient initfs manager's registration survived into the
  rootfs phase and made the resident manager's registration fail
  EEXIST (which then exit(1)'d the rootfs manager).
- config: matchless [[driver]] entries now parse (serde default) —
  70-usb-class.toml's USB-class drivers (no [[driver.match]]) broke
  config loading entirely on the first gate. Includes a regression
  test for load_all with matchless entries.
- graphics: 30-graphics.toml moves from the shared
  redbear-device-services.toml to redbear-full.toml (redox-drm is a
  full-only driver; mini no longer defers it every hotplug cycle).
  vesad removed from drivers.d — it is an init-managed service, not
  a spawnable driver.

Gate evidence: initfs 'bound: 0000--00--1f.2 -> ahcid', switchroot,
rootfs 'bound: 0000--00--02.0 -> e1000d' with ZERO deferred,
scheme:driver-manager registered, resident hotplug loop (250ms),
pcid-spawner dormant on both phases.
This commit is contained in:
2026-07-24 05:38:16 +09:00
parent 431aa55e38
commit 78665ede70
6 changed files with 116 additions and 59 deletions
-21
View File
@@ -479,27 +479,6 @@ data = """
pattern = "i915/adlp_dmc_ver2_16.bin"
chain = ["i915/adlp_dmc_ver2_14.bin", "i915/adlp_dmc_ver2_12.bin"]
"""
[[files]]
path = "/lib/drivers.d/30-graphics.toml"
data = """
[[driver]]
name = "vesad"
description = "VESA BIOS display driver"
priority = 60
command = ["/usr/lib/drivers/vesad"]
[[driver.match]]
class = 0x03
[[driver]]
name = "redox-drm"
description = "DRM/KMS display driver (AMD + Intel)"
priority = 60
command = ["/usr/bin/redox-drm"]
[[driver.match]]
class = 0x03
"""
# Firmware fallback chain configs
[[files]]
+45
View File
@@ -587,6 +587,51 @@ members = ["greeter"]
gid = 100
members = ["messagebus"]
[[files]]
path = "/lib/drivers.d/30-graphics.toml"
data = """
[[driver]]
name = "redox-drm"
description = "DRM/KMS display driver (AMD + Intel + VirtIO)"
priority = 60
command = ["/usr/bin/redox-drm"]
[[driver.match]]
class = 0x03
[[driver]]
name = "virtio-gpud"
description = "VirtIO GPU driver"
priority = 60
command = ["/usr/lib/drivers/virtio-gpud"]
[[driver.match]]
vendor = 0x1AF4
class = 0x03
[[driver]]
name = "redox-drm"
description = "Intel GPU display driver"
priority = 61
command = ["/usr/bin/redox-drm"]
[[driver.match]]
vendor = 0x8086
class = 0x03
subclass = 0x00
[[driver]]
name = "redox-drm"
description = "AMD GPU display driver"
priority = 61
command = ["/usr/bin/redox-drm"]
[[driver.match]]
vendor = 0x1002
class = 0x03
subclass = 0x00
"""
[[files]]
path = "/etc/pcid.d/ihdgd.toml"
data = """
+2 -9
View File
@@ -1,13 +1,6 @@
# Graphics and display drivers
[[driver]]
name = "vesad"
description = "VESA BIOS display driver"
priority = 60
command = ["/usr/lib/drivers/vesad"]
[[driver.match]]
class = 0x03
# vesad runs as an init-managed service from initfs (framebuffer console) and
# has no rootfs binary, so it is not spawnable here and must not be listed.
[[driver]]
name = "redox-drm"
@@ -5,7 +5,7 @@
extern crate alloc;
use std::sync::{mpsc, Arc, Condvar, Mutex, RwLock};
use std::sync::{mpsc, Arc, Condvar, Mutex};
use std::thread;
use crate::device::{BoundDevice, DeviceId, DeviceInfo};
@@ -31,21 +31,21 @@ impl CountingSemaphore {
}
}
fn acquire(&self) -> SemaphoreGuard<'_> {
fn acquire(self: &Arc<Self>) -> SemaphoreGuard {
let mut count = self.inner.lock().unwrap_or_else(|e| e.into_inner());
while *count == 0 {
count = self.cvar.wait(count).unwrap_or_else(|e| e.into_inner());
}
*count -= 1;
SemaphoreGuard { sem: self }
SemaphoreGuard { sem: self.clone() }
}
}
struct SemaphoreGuard<'a> {
sem: &'a CountingSemaphore,
struct SemaphoreGuard {
sem: Arc<CountingSemaphore>,
}
impl<'a> Drop for SemaphoreGuard<'a> {
impl Drop for SemaphoreGuard {
fn drop(&mut self) {
if let Ok(mut count) = self.sem.inner.lock() {
*count += 1;
@@ -64,7 +64,7 @@ pub struct ConcurrentDeviceManager {
/// Pre-events emitted in order before any probe result.
pre_events: Vec<ProbeEvent>,
/// Bound-device map shared with workers.
bound: Arc<RwLock<BTreeMap<DeviceId, BoundDevice>>>,
bound: Arc<Mutex<BTreeMap<DeviceId, BoundDevice>>>,
/// Deferred queue shared with workers.
deferred: Arc<Mutex<Vec<(DeviceInfo, String)>>>,
}
@@ -132,7 +132,7 @@ impl ConcurrentDeviceManager {
Self {
jobs,
pre_events,
bound: Arc::new(RwLock::new(mgr.bound_devices_snapshot())),
bound: Arc::new(Mutex::new(mgr.bound_devices_snapshot())),
deferred: Arc::new(Mutex::new(mgr.deferred_queue_snapshot())),
}
}
@@ -144,7 +144,7 @@ impl ConcurrentDeviceManager {
/// Snapshot of the bound map (post-probe).
pub fn bound_snapshot(&self) -> BTreeMap<DeviceId, BoundDevice> {
self.bound.read().map(|m| m.clone()).unwrap_or_default()
self.bound.lock().map(|m| m.clone()).unwrap_or_default()
}
/// Snapshot of the deferred queue.
@@ -177,21 +177,26 @@ impl ConcurrentDeviceManager {
.map(|_| semaphore.acquire())
.collect();
thread::scope(|s| {
for ((device_id, info, candidates), permit) in
self.jobs.iter().cloned().zip(permits.drain(..))
{
let tx = tx.clone();
let bound = Arc::clone(&bound);
let deferred = Arc::clone(&deferred);
s.spawn(move || {
let _permit = permit;
for outcome in run_probe(bound, deferred, device_id, info, candidates) {
let _ = tx.send(outcome);
}
});
}
});
// Plain spawn + join: every capture is owned (Arc/moved), so
// scoped threads are unnecessary, and JoinHandle::join does not
// depend on the park/unpark path that thread::scope relies on.
let mut handles = Vec::new();
for ((device_id, info, candidates), permit) in
self.jobs.iter().cloned().zip(permits.drain(..))
{
let tx = tx.clone();
let bound = Arc::clone(&bound);
let deferred = Arc::clone(&deferred);
handles.push(thread::spawn(move || {
let _permit = permit;
for outcome in run_probe(bound, deferred, device_id, info, candidates) {
let _ = tx.send(outcome);
}
}));
}
for handle in handles {
let _ = handle.join();
}
drop(tx);
while let Ok(event) = rx.try_recv() {
@@ -202,7 +207,7 @@ impl ConcurrentDeviceManager {
}
fn run_probe(
bound: Arc<RwLock<BTreeMap<DeviceId, BoundDevice>>>,
bound: Arc<Mutex<BTreeMap<DeviceId, BoundDevice>>>,
deferred: Arc<Mutex<Vec<(DeviceInfo, String)>>>,
device_id: DeviceId,
info: DeviceInfo,
@@ -214,7 +219,7 @@ fn run_probe(
let result = driver.probe(&info);
match &result {
ProbeResult::Bound => {
if let Ok(mut map) = bound.write() {
if let Ok(mut map) = bound.lock() {
map.insert(
device_id.clone(),
BoundDevice {
@@ -561,7 +566,7 @@ mod tests {
#[test]
fn semaphore_releases_on_drop() {
let sem = CountingSemaphore::new(1);
let sem = Arc::new(CountingSemaphore::new(1));
{
let _p = sem.acquire();
}
@@ -70,7 +70,7 @@ struct RawDriverMatch {
#[cfg(test)]
mod tests {
use super::{is_process_alive, signal_then_collect};
use super::{is_process_alive, signal_then_collect, DriverConfig};
use std::collections::BTreeMap;
use std::sync::Mutex;
@@ -81,6 +81,38 @@ mod tests {
body()
}
#[test]
fn load_all_accepts_matchless_driver_entries() {
serialized(|| {
let dir = std::env::temp_dir().join(format!(
"rb-dm-matchless-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("70-usb-class.toml"),
r#"
[[driver]]
name = "redbear-acmd"
description = "USB CDC ACM serial driver"
priority = 70
command = ["/usr/bin/redbear-acmd"]
"#,
)
.unwrap();
let configs = DriverConfig::load_all(dir.to_str().unwrap())
.expect("matchless driver entries must load");
assert_eq!(configs.len(), 1);
assert_eq!(configs[0].name, "redbear-acmd");
assert!(configs[0].matches.is_empty());
let _ = std::fs::remove_dir_all(&dir);
});
}
#[test]
fn is_process_alive_returns_false_for_unused_pid() {
let dead = u32::MAX;
@@ -885,6 +917,7 @@ struct RawDriverEntry {
#[serde(default)]
command: Vec<String>,
#[serde(rename = "match")]
#[serde(default)]
r#match: Vec<RawDriverMatch>,
#[serde(default)]
depends_on: Vec<String>,
@@ -452,7 +452,9 @@ fn main() {
log::info!("enum complete: {} bound, {} deferred", bound, deferred);
}
if let Err(err) = scheme::start_scheme_server(Arc::clone(&scheme)) {
if initfs {
log::info!("initfs mode: skipping scheme registration (transient)");
} else if let Err(err) = scheme::start_scheme_server(Arc::clone(&scheme)) {
log::error!("{err}");
process::exit(1);
}