Red Bear OS — microkernel OS in Rust, based on Redox
Derivative of Redox OS (https://www.redox-os.org) adding: - AMD GPU driver (amdgpu) via LinuxKPI compat layer - ext4 filesystem support (ext4d scheme daemon) - ACPI fixes for AMD bare metal (x2APIC, DMAR, IVRS, MCFG) - Custom branding (hostname, os-release, boot identity) Build system is full upstream Redox with RBOS overlay in local/. Patches for kernel, base, and relibc are symlinked from local/patches/ and protected from make clean/distclean. Custom recipes live in local/recipes/ with symlinks into the recipes/ search path. Build: make all CONFIG_NAME=redbear-full Sync: ./local/scripts/sync-upstream.sh
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "firmware-loader"
|
||||
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" }
|
||||
log = { version = "0.4", features = ["std"] }
|
||||
thiserror = "2"
|
||||
@@ -0,0 +1,163 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use log::{info, warn};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum BlobError {
|
||||
#[error("firmware directory not found: {0}")]
|
||||
DirNotFound(PathBuf),
|
||||
#[error("failed to read firmware directory: {0}")]
|
||||
DirReadError(PathBuf, #[source] std::io::Error),
|
||||
#[error("firmware not found: {0}")]
|
||||
FirmwareNotFound(PathBuf),
|
||||
#[error("failed to read firmware blob {path}: {source}")]
|
||||
ReadError {
|
||||
path: PathBuf,
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct FirmwareBlob {
|
||||
#[allow(dead_code)]
|
||||
pub name: String,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
pub struct FirmwareRegistry {
|
||||
base_dir: PathBuf,
|
||||
blobs: HashMap<String, FirmwareBlob>,
|
||||
cache: Arc<Mutex<HashMap<String, Arc<Vec<u8>>>>>,
|
||||
}
|
||||
|
||||
impl FirmwareRegistry {
|
||||
pub fn new(base_dir: &Path) -> Result<Self, BlobError> {
|
||||
if !base_dir.exists() {
|
||||
return Err(BlobError::DirNotFound(base_dir.to_path_buf()));
|
||||
}
|
||||
|
||||
let blobs = discover_firmware(base_dir)?;
|
||||
info!(
|
||||
"firmware-loader: indexed {} firmware blob(s) from {}",
|
||||
blobs.len(),
|
||||
base_dir.display()
|
||||
);
|
||||
|
||||
Ok(FirmwareRegistry {
|
||||
base_dir: base_dir.to_path_buf(),
|
||||
blobs,
|
||||
cache: Arc::new(Mutex::new(HashMap::new())),
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn base_dir(&self) -> &Path {
|
||||
&self.base_dir
|
||||
}
|
||||
|
||||
pub fn contains(&self, key: &str) -> bool {
|
||||
self.blobs.contains_key(key)
|
||||
}
|
||||
|
||||
pub fn load(&self, key: &str) -> Result<Arc<Vec<u8>>, BlobError> {
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
let blob = self.blobs.get(key).ok_or_else(|| {
|
||||
warn!("firmware-loader: requested firmware not found: {}", key);
|
||||
BlobError::FirmwareNotFound(self.base_dir.join(key))
|
||||
})?;
|
||||
|
||||
let data = fs::read(&blob.path).map_err(|e| BlobError::ReadError {
|
||||
path: blob.path.clone(),
|
||||
source: e,
|
||||
})?;
|
||||
|
||||
info!(
|
||||
"firmware-loader: loaded firmware blob {} ({} bytes) from {}",
|
||||
key,
|
||||
data.len(),
|
||||
blob.path.display()
|
||||
);
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn list_keys(&self) -> Vec<&str> {
|
||||
self.blobs.keys().map(|s| s.as_str()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn discover_firmware(base_dir: &Path) -> Result<HashMap<String, FirmwareBlob>, BlobError> {
|
||||
let mut blobs = HashMap::new();
|
||||
let mut stack = vec![(base_dir.to_path_buf(), String::new())];
|
||||
|
||||
while let Some((dir, prefix)) = stack.pop() {
|
||||
let entries = fs::read_dir(&dir).map_err(|e| BlobError::DirReadError(dir.clone(), e))?;
|
||||
|
||||
for entry in entries {
|
||||
let entry = match entry {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
warn!("firmware-loader: skipping unreadable dir entry: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let path = entry.path();
|
||||
let file_name = match entry.file_name().into_string() {
|
||||
Ok(n) => n,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let metadata = match fs::metadata(&path) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
warn!("firmware-loader: skipping {}: {}", path.display(), e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if metadata.is_dir() {
|
||||
let new_prefix = if prefix.is_empty() {
|
||||
file_name
|
||||
} else {
|
||||
format!("{}/{}", prefix, file_name)
|
||||
};
|
||||
stack.push((path, new_prefix));
|
||||
} else if metadata.is_file() && file_name.ends_with(".bin") {
|
||||
let stem = file_name.trim_end_matches(".bin");
|
||||
let key = if prefix.is_empty() {
|
||||
stem.to_string()
|
||||
} else {
|
||||
format!("{}/{}", prefix, stem)
|
||||
};
|
||||
|
||||
blobs.insert(key.clone(), FirmwareBlob { name: key, path });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(blobs)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
mod blob;
|
||||
mod scheme;
|
||||
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
|
||||
use log::{error, info, LevelFilter, Metadata, Record};
|
||||
use redox_scheme::{SignalBehavior, Socket};
|
||||
|
||||
use blob::FirmwareRegistry;
|
||||
use scheme::FirmwareScheme;
|
||||
|
||||
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 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());
|
||||
|
||||
info!(
|
||||
"firmware-loader: starting with directory {}",
|
||||
firmware_dir.display()
|
||||
);
|
||||
|
||||
let registry = FirmwareRegistry::new(&firmware_dir)
|
||||
.map_err(|e| format!("failed to initialize firmware registry: {e}"))?;
|
||||
|
||||
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);
|
||||
|
||||
loop {
|
||||
let request = match socket.next_request(SignalBehavior::Restart) {
|
||||
Ok(Some(request)) => request,
|
||||
Ok(None) => {
|
||||
info!("firmware-loader: scheme unmounted, exiting");
|
||||
break;
|
||||
}
|
||||
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(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let log_level = match env::var("FIRMWARE_LOADER_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!("firmware-loader: fatal error: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
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 crate::blob::FirmwareRegistry;
|
||||
|
||||
struct Handle {
|
||||
blob_key: String,
|
||||
data: Arc<Vec<u8>>,
|
||||
offset: u64,
|
||||
map_count: usize,
|
||||
closed: bool,
|
||||
}
|
||||
|
||||
pub struct FirmwareScheme {
|
||||
registry: FirmwareRegistry,
|
||||
next_id: usize,
|
||||
handles: BTreeMap<usize, Handle>,
|
||||
}
|
||||
|
||||
impl FirmwareScheme {
|
||||
pub fn new(registry: FirmwareRegistry) -> Self {
|
||||
FirmwareScheme {
|
||||
registry,
|
||||
next_id: 0,
|
||||
handles: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_key(path: &str) -> Option<String> {
|
||||
let cleaned = path.trim_matches('/');
|
||||
if cleaned.is_empty() || cleaned.ends_with('/') {
|
||||
return None;
|
||||
}
|
||||
// Reject path traversal attempts — only allow safe characters
|
||||
if cleaned.starts_with('.') || cleaned.contains("..") {
|
||||
log::warn!(
|
||||
"firmware-loader: rejecting path traversal in key: {:?}",
|
||||
path
|
||||
);
|
||||
return None;
|
||||
}
|
||||
let key = if cleaned.ends_with(".bin") {
|
||||
cleaned.trim_end_matches(".bin").to_string()
|
||||
} else {
|
||||
cleaned.to_string()
|
||||
};
|
||||
// Final sanity: key must be purely alphanumeric with /, -, _
|
||||
if !key
|
||||
.chars()
|
||||
.all(|c| c.is_alphanumeric() || c == '/' || c == '-' || c == '_')
|
||||
{
|
||||
log::warn!(
|
||||
"firmware-loader: rejecting invalid characters in key: {:?}",
|
||||
key
|
||||
);
|
||||
return None;
|
||||
}
|
||||
Some(key)
|
||||
}
|
||||
|
||||
impl SchemeBlockMut for FirmwareScheme {
|
||||
fn open(&mut self, path: &str, _flags: usize, _uid: u32, _gid: u32) -> Result<Option<usize>> {
|
||||
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 id = self.next_id;
|
||||
self.next_id += 1;
|
||||
|
||||
self.handles.insert(
|
||||
id,
|
||||
Handle {
|
||||
blob_key: key,
|
||||
data,
|
||||
offset: 0,
|
||||
map_count: 0,
|
||||
closed: false,
|
||||
},
|
||||
);
|
||||
|
||||
Ok(Some(id))
|
||||
}
|
||||
|
||||
fn seek(&mut self, id: usize, pos: isize, whence: usize) -> Result<Option<isize>> {
|
||||
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
|
||||
let len = handle.data.len() as i64;
|
||||
let new_offset = match whence {
|
||||
SEEK_SET => pos as i64,
|
||||
SEEK_CUR => handle.offset as i64 + pos as i64,
|
||||
SEEK_END => len + pos as i64,
|
||||
_ => return Err(Error::new(EINVAL)),
|
||||
};
|
||||
if new_offset < 0 {
|
||||
return Err(Error::new(EINVAL));
|
||||
}
|
||||
handle.offset = new_offset as u64;
|
||||
let new_offset = isize::try_from(new_offset).map_err(|_| Error::new(EINVAL))?;
|
||||
Ok(Some(new_offset))
|
||||
}
|
||||
|
||||
fn read(&mut self, id: usize, buf: &mut [u8]) -> Result<Option<usize>> {
|
||||
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
|
||||
let offset = handle.offset as usize;
|
||||
let data = &handle.data;
|
||||
|
||||
if offset >= data.len() {
|
||||
return Ok(Some(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))
|
||||
}
|
||||
|
||||
fn write(&mut self, id: usize, _buf: &[u8]) -> Result<Option<usize>> {
|
||||
let _ = self.handles.get(&id).ok_or(Error::new(EBADF))?;
|
||||
Err(Error::new(EROFS))
|
||||
}
|
||||
|
||||
fn fpath(&mut self, id: usize, buf: &mut [u8]) -> Result<Option<usize>> {
|
||||
let handle = self.handles.get(&id).ok_or(Error::new(EBADF))?;
|
||||
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))
|
||||
}
|
||||
|
||||
fn fstat(&mut self, id: usize, stat: &mut Stat) -> Result<Option<usize>> {
|
||||
let handle = self.handles.get(&id).ok_or(Error::new(EBADF))?;
|
||||
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))
|
||||
}
|
||||
|
||||
fn fsync(&mut self, id: usize) -> Result<Option<usize>> {
|
||||
if !self.handles.contains_key(&id) {
|
||||
return Err(Error::new(EBADF));
|
||||
}
|
||||
Ok(Some(0))
|
||||
}
|
||||
|
||||
fn fevent(&mut self, id: usize, _flags: EventFlags) -> Result<Option<EventFlags>> {
|
||||
if !self.handles.contains_key(&id) {
|
||||
return Err(Error::new(EBADF));
|
||||
}
|
||||
Ok(Some(EventFlags::empty()))
|
||||
}
|
||||
|
||||
fn close(&mut self, id: usize) -> Result<Option<usize>> {
|
||||
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
|
||||
handle.closed = true;
|
||||
let should_remove = handle.map_count == 0;
|
||||
if should_remove {
|
||||
self.handles.remove(&id);
|
||||
}
|
||||
Ok(Some(0))
|
||||
}
|
||||
|
||||
fn mmap_prep(
|
||||
&mut self,
|
||||
id: usize,
|
||||
offset: u64,
|
||||
size: usize,
|
||||
_flags: MapFlags,
|
||||
) -> Result<Option<usize>> {
|
||||
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
|
||||
let data_len = handle.data.len() as u64;
|
||||
|
||||
if offset > data_len {
|
||||
return Err(Error::new(EINVAL));
|
||||
}
|
||||
if offset + size as u64 > data_len {
|
||||
return Err(Error::new(EINVAL));
|
||||
}
|
||||
|
||||
let ptr = &handle.data[offset as usize] as *const u8;
|
||||
handle.map_count += 1;
|
||||
Ok(Some(ptr as usize))
|
||||
}
|
||||
|
||||
fn munmap(
|
||||
&mut self,
|
||||
id: usize,
|
||||
_offset: u64,
|
||||
_size: usize,
|
||||
_flags: MunmapFlags,
|
||||
) -> Result<Option<usize>> {
|
||||
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
|
||||
if handle.map_count > 0 {
|
||||
handle.map_count -= 1;
|
||||
}
|
||||
let should_cleanup = handle.closed && handle.map_count == 0;
|
||||
if should_cleanup {
|
||||
self.handles.remove(&id);
|
||||
}
|
||||
Ok(Some(0))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user