diff --git a/ostool/src/artifact/elf_metadata.rs b/ostool/src/artifact/elf_metadata.rs index a07c77c..6479a72 100644 --- a/ostool/src/artifact/elf_metadata.rs +++ b/ostool/src/artifact/elf_metadata.rs @@ -1,16 +1,19 @@ use anyhow::{Context, bail}; use object::{ - Object, ObjectSegment, ObjectSymbol, - read::elf::{ElfFile, FileHeader, ProgramHeader}, + Object, ObjectSection, ObjectSegment, ObjectSymbol, + read::elf::{ElfFile, FileHeader, ProgramHeader, SectionHeader}, }; pub(crate) struct ElfMetadata { pub(crate) arch: object::Architecture, pub(crate) entry: u64, pub(crate) load_segments: Vec, + /// Allocated sections with file bytes that can appear in a raw binary image. + pub(crate) load_sections: Vec, pub(crate) executable_start: Option, } +#[derive(Debug, PartialEq, Eq)] pub(crate) struct LoadSegment { pub(crate) virtual_address: u64, pub(crate) physical_address: u64, @@ -21,6 +24,14 @@ pub(crate) struct LoadSegment { pub(crate) flags: u32, } +/// A file-backed allocated section, represented in both ELF address spaces. +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct LoadSection { + pub(crate) virtual_address: u64, + pub(crate) file_offset: u64, + pub(crate) size: u64, +} + impl ElfMetadata { pub(crate) fn parse(data: &[u8]) -> anyhow::Result { let file = object::File::parse(data).context("failed to parse ELF")?; @@ -53,6 +64,26 @@ impl ElfMetadata { }) .collect(); + let load_sections = file + .sections() + .filter_map(|section| { + let header = section.elf_section_header(); + let flags: u64 = header.sh_flags(endian).into(); + if flags & u64::from(object::elf::SHF_ALLOC) == 0 + || header.sh_type(endian) == object::elf::SHT_NOBITS + { + return None; + } + + let (file_offset, size) = section.file_range()?; + (size != 0).then_some(LoadSection { + virtual_address: section.address(), + file_offset, + size, + }) + }) + .collect(); + let mut executable_start = None; for symbol in file.symbols() { if symbol.name()? == "__executable_start" && symbol.is_definition() { @@ -65,6 +96,7 @@ impl ElfMetadata { arch: file.architecture(), entry: file.entry(), load_segments, + load_sections, executable_start, }) } diff --git a/ostool/src/artifact/runtime.rs b/ostool/src/artifact/runtime.rs index a66f57d..d59a191 100644 --- a/ostool/src/artifact/runtime.rs +++ b/ostool/src/artifact/runtime.rs @@ -79,6 +79,24 @@ impl PreparedRuntimeArtifacts { pub(crate) fn arch(&self) -> Option { self.arch } + + #[cfg(test)] + pub(crate) fn for_test(source_elf: PathBuf, elf: PathBuf) -> Self { + let source_artifact_dir = source_elf + .parent() + .map(Path::to_path_buf) + .unwrap_or_default(); + let runtime_artifact_dir = elf.parent().map(Path::to_path_buf); + Self { + source_elf, + elf, + bin: None, + source_artifact_dir, + cargo_artifact_dir: None, + runtime_artifact_dir, + arch: None, + } + } } /// Prepares runtime ELF/BIN artifacts from a Cargo or custom-build ELF. diff --git a/ostool/src/boot/fit.rs b/ostool/src/boot/fit.rs index 922576d..0de314f 100644 --- a/ostool/src/boot/fit.rs +++ b/ostool/src/boot/fit.rs @@ -10,7 +10,15 @@ use log::{info, warn}; use object::Architecture; use tokio::fs; -use crate::utils::PathResultExt; +use crate::{ + artifact::{ + elf_metadata::ElfMetadata, + runtime::{PreparedRuntimeArtifacts, RuntimeArtifactOptions, prepare_runtime_artifacts}, + }, + boot::fit_config::{FitConfig, FitFormat, FitOs, loongarch_image_entry}, + process::ProcessContext, + utils::PathResultExt, +}; const KERNEL_COMPONENT_NAME: &str = "kernel"; const FDT_COMPONENT_NAME: &str = "fdt"; @@ -18,9 +26,6 @@ const RAMDISK_COMPONENT_NAME: &str = "ramdisk"; const DEFAULT_CONFIG_NAME: &str = "config-ostool"; const FIT_DESCRIPTION: &str = "Various kernels, ramdisks and FDT blobs"; const FIT_IMAGE_NAME: &str = "image.fit"; -const LOONGARCH_IMAGE_MAGIC: &[u8] = b"MZ"; -const LOONGARCH_IMAGE_ENTRY_OFFSET: usize = 8; -const LOONGARCH_IMAGE_LOAD_OFFSET: usize = 24; mod errors { pub const KERNEL_READ_ERROR: &str = "读取 kernel 文件失败"; @@ -53,7 +58,190 @@ impl GeneratedFitImage { } } -pub(crate) async fn generate_fit_image(input: FitInput) -> anyhow::Result { +/// Builds a configured payload from one prepared ELF selection. Paths in the +/// configuration are already resolved by the caller; this does not run a guest. +/// Symbols come from the original ELF, even when the runtime copy is stripped. +#[allow(dead_code)] // Consumed by the boot-prepare entry point in the next change. +pub(crate) async fn generate_configured_fit( + context: &ProcessContext, + prepared: &PreparedRuntimeArtifacts, + config: &FitConfig, +) -> anyhow::Result { + let source_data = fs::read(prepared.source_elf()) + .await + .with_path("failed to read source ELF", prepared.source_elf())?; + let metadata = ElfMetadata::parse(&source_data) + .with_context(|| format!("invalid source ELF: {}", prepared.source_elf().display()))?; + validate_elf_load_data(&metadata, source_data.len())?; + fit_arch_name(metadata.arch)?; + + let runtime_data = if prepared.source_elf() == prepared.elf() { + source_data + } else { + let runtime_data = fs::read(prepared.elf()) + .await + .with_path("failed to read runtime ELF", prepared.elf())?; + let runtime_metadata = ElfMetadata::parse(&runtime_data) + .with_context(|| format!("invalid runtime ELF: {}", prepared.elf().display()))?; + validate_elf_load_data(&runtime_metadata, runtime_data.len())?; + if metadata.arch != runtime_metadata.arch + || metadata.entry != runtime_metadata.entry + || metadata.load_segments != runtime_metadata.load_segments + || metadata.load_sections != runtime_metadata.load_sections + { + anyhow::bail!( + "source and runtime ELF load layouts differ; prepare the selected ELF again" + ); + } + runtime_data + }; + let load = config.resolve_load(&metadata)?; + let output = match &config.output { + Some(path) => path.clone(), + None => default_output_path(prepared.elf())?, + }; + // An output alias must not destroy an input needed by subsequent preparation. + if let Ok(destination) = output.canonicalize() { + for input in [ + Some(prepared.source_elf()), + Some(prepared.elf()), + config.fdt.as_ref().map(|fdt| fdt.path.as_path()), + ] + .into_iter() + .flatten() + { + if input.canonicalize().ok().as_ref() == Some(&destination) { + anyhow::bail!("FIT output would overwrite an input: {}", output.display()); + } + } + } + let kernel_data = match config.format { + FitFormat::Elf => runtime_data, + FitFormat::Bin => { + // Derive from this runtime ELF, never reuse an unrelated/stale BIN. + let temporary = tempfile::tempdir().context("failed to stage FIT BIN payload")?; + let binary = prepare_runtime_artifacts( + context, + RuntimeArtifactOptions { + elf_path: prepared.elf().to_path_buf(), + to_bin: true, + bin_dir: Some(temporary.path().to_path_buf()), + debug: false, + cargo_artifact_dir: None, + strip_elf: false, + }, + ) + .with_context(|| { + format!( + "failed to derive FIT BIN payload with llvm-objcopy from {}", + prepared.elf().display() + ) + })?; + fs::read(binary.bin().context("BIN conversion produced no path")?) + .await + .context("failed to read derived FIT BIN payload")? + } + }; + if kernel_data.is_empty() { + anyhow::bail!("cannot build FIT with an empty kernel payload"); + } + // Resolve entry after payload selection so format-specific header rules see + // the exact bytes that will be stored in the FIT image. + let entry = config.resolve_entry(&metadata, load, &kernel_data)?; + // Replace the output directory entry only after success. In particular, a + // hard link to an input must not truncate that input's shared inode. + let output_dir = output + .parent() + .filter(|parent| !parent.as_os_str().is_empty()) + .unwrap_or(Path::new(".")); + #[cfg(unix)] + let output_permissions = match fs::metadata(&output).await { + Ok(metadata) => Some(metadata.permissions()), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => None, + Err(error) => { + return Err(error).with_path("failed to read FIT output permissions", &output); + } + }; + let builder = tempfile::Builder::new(); + #[cfg(unix)] + let builder = { + use std::os::unix::fs::PermissionsExt; + let mut builder = builder; + // New outputs follow normal file creation (0666 masked by umask). + // Keep replacements private until their existing mode is restored. + if output_permissions.is_none() { + builder.permissions(std::fs::Permissions::from_mode(0o666)); + } + builder + }; + let temporary = builder + .tempfile_in(output_dir) + .with_path("failed to stage FIT output", &output)?; + write_fit_image( + FitInput { + kernel_path: prepared.elf().to_path_buf(), + dtb_path: config.fdt.as_ref().map(|fdt| fdt.path.clone()), + initramfs_path: None, + arch: metadata.arch, + kernel_load_addr: load, + kernel_entry_addr: entry, + fdt_load_addr: config.fdt.as_ref().and_then(|fdt| fdt.load), + output_path: Some(temporary.path().to_path_buf()), + }, + kernel_data, + config.os, + ) + .await?; + #[cfg(unix)] + if let Some(permissions) = output_permissions { + temporary + .as_file() + .set_permissions(permissions) + .with_path("failed to preserve FIT output permissions", &output)?; + } + temporary + .persist(&output) + .map_err(|error| error.error) + .with_path(errors::FIT_SAVE_ERROR, &output)?; + info!("FIT image ok: {}", output.display()); + Ok(GeneratedFitImage { path: output }) +} + +// Metadata parsing reads descriptions, not the bytes they describe. Explicit +// addresses bypass inference, but cannot make truncated load data valid. +fn validate_elf_load_data(metadata: &ElfMetadata, file_size: usize) -> anyhow::Result<()> { + let in_file = |offset: u64, size: u64| { + offset + .checked_add(size) + .is_some_and(|end| end <= file_size as u64) + }; + for segment in &metadata.load_segments { + if segment.file_size > segment.memory_size + || !in_file(segment.file_offset, segment.file_size) + || segment + .virtual_address + .checked_add(segment.memory_size) + .is_none() + || segment + .physical_address + .checked_add(segment.memory_size) + .is_none() + { + anyhow::bail!("invalid ELF PT_LOAD data range"); + } + } + for section in &metadata.load_sections { + if !in_file(section.file_offset, section.size) + || section.virtual_address.checked_add(section.size).is_none() + { + anyhow::bail!("invalid ELF allocated section data range"); + } + } + Ok(()) +} + +/// Existing U-Boot path: Linux payload with its historical header handling. +pub(crate) async fn generate_fit_image(mut input: FitInput) -> anyhow::Result { info!("Making FIT image..."); let kernel_data = fs::read(&input.kernel_path) @@ -66,16 +254,24 @@ pub(crate) async fn generate_fit_image(input: FitInput) -> anyhow::Result, + os: FitOs, +) -> anyhow::Result { let arch_name = fit_arch_name(input.arch)?; let dtb_data = if let Some(dtb_path) = &input.dtb_path { let data = fs::read(dtb_path) @@ -105,13 +301,14 @@ pub(crate) async fn generate_fit_image(input: FitInput) -> anyhow::Result None, }; - let config = build_default_fit_config( + let config = build_fit_config( arch_name, + os, kernel_data, dtb_data, initramfs_data, input.kernel_load_addr, - kernel_entry_addr, + input.kernel_entry_addr, input.fdt_load_addr, ); @@ -127,7 +324,6 @@ pub(crate) async fn generate_fit_image(input: FitInput) -> anyhow::Result anyhow::Result<&'static str> } } -fn resolve_kernel_entry_addr( - arch: Architecture, - kernel_data: &[u8], - kernel_load_addr: u64, - fallback_entry_addr: u64, -) -> anyhow::Result { - if arch != Architecture::LoongArch64 || !kernel_data.starts_with(LOONGARCH_IMAGE_MAGIC) { - return Ok(fallback_entry_addr); - } - - let image_entry = read_image_header_u64(kernel_data, LOONGARCH_IMAGE_ENTRY_OFFSET)?; - let image_load_addr = read_image_header_u64(kernel_data, LOONGARCH_IMAGE_LOAD_OFFSET)?; - let entry_offset = image_entry.checked_sub(image_load_addr).ok_or_else(|| { - anyhow!( - "invalid LoongArch image header: entry {image_entry:#x} precedes load address {image_load_addr:#x}" - ) - })?; - if entry_offset >= kernel_data.len() as u64 { - anyhow::bail!( - "invalid LoongArch image header: entry offset {entry_offset:#x} exceeds image size {:#x}", - kernel_data.len() - ); - } - - kernel_load_addr - .checked_add(entry_offset) - .ok_or_else(|| anyhow!("LoongArch kernel entry address overflow")) -} - -fn read_image_header_u64(kernel_data: &[u8], offset: usize) -> anyhow::Result { - let bytes = kernel_data - .get(offset..offset + size_of::()) - .ok_or_else(|| anyhow!("truncated LoongArch image header at offset {offset:#x}"))?; - let bytes = <[u8; size_of::()]>::try_from(bytes) - .map_err(|_| anyhow!("invalid LoongArch image header field at offset {offset:#x}"))?; - Ok(u64::from_le_bytes(bytes)) -} - fn default_output_path(kernel_path: &Path) -> anyhow::Result { let output_dir = kernel_path .parent() @@ -223,8 +381,10 @@ fn default_output_path(kernel_path: &Path) -> anyhow::Result { Ok(Path::new(output_dir).join(FIT_IMAGE_NAME)) } -fn build_default_fit_config( +#[allow(clippy::too_many_arguments)] +fn build_fit_config( arch_name: &'static str, + os: FitOs, kernel_data: Vec, dtb_data: Option>, initramfs_data: Option>, @@ -237,7 +397,7 @@ fn build_default_fit_config( .with_description("This kernel") .with_type("kernel") .with_arch(arch_name) - .with_os("linux") + .with_os(os.as_str()) .with_compression(false) .with_load_address(kernel_load_addr) .with_entry_point(kernel_entry_addr), @@ -288,13 +448,112 @@ fn build_default_fit_config( #[cfg(test)] mod tests { + use std::{fs, path::Path}; + use super::{ - FitInput, build_default_fit_config, default_output_path, fit_arch_name, - resolve_kernel_entry_addr, + FitInput, FitOs, build_fit_config, default_output_path, fit_arch_name, + generate_configured_fit, loongarch_image_entry, + }; + use crate::{ + artifact::runtime::{ + PreparedRuntimeArtifacts, RuntimeArtifactOptions, prepare_runtime_artifacts, + }, + boot::fit_config::{FitAddress, FitConfig, FitFormat}, + process::ProcessContext, + project::variables::VariableScope, }; use fdt_edit::{Fdt, Node, Property}; use object::Architecture; + fn write_u16(data: &mut [u8], offset: usize, value: u16) { + data[offset..offset + 2].copy_from_slice(&value.to_le_bytes()); + } + + fn write_u32(data: &mut [u8], offset: usize, value: u32) { + data[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); + } + + fn write_u64(data: &mut [u8], offset: usize, value: u64) { + data[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); + } + + fn minimal_aarch64_elf() -> Vec { + const DATA_OFFSET: usize = 0x100; + const STRING_OFFSET: usize = 0x180; + const SECTION_OFFSET: usize = 0x200; + const SECTION_SIZE: usize = 64; + const SECTION_COUNT: usize = 3; + let mut data = vec![0_u8; SECTION_OFFSET + SECTION_COUNT * SECTION_SIZE]; + data[..4].copy_from_slice(b"\x7fELF"); + data[4] = 2; // ELFCLASS64 + data[5] = 1; // ELFDATA2LSB + data[6] = 1; // EV_CURRENT + write_u16(&mut data, 16, 2); // ET_EXEC + write_u16(&mut data, 18, 183); // EM_AARCH64 + write_u32(&mut data, 20, 1); // EV_CURRENT + write_u64(&mut data, 24, 0x8000_0000); // e_entry + write_u64(&mut data, 32, 64); // e_phoff + write_u64(&mut data, 40, SECTION_OFFSET as u64); // e_shoff + write_u16(&mut data, 52, 64); // e_ehsize + write_u16(&mut data, 54, 56); // e_phentsize + write_u16(&mut data, 56, 1); // e_phnum + write_u16(&mut data, 58, SECTION_SIZE as u16); // e_shentsize + write_u16(&mut data, 60, SECTION_COUNT as u16); // e_shnum + write_u16(&mut data, 62, 2); // e_shstrndx + + // One file-backed executable PT_LOAD containing the section below. + write_u32(&mut data, 64, 1); // PT_LOAD + write_u32(&mut data, 68, 5); // PF_R | PF_X + write_u64(&mut data, 72, DATA_OFFSET as u64); // p_offset + write_u64(&mut data, 80, 0x8000_0000); // p_vaddr + write_u64(&mut data, 88, 0x8000_0000); // p_paddr + write_u64(&mut data, 96, 4); // p_filesz + write_u64(&mut data, 104, 4); // p_memsz + write_u64(&mut data, 112, 0x1000); // p_align + + data[DATA_OFFSET..DATA_OFFSET + 4].copy_from_slice(&[0x00, 0x00, 0x00, 0x14]); + data[STRING_OFFSET..STRING_OFFSET + 17].copy_from_slice(b"\0.text\0.shstrtab\0"); + // A .text-like allocated section; section zero stays all zero. + let section = SECTION_OFFSET + SECTION_SIZE; + write_u32(&mut data, section, 1); // sh_name = ".text" + write_u32(&mut data, section + 4, 1); // SHT_PROGBITS + write_u64(&mut data, section + 8, 0x6); // SHF_ALLOC | SHF_EXECINSTR + write_u64(&mut data, section + 16, 0x8000_0000); // sh_addr + write_u64(&mut data, section + 24, DATA_OFFSET as u64); // sh_offset + write_u64(&mut data, section + 32, 4); // sh_size + write_u64(&mut data, section + 48, 4); // sh_addralign + let strings = SECTION_OFFSET + 2 * SECTION_SIZE; + write_u32(&mut data, strings, 7); // sh_name = ".shstrtab" + write_u32(&mut data, strings + 4, 3); // SHT_STRTAB + write_u64(&mut data, strings + 24, STRING_OFFSET as u64); // sh_offset + write_u64(&mut data, strings + 32, 17); // sh_size + write_u64(&mut data, strings + 48, 1); // sh_addralign + data + } + + fn test_process_context(root: &Path) -> ProcessContext { + ProcessContext::new( + root.to_path_buf(), + root.to_path_buf(), + VariableScope::new(root.to_path_buf(), root.to_path_buf(), root.to_path_buf()), + None, + ) + } + + fn fit_kernel_property(fit: &Fdt, name: &str) -> Vec { + fit.get_by_path("/images/kernel") + .unwrap() + .as_node() + .get_property(name) + .unwrap() + .data + .to_vec() + } + + fn fit_u64_property(fit: &Fdt, name: &str) -> u64 { + u64::from_be_bytes(fit_kernel_property(fit, name).try_into().unwrap()) + } + #[test] fn maps_supported_architectures_to_fit_names() { assert_eq!(fit_arch_name(Architecture::Aarch64).unwrap(), "arm64"); @@ -323,17 +582,109 @@ mod tests { kernel_data[8..16].copy_from_slice(&0x5c_8b40_u64.to_le_bytes()); kernel_data[24..32].copy_from_slice(&0x20_0000_u64.to_le_bytes()); - let entry = resolve_kernel_entry_addr( + let entry = loongarch_image_entry( Architecture::LoongArch64, &kernel_data, 0x9000_0000_9800_0000, - 0x9000_0000_9800_0000, ) + .unwrap() .unwrap(); assert_eq!(entry, 0x9000_0000_983c_8b40); } + #[tokio::test] + async fn generates_configured_fit_from_a_real_elf_for_elf_and_bin_payloads() { + let temp = tempfile::tempdir().unwrap(); + let source = temp.path().join("kernel.elf"); + let source_bytes = minimal_aarch64_elf(); + fs::write(&source, &source_bytes).unwrap(); + let context = test_process_context(temp.path()); + let prepared = prepare_runtime_artifacts( + &context, + RuntimeArtifactOptions { + elf_path: source, + to_bin: false, + bin_dir: None, + debug: false, + cargo_artifact_dir: None, + strip_elf: false, + }, + ) + .unwrap(); + + let elf_output = temp.path().join("kernel-elf.fit"); + let elf_config = FitConfig { + format: FitFormat::Elf, + output: Some(elf_output), + ..FitConfig::default() + }; + let elf_image = generate_configured_fit(&context, &prepared, &elf_config) + .await + .unwrap(); + let elf_fit = Fdt::from_bytes(&fs::read(elf_image.path()).unwrap()).unwrap(); + assert_eq!(fit_kernel_property(&elf_fit, "os"), b"linux\0"); + assert_eq!(fit_u64_property(&elf_fit, "load"), 0x7fff_ff00); + assert_eq!(fit_u64_property(&elf_fit, "entry"), 0x8000_0000); + assert_eq!(fit_kernel_property(&elf_fit, "data"), source_bytes); + + let bin_output = temp.path().join("kernel-bin.fit"); + let bin_config = FitConfig { + format: FitFormat::Bin, + output: Some(bin_output), + ..FitConfig::default() + }; + let bin_image = generate_configured_fit(&context, &prepared, &bin_config) + .await + .unwrap(); + let bin_fit = Fdt::from_bytes(&fs::read(bin_image.path()).unwrap()).unwrap(); + assert_eq!(fit_kernel_property(&bin_fit, "os"), b"linux\0"); + assert_eq!(fit_u64_property(&bin_fit, "load"), 0x8000_0000); + assert_eq!(fit_u64_property(&bin_fit, "entry"), 0x8000_0000); + assert_eq!( + fit_kernel_property(&bin_fit, "data"), + &[0x00, 0x00, 0x00, 0x14] + ); + + let elf_os_output = temp.path().join("kernel-bootelf.fit"); + let elf_os_config = FitConfig { + format: FitFormat::Elf, + os: FitOs::Elf, + load: FitAddress::Explicit(0x4000_0000), + output: Some(elf_os_output), + ..FitConfig::default() + }; + let elf_os_image = generate_configured_fit(&context, &prepared, &elf_os_config) + .await + .unwrap(); + let elf_os_fit = Fdt::from_bytes(&fs::read(elf_os_image.path()).unwrap()).unwrap(); + assert_eq!(fit_kernel_property(&elf_os_fit, "os"), b"elf\0"); + assert_eq!(fit_u64_property(&elf_os_fit, "load"), 0x4000_0000); + assert_eq!(fit_u64_property(&elf_os_fit, "entry"), 0x4000_0000); + assert_eq!(fit_kernel_property(&elf_os_fit, "data"), source_bytes); + + let runtime = temp.path().join("kernel-runtime.elf"); + fs::copy(prepared.source_elf(), &runtime).unwrap(); + let separate_runtime = PreparedRuntimeArtifacts::for_test( + prepared.source_elf().to_path_buf(), + runtime.clone(), + ); + let separate_output = temp.path().join("kernel-separate-runtime.fit"); + let separate_config = FitConfig { + format: FitFormat::Elf, + output: Some(separate_output), + ..FitConfig::default() + }; + let separate_image = generate_configured_fit(&context, &separate_runtime, &separate_config) + .await + .unwrap(); + let separate_fit = Fdt::from_bytes(&fs::read(separate_image.path()).unwrap()).unwrap(); + assert_eq!( + fit_kernel_property(&separate_fit, "data"), + fs::read(runtime).unwrap() + ); + } + #[test] fn default_output_path_uses_kernel_directory() { let output_path = default_output_path(std::path::Path::new("/tmp/kernel.bin")).unwrap(); @@ -343,8 +694,16 @@ mod tests { #[test] fn default_fit_config_keeps_linux_kernel_defaults_without_dtb() { - let config = - build_default_fit_config("arm64", vec![1, 2, 3], None, None, 0x80000, 0x80000, None); + let config = build_fit_config( + "arm64", + FitOs::Linux, + vec![1, 2, 3], + None, + None, + 0x80000, + 0x80000, + None, + ); let kernel = config.kernel.as_ref().unwrap(); assert_eq!( @@ -368,8 +727,9 @@ mod tests { #[test] fn default_fit_config_adds_optional_fdt_component() { - let config = build_default_fit_config( + let config = build_fit_config( "riscv", + FitOs::Linux, vec![1, 2, 3], Some(vec![4, 5, 6]), None, @@ -389,8 +749,9 @@ mod tests { #[test] fn fit_links_initramfs_without_relocation_for_target_architecture() { - let config = build_default_fit_config( + let config = build_fit_config( "riscv", + FitOs::Linux, vec![1, 2, 3], None, Some(vec![4, 5, 6]), diff --git a/ostool/src/boot/fit_config.rs b/ostool/src/boot/fit_config.rs new file mode 100644 index 0000000..5ab7cde --- /dev/null +++ b/ostool/src/boot/fit_config.rs @@ -0,0 +1,622 @@ +//! Typed internal configuration and address resolution for FIT images. + +use std::{mem::size_of, path::PathBuf}; + +use anyhow::{Context, Result, anyhow, bail}; +use object::Architecture; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::artifact::elf_metadata::{ElfMetadata, LoadSection, LoadSegment}; + +const LOONGARCH_IMAGE_MAGIC: &[u8] = b"MZ"; +const LOONGARCH_IMAGE_ENTRY_OFFSET: usize = 8; +const LOONGARCH_IMAGE_LOAD_OFFSET: usize = 24; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub(crate) enum FitFormat { + Elf, + #[default] + Bin, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub(crate) enum FitOs { + #[default] + Linux, + Elf, +} + +impl FitOs { + pub(crate) fn as_str(self) -> &'static str { + match self { + Self::Linux => "linux", + Self::Elf => "elf", + } + } +} + +/// An address supplied directly or derived from ELF metadata. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub(crate) enum FitAddress { + #[default] + Auto, + Explicit(u64), +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub(crate) struct FitFdt { + pub(crate) path: PathBuf, + pub(crate) load: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(default, deny_unknown_fields)] +pub(crate) struct FitConfig { + pub(crate) format: FitFormat, + pub(crate) os: FitOs, + pub(crate) load: FitAddress, + pub(crate) entry: FitAddress, + pub(crate) fdt: Option, + pub(crate) output: Option, +} + +impl Default for FitConfig { + fn default() -> Self { + Self { + format: FitFormat::Bin, + os: FitOs::Linux, + load: FitAddress::Auto, + entry: FitAddress::Auto, + fdt: None, + output: None, + } + } +} + +impl FitConfig { + fn validate(&self) -> Result<()> { + if self.os == FitOs::Elf && self.format != FitFormat::Elf { + bail!("FIT OS elf requires format elf and U-Boot CONFIG_BOOTM_ELF"); + } + Ok(()) + } + + /// Resolves the payload load address from the original, unstripped ELF. + pub(crate) fn resolve_load(&self, metadata: &ElfMetadata) -> Result { + self.validate()?; + match self.load { + FitAddress::Explicit(address) => Ok(address), + // An ELF container needs staging memory, independent of its runtime + // sections. Metadata cannot establish a safe region on the board. + FitAddress::Auto if self.os == FitOs::Elf => { + bail!("FIT OS elf requires an explicit load address in safe staging memory") + } + FitAddress::Auto if self.format == FitFormat::Bin => { + let base = bin_load_base(metadata) + .context("cannot derive BIN FIT load address; set load explicitly")?; + if metadata + .executable_start + .is_some_and(|address| address != base) + { + bail!( + "cannot derive BIN FIT load address: __executable_start differs from the first payload LMA; set load explicitly" + ); + } + Ok(base) + } + FitAddress::Auto => { + let base = elf_load_base(metadata).context("cannot derive ELF FIT load address")?; + if metadata + .executable_start + .is_some_and(|address| address != base) + { + bail!( + "cannot derive ELF FIT load address: __executable_start differs from the ELF container base; set load explicitly" + ); + } + Ok(base) + } + } + } + + /// FIT OS elf passes the ELF container address to bootelf, which reads e_entry + /// itself. Other OS handlers consume an execution address. For `format = elf` + /// with a Linux OS, the payload is a staged ELF container: the entry must be in + /// a file-backed section whose LMA equals its VMA. Other sections may rely on + /// the kernel's own early mapping or relocation code. + pub(crate) fn resolve_entry( + &self, + metadata: &ElfMetadata, + load: u64, + payload: &[u8], + ) -> Result { + self.validate()?; + if self.os == FitOs::Elf { + if let FitAddress::Explicit(address) = self.entry + && address != load + { + bail!("FIT OS elf entry must equal the ELF payload load address; use entry auto"); + } + return Ok(load); + } + match self.entry { + FitAddress::Explicit(address) => Ok(address), + FitAddress::Auto => { + if self.format == FitFormat::Bin + && self.os == FitOs::Linux + && let Some(entry) = loongarch_image_entry(metadata.arch, payload, load)? + { + // LoongArch Linux BIN images carry their execution offset in + // the MZ image header. That header is authoritative for this + // format and intentionally replaces ELF e_entry validation. + return Ok(entry); + } + direct_entry(metadata, load, self.format) + .context("cannot derive FIT entry; set entry explicitly") + } + } + } +} + +/// Returns the execution address encoded in a LoongArch Linux image header. +/// `None` means the payload is not a LoongArch MZ image and should use the +/// normal ELF metadata rules. +pub(crate) fn loongarch_image_entry( + arch: Architecture, + payload: &[u8], + load: u64, +) -> Result> { + if arch != Architecture::LoongArch64 || !payload.starts_with(LOONGARCH_IMAGE_MAGIC) { + return Ok(None); + } + + let image_entry = read_image_header_u64(payload, LOONGARCH_IMAGE_ENTRY_OFFSET)?; + let image_load_addr = read_image_header_u64(payload, LOONGARCH_IMAGE_LOAD_OFFSET)?; + let entry_offset = image_entry.checked_sub(image_load_addr).ok_or_else(|| { + anyhow!( + "invalid LoongArch image header: entry {image_entry:#x} precedes load address {image_load_addr:#x}" + ) + })?; + if entry_offset >= payload.len() as u64 { + bail!( + "invalid LoongArch image header: entry offset {entry_offset:#x} exceeds image size {:#x}", + payload.len() + ); + } + + Ok(Some(load.checked_add(entry_offset).ok_or_else(|| { + anyhow!("LoongArch kernel entry address overflow") + })?)) +} + +fn read_image_header_u64(payload: &[u8], offset: usize) -> Result { + let bytes = payload + .get(offset..offset + size_of::()) + .ok_or_else(|| anyhow!("truncated LoongArch image header at offset {offset:#x}"))?; + let bytes = <[u8; size_of::()]>::try_from(bytes) + .map_err(|_| anyhow!("invalid LoongArch image header field at offset {offset:#x}"))?; + Ok(u64::from_le_bytes(bytes)) +} + +fn direct_entry(metadata: &ElfMetadata, load: u64, format: FitFormat) -> Result { + // Only infer an entry that directly addresses its file-backed bytes at load time. + // Startup code may map or relocate other sections, so they need not have VMA == LMA. + // This validates the entry mapping, not the complete runtime memory layout. + let base = match format { + FitFormat::Bin => bin_load_base(metadata)?, + FitFormat::Elf => elf_load_base(metadata)?, + }; + if load != base { + bail!("payload load address differs from its original base"); + } + let mut found = false; + for section in &metadata.load_sections { + if metadata.entry >= section.virtual_address + && metadata.entry + < checked_end( + section.virtual_address, + section.size, + "section virtual range", + )? + { + if section_lma(section, &metadata.load_segments)? != section.virtual_address { + bail!("ELF entry VMA differs from its LMA"); + } + found = true; + } + } + if !found { + bail!("ELF entry is outside the file-backed payload"); + } + Ok(metadata.entry) +} + +fn elf_load_base(metadata: &ElfMetadata) -> Result { + // An ELF payload used as a Linux image is staged as one contiguous byte + // container. Every file-backed PT_LOAD therefore needs the same + // `p_paddr - p_offset` base; an explicit address cannot rearrange bytes. + let mut segments = metadata + .load_segments + .iter() + .filter(|segment| segment.file_size != 0); + let first_segment = segments + .next() + .ok_or_else(|| anyhow!("no file-backed PT_LOAD segment"))?; + let first = elf_container_base(first_segment)?; + for segment in segments { + let base = elf_container_base(segment)?; + if base != first { + bail!( + "PT_LOAD at file offset {:#x} requires base {first:#x}, but PT_LOAD at file offset {:#x} requires base {base:#x}; no single load address preserves this layout; overriding load/entry does not rearrange payload bytes", + first_segment.file_offset, + segment.file_offset, + ); + } + } + Ok(first) +} + +fn elf_container_base(segment: &LoadSegment) -> Result { + valid_segment_range(segment)?; + segment + .physical_address + .checked_sub(segment.file_offset) + .ok_or_else(|| { + anyhow!( + "PT_LOAD physical address {:#x} precedes file offset {:#x}", + segment.physical_address, + segment.file_offset + ) + }) +} + +fn bin_load_base(metadata: &ElfMetadata) -> Result { + metadata + .load_sections + .iter() + .map(|section| section_lma(section, &metadata.load_segments)) + .try_fold(None::, |minimum, address| { + let address = address?; + Ok::<_, anyhow::Error>(Some( + minimum.map_or(address, |current| current.min(address)), + )) + })? + .ok_or_else(|| anyhow!("no file-backed allocated section")) +} + +fn section_lma(section: &LoadSection, segments: &[LoadSegment]) -> Result { + let section_file_end = checked_end(section.file_offset, section.size, "section file range")?; + let section_virtual_end = checked_end( + section.virtual_address, + section.size, + "section virtual range", + )?; + let mut candidates = Vec::new(); + + for segment in segments.iter().filter(|segment| segment.file_size != 0) { + valid_segment_range(segment)?; + let segment_file_end = + checked_end(segment.file_offset, segment.file_size, "PT_LOAD file range")?; + let segment_virtual_end = checked_end( + segment.virtual_address, + segment.file_size, + "PT_LOAD virtual range", + )?; + if section.file_offset < segment.file_offset + || section_file_end > segment_file_end + || section.virtual_address < segment.virtual_address + || section_virtual_end > segment_virtual_end + { + continue; + } + + let file_delta = section.file_offset - segment.file_offset; + let virtual_delta = section.virtual_address - segment.virtual_address; + if file_delta != virtual_delta { + continue; + } + candidates.push( + segment + .physical_address + .checked_add(file_delta) + .ok_or_else(|| anyhow!("PT_LOAD LMA overflow"))?, + ); + } + + let first = candidates.first().copied().ok_or_else(|| { + anyhow!("an allocated file-backed section has no matching PT_LOAD segment") + })?; + if candidates.iter().any(|candidate| *candidate != first) { + bail!("an allocated file-backed section has ambiguous PT_LOAD LMA mappings"); + } + Ok(first) +} + +fn valid_segment_range(segment: &LoadSegment) -> Result<()> { + checked_end(segment.file_offset, segment.file_size, "PT_LOAD file range")?; + checked_end( + segment.virtual_address, + segment.file_size, + "PT_LOAD virtual range", + )?; + checked_end( + segment.physical_address, + segment.file_size, + "PT_LOAD physical range", + )?; + Ok(()) +} + +fn checked_end(start: u64, size: u64, kind: &str) -> Result { + start + .checked_add(size) + .ok_or_else(|| anyhow!("{kind} overflows")) +} + +#[cfg(test)] +mod tests { + use super::*; + use object::Architecture; + + #[test] + fn resolves_payload_addresses_and_rejects_ambiguous_auto_values() { + let mut metadata = ElfMetadata { + arch: Architecture::Riscv64, + entry: 0x8000_0100, + executable_start: None, + load_segments: vec![LoadSegment { + virtual_address: 0x8000_0000, + physical_address: 0x8000_0000, + file_offset: 0x100, + file_size: 0x200, + memory_size: 0x200, + alignment: 1, + flags: 0, + }], + load_sections: vec![LoadSection { + virtual_address: 0x8000_0100, + file_offset: 0x200, + size: 0x20, + }], + }; + let mut config = FitConfig::default(); + let load = config.resolve_load(&metadata).unwrap(); + assert_eq!(load, 0x8000_0100); + assert_eq!( + config.resolve_entry(&metadata, load, &[]).unwrap(), + metadata.entry + ); + metadata.executable_start = Some(0x9000_0100); + assert!(config.resolve_load(&metadata).is_err()); + metadata.load_segments[0].virtual_address += 0x1000_0000; + metadata.load_sections[0].virtual_address += 0x1000_0000; + metadata.entry += 0x1000_0000; + assert!(config.resolve_entry(&metadata, load, &[]).is_err()); + config.entry = FitAddress::Explicit(0); + config.load = FitAddress::Explicit(0); + assert_eq!(config.resolve_load(&metadata).unwrap(), 0); + assert_eq!(config.resolve_entry(&metadata, 0, &[]).unwrap(), 0); + config.load = FitAddress::Auto; + metadata.executable_start = None; + config.format = FitFormat::Elf; + let load = config.resolve_load(&metadata).unwrap(); + assert_eq!(load, 0x7fff_ff00); + // A VMA symbol cannot override the file base, even if it names entry. + metadata.executable_start = Some(metadata.entry); + assert!(config.resolve_load(&metadata).is_err()); + metadata.executable_start = Some(load); + assert_eq!(config.resolve_load(&metadata).unwrap(), load); + config.entry = FitAddress::Auto; + assert!(config.resolve_entry(&metadata, load, &[]).is_err()); + config.entry = FitAddress::Explicit(metadata.entry); + assert_eq!( + config.resolve_entry(&metadata, load, &[]).unwrap(), + metadata.entry + ); + config.os = FitOs::Elf; + assert!(config.resolve_load(&metadata).is_err()); + config.load = FitAddress::Explicit(load); + config.entry = FitAddress::Auto; + assert_eq!(config.resolve_entry(&metadata, load, &[]).unwrap(), load); + config.entry = FitAddress::Explicit(metadata.entry); + assert!(config.resolve_entry(&metadata, load, &[]).is_err()); + config.format = FitFormat::Bin; + assert!(config.resolve_load(&metadata).is_err()); + + // The same section mapping failure belongs to entry for ELF, load for BIN. + metadata.load_sections[0].file_offset = 0x400; + config = FitConfig { + format: FitFormat::Elf, + ..FitConfig::default() + }; + assert_eq!(config.resolve_load(&metadata).unwrap(), load); + let entry_error = config.resolve_entry(&metadata, load, &[]).unwrap_err(); + assert_eq!( + entry_error.to_string(), + "cannot derive FIT entry; set entry explicitly" + ); + assert!(!format!("{entry_error:#}").contains("BIN FIT load")); + assert!(!format!("{entry_error:#}").contains("set load explicitly")); + config.format = FitFormat::Bin; + let load_error = config.resolve_load(&metadata).unwrap_err(); + assert_eq!( + load_error.to_string(), + "cannot derive BIN FIT load address; set load explicitly" + ); + assert_eq!( + entry_error.root_cause().to_string(), + load_error.root_cause().to_string() + ); + } + + #[test] + fn rejects_incompatible_os_and_format_from_both_resolvers() { + let metadata = ElfMetadata { + arch: Architecture::Aarch64, + entry: 0, + executable_start: None, + load_segments: Vec::new(), + load_sections: Vec::new(), + }; + let config = FitConfig { + format: FitFormat::Bin, + os: FitOs::Elf, + load: FitAddress::Explicit(0x4000_0000), + ..FitConfig::default() + }; + + let load_error = config.resolve_load(&metadata).unwrap_err(); + assert_eq!( + load_error.to_string(), + "FIT OS elf requires format elf and U-Boot CONFIG_BOOTM_ELF" + ); + let entry_error = config + .resolve_entry(&metadata, 0x4000_0000, &[]) + .unwrap_err(); + assert_eq!(entry_error.to_string(), load_error.to_string()); + } + + #[test] + fn resolves_loongarch_image_entry_in_the_configured_entry_path() { + let metadata = ElfMetadata { + arch: Architecture::LoongArch64, + entry: 0x1000, + executable_start: None, + load_segments: Vec::new(), + load_sections: Vec::new(), + }; + let mut payload = vec![0_u8; 0x40_0000]; + payload[..2].copy_from_slice(b"MZ"); + payload[8..16].copy_from_slice(&0x5c_8b40_u64.to_le_bytes()); + payload[24..32].copy_from_slice(&0x20_0000_u64.to_le_bytes()); + let config = FitConfig { + format: FitFormat::Bin, + load: FitAddress::Explicit(0x9000_0000_9800_0000), + ..FitConfig::default() + }; + + assert_eq!( + config + .resolve_entry(&metadata, 0x9000_0000_9800_0000, &payload) + .unwrap(), + 0x9000_0000_983c_8b40 + ); + } + + #[test] + fn allows_elf_container_gaps_with_one_file_base() { + let metadata = ElfMetadata { + arch: Architecture::Aarch64, + entry: 0x8000_0100, + executable_start: None, + load_segments: vec![ + LoadSegment { + virtual_address: 0x8000_0000, + physical_address: 0x8000_0000, + file_offset: 0x100, + file_size: 0x200, + memory_size: 0x200, + alignment: 0x1000, + flags: 5, + }, + LoadSegment { + virtual_address: 0x9000_0000, + physical_address: 0x8000_0300, + file_offset: 0x400, + file_size: 0x100, + memory_size: 0x100, + alignment: 0x1000, + flags: 4, + }, + ], + load_sections: vec![ + LoadSection { + virtual_address: 0x8000_0100, + file_offset: 0x200, + size: 0x20, + }, + LoadSection { + virtual_address: 0x9000_0000, + file_offset: 0x400, + size: 0x20, + }, + ], + }; + let config = FitConfig { + format: FitFormat::Elf, + ..FitConfig::default() + }; + + let load = config.resolve_load(&metadata).unwrap(); + assert_eq!(load, 0x7fff_ff00); + assert_eq!( + config.resolve_entry(&metadata, load, &[]).unwrap(), + metadata.entry + ); + } + + #[test] + fn rejects_elf_segments_with_different_container_bases() { + let mut metadata = ElfMetadata { + arch: Architecture::Aarch64, + entry: 0, + executable_start: None, + load_segments: vec![ + LoadSegment { + virtual_address: 0x8000_0000, + physical_address: 0x8000_0100, + file_offset: 0x100, + file_size: 0x100, + memory_size: 0x100, + alignment: 0x1000, + flags: 5, + }, + LoadSegment { + virtual_address: 0x9000_0000, + physical_address: 0x8000_0400, + file_offset: 0x400, + file_size: 0x100, + memory_size: 0x100, + alignment: 0x1000, + flags: 4, + }, + ], + load_sections: Vec::new(), + }; + let config = FitConfig { + format: FitFormat::Elf, + ..FitConfig::default() + }; + assert!(config.resolve_load(&metadata).is_ok()); + + metadata.load_segments[1].physical_address += 0x1000; + let error = config.resolve_load(&metadata).unwrap_err(); + assert!(format!("{error:#}").contains("no single load address preserves this layout")); + } + + #[test] + fn config_round_trips_json_and_toml_with_explicit_zero_and_high_addresses() { + let config = FitConfig { + format: FitFormat::Elf, + os: FitOs::Elf, + load: FitAddress::Explicit(0), + entry: FitAddress::Explicit(0x9000_0000_8000_0000), + fdt: Some(FitFdt { + path: PathBuf::from("board.dtb"), + load: Some(0), + }), + output: Some(PathBuf::from("boot.fit")), + }; + + let json = serde_json::to_string(&config).unwrap(); + assert_eq!(serde_json::from_str::(&json).unwrap(), config); + + let toml = toml::to_string(&config).unwrap(); + assert_eq!(toml::from_str::(&toml).unwrap(), config); + } +} diff --git a/ostool/src/boot/mod.rs b/ostool/src/boot/mod.rs index 11778a6..b8d4d74 100644 --- a/ostool/src/boot/mod.rs +++ b/ostool/src/boot/mod.rs @@ -2,4 +2,7 @@ pub(crate) mod artifacts; pub(crate) mod fit; +// The configuration entry point is internal until boot preparation consumes it. +#[allow(dead_code)] +pub(crate) mod fit_config; pub mod payload;