From 1b785fea6a7e31ca75667d3ab302441032ce7594 Mon Sep 17 00:00:00 2001 From: Vasilito Date: Fri, 17 Apr 2026 22:21:08 +0100 Subject: [PATCH] Fix GRUB reassessment findings: clean-build gap, validation, robustness - Add grub package to redbear-full-grub.toml so make all works from a clean tree (the installer needs grub.efi before it runs) - Fix stat -f%z (macOS-only) to stat -c%s (Linux) in GRUB recipe - Normalize bootloader config value to lowercase in install_inner so bootloader = "GRUB" from config files is accepted - Add bad-cluster marker (0x0FFFFFF7) check in fat_tool.py _next_cluster to prevent potential infinite loops on degraded media - Fix file handle leak in fat_tool.py if _read_bpb raises - Clean up temp directory in fetch_bootloaders on error - Update AGENTS.md with GRUB recipe and installer documentation - Update GRUB plan with clean-build prerequisite note --- config/redbear-full-grub.toml | 5 + local/AGENTS.md | 75 ++++++++++++-- local/docs/GRUB-INTEGRATION-PLAN.md | 5 +- local/patches/installer/redox.patch | 153 ++++++++++++++++++---------- local/recipes/core/grub/recipe.toml | 2 +- local/scripts/fat_tool.py | 13 ++- 6 files changed, 189 insertions(+), 64 deletions(-) diff --git a/config/redbear-full-grub.toml b/config/redbear-full-grub.toml index 471a2e53..c8befa64 100644 --- a/config/redbear-full-grub.toml +++ b/config/redbear-full-grub.toml @@ -12,3 +12,8 @@ include = ["redbear-full.toml"] [general] bootloader = "grub" efi_partition_size = 16 + +# GRUB recipe must be built before the installer runs. +# Including it here ensures `make all` builds it as part of the normal package set. +[packages] +grub = {} diff --git a/local/AGENTS.md b/local/AGENTS.md index a77c2bb1..91cbe00a 100644 --- a/local/AGENTS.md +++ b/local/AGENTS.md @@ -142,7 +142,7 @@ redox-master/ ← git pull updates mainline Redox │ ├── AGENTS.md ← This file │ ├── config/ ← Legacy configs (my-*, gitignored) │ ├── recipes/ -│ │ ├── core/ ← ext4d (ext4 filesystem scheme daemon + mkfs tool) +│ │ ├── core/ ← ext4d (ext4 filesystem scheme daemon + mkfs tool), grub (GRUB 2.12 UEFI bootloader) │ │ ├── branding/ ← redbear-release (os-release, hostname, motd) │ │ ├── drivers/ ← redox-driver-sys, linux-kpi (GPU/Wi-Fi compat only — NOT USB) │ │ ├── gpu/ ← redox-drm (AMD + Intel display drivers), amdgpu (C port) @@ -156,7 +156,7 @@ redox-master/ ← git pull updates mainline Redox │ │ ├── base/ ← Base patches (acpid fixes, power methods, pcid /config endpoint) │ │ ├── relibc/ ← relibc compatibility overlays still needed beyond upstream (eventfd, signalfd, timerfd, waitid, SysV IPC) │ │ ├── bootloader/ ← Bootloader patches -│ │ └── installer/ ← Installer patches (ext4 filesystem support) +│ │ └── installer/ ← Installer patches (ext4 filesystem support + GRUB bootloader) │ ├── Assets/ ← Branding assets (icon, loading background) │ │ └── images/ ← Red Bear OS icon (1254x1254) + loading bg (1536x1024) │ ├── firmware/ ← GPU firmware blobs (gitignored, fetched) @@ -265,6 +265,13 @@ make all CONFIG_NAME=redbear-desktop ./target/release/repo cook local/recipes/branding/redbear-release ./target/release/repo cook local/recipes/system/redbear-meta ./target/release/repo cook local/recipes/core/ext4d +./target/release/repo cook local/recipes/core/grub # GRUB bootloader (host build, produces EFI binary) + +# GRUB boot manager (installer-native, Phase 2): +make r.grub # Build GRUB recipe +make all CONFIG_NAME=redbear-full-grub # Build with GRUB chainload +# Or post-build script (Phase 1): +./local/scripts/install-grub.sh build/x86_64/harddrive.img # Modify existing image ``` ## TRACKING MAINLINE CHANGES @@ -280,7 +287,7 @@ When mainline updates affect our work: | Mesa | OpenGL/Vulkan backend changes | `recipes/libs/mesa/` | | Build system | Makefile/config changes | `mk/`, `src/` | | rsext4 | ext4 crate API changes | `local/recipes/core/ext4d/source/` Cargo.toml | -| Installer | ext4 dispatch, filesystem selection | `local/patches/installer/redox.patch` | +| Installer | ext4 dispatch, filesystem selection, GRUB bootloader | `local/patches/installer/redox.patch` | | Quirks | New Linux quirk entries to port | `local/recipes/drivers/redox-driver-sys/source/src/quirks/` | ## PLANNING NOTES @@ -318,12 +325,13 @@ Do not present USB, Wi-Fi, Bluetooth, or low-level controller work as optional o ## FILESYSTEMS -Red Bear OS supports two filesystems: +Red Bear OS supports three filesystems: | Filesystem | Implementation | Package | Status | |------------|---------------|---------|--------| | RedoxFS | Mainline Redox (default) | `recipes/core/redoxfs` | ✅ Stable | | ext4 | rsext4 0.3 crate + ext4d scheme daemon | `local/recipes/core/ext4d` | ✅ Compiles + Installer wired | +| FAT (VFAT) | fatfs 0.3.6 crate + fatd scheme daemon | `local/recipes/core/fatd` | ✅ Compiles + Tools tested + label write verified | ### ext4 Workspace (`local/recipes/core/ext4d/source/`) @@ -369,23 +377,76 @@ recipes/core/ext4d → local/recipes/core/ext4d - `libredox = "0.1.13"` — High-level Redox syscalls (open, read, write, fstat) - `redox-path = "0.3.0"` — Redox path utilities -### Installer ext4 Integration (`local/patches/installer/redox.patch`) +### Installer ext4 + GRUB Integration (`local/patches/installer/redox.patch`) -The mainline installer is patched to support ext4 as an install target filesystem: +The mainline installer is patched to support ext4 as an install target filesystem and +GRUB as an alternative boot manager: - `GeneralConfig.filesystem: Option` — TOML field, accepts `"redoxfs"` (default) or `"ext4"` +- `GeneralConfig.bootloader: Option` — TOML field, accepts `"redox"` (default) or `"grub"` - `FilesystemType` enum — dispatch tag used by `install_inner` - `with_whole_disk_ext4()` — GPT partition layout + ext4 mkfs + file sync (mirrors `with_whole_disk`) - `Ext4SliceDisk` — adapts `DiskWrapper` to rsext4's `BlockDevice` trait - `sync_host_dir_to_ext4()` — copies staged sysroot files into ext4 filesystem -- CLI flag: `--filesystem ext4` or `--filesystem redoxfs` +- GRUB chainload: when `bootloader = "grub"`, writes GRUB EFI + grub.cfg to ESP alongside Redox bootloader +- CLI flags: `--filesystem ext4` / `--bootloader grub` Usage in config TOML: ```toml [general] filesystem = "ext4" # "redoxfs" is default +bootloader = "grub" # "redox" is default +efi_partition_size = 16 # Required for GRUB (default 1 MiB is too small) filesystem_size = 10240 # MB ``` +See `local/docs/GRUB-INTEGRATION-PLAN.md` for the full GRUB architecture and usage guide. + +### FAT (VFAT) Workspace (`local/recipes/core/fatd/source/`) + +``` +fatd/source/ +├── Cargo.toml ← Workspace: fat-blockdev, fatd, fat-mkfs, fat-label, fat-check +├── fat-blockdev/ ← Block device adapter for fatfs crate +│ ├── src/lib.rs ← Re-exports: FileDisk (always), RedoxDisk (feature-gated) +│ ├── src/file_disk.rs ← FileDisk: std::fs::File → Read+Write+Seek +│ └── src/redox_disk.rs ← RedoxDisk: libredox → Read+Write+Seek (redox feature) +├── fatd/ ← FAT filesystem scheme daemon (Redox userspace) +│ ├── src/main.rs ← Daemon: fork, SIGTERM, dispatch to FileDisk/RedoxDisk +│ ├── src/mount.rs ← Scheme event loop (redox_scheme::SchemeSync) +│ ├── src/scheme.rs ← FatScheme: full FSScheme (open/read/write/mkdir/unlink/stat...) +│ └── src/handle.rs ← FileHandle, DirectoryHandle, Handle types +├── fat-mkfs/ ← mkfs.fat equivalent (create FAT12/16/32 filesystems) +│ └── src/main.rs +├── fat-label/ ← fatlabel equivalent (read + write volume labels via BPB) +│ └── src/main.rs ← `-s "LABEL"` writes label at BPB offset 43/71; verifies round-trip +└── fat-check/ ← fsck.fat equivalent (verify BPB, FAT chains, directory tree + safe repair) + └── src/main.rs ← `--repair` clears dirty flag, fixes FSInfo, reclaims lost clusters +``` + +**Architecture**: `fatd` is a Redox scheme daemon using `fatfs` v0.3.6 (MIT, no_std capable). +FAT is for data volumes and ESP only — NOT for root filesystem. +`fscommon::BufStream` wraps block device for mandatory caching. + +**Recipe**: Symlinked into mainline search path: +``` +recipes/core/fatd → ../../local/recipes/core/fatd +``` + +**Config**: Packages included via `config/redbear-device-services.toml` (inherited by +`redbear-desktop.toml` and `redbear-full.toml`). Init service at +`/usr/lib/init.d/15_fatd.service`. + +**Dependencies**: fatfs 0.3.6, fscommon 0.1.1, redox_syscall, redox-scheme, libredox, libc + +**Tool verification status** (2026-04-17): +- `fat-mkfs`: ✅ Creates FAT12/16/32, labels, auto-detection, tested up to 1GB +- `fat-label`: ✅ Reads labels; writes BPB + creates/updates root-directory volume-label entry; verifies round-trip on all FAT types (including previously unlabeled volumes) +- `fat-check`: ✅ BPB validation, boot signature check, directory tree walk, cluster stats; ✅ safe repair (dirty flag, FSInfo, lost clusters, orphaned LFN). Handles 0xFFFFFFFF FSInfo sentinel on fresh images. +- `fatd`: ✅ Compiles (links on Redox target only — expected). NOT runtime-tested (requires QEMU/bare metal). +- Phase 4 (runtime auto-mount): Deferred to runtime validation. Static init service exists. +- Known limitation: fatfs v0.3.6 strictly requires `total_sectors_16 == 0` for FAT32, rejecting some Linux `mkfs.fat` images +- `cargo test`: 0 unit tests (all testing done via integration tests with disk images) + ## BRANDING ASSETS Red Bear OS visual identity files live in `local/Assets/`. diff --git a/local/docs/GRUB-INTEGRATION-PLAN.md b/local/docs/GRUB-INTEGRATION-PLAN.md index f3bf04ad..70876d5b 100644 --- a/local/docs/GRUB-INTEGRATION-PLAN.md +++ b/local/docs/GRUB-INTEGRATION-PLAN.md @@ -1,7 +1,10 @@ # GRUB Integration Plan — Red Bear OS **Date:** 2026-04-17 -**Status:** Phase 2 complete — installer-native GRUB support implemented and compiling +**Status:** Phase 2 complete — installer-native GRUB support implemented and compiling. +**Prerequisite:** The `grub` package must be in the build plan (included in `redbear-full-grub.toml`) +for `make all` to work from a clean tree. Phase 2 is automatic only after `grub` is available in +the local repo or build output. **Approach:** Option A — GRUB as boot manager, chainloading Redox bootloader ## Overview diff --git a/local/patches/installer/redox.patch b/local/patches/installer/redox.patch index 8c7a7f85..8b001440 100644 --- a/local/patches/installer/redox.patch +++ b/local/patches/installer/redox.patch @@ -180,7 +180,7 @@ index 417ff2d..4ad2202 100644 } } diff --git a/src/installer.rs b/src/installer.rs -index 4e077a9..a6f4e84 100644 +index 4e077a9..b11d5b8 100644 --- a/src/installer.rs +++ b/src/installer.rs @@ -3,6 +3,13 @@ use anyhow::{bail, Result}; @@ -467,61 +467,109 @@ index 4e077a9..a6f4e84 100644 let bootloader_dir = PathBuf::from(format!("/tmp/redox_installer_bootloader_{}", process::id())); -@@ -493,6 +741,20 @@ pub fn fetch_bootloaders( +@@ -491,36 +739,75 @@ pub fn fetch_bootloaders( - let mut bootloader_config = Config::bootloader_config(); - bootloader_config.general = config.general.clone(); -+ -+ let use_grub = config -+ .general -+ .bootloader -+ .as_deref() -+ .unwrap_or("redox") -+ .eq_ignore_ascii_case("grub"); -+ -+ if use_grub { -+ bootloader_config -+ .packages -+ .insert("grub".to_string(), Default::default()); -+ } -+ - install_packages(&bootloader_config, &bootloader_dir, cookbook)?; + fs::create_dir(&bootloader_dir)?; - let boot_dir = bootloader_dir.join("usr/lib/boot"); -@@ -518,9 +780,31 @@ pub fn fetch_bootloaders( - Vec::new() - }; - -+ let grub_efi_data = if use_grub { -+ let grub_path = boot_dir.join("grub.efi"); -+ if grub_path.exists() { -+ Some(fs::read(grub_path)?) -+ } else { -+ bail!("GRUB mode requested (bootloader = \"grub\") but grub.efi not found in package output. Build the GRUB recipe first: make r.grub"); +- let mut bootloader_config = Config::bootloader_config(); +- bootloader_config.general = config.general.clone(); +- install_packages(&bootloader_config, &bootloader_dir, cookbook)?; ++ let result = (|| -> Result<_> { ++ let mut bootloader_config = Config::bootloader_config(); ++ bootloader_config.general = config.general.clone(); ++ ++ let use_grub = config ++ .general ++ .bootloader ++ .as_deref() ++ .unwrap_or("redox") ++ .eq_ignore_ascii_case("grub"); ++ ++ if use_grub { ++ bootloader_config ++ .packages ++ .insert("grub".to_string(), Default::default()); + } -+ } else { -+ None -+ }; -+ -+ let grub_cfg_data = if use_grub { -+ let cfg_path = boot_dir.join("grub.cfg"); -+ if cfg_path.exists() { -+ Some(fs::read(cfg_path)?) + +- let boot_dir = bootloader_dir.join("usr/lib/boot"); +- let bios_path = boot_dir.join(if live { +- "bootloader-live.bios" +- } else { +- "bootloader.bios" +- }); +- let efi_path = boot_dir.join(if live { +- "bootloader-live.efi" +- } else { +- "bootloader.efi" +- }); ++ install_packages(&bootloader_config, &bootloader_dir, cookbook)?; + +- let bios_data = if bios_path.exists() { +- fs::read(bios_path)? +- } else { +- Vec::new() +- }; +- let efi_data = if efi_path.exists() { +- fs::read(efi_path)? +- } else { +- Vec::new() +- }; ++ let boot_dir = bootloader_dir.join("usr/lib/boot"); ++ let bios_path = boot_dir.join(if live { ++ "bootloader-live.bios" + } else { -+ bail!("GRUB mode requested (bootloader = \"grub\") but grub.cfg not found in package output. Build the GRUB recipe first: make r.grub"); -+ } -+ } else { -+ None -+ }; -+ - fs::remove_dir_all(&bootloader_dir)?; ++ "bootloader.bios" ++ }); ++ let efi_path = boot_dir.join(if live { ++ "bootloader-live.efi" ++ } else { ++ "bootloader.efi" ++ }); + +- fs::remove_dir_all(&bootloader_dir)?; ++ let bios_data = if bios_path.exists() { ++ fs::read(bios_path)? ++ } else { ++ Vec::new() ++ }; ++ let efi_data = if efi_path.exists() { ++ fs::read(efi_path)? ++ } else { ++ Vec::new() ++ }; - Ok((bios_data, efi_data)) -+ Ok((bios_data, efi_data, grub_efi_data, grub_cfg_data)) ++ let grub_efi_data = if use_grub { ++ let grub_path = boot_dir.join("grub.efi"); ++ if grub_path.exists() { ++ Some(fs::read(grub_path)?) ++ } else { ++ bail!("GRUB mode requested (bootloader = \"grub\") but grub.efi not found in package output. Build the GRUB recipe first: make r.grub"); ++ } ++ } else { ++ None ++ }; ++ ++ let grub_cfg_data = if use_grub { ++ let cfg_path = boot_dir.join("grub.cfg"); ++ if cfg_path.exists() { ++ Some(fs::read(cfg_path)?) ++ } else { ++ bail!("GRUB mode requested (bootloader = \"grub\") but grub.cfg not found in package output. Build the GRUB recipe first: make r.grub"); ++ } ++ } else { ++ None ++ }; ++ ++ Ok((bios_data, efi_data, grub_efi_data, grub_cfg_data)) ++ })(); ++ ++ let _ = fs::remove_dir_all(&bootloader_dir); ++ result } //TODO: make bootloaders use Option, dynamically create BIOS and EFI partitions -@@ -683,20 +967,58 @@ where +@@ -683,20 +970,58 @@ where eprintln!("Creating EFI directory"); let root_dir = fs.root_dir(); root_dir.create_dir("EFI")?; @@ -592,7 +640,7 @@ index 4e077a9..a6f4e84 100644 } // Format and install RedoxFS partition -@@ -712,6 +1034,222 @@ where +@@ -712,6 +1037,222 @@ where with_redoxfs(disk_redoxfs, disk_option.password_opt, callback) } @@ -815,20 +863,21 @@ index 4e077a9..a6f4e84 100644 #[cfg(not(target_os = "redox"))] pub fn try_fast_install( _fs: &mut redoxfs::FileSystem, -@@ -801,6 +1339,23 @@ pub fn try_fast_install( +@@ -801,6 +1342,24 @@ pub fn try_fast_install( fn install_inner(config: Config, output: &Path) -> Result<()> { println!("Installing to {}:\n{}", output.display(), config); + + if let Some(ref bl) = config.general.bootloader { -+ match bl.as_str() { ++ let bl_lower = bl.to_lowercase(); ++ match bl_lower.as_str() { + "redox" | "grub" => {} + other => bail!( + "Unknown bootloader '{}': expected \"redox\" or \"grub\"", + other + ), + } -+ if bl.eq_ignore_ascii_case("grub") { ++ if bl_lower == "grub" { + let efi_size = config.general.efi_partition_size.unwrap_or(1); + if efi_size < 8 { + bail!("GRUB bootloader requires efi_partition_size >= 8 MiB (got {} MiB). Add efi_partition_size = 16 to your config.", efi_size); @@ -839,7 +888,7 @@ index 4e077a9..a6f4e84 100644 let cookbook = config.general.cookbook.clone(); let cookbook = cookbook.as_ref().map(|p| p.as_str()); if output.is_dir() { -@@ -823,28 +1378,41 @@ fn install_inner(config: Config, output: &Path) -> Result<()> { +@@ -823,28 +1382,41 @@ fn install_inner(config: Config, output: &Path) -> Result<()> { let live = config.general.live_disk.unwrap_or(false); let password_opt = config.general.encrypt_disk.clone(); let password_opt = password_opt.as_ref().map(|p| p.as_bytes()); diff --git a/local/recipes/core/grub/recipe.toml b/local/recipes/core/grub/recipe.toml index d8ace826..601829b4 100644 --- a/local/recipes/core/grub/recipe.toml +++ b/local/recipes/core/grub/recipe.toml @@ -117,7 +117,7 @@ if [ ! -f "${COOKBOOK_STAGE}/usr/lib/boot/grub.efi" ]; then exit 1 fi -GRUB_SIZE=$(stat -f%z "${COOKBOOK_STAGE}/usr/lib/boot/grub.efi" 2>/dev/null || stat -c%s "${COOKBOOK_STAGE}/usr/lib/boot/grub.efi" 2>/dev/null || echo "unknown") +GRUB_SIZE=$(stat -c%s "${COOKBOOK_STAGE}/usr/lib/boot/grub.efi" 2>/dev/null || echo "unknown") echo "GRUB EFI image size: ${GRUB_SIZE} bytes" # Copy default GRUB configuration from recipe directory. diff --git a/local/scripts/fat_tool.py b/local/scripts/fat_tool.py index d9c6336d..1e058fff 100644 --- a/local/scripts/fat_tool.py +++ b/local/scripts/fat_tool.py @@ -47,8 +47,12 @@ class Fat32: self.f = open(image_path, "r+b") self.image_size = os.fstat(self.f.fileno()).st_size self.offset = offset - self._read_bpb() - self._load_fat() + try: + self._read_bpb() + self._load_fat() + except: + self.f.close() + raise def _read_bpb(self): self.f.seek(self.offset) @@ -118,7 +122,10 @@ class Fat32: if not 2 <= cluster <= self.max_cluster: raise RuntimeError(f"FAT32: invalid cluster {cluster}") idx = cluster * 4 - return read_le32(self.fat, idx) & 0x0FFFFFFF + val = read_le32(self.fat, idx) & 0x0FFFFFFF + if val == 0x0FFFFFF7: + raise RuntimeError(f"FAT32: bad cluster marker at {cluster}") + return val def _set_fat(self, cluster, value): write_le32(self.fat, cluster * 4, value & 0x0FFFFFFF)