use crate::error::{Error, Result};
pub const PREV_INUSE: u64 = 0x1;
pub const IS_MMAPPED: u64 = 0x2;
pub const NON_MAIN_ARENA: u64 = 0x4;
pub const SIZE_MASK: u64 = !0x7;
pub const NFASTBINS: usize = 10;
pub const TCACHE_MAX_BINS: usize = 64;
pub const NBINS: usize = 254;
#[derive(Debug, Clone)]
pub struct MallocChunk {
pub addr: u64,
pub prev_size: u64,
pub size_raw: u64,
pub size: u64,
pub prev_inuse: bool,
pub is_mmapped: bool,
pub non_main_arena: bool,
pub fd: u64,
pub bk: u64,
}
#[derive(Debug, Clone)]
pub struct MallocState {
pub addr: u64,
pub fastbins: Vec<u64>,
pub top: u64,
pub last_remainder: u64,
pub bins: Vec<u64>,
pub system_mem: u64,
}
#[derive(Debug, Clone)]
pub struct TcacheEntry {
pub bin: usize,
pub count: u16,
pub head: u64,
}
#[derive(Debug, Clone)]
pub struct BinEntry {
pub bin_type: BinType,
pub index: usize,
pub chunk_addr: u64,
pub chunk_size: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BinType {
Tcache,
Fast,
Unsorted,
Small,
Large,
}
impl std::fmt::Display for BinType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Tcache => write!(f, "tcache"),
Self::Fast => write!(f, "fastbin"),
Self::Unsorted => write!(f, "unsorted"),
Self::Small => write!(f, "smallbin"),
Self::Large => write!(f, "largebin"),
}
}
}
pub fn parse_chunk(data: &[u8], addr: u64) -> Result<MallocChunk> {
if data.len() < 16 {
return Err(Error::Other("chunk data too short (need 16+ bytes)".into()));
}
let prev_size = u64::from_le_bytes(data[0..8].try_into().unwrap());
let size_raw = u64::from_le_bytes(data[8..16].try_into().unwrap());
let fd = if data.len() >= 24 {
u64::from_le_bytes(data[16..24].try_into().unwrap())
} else {
0
};
let bk = if data.len() >= 32 {
u64::from_le_bytes(data[24..32].try_into().unwrap())
} else {
0
};
Ok(MallocChunk {
addr,
prev_size,
size_raw,
size: size_raw & SIZE_MASK,
prev_inuse: size_raw & PREV_INUSE != 0,
is_mmapped: size_raw & IS_MMAPPED != 0,
non_main_arena: size_raw & NON_MAIN_ARENA != 0,
fd,
bk,
})
}
pub fn walk_chunks<F>(
start_addr: u64,
top_addr: u64,
read_memory: &F,
max_chunks: usize,
) -> Result<Vec<MallocChunk>>
where
F: Fn(u64, usize) -> Result<Vec<u8>>,
{
let max_chunks = if max_chunks == 0 { 4096 } else { max_chunks };
let mut chunks = Vec::new();
let mut addr = start_addr;
for _ in 0..max_chunks {
let data = read_memory(addr, 32)?;
let chunk = parse_chunk(&data, addr)?;
if chunk.size == 0 || chunk.size > 0x1_0000_0000 {
break; }
let is_top = addr == top_addr;
chunks.push(chunk.clone());
if is_top {
break;
}
addr += chunk.size;
}
Ok(chunks)
}
pub fn walk_fastbin<F>(head: u64, read_memory: &F, max_entries: usize) -> Result<Vec<u64>>
where
F: Fn(u64, usize) -> Result<Vec<u8>>,
{
let max_entries = if max_entries == 0 { 256 } else { max_entries };
let mut entries = Vec::new();
let mut ptr = head;
for _ in 0..max_entries {
if ptr == 0 {
break;
}
entries.push(ptr);
let data = read_memory(ptr + 16, 8)?;
ptr = u64::from_le_bytes(data[..8].try_into().unwrap());
}
Ok(entries)
}
pub fn parse_tcache(data: &[u8]) -> Result<Vec<TcacheEntry>> {
if data.len() < 640 {
return Err(Error::Other(format!(
"tcache data too short: {} (need 640)",
data.len()
)));
}
let mut entries = Vec::new();
for i in 0..TCACHE_MAX_BINS {
let count = u16::from_le_bytes(data[i * 2..i * 2 + 2].try_into().unwrap());
let head_offset = 128 + i * 8; let head = u64::from_le_bytes(data[head_offset..head_offset + 8].try_into().unwrap());
if count > 0 || head != 0 {
entries.push(TcacheEntry {
bin: i,
count,
head,
});
}
}
Ok(entries)
}
pub fn classify_bin(index: usize) -> BinType {
match index {
0 => BinType::Unsorted,
1..=62 => BinType::Small,
_ => BinType::Large,
}
}
pub fn fastbin_size(index: usize) -> u64 {
(index as u64 + 2) * 16
}
pub fn tcache_bin_size(index: usize) -> u64 {
(index as u64 + 2) * 16
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_chunk_basic() {
let mut data = vec![0u8; 32];
data[8..16].copy_from_slice(&0x91u64.to_le_bytes());
data[16..24].copy_from_slice(&0xDEAD0000u64.to_le_bytes());
data[24..32].copy_from_slice(&0xBEEF0000u64.to_le_bytes());
let chunk = parse_chunk(&data, 0x1000).unwrap();
assert_eq!(chunk.addr, 0x1000);
assert_eq!(chunk.prev_size, 0);
assert_eq!(chunk.size_raw, 0x91);
assert_eq!(chunk.size, 0x90);
assert!(chunk.prev_inuse);
assert!(!chunk.is_mmapped);
assert!(!chunk.non_main_arena);
assert_eq!(chunk.fd, 0xDEAD0000);
assert_eq!(chunk.bk, 0xBEEF0000);
}
#[test]
fn parse_chunk_flags() {
let mut data = vec![0u8; 16];
data[8..16].copy_from_slice(&0x97u64.to_le_bytes());
let chunk = parse_chunk(&data, 0x2000).unwrap();
assert_eq!(chunk.size, 0x90);
assert!(chunk.prev_inuse);
assert!(chunk.is_mmapped);
assert!(chunk.non_main_arena);
}
#[test]
fn parse_chunk_too_short() {
let data = vec![0u8; 8];
assert!(parse_chunk(&data, 0).is_err());
}
#[test]
fn fastbin_sizes() {
assert_eq!(fastbin_size(0), 32);
assert_eq!(fastbin_size(1), 48);
assert_eq!(fastbin_size(2), 64);
assert_eq!(fastbin_size(9), 176);
}
#[test]
fn tcache_sizes() {
assert_eq!(tcache_bin_size(0), 32);
assert_eq!(tcache_bin_size(1), 48);
assert_eq!(tcache_bin_size(63), 1040);
}
#[test]
fn classify_bins() {
assert_eq!(classify_bin(0), BinType::Unsorted);
assert_eq!(classify_bin(1), BinType::Small);
assert_eq!(classify_bin(62), BinType::Small);
assert_eq!(classify_bin(63), BinType::Large);
}
#[test]
fn parse_tcache_basic() {
let mut data = vec![0u8; 640];
data[0..2].copy_from_slice(&2u16.to_le_bytes());
data[128..136].copy_from_slice(&0x5555_0000u64.to_le_bytes());
data[10..12].copy_from_slice(&1u16.to_le_bytes());
data[168..176].copy_from_slice(&0x6666_0000u64.to_le_bytes());
let entries = parse_tcache(&data).unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].bin, 0);
assert_eq!(entries[0].count, 2);
assert_eq!(entries[0].head, 0x5555_0000);
assert_eq!(entries[1].bin, 5);
assert_eq!(entries[1].count, 1);
}
#[test]
fn walk_chunks_basic() {
let read_memory = |addr: u64, len: usize| -> Result<Vec<u8>> {
let mut data = vec![0u8; len];
match addr {
0x1000 => {
if len >= 16 {
data[8..16].copy_from_slice(&0x41u64.to_le_bytes());
}
}
0x1040 => {
if len >= 16 {
data[8..16].copy_from_slice(&0x41u64.to_le_bytes());
}
}
_ => {}
}
Ok(data)
};
let chunks = walk_chunks(0x1000, 0x1040, &read_memory, 100).unwrap();
assert_eq!(chunks.len(), 2);
assert_eq!(chunks[0].addr, 0x1000);
assert_eq!(chunks[0].size, 0x40);
assert_eq!(chunks[1].addr, 0x1040);
}
}