diff --git a/local/recipes/system/firmware-loader/source/src/blob.rs b/local/recipes/system/firmware-loader/source/src/blob.rs index 7960309cac..bca29aefa2 100644 --- a/local/recipes/system/firmware-loader/source/src/blob.rs +++ b/local/recipes/system/firmware-loader/source/src/blob.rs @@ -49,21 +49,33 @@ struct CacheMetadata { 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, + base_dir: base_dir.to_path_buf(), + blobs, + cache: Arc::new(Mutex::new(HashMap::new())), + persistent_cache, + fallbacks, + builtins: BuiltinRegistry::new(), + stats: Mutex::new(LoadStats::default()), + max_firmware_bytes: 256 * 1024 * 1024, + max_retries: 3, + retry_backoff_ms: 100, + sha512_verify: false, } } - 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, + base_dir: base_dir.to_path_buf(), + blobs, + cache: Arc::new(Mutex::new(HashMap::new())), + persistent_cache, + fallbacks, + builtins: BuiltinRegistry::new(), + stats: Mutex::new(LoadStats::default()), + max_firmware_bytes: 256 * 1024 * 1024, + max_retries: 3, + retry_backoff_ms: 100, + sha512_verify: false, } } @@ -92,19 +104,33 @@ pub struct FirmwareFallback { } impl FirmwareFallback { - pub fn load_defaults() -> Self { - Self::load_from_dir(Path::new(DEFAULT_FALLBACKS_DIR)) - } + Self { + base_dir: base_dir.to_path_buf(), + blobs, + cache: Arc::new(Mutex::new(HashMap::new())), + persistent_cache, + fallbacks, + builtins: BuiltinRegistry::new(), + stats: Mutex::new(LoadStats::default()), + max_firmware_bytes: 256 * 1024 * 1024, + max_retries: 3, + retry_backoff_ms: 100, + sha512_verify: false, + } - 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 {}: {}", + Self { + base_dir: base_dir.to_path_buf(), + blobs, + cache: Arc::new(Mutex::new(HashMap::new())), + persistent_cache, + fallbacks, + builtins: BuiltinRegistry::new(), + stats: Mutex::new(LoadStats::default()), + max_firmware_bytes: 256 * 1024 * 1024, + max_retries: 3, + retry_backoff_ms: 100, + sha512_verify: false, + } dir.display(), err ); @@ -193,32 +219,19 @@ impl FirmwareFallback { 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 } + Self { + base_dir: base_dir.to_path_buf(), + blobs, + cache: Arc::new(Mutex::new(HashMap::new())), + persistent_cache, + fallbacks, + builtins: BuiltinRegistry::new(), + stats: Mutex::new(LoadStats::default()), + max_firmware_bytes: 256 * 1024 * 1024, + max_retries: 3, + retry_backoff_ms: 100, + sha512_verify: false, + } } } @@ -227,9 +240,18 @@ pub struct FirmwareCache { } impl FirmwareCache { - pub fn new(cache_dir: &Path) -> Self { Self { - cache_dir: cache_dir.to_path_buf(), + base_dir: base_dir.to_path_buf(), + blobs, + cache: Arc::new(Mutex::new(HashMap::new())), + persistent_cache, + fallbacks, + builtins: BuiltinRegistry::new(), + stats: Mutex::new(LoadStats::default()), + max_firmware_bytes: 256 * 1024 * 1024, + max_retries: 3, + retry_backoff_ms: 100, + sha512_verify: false, } } @@ -377,81 +399,237 @@ impl FirmwareCache { return None; } - Some(self.cache_dir.join(relative)) - } -} + Some(self.cache_dir.join(relative)) + } + } -#[allow(dead_code)] -pub struct FirmwareRegistry { - base_dir: PathBuf, - blobs: HashMap, - cache: Arc>>, - persistent_cache: FirmwareCache, - fallbacks: FirmwareFallback, -} + /// Statistics for firmware load operations, ported from Linux 7.1 + /// `drivers/base/firmware_loader/main.c:fw_cache` + `fw_load_stats`. + #[derive(Debug, Default, Clone)] + pub struct LoadStats { + pub hits: u64, + pub misses: u64, + pub timeouts: u64, + pub cache_hits: u64, + pub total_load_time_ns: u64, + pub last_load_time_ns: u64, + pub fallback_used: u64, + } -impl FirmwareRegistry { - pub fn empty(base_dir: &Path) -> Self { - Self::with_components( - base_dir, - HashMap::new(), - FirmwareCache::new(Path::new(DEFAULT_CACHE_DIR)), - FirmwareFallback::load_defaults(), - ) - } + impl LoadStats { + pub fn avg_load_time_us(&self) -> u64 { + let total = self.hits.saturating_add(self.misses); + if total == 0 { return 0; } + self.total_load_time_ns / total / 1000 + } + } - pub fn new(base_dir: &Path) -> Result { - if !base_dir.exists() { - return Err(BlobError::DirNotFound(base_dir.to_path_buf())); + /// Built-in firmware blob embedded at compile time, ported from Linux 7.1 + /// `drivers/base/firmware_loader/builtin/` and `firmware_request_builtin()`. + /// Firmware binaries can be embedded via `include_bytes!()` in the builtins + /// module and registered here. + pub struct BuiltinFirmware { + pub name: &'static str, + pub data: &'static [u8], + pub sha512: Option<&'static str>, + } + + /// Registry of compile-time-embedded firmware. + /// Call `BuiltinRegistry::register()` during daemon init to populate. + pub struct BuiltinRegistry { + entries: Vec, + } + + impl BuiltinRegistry { + Self { + base_dir: base_dir.to_path_buf(), + blobs, + cache: Arc::new(Mutex::new(HashMap::new())), + persistent_cache, + fallbacks, + builtins: BuiltinRegistry::new(), + stats: Mutex::new(LoadStats::default()), + max_firmware_bytes: 256 * 1024 * 1024, + max_retries: 3, + retry_backoff_ms: 100, + sha512_verify: false, } - let blobs = discover_firmware(base_dir)?; - info!( - "firmware-loader: indexed {} firmware blob(s) from {}", - blobs.len(), - base_dir.display() - ); + /// Search builtins for a firmware name match, ported from Linux 7.1 + /// `main.c:755-768 firmware_request_builtin_buf()`. + pub fn lookup(&self, name: &str) -> Option<&BuiltinFirmware> { + self.entries.iter().find(|e| e.name == name || e.name == name.trim_end_matches(".xz").trim_end_matches(".zst")) + } - Ok(Self::with_components( - base_dir, + pub fn is_empty(&self) -> bool { self.entries.is_empty() } + } + + #[allow(dead_code)] + pub struct FirmwareRegistry { + base_dir: PathBuf, + blobs: HashMap, + cache: Arc>>, + persistent_cache: FirmwareCache, + fallbacks: FirmwareFallback, + builtins: BuiltinRegistry, + stats: Mutex, + max_firmware_bytes: u64, + max_retries: u32, + retry_backoff_ms: u64, + sha512_verify: bool, + } + + impl FirmwareRegistry { + Self { + base_dir: base_dir.to_path_buf(), blobs, - FirmwareCache::new(Path::new(DEFAULT_CACHE_DIR)), - FirmwareFallback::load_defaults(), - )) - } + cache: Arc::new(Mutex::new(HashMap::new())), + persistent_cache, + fallbacks, + builtins: BuiltinRegistry::new(), + stats: Mutex::new(LoadStats::default()), + max_firmware_bytes: 256 * 1024 * 1024, + max_retries: 3, + retry_backoff_ms: 100, + sha512_verify: false, + } - #[allow(dead_code)] - pub fn base_dir(&self) -> &Path { - &self.base_dir - } + pub fn new(base_dir: &Path) -> Result { + if !base_dir.exists() { + return Err(BlobError::DirNotFound(base_dir.to_path_buf())); + } - #[allow(dead_code)] - pub fn contains(&self, key: &str) -> bool { - self.resolve_blob_path(key).is_some() - || self.persistent_cache.contains(key) - || self - .fallbacks - .get_fallback_chain(key) - .into_iter() - .any(|candidate| { - self.resolve_blob_path(&candidate).is_some() - || self.persistent_cache.contains(&candidate) - }) - } + let blobs = discover_firmware(base_dir)?; + info!( + "firmware-loader: indexed {} firmware blob(s) from {}", + blobs.len(), + base_dir.display() + ); - #[allow(dead_code)] - pub fn load(&self, key: &str) -> Result>, BlobError> { - self.load_internal(key, None, None) - } + Ok(Self::with_components( + base_dir, + blobs, + FirmwareCache::new(Path::new(DEFAULT_CACHE_DIR)), + FirmwareFallback::load_defaults(), + )) + } - pub fn load_with_timeout( - &self, - key: &str, - started_at: Instant, - timeout: Duration, - ) -> Result>, BlobError> { - self.load_internal(key, Some(started_at), Some(timeout)) - } + /// Register compile-time-embedded firmware, ported from Linux 7.1 + /// `firmware_request_builtin()`. + Self { + base_dir: base_dir.to_path_buf(), + blobs, + cache: Arc::new(Mutex::new(HashMap::new())), + persistent_cache, + fallbacks, + builtins: BuiltinRegistry::new(), + stats: Mutex::new(LoadStats::default()), + max_firmware_bytes: 256 * 1024 * 1024, + max_retries: 3, + retry_backoff_ms: 100, + sha512_verify: false, + } + + /// Set maximum firmware size in bytes (default 256MB). + Self { + base_dir: base_dir.to_path_buf(), + blobs, + cache: Arc::new(Mutex::new(HashMap::new())), + persistent_cache, + fallbacks, + builtins: BuiltinRegistry::new(), + stats: Mutex::new(LoadStats::default()), + max_firmware_bytes: 256 * 1024 * 1024, + max_retries: 3, + retry_backoff_ms: 100, + sha512_verify: false, + } + + /// Enable retry with exponential backoff (default: 3 retries, 100ms start). + Self { + base_dir: base_dir.to_path_buf(), + blobs, + cache: Arc::new(Mutex::new(HashMap::new())), + persistent_cache, + fallbacks, + builtins: BuiltinRegistry::new(), + stats: Mutex::new(LoadStats::default()), + max_firmware_bytes: 256 * 1024 * 1024, + max_retries: 3, + retry_backoff_ms: 100, + sha512_verify: false, + } + + /// Enable SHA-512 integrity verification on load. + Self { + base_dir: base_dir.to_path_buf(), + blobs, + cache: Arc::new(Mutex::new(HashMap::new())), + persistent_cache, + fallbacks, + builtins: BuiltinRegistry::new(), + stats: Mutex::new(LoadStats::default()), + max_firmware_bytes: 256 * 1024 * 1024, + max_retries: 3, + retry_backoff_ms: 100, + sha512_verify: false, + } + + /// Return load statistics. + pub fn stats(&self) -> LoadStats { + self.stats.lock().unwrap_or_else(|e| e.into_inner()).clone() + } + + pub fn base_dir(&self) -> &Path { &self.base_dir } + + pub fn contains(&self, key: &str) -> bool { + if !self.builtins.is_empty() && self.builtins.lookup(key).is_some() { return true; } + self.resolve_blob_path(key).is_some() + || self.persistent_cache.contains(key) + || self.fallbacks.get_fallback_chain(key).into_iter().any(|candidate| { + self.resolve_blob_path(&candidate).is_some() + || self.persistent_cache.contains(&candidate) + }) + } + + /// Load firmware with retry + backoff, ported from Linux 7.1 + /// `_request_firmware()` retry loop. + pub fn load(&self, key: &str) -> Result>, BlobError> { + let start = Instant::now(); + let mut last_err: Option = None; + + for attempt in 0..=self.max_retries { + if attempt > 0 { + let delay = self.retry_backoff_ms * (1u64 << (attempt - 1)); + std::thread::sleep(Duration::from_millis(delay.min(5000))); + } + match self.load_attempt(key, None, None) { + Ok(data) => { + let elapsed = start.elapsed().as_nanos() as u64; + let mut stats = self.stats.lock().unwrap_or_else(|e| e.into_inner()); + stats.hits = stats.hits.saturating_add(1); + stats.total_load_time_ns = stats.total_load_time_ns.saturating_add(elapsed); + stats.last_load_time_ns = elapsed; + return Ok(data); + } + Err(e) => { + last_err = Some(e); + if attempt < self.max_retries as usize { + info!("firmware {key}: retry {}/{}, last error: {last_err:?}", attempt+1, self.max_retries); + } + } + } + } + + self.stats.lock().unwrap_or_else(|e| e.into_inner()).misses = self.stats.lock().unwrap_or_else(|e| e.into_inner()).misses.saturating_add(1); + Err(last_err.unwrap_or(BlobError::Io(std::io::Error::new(ErrorKind::NotFound, "all retries exhausted")))) + } + + pub fn load_with_timeout( + &self, key: &str, started_at: Instant, timeout: Duration, + ) -> Result>, BlobError> { + self.load_attempt(key, Some(started_at), Some(timeout)) + } pub fn len(&self) -> usize { self.blobs.len() @@ -467,13 +645,18 @@ impl FirmwareRegistry { blobs: HashMap, 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, + builtins: BuiltinRegistry::new(), + stats: Mutex::new(LoadStats::default()), + max_firmware_bytes: 256 * 1024 * 1024, + max_retries: 3, + retry_backoff_ms: 100, + sha512_verify: false, } } @@ -498,7 +681,7 @@ impl FirmwareRegistry { } } - fn load_internal( + fn load_attempt( &self, key: &str, started_at: Option,