use core::mem;
use crate::elf;
use crate::endian::{U32, U64};
use crate::read::{ReadError, ReadRef, Result, SymbolIndex};
use super::{FileHeader, Sym, SymbolTable, Version, VersionTable};
#[derive(Debug)]
pub struct HashTable<'data, Elf: FileHeader> {
buckets: &'data [U32<Elf::Endian>],
chains: &'data [U32<Elf::Endian>],
}
impl<'data, Elf: FileHeader> HashTable<'data, Elf> {
pub fn parse(endian: Elf::Endian, data: &'data [u8]) -> Result<Self> {
let mut offset = 0;
let header = data
.read::<elf::HashHeader<Elf::Endian>>(&mut offset)
.read_error("Invalid hash header")?;
let buckets = data
.read_slice(&mut offset, header.bucket_count.get(endian) as usize)
.read_error("Invalid hash buckets")?;
let chains = data
.read_slice(&mut offset, header.chain_count.get(endian) as usize)
.read_error("Invalid hash chains")?;
Ok(HashTable { buckets, chains })
}
pub fn symbol_table_length(&self) -> u32 {
self.chains.len() as u32
}
fn bucket(&self, endian: Elf::Endian, hash: u32) -> Option<SymbolIndex> {
let bucket_count = self.buckets.len();
if bucket_count == 0 {
return None;
}
Some(SymbolIndex(
self.buckets[(hash as usize) % bucket_count].get(endian) as usize,
))
}
fn chain(&self, endian: Elf::Endian, index: SymbolIndex) -> Option<SymbolIndex> {
Some(SymbolIndex(self.chains.get(index.0)?.get(endian) as usize))
}
pub fn find<R: ReadRef<'data>>(
&self,
endian: Elf::Endian,
name: &[u8],
hash: u32,
version: Option<&Version<'_>>,
symbols: &SymbolTable<'data, Elf, R>,
versions: &VersionTable<'data, Elf>,
) -> Option<(SymbolIndex, &'data Elf::Sym)> {
let mut index = self.bucket(endian, hash)?;
let mut i = 0;
let strings = symbols.strings();
while index != SymbolIndex(0) && i < self.chains.len() {
if let Ok(symbol) = symbols.symbol(index) {
if symbol.name(endian, strings) == Ok(name)
&& versions.matches(endian, index, version)
{
return Some((index, symbol));
}
}
index = self.chain(endian, index)?;
i += 1;
}
None
}
}
#[derive(Debug)]
pub struct GnuHashTable<'data, Elf: FileHeader> {
symbol_base: u32,
bloom_shift: u32,
bloom_filters: &'data [u8],
buckets: &'data [U32<Elf::Endian>],
values: &'data [U32<Elf::Endian>],
}
impl<'data, Elf: FileHeader> GnuHashTable<'data, Elf> {
pub fn parse(endian: Elf::Endian, data: &'data [u8]) -> Result<Self> {
let mut offset = 0;
let header = data
.read::<elf::GnuHashHeader<Elf::Endian>>(&mut offset)
.read_error("Invalid GNU hash header")?;
let bloom_len =
u64::from(header.bloom_count.get(endian)) * mem::size_of::<Elf::Word>() as u64;
let bloom_filters = data
.read_bytes(&mut offset, bloom_len)
.read_error("Invalid GNU hash bloom filters")?;
let buckets = data
.read_slice(&mut offset, header.bucket_count.get(endian) as usize)
.read_error("Invalid GNU hash buckets")?;
let chain_count = (data.len() - offset as usize) / 4;
let values = data
.read_slice(&mut offset, chain_count)
.read_error("Invalid GNU hash values")?;
Ok(GnuHashTable {
symbol_base: header.symbol_base.get(endian),
bloom_shift: header.bloom_shift.get(endian),
bloom_filters,
buckets,
values,
})
}
pub fn symbol_base(&self) -> u32 {
self.symbol_base
}
pub fn symbol_table_length(&self, endian: Elf::Endian) -> Option<u32> {
if self.symbol_base == 0 {
return None;
}
let max_bucket = self.buckets.iter().map(|b| b.get(endian)).max()?;
let mut chain_length = 0;
for value in self
.values
.get(max_bucket.checked_sub(self.symbol_base)? as usize..)?
{
chain_length += 1;
if value.get(endian) & 1 != 0 {
return max_bucket.checked_add(chain_length);
}
}
None
}
fn bucket(&self, endian: Elf::Endian, hash: u32) -> Option<SymbolIndex> {
let bucket_count = self.buckets.len();
if bucket_count == 0 {
return None;
}
Some(SymbolIndex(
self.buckets[(hash as usize) % bucket_count].get(endian) as usize,
))
}
pub fn find<R: ReadRef<'data>>(
&self,
endian: Elf::Endian,
name: &[u8],
hash: u32,
version: Option<&Version<'_>>,
symbols: &SymbolTable<'data, Elf, R>,
versions: &VersionTable<'data, Elf>,
) -> Option<(SymbolIndex, &'data Elf::Sym)> {
let word_bits = mem::size_of::<Elf::Word>() as u32 * 8;
let bloom_count = self.bloom_filters.len() / mem::size_of::<Elf::Word>();
if bloom_count == 0 {
return None;
}
let offset =
((hash / word_bits) & (bloom_count as u32 - 1)) * mem::size_of::<Elf::Word>() as u32;
let filter = if word_bits == 64 {
self.bloom_filters
.read_at::<U64<Elf::Endian>>(offset.into())
.ok()?
.get(endian)
} else {
self.bloom_filters
.read_at::<U32<Elf::Endian>>(offset.into())
.ok()?
.get(endian)
.into()
};
if filter & (1 << (hash % word_bits)) == 0 {
return None;
}
if filter & (1 << ((hash >> self.bloom_shift) % word_bits)) == 0 {
return None;
}
let mut index = self.bucket(endian, hash)?;
if index == SymbolIndex(0) {
return None;
}
let strings = symbols.strings();
let symbols = symbols.symbols().get(index.0..)?;
let values = self
.values
.get(index.0.checked_sub(self.symbol_base as usize)?..)?;
for (symbol, value) in symbols.iter().zip(values.iter()) {
let value = value.get(endian);
if value | 1 == hash | 1 {
if symbol.name(endian, strings) == Ok(name)
&& versions.matches(endian, index, version)
{
return Some((index, symbol));
}
}
if value & 1 != 0 {
break;
}
index.0 += 1;
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::elf;
use crate::endian::LittleEndian;
type FileHeader = elf::FileHeader64<LittleEndian>;
#[test]
fn hash_table_zero_buckets() {
let data = [0u8; 8];
let table = HashTable::<FileHeader>::parse(LittleEndian, &data).unwrap();
let symbols = SymbolTable::<FileHeader, &[u8]>::default();
let versions = VersionTable::default();
assert!(
table
.find(LittleEndian, b"foo", 0, None, &symbols, &versions)
.is_none()
);
}
#[test]
fn hash_table_chain_out_of_bounds() {
let data = [
1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, ];
let table = HashTable::<FileHeader>::parse(LittleEndian, &data).unwrap();
let symbols = SymbolTable::<FileHeader, &[u8]>::default();
let versions = VersionTable::default();
assert!(
table
.find(LittleEndian, b"foo", 0, None, &symbols, &versions)
.is_none()
);
}
#[test]
fn gnu_hash_table_zero_bloom() {
let data = [
1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, ];
let table = GnuHashTable::<FileHeader>::parse(LittleEndian, &data).unwrap();
let symbols = SymbolTable::<FileHeader, &[u8]>::default();
let versions = VersionTable::default();
assert!(
table
.find(LittleEndian, b"foo", 0, None, &symbols, &versions)
.is_none()
);
}
#[test]
fn gnu_hash_table_zero_buckets() {
let data = [
0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 1, 0, 0, 0, ];
let table = GnuHashTable::<FileHeader>::parse(LittleEndian, &data).unwrap();
let symbols = SymbolTable::<FileHeader, &[u8]>::default();
let versions = VersionTable::default();
assert!(
table
.find(LittleEndian, b"foo", 0, None, &symbols, &versions)
.is_none()
);
}
#[test]
fn gnu_hash_table_symbol_length_overflow() {
let data = [
1, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 1, 0, 0, 0, ];
let table = GnuHashTable::<FileHeader>::parse(LittleEndian, &data).unwrap();
assert_eq!(table.symbol_table_length(LittleEndian), None);
}
}