Files
RedBear-OS/local/patches/local/P3-firmware-fallback-cache.patch
T
vasilito 34360e1e4f feat: P0-P6 kernel scheduler + relibc threading comprehensive implementation
P0-P2: Barrier SMP, sigmask/pthread_kill races, robust mutexes, RT scheduling, POSIX sched API
P3: PerCpuSched struct, per-CPU wiring, work stealing, load balancing, initial placement
P4: 64-shard futex table, REQUEUE, PI futexes (LOCK_PI/UNLOCK_PI/TRYLOCK_PI), robust futexes, vruntime tracking, min-vruntime SCHED_OTHER selection
P5: setpriority/getpriority, pthread_setaffinity_np, pthread_setname_np, pthread_setschedparam (Redox)
P6: Cache-affine scheduling (last_cpu + vruntime bonus), NUMA topology kernel hints + numad userspace daemon

Stability fixes: make_consistent stores 0 (dead TID fix), cond.rs error propagation, SPIN_COUNT adaptive spinning, Sys::open &str fix, PI futex CAS race, proc.rs lock ordering, barrier destroy

Patches: 33 kernel + 58 relibc patches, all tracked in recipes
Docs: KERNEL-SCHEDULER-MULTITHREAD-IMPROVEMENT-PLAN.md updated, SCHEDULER-REVIEW-FINAL.md created
Architecture: NUMA topology parsing stays userspace (numad daemon), kernel stores lightweight NumaTopology hints
2026-04-30 18:21:48 +01:00

1415 lines
46 KiB
Diff

