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:
2026-04-12 19:05:00 +01:00
commit 50b731f1b7
3392 changed files with 98327 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
[source]
path = "source"
[build]
template = "custom"
script = """
# Build and install ext4d scheme daemon
COOKBOOK_CARGO_PATH=ext4d cookbook_cargo
# Build and install ext4-mkfs tool
COOKBOOK_CARGO_PATH=ext4-mkfs cookbook_cargo
"""
@@ -0,0 +1,3 @@
[build]
target-dir = "target"
# Target will be set by cookbook's COOKBOOK_TARGET
@@ -0,0 +1,22 @@
[workspace]
members = [
"ext4-blockdev",
"ext4d",
"ext4-mkfs",
]
resolver = "3"
[workspace.package]
version = "0.1.0"
edition = "2024"
license = "MIT"
[workspace.dependencies]
rsext4 = "0.3"
redox_syscall = "0.7.3"
redox-scheme = "0.11.0"
libredox = "0.1.13"
redox-path = "0.3.0"
log = "0.4"
env_logger = "0.11"
libc = "0.2"
@@ -0,0 +1,16 @@
[package]
name = "ext4-blockdev"
description = "BlockDevice trait implementations for rsext4 on Redox OS"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
rsext4.workspace = true
redox_syscall = { workspace = true, optional = true }
libredox = { workspace = true, optional = true }
log.workspace = true
[features]
default = ["redox"]
redox = ["dep:redox_syscall", "dep:libredox"]
@@ -0,0 +1,100 @@
use std::fs::{File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::Path;
use std::time::UNIX_EPOCH;
use rsext4::bmalloc::AbsoluteBN;
use rsext4::disknode::Ext4Timestamp;
use rsext4::{BlockDevice, Ext4Error, Ext4Result};
pub struct FileDisk {
file: File,
total_blocks: u64,
block_size: u32,
}
impl FileDisk {
pub fn open<P: AsRef<Path>>(path: P, block_size: u32) -> std::io::Result<Self> {
let file = OpenOptions::new().read(true).write(true).open(path)?;
let len = file.metadata()?.len();
Ok(Self {
file,
total_blocks: len / block_size as u64,
block_size,
})
}
pub fn create<P: AsRef<Path>>(path: P, size: u64, block_size: u32) -> std::io::Result<Self> {
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(path)?;
file.set_len(size)?;
Ok(Self {
file,
total_blocks: size / block_size as u64,
block_size,
})
}
}
impl BlockDevice for FileDisk {
fn read(&mut self, buffer: &mut [u8], block_id: AbsoluteBN, count: u32) -> Ext4Result<()> {
let offset = block_id.raw() * self.block_size as u64;
self.file
.seek(SeekFrom::Start(offset))
.map_err(|_| Ext4Error::io())?;
let total = count as usize * self.block_size as usize;
if buffer.len() < total {
return Err(Ext4Error::invalid_input());
}
self.file
.read_exact(&mut buffer[..total])
.map_err(|_| Ext4Error::io())?;
Ok(())
}
fn write(&mut self, buffer: &[u8], block_id: AbsoluteBN, count: u32) -> Ext4Result<()> {
let offset = block_id.raw() * self.block_size as u64;
self.file
.seek(SeekFrom::Start(offset))
.map_err(|_| Ext4Error::io())?;
let total = count as usize * self.block_size as usize;
if buffer.len() < total {
return Err(Ext4Error::invalid_input());
}
self.file
.write_all(&buffer[..total])
.map_err(|_| Ext4Error::io())?;
Ok(())
}
fn open(&mut self) -> Ext4Result<()> {
Ok(())
}
fn close(&mut self) -> Ext4Result<()> {
Ok(())
}
fn total_blocks(&self) -> u64 {
self.total_blocks
}
fn block_size(&self) -> u32 {
self.block_size
}
fn flush(&mut self) -> Ext4Result<()> {
self.file.sync_data().map_err(|_| Ext4Error::io())
}
fn current_time(&self) -> Ext4Result<Ext4Timestamp> {
let dur = std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| Ext4Error::io())?;
Ok(Ext4Timestamp::new(dur.as_secs() as i64, dur.subsec_nanos()))
}
}
@@ -0,0 +1,13 @@
pub mod file_disk;
#[cfg(feature = "redox")]
pub mod redox_disk;
pub use file_disk::FileDisk;
#[cfg(feature = "redox")]
pub use redox_disk::RedoxDisk;
pub use rsext4::bmalloc::AbsoluteBN;
pub use rsext4::disknode::Ext4Timestamp;
pub use rsext4::{BlockDevice, Ext4Error, Ext4Result};
@@ -0,0 +1,93 @@
use rsext4::bmalloc::AbsoluteBN;
use rsext4::disknode::Ext4Timestamp;
use rsext4::{BlockDevice, Ext4Error, Ext4Result};
pub struct RedoxDisk {
fd: usize,
total_blocks: u64,
block_size: u32,
}
impl RedoxDisk {
pub fn open(disk_path: &str, block_size: u32) -> syscall::error::Result<Self> {
let fd = libredox::call::open(disk_path, libredox::flag::O_RDWR, 0)?;
let mut stat = syscall::data::Stat::default();
syscall::call::fstat(fd, &mut stat)?;
let total_blocks = stat.st_size / block_size as u64;
Ok(Self {
fd,
total_blocks,
block_size,
})
}
}
impl BlockDevice for RedoxDisk {
fn read(&mut self, buffer: &mut [u8], block_id: AbsoluteBN, count: u32) -> Ext4Result<()> {
let offset = block_id.raw() * self.block_size as u64;
let total = count as usize * self.block_size as usize;
if buffer.len() < total {
return Err(Ext4Error::invalid_input());
}
syscall::call::lseek(self.fd, offset as isize, syscall::flag::SEEK_SET)
.map_err(|_| Ext4Error::io())?;
let mut read_total = 0;
while read_total < total {
let n = syscall::call::read(self.fd, &mut buffer[read_total..total])
.map_err(|_| Ext4Error::io())?;
if n == 0 {
return Err(Ext4Error::io());
}
read_total += n;
}
Ok(())
}
fn write(&mut self, buffer: &[u8], block_id: AbsoluteBN, count: u32) -> Ext4Result<()> {
let offset = block_id.raw() * self.block_size as u64;
let total = count as usize * self.block_size as usize;
if buffer.len() < total {
return Err(Ext4Error::invalid_input());
}
syscall::call::lseek(self.fd, offset as isize, syscall::flag::SEEK_SET)
.map_err(|_| Ext4Error::io())?;
let mut written_total = 0;
while written_total < total {
let n = syscall::call::write(self.fd, &buffer[written_total..total])
.map_err(|_| Ext4Error::io())?;
if n == 0 {
return Err(Ext4Error::io());
}
written_total += n;
}
Ok(())
}
fn open(&mut self) -> Ext4Result<()> {
Ok(())
}
fn close(&mut self) -> Ext4Result<()> {
Ok(())
}
fn total_blocks(&self) -> u64 {
self.total_blocks
}
fn block_size(&self) -> u32 {
self.block_size
}
fn flush(&mut self) -> Ext4Result<()> {
syscall::call::fsync(self.fd).map_err(|_| Ext4Error::io())?;
Ok(())
}
fn current_time(&self) -> Ext4Result<Ext4Timestamp> {
let mut ts = syscall::data::TimeSpec::default();
syscall::call::clock_gettime(syscall::flag::CLOCK_REALTIME, &mut ts)
.map_err(|_| Ext4Error::io())?;
Ok(Ext4Timestamp::new(ts.tv_sec, ts.tv_nsec as u32))
}
}
@@ -0,0 +1,16 @@
[package]
name = "ext4-mkfs"
description = "Create ext4 filesystems (mkfs for Redox OS)"
version.workspace = true
edition.workspace = true
license.workspace = true
[[bin]]
name = "ext4-mkfs"
path = "src/main.rs"
[dependencies]
ext4-blockdev = { path = "../ext4-blockdev" }
rsext4.workspace = true
log.workspace = true
env_logger.workspace = true
@@ -0,0 +1,40 @@
use std::env;
use std::process;
use ext4_blockdev::FileDisk;
use rsext4::{mkfs, Jbd2Dev};
fn main() {
env_logger::init();
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: ext4-mkfs <image> [size_in_mb]");
process::exit(1);
}
let path = &args[1];
let size_mb: u64 = if args.len() > 2 {
args[2].parse().unwrap_or(100)
} else {
100
};
let block_size = 4096u32;
let size = size_mb * 1024 * 1024;
let disk = FileDisk::create(path, size, block_size).unwrap_or_else(|e| {
eprintln!("ext4-mkfs: failed to create {}: {}", path, e);
process::exit(1);
});
let mut jbd = Jbd2Dev::initial_jbd2dev(0, disk, false);
mkfs(&mut jbd).unwrap_or_else(|e| {
eprintln!("ext4-mkfs: failed to format: {}", e);
process::exit(1);
});
eprintln!(
"ext4-mkfs: created ext4 filesystem on {} ({}MB)",
path, size_mb
);
}
@@ -0,0 +1,143 @@
use ext4_blockdev::FileDisk;
use rsext4::{
api, dir, entries::DirEntryIterator, loopfile, mkdir, mkfile, mkfs, mount as ext4_mount,
umount, Jbd2Dev,
};
#[test]
fn roundtrip_mkfs_mount_read_write_remount() {
let _ = env_logger::builder().is_test(true).try_init();
let path = "/tmp/test-ext4-roundtrip.img";
let size: u64 = 100 * 1024 * 1024; // 100MB
let block_size = 4096u32;
// Step 1: Create and format
println!("=== Step 1: Create ext4 image ===");
let disk = FileDisk::create(path, size, block_size).expect("create disk");
let mut jbd = Jbd2Dev::initial_jbd2dev(0, disk, false);
mkfs(&mut jbd).expect("mkfs");
println!("Formatted {} ({}MB)", path, size / (1024 * 1024));
// Step 2: Mount
println!("\n=== Step 2: Mount ===");
let disk = FileDisk::open(path, block_size).expect("open for mount");
let mut jbd = Jbd2Dev::initial_jbd2dev(0, disk, true);
let mut fs = ext4_mount(&mut jbd).expect("mount");
println!(
"Mounted: {} blocks, {} free",
fs.superblock.blocks_count(),
fs.statfs().free_blocks
);
// Step 3: Create directory
println!("\n=== Step 3: Create directory /testdir ===");
mkdir(&mut jbd, &mut fs, "/testdir").expect("mkdir");
println!("Created /testdir");
// Step 4: Create file
println!("\n=== Step 4: Create file /testdir/hello.txt ===");
mkfile(&mut jbd, &mut fs, "/testdir/hello.txt", None, None).expect("mkfile");
println!("Created /testdir/hello.txt");
// Step 5: Open and write
println!("\n=== Step 5: Write data ===");
let mut file = api::open(&mut jbd, &mut fs, "/testdir/hello.txt", false).expect("open file");
let data = b"Hello from Red Bear OS ext4!\n";
api::write_at(&mut jbd, &mut fs, &mut file, data).expect("write");
println!("Wrote {} bytes to /testdir/hello.txt", data.len());
// Step 6: Read back
println!("\n=== Step 6: Read back ===");
api::lseek(&mut file, 0).expect("seek to 0");
let read_data = api::read_at(&mut jbd, &mut fs, &mut file, data.len()).expect("read");
let read_str = std::str::from_utf8(&read_data).expect("utf8");
println!("Read back: {:?}", read_str.trim());
assert_eq!(
data,
&read_data[..data.len()],
"read data matches written data"
);
// Step 7: List root directory
println!("\n=== Step 7: List root directory ===");
let (_, root_inode) = dir::get_inode_with_num(&mut fs, &mut jbd, "/")
.expect("get root inode")
.expect("root inode found");
let mut root_copy = root_inode;
let blocks = loopfile::resolve_inode_block_allextend(&mut fs, &mut jbd, &mut root_copy)
.expect("resolve root blocks");
let block_size_usize = fs.superblock.block_size() as usize;
for (&_logical, &phys) in blocks.iter() {
let cached = fs
.datablock_cache
.get_or_load(&mut jbd, phys)
.expect("cache load");
for (entry, _) in DirEntryIterator::new(&cached.data[..block_size_usize]) {
if let Some(name) = entry.name_str() {
if !name.is_empty() && name != "." && name != ".." {
println!(" /{} (inode={})", name, entry.inode);
}
}
}
}
// Step 8: List /testdir
println!("\n=== Step 8: List /testdir ===");
let (_, dir_inode) = dir::get_inode_with_num(&mut fs, &mut jbd, "/testdir")
.expect("get testdir inode")
.expect("testdir found");
let mut dir_copy = dir_inode;
let dir_blocks = loopfile::resolve_inode_block_allextend(&mut fs, &mut jbd, &mut dir_copy)
.expect("resolve testdir blocks");
for (&_logical, &phys) in dir_blocks.iter() {
let cached = fs
.datablock_cache
.get_or_load(&mut jbd, phys)
.expect("cache load dir");
for (entry, _) in DirEntryIterator::new(&cached.data[..block_size_usize]) {
if let Some(name) = entry.name_str() {
if !name.is_empty() && name != "." && name != ".." {
println!(" /testdir/{} (inode={})", name, entry.inode);
}
}
}
}
// Step 9: Stat filesystem
println!("\n=== Step 9: Filesystem stats ===");
let stats = fs.statfs();
println!(" block_size: {}", stats.block_size);
println!(" total_blocks: {}", stats.total_blocks);
println!(" free_blocks: {}", stats.free_blocks);
println!(" total_inodes: {}", stats.total_inodes);
println!(" free_inodes: {}", stats.free_inodes);
// Step 10: Sync and unmount
println!("\n=== Step 10: Sync + Unmount ===");
fs.sync_filesystem(&mut jbd).expect("sync");
umount(fs, &mut jbd).expect("umount");
println!("Synced and unmounted cleanly");
// Step 11: Re-mount and verify data persists
println!("\n=== Step 11: Re-mount and verify persistence ===");
let disk2 = FileDisk::open(path, block_size).expect("reopen");
let mut jbd2 = Jbd2Dev::initial_jbd2dev(0, disk2, true);
let mut fs2 = ext4_mount(&mut jbd2).expect("remount");
let mut file2 =
api::open(&mut jbd2, &mut fs2, "/testdir/hello.txt", false).expect("reopen file");
let read_data2 = api::read_at(&mut jbd2, &mut fs2, &mut file2, data.len()).expect("reread");
assert_eq!(
data,
&read_data2[..data.len()],
"data persists after remount"
);
let read_str2 = std::str::from_utf8(&read_data2).expect("utf8");
println!("After remount, read: {:?}", read_str2.trim());
fs2.sync_filesystem(&mut jbd2).expect("sync2");
umount(fs2, &mut jbd2).expect("umount2");
}
@@ -0,0 +1,25 @@
[package]
name = "ext4d"
description = "ext4 filesystem scheme daemon for Redox OS"
version.workspace = true
edition.workspace = true
license.workspace = true
[[bin]]
name = "ext4d"
path = "src/main.rs"
[dependencies]
ext4-blockdev = { path = "../ext4-blockdev" }
rsext4.workspace = true
redox_syscall.workspace = true
redox-scheme.workspace = true
libredox = { workspace = true, optional = true }
redox-path = { workspace = true, optional = true }
log.workspace = true
env_logger = { workspace = true, optional = true }
libc.workspace = true
[features]
default = ["redox"]
redox = ["dep:libredox", "dep:redox-path", "ext4-blockdev/redox", "dep:env_logger"]
@@ -0,0 +1,96 @@
use rsext4::{api::OpenFile, bmalloc::InodeNumber, disknode::Ext4Inode};
use syscall::flag::{O_ACCMODE, O_RDONLY, O_RDWR, O_WRONLY};
pub enum Handle {
File(FileHandle),
Directory(DirectoryHandle),
SchemeRoot,
}
pub struct FileHandle {
path: String,
pub file: OpenFile,
flags: usize,
}
pub struct DirectoryHandle {
path: String,
inode_num: InodeNumber,
inode: Ext4Inode,
flags: usize,
}
impl FileHandle {
pub fn new(path: String, file: OpenFile, flags: usize) -> Self {
Self { path, file, flags }
}
pub fn path(&self) -> &str {
&self.path
}
pub fn inode_num(&self) -> InodeNumber {
self.file.inode_num
}
pub fn flags(&self) -> usize {
self.flags
}
pub fn can_read(&self) -> bool {
matches!(self.flags & O_ACCMODE, O_RDONLY | O_RDWR)
}
pub fn can_write(&self) -> bool {
matches!(self.flags & O_ACCMODE, O_WRONLY | O_RDWR)
}
pub fn set_path(&mut self, path: String) {
self.path = path;
}
}
impl DirectoryHandle {
pub fn new(path: String, inode_num: InodeNumber, inode: Ext4Inode, flags: usize) -> Self {
Self {
path,
inode_num,
inode,
flags,
}
}
pub fn path(&self) -> &str {
&self.path
}
pub fn inode_num(&self) -> InodeNumber {
self.inode_num
}
pub fn inode(&self) -> &Ext4Inode {
&self.inode
}
pub fn flags(&self) -> usize {
self.flags
}
}
impl Handle {
pub fn path(&self) -> Option<&str> {
match self {
Self::File(handle) => Some(handle.path()),
Self::Directory(handle) => Some(handle.path()),
Self::SchemeRoot => Some(""),
}
}
pub fn flags(&self) -> Option<usize> {
match self {
Self::File(handle) => Some(handle.flags()),
Self::Directory(handle) => Some(handle.flags()),
Self::SchemeRoot => None,
}
}
}
@@ -0,0 +1,196 @@
use std::{
env,
fs::File,
io::{self, Read, Write},
os::unix::io::{FromRawFd, RawFd},
process,
sync::atomic::{AtomicUsize, Ordering},
};
use ext4_blockdev::FileDisk;
#[cfg(target_os = "redox")]
use ext4_blockdev::RedoxDisk;
use rsext4::{Jbd2Dev, mount as ext4_mount};
mod handle;
mod mount;
mod scheme;
pub static IS_UMT: AtomicUsize = AtomicUsize::new(0);
extern "C" fn unmount_handler(_signal: usize) {
IS_UMT.store(1, Ordering::SeqCst);
}
fn install_sigterm_handler() -> io::Result<()> {
unsafe {
let mut action: libc::sigaction = std::mem::zeroed();
if libc::sigemptyset(&mut action.sa_mask) != 0 {
return Err(io::Error::last_os_error());
}
action.sa_flags = 0;
action.sa_sigaction = unmount_handler as usize;
if libc::sigaction(libc::SIGTERM, &action, std::ptr::null_mut()) != 0 {
return Err(io::Error::last_os_error());
}
}
Ok(())
}
fn fork_process() -> io::Result<libc::pid_t> {
let pid = unsafe { libc::fork() };
if pid < 0 {
Err(io::Error::last_os_error())
} else {
Ok(pid)
}
}
fn make_pipe() -> io::Result<[i32; 2]> {
let mut pipes = [0; 2];
if unsafe { libc::pipe(pipes.as_mut_ptr()) } != 0 {
return Err(io::Error::last_os_error());
}
Ok(pipes)
}
#[cfg(target_os = "redox")]
fn capability_mode() {
if let Err(err) = libredox::call::setrens(0, 0) {
log::error!("ext4d: failed to enter null namespace: {err}");
}
}
#[cfg(not(target_os = "redox"))]
fn capability_mode() {}
fn usage() {
eprintln!("ext4d [--no-daemon|-d] <disk_path> <mountpoint>");
}
fn fail_usage(message: &str) -> ! {
eprintln!("ext4d: {message}");
usage();
process::exit(1);
}
#[cfg(target_os = "redox")]
fn run_mount(disk_path: &str, mountpoint: &str) -> Result<(), String> {
let disk = RedoxDisk::open(disk_path, 4096)
.map_err(|err| format!("failed to open {disk_path}: {err}"))?;
let mut journal = Jbd2Dev::initial_jbd2dev(0, disk, true);
let filesystem = ext4_mount(&mut journal)
.map_err(|err| format!("failed to mount ext4 on {disk_path}: {err}"))?;
mount::mount(filesystem, journal, mountpoint, |mounted_path| {
capability_mode();
log::info!("mounted ext4 filesystem on {disk_path} to {mounted_path}");
})
.map_err(|err| format!("failed to serve scheme {mountpoint}: {err}"))
}
#[cfg(not(target_os = "redox"))]
fn run_mount(disk_path: &str, mountpoint: &str) -> Result<(), String> {
let disk = FileDisk::open(disk_path, 4096)
.map_err(|err| format!("failed to open {disk_path}: {err}"))?;
let mut journal = Jbd2Dev::initial_jbd2dev(0, disk, true);
let filesystem = ext4_mount(&mut journal)
.map_err(|err| format!("failed to mount ext4 on {disk_path}: {err}"))?;
mount::mount(filesystem, journal, mountpoint, |mounted_path| {
capability_mode();
log::info!("mounted ext4 filesystem on {disk_path} to {mounted_path}");
})
.map_err(|err| format!("failed to serve scheme {mountpoint}: {err}"))
}
fn daemon(disk_path: &str, mountpoint: &str, mut status_pipe: Option<File>) -> i32 {
IS_UMT.store(0, Ordering::SeqCst);
if let Err(err) = install_sigterm_handler() {
log::error!("failed to install SIGTERM handler: {err}");
if let Some(pipe) = status_pipe.as_mut() {
let _ = pipe.write_all(&[1]);
}
return 1;
}
match run_mount(disk_path, mountpoint) {
Ok(()) => {
if let Some(pipe) = status_pipe.as_mut() {
let _ = pipe.write_all(&[0]);
}
0
}
Err(err) => {
log::error!("{err}");
if let Some(pipe) = status_pipe.as_mut() {
let _ = pipe.write_all(&[1]);
}
1
}
}
}
fn main() {
#[cfg(feature = "redox")]
env_logger::init();
let mut daemonize = true;
let mut disk_path: Option<String> = None;
let mut mountpoint: Option<String> = None;
for arg in env::args().skip(1) {
match arg.as_str() {
"--no-daemon" | "-d" => daemonize = false,
_ if disk_path.is_none() => disk_path = Some(arg),
_ if mountpoint.is_none() => mountpoint = Some(arg),
_ => fail_usage("too many arguments provided"),
}
}
let Some(disk_path) = disk_path else {
fail_usage("no disk path provided");
};
let Some(mountpoint) = mountpoint else {
fail_usage("no mountpoint provided");
};
if daemonize {
let pipes = match make_pipe() {
Ok(pipes) => pipes,
Err(err) => {
eprintln!("ext4d: failed to create pipe: {err}");
process::exit(1);
}
};
let mut read = unsafe { File::from_raw_fd(pipes[0] as RawFd) };
let write = unsafe { File::from_raw_fd(pipes[1] as RawFd) };
match fork_process() {
Ok(0) => {
drop(read);
process::exit(daemon(&disk_path, &mountpoint, Some(write)));
}
Ok(_pid) => {
drop(write);
let mut response = [1u8; 1];
if let Err(err) = read.read_exact(&mut response) {
eprintln!("ext4d: failed to read child status: {err}");
process::exit(1);
}
process::exit(i32::from(response[0]));
}
Err(err) => {
eprintln!("ext4d: failed to fork: {err}");
process::exit(1);
}
}
} else {
log::info!("running ext4d in foreground");
process::exit(daemon(&disk_path, &mountpoint, None));
}
}
@@ -0,0 +1,70 @@
use std::sync::atomic::Ordering;
use redox_scheme::{
RequestKind, Response, SignalBehavior, Socket,
scheme::{SchemeState, SchemeSync, register_sync_scheme},
};
use rsext4::{BlockDevice, Ext4FileSystem, Jbd2Dev};
use crate::{IS_UMT, scheme::Ext4Scheme};
pub fn mount<D, T, F>(
filesystem: Ext4FileSystem,
journal: Jbd2Dev<D>,
mountpoint: &str,
callback: F,
) -> syscall::error::Result<T>
where
D: BlockDevice,
F: FnOnce(&str) -> T,
{
let socket = Socket::create()?;
let scheme_name = mountpoint.to_string();
let mounted_path = format!("/scheme/{mountpoint}");
let mut state = SchemeState::new();
let mut scheme = Ext4Scheme::new(scheme_name, mounted_path.clone(), filesystem, journal);
register_sync_scheme(&socket, mountpoint, &mut scheme)?;
let result = callback(&mounted_path);
while IS_UMT.load(Ordering::SeqCst) == 0 {
let request = match socket.next_request(SignalBehavior::Restart)? {
None => break,
Some(request) => match request.kind() {
RequestKind::Call(request) => request,
RequestKind::SendFd(sendfd_request) => {
let response = Response::new(scheme.on_sendfd(&sendfd_request), sendfd_request);
if !socket.write_response(response, SignalBehavior::Restart)? {
break;
}
continue;
}
RequestKind::OnClose { id } => {
scheme.on_close(id);
state.on_close(id);
continue;
}
RequestKind::OnDetach { id, pid } => {
let Ok(inode) = scheme.inode(id) else {
log::warn!("OnDetach received unknown handle id={id}");
continue;
};
state.on_detach(id, inode, pid);
continue;
}
_ => continue,
},
};
let response = request.handle_sync(&mut scheme, &mut state);
if !socket.write_response(response, SignalBehavior::Restart)? {
break;
}
}
scheme.cleanup()?;
Ok(result)
}
@@ -0,0 +1,679 @@
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use redox_scheme::{CallerCtx, OpenResult, SendFdRequest, scheme::SchemeSync};
use rsext4::{
BlockDevice, Ext4Error, Ext4FileSystem, Jbd2Dev, api, delete_dir, delete_file, dir,
disknode::Ext4Inode,
entries::{DirEntryIterator, Ext4DirEntry2},
loopfile, mkdir, mkfile, truncate, umount,
};
use syscall::{
data::{Stat, StatVfs},
dirent::{DirEntry, DirentBuf, DirentKind},
error::{
EACCES, EBADF, EEXIST, EINVAL, EISDIR, ENOENT, ENOTDIR, ENOTEMPTY, EPERM, Error, Result,
},
flag::{
AT_REMOVEDIR, EventFlags, F_GETFD, F_GETFL, F_SETFD, F_SETFL, O_ACCMODE, O_CREAT,
O_DIRECTORY, O_EXCL, O_RDONLY, O_TRUNC, O_WRONLY,
},
schemev2::NewFdFlags,
};
use crate::handle::{DirectoryHandle, FileHandle, Handle};
const PERM_EXEC: u16 = 0o1;
const PERM_WRITE: u16 = 0o2;
const PERM_READ: u16 = 0o4;
struct Lookup {
path: String,
inode_num: rsext4::bmalloc::InodeNumber,
inode: Ext4Inode,
}
pub struct Ext4Scheme<D: BlockDevice> {
mounted_path: String,
fs: Ext4FileSystem,
journal: Jbd2Dev<D>,
next_id: AtomicUsize,
handles: BTreeMap<usize, Handle>,
}
impl<D: BlockDevice> Ext4Scheme<D> {
pub fn new(
_scheme_name: String,
mounted_path: String,
fs: Ext4FileSystem,
journal: Jbd2Dev<D>,
) -> Self {
Self {
mounted_path,
fs,
journal,
next_id: AtomicUsize::new(1),
handles: BTreeMap::new(),
}
}
pub fn cleanup(self) -> Result<()> {
let Ext4Scheme {
mut fs,
mut journal,
..
} = self;
fs.sync_filesystem(&mut journal).map_err(ext4_error)?;
umount(fs, &mut journal).map_err(ext4_error)
}
fn insert_handle(&mut self, handle: Handle) -> usize {
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
self.handles.insert(id, handle);
id
}
fn root_lookup(&mut self) -> Result<Lookup> {
let (inode_num, inode) = dir::get_inode_with_num(&mut self.fs, &mut self.journal, "/")
.map_err(ext4_error)?
.ok_or(Error::new(ENOENT))?;
Ok(Lookup {
path: String::new(),
inode_num,
inode,
})
}
fn make_ext4_path(path: &str) -> String {
if path.is_empty() {
"/".to_string()
} else {
format!("/{path}")
}
}
fn normalize_path(path: &str) -> String {
let mut components = Vec::new();
for component in path.split('/') {
match component {
"" | "." => {}
".." => {
let _ = components.pop();
}
part => components.push(part),
}
}
components.join("/")
}
fn join_path(base: &str, path: &str) -> String {
if path.starts_with('/') {
return Self::normalize_path(path);
}
if base.is_empty() {
Self::normalize_path(path)
} else if path.is_empty() {
base.to_string()
} else {
Self::normalize_path(&format!("{base}/{path}"))
}
}
fn dirfd_base_path(&self, dirfd: usize, path: &str) -> Result<String> {
if path.starts_with('/') {
return Ok(Self::normalize_path(path));
}
match self.handles.get(&dirfd) {
Some(Handle::SchemeRoot) => Ok(Self::normalize_path(path)),
Some(Handle::Directory(handle)) => Ok(Self::join_path(handle.path(), path)),
Some(Handle::File(_)) => Err(Error::new(ENOTDIR)),
None => Err(Error::new(EBADF)),
}
}
fn split_parent_child(path: &str) -> Result<(String, String)> {
let normalized = Self::normalize_path(path);
if normalized.is_empty() {
return Err(Error::new(EPERM));
}
match normalized.rsplit_once('/') {
Some((parent, child)) if !child.is_empty() => {
Ok((parent.to_string(), child.to_string()))
}
None => Ok((String::new(), normalized)),
_ => Err(Error::new(EINVAL)),
}
}
fn check_permission(inode: &Ext4Inode, ctx: &CallerCtx, perm: u16) -> bool {
if ctx.uid == 0 {
return true;
}
let mode = inode.permissions();
let granted = if ctx.uid == inode.uid() {
(mode >> 6) & 0o7
} else if ctx.gid == inode.gid() {
(mode >> 3) & 0o7
} else {
mode & 0o7
};
granted & perm == perm
}
fn require_permission(inode: &Ext4Inode, ctx: &CallerCtx, perm: u16) -> Result<()> {
if Self::check_permission(inode, ctx, perm) {
Ok(())
} else {
Err(Error::new(EACCES))
}
}
fn lookup_path(&mut self, path: &str, ctx: &CallerCtx) -> Result<Option<Lookup>> {
let normalized = Self::normalize_path(path);
if normalized.is_empty() {
return self.root_lookup().map(Some);
}
let mut current = self.root_lookup()?;
for component in normalized.split('/') {
if !current.inode.is_dir() {
return Err(Error::new(ENOTDIR));
}
Self::require_permission(&current.inode, ctx, PERM_EXEC)?;
let next_path = if current.path.is_empty() {
component.to_string()
} else {
format!("{}/{}", current.path, component)
};
let Some((inode_num, inode)) = dir::get_inode_with_num(
&mut self.fs,
&mut self.journal,
&Self::make_ext4_path(&next_path),
)
.map_err(ext4_error)?
else {
return Ok(None);
};
current = Lookup {
path: next_path,
inode_num,
inode,
};
}
Ok(Some(current))
}
fn lookup_existing(&mut self, path: &str, ctx: &CallerCtx) -> Result<Lookup> {
self.lookup_path(path, ctx)?.ok_or(Error::new(ENOENT))
}
fn lookup_parent(&mut self, path: &str, ctx: &CallerCtx) -> Result<(Lookup, String)> {
let (parent_path, child) = Self::split_parent_child(path)?;
let parent = self.lookup_existing(&parent_path, ctx)?;
if !parent.inode.is_dir() {
return Err(Error::new(ENOTDIR));
}
Self::require_permission(&parent.inode, ctx, PERM_EXEC | PERM_WRITE)?;
Ok((parent, child))
}
fn stat_from_lookup(&self, lookup: &Lookup, stat: &mut Stat) {
*stat = Stat::default();
stat.st_dev = 0;
stat.st_ino = u64::from(lookup.inode_num.raw());
stat.st_mode = lookup.inode.i_mode;
stat.st_nlink = u32::from(lookup.inode.i_links_count);
stat.st_uid = lookup.inode.uid();
stat.st_gid = lookup.inode.gid();
stat.st_size = lookup.inode.size();
stat.st_blksize = self.fs.superblock.block_size() as u32;
stat.st_blocks = lookup.inode.blocks_count();
let inode_size = self.fs.superblock.inode_size();
let atime = lookup.inode.atime_ts(inode_size);
let mtime = lookup.inode.mtime_ts(inode_size);
let ctime = lookup.inode.ctime_ts(inode_size);
stat.st_atime = atime.sec.max(0) as u64;
stat.st_atime_nsec = atime.nsec;
stat.st_mtime = mtime.sec.max(0) as u64;
stat.st_mtime_nsec = mtime.nsec;
stat.st_ctime = ctime.sec.max(0) as u64;
stat.st_ctime_nsec = ctime.nsec;
}
fn refresh_file_handle(&mut self, id: usize) -> Result<()> {
let (path, offset) = match self.handles.get(&id) {
Some(Handle::File(handle)) => (handle.path().to_string(), handle.file.offset),
_ => return Err(Error::new(EBADF)),
};
let file = api::open(
&mut self.journal,
&mut self.fs,
&Self::make_ext4_path(&path),
false,
)
.map_err(ext4_error)?;
let mut file = file;
api::lseek(&mut file, offset).map_err(ext4_error)?;
match self.handles.get_mut(&id) {
Some(Handle::File(handle)) => {
handle.file = file;
handle.set_path(path);
Ok(())
}
_ => Err(Error::new(EBADF)),
}
}
fn dirent_kind_from_file_type(file_type: u8) -> DirentKind {
match file_type {
Ext4DirEntry2::EXT4_FT_DIR => DirentKind::Directory,
Ext4DirEntry2::EXT4_FT_REG_FILE => DirentKind::Regular,
Ext4DirEntry2::EXT4_FT_CHRDEV => DirentKind::CharDev,
Ext4DirEntry2::EXT4_FT_BLKDEV => DirentKind::BlockDev,
Ext4DirEntry2::EXT4_FT_SYMLINK => DirentKind::Symlink,
Ext4DirEntry2::EXT4_FT_SOCK => DirentKind::Socket,
_ => DirentKind::Unspecified,
}
}
fn directory_entries(
&mut self,
_path: &str,
inode: &Ext4Inode,
) -> Result<Vec<(u64, u64, String, DirentKind)>> {
let mut inode_copy = *inode;
let blocks = loopfile::resolve_inode_block_allextend(
&mut self.fs,
&mut self.journal,
&mut inode_copy,
)
.map_err(ext4_error)?;
let block_size = self.fs.superblock.block_size() as usize;
let mut entries = Vec::new();
let mut opaque = 1u64;
for &phys in blocks.values() {
let cached = self
.fs
.datablock_cache
.get_or_load(&mut self.journal, phys)
.map_err(ext4_error)?;
for (entry, _) in DirEntryIterator::new(&cached.data[..block_size]) {
let Some(name) = entry.name_str() else {
continue;
};
let kind = match name {
"." | ".." => DirentKind::Directory,
_ => Self::dirent_kind_from_file_type(entry.file_type),
};
entries.push((u64::from(entry.inode), opaque, name.to_string(), kind));
opaque = opaque.saturating_add(1);
}
}
Ok(entries)
}
fn create_directory_handle(&mut self, lookup: Lookup, flags: usize) -> OpenResult {
let id = self.insert_handle(Handle::Directory(DirectoryHandle::new(
lookup.path,
lookup.inode_num,
lookup.inode,
flags,
)));
OpenResult::ThisScheme {
number: id,
flags: NewFdFlags::POSITIONED,
}
}
fn create_file_handle(
&mut self,
path: String,
file: api::OpenFile,
flags: usize,
) -> OpenResult {
let id = self.insert_handle(Handle::File(FileHandle::new(path, file, flags)));
OpenResult::ThisScheme {
number: id,
flags: NewFdFlags::POSITIONED,
}
}
fn handle_lookup_for_stat(&mut self, id: usize, ctx: &CallerCtx) -> Result<Lookup> {
let path = match self.handles.get(&id) {
Some(Handle::SchemeRoot) => None,
Some(Handle::Directory(handle)) => Some(handle.path().to_string()),
Some(Handle::File(handle)) => Some(handle.path().to_string()),
None => return Err(Error::new(EBADF)),
};
match path {
Some(path) => self.lookup_existing(&path, ctx),
None => self.root_lookup(),
}
}
fn ensure_regular_file_access(handle: &FileHandle, write: bool) -> Result<()> {
if write && !handle.can_write() {
return Err(Error::new(EBADF));
}
if !write && !handle.can_read() {
return Err(Error::new(EBADF));
}
Ok(())
}
}
impl<D: BlockDevice> SchemeSync for Ext4Scheme<D> {
fn scheme_root(&mut self) -> Result<usize> {
Ok(self.insert_handle(Handle::SchemeRoot))
}
fn openat(
&mut self,
dirfd: usize,
path: &str,
flags: usize,
_fcntl_flags: u32,
ctx: &CallerCtx,
) -> Result<OpenResult> {
let resolved_path = self.dirfd_base_path(dirfd, path)?;
match self.lookup_path(&resolved_path, ctx)? {
Some(lookup) => {
if flags & (O_CREAT | O_EXCL) == O_CREAT | O_EXCL {
return Err(Error::new(EEXIST));
}
if lookup.inode.is_dir() {
if flags & O_ACCMODE != O_RDONLY {
return Err(Error::new(EISDIR));
}
Self::require_permission(&lookup.inode, ctx, PERM_READ)?;
return Ok(self.create_directory_handle(lookup, flags));
}
if flags & O_DIRECTORY == O_DIRECTORY {
return Err(Error::new(ENOTDIR));
}
if flags & O_ACCMODE != O_WRONLY {
Self::require_permission(&lookup.inode, ctx, PERM_READ)?;
}
if flags & O_ACCMODE != O_RDONLY {
Self::require_permission(&lookup.inode, ctx, PERM_WRITE)?;
}
let ext4_path = Self::make_ext4_path(&resolved_path);
if flags & O_TRUNC == O_TRUNC {
truncate(&mut self.journal, &mut self.fs, &ext4_path, 0).map_err(ext4_error)?;
}
let file = api::open(&mut self.journal, &mut self.fs, &ext4_path, false)
.map_err(ext4_error)?;
Ok(self.create_file_handle(resolved_path, file, flags))
}
None => {
if flags & O_CREAT != O_CREAT {
return Err(Error::new(ENOENT));
}
let (_parent, _name) = self.lookup_parent(&resolved_path, ctx)?;
let ext4_path = Self::make_ext4_path(&resolved_path);
if flags & O_DIRECTORY == O_DIRECTORY {
mkdir(&mut self.journal, &mut self.fs, &ext4_path).map_err(ext4_error)?;
let lookup = self.lookup_existing(&resolved_path, ctx)?;
Ok(self.create_directory_handle(lookup, flags))
} else {
mkfile(&mut self.journal, &mut self.fs, &ext4_path, None, None)
.map_err(ext4_error)?;
let file = api::open(&mut self.journal, &mut self.fs, &ext4_path, false)
.map_err(ext4_error)?;
Ok(self.create_file_handle(resolved_path, file, flags))
}
}
}
}
fn read(
&mut self,
id: usize,
buf: &mut [u8],
offset: u64,
_fcntl_flags: u32,
_ctx: &CallerCtx,
) -> Result<usize> {
match self.handles.get_mut(&id) {
Some(Handle::File(handle)) => {
Self::ensure_regular_file_access(handle, false)?;
api::lseek(&mut handle.file, offset).map_err(ext4_error)?;
let data =
api::read_at(&mut self.journal, &mut self.fs, &mut handle.file, buf.len())
.map_err(ext4_error)?;
let count = data.len();
buf[..count].copy_from_slice(&data);
Ok(count)
}
Some(Handle::Directory(_)) | Some(Handle::SchemeRoot) => Err(Error::new(EISDIR)),
None => Err(Error::new(EBADF)),
}
}
fn write(
&mut self,
id: usize,
buf: &[u8],
offset: u64,
_fcntl_flags: u32,
_ctx: &CallerCtx,
) -> Result<usize> {
match self.handles.get_mut(&id) {
Some(Handle::File(handle)) => {
Self::ensure_regular_file_access(handle, true)?;
api::lseek(&mut handle.file, offset).map_err(ext4_error)?;
api::write_at(&mut self.journal, &mut self.fs, &mut handle.file, buf)
.map_err(ext4_error)?;
Ok(buf.len())
}
Some(Handle::Directory(_)) | Some(Handle::SchemeRoot) => Err(Error::new(EISDIR)),
None => Err(Error::new(EBADF)),
}
}
fn fsize(&mut self, id: usize, ctx: &CallerCtx) -> Result<u64> {
Ok(self.handle_lookup_for_stat(id, ctx)?.inode.size())
}
fn fcntl(&mut self, id: usize, cmd: usize, _arg: usize, _ctx: &CallerCtx) -> Result<usize> {
let handle = self.handles.get(&id).ok_or(Error::new(EBADF))?;
match cmd {
F_GETFL => Ok(handle.flags().unwrap_or(O_RDONLY)),
F_GETFD => Ok(0),
F_SETFL | F_SETFD => Ok(0),
_ => Err(Error::new(EINVAL)),
}
}
fn fevent(&mut self, id: usize, _flags: EventFlags, _ctx: &CallerCtx) -> Result<EventFlags> {
if self.handles.contains_key(&id) {
Err(Error::new(EPERM))
} else {
Err(Error::new(EBADF))
}
}
fn fpath(&mut self, id: usize, buf: &mut [u8], _ctx: &CallerCtx) -> Result<usize> {
let handle = self.handles.get(&id).ok_or(Error::new(EBADF))?;
let Some(path) = handle.path() else {
return Err(Error::new(EBADF));
};
let full_path = if path.is_empty() {
self.mounted_path.clone()
} else {
format!("{}/{}", self.mounted_path, path)
};
let bytes = full_path.as_bytes();
let count = bytes.len().min(buf.len());
buf[..count].copy_from_slice(&bytes[..count]);
Ok(count)
}
fn fstat(&mut self, id: usize, stat: &mut Stat, ctx: &CallerCtx) -> Result<()> {
let lookup = self.handle_lookup_for_stat(id, ctx)?;
self.stat_from_lookup(&lookup, stat);
Ok(())
}
fn fstatvfs(&mut self, id: usize, stat: &mut StatVfs, _ctx: &CallerCtx) -> Result<()> {
if !self.handles.contains_key(&id) {
return Err(Error::new(EBADF));
}
let stats = self.fs.statfs();
stat.f_bsize = stats.block_size as u32;
stat.f_blocks = stats.total_blocks;
stat.f_bfree = stats.free_blocks;
stat.f_bavail = stats.free_blocks;
Ok(())
}
fn getdents<'buf>(
&mut self,
id: usize,
mut buf: DirentBuf<&'buf mut [u8]>,
opaque_offset: u64,
) -> Result<DirentBuf<&'buf mut [u8]>> {
let (path, inode) = match self.handles.get(&id) {
Some(Handle::Directory(handle)) => (handle.path().to_string(), *handle.inode()),
Some(Handle::SchemeRoot) => {
let lookup = self.root_lookup()?;
(lookup.path, lookup.inode)
}
Some(Handle::File(_)) => return Err(Error::new(ENOTDIR)),
None => return Err(Error::new(EBADF)),
};
let entries = self.directory_entries(&path, &inode)?;
for (inode, next_opaque_id, name, kind) in entries {
if next_opaque_id <= opaque_offset {
continue;
}
buf.entry(DirEntry {
inode,
next_opaque_id,
name: &name,
kind,
})?;
}
Ok(buf)
}
fn fsync(&mut self, id: usize, _ctx: &CallerCtx) -> Result<()> {
if !self.handles.contains_key(&id) {
return Err(Error::new(EBADF));
}
self.fs
.sync_filesystem(&mut self.journal)
.map_err(ext4_error)
}
fn ftruncate(&mut self, id: usize, len: u64, _ctx: &CallerCtx) -> Result<()> {
let path = match self.handles.get(&id) {
Some(Handle::File(handle)) => handle.path().to_string(),
Some(Handle::Directory(_)) | Some(Handle::SchemeRoot) => {
return Err(Error::new(EISDIR));
}
None => return Err(Error::new(EBADF)),
};
truncate(
&mut self.journal,
&mut self.fs,
&Self::make_ext4_path(&path),
len,
)
.map_err(ext4_error)?;
self.refresh_file_handle(id)
}
fn unlinkat(&mut self, dirfd: usize, path: &str, flags: usize, ctx: &CallerCtx) -> Result<()> {
let resolved_path = self.dirfd_base_path(dirfd, path)?;
let lookup = self.lookup_existing(&resolved_path, ctx)?;
let (_parent, _name) = self.lookup_parent(&resolved_path, ctx)?;
let ext4_path = Self::make_ext4_path(&resolved_path);
if flags & AT_REMOVEDIR == AT_REMOVEDIR {
if !lookup.inode.is_dir() {
return Err(Error::new(ENOTDIR));
}
let entries = self.directory_entries(&lookup.path, &lookup.inode)?;
if entries
.into_iter()
.any(|(_, _, name, _)| name != "." && name != "..")
{
return Err(Error::new(ENOTEMPTY));
}
delete_dir(&mut self.fs, &mut self.journal, &ext4_path).map_err(ext4_error)
} else {
if lookup.inode.is_dir() {
return Err(Error::new(EISDIR));
}
delete_file(&mut self.fs, &mut self.journal, &ext4_path).map_err(ext4_error)
}
}
fn on_close(&mut self, id: usize) {
let _ = self.handles.remove(&id);
}
fn on_sendfd(&mut self, _sendfd_request: &SendFdRequest) -> Result<usize> {
Err(Error::new(EPERM))
}
fn inode(&self, id: usize) -> Result<usize> {
match self.handles.get(&id) {
Some(Handle::File(handle)) => Ok(handle.inode_num().raw() as usize),
Some(Handle::Directory(handle)) => Ok(handle.inode_num().raw() as usize),
Some(Handle::SchemeRoot) => Ok(2),
None => Err(Error::new(EBADF)),
}
}
}
fn ext4_error(err: Ext4Error) -> Error {
Error::new(err.code.as_i32())
}