2e764746e7
5-phase hardening to prevent silent file-layer collisions (the D-Bus regression class): Phase 1: lint-config-paths.sh + make lint-config in depends.mk Phase 2: CollisionTracker in installer (content-hash comparison) Phase 3: installs manifests in recipe.toml + validate-file-ownership.sh Phase 4: validate-init-services.sh + make validate in disk.mk Phase 5: documentation (AGENTS.md, BUILD-SYSTEM-HARDENING-PLAN.md) Both redbear-mini and redbear-full build and validate clean. 66 declared install paths in base, zero conflicts.
1334 lines
45 KiB
Diff
1334 lines
45 KiB
Diff
diff --git a/Cargo.lock b/Cargo.lock
|
|
index 939ae14..50bdb63 100644
|
|
--- a/Cargo.lock
|
|
+++ b/Cargo.lock
|
|
@@ -489,6 +489,14 @@ version = "1.0.2"
|
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
|
|
|
+[[package]]
|
|
+name = "ext4-blockdev"
|
|
+version = "0.1.0"
|
|
+dependencies = [
|
|
+ "log",
|
|
+ "rsext4",
|
|
+]
|
|
+
|
|
[[package]]
|
|
name = "fatfs"
|
|
version = "0.3.6"
|
|
@@ -995,6 +1003,9 @@ name = "lazy_static"
|
|
version = "1.5.0"
|
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
|
+dependencies = [
|
|
+ "spin",
|
|
+]
|
|
|
|
[[package]]
|
|
name = "libc"
|
|
@@ -1395,6 +1406,7 @@ version = "0.2.42"
|
|
dependencies = [
|
|
"anyhow",
|
|
"arg_parser",
|
|
+ "ext4-blockdev",
|
|
"fatfs",
|
|
"fscommon",
|
|
"gpt",
|
|
@@ -1408,6 +1420,7 @@ dependencies = [
|
|
"redox_syscall",
|
|
"redoxfs",
|
|
"ring",
|
|
+ "rsext4",
|
|
"rust-argon2",
|
|
"serde",
|
|
"serde_derive",
|
|
@@ -1538,6 +1551,17 @@ dependencies = [
|
|
"windows-sys 0.52.0",
|
|
]
|
|
|
|
+[[package]]
|
|
+name = "rsext4"
|
|
+version = "0.3.0"
|
|
+source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
+checksum = "7ecc26a7a22732f5cd14906e3cd81325ced46e8941d93db8cb522ea084fd351d"
|
|
+dependencies = [
|
|
+ "bitflags 2.10.0",
|
|
+ "lazy_static",
|
|
+ "log",
|
|
+]
|
|
+
|
|
[[package]]
|
|
name = "rust-argon2"
|
|
version = "3.0.0"
|
|
diff --git a/Cargo.toml b/Cargo.toml
|
|
index e3c6700..b1d5d72 100644
|
|
--- a/Cargo.toml
|
|
+++ b/Cargo.toml
|
|
@@ -25,6 +25,7 @@ path = "src/lib.rs"
|
|
[dependencies]
|
|
anyhow = "1"
|
|
arg_parser = "0.1.0"
|
|
+ext4-blockdev = { path = "../../../../local/recipes/core/ext4d/source/ext4-blockdev", optional = true, default-features = false }
|
|
fatfs = { version = "0.3.0", optional = true }
|
|
fscommon = { version = "0.1.1", optional = true }
|
|
gpt = { version = "3.0.0", optional = true }
|
|
@@ -36,6 +37,7 @@ rand = { version = "0.9", optional = true }
|
|
redox-pkg = { version = "0.3.1", features = ["indicatif"], optional = true }
|
|
redox_syscall = { version = "0.7", optional = true }
|
|
redoxfs = { version = "0.9", optional = true, default-features = false, features = ["std", "log"] }
|
|
+rsext4 = { version = "0.3", optional = true }
|
|
rust-argon2 = { version = "3", optional = true }
|
|
serde = "1"
|
|
serde_derive = "1.0"
|
|
@@ -63,6 +65,7 @@ installer = [
|
|
"redox_syscall",
|
|
"redoxfs",
|
|
"ring",
|
|
+ "rsext4",
|
|
"rust-argon2",
|
|
"termion",
|
|
"uuid",
|
|
diff --git a/src/bin/installer.rs b/src/bin/installer.rs
|
|
index c3ce487..7d3139b 100644
|
|
--- a/src/bin/installer.rs
|
|
+++ b/src/bin/installer.rs
|
|
@@ -20,6 +20,8 @@ Using redox_installer as an installer:
|
|
<diskpath.img> Disk file to write
|
|
--config Path to filesystem config TOML
|
|
--write-bootloader Path to write UEFI bootloader to in addition to the embedded ESP
|
|
+ --bootloader Boot manager: "redox" (default) or "grub"
|
|
+ --filesystem Filesystem type: "redoxfs" (default) or "ext4"
|
|
--skip-partition Skip writing GPT partition tables
|
|
Use this only if you plan to use other partition tool
|
|
--live Use bootloader configured for live disk
|
|
@@ -31,6 +33,10 @@ Using redox_installer as a configuration parser:
|
|
--list-packages List packages will be installed
|
|
--filesystem-size Output filesystem size in MB
|
|
--output-config Path to write the parsed config as another TOML
|
|
+
|
|
+For GRUB installation with Linux-compatible CLI, use:
|
|
+ local/scripts/grub-install --target=x86_64-efi --disk-image=build/x86_64/harddrive.img
|
|
+ local/scripts/grub-mkconfig -o local/recipes/core/grub/grub.cfg
|
|
"#;
|
|
|
|
fn main() {
|
|
@@ -39,6 +45,8 @@ fn main() {
|
|
.add_opt("c", "config")
|
|
.add_opt("o", "output-config")
|
|
.add_opt("", "write-bootloader")
|
|
+ .add_opt("", "filesystem")
|
|
+ .add_opt("", "bootloader")
|
|
.add_flag(&["skip-partition"])
|
|
.add_flag(&["filesystem-size"])
|
|
.add_flag(&["r", "repo-binary"]) // TODO: Remove
|
|
@@ -116,6 +124,12 @@ fn main() {
|
|
if parser.found("no-mount") {
|
|
config.general.no_mount = Some(true);
|
|
}
|
|
+ if let Some(fs_type) = parser.get_opt("filesystem") {
|
|
+ config.general.filesystem = Some(fs_type);
|
|
+ }
|
|
+ if let Some(bl) = parser.get_opt("bootloader") {
|
|
+ config.general.bootloader = Some(bl.to_lowercase());
|
|
+ }
|
|
let write_bootloader = parser.get_opt("write-bootloader");
|
|
if write_bootloader.is_some() {
|
|
config.general.write_bootloader = write_bootloader;
|
|
diff --git a/src/bin/installer_tui.rs b/src/bin/installer_tui.rs
|
|
index 2739983..0d2d6f3 100644
|
|
--- a/src/bin/installer_tui.rs
|
|
+++ b/src/bin/installer_tui.rs
|
|
@@ -2,7 +2,9 @@ use anyhow::{anyhow, bail, Result};
|
|
use pkgar::{ext::EntryExt, PackageHead};
|
|
use pkgar_core::PackageSrc;
|
|
use pkgar_keys::PublicKeyFile;
|
|
-use redox_installer::{try_fast_install, with_redoxfs_mount, with_whole_disk, Config, DiskOption};
|
|
+use redox_installer::{
|
|
+ try_fast_install, with_redoxfs_mount, with_whole_disk, Config, DiskOption, FilesystemType,
|
|
+};
|
|
use std::{
|
|
ffi::OsStr,
|
|
fs,
|
|
@@ -316,8 +318,11 @@ fn main() {
|
|
bootloader_bios: &bootloader_bios,
|
|
bootloader_efi: &bootloader_efi,
|
|
password_opt: password_opt.as_ref().map(|x| x.as_bytes()),
|
|
+ filesystem_type: FilesystemType::RedoxFS,
|
|
efi_partition_size: None,
|
|
skip_partitions: false, // TODO?
|
|
+ grub_efi: None,
|
|
+ grub_config: None,
|
|
};
|
|
let res = with_whole_disk(&disk_path, &disk_option, |mut fs| {
|
|
// Fast install method via filesystem clone
|
|
diff --git a/src/config/general.rs b/src/config/general.rs
|
|
index 417ff2d..fab677c 100644
|
|
--- a/src/config/general.rs
|
|
+++ b/src/config/general.rs
|
|
@@ -6,7 +6,7 @@ pub struct GeneralConfig {
|
|
pub prompt: Option<bool>,
|
|
/// Total filesystem size in MB
|
|
pub filesystem_size: Option<u32>,
|
|
- /// EFI partition size in MB, default to 2MB
|
|
+ /// EFI partition size in MiB, defaults to 1 MiB when not set
|
|
pub efi_partition_size: Option<u32>,
|
|
/// Skip disk partitioning, assume whole disk is a partition
|
|
pub skip_partitions: Option<bool>,
|
|
@@ -19,6 +19,11 @@ pub struct GeneralConfig {
|
|
/// Use AR to write files instead of FUSE-based mount
|
|
/// (bypasses FUSE, but slower and requires namespaced context such as "podman unshare")
|
|
pub no_mount: Option<bool>,
|
|
+ /// Filesystem type for the install target: "redoxfs" (default) or "ext4"
|
|
+ pub filesystem: Option<String>,
|
|
+ /// Boot manager: "redox" (default, use Redox bootloader directly) or "grub"
|
|
+ /// When "grub", the installer writes GRUB as primary and chainloads Redox bootloader
|
|
+ pub bootloader: Option<String>,
|
|
}
|
|
|
|
impl GeneralConfig {
|
|
@@ -38,5 +43,11 @@ impl GeneralConfig {
|
|
self.write_bootloader = Some(write_bootloader);
|
|
}
|
|
self.no_mount = other.no_mount.or(self.no_mount);
|
|
+ if let Some(filesystem) = other.filesystem {
|
|
+ self.filesystem = Some(filesystem);
|
|
+ }
|
|
+ if let Some(bootloader) = other.bootloader {
|
|
+ self.bootloader = Some(bootloader);
|
|
+ }
|
|
}
|
|
}
|
|
diff --git a/src/installer.rs b/src/installer.rs
|
|
index 4e077a9..7432444 100644
|
|
--- a/src/installer.rs
|
|
+++ b/src/installer.rs
|
|
@@ -3,11 +3,19 @@ use anyhow::{bail, Result};
|
|
use pkg::Library;
|
|
use rand::{rngs::OsRng, TryRngCore};
|
|
use redoxfs::{unmount_path, Disk, DiskIo, FileSystem, BLOCK_SIZE};
|
|
+use rsext4::bmalloc::AbsoluteBN;
|
|
+use rsext4::{
|
|
+ chmod as ext4_chmod, chown as ext4_chown, create_symbol_link as ext4_create_symbol_link,
|
|
+ mkdir as ext4_mkdir, mkfile as ext4_mkfile, mkfs as ext4_mkfs, mount as ext4_mount,
|
|
+ umount as ext4_umount, BlockDevice, Ext4Error, Ext4FileSystem, Ext4Result, Ext4Timestamp,
|
|
+ Jbd2Dev,
|
|
+};
|
|
use termion::input::TermRead;
|
|
|
|
use crate::config::file::FileConfig;
|
|
use crate::config::package::PackageConfig;
|
|
use crate::config::Config;
|
|
+use crate::collision::CollisionTracker;
|
|
use crate::disk_wrapper::DiskWrapper;
|
|
|
|
use std::{
|
|
@@ -23,12 +31,104 @@ use std::{
|
|
time::{SystemTime, UNIX_EPOCH},
|
|
};
|
|
|
|
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
+pub enum FilesystemType {
|
|
+ RedoxFS,
|
|
+ Ext4,
|
|
+}
|
|
+
|
|
pub struct DiskOption<'a> {
|
|
pub bootloader_bios: &'a [u8],
|
|
pub bootloader_efi: &'a [u8],
|
|
pub password_opt: Option<&'a [u8]>,
|
|
+ pub filesystem_type: FilesystemType,
|
|
pub efi_partition_size: Option<u32>, //MiB
|
|
pub skip_partitions: bool,
|
|
+ pub grub_efi: Option<&'a [u8]>,
|
|
+ pub grub_config: Option<&'a [u8]>,
|
|
+}
|
|
+
|
|
+struct Ext4SliceDisk<T> {
|
|
+ device: T,
|
|
+ total_blocks: u64,
|
|
+ block_size: u32,
|
|
+}
|
|
+
|
|
+impl<T> Ext4SliceDisk<T> {
|
|
+ fn new(device: T, size: u64, block_size: u32) -> Self {
|
|
+ Self {
|
|
+ device,
|
|
+ total_blocks: size / block_size as u64,
|
|
+ block_size,
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+impl<T> BlockDevice for Ext4SliceDisk<T>
|
|
+where
|
|
+ T: io::Read + Seek + Write,
|
|
+{
|
|
+ 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::buffer_too_small(buffer.len(), total));
|
|
+ }
|
|
+
|
|
+ self.device
|
|
+ .seek(SeekFrom::Start(offset))
|
|
+ .map_err(|_| Ext4Error::io())?;
|
|
+ self.device
|
|
+ .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;
|
|
+ let total = count as usize * self.block_size as usize;
|
|
+ if buffer.len() < total {
|
|
+ return Err(Ext4Error::buffer_too_small(buffer.len(), total));
|
|
+ }
|
|
+
|
|
+ self.device
|
|
+ .seek(SeekFrom::Start(offset))
|
|
+ .map_err(|_| Ext4Error::io())?;
|
|
+ self.device
|
|
+ .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 current_time(&self) -> Ext4Result<Ext4Timestamp> {
|
|
+ let now = SystemTime::now()
|
|
+ .duration_since(UNIX_EPOCH)
|
|
+ .map_err(|_| Ext4Error::io())?;
|
|
+ Ok(Ext4Timestamp::new(
|
|
+ now.as_secs().try_into().map_err(|_| Ext4Error::io())?,
|
|
+ now.subsec_nanos(),
|
|
+ ))
|
|
+ }
|
|
+
|
|
+ fn block_size(&self) -> u32 {
|
|
+ self.block_size
|
|
+ }
|
|
+
|
|
+ fn flush(&mut self) -> Ext4Result<()> {
|
|
+ self.device.flush().map_err(|_| Ext4Error::io())
|
|
+ }
|
|
}
|
|
|
|
fn get_target() -> String {
|
|
@@ -136,17 +236,36 @@ pub fn install_dir(
|
|
|
|
let output_dir = output_dir.to_owned();
|
|
|
|
+ let mut tracker = CollisionTracker::new();
|
|
+
|
|
for file in &config.files {
|
|
if !file.postinstall {
|
|
file.create(&output_dir)?;
|
|
+ let path = Path::new(file.path.trim_start_matches('/'));
|
|
+ tracker.record_config_pre_install(path, &output_dir);
|
|
}
|
|
}
|
|
|
|
+ tracker.snapshot_pre_package(&output_dir)?;
|
|
+
|
|
install_packages(&config, &output_dir, cookbook)?;
|
|
|
|
+ let package_names: Vec<&String> = config
|
|
+ .packages
|
|
+ .iter()
|
|
+ .filter_map(|(name, pkg)| match pkg {
|
|
+ PackageConfig::Build(rule) if rule == "ignore" => None,
|
|
+ _ => Some(name),
|
|
+ })
|
|
+ .collect();
|
|
+ tracker.detect_package_overwrites(&output_dir, &package_names)?;
|
|
+ tracker.report()?;
|
|
+
|
|
for file in &config.files {
|
|
if file.postinstall {
|
|
file.create(&output_dir)?;
|
|
+ let path = Path::new(file.path.trim_start_matches('/'));
|
|
+ tracker.record_config_post_install(path, &output_dir);
|
|
}
|
|
}
|
|
|
|
@@ -360,6 +479,155 @@ fn decide_mount_path(mount_path: Option<&Path>) -> PathBuf {
|
|
mount_path
|
|
}
|
|
|
|
+fn ext4_error<E>(err: E) -> anyhow::Error
|
|
+where
|
|
+ E: std::fmt::Display,
|
|
+{
|
|
+ anyhow::anyhow!("{err}")
|
|
+}
|
|
+
|
|
+fn host_path_to_ext4_path(host_root: &Path, path: &Path) -> Result<String> {
|
|
+ let relative = path
|
|
+ .strip_prefix(host_root)
|
|
+ .with_context(|| format!("{} is outside {}", path.display(), host_root.display()))?;
|
|
+ let relative = relative
|
|
+ .to_str()
|
|
+ .with_context(|| format!("{} is not valid UTF-8", path.display()))?;
|
|
+
|
|
+ if relative.is_empty() {
|
|
+ Ok("/".to_string())
|
|
+ } else {
|
|
+ Ok(format!("/{relative}"))
|
|
+ }
|
|
+}
|
|
+
|
|
+fn apply_ext4_metadata<B: BlockDevice>(
|
|
+ metadata: &fs::Metadata,
|
|
+ ext4_path: &str,
|
|
+ disk: &mut Jbd2Dev<B>,
|
|
+ ext4: &mut Ext4FileSystem,
|
|
+) -> Result<()> {
|
|
+ use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
|
+
|
|
+ ext4_chmod(
|
|
+ disk,
|
|
+ ext4,
|
|
+ ext4_path,
|
|
+ (metadata.permissions().mode() & 0o7777) as u16,
|
|
+ )
|
|
+ .map_err(ext4_error)?;
|
|
+ ext4_chown(
|
|
+ disk,
|
|
+ ext4,
|
|
+ ext4_path,
|
|
+ Some(metadata.uid()),
|
|
+ Some(metadata.gid()),
|
|
+ )
|
|
+ .map_err(ext4_error)?;
|
|
+ Ok(())
|
|
+}
|
|
+
|
|
+fn sync_host_dir_entries_to_ext4<B: BlockDevice>(
|
|
+ host_root: &Path,
|
|
+ dir: &Path,
|
|
+ disk: &mut Jbd2Dev<B>,
|
|
+ ext4: &mut Ext4FileSystem,
|
|
+ symlinks: &mut Vec<(String, String)>,
|
|
+) -> Result<()> {
|
|
+ for entry in fs::read_dir(dir)? {
|
|
+ let entry = entry?;
|
|
+ let path = entry.path();
|
|
+ let file_type = entry.file_type()?;
|
|
+ let metadata = fs::symlink_metadata(&path)?;
|
|
+ let ext4_path = host_path_to_ext4_path(host_root, &path)?;
|
|
+
|
|
+ if file_type.is_dir() {
|
|
+ ext4_mkdir(disk, ext4, &ext4_path).map_err(ext4_error)?;
|
|
+ apply_ext4_metadata(&metadata, &ext4_path, disk, ext4)?;
|
|
+ sync_host_dir_entries_to_ext4(host_root, &path, disk, ext4, symlinks)?;
|
|
+ } else if file_type.is_file() {
|
|
+ let data = fs::read(&path)
|
|
+ .with_context(|| format!("Reading staged file {}", path.display()))?;
|
|
+ ext4_mkfile(disk, ext4, &ext4_path, Some(&data), None).map_err(ext4_error)?;
|
|
+ apply_ext4_metadata(&metadata, &ext4_path, disk, ext4)?;
|
|
+ } else if file_type.is_symlink() {
|
|
+ let target = fs::read_link(&path)
|
|
+ .with_context(|| format!("Reading staged symlink {}", path.display()))?;
|
|
+ let target = target
|
|
+ .to_str()
|
|
+ .with_context(|| format!("{} has a non-UTF-8 symlink target", path.display()))?;
|
|
+ symlinks.push((target.to_string(), ext4_path));
|
|
+ }
|
|
+ }
|
|
+
|
|
+ Ok(())
|
|
+}
|
|
+
|
|
+fn sync_host_dir_to_ext4<B: BlockDevice>(
|
|
+ host_root: &Path,
|
|
+ disk: &mut Jbd2Dev<B>,
|
|
+ ext4: &mut Ext4FileSystem,
|
|
+) -> Result<()> {
|
|
+ let mut symlinks = Vec::new();
|
|
+ sync_host_dir_entries_to_ext4(host_root, host_root, disk, ext4, &mut symlinks)?;
|
|
+
|
|
+ for (target, link_path) in symlinks {
|
|
+ ext4_create_symbol_link(disk, ext4, &target, &link_path).map_err(ext4_error)?;
|
|
+ }
|
|
+
|
|
+ Ok(())
|
|
+}
|
|
+
|
|
+pub fn with_ext4_mount<B, T, F>(
|
|
+ mut disk: Jbd2Dev<B>,
|
|
+ mount_path: Option<&Path>,
|
|
+ callback: F,
|
|
+) -> Result<T>
|
|
+where
|
|
+ B: BlockDevice,
|
|
+ F: FnOnce(&Path) -> Result<T>,
|
|
+{
|
|
+ let mount_path = decide_mount_path(mount_path);
|
|
+
|
|
+ if !mount_path.exists() {
|
|
+ fs::create_dir(&mount_path)?;
|
|
+ }
|
|
+
|
|
+ let mut ext4 = match ext4_mount(&mut disk).map_err(ext4_error) {
|
|
+ Ok(ext4) => ext4,
|
|
+ Err(err) => {
|
|
+ if mount_path.exists() {
|
|
+ let _ = fs::remove_dir_all(&mount_path);
|
|
+ }
|
|
+ return Err(err);
|
|
+ }
|
|
+ };
|
|
+
|
|
+ let mut res = callback(&mount_path);
|
|
+
|
|
+ if res.is_ok() {
|
|
+ if let Err(err) = sync_host_dir_to_ext4(&mount_path, &mut disk, &mut ext4) {
|
|
+ res = Err(err);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if let Err(err) = ext4_umount(ext4, &mut disk).map_err(ext4_error) {
|
|
+ if res.is_ok() {
|
|
+ res = Err(err);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if mount_path.exists() {
|
|
+ if let Err(err) = fs::remove_dir_all(&mount_path) {
|
|
+ if res.is_ok() {
|
|
+ res = Err(err.into());
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ res
|
|
+}
|
|
+
|
|
pub fn with_redoxfs_mount<D, T, F>(
|
|
fs: FileSystem<D>,
|
|
mount_path: Option<&Path>,
|
|
@@ -481,7 +749,7 @@ pub fn fetch_bootloaders(
|
|
config: &Config,
|
|
cookbook: Option<&str>,
|
|
live: bool,
|
|
-) -> Result<(Vec<u8>, Vec<u8>)> {
|
|
+) -> Result<(Vec<u8>, Vec<u8>, Option<Vec<u8>>, Option<Vec<u8>>)> {
|
|
let bootloader_dir =
|
|
PathBuf::from(format!("/tmp/redox_installer_bootloader_{}", process::id()));
|
|
|
|
@@ -491,39 +759,78 @@ pub fn fetch_bootloaders(
|
|
|
|
fs::create_dir(&bootloader_dir)?;
|
|
|
|
- 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());
|
|
+ }
|
|
|
|
- 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 {
|
|
+ "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()
|
|
+ };
|
|
+
|
|
+ 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))
|
|
+ })();
|
|
|
|
- Ok((bios_data, efi_data))
|
|
+ let _ = fs::remove_dir_all(&bootloader_dir);
|
|
+ result
|
|
}
|
|
|
|
-//TODO: make bootloaders use Option, dynamically create BIOS and EFI partitions
|
|
+//TODO: dynamically create BIOS and EFI partitions instead of always creating both
|
|
pub fn with_whole_disk<P, F, T>(disk_path: P, disk_option: &DiskOption, callback: F) -> Result<T>
|
|
where
|
|
P: AsRef<Path>,
|
|
@@ -570,7 +877,7 @@ where
|
|
let gpt_reserved = 34 * 512; // GPT always reserves 34 512-byte sectors
|
|
let mibi = 1024 * 1024;
|
|
|
|
- // First megabyte of the disk is reserved for BIOS partition, wich includes GPT tables
|
|
+ // First megabyte of the disk is reserved for BIOS partition, which includes GPT tables
|
|
let bios_start = gpt_reserved / block_size;
|
|
let bios_end = (mibi / block_size) - 1;
|
|
|
|
@@ -672,10 +979,13 @@ where
|
|
fscommon::StreamSlice::new(&mut disk_file, disk_efi_start, disk_efi_end)?;
|
|
|
|
eprintln!(
|
|
- "Formatting EFI partition with size {:#x}",
|
|
+ "Formatting EFI partition as FAT32 (size {:#x})",
|
|
disk_efi_end - disk_efi_start
|
|
);
|
|
- fatfs::format_volume(&mut disk_efi, fatfs::FormatVolumeOptions::new())?;
|
|
+ fatfs::format_volume(
|
|
+ &mut disk_efi,
|
|
+ fatfs::FormatVolumeOptions::new().fat_type(fatfs::FatType::Fat32),
|
|
+ )?;
|
|
|
|
eprintln!("Opening EFI partition");
|
|
let fs = fatfs::FileSystem::new(&mut disk_efi, fatfs::FsOptions::new())?;
|
|
@@ -683,20 +993,58 @@ where
|
|
eprintln!("Creating EFI directory");
|
|
let root_dir = fs.root_dir();
|
|
root_dir.create_dir("EFI")?;
|
|
-
|
|
- eprintln!("Creating EFI/BOOT directory");
|
|
let efi_dir = root_dir.open_dir("EFI")?;
|
|
- efi_dir.create_dir("BOOT")?;
|
|
|
|
- eprintln!(
|
|
- "Writing EFI/BOOT/{} file with size {:#x}",
|
|
- bootloader_efi_name,
|
|
- disk_option.bootloader_efi.len()
|
|
- );
|
|
- let boot_dir = efi_dir.open_dir("BOOT")?;
|
|
- let mut file = boot_dir.create_file(bootloader_efi_name)?;
|
|
- file.truncate()?;
|
|
- file.write_all(&disk_option.bootloader_efi)?;
|
|
+ if let (Some(grub_data), Some(grub_cfg)) = (disk_option.grub_efi, disk_option.grub_config) {
|
|
+ if grub_data.is_empty() {
|
|
+ bail!("GRUB EFI binary is empty (0 bytes). The GRUB recipe may have failed to produce a valid image.");
|
|
+ }
|
|
+ if grub_cfg.is_empty() {
|
|
+ bail!("GRUB configuration is empty (0 bytes). Check that grub.cfg has content.");
|
|
+ }
|
|
+ if disk_option.bootloader_efi.is_empty() {
|
|
+ bail!("Redox bootloader is empty (0 bytes). Cannot set up GRUB chainload without a valid Redox bootloader.");
|
|
+ }
|
|
+
|
|
+ efi_dir.create_dir("BOOT")?;
|
|
+ efi_dir.create_dir("REDBEAR")?;
|
|
+
|
|
+ eprintln!(
|
|
+ "Writing EFI/BOOT/{} (GRUB, {:#x} bytes)",
|
|
+ bootloader_efi_name,
|
|
+ grub_data.len()
|
|
+ );
|
|
+ let boot_dir = efi_dir.open_dir("BOOT")?;
|
|
+ let mut file = boot_dir.create_file(bootloader_efi_name)?;
|
|
+ file.truncate()?;
|
|
+ file.write_all(grub_data)?;
|
|
+
|
|
+ eprintln!("Writing EFI/BOOT/grub.cfg ({:#x} bytes)", grub_cfg.len());
|
|
+ let mut file = boot_dir.create_file("grub.cfg")?;
|
|
+ file.truncate()?;
|
|
+ file.write_all(grub_cfg)?;
|
|
+
|
|
+ eprintln!(
|
|
+ "Writing EFI/REDBEAR/redbear.efi (Redox bootloader, {:#x} bytes)",
|
|
+ disk_option.bootloader_efi.len()
|
|
+ );
|
|
+ let redbear_dir = efi_dir.open_dir("REDBEAR")?;
|
|
+ let mut file = redbear_dir.create_file("redbear.efi")?;
|
|
+ file.truncate()?;
|
|
+ file.write_all(&disk_option.bootloader_efi)?;
|
|
+ } else {
|
|
+ efi_dir.create_dir("BOOT")?;
|
|
+
|
|
+ eprintln!(
|
|
+ "Writing EFI/BOOT/{} file with size {:#x}",
|
|
+ bootloader_efi_name,
|
|
+ disk_option.bootloader_efi.len()
|
|
+ );
|
|
+ let boot_dir = efi_dir.open_dir("BOOT")?;
|
|
+ let mut file = boot_dir.create_file(bootloader_efi_name)?;
|
|
+ file.truncate()?;
|
|
+ file.write_all(&disk_option.bootloader_efi)?;
|
|
+ }
|
|
}
|
|
|
|
// Format and install RedoxFS partition
|
|
@@ -712,6 +1060,225 @@ where
|
|
with_redoxfs(disk_redoxfs, disk_option.password_opt, callback)
|
|
}
|
|
|
|
+pub fn with_whole_disk_ext4<P, F, T>(
|
|
+ disk_path: P,
|
|
+ disk_option: &DiskOption,
|
|
+ callback: F,
|
|
+) -> Result<T>
|
|
+where
|
|
+ P: AsRef<Path>,
|
|
+ F: FnOnce(&Path) -> Result<T>,
|
|
+{
|
|
+ let target = get_target();
|
|
+
|
|
+ let bootloader_efi_name = match target.as_str() {
|
|
+ "aarch64-unknown-redox" => "BOOTAA64.EFI",
|
|
+ "i586-unknown-redox" | "i686-unknown-redox" => "BOOTIA32.EFI",
|
|
+ "x86_64-unknown-redox" => "BOOTX64.EFI",
|
|
+ "riscv64gc-unknown-redox" => "BOOTRISCV64.EFI",
|
|
+ _ => {
|
|
+ bail!("target '{target}' not supported");
|
|
+ }
|
|
+ };
|
|
+
|
|
+ eprintln!("Opening disk {}", disk_path.as_ref().display());
|
|
+
|
|
+ if disk_option.skip_partitions {
|
|
+ let disk_ext4 = Ext4SliceDisk::new(
|
|
+ DiskWrapper::open(disk_path.as_ref())?,
|
|
+ std::fs::metadata(disk_path.as_ref())?.len(),
|
|
+ rsext4::BLOCK_SIZE_U32,
|
|
+ );
|
|
+ let mut jbd = Jbd2Dev::initial_jbd2dev(0, disk_ext4, false);
|
|
+ eprintln!("Formatting whole disk as ext4");
|
|
+ ext4_mkfs(&mut jbd).map_err(ext4_error)?;
|
|
+ return with_ext4_mount(jbd, None, callback);
|
|
+ }
|
|
+
|
|
+ let mut disk_file = DiskWrapper::open(disk_path.as_ref())?;
|
|
+ let disk_size = disk_file.size();
|
|
+ let block_size = disk_file.block_size() as u64;
|
|
+
|
|
+ let gpt_block_size = match block_size {
|
|
+ 512 => gpt::disk::LogicalBlockSize::Lb512,
|
|
+ _ => {
|
|
+ bail!("block size {block_size} not supported");
|
|
+ }
|
|
+ };
|
|
+
|
|
+ let gpt_reserved = 34 * 512;
|
|
+ let mibi = 1024 * 1024;
|
|
+
|
|
+ let bios_start = gpt_reserved / block_size;
|
|
+ let bios_end = (mibi / block_size) - 1;
|
|
+
|
|
+ let efi_start = bios_end + 1;
|
|
+ let efi_size = if let Some(size) = disk_option.efi_partition_size {
|
|
+ size as u64
|
|
+ } else {
|
|
+ 1
|
|
+ };
|
|
+ let efi_end = efi_start + (efi_size * mibi / block_size) - 1;
|
|
+
|
|
+ let filesystem_start = efi_end + 1;
|
|
+ let filesystem_end = ((((disk_size - gpt_reserved) / mibi) * mibi) / block_size) - 1;
|
|
+
|
|
+ {
|
|
+ eprintln!(
|
|
+ "Write bootloader with size {:#x}",
|
|
+ disk_option.bootloader_bios.len()
|
|
+ );
|
|
+ disk_file.seek(SeekFrom::Start(0))?;
|
|
+ disk_file.write_all(&disk_option.bootloader_bios)?;
|
|
+
|
|
+ let mbr_blocks = ((disk_size + block_size - 1) / block_size) - 1;
|
|
+ eprintln!("Writing protective MBR with disk blocks {mbr_blocks:#x}");
|
|
+ gpt::mbr::ProtectiveMBR::with_lb_size(mbr_blocks as u32)
|
|
+ .update_conservative(&mut disk_file)?;
|
|
+
|
|
+ let mut gpt_disk = gpt::GptConfig::new()
|
|
+ .initialized(false)
|
|
+ .writable(true)
|
|
+ .logical_block_size(gpt_block_size)
|
|
+ .create_from_device(Box::new(&mut disk_file), None)?;
|
|
+
|
|
+ let mut partitions = BTreeMap::new();
|
|
+ let mut partition_id = 1;
|
|
+ partitions.insert(
|
|
+ partition_id,
|
|
+ gpt::partition::Partition {
|
|
+ part_type_guid: gpt::partition_types::BIOS,
|
|
+ part_guid: uuid::Uuid::new_v4(),
|
|
+ first_lba: bios_start,
|
|
+ last_lba: bios_end,
|
|
+ flags: 0,
|
|
+ name: "BIOS".to_string(),
|
|
+ },
|
|
+ );
|
|
+ partition_id += 1;
|
|
+
|
|
+ partitions.insert(
|
|
+ partition_id,
|
|
+ gpt::partition::Partition {
|
|
+ part_type_guid: gpt::partition_types::EFI,
|
|
+ part_guid: uuid::Uuid::new_v4(),
|
|
+ first_lba: efi_start,
|
|
+ last_lba: efi_end,
|
|
+ flags: 0,
|
|
+ name: "EFI".to_string(),
|
|
+ },
|
|
+ );
|
|
+ partition_id += 1;
|
|
+
|
|
+ partitions.insert(
|
|
+ partition_id,
|
|
+ gpt::partition::Partition {
|
|
+ part_type_guid: gpt::partition_types::LINUX_FS,
|
|
+ part_guid: uuid::Uuid::new_v4(),
|
|
+ first_lba: filesystem_start,
|
|
+ last_lba: filesystem_end,
|
|
+ flags: 0,
|
|
+ name: "REDOX".to_string(),
|
|
+ },
|
|
+ );
|
|
+
|
|
+ eprintln!("Writing GPT tables: {partitions:#?}");
|
|
+ gpt_disk.update_partitions(partitions)?;
|
|
+ gpt_disk.write()?;
|
|
+ }
|
|
+
|
|
+ {
|
|
+ let disk_efi_start = efi_start * block_size;
|
|
+ let disk_efi_end = (efi_end + 1) * block_size;
|
|
+ let mut disk_efi =
|
|
+ fscommon::StreamSlice::new(&mut disk_file, disk_efi_start, disk_efi_end)?;
|
|
+
|
|
+ eprintln!(
|
|
+ "Formatting EFI partition as FAT32 (size {:#x})",
|
|
+ disk_efi_end - disk_efi_start
|
|
+ );
|
|
+ fatfs::format_volume(
|
|
+ &mut disk_efi,
|
|
+ fatfs::FormatVolumeOptions::new().fat_type(fatfs::FatType::Fat32),
|
|
+ )?;
|
|
+
|
|
+ eprintln!("Opening EFI partition");
|
|
+ let fs = fatfs::FileSystem::new(&mut disk_efi, fatfs::FsOptions::new())?;
|
|
+
|
|
+ eprintln!("Creating EFI directory");
|
|
+ let root_dir = fs.root_dir();
|
|
+ root_dir.create_dir("EFI")?;
|
|
+ let efi_dir = root_dir.open_dir("EFI")?;
|
|
+
|
|
+ if let (Some(grub_data), Some(grub_cfg)) = (disk_option.grub_efi, disk_option.grub_config) {
|
|
+ if grub_data.is_empty() {
|
|
+ bail!("GRUB EFI binary is empty (0 bytes). The GRUB recipe may have failed to produce a valid image.");
|
|
+ }
|
|
+ if grub_cfg.is_empty() {
|
|
+ bail!("GRUB configuration is empty (0 bytes). Check that grub.cfg has content.");
|
|
+ }
|
|
+ if disk_option.bootloader_efi.is_empty() {
|
|
+ bail!("Redox bootloader is empty (0 bytes). Cannot set up GRUB chainload without a valid Redox bootloader.");
|
|
+ }
|
|
+
|
|
+ efi_dir.create_dir("BOOT")?;
|
|
+ efi_dir.create_dir("REDBEAR")?;
|
|
+
|
|
+ eprintln!(
|
|
+ "Writing EFI/BOOT/{} (GRUB, {:#x} bytes)",
|
|
+ bootloader_efi_name,
|
|
+ grub_data.len()
|
|
+ );
|
|
+ let boot_dir = efi_dir.open_dir("BOOT")?;
|
|
+ let mut file = boot_dir.create_file(bootloader_efi_name)?;
|
|
+ file.truncate()?;
|
|
+ file.write_all(grub_data)?;
|
|
+
|
|
+ eprintln!("Writing EFI/BOOT/grub.cfg ({:#x} bytes)", grub_cfg.len());
|
|
+ let mut file = boot_dir.create_file("grub.cfg")?;
|
|
+ file.truncate()?;
|
|
+ file.write_all(grub_cfg)?;
|
|
+
|
|
+ eprintln!(
|
|
+ "Writing EFI/REDBEAR/redbear.efi (Redox bootloader, {:#x} bytes)",
|
|
+ disk_option.bootloader_efi.len()
|
|
+ );
|
|
+ let redbear_dir = efi_dir.open_dir("REDBEAR")?;
|
|
+ let mut file = redbear_dir.create_file("redbear.efi")?;
|
|
+ file.truncate()?;
|
|
+ file.write_all(&disk_option.bootloader_efi)?;
|
|
+ } else {
|
|
+ efi_dir.create_dir("BOOT")?;
|
|
+
|
|
+ eprintln!(
|
|
+ "Writing EFI/BOOT/{} file with size {:#x}",
|
|
+ bootloader_efi_name,
|
|
+ disk_option.bootloader_efi.len()
|
|
+ );
|
|
+ let boot_dir = efi_dir.open_dir("BOOT")?;
|
|
+ let mut file = boot_dir.create_file(bootloader_efi_name)?;
|
|
+ file.truncate()?;
|
|
+ file.write_all(&disk_option.bootloader_efi)?;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ let disk_ext4_start = filesystem_start * block_size;
|
|
+ let disk_ext4_end = (filesystem_end + 1) * block_size;
|
|
+ eprintln!(
|
|
+ "Installing to ext4 partition with size {:#x}",
|
|
+ disk_ext4_end - disk_ext4_start
|
|
+ );
|
|
+
|
|
+ let disk_ext4 = Ext4SliceDisk::new(
|
|
+ fscommon::StreamSlice::new(&mut disk_file, disk_ext4_start, disk_ext4_end)?,
|
|
+ disk_ext4_end - disk_ext4_start,
|
|
+ rsext4::BLOCK_SIZE_U32,
|
|
+ );
|
|
+ let mut jbd = Jbd2Dev::initial_jbd2dev(0, disk_ext4, false);
|
|
+ ext4_mkfs(&mut jbd).map_err(ext4_error)?;
|
|
+ with_ext4_mount(jbd, None, callback)
|
|
+}
|
|
+
|
|
#[cfg(not(target_os = "redox"))]
|
|
pub fn try_fast_install<D: redoxfs::Disk, F: FnMut(u64, u64)>(
|
|
_fs: &mut redoxfs::FileSystem<D>,
|
|
@@ -801,6 +1368,27 @@ pub fn try_fast_install<D: redoxfs::Disk, F: FnMut(u64, u64)>(
|
|
|
|
fn install_inner(config: Config, output: &Path) -> Result<()> {
|
|
println!("Installing to {}:\n{}", output.display(), config);
|
|
+
|
|
+ if let Some(ref bl) = config.general.bootloader {
|
|
+ let bl_lower = bl.to_lowercase();
|
|
+ match bl_lower.as_str() {
|
|
+ "redox" | "grub" => {}
|
|
+ other => bail!(
|
|
+ "Unknown bootloader '{}': expected \"redox\" or \"grub\"",
|
|
+ other
|
|
+ ),
|
|
+ }
|
|
+ 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);
|
|
+ }
|
|
+ if config.general.skip_partitions.unwrap_or(false) {
|
|
+ bail!("GRUB bootloader is incompatible with skip_partitions = true. GRUB requires GPT partition layout with ESP for chainloading.");
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
let cookbook = config.general.cookbook.clone();
|
|
let cookbook = cookbook.as_ref().map(|p| p.as_str());
|
|
if output.is_dir() {
|
|
@@ -823,28 +1411,45 @@ 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());
|
|
- let (bootloader_bios, bootloader_efi) = fetch_bootloaders(&config, cookbook, live)?;
|
|
+ let (bootloader_bios, bootloader_efi, grub_efi, grub_cfg) =
|
|
+ fetch_bootloaders(&config, cookbook, live)?;
|
|
if let Some(write_bootloader) = &config.general.write_bootloader {
|
|
- std::fs::write(write_bootloader, &bootloader_efi)?;
|
|
+ let primary_efi = grub_efi.as_deref().unwrap_or(&bootloader_efi);
|
|
+ if primary_efi.is_empty() {
|
|
+ bail!("Cannot write bootloader to {:?}: EFI binary is empty (0 bytes). The recipe may have failed to produce a valid image.", write_bootloader);
|
|
+ }
|
|
+ std::fs::write(write_bootloader, primary_efi)?;
|
|
}
|
|
+ let filesystem_type = match config.general.filesystem.as_deref() {
|
|
+ Some("ext4") => FilesystemType::Ext4,
|
|
+ _ => FilesystemType::RedoxFS,
|
|
+ };
|
|
let disk_option = DiskOption {
|
|
bootloader_bios: &bootloader_bios,
|
|
bootloader_efi: &bootloader_efi,
|
|
password_opt: password_opt,
|
|
+ filesystem_type,
|
|
efi_partition_size: config.general.efi_partition_size,
|
|
skip_partitions: config.general.skip_partitions.unwrap_or(false),
|
|
+ grub_efi: grub_efi.as_deref(),
|
|
+ grub_config: grub_cfg.as_deref(),
|
|
};
|
|
- with_whole_disk(output, &disk_option, move |fs| {
|
|
- if config.general.no_mount.unwrap_or(false) {
|
|
- with_redoxfs_ar(fs, None, move |mount_path| {
|
|
- install_dir(config, mount_path, cookbook)
|
|
- })
|
|
- } else {
|
|
- with_redoxfs_mount(fs, None, move |mount_path| {
|
|
- install_dir(config, mount_path, cookbook)
|
|
- })
|
|
- }
|
|
- })
|
|
+ match filesystem_type {
|
|
+ FilesystemType::RedoxFS => with_whole_disk(output, &disk_option, move |fs| {
|
|
+ if config.general.no_mount.unwrap_or(false) {
|
|
+ with_redoxfs_ar(fs, None, move |mount_path| {
|
|
+ install_dir(config, mount_path, cookbook)
|
|
+ })
|
|
+ } else {
|
|
+ with_redoxfs_mount(fs, None, move |mount_path| {
|
|
+ install_dir(config, mount_path, cookbook)
|
|
+ })
|
|
+ }
|
|
+ }),
|
|
+ FilesystemType::Ext4 => with_whole_disk_ext4(output, &disk_option, move |mount_path| {
|
|
+ install_dir(config, mount_path, cookbook)
|
|
+ }),
|
|
+ }
|
|
}
|
|
}
|
|
|
|
diff --git a/src/lib.rs b/src/lib.rs
|
|
index 216478d..186c24b 100644
|
|
--- a/src/lib.rs
|
|
+++ b/src/lib.rs
|
|
@@ -3,6 +3,8 @@ extern crate serde_derive;
|
|
|
|
mod config;
|
|
#[cfg(feature = "installer")]
|
|
+mod collision;
|
|
+#[cfg(feature = "installer")]
|
|
mod disk_wrapper;
|
|
#[cfg(feature = "installer")]
|
|
mod installer;
|
|
diff --git a/src/collision.rs b/src/collision.rs
|
|
new file mode 100644
|
|
index 0000000..145c62c
|
|
--- /dev/null
|
|
+++ b/src/collision.rs
|
|
@@ -0,0 +1,267 @@
|
|
+use std::collections::BTreeMap;
|
|
+use std::path::{Path, PathBuf};
|
|
+
|
|
+use anyhow::{Context, Result, bail};
|
|
+
|
|
+#[allow(dead_code)]
|
|
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
+pub enum FileLayer {
|
|
+ ConfigPreInstall,
|
|
+ Package,
|
|
+ ConfigPostInstall,
|
|
+}
|
|
+
|
|
+#[allow(dead_code)]
|
|
+#[derive(Debug)]
|
|
+pub struct CollisionReport {
|
|
+ pub path: PathBuf,
|
|
+ pub previous_layer: FileLayer,
|
|
+ pub current_layer: FileLayer,
|
|
+ #[allow(dead_code)]
|
|
+ pub previous_source: String,
|
|
+ #[allow(dead_code)]
|
|
+ pub current_source: String,
|
|
+ pub is_init_service: bool,
|
|
+}
|
|
+
|
|
+impl std::fmt::Display for FileLayer {
|
|
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
+ match self {
|
|
+ FileLayer::ConfigPreInstall => write!(f, "config (pre-install)"),
|
|
+ FileLayer::Package => write!(f, "package staging"),
|
|
+ FileLayer::ConfigPostInstall => write!(f, "config (post-install)"),
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+impl CollisionReport {
|
|
+ fn severity(&self) -> &'static str {
|
|
+ if self.is_init_service {
|
|
+ "ERROR"
|
|
+ } else {
|
|
+ "WARN"
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+impl std::fmt::Display for CollisionReport {
|
|
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
+ let severity = self.severity();
|
|
+ write!(
|
|
+ f,
|
|
+ "[COLLISION-{}] {} (was: {}, now: {})",
|
|
+ severity,
|
|
+ self.path.display(),
|
|
+ self.previous_layer,
|
|
+ self.current_layer
|
|
+ )
|
|
+ }
|
|
+}
|
|
+
|
|
+struct FileRecord {
|
|
+ layer: FileLayer,
|
|
+ source: String,
|
|
+ hash: Option<String>,
|
|
+}
|
|
+
|
|
+pub struct CollisionTracker {
|
|
+ files: BTreeMap<PathBuf, FileRecord>,
|
|
+ collisions: Vec<CollisionReport>,
|
|
+ strict: bool,
|
|
+}
|
|
+
|
|
+fn is_init_service_path(path: &Path) -> bool {
|
|
+ let path_str = path.to_string_lossy();
|
|
+ (path_str.contains("/init.d/") || path_str.contains("init.d"))
|
|
+ && (path_str.ends_with(".service") || path_str.ends_with(".target"))
|
|
+}
|
|
+
|
|
+fn is_environment_override_path(path: &Path) -> bool {
|
|
+ let path_str = path.to_string_lossy();
|
|
+ path_str.contains("/environment.d/")
|
|
+}
|
|
+
|
|
+impl CollisionTracker {
|
|
+ pub fn new() -> Self {
|
|
+ let strict = std::env::var("REDBEAR_STRICT_COLLISION")
|
|
+ .map(|v| v == "1" || v == "true")
|
|
+ .unwrap_or(false);
|
|
+ Self {
|
|
+ files: BTreeMap::new(),
|
|
+ collisions: Vec::new(),
|
|
+ strict,
|
|
+ }
|
|
+ }
|
|
+
|
|
+ pub fn record_config_pre_install(&mut self, path: &Path, dest: &Path) {
|
|
+ let full_path = dest.join(path);
|
|
+ let hash = Self::hash_file(&full_path);
|
|
+ self.files.insert(
|
|
+ path.to_path_buf(),
|
|
+ FileRecord { layer: FileLayer::ConfigPreInstall, source: "config pre-install".to_string(), hash },
|
|
+ );
|
|
+ }
|
|
+
|
|
+ pub fn record_config_post_install(&mut self, path: &Path, dest: &Path) {
|
|
+ let full_path = dest.join(path);
|
|
+ let hash = Self::hash_file(&full_path);
|
|
+ self.files.insert(
|
|
+ path.to_path_buf(),
|
|
+ FileRecord { layer: FileLayer::ConfigPostInstall, source: "config post-install".to_string(), hash },
|
|
+ );
|
|
+ }
|
|
+
|
|
+ pub fn snapshot_pre_package(&mut self, dest: &Path) -> Result<()> {
|
|
+ Self::walk_dir(dest, dest, &mut |relative_path| {
|
|
+ if !self.files.contains_key(&relative_path) {
|
|
+ let full_path = dest.join(&relative_path);
|
|
+ let hash = Self::hash_file(&full_path);
|
|
+ self.files.insert(
|
|
+ relative_path,
|
|
+ FileRecord {
|
|
+ layer: FileLayer::ConfigPreInstall,
|
|
+ source: "config pre-install".to_string(),
|
|
+ hash,
|
|
+ },
|
|
+ );
|
|
+ }
|
|
+ Ok(())
|
|
+ })
|
|
+ }
|
|
+
|
|
+ pub fn detect_package_overwrites(&mut self, dest: &Path, package_names: &[&String]) -> Result<()> {
|
|
+ let pkg_label = if package_names.len() == 1 {
|
|
+ format!("package {}", package_names[0])
|
|
+ } else {
|
|
+ format!("packages ({} total)", package_names.len())
|
|
+ };
|
|
+
|
|
+ Self::walk_dir(dest, dest, &mut |relative_path| {
|
|
+ if let Some(existing) = self.files.get(&relative_path) {
|
|
+ if existing.layer == FileLayer::ConfigPreInstall {
|
|
+ let full_path = dest.join(&relative_path);
|
|
+ let is_init = is_init_service_path(&relative_path);
|
|
+ let is_env = is_environment_override_path(&relative_path);
|
|
+ let contents_differ = existing.hash.as_ref().map_or(true, |h| {
|
|
+ Self::hash_file(&full_path).as_ref() != Some(h)
|
|
+ });
|
|
+ if contents_differ {
|
|
+ let report = CollisionReport {
|
|
+ path: relative_path.clone(),
|
|
+ previous_layer: existing.layer,
|
|
+ current_layer: FileLayer::Package,
|
|
+ previous_source: existing.source.clone(),
|
|
+ current_source: pkg_label.clone(),
|
|
+ is_init_service: is_init || is_env,
|
|
+ };
|
|
+ self.collisions.push(report);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ self.files.insert(
|
|
+ relative_path,
|
|
+ FileRecord {
|
|
+ layer: FileLayer::Package,
|
|
+ source: pkg_label.clone(),
|
|
+ hash: None,
|
|
+ },
|
|
+ );
|
|
+ Ok(())
|
|
+ })
|
|
+ }
|
|
+
|
|
+ fn hash_file(path: &Path) -> Option<String> {
|
|
+ use std::collections::hash_map::DefaultHasher;
|
|
+ use std::io::Read;
|
|
+ let mut file = std::fs::File::open(path).ok()?;
|
|
+ let mut hasher = DefaultHasher::new();
|
|
+ let mut buf = [0u8; 8192];
|
|
+ loop {
|
|
+ let n = file.read(&mut buf).ok()?;
|
|
+ if n == 0 {
|
|
+ break;
|
|
+ }
|
|
+ std::hash::Hasher::write(&mut hasher, &buf[..n]);
|
|
+ }
|
|
+ Some(format!("{:016x}", std::hash::Hasher::finish(&hasher)))
|
|
+ }
|
|
+
|
|
+ pub fn report(&self) -> Result<()> {
|
|
+ if self.collisions.is_empty() {
|
|
+ println!("[COLLISION-CHECK] No file collisions detected");
|
|
+ return Ok(());
|
|
+ }
|
|
+
|
|
+ let init_collisions: Vec<&CollisionReport> = self
|
|
+ .collisions
|
|
+ .iter()
|
|
+ .filter(|c| c.is_init_service)
|
|
+ .collect();
|
|
+ let other_collisions: Vec<&CollisionReport> = self
|
|
+ .collisions
|
|
+ .iter()
|
|
+ .filter(|c| !c.is_init_service)
|
|
+ .collect();
|
|
+
|
|
+ for collision in &other_collisions {
|
|
+ eprintln!(
|
|
+ "[COLLISION-WARN] {} overwritten by {}",
|
|
+ collision.path.display(),
|
|
+ collision.current_source
|
|
+ );
|
|
+ }
|
|
+
|
|
+ if !init_collisions.is_empty() {
|
|
+ eprintln!("[COLLISION-ERROR] {} init service file collision(s) detected:", init_collisions.len());
|
|
+ for collision in &init_collisions {
|
|
+ eprintln!(" {}", collision);
|
|
+ if collision.path.to_string_lossy().contains("/usr/lib/init.d/") {
|
|
+ eprintln!(" Fix: Change config [[files]] path from /usr/lib/init.d/ to /etc/init.d/");
|
|
+ }
|
|
+ }
|
|
+ bail!(
|
|
+ "Build aborted: {} init service file collision(s). \
|
|
+ Config overrides were silently overwritten by package staging. \
|
|
+ See local/docs/BUILD-SYSTEM-HARDENING-PLAN.md for details.",
|
|
+ init_collisions.len()
|
|
+ );
|
|
+ }
|
|
+
|
|
+ if self.strict && !other_collisions.is_empty() {
|
|
+ bail!(
|
|
+ "Build aborted (strict mode): {} file collision(s) detected. \
|
|
+ Set REDBEAR_STRICT_COLLISION=0 to downgrade to warnings.",
|
|
+ other_collisions.len()
|
|
+ );
|
|
+ }
|
|
+
|
|
+ println!(
|
|
+ "[COLLISION-WARN] {} non-critical file collision(s) detected (see above)",
|
|
+ other_collisions.len()
|
|
+ );
|
|
+ Ok(())
|
|
+ }
|
|
+
|
|
+ fn walk_dir<F>(root: &Path, prefix: &Path, callback: &mut F) -> Result<()>
|
|
+ where
|
|
+ F: FnMut(PathBuf) -> Result<()>,
|
|
+ {
|
|
+ if !root.is_dir() {
|
|
+ return Ok(());
|
|
+ }
|
|
+ for entry in std::fs::read_dir(root)
|
|
+ .with_context(|| format!("reading dir {}", root.display()))?
|
|
+ {
|
|
+ let entry = entry?;
|
|
+ let path = entry.path();
|
|
+ if path.is_dir() {
|
|
+ Self::walk_dir(&path, prefix, callback)?;
|
|
+ } else {
|
|
+ if let Ok(relative) = path.strip_prefix(prefix) {
|
|
+ callback(relative.to_path_buf())?;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ Ok(())
|
|
+ }
|
|
+}
|