#![cfg(target_arch = "x86_64")]
use std::{
fs,
path::{Path, PathBuf},
process::Command,
};
#[path = "support/elf_image.rs"]
pub mod elf_image;
use elf_image::{ElfImage, MappingPermissions};
const FIXTURE_LINK_BASE: u64 = 0xffff_ffff_8000_0000;
#[test]
fn raw_x86_entry_relocates_in_naked_pic_code_before_rust() {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let head = fs::read_to_string(manifest_dir.join("src/arch/x86_64/head.rs"))
.expect("x86 head source must be readable");
let entry = fs::read_to_string(manifest_dir.join("src/arch/x86_64/entry.rs"))
.expect("x86 entry source must be readable");
let linker = fs::read_to_string(manifest_dir.join("src/arch/x86_64/link.ld"))
.expect("x86 linker template must be readable");
let raw_entry = function_body(&head, "pub unsafe extern \"C\" fn x86_64_raw_entry(");
assert!(
head.contains(
"#[unsafe(naked)]\n#[unsafe(no_mangle)]\n#[unsafe(link_section = \
\".head.text.100.raw_entry\")]\npub unsafe extern \"C\" fn x86_64_raw_entry("
),
"the raw image must enter a naked function before any Rust prologue"
);
for required in [
"lea r8, [rip + {head}]",
"__rela_dyn_begin",
"__rela_dyn_end",
"relative_relocation = const 8_u32",
"jmp {rust_entry}",
] {
assert!(
raw_entry.contains(required),
"raw x86 relocation entry is missing {required:?}"
);
}
assert!(
!raw_entry.contains("call "),
"raw relocation must not call Rust or use a return stack"
);
assert!(
!function_body(&entry, "pub extern \"C\" fn kernel_entry(").contains("relocate::relocate"),
"ordinary Rust entry is already too late to apply its own relocations"
);
assert!(
linker.contains("ENTRY(x86_64_raw_entry)"),
"ELF loaders must enter the relocation trampoline rather than the image header"
);
assert!(
linker.contains(
"_kernel_entry = ABSOLUTE(KERNEL_LOAD_ADDRESS + (x86_64_raw_entry - _head));"
),
"the raw loader entry must be a physical load-base-relative trampoline"
);
let archive = find_someboot_archive();
let output = Command::new("objdump")
.args(["-dr", "--disassemble=x86_64_raw_entry"])
.arg(&archive)
.output()
.unwrap_or_else(|error| panic!("failed to disassemble {}: {error}", archive.display()));
assert!(
output.status.success(),
"objdump failed for {}: {}",
archive.display(),
String::from_utf8_lossy(&output.stderr)
);
let disassembly = String::from_utf8(output.stdout).expect("objdump output must be UTF-8");
assert!(
disassembly.contains("<x86_64_raw_entry>:")
&& disassembly.contains("R_X86_64_PC32")
&& disassembly.contains("__rela_dyn_begin")
&& disassembly.contains("__rela_dyn_end")
&& disassembly.contains("kernel_entry"),
"compiled trampoline must retain only PC-relative symbol transfers:\n{disassembly}"
);
assert!(
!disassembly.lines().any(|line| line.contains("\tcall")),
"compiled raw trampoline must not contain a call instruction:\n{disassembly}"
);
verify_three_runtime_load_biases(&archive);
}
fn verify_three_runtime_load_biases(archive: &Path) {
let temp = std::env::temp_dir().join(format!(
"someboot-x86-relocation-{}-{}",
std::process::id(),
std::thread::current().name().unwrap_or("test")
));
let _ = fs::remove_dir_all(&temp);
fs::create_dir_all(&temp).expect("temporary relocation fixture directory must be created");
let full_object = temp.join("someboot-head.o");
extract_archive_member_with_symbol(archive, "x86_64_raw_entry", &full_object);
let fixture_object = temp.join("someboot-head-fixture.o");
run_checked(
Command::new("objcopy")
.arg("--remove-relocations=.head.text.000.header")
.arg(&full_object)
.arg(&fixture_object),
"remove non-executed image-header relocations from the loader fixture",
);
let stub_source = temp.join("fixture.S");
fs::write(
&stub_source,
r#"
.section .text.fixture,"ax",@progbits
.globl kernel_entry
.type kernel_entry,@function
kernel_entry:
lea marker(%rip), %rdx
mov target_ptr(%rip), %rax
cmp %rdx, %rax
sete %al
movzbq %al, %rax
ret
.globl __x86_64_efi_pe_entry
.type __x86_64_efi_pe_entry,@function
__x86_64_efi_pe_entry:
ret
.section .data.fixture,"aw",@progbits
.p2align 3
target_ptr:
.quad marker
.hidden marker
marker:
.quad 0x5a5aa5a5
"#,
)
.expect("fixture assembly must be written");
let stub_object = temp.join("fixture.o");
run_checked(
Command::new("cc")
.args(["-c", "-fPIC"])
.arg(&stub_source)
.arg("-o")
.arg(&stub_object),
"assemble relocation fixture",
);
let linker_script = temp.join("fixture.ld");
fs::write(
&linker_script,
format!(
r#"
OUTPUT_FORMAT(elf64-x86-64)
ENTRY(x86_64_raw_entry)
PHDRS {{
image PT_LOAD FLAGS(7);
dynamic PT_DYNAMIC FLAGS(6);
}}
SECTIONS {{
. = {FIXTURE_LINK_BASE:#x};
.head.text : {{
*(.head.text.000.header)
*(.head.text.100.raw_entry)
}} :image
.text : {{ *(.text.fixture) }} :image
.rodata : {{ *(.rodata*) }} :image
.data : {{ *(.data.fixture) *(.got .got.*) }} :image
.dynamic : {{ *(.dynamic) }} :image :dynamic
.rela.dyn : {{
__rela_dyn_begin = .;
*(.rela.dyn)
}} :image
__rela_dyn_end = ADDR(.rela.dyn) + SIZEOF(.rela.dyn);
_end = .;
PAGE_SIZE = 0x1000;
PECOFF_FILE_ALIGN = 0x200;
_kernel_entry = x86_64_raw_entry;
_kernel_asize = _end - _head;
_kernel_code_size = SIZEOF(.text);
_kernel_rsize = SIZEOF(.data);
_kernel_bss_size = 0;
_kernel_text_offset = ADDR(.text) - _head;
_kernel_image_size = _end - _head;
_etext = ADDR(.data);
}}
"#
),
)
.expect("fixture linker script must be written");
let fixture = temp.join("raw-entry-fixture.elf");
run_checked(
Command::new("ld")
.args([
"-pie",
"--no-dynamic-linker",
"--gc-sections",
"-z",
"norelro",
])
.arg("-T")
.arg(&linker_script)
.arg(&fixture_object)
.arg(&stub_object)
.arg("-o")
.arg(&fixture),
"link the executable relocation fixture",
);
let relocations = run_output(
Command::new("readelf").args(["-rW"]).arg(&fixture),
"read fixture relocations",
);
assert!(
relocations.contains("R_X86_64_RELATIVE"),
"fixture must exercise a real ELF relative relocation:\n{relocations}"
);
let image = fs::read(&fixture).expect("linked relocation fixture must be readable");
let elf = ElfImage::parse(&image);
let mut mappings = Vec::new();
for _ in 0..3 {
mappings.push(elf.load(&image, MappingPermissions::ReadWriteExecute));
}
let biases = mappings
.iter()
.map(|mapping| mapping.base_address().wrapping_sub(elf.link_base()))
.collect::<Vec<_>>();
assert_eq!(
biases
.iter()
.copied()
.collect::<std::collections::BTreeSet<_>>()
.len(),
3,
"the loader fixture must exercise three distinct runtime slides"
);
for mapping in &mappings {
let entry: unsafe extern "C" fn() -> usize =
unsafe { core::mem::transmute(elf.entry_address(mapping)) };
assert_eq!(
unsafe { entry() },
1,
"relocation failed at a runtime slide"
);
}
drop(mappings);
fs::remove_dir_all(&temp).expect("temporary relocation fixture must be removable");
}
fn extract_archive_member_with_symbol(archive: &Path, symbol: &str, output: &Path) {
let members = run_output(
Command::new("ar").arg("t").arg(archive),
"list someboot archive members",
);
for member in members.lines().filter(|member| !member.is_empty()) {
let extracted = Command::new("ar")
.arg("p")
.arg(archive)
.arg(member)
.output()
.expect("archive member extraction must start");
assert!(extracted.status.success(), "failed to extract {member}");
fs::write(output, extracted.stdout).expect("archive member must be written");
let symbols = run_output(
Command::new("nm").arg(output),
"inspect someboot archive member",
);
if symbols.contains(symbol) {
return;
}
}
panic!(
"no archive member in {} defines {symbol}",
archive.display()
);
}
fn run_checked(command: &mut Command, operation: &str) {
let output = command
.output()
.unwrap_or_else(|error| panic!("failed to {operation}: {error}"));
assert!(
output.status.success(),
"failed to {operation}:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
fn run_output(command: &mut Command, operation: &str) -> String {
let output = command
.output()
.unwrap_or_else(|error| panic!("failed to {operation}: {error}"));
assert!(
output.status.success(),
"failed to {operation}: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout).expect("tool output must be UTF-8")
}
fn find_someboot_archive() -> PathBuf {
let deps = std::env::current_exe()
.expect("test executable path must be available")
.parent()
.expect("test executable must live in a dependency directory")
.to_path_buf();
let mut candidates = fs::read_dir(&deps)
.expect("dependency directory must be readable")
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with("libsomeboot-") && name.ends_with(".rlib"))
})
.collect::<Vec<_>>();
candidates.sort_by_key(|path| {
path.metadata()
.and_then(|metadata| metadata.modified())
.ok()
});
candidates
.into_iter()
.rev()
.find(|archive| {
Command::new("nm")
.arg(archive)
.output()
.is_ok_and(|output| {
output.status.success()
&& String::from_utf8_lossy(&output.stdout).contains("x86_64_raw_entry")
})
})
.unwrap_or_else(|| {
panic!(
"no someboot archive with raw x86 entry found in {}",
deps.display()
)
})
}
fn function_body<'source>(source: &'source str, signature: &str) -> &'source str {
let function_start = source
.find(signature)
.unwrap_or_else(|| panic!("missing function signature {signature:?}"));
let body_start = source[function_start..]
.find('{')
.map(|offset| function_start + offset)
.expect("function must have a body");
let mut depth = 0usize;
for (offset, byte) in source[body_start..].bytes().enumerate() {
match byte {
b'{' => depth += 1,
b'}' => {
depth -= 1;
if depth == 0 {
return &source[body_start..=body_start + offset];
}
}
_ => {}
}
}
panic!("unterminated function body for {signature:?}")
}