use btf_rs::Btf;
use crate::monitor::Kva;
use crate::monitor::btf_render::{MemReader, RenderedValue, render_value_with_mem};
use crate::monitor::dump::hex_dump;
use crate::monitor::guest::GuestKernel;
use super::{
MAX_ELEM_SIZE, MAX_SDT_ALLOC_ENTRIES, MIN_ELEM_SIZE, SDT_TASK_CHUNK_BITMAP_U64S,
SDT_TASK_ENTS_PER_CHUNK, SDT_TASK_LEVELS, SdtAllocEntry, SdtAllocOffsets, SdtAllocatorSnapshot,
read_u64_at,
};
#[allow(clippy::too_many_arguments)]
pub fn walk_sdt_allocator(
kernel: &GuestKernel,
kern_vm_start: u64,
allocator_bytes: &[u8],
offsets: &SdtAllocOffsets,
btf: &Btf,
target_type_id: u32,
payload_type_reason: impl Into<String>,
allocator_name: impl Into<String>,
mem: &dyn MemReader,
) -> SdtAllocatorSnapshot {
let mut snap = SdtAllocatorSnapshot {
allocator_name: allocator_name.into(),
entries: Vec::new(),
truncated: false,
skipped_subtrees: 0,
elem_size: 0,
target_type_id,
payload_type_reason: payload_type_reason.into(),
all_slot_addrs: Vec::new(),
};
let pool_off = offsets.allocator_pool + offsets.pool_elem_size;
let Some(elem_size) = read_u64_at(allocator_bytes, pool_off) else {
return snap;
};
if !(MIN_ELEM_SIZE..=MAX_ELEM_SIZE).contains(&elem_size) {
snap.elem_size = elem_size;
return snap;
}
snap.elem_size = elem_size;
let header = offsets.data_header_size;
if elem_size < header as u64 {
return snap;
}
let payload_size = (elem_size - header as u64) as usize;
let Some(root_ptr) = read_u64_at(allocator_bytes, offsets.allocator_root) else {
return snap;
};
if root_ptr == 0 {
return snap;
}
let mut walker = TreeWalker {
kernel,
kern_vm_start,
offsets,
btf,
target_type_id,
payload_size,
mem,
out: &mut snap,
};
walker.descend(root_ptr, 0);
snap
}
struct TreeWalker<'a> {
kernel: &'a GuestKernel,
kern_vm_start: u64,
offsets: &'a SdtAllocOffsets,
btf: &'a Btf,
target_type_id: u32,
payload_size: usize,
mem: &'a dyn MemReader,
out: &'a mut SdtAllocatorSnapshot,
}
impl<'a> TreeWalker<'a> {
fn descend(&mut self, desc_ptr: u64, level: usize) {
if level >= SDT_TASK_LEVELS {
return;
}
if self.out.truncated {
return;
}
let Some(desc_pa) = self.translate_arena_ptr(desc_ptr) else {
self.out.skipped_subtrees = self.out.skipped_subtrees.saturating_add(1);
return;
};
let mut allocated = [0u64; SDT_TASK_CHUNK_BITMAP_U64S];
let mem = self.kernel.mem();
for (i, slot) in allocated.iter_mut().enumerate() {
*slot = mem.read_u64(desc_pa, self.offsets.desc_allocated + i * 8);
}
let nr_free = mem.read_u64(desc_pa, self.offsets.desc_nr_free);
if nr_free > SDT_TASK_ENTS_PER_CHUNK as u64 {
self.out.skipped_subtrees = self.out.skipped_subtrees.saturating_add(1);
return;
}
let chunk_ptr = mem.read_u64(desc_pa, self.offsets.desc_chunk);
if chunk_ptr == 0 {
self.out.skipped_subtrees = self.out.skipped_subtrees.saturating_add(1);
return;
}
let Some(chunk_pa) = self.translate_arena_ptr(chunk_ptr) else {
self.out.skipped_subtrees = self.out.skipped_subtrees.saturating_add(1);
return;
};
if level == SDT_TASK_LEVELS - 1 {
for (word_idx, &word_value) in allocated.iter().enumerate() {
let mut word = word_value;
while word != 0 {
let bit = word.trailing_zeros() as usize;
word &= word - 1;
let pos = word_idx * 64 + bit;
if pos >= SDT_TASK_ENTS_PER_CHUNK {
continue;
}
let entry_ptr_off = self.offsets.chunk_union + pos * 8;
let entry_ptr = mem.read_u64(chunk_pa, entry_ptr_off);
if entry_ptr == 0 {
tracing::debug!(
allocator = %self.out.allocator_name,
pos,
"sdt_alloc walker: leaf data[pos] == 0 (bit set, \
pointer store not yet committed — scx_alloc_internal \
populates the pointer after the bitmap bit)",
);
continue;
}
self.emit_leaf(entry_ptr);
}
}
} else {
for pos in 0..SDT_TASK_ENTS_PER_CHUNK {
let entry_ptr_off = self.offsets.chunk_union + pos * 8;
let entry_ptr = mem.read_u64(chunk_pa, entry_ptr_off);
if entry_ptr == 0 {
tracing::trace!(
allocator = %self.out.allocator_name,
level,
pos,
"sdt_alloc walker: internal desc[pos] == 0 \
(never-created subtree)",
);
continue;
}
self.descend(entry_ptr, level + 1);
}
}
}
fn emit_leaf(&mut self, data_ptr: u64) {
self.out.all_slot_addrs.push(data_ptr & 0xFFFF_FFFF);
if self.out.entries.len() >= MAX_SDT_ALLOC_ENTRIES {
self.out.truncated = true;
return;
}
let Some(data_pa) = self.translate_arena_ptr(data_ptr) else {
self.out.skipped_subtrees = self.out.skipped_subtrees.saturating_add(1);
return;
};
let mem = self.kernel.mem();
let idx = mem.read_u32(data_pa, 0) as i32;
let genn = mem.read_u32(data_pa, 4) as i32;
let mut payload_bytes = vec![0u8; self.payload_size];
let n = mem.read_bytes(
data_pa + self.offsets.data_header_size as u64,
&mut payload_bytes,
);
payload_bytes.truncate(n);
if payload_bytes.is_empty() {
self.out.entries.push(SdtAllocEntry {
idx,
genn,
user_addr: data_ptr & 0xFFFF_FFFF,
payload: RenderedValue::Unsupported {
reason: "payload read failed: end-of-DRAM or unmapped page".into(),
},
});
return;
}
if payload_bytes.iter().all(|&b| b == 0) {
tracing::debug!(
allocator = %self.out.allocator_name,
idx,
genn,
user_addr = format_args!("{:#x}", data_ptr & 0xFFFF_FFFF),
payload_len = payload_bytes.len(),
"sdt_alloc walker: all-zero payload (mid-free race? scx_alloc_free_idx \
zeros payload before clearing the bitmap)",
);
}
let payload = if self.target_type_id != 0 {
render_value_with_mem(self.btf, self.target_type_id, &payload_bytes, self.mem)
} else {
RenderedValue::Bytes {
hex: hex_dump(&payload_bytes),
}
};
self.out.entries.push(SdtAllocEntry {
idx,
genn,
user_addr: data_ptr & 0xFFFF_FFFF,
payload,
});
}
fn translate_arena_ptr(&self, ptr: u64) -> Option<u64> {
if ptr == 0 {
return None;
}
let kva = self.kern_vm_start.wrapping_add(ptr & 0xFFFF_FFFF);
let pa = self.kernel.mem().translate_kva(
self.kernel.cr3_pa(),
Kva(kva),
self.kernel.l5(),
self.kernel.tcr_el1(),
)?;
if pa >= self.kernel.mem().size() {
return None;
}
Some(pa)
}
}