19 KiB
Live ISO Mount — Architecture, Failure Analysis, and Fix Plan
Date: 2026-05-27 Status: Draft — fixes not yet implemented Scope: Bootloader live preload, lived daemon, RedoxFS mount chain
1. Current Architecture
1.1 Boot Flow (Live ISO)
UEFI firmware
→ Bootloader (recipes/core/bootloader/source/src/main.rs)
1. Find RedoxFS partition on disk
2. Read filesystem header → get total filesystem size (e.g., 4093 MiB)
3. Live preload: read first N MiB of filesystem into RAM
- Cap: max_preload = 1024 MiB (line 559)
- Set env: DISK_LIVE_ADDR=<phys addr>, DISK_LIVE_SIZE=<preload size>
- Set env: REDOXFS_BLOCK=0 (start of partition)
4. Load kernel from RedoxFS into memory
5. Load initfs from RedoxFS into memory
6. Set up paging, pass env to kernel
7. Jump to kernel entry point
Kernel
→ bootstrap (initfs)
→ init daemon
→ lived daemon (10_lived.service)
- Reads DISK_LIVE_ADDR + DISK_LIVE_SIZE from env
- Maps preloaded RAM as LiveDisk via /scheme/memory/physical
- Registers scheme:disk.live
- LiveDisk.size() = preloaded size (1024 MiB)
- LiveDisk.block_size() = PAGE_SIZE (4096) [P6 patch changes to 512]
→ redoxfs daemon (50_rootfs.service)
- Opens /scheme/disk.live/0 as DiskFile
- Calls FileSystem::open(disk, password, block=0, cleanup=true)
- Reads header at block 0 (inside preloaded region → works)
- Calls fs.reset_allocator() → walks the allocation tree
- Calls fs.cleanup() → may read blocks across the entire filesystem
- FAILURE: any read beyond preloaded size returns EINVAL
1.2 Component Map
| Component | Source | Role |
|---|---|---|
| Bootloader | recipes/core/bootloader/source/src/main.rs |
Preloads filesystem into RAM, passes env vars |
| lived | recipes/core/base/source/drivers/storage/lived/src/main.rs |
Maps preloaded RAM as scheme:disk.live |
| RedoxFS mount | recipes/core/redoxfs/source/src/bin/mount.rs |
Opens disk scheme, calls FileSystem::open |
| RedoxFS lib | recipes/core/redoxfs/source/src/filesystem.rs |
Reads header, walks allocator tree |
| driver-block | recipes/core/base/source/drivers/storage/driver-block/src/lib.rs |
DiskWrapper with block_size alignment checks |
| P6 patch | local/patches/base/P6-lived-block-size-512.patch |
Changes block_size from PAGE_SIZE to 512 |
1.3 The Preload Cap
// bootloader/src/main.rs:559
let max_preload: u64 = 1024 * MIBI as u64; // 1 GiB hard cap
let preload_size = if size > max_preload {
max_preload // Cap at 1 GiB
} else {
size // Preload entire filesystem if ≤ 1 GiB
};
For redbear-full (4093 MiB filesystem): preloads 1024 MiB, 3069 MiB must come from disk. For redbear-mini (1533 MiB filesystem): preloads 1024 MiB, 509 MiB must come from disk.
1.4 The lived Disk
// lived/src/main.rs - LiveDisk::read (CURRENT, unpatched source)
fn block_size(&self) -> u32 {
PAGE_SIZE as u32 // P6 changes this to 512
}
fn size(&self) -> u64 {
self.original.len() as u64 // This is the PRELOADED size, not total filesystem size
}
async fn read(&mut self, mut block: u64, buffer: &mut [u8]) -> syscall::Result<usize> {
let mut offset = (block as usize) * PAGE_SIZE;
if offset + buffer.len() > self.original.len() {
return Err(syscall::Error::new(EINVAL)); // ← THIS IS THE FAILURE POINT
}
// ... read from preloaded buffer
}
The fundamental problem: lived only has the preloaded buffer (1024 MiB). It has no
access to the remaining filesystem data on the physical disk. When RedoxFS tries to read
beyond 1024 MiB, lived returns EINVAL.
2. Failure Analysis
2.1 Why Does the Mini ISO Work?
The mini ISO (1533 MiB) also has 509 MiB beyond the preload. However:
- RedoxFS
FileSystem::openreads the header at block 0 (within preload) → OK reset_allocatorwalks the free block tree. For a 1533 MiB filesystem with minimal contents, the allocator metadata is concentrated near the start → likely within 1024 MiBcleanupreads extent nodes — for a small filesystem, these are also near the start
For the full ISO (4093 MiB) with hundreds of packages:
- The allocator tree and extent nodes span the entire 4093 MiB range
- RedoxFS needs to read blocks at offsets > 1024 MiB during
FileSystem::open - lived rejects those reads → mount fails
The mini ISO works by luck — its metadata happens to fit within the preload window. This is not a reliable design.
2.2 The Exact Error Chain
RedoxFS FileSystem::open
→ disk.read_at(block_N, &mut header)
→ DiskFile::read_at(buffer, block_N * BLOCK_SIZE)
→ syscall::read(scheme:disk.live/0, offset=block_N * 512)
→ lived::LiveDisk::read(block_N, buffer)
→ offset = block_N * PAGE_SIZE // or 512 with P6
→ if offset + buffer.len() > self.original.len():
return Err(EINVAL) // ← HERE
The error propagates:
- lived → EINVAL
- DiskFile → "RedoxFS: IO ERROR: Invalid argument (os error 22)"
- FileSystem::open → Err(EINVAL)
- mount.rs → "not able to mount uuid ..."
2.3 The P6 Block Size Patch
The P6 patch (local/patches/base/P6-lived-block-size-512.patch) fixes a different but
related issue: the original block_size() returned PAGE_SIZE (4096), but RedoxFS reads
in 512-byte chunks (BLOCK_SIZE = 4096 but individual reads may be 512). The DiskWrapper
in driver-block rejects misaligned reads. Changing to 512 fixes alignment but does NOT
fix the size/out-of-bounds problem.
Note: The current source tree (recipes/core/base/source/drivers/storage/lived/src/main.rs)
does NOT have the P6 patch applied — it still shows PAGE_SIZE as u32. The P6 patch is
applied during repo fetch base and only exists durably in local/patches/base/.
3. Fix Strategy
3.1 Design Principle
Preload the minimum needed to boot the kernel + initfs. Once the OS is running, mount the filesystem from the actual disk device, not from the RAM preload.
The bootloader already loads kernel + initfs from RedoxFS before switching to live mode. After that, the running OS has access to the AHCI driver (ahcid) and can mount the filesystem directly from the physical disk.
3.2 Two-Phase Approach
Phase A: Bootloader Changes (bootloader is UEFI code, runs before the OS)
-
Reduce preload to the minimum needed for kernel + initfs discovery
- The bootloader needs to read the RedoxFS superblock + directory tree to find
usr/lib/boot/kernelandusr/lib/boot/initfs. This requires reading the header, the root node, and walking directory entries. - Instead of preloading a fixed 1024 MiB, preload only what's needed to locate and read these two files. In practice, this is the first few MiB of the filesystem.
- Fallback: if the filesystem is small enough (≤ 64 MiB?), preload everything.
- The bootloader needs to read the RedoxFS superblock + directory tree to find
-
Pass the physical disk location to the kernel
- Set
DISK_PHYS_ADDRandDISK_PHYS_SIZEenv vars with the full disk geometry - Keep
DISK_LIVE_ADDR/DISK_LIVE_SIZEfor the minimal preload - Add
REDOXFS_FULL_SIZEso the OS knows the true filesystem extent
- Set
Phase B: lived Daemon Changes (OS-level, patchable via local/patches/base/)
-
Accept the full filesystem size as an additional env var
- Read
REDOXFS_FULL_SIZEor derive from the RedoxFS header - Report
LiveDisk::size()as the FULL filesystem size, not just the preload
- Read
-
Fall through to the physical disk for reads beyond the preload
- When
read(block, buffer)is called with an offset beyondself.original.len():- Open the underlying block device (e.g.,
/scheme/disk/0after ahcid starts) - Read the data from the physical disk
- Cache the result in the overlay HashMap
- Open the underlying block device (e.g.,
- This makes lived act as a write-through cache: preload in RAM, fallback to disk
- When
-
Alternative simpler approach: bypass lived entirely for large images
- After ahcid starts and registers
/scheme/disk/0, the init system could mount RedoxFS directly from/scheme/disk/0instead of/scheme/disk.live/0 - The preload would only be used by the bootloader to load kernel + initfs
- Once the OS boots, lived is unnecessary — mount from the real disk
- After ahcid starts and registers
4. Concrete Fix Plan
4.1 Fix 1: Reduce Bootloader Preload (bootloader patch)
File: recipes/core/bootloader/source/src/main.rs
Current:
let max_preload: u64 = 1024 * MIBI as u64;
Proposed change:
// Only preload what the bootloader actually needs:
// - RedoxFS header + allocator (first ~1 MiB)
// - Root directory tree (typically first 32-64 MiB)
// - kernel and initfs files (loaded separately after preload)
// 64 MiB is generous for the metadata region of any reasonable filesystem.
// The kernel and initfs are loaded separately via fs.disk.read_at() directly
// from the physical disk, so they don't need to be in the preload.
let max_preload: u64 = 64 * MIBI as u64;
Wait — this doesn't work. The bootloader reads kernel and initfs from the RedoxFS
filesystem using load_to_memory(os, &mut fs, "usr/lib/boot/kernel", ...). After the
preload, the bootloader has already switched the disk to the live buffer. So the kernel
and initfs must be within the preload, OR the bootloader must load them before switching
to live mode.
Looking at the actual bootloader flow:
1. Open RedoxFS from physical disk → fs
2. Preload first N MiB into RAM buffer
3. Set LIVE_OPT = Some((fs.block, buffer))
4. Load kernel from fs (still using physical disk? or from buffer?)
5. Load initfs from fs
6. Pass LIVE_OPT to kernel env
The live buffer is set in LIVE_OPT at line 625, but the kernel and initfs are loaded
at lines 642-663, AFTER the live preload. The load_to_memory function uses fs which
still uses the original disk handle. So the kernel and initfs are read from the physical
disk, not from the live buffer.
This means the preload doesn't need to include kernel or initfs at all. The preload
exists solely so that lived can serve the filesystem to the running OS via scheme:disk.live.
Revised Fix 1: Reduce max_preload to a small value (e.g., 4-64 MiB) that covers just the RedoxFS metadata needed for initial mount, then rely on the disk fallback for the rest.
BUT: this only works if lived can fall through to the physical disk for out-of-bounds
reads. Without the fallback, reducing preload makes the problem worse.
4.2 Fix 2: lived Disk Fallback (base patch)
File: recipes/core/base/source/drivers/storage/lived/src/main.rs
This is the core fix. Make lived aware of the full filesystem and able to read from
the physical disk when the preload doesn't cover the requested region.
Design:
struct LiveDisk {
// Preloaded RAM buffer (may be smaller than total filesystem)
preload: &'static [u8],
// Full filesystem size (from RedoxFS header or env var)
total_size: u64,
// Physical disk offset where the filesystem starts
disk_block: u64,
// Handle to the physical disk (opened after ahcid starts)
disk_handle: Option<File>,
// Write overlay (same as before)
overlay: HashMap<u64, Box<[u8]>>,
}
impl Disk for LiveDisk {
fn block_size(&self) -> u32 { 512 }
fn size(&self) -> u64 { self.total_size }
async fn read(&mut self, block: u64, buffer: &mut [u8]) -> syscall::Result<usize> {
let bs = self.block_size() as usize;
let offset = (block as usize) * bs;
if offset + buffer.len() > self.total_size as usize {
return Err(syscall::Error::new(EINVAL));
}
let preload_bytes = self.preload.len();
for (i, chunk) in buffer.chunks_mut(bs).enumerate() {
let block_i = block + i as u64;
let offset_i = offset + i * bs;
// Check overlay first
if let Some(overlay) = self.overlay.get(&block_i) {
chunk.copy_from_slice(&overlay[..chunk.len()]);
continue;
}
if offset_i + chunk.len() <= preload_bytes {
// Within preload → read from RAM
chunk.copy_from_slice(&self.preload[offset_i..offset_i + chunk.len()]);
} else {
// Beyond preload → read from physical disk
self.read_from_disk(block_i, chunk)?;
}
}
Ok(buffer.len())
}
fn read_from_disk(&mut self, block: u64, buffer: &mut [u8]) -> syscall::Result<()> {
// Try to open the physical disk if not already open
if self.disk_handle.is_none() {
// Try common disk scheme paths
for path in &["/scheme/disk/0", "/scheme/disk/1"] {
if let Ok(file) = OpenOptions::new().read(true).open(path) {
self.disk_handle = Some(file);
break;
}
}
}
if let Some(ref mut disk) = self.disk_handle {
// Seek to the correct block (accounting for partition offset)
let abs_block = self.disk_block + block;
disk.read_at(buffer, abs_block * self.block_size() as u64)
.map_err(|_| syscall::Error::new(EIO))?;
Ok(())
} else {
// No disk available yet — return what we have from preload
// (fill with zeros for regions not in preload)
buffer.fill(0);
Err(syscall::Error::new(EIO))
}
}
}
Problem with this approach: lived starts before ahcid (it's at priority 10 in
init.initfs.d, while ahcid is at priority 40). So when lived first starts, there IS no
/scheme/disk/0 to fall back to. The disk fallback would only work after ahcid initializes.
4.3 Fix 3: Two-Stage Mount (Recommended)
The cleanest fix is to change the init sequence:
Stage 1: lived serves the preloaded buffer (for early boot)
- lived starts as before, serves the preload via
scheme:disk.live - RedoxFS does NOT mount from
disk.livefor the root filesystem - The initfs has everything needed for early boot (lived, ahcid, basic tools)
Stage 2: Mount from physical disk (after drivers start)
- After
40_drivers.targetcompletes, ahcid has registered/scheme/disk/0 - Init runs
redoxfs --uuid $REDOXFS_UUID file $REDOXFS_BLOCKpointing to/scheme/disk/0 - This reads the full filesystem from the physical disk
Current init flow:
10_lived.service → starts lived (scheme:disk.live)
40_drivers.target → starts ahcid (scheme:disk/0)
50_rootfs.service → redoxfs mounts from... whichever disk scheme it finds first
(scans all /scheme/disk/* for matching UUID)
The issue is timing: 50_rootfs.service requires 40_drivers.target, so it should
wait for ahcid. But redoxfs scans ALL disk schemes, and disk.live matches first
(since lived starts earlier). RedoxFS finds the UUID in disk.live and tries to mount
from it, but the disk is too small.
Proposed fix:
-
Make lived report the full filesystem size by reading the RedoxFS header from the preload buffer to determine
total_size. Report that assize(). -
Make lived fall through to disk reads for out-of-bounds regions. Use a lazy-open approach: when a read goes beyond the preload and no disk handle is open yet, try to open
/scheme/disk/0. If it fails, return EIO (which RedoxFS will retry). -
Reduce the preload in the bootloader. Since lived now handles disk fallback, we can preload much less (e.g., 4-64 MiB). The preload just needs to cover the RedoxFS header and enough metadata for the initial mount.
5. Recommended Implementation Order
Step 1: Fix lived to report full size + disk fallback (base patch)
Create local/patches/base/P59-lived-disk-fallback.patch:
- Add
total_size: u64field to LiveDisk - Parse RedoxFS header from preload buffer to determine total filesystem size
- Report
total_sizefromsize()instead ofpreload.len() - For reads beyond preload: attempt to open and read from
/scheme/disk/0 - Keep overlay for writes
- Keep block_size = 512 (from P6)
- Add env var
DISK_PHYS_BLOCKfor the partition offset on the physical disk
Step 2: Reduce bootloader preload cap (bootloader patch)
Create local/patches/bootloader/P1-reduce-live-preload.patch:
- Change
max_preloadfrom 1024 MiB to a calculated minimum:- Read the RedoxFS header to determine filesystem size
- Calculate the minimum preload needed: max(header + allocator extent, 4 MiB)
- Cap at 128 MiB (generous upper bound for metadata region)
- Add
DISK_PHYS_BLOCKenv var so lived knows where the partition starts on disk
Step 3: Verify
- Build and test redbear-full ISO in QEMU with virtio-gpu
- Verify RedoxFS mounts the full 4093 MiB filesystem
- Verify login prompt appears
- Verify KDE desktop loads (or at minimum, the greeter starts)
6. Risk Assessment
| Risk | Impact | Mitigation |
|---|---|---|
| RedoxFS header format changes between versions | lived parses header incorrectly | Use the same header parsing code as RedoxFS lib |
| ahcid not started when lived first needs disk | Read fails with ENOENT | Retry with backoff; RedoxFS mount retries automatically |
| Physical disk block offset wrong | Read corrupt data | Pass exact block offset from bootloader via env var |
| Preload too small for RedoxFS to find header | Mount fails immediately | Keep minimum preload at 4 MiB (covers any superblock) |
| Mini ISO regression | Small images broken | Test mini ISO after every change |
7. Alternative Approach: Mount From Physical Disk Directly
Instead of fixing lived, we could modify the init sequence to skip disk.live entirely
for the root filesystem mount:
- Bootloader preloads just enough for kernel + initfs (no change needed)
- lived starts but is only used for early boot I/O
50_rootfs.serviceis changed to explicitly mount from/scheme/disk/0(via ahcid) instead of scanning all disk schemes- This requires passing the disk path and block offset from bootloader to init
Pros: Simpler lived (no disk fallback), cleaner architecture Cons: Requires knowing which disk scheme serves the boot device; may not work if the AHCI driver assigns a different number to the boot disk
Verdict: Fix 3 (lived with disk fallback) is more robust because it works regardless of which disk scheme is assigned. The lived approach acts as a transparent cache layer.
8. Implementation Notes
Bootloader env vars (current)
DISK_LIVE_ADDR=<hex phys addr of preload buffer>
DISK_LIVE_SIZE=<hex size of preload buffer>
REDOXFS_BLOCK=0 (always 0 for live mode)
REDOXFS_UUID=<uuid>
Bootloader env vars (proposed additions)
DISK_PHYS_BLOCK=<hex block offset of partition on physical disk>
REDOXFS_FULL_SIZE=<hex total filesystem size>
lived env vars (current)
DISK_LIVE_ADDR → phys addr to mmap
DISK_LIVE_SIZE → size to mmap (= preload size, NOT total filesystem size)
lived env vars (proposed)
DISK_LIVE_ADDR → phys addr of preload buffer
DISK_LIVE_SIZE → size of preload buffer
DISK_PHYS_BLOCK → block offset for disk fallback reads
REDOXFS_FULL_SIZE → total filesystem size (for size() reporting)