diff --git a/local/recipes/system/firmware-loader/source/Cargo.toml b/local/recipes/system/firmware-loader/source/Cargo.toml
index 0e273efcd..6f91edb3a 100644
--- a/local/recipes/system/firmware-loader/source/Cargo.toml
+++ b/local/recipes/system/firmware-loader/source/Cargo.toml
@@ -10,3 +10,4 @@ redox_scheme = { package = "redox-scheme", version = "0.11" }
libredox = "0.1"
log = { version = "0.4", features = ["std"] }
thiserror = "2"
+toml = "0.8"
diff --git a/local/recipes/system/firmware-loader/source/src/blob.rs b/local/recipes/system/firmware-loader/source/src/blob.rs
index 911b6e5b0..349d29657 100644
--- a/local/recipes/system/firmware-loader/source/src/blob.rs
+++ b/local/recipes/system/firmware-loader/source/src/blob.rs
@@ -1,11 +1,18 @@
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
+use std::ffi::OsStr;
use std::fs;
-use std::path::{Path, PathBuf};
+use std::io::ErrorKind;
+use std::path::{Component, Path, PathBuf};
+use std::sync::mpsc::{self, RecvTimeoutError};
use std::sync::{Arc, Mutex};
+use std::time::{Duration, Instant, UNIX_EPOCH};
use log::{info, warn};
use thiserror::Error;
+const DEFAULT_FALLBACKS_DIR: &str = "/etc/firmware-fallbacks.d";
+const DEFAULT_CACHE_DIR: &str = "/var/lib/firmware/cache";
+
#[allow(dead_code)]
#[derive(Error, Debug)]
pub enum BlobError {
@@ -21,6 +28,8 @@ pub enum BlobError {
#[source]
source: std::io::Error,
},
+ #[error("firmware load timed out for {key} after {timeout:?}")]
+ LoadTimeout { key: String, timeout: Duration },
}
#[allow(dead_code)]
@@ -30,20 +39,365 @@ pub struct FirmwareBlob {
pub path: PathBuf,
}
+#[derive(Clone, Debug, Eq, PartialEq)]
+struct CacheMetadata {
+ requested_key: String,
+ source_key: String,
+ source_mtime_ns: u128,
+ source_len: u64,
+}
+
+impl CacheMetadata {
+ #[allow(dead_code)]
+ fn placeholder(key: &str, len: u64) -> Self {
+ Self {
+ requested_key: key.to_string(),
+ source_key: key.to_string(),
+ source_mtime_ns: 0,
+ source_len: len,
+ }
+ }
+
+ fn from_source(requested_key: &str, source_key: &str, signature: &SourceSignature) -> Self {
+ Self {
+ requested_key: requested_key.to_string(),
+ source_key: source_key.to_string(),
+ source_mtime_ns: signature.modified_ns,
+ source_len: signature.len,
+ }
+ }
+
+ fn matches(&self, requested_key: &str, source_key: &str, signature: &SourceSignature) -> bool {
+ self.requested_key == requested_key
+ && self.source_key == source_key
+ && self.source_mtime_ns == signature.modified_ns
+ && self.source_len == signature.len
+ }
+}
+
+#[derive(Clone)]
+struct CachedBlob {
+ data: Arc<Vec<u8>>,
+ metadata: CacheMetadata,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+struct SourceSignature {
+ modified_ns: u128,
+ len: u64,
+}
+
+pub struct FirmwareFallback {
+ fallbacks: HashMap<String, Vec<String>>,
+}
+
+impl FirmwareFallback {
+ pub fn load_defaults() -> Self {
+ Self::load_from_dir(Path::new(DEFAULT_FALLBACKS_DIR))
+ }
+
+ fn load_from_dir(dir: &Path) -> Self {
+ let mut fallbacks = Self::builtins();
+
+ let entries = match fs::read_dir(dir) {
+ Ok(entries) => entries,
+ Err(err) if err.kind() == ErrorKind::NotFound => return fallbacks,
+ Err(err) => {
+ warn!(
+ "firmware-loader: failed to read fallback directory {}: {}",
+ dir.display(),
+ err
+ );
+ return fallbacks;
+ }
+ };
+
+ let mut paths = Vec::new();
+ for entry in entries {
+ match entry {
+ Ok(entry) => {
+ let path = entry.path();
+ if path.extension() == Some(OsStr::new("toml")) {
+ paths.push(path);
+ }
+ }
+ Err(err) => warn!(
+ "firmware-loader: skipping unreadable fallback entry in {}: {}",
+ dir.display(),
+ err
+ ),
+ }
+ }
+ paths.sort();
+
+ for path in paths {
+ let contents = match fs::read_to_string(&path) {
+ Ok(contents) => contents,
+ Err(err) => {
+ warn!(
+ "firmware-loader: failed to read fallback file {}: {}",
+ path.display(),
+ err
+ );
+ continue;
+ }
+ };
+
+ match parse_fallback_file(&contents) {
+ Ok(loaded) => {
+ for (pattern, variants) in loaded {
+ if variants.is_empty() {
+ continue;
+ }
+ fallbacks
+ .fallbacks
+ .entry(pattern)
+ .or_default()
+ .extend(variants);
+ }
+ }
+ Err(err) => warn!(
+ "firmware-loader: failed to parse fallback file {}: {}",
+ path.display(),
+ err
+ ),
+ }
+ }
+
+ fallbacks
+ }
+
+ pub fn get_fallback_chain(&self, key: &str) -> Vec<String> {
+ let mut chain = Vec::new();
+ let mut seen = HashSet::new();
+
+ if let Some(exact) = self.fallbacks.get(key) {
+ append_variants(key, "", exact, &mut seen, &mut chain);
+ }
+
+ let mut patterns: Vec<&str> = self.fallbacks.keys().map(String::as_str).collect();
+ patterns.sort_unstable();
+
+ for pattern in patterns {
+ if pattern == key {
+ continue;
+ }
+
+ if let Some(capture) = pattern_capture(pattern, key) {
+ if let Some(variants) = self.fallbacks.get(pattern) {
+ append_variants(key, capture, variants, &mut seen, &mut chain);
+ }
+ }
+ }
+
+ chain
+ }
+
+ fn builtins() -> Self {
+ let mut fallbacks = HashMap::new();
+ fallbacks.insert(
+ "amdgpu/dmcub_dcn31.bin".to_string(),
+ vec![
+ "amdgpu/dmcub_dcn30.bin".to_string(),
+ "amdgpu/dmcub_dcn20.bin".to_string(),
+ ],
+ );
+ fallbacks.insert(
+ "amdgpu/dmcub_dcn30.bin".to_string(),
+ vec!["amdgpu/dmcub_dcn20.bin".to_string()],
+ );
+ fallbacks.insert(
+ "iwlwifi-*-92.ucode".to_string(),
+ vec![
+ "iwlwifi-*-83.ucode".to_string(),
+ "iwlwifi-*-77.ucode".to_string(),
+ ],
+ );
+ fallbacks.insert(
+ "iwlwifi-*-83.ucode".to_string(),
+ vec!["iwlwifi-*-77.ucode".to_string()],
+ );
+
+ Self { fallbacks }
+ }
+}
+
+pub struct FirmwareCache {
+ cache_dir: PathBuf,
+}
+
+impl FirmwareCache {
+ pub fn new(cache_dir: &Path) -> Self {
+ Self {
+ cache_dir: cache_dir.to_path_buf(),
+ }
+ }
+
+ #[allow(dead_code)]
+ pub fn get(&self, key: &str) -> Option<Vec<u8>> {
+ self.load_entry(key, None, None)
+ .ok()
+ .flatten()
+ .map(|entry| entry.data.as_ref().clone())
+ }
+
+ #[allow(dead_code)]
+ pub fn store(&self, key: &str, data: &[u8]) -> Result<(), std::io::Error> {
+ self.store_entry(
+ key,
+ data,
+ &CacheMetadata::placeholder(key, data.len() as u64),
+ )
+ }
+
+ pub fn invalidate(&self, key: &str) {
+ let Some(path) = self.cache_path(key) else {
+ return;
+ };
+
+ for cache_file in [path.clone(), metadata_path_for(&path)] {
+ match fs::remove_file(&cache_file) {
+ Ok(()) => {}
+ Err(err) if err.kind() == ErrorKind::NotFound => {}
+ Err(err) => warn!(
+ "firmware-loader: failed to invalidate persistent cache {}: {}",
+ cache_file.display(),
+ err
+ ),
+ }
+ }
+ }
+
+ fn contains(&self, key: &str) -> bool {
+ self.cache_path(key).is_some_and(|path| path.exists())
+ }
+
+ fn load_entry(
+ &self,
+ key: &str,
+ started_at: Option<Instant>,
+ timeout: Option<Duration>,
+ ) -> Result<Option<CachedBlob>, BlobError> {
+ let Some(path) = self.cache_path(key) else {
+ warn!(
+ "firmware-loader: refusing to read invalid persistent cache key {}",
+ key
+ );
+ return Ok(None);
+ };
+
+ let metadata_path = metadata_path_for(&path);
+ if !path.exists() {
+ return Ok(None);
+ }
+
+ let metadata = match load_cache_metadata(&metadata_path, key, started_at, timeout) {
+ Ok(metadata) => {
+ let Some(metadata) = metadata else {
+ self.invalidate(key);
+ return Ok(None);
+ };
+ metadata
+ }
+ Err(BlobError::LoadTimeout { .. }) => {
+ return Err(BlobError::LoadTimeout {
+ key: key.to_string(),
+ timeout: timeout.unwrap_or_default(),
+ })
+ }
+ Err(err) => {
+ warn!(
+ "firmware-loader: failed to load metadata for persistent cache {}: {}",
+ metadata_path.display(),
+ err
+ );
+ self.invalidate(key);
+ return Ok(None);
+ }
+ };
+
+ match read_path_bytes(&path, key, started_at, timeout) {
+ Ok(data) => Ok(Some(CachedBlob {
+ data: Arc::new(data),
+ metadata,
+ })),
+ Err(BlobError::ReadError { .. }) => {
+ warn!(
+ "firmware-loader: failed to read persistent cache {}, invalidating entry",
+ path.display()
+ );
+ self.invalidate(key);
+ Ok(None)
+ }
+ Err(err) => Err(err),
+ }
+ }
+
+ fn store_entry(
+ &self,
+ key: &str,
+ data: &[u8],
+ metadata: &CacheMetadata,
+ ) -> Result<(), std::io::Error> {
+ let path = self.cache_path(key).ok_or_else(|| {
+ std::io::Error::new(
+ ErrorKind::InvalidInput,
+ format!("invalid cache key for persistent firmware cache: {key}"),
+ )
+ })?;
+ let metadata_path = metadata_path_for(&path);
+
+ if let Some(parent) = path.parent() {
+ fs::create_dir_all(parent)?;
+ }
+
+ fs::write(&path, data)?;
+ write_cache_metadata(&metadata_path, metadata)
+ }
+
+ fn cache_path(&self, key: &str) -> Option<PathBuf> {
+ if !is_safe_key(key) {
+ return None;
+ }
+
+ let relative = Path::new(key);
+ if relative.is_absolute() {
+ return None;
+ }
+
+ if relative.components().any(|component| {
+ matches!(
+ component,
+ Component::ParentDir
+ | Component::CurDir
+ | Component::Prefix(_)
+ | Component::RootDir
+ )
+ }) {
+ return None;
+ }
+
+ Some(self.cache_dir.join(relative))
+ }
+}
+
#[allow(dead_code)]
pub struct FirmwareRegistry {
base_dir: PathBuf,
blobs: HashMap<String, FirmwareBlob>,
- cache: Arc<Mutex<HashMap<String, Arc<Vec<u8>>>>>,
+ cache: Arc<Mutex<HashMap<String, CachedBlob>>>,
+ persistent_cache: FirmwareCache,
+ fallbacks: FirmwareFallback,
}
impl FirmwareRegistry {
pub fn empty(base_dir: &Path) -> Self {
- FirmwareRegistry {
- base_dir: base_dir.to_path_buf(),
- blobs: HashMap::new(),
- cache: Arc::new(Mutex::new(HashMap::new())),
- }
+ Self::with_components(
+ base_dir,
+ HashMap::new(),
+ FirmwareCache::new(Path::new(DEFAULT_CACHE_DIR)),
+ FirmwareFallback::load_defaults(),
+ )
}
pub fn new(base_dir: &Path) -> Result<Self, BlobError> {
@@ -58,11 +412,12 @@ impl FirmwareRegistry {
base_dir.display()
);
- Ok(FirmwareRegistry {
- base_dir: base_dir.to_path_buf(),
+ Ok(Self::with_components(
+ base_dir,
blobs,
- cache: Arc::new(Mutex::new(HashMap::new())),
- })
+ FirmwareCache::new(Path::new(DEFAULT_CACHE_DIR)),
+ FirmwareFallback::load_defaults(),
+ ))
}
#[allow(dead_code)]
@@ -73,56 +428,236 @@ impl FirmwareRegistry {
#[allow(dead_code)]
pub fn contains(&self, key: &str) -> bool {
self.blobs.contains_key(key)
+ || self.persistent_cache.contains(key)
+ || self
+ .fallbacks
+ .get_fallback_chain(key)
+ .into_iter()
+ .any(|candidate| {
+ self.blobs.contains_key(&candidate)
+ || self.persistent_cache.contains(&candidate)
+ })
}
#[allow(dead_code)]
pub fn load(&self, key: &str) -> Result<Arc<Vec<u8>>, BlobError> {
+ self.load_internal(key, None, None)
+ }
+
+ pub fn load_with_timeout(
+ &self,
+ key: &str,
+ started_at: Instant,
+ timeout: Duration,
+ ) -> Result<Arc<Vec<u8>>, BlobError> {
+ self.load_internal(key, Some(started_at), Some(timeout))
+ }
+
+ pub fn len(&self) -> usize {
+ self.blobs.len()
+ }
+
+ #[allow(dead_code)]
+ pub fn list_keys(&self) -> Vec<&str> {
+ self.blobs.keys().map(|s| s.as_str()).collect()
+ }
+
+ fn with_components(
+ base_dir: &Path,
+ blobs: HashMap<String, FirmwareBlob>,
+ persistent_cache: FirmwareCache,
+ fallbacks: FirmwareFallback,
+ ) -> Self {
+ Self {
+ base_dir: base_dir.to_path_buf(),
+ blobs,
+ cache: Arc::new(Mutex::new(HashMap::new())),
+ persistent_cache,
+ fallbacks,
+ }
+ }
+
+ fn load_internal(
+ &self,
+ key: &str,
+ started_at: Option<Instant>,
+ timeout: Option<Duration>,
+ ) -> Result<Arc<Vec<u8>>, BlobError> {
+ if let Some(entry) = self.load_validated_persistent_cache(key, started_at, timeout)? {
+ self.insert_memory_cache(key, entry.clone());
+ info!(
+ "firmware-loader: loaded firmware blob {} ({} bytes) from persistent cache",
+ key,
+ entry.data.len()
+ );
+ return Ok(entry.data);
+ }
+
+ if let Some(entry) = self.memory_cache_get_validated(key, started_at, timeout)? {
+ return Ok(entry.data);
+ }
+
+ let mut last_not_found = BlobError::FirmwareNotFound(self.base_dir.join(key));
+ for candidate in
+ std::iter::once(key.to_string()).chain(self.fallbacks.get_fallback_chain(key))
{
- let cache = self.cache.lock().map_err(|e| BlobError::ReadError {
- path: self.base_dir.clone(),
- source: std::io::Error::new(std::io::ErrorKind::Other, e.to_string()),
- })?;
- if let Some(data) = cache.get(key) {
- return Ok(Arc::clone(data));
+ match self.read_from_filesystem(&candidate, key, started_at, timeout) {
+ Ok(entry) => {
+ self.insert_memory_cache(key, entry.clone());
+
+ if let Err(err) = self.persistent_cache.store_entry(
+ key,
+ entry.data.as_slice(),
+ &entry.metadata,
+ ) {
+ warn!(
+ "firmware-loader: failed to persist cache entry for {}: {}",
+ key, err
+ );
+ }
+
+ if candidate != key {
+ info!(
+ "firmware-loader: resolved firmware {} via fallback {} ({} bytes)",
+ key,
+ candidate,
+ entry.data.len()
+ );
+ }
+
+ return Ok(entry.data);
+ }
+ Err(BlobError::FirmwareNotFound(path)) => {
+ last_not_found = BlobError::FirmwareNotFound(path);
+ }
+ Err(err) => return Err(err),
}
}
- let blob = self.blobs.get(key).ok_or_else(|| {
- warn!("firmware-loader: requested firmware not found: {}", key);
- BlobError::FirmwareNotFound(self.base_dir.join(key))
- })?;
+ warn!("firmware-loader: requested firmware not found: {}", key);
+ Err(last_not_found)
+ }
- let data = fs::read(&blob.path).map_err(|e| BlobError::ReadError {
- path: blob.path.clone(),
- source: e,
- })?;
+ fn load_validated_persistent_cache(
+ &self,
+ key: &str,
+ started_at: Option<Instant>,
+ timeout: Option<Duration>,
+ ) -> Result<Option<CachedBlob>, BlobError> {
+ let Some(entry) = self.persistent_cache.load_entry(key, started_at, timeout)? else {
+ return Ok(None);
+ };
- info!(
- "firmware-loader: loaded firmware blob {} ({} bytes) from {}",
- key,
- data.len(),
- blob.path.display()
- );
+ if self.is_cached_entry_valid(key, &entry, started_at, timeout)? {
+ return Ok(Some(entry));
+ }
- let data = Arc::new(data);
- {
- let mut cache = self.cache.lock().map_err(|e| BlobError::ReadError {
- path: self.base_dir.clone(),
- source: std::io::Error::new(std::io::ErrorKind::Other, e.to_string()),
- })?;
- cache.insert(key.to_string(), Arc::clone(&data));
+ self.persistent_cache.invalidate(key);
+ Ok(None)
+ }
+
+ fn memory_cache_get_validated(
+ &self,
+ key: &str,
+ started_at: Option<Instant>,
+ timeout: Option<Duration>,
+ ) -> Result<Option<CachedBlob>, BlobError> {
+ let entry = match self.cache.lock() {
+ Ok(cache) => cache.get(key).cloned(),
+ Err(err) => {
+ warn!(
+ "firmware-loader: in-memory cache poisoned while loading {}: {}",
+ key, err
+ );
+ None
+ }
+ };
+
+ let Some(entry) = entry else {
+ return Ok(None);
+ };
+
+ if self.is_cached_entry_valid(key, &entry, started_at, timeout)? {
+ return Ok(Some(entry));
}
- Ok(data)
+ match self.cache.lock() {
+ Ok(mut cache) => {
+ cache.remove(key);
+ }
+ Err(err) => warn!(
+ "firmware-loader: failed to invalidate in-memory cache for {}: {}",
+ key, err
+ ),
+ }
+
+ Ok(None)
}
- pub fn len(&self) -> usize {
- self.blobs.len()
+ fn is_cached_entry_valid(
+ &self,
+ key: &str,
+ entry: &CachedBlob,
+ started_at: Option<Instant>,
+ timeout: Option<Duration>,
+ ) -> Result<bool, BlobError> {
+ if let Some(exact_blob) = self.blobs.get(key) {
+ if entry.metadata.source_key != key {
+ return Ok(false);
+ }
+
+ let signature = source_signature(&exact_blob.path, key, started_at, timeout)?;
+ return Ok(entry.metadata.matches(key, key, &signature));
+ }
+
+ if let Some(source_blob) = self.blobs.get(&entry.metadata.source_key) {
+ let signature = source_signature(&source_blob.path, key, started_at, timeout)?;
+ return Ok(entry
+ .metadata
+ .matches(key, &entry.metadata.source_key, &signature));
+ }
+
+ Ok(entry.metadata.requested_key == key)
}
- #[allow(dead_code)]
- pub fn list_keys(&self) -> Vec<&str> {
- self.blobs.keys().map(|s| s.as_str()).collect()
+ fn insert_memory_cache(&self, key: &str, entry: CachedBlob) {
+ match self.cache.lock() {
+ Ok(mut cache) => {
+ cache.insert(key.to_string(), entry);
+ }
+ Err(err) => warn!(
+ "firmware-loader: failed to update in-memory cache for {}: {}",
+ key, err
+ ),
+ }
+ }
+
+ fn read_from_filesystem(
+ &self,
+ source_key: &str,
+ requested_key: &str,
+ started_at: Option<Instant>,
+ timeout: Option<Duration>,
+ ) -> Result<CachedBlob, BlobError> {
+ let blob = self
+ .blobs
+ .get(source_key)
+ .ok_or_else(|| BlobError::FirmwareNotFound(self.base_dir.join(source_key)))?;
+
+ let signature = source_signature(&blob.path, requested_key, started_at, timeout)?;
+ let data = read_path_bytes(&blob.path, requested_key, started_at, timeout)?;
+
+ info!(
+ "firmware-loader: loaded firmware blob {} ({} bytes) from {}",
+ source_key,
+ data.len(),
+ blob.path.display()
+ );
+
+ Ok(CachedBlob {
+ data: Arc::new(data),
+ metadata: CacheMetadata::from_source(requested_key, source_key, &signature),
+ })
}
}
@@ -190,37 +725,606 @@ fn is_metadata_file(file_name: &str) -> bool {
|| file_name.starts_with("LICENSE")
}
+fn is_safe_key(key: &str) -> bool {
+ !key.is_empty()
+ && !key.starts_with('.')
+ && !key.contains("..")
+ && key
+ .chars()
+ .all(|c| c.is_alphanumeric() || c == '/' || c == '-' || c == '_' || c == '.')
+}
+
+fn load_cache_metadata(
+ path: &Path,
+ key: &str,
+ started_at: Option<Instant>,
+ timeout: Option<Duration>,
+) -> Result<Option<CacheMetadata>, BlobError> {
+ let bytes = match read_path_bytes(path, key, started_at, timeout) {
+ Ok(bytes) => bytes,
+ Err(BlobError::ReadError { source, .. }) if source.kind() == ErrorKind::NotFound => {
+ return Ok(None);
+ }
+ Err(err) => return Err(err),
+ };
+
+ let contents = String::from_utf8(bytes).map_err(|err| BlobError::ReadError {
+ path: path.to_path_buf(),
+ source: std::io::Error::new(ErrorKind::InvalidData, err),
+ })?;
+
+ parse_cache_metadata(&contents)
+ .map(Some)
+ .map_err(|err| BlobError::ReadError {
+ path: path.to_path_buf(),
+ source: std::io::Error::new(ErrorKind::InvalidData, err),
+ })
+}
+
+fn write_cache_metadata(path: &Path, metadata: &CacheMetadata) -> Result<(), std::io::Error> {
+ fs::write(path, serialize_cache_metadata(metadata))
+}
+
+fn serialize_cache_metadata(metadata: &CacheMetadata) -> String {
+ format!(
+ "requested_key = {}\nsource_key = {}\nsource_mtime_ns = {}\nsource_len = {}\n",
+ toml::Value::String(metadata.requested_key.clone()),
+ toml::Value::String(metadata.source_key.clone()),
+ metadata.source_mtime_ns,
+ metadata.source_len,
+ )
+}
+
+fn parse_cache_metadata(contents: &str) -> Result<CacheMetadata, String> {
+ let value = contents
+ .parse::<toml::Value>()
+ .map_err(|err| err.to_string())?;
+ let table = value
+ .as_table()
+ .ok_or_else(|| "cache metadata must be a TOML table".to_string())?;
+
+ let requested_key = table
+ .get("requested_key")
+ .and_then(toml::Value::as_str)
+ .ok_or_else(|| "cache metadata missing requested_key".to_string())?;
+ let source_key = table
+ .get("source_key")
+ .and_then(toml::Value::as_str)
+ .ok_or_else(|| "cache metadata missing source_key".to_string())?;
+ let source_mtime_ns = table
+ .get("source_mtime_ns")
+ .and_then(toml::Value::as_integer)
+ .ok_or_else(|| "cache metadata missing source_mtime_ns".to_string())?;
+ let source_len = table
+ .get("source_len")
+ .and_then(toml::Value::as_integer)
+ .ok_or_else(|| "cache metadata missing source_len".to_string())?;
+
+ let source_mtime_ns = u128::try_from(source_mtime_ns)
+ .map_err(|_| "cache metadata source_mtime_ns must be non-negative".to_string())?;
+ let source_len = u64::try_from(source_len)
+ .map_err(|_| "cache metadata source_len must be non-negative".to_string())?;
+
+ Ok(CacheMetadata {
+ requested_key: requested_key.to_string(),
+ source_key: source_key.to_string(),
+ source_mtime_ns,
+ source_len,
+ })
+}
+
+fn parse_fallback_file(contents: &str) -> Result<HashMap<String, Vec<String>>, String> {
+ let value = contents
+ .parse::<toml::Value>()
+ .map_err(|err| err.to_string())?;
+ let table = value
+ .as_table()
+ .ok_or_else(|| "fallback config must be a TOML table".to_string())?;
+
+ let mut fallbacks = HashMap::new();
+
+ for (key, value) in table {
+ if key == "fallbacks" {
+ let nested = value
+ .as_table()
+ .ok_or_else(|| "fallbacks must be a table of string arrays".to_string())?;
+ parse_fallback_entries(nested, &mut fallbacks)?;
+ continue;
+ }
+
+ if value.is_array() {
+ parse_fallback_entry(key, value, &mut fallbacks)?;
+ }
+ }
+
+ Ok(fallbacks)
+}
+
+fn parse_fallback_entries(
+ entries: &toml::map::Map<String, toml::Value>,
+ fallbacks: &mut HashMap<String, Vec<String>>,
+) -> Result<(), String> {
+ for (key, value) in entries {
+ parse_fallback_entry(key, value, fallbacks)?;
+ }
+ Ok(())
+}
+
+fn parse_fallback_entry(
+ key: &str,
+ value: &toml::Value,
+ fallbacks: &mut HashMap<String, Vec<String>>,
+) -> Result<(), String> {
+ let array = value
+ .as_array()
+ .ok_or_else(|| format!("fallback entry {key} must be an array"))?;
+
+ let mut variants = Vec::with_capacity(array.len());
+ for item in array {
+ let variant = item
+ .as_str()
+ .ok_or_else(|| format!("fallback entry {key} must contain only strings"))?;
+ variants.push(variant.to_string());
+ }
+
+ fallbacks.insert(key.to_string(), variants);
+ Ok(())
+}
+
+fn source_signature(
+ path: &Path,
+ key: &str,
+ started_at: Option<Instant>,
+ timeout: Option<Duration>,
+) -> Result<SourceSignature, BlobError> {
+ let metadata = run_io_with_timeout(path, key, started_at, timeout, |path| fs::metadata(path))?;
+ let modified_ns = match metadata.modified() {
+ Ok(modified) => match modified.duration_since(UNIX_EPOCH) {
+ Ok(duration) => duration.as_nanos(),
+ Err(_) => 0,
+ },
+ Err(_) => 0,
+ };
+
+ Ok(SourceSignature {
+ modified_ns,
+ len: metadata.len(),
+ })
+}
+
+fn read_path_bytes(
+ path: &Path,
+ key: &str,
+ started_at: Option<Instant>,
+ timeout: Option<Duration>,
+) -> Result<Vec<u8>, BlobError> {
+ run_io_with_timeout(path, key, started_at, timeout, |path| fs::read(path))
+}
+
+fn run_io_with_timeout<T, F>(
+ path: &Path,
+ key: &str,
+ started_at: Option<Instant>,
+ timeout: Option<Duration>,
+ operation: F,
+) -> Result<T, BlobError>
+where
+ T: Send + 'static,
+ F: FnOnce(PathBuf) -> Result<T, std::io::Error> + Send + 'static,
+{
+ let path_buf = path.to_path_buf();
+
+ if timeout.is_none() {
+ return operation(path_buf.clone()).map_err(|source| BlobError::ReadError {
+ path: path_buf,
+ source,
+ });
+ }
+
+ let total_timeout = timeout.unwrap_or_default();
+ let remaining = remaining_timeout(key, started_at, timeout)?;
+ let (tx, rx) = mpsc::sync_channel(1);
+
+ std::thread::spawn(move || {
+ let result = operation(path_buf.clone());
+ let _ = tx.send((path_buf, result));
+ });
+
+ match rx.recv_timeout(remaining) {
+ Ok((_path, Ok(value))) => Ok(value),
+ Ok((path, Err(source))) => Err(BlobError::ReadError { path, source }),
+ Err(RecvTimeoutError::Timeout) => Err(BlobError::LoadTimeout {
+ key: key.to_string(),
+ timeout: total_timeout,
+ }),
+ Err(RecvTimeoutError::Disconnected) => Err(BlobError::ReadError {
+ path: path.to_path_buf(),
+ source: std::io::Error::new(
+ ErrorKind::BrokenPipe,
+ "firmware-loader I/O worker disconnected unexpectedly",
+ ),
+ }),
+ }
+}
+
+fn remaining_timeout(
+ key: &str,
+ started_at: Option<Instant>,
+ timeout: Option<Duration>,
+) -> Result<Duration, BlobError> {
+ match (started_at, timeout) {
+ (Some(started_at), Some(timeout)) => {
+ timeout
+ .checked_sub(started_at.elapsed())
+ .ok_or_else(|| BlobError::LoadTimeout {
+ key: key.to_string(),
+ timeout,
+ })
+ }
+ _ => Ok(Duration::MAX),
+ }
+}
+
+fn metadata_path_for(path: &Path) -> PathBuf {
+ let mut file_name = path
+ .file_name()
+ .map(OsStr::to_os_string)
+ .unwrap_or_default();
+ file_name.push(".meta");
+ path.with_file_name(file_name)
+}
+
+fn pattern_capture<'a>(pattern: &'a str, key: &'a str) -> Option<&'a str> {
+ if let Some(index) = pattern.find('*') {
+ let prefix = &pattern[..index];
+ let suffix = &pattern[index + 1..];
+ if !key.starts_with(prefix) || !key.ends_with(suffix) {
+ return None;
+ }
+ let capture_end = key.len().checked_sub(suffix.len())?;
+ if capture_end < prefix.len() {
+ return None;
+ }
+ return Some(&key[prefix.len()..capture_end]);
+ }
+
+ if key == pattern {
+ return Some("");
+ }
+
+ if key.starts_with(pattern) {
+ let boundary = key.as_bytes().get(pattern.len()).copied();
+ if matches!(boundary, Some(b'/')) {
+ return Some("");
+ }
+ }
+
+ None
+}
+
+fn append_variants(
+ key: &str,
+ capture: &str,
+ variants: &[String],
+ seen: &mut HashSet<String>,
+ chain: &mut Vec<String>,
+) {
+ for variant in variants {
+ let candidate = if variant.contains('*') {
+ variant.replace('*', capture)
+ } else {
+ variant.clone()
+ };
+
+ if candidate != key && seen.insert(candidate.clone()) {
+ chain.push(candidate);
+ }
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
+ use std::ffi::CString;
+ #[cfg(unix)]
+ use std::os::unix::ffi::OsStrExt;
use std::time::{SystemTime, UNIX_EPOCH};
fn temp_root(prefix: &str) -> PathBuf {
- let stamp = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .unwrap()
- .as_nanos();
+ let stamp = match SystemTime::now().duration_since(UNIX_EPOCH) {
+ Ok(duration) => duration.as_nanos(),
+ Err(err) => panic!("system clock error while creating temp path: {err}"),
+ };
let path = std::env::temp_dir().join(format!("{prefix}-{stamp}"));
- fs::create_dir_all(&path).unwrap();
+ if let Err(err) = fs::create_dir_all(&path) {
+ panic!("failed to create temp directory {}: {err}", path.display());
+ }
path
}
+ fn registry_with_cache(
+ base_dir: &Path,
+ cache_dir: &Path,
+ fallbacks: FirmwareFallback,
+ ) -> FirmwareRegistry {
+ let blobs = match discover_firmware(base_dir) {
+ Ok(blobs) => blobs,
+ Err(err) => panic!(
+ "failed to discover firmware in {}: {err}",
+ base_dir.display()
+ ),
+ };
+
+ FirmwareRegistry::with_components(base_dir, blobs, FirmwareCache::new(cache_dir), fallbacks)
+ }
+
#[test]
fn discovers_ucode_pnvm_and_bin_but_skips_license_metadata() {
let root = temp_root("rbos-fw-discover");
- fs::write(root.join("demo.bin"), []).unwrap();
- fs::write(root.join("iwlwifi-bz-b0-gf-a0-92.ucode"), []).unwrap();
- fs::write(root.join("iwlwifi-bz-b0-gf-a0.pnvm"), []).unwrap();
- fs::write(root.join("LICENCE.test"), "license").unwrap();
- fs::write(root.join("WHENCE"), "meta").unwrap();
+ if let Err(err) = fs::write(root.join("demo.bin"), []) {
+ panic!("failed to write demo firmware: {err}");
+ }
+ if let Err(err) = fs::write(root.join("iwlwifi-bz-b0-gf-a0-92.ucode"), []) {
+ panic!("failed to write ucode firmware: {err}");
+ }
+ if let Err(err) = fs::write(root.join("iwlwifi-bz-b0-gf-a0.pnvm"), []) {
+ panic!("failed to write pnvm firmware: {err}");
+ }
+ if let Err(err) = fs::write(root.join("LICENCE.test"), "license") {
+ panic!("failed to write metadata file: {err}");
+ }
+ if let Err(err) = fs::write(root.join("WHENCE"), "meta") {
+ panic!("failed to write whence file: {err}");
+ }
- let blobs = discover_firmware(&root).unwrap();
+ let blobs = match discover_firmware(&root) {
+ Ok(blobs) => blobs,
+ Err(err) => panic!("failed to discover firmware: {err}"),
+ };
assert!(blobs.contains_key("demo.bin"));
assert!(blobs.contains_key("iwlwifi-bz-b0-gf-a0-92.ucode"));
assert!(blobs.contains_key("iwlwifi-bz-b0-gf-a0.pnvm"));
assert!(!blobs.contains_key("LICENCE.test"));
assert!(!blobs.contains_key("WHENCE"));
- fs::remove_dir_all(root).unwrap();
+ if let Err(err) = fs::remove_dir_all(&root) {
+ panic!("failed to remove temp directory {}: {err}", root.display());
+ }
+ }
+
+ #[test]
+ fn fallback_chain_matches_builtin_wildcards() {
+ let fallbacks = FirmwareFallback::builtins();
+ let chain = fallbacks.get_fallback_chain("iwlwifi-bz-b0-gf-a0-92.ucode");
+
+ assert_eq!(
+ chain,
+ vec![
+ "iwlwifi-bz-b0-gf-a0-83.ucode".to_string(),
+ "iwlwifi-bz-b0-gf-a0-77.ucode".to_string(),
+ ]
+ );
+ }
+
+ #[test]
+ fn load_uses_fallback_and_populates_persistent_cache() {
+ let root = temp_root("rbos-fw-fallback");
+ let cache = temp_root("rbos-fw-cache");
+ let amdgpu = root.join("amdgpu");
+ if let Err(err) = fs::create_dir_all(&amdgpu) {
+ panic!("failed to create amdgpu directory: {err}");
+ }
+ if let Err(err) = fs::write(amdgpu.join("dmcub_dcn30.bin"), b"dcn30") {
+ panic!("failed to write fallback firmware: {err}");
+ }
+
+ let registry = registry_with_cache(&root, &cache, FirmwareFallback::builtins());
+ let data = match registry.load("amdgpu/dmcub_dcn31.bin") {
+ Ok(data) => data,
+ Err(err) => panic!("failed to load fallback firmware: {err}"),
+ };
+
+ assert_eq!(data.as_slice(), b"dcn30");
+
+ let cached = match fs::read(cache.join("amdgpu/dmcub_dcn31.bin")) {
+ Ok(data) => data,
+ Err(err) => panic!("failed to read persistent cache file: {err}"),
+ };
+ assert_eq!(cached, b"dcn30");
+
+ if let Err(err) = fs::remove_dir_all(&root) {
+ panic!("failed to remove temp directory {}: {err}", root.display());
+ }
+ if let Err(err) = fs::remove_dir_all(&cache) {
+ panic!("failed to remove temp directory {}: {err}", cache.display());
+ }
+ }
+
+ #[test]
+ fn persistent_cache_survives_registry_restart() {
+ let root = temp_root("rbos-fw-restart");
+ let cache = temp_root("rbos-fw-restart-cache");
+ let amdgpu = root.join("amdgpu");
+ if let Err(err) = fs::create_dir_all(&amdgpu) {
+ panic!("failed to create amdgpu directory: {err}");
+ }
+ if let Err(err) = fs::write(amdgpu.join("dmcub_dcn30.bin"), b"persistent") {
+ panic!("failed to write fallback firmware: {err}");
+ }
+
+ let first_registry = registry_with_cache(&root, &cache, FirmwareFallback::builtins());
+ if let Err(err) = first_registry.load("amdgpu/dmcub_dcn31.bin") {
+ panic!("failed to prime persistent cache: {err}");
+ }
+
+ if let Err(err) = fs::remove_file(amdgpu.join("dmcub_dcn30.bin")) {
+ panic!("failed to remove source firmware: {err}");
+ }
+
+ let restarted_registry = registry_with_cache(&root, &cache, FirmwareFallback::builtins());
+ let data = match restarted_registry.load("amdgpu/dmcub_dcn31.bin") {
+ Ok(data) => data,
+ Err(err) => panic!("failed to load firmware from persistent cache: {err}"),
+ };
+ assert_eq!(data.as_slice(), b"persistent");
+
+ if let Err(err) = fs::remove_dir_all(&root) {
+ panic!("failed to remove temp directory {}: {err}", root.display());
+ }
+ if let Err(err) = fs::remove_dir_all(&cache) {
+ panic!("failed to remove temp directory {}: {err}", cache.display());
+ }
+ }
+
+ #[test]
+ fn persistent_cache_invalidates_when_exact_firmware_appears() {
+ let root = temp_root("rbos-fw-exact-wins");
+ let cache = temp_root("rbos-fw-exact-cache");
+ let amdgpu = root.join("amdgpu");
+ if let Err(err) = fs::create_dir_all(&amdgpu) {
+ panic!("failed to create amdgpu directory: {err}");
+ }
+ if let Err(err) = fs::write(amdgpu.join("dmcub_dcn30.bin"), b"fallback") {
+ panic!("failed to write fallback firmware: {err}");
+ }
+
+ let first_registry = registry_with_cache(&root, &cache, FirmwareFallback::builtins());
+ if let Err(err) = first_registry.load("amdgpu/dmcub_dcn31.bin") {
+ panic!("failed to prime persistent cache: {err}");
+ }
+
+ if let Err(err) = fs::write(amdgpu.join("dmcub_dcn31.bin"), b"exact") {
+ panic!("failed to write exact firmware: {err}");
+ }
+
+ let restarted_registry = registry_with_cache(&root, &cache, FirmwareFallback::builtins());
+ let data = match restarted_registry.load("amdgpu/dmcub_dcn31.bin") {
+ Ok(data) => data,
+ Err(err) => panic!("failed to reload firmware after exact install: {err}"),
+ };
+ assert_eq!(data.as_slice(), b"exact");
+
+ if let Err(err) = fs::remove_dir_all(&root) {
+ panic!("failed to remove temp directory {}: {err}", root.display());
+ }
+ if let Err(err) = fs::remove_dir_all(&cache) {
+ panic!("failed to remove temp directory {}: {err}", cache.display());
+ }
+ }
+
+ #[test]
+ fn persistent_cache_refreshes_when_source_blob_changes() {
+ let root = temp_root("rbos-fw-refresh");
+ let cache = temp_root("rbos-fw-refresh-cache");
+ if let Err(err) = fs::write(root.join("demo.bin"), b"old") {
+ panic!("failed to write initial firmware: {err}");
+ }
+
+ let first_registry = registry_with_cache(&root, &cache, FirmwareFallback::builtins());
+ if let Err(err) = first_registry.load("demo.bin") {
+ panic!("failed to prime exact persistent cache: {err}");
+ }
+
+ std::thread::sleep(Duration::from_millis(5));
+ if let Err(err) = fs::write(root.join("demo.bin"), b"new") {
+ panic!("failed to update firmware: {err}");
+ }
+
+ let restarted_registry = registry_with_cache(&root, &cache, FirmwareFallback::builtins());
+ let data = match restarted_registry.load("demo.bin") {
+ Ok(data) => data,
+ Err(err) => panic!("failed to reload updated firmware: {err}"),
+ };
+ assert_eq!(data.as_slice(), b"new");
+
+ if let Err(err) = fs::remove_dir_all(&root) {
+ panic!("failed to remove temp directory {}: {err}", root.display());
+ }
+ if let Err(err) = fs::remove_dir_all(&cache) {
+ panic!("failed to remove temp directory {}: {err}", cache.display());
+ }
+ }
+
+ #[cfg(unix)]
+ #[test]
+ fn actual_blocking_read_times_out_within_budget() {
+ let root = temp_root("rbos-fw-timeout");
+ let fifo = root.join("blocking.fifo");
+
+ let fifo_c_string = match CString::new(fifo.as_os_str().as_bytes()) {
+ Ok(value) => value,
+ Err(err) => panic!("failed to build fifo path string: {err}"),
+ };
+ let result = unsafe { libc::mkfifo(fifo_c_string.as_ptr(), 0o644) };
+ if result != 0 {
+ let errno = std::io::Error::last_os_error();
+ panic!("failed to create fifo {}: {errno}", fifo.display());
+ }
+
+ let started = Instant::now();
+ let result = read_path_bytes(
+ &fifo,
+ "blocking-firmware.bin",
+ Some(started),
+ Some(Duration::from_millis(100)),
+ );
+ let elapsed = started.elapsed();
+
+ match result {
+ Err(BlobError::LoadTimeout { key, timeout }) => {
+ assert_eq!(key, "blocking-firmware.bin");
+ assert_eq!(timeout, Duration::from_millis(100));
+ }
+ other => panic!("expected timeout error, got {other:?}"),
+ }
+ assert!(elapsed < Duration::from_secs(1));
+
+ if let Err(err) = fs::remove_file(&fifo) {
+ panic!("failed to remove fifo {}: {err}", fifo.display());
+ }
+ if let Err(err) = fs::remove_dir_all(&root) {
+ panic!("failed to remove temp directory {}: {err}", root.display());
+ }
+ }
+
+ #[test]
+ fn parse_fallback_file_supports_nested_and_top_level_rules() {
+ let parsed = match parse_fallback_file(
+ r#"
+"amdgpu/dmcub_dcn31.bin" = ["amdgpu/dmcub_dcn30.bin"]
+
+[fallbacks]
+"iwlwifi-*-92.ucode" = ["iwlwifi-*-83.ucode"]
+"#,
+ ) {
+ Ok(parsed) => parsed,
+ Err(err) => panic!("failed to parse fallback config: {err}"),
+ };
+
+ assert_eq!(
+ parsed.get("amdgpu/dmcub_dcn31.bin"),
+ Some(&vec!["amdgpu/dmcub_dcn30.bin".to_string()])
+ );
+ assert_eq!(
+ parsed.get("iwlwifi-*-92.ucode"),
+ Some(&vec!["iwlwifi-*-83.ucode".to_string()])
+ );
+ }
+
+ #[test]
+ fn parse_cache_metadata_round_trips() {
+ let metadata = CacheMetadata {
+ requested_key: "demo.bin".to_string(),
+ source_key: "demo.bin".to_string(),
+ source_mtime_ns: 123,
+ source_len: 456,
+ };
+
+ let parsed = match parse_cache_metadata(&serialize_cache_metadata(&metadata)) {
+ Ok(parsed) => parsed,
+ Err(err) => panic!("failed to parse cache metadata: {err}"),
+ };
+
+ assert_eq!(parsed, metadata);
}
}
diff --git a/local/recipes/system/firmware-loader/source/src/scheme.rs b/local/recipes/system/firmware-loader/source/src/scheme.rs
index 2a62b0737..d1d8cf499 100644
--- a/local/recipes/system/firmware-loader/source/src/scheme.rs
+++ b/local/recipes/system/firmware-loader/source/src/scheme.rs
@@ -1,5 +1,6 @@
use std::collections::BTreeMap;
use std::sync::Arc;
+use std::time::Instant;
use log::warn;
use redox_scheme::scheme::SchemeSync;
@@ -12,6 +13,7 @@ use crate::blob::FirmwareRegistry;
#[cfg_attr(not(target_os = "redox"), allow(dead_code))]
const SCHEME_ROOT_ID: usize = 1;
+const FIRMWARE_LOAD_TIMEOUT_MS: u64 = 5000;
#[cfg_attr(not(target_os = "redox"), allow(dead_code))]
struct Handle {
@@ -94,15 +96,22 @@ impl SchemeSync for FirmwareScheme {
let key = resolve_key(path).ok_or(Error::new(EISDIR))?;
- if !self.registry.contains(&key) {
- warn!("firmware-loader: firmware not found: {}", path);
- return Err(Error::new(ENOENT));
- }
-
- let data = self.registry.load(&key).map_err(|e| {
- warn!("firmware-loader: failed to load firmware '{}': {}", key, e);
- Error::new(ENOENT)
- })?;
+ let started_at = Instant::now();
+ let data = self
+ .registry
+ .load_with_timeout(
+ &key,
+ started_at,
+ std::time::Duration::from_millis(FIRMWARE_LOAD_TIMEOUT_MS),
+ )
+ .map_err(|e| {
+ warn!("firmware-loader: failed to load firmware '{}': {}", key, e);
+ match e {
+ crate::blob::BlobError::LoadTimeout { .. } => Error::new(ETIMEDOUT),
+ crate::blob::BlobError::ReadError { .. } => Error::new(EIO),
+ _ => Error::new(ENOENT),
+ }
+ })?;
let id = self.next_id;
self.next_id += 1;
@@ -172,7 +181,7 @@ impl SchemeSync for FirmwareScheme {
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;
+ stat.st_blocks = (handle.data.len() as u64).div_ceil(512);
stat.st_nlink = 1;
Ok(())
@@ -386,9 +395,7 @@ mod tests {
let mut scheme = FirmwareScheme::new(registry);
let ctx = test_ctx();
- let err = scheme
- .openat(SCHEME_ROOT_ID, "", 0, 0, &ctx)
- .unwrap_err();
+ let err = scheme.openat(SCHEME_ROOT_ID, "", 0, 0, &ctx).unwrap_err();
assert_eq!(err.errno, EISDIR);
let _ = fs::remove_dir_all(&dir);
}
@@ -399,9 +406,7 @@ mod tests {
let mut scheme = FirmwareScheme::new(registry);
let ctx = test_ctx();
- let err = scheme
- .openat(999, "test-blob.bin", 0, 0, &ctx)
- .unwrap_err();
+ let err = scheme.openat(999, "test-blob.bin", 0, 0, &ctx).unwrap_err();
assert_eq!(err.errno, EACCES);
let _ = fs::remove_dir_all(&dir);
}
@@ -641,9 +646,7 @@ mod tests {
let id = open_test_blob(&mut scheme);
let ctx = test_ctx();
- let flags = scheme
- .fevent(id, EventFlags::empty(), &ctx)
- .unwrap();
+ let flags = scheme.fevent(id, EventFlags::empty(), &ctx).unwrap();
assert_eq!(flags, EventFlags::empty());
let _ = fs::remove_dir_all(&dir);
}