Merge PR #6: support SysV-only ELF hash tables

This commit is contained in:
2026-09-20 10:04:23 +08:00
3 changed files with 101 additions and 39 deletions
+7 -10
View File
@@ -15,26 +15,23 @@ pub use layout::{
/// ELF machine identifier for AArch64.
pub const AARCH64_MACHINE: u16 = EM_AARCH64;
/// Dynamic sections required by the restored AArch64 loader image.
pub const DYNAMIC_SECTION_NAMES: [&str; 8] = [
/// Dynamic sections required by every restored AArch64 loader image.
pub const DYNAMIC_SECTION_NAMES: [&str; 7] = [
".dynsym",
".gnu.version",
".gnu.version_r",
".gnu.hash",
".dynstr",
".rela.dyn",
".rela.plt",
".dynamic",
];
/// Supported dynamic symbol-hash sections. At least one must be present.
pub const SYMBOL_HASH_SECTION_NAMES: [&str; 2] = [".gnu.hash", ".hash"];
/// Dynamic sections needed to identify a protected image before extraction.
pub const PROBE_SECTION_NAMES: [&str; 5] = [
".dynsym",
".dynstr",
".gnu.hash",
".gnu.version",
".gnu.version_r",
];
/// Hash tables are checked separately because either GNU or SysV hashing is valid.
pub const PROBE_SECTION_NAMES: [&str; 4] = [".dynsym", ".dynstr", ".gnu.version", ".gnu.version_r"];
/// ELF64 dynamic table record sizes.
pub const ELF64_SYMBOL_SIZE: usize = 0x18;
@@ -70,6 +70,16 @@ pub(crate) fn inspect(
return invalid(format!("protected ELF lacks required section {wanted}"));
}
}
let has_symbol_hash = senbei_elf::SYMBOL_HASH_SECTION_NAMES.iter().any(|wanted| {
elf.section_headers.iter().any(|section| {
elf.shdr_strtab
.get_at(section.sh_name)
.is_some_and(|name| name == *wanted)
})
});
if !has_symbol_hash {
return invalid("protected ELF lacks a .gnu.hash or .hash symbol-hash section");
}
let (section_index, section) = matches[0];
let section_offset = usize::try_from(section.sh_offset)
.map_err(|_| Error::Invalid("SHT_LOUSER offset exceeds usize".to_owned()))?;
+84 -29
View File
@@ -757,7 +757,18 @@ fn dynamic_contains_tag(output: &[u8], dynamic: SectionHeader, wanted: u64) -> R
}
fn required_section_indices(names: &[String]) -> Result<HashMap<&'static str, usize>> {
let mut result = HashMap::with_capacity(senbei_elf::DYNAMIC_SECTION_NAMES.len());
let present = senbei_elf::SYMBOL_HASH_SECTION_NAMES
.iter()
.filter(|name| names.iter().any(|section| section == *name))
.count();
if present == 0 {
return invalid(
"ELF must carry at least one of the .gnu.hash / .hash symbol-hash sections",
);
}
let mut result = HashMap::with_capacity(
senbei_elf::DYNAMIC_SECTION_NAMES.len() + senbei_elf::SYMBOL_HASH_SECTION_NAMES.len(),
);
for required in senbei_elf::DYNAMIC_SECTION_NAMES {
let indices = names
.iter()
@@ -772,17 +783,19 @@ fn required_section_indices(names: &[String]) -> Result<HashMap<&'static str, us
_ => return invalid(format!("ELF contains duplicate section {required}")),
}
}
let sysv_hash = names
.iter()
.enumerate()
.filter_map(|(index, name)| (name == ".hash").then_some(index))
.collect::<Vec<_>>();
match sysv_hash.as_slice() {
[index] => {
result.insert(".hash", *index);
for hash_table in senbei_elf::SYMBOL_HASH_SECTION_NAMES {
let indices = names
.iter()
.enumerate()
.filter_map(|(index, name)| (name == hash_table).then_some(index))
.collect::<Vec<_>>();
match indices.as_slice() {
[index] => {
result.insert(hash_table, *index);
}
[] => {}
_ => return invalid(format!("ELF contains duplicate section {hash_table}")),
}
[] => {}
_ => return invalid("ELF contains duplicate section .hash"),
}
Ok(result)
}
@@ -862,17 +875,21 @@ fn metadata_mapping_length(
usize_from_u64(rela_plt.size / ELF64_RELA_SIZE as u64, ".rela.plt count")?
.checked_add(auxiliary.relocation2_count as usize)
.ok_or_else(|| Error::Invalid("merged .rela.plt count overflow".to_owned()))?;
let gnu_hash_size = 28_usize
.checked_add(
new_symbol_count
.checked_sub(1)
.ok_or_else(|| {
Error::Invalid("dynamic symbol table is unexpectedly empty".to_owned())
})?
.checked_mul(4)
.ok_or_else(|| Error::Invalid("GNU hash size overflow".to_owned()))?,
)
.ok_or_else(|| Error::Invalid("GNU hash size overflow".to_owned()))?;
let gnu_hash_size = if indices.contains_key(".gnu.hash") {
28_usize
.checked_add(
new_symbol_count
.checked_sub(1)
.ok_or_else(|| {
Error::Invalid("dynamic symbol table is unexpectedly empty".to_owned())
})?
.checked_mul(4)
.ok_or_else(|| Error::Invalid("GNU hash size overflow".to_owned()))?,
)
.ok_or_else(|| Error::Invalid("GNU hash size overflow".to_owned()))?
} else {
0
};
let sysv_hash_size = indices.contains_key(".hash").then(|| {
new_symbol_count
.checked_mul(2)
@@ -1031,7 +1048,10 @@ fn materialize_static_elf_tables(
.contains_key(".hash")
.then(|| build_sysv_hash(&merged_names))
.transpose()?;
let gnu_hash_table = build_gnu_hash(&merged_names)?;
let gnu_hash_table = indices
.contains_key(".gnu.hash")
.then(|| build_gnu_hash(&merged_names))
.transpose()?;
let new_symbol_count = merged_names.len();
let new_dynstr_size = merged_strings.len();
@@ -1102,6 +1122,11 @@ fn materialize_static_elf_tables(
let rela_dyn_count = merged_rela_dyn.len() / ELF64_RELA_SIZE;
let rela_plt_count = merged_rela_plt.len() / ELF64_RELA_SIZE;
let gnu_hash = gnu_hash_table.map(|data| TablePayload {
name: ".gnu.hash",
alignment: 8,
data,
});
let mut tables = vec![
TablePayload {
name: ".dynsym",
@@ -1118,12 +1143,10 @@ fn materialize_static_elf_tables(
alignment: 4,
data: version_requirements,
},
TablePayload {
name: ".gnu.hash",
alignment: 8,
data: gnu_hash_table,
},
];
if let Some(payload) = gnu_hash {
tables.push(payload);
}
if let Some(data) = sysv_hash {
tables.push(TablePayload {
name: ".hash",
@@ -1231,10 +1254,12 @@ fn materialize_static_elf_tables(
(DT_RELASZ, (rela_dyn_count * ELF64_RELA_SIZE) as u64),
(DT_STRSZ, new_dynstr_size as u64),
(DT_JMPREL, section_address(".rela.plt")),
(DT_GNU_HASH, section_address(".gnu.hash")),
(DT_VERSYM, section_address(".gnu.version")),
(DT_VERNEED, section_address(".gnu.version_r")),
]);
if indices.contains_key(".gnu.hash") {
dynamic_values.insert(DT_GNU_HASH, section_address(".gnu.hash"));
}
if dynamic_contains_tag(output, dynamic, DT_RELACOUNT)? {
dynamic_values.insert(DT_RELACOUNT, relative_count as u64);
}
@@ -1727,4 +1752,34 @@ mod tests {
let error = AuxiliaryElfImage::parse(&auxiliary_image(0)).expect_err("missing null symbol");
assert!(error.to_string().contains("no null entry"));
}
fn dynamic_section_names(hash_sections: &[&str]) -> Vec<String> {
senbei_elf::DYNAMIC_SECTION_NAMES
.into_iter()
.chain(hash_sections.iter().copied())
.map(str::to_owned)
.collect()
}
#[test]
fn required_sections_accept_each_hash_layout() {
for hashes in [
[".gnu.hash"].as_slice(),
[".hash"].as_slice(),
[".gnu.hash", ".hash"].as_slice(),
] {
let indices = required_section_indices(&dynamic_section_names(hashes))
.expect("valid hash layout");
for hash in hashes {
assert!(indices.contains_key(hash));
}
}
}
#[test]
fn required_sections_reject_missing_symbol_hash() {
let error =
required_section_indices(&dynamic_section_names(&[])).expect_err("missing hash");
assert!(error.to_string().contains("at least one"));
}
}