use btf_rs::Btf;
use super::super::arena::{ArenaSnapshot, BpfArenaOffsets, snapshot_arena};
use super::super::bpf_map::{
BPF_MAP_TYPE_ARENA, BPF_MAP_TYPE_ARRAY, BPF_MAP_TYPE_ARRAY_OF_MAPS, BPF_MAP_TYPE_BLOOM_FILTER,
BPF_MAP_TYPE_CGROUP_ARRAY, BPF_MAP_TYPE_CGROUP_STORAGE, BPF_MAP_TYPE_CGRP_STORAGE,
BPF_MAP_TYPE_CPUMAP, BPF_MAP_TYPE_DEVMAP, BPF_MAP_TYPE_DEVMAP_HASH, BPF_MAP_TYPE_HASH,
BPF_MAP_TYPE_HASH_OF_MAPS, BPF_MAP_TYPE_INODE_STORAGE, BPF_MAP_TYPE_INSN_ARRAY,
BPF_MAP_TYPE_LPM_TRIE, BPF_MAP_TYPE_LRU_HASH, BPF_MAP_TYPE_LRU_PERCPU_HASH,
BPF_MAP_TYPE_PERCPU_ARRAY, BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE, BPF_MAP_TYPE_PERCPU_HASH,
BPF_MAP_TYPE_PERF_EVENT_ARRAY, BPF_MAP_TYPE_PROG_ARRAY, BPF_MAP_TYPE_QUEUE,
BPF_MAP_TYPE_REUSEPORT_SOCKARRAY, BPF_MAP_TYPE_RINGBUF, BPF_MAP_TYPE_SK_STORAGE,
BPF_MAP_TYPE_SOCKHASH, BPF_MAP_TYPE_SOCKMAP, BPF_MAP_TYPE_STACK, BPF_MAP_TYPE_STACK_TRACE,
BPF_MAP_TYPE_STRUCT_OPS, BPF_MAP_TYPE_TASK_STORAGE, BPF_MAP_TYPE_USER_RINGBUF,
BPF_MAP_TYPE_XSKMAP, BpfMapAccessor, BpfMapInfo, GuestMemMapAccessor,
};
use super::super::btf_render::{MemReader, RenderedValue, render_value_with_mem};
use super::{
FailureDumpEntry, FailureDumpFdArray, FailureDumpMap, FailureDumpPercpuEntry,
FailureDumpPercpuHashEntry, FailureDumpRingbuf, FailureDumpStackTrace,
FailureDumpStackTraceEntry, hex_dump,
};
pub(super) const MAX_PERCPU_KEYS: u32 = 256;
pub(super) const MAX_HASH_ENTRIES: usize = 4096;
pub(super) const MAX_STACK_TRACE_BUCKETS: u32 = 16_384;
pub(super) const MAX_STACK_TRACE_PCS: u32 = 128;
pub(super) const MAX_FD_ARRAY_SLOTS: u32 = 4096;
pub(super) const MAX_FD_ARRAY_INDICES: usize = 1024;
pub(super) type ArenaPageIndex = std::collections::HashMap<u64, usize>;
pub(super) fn build_arena_page_index(
snap: Option<&super::super::arena::ArenaSnapshot>,
) -> ArenaPageIndex {
let mut index = ArenaPageIndex::new();
let Some(snap) = snap else {
return index;
};
for (i, p) in snap.pages.iter().enumerate() {
match index.entry(p.user_addr) {
std::collections::hash_map::Entry::Vacant(v) => {
v.insert(i);
}
std::collections::hash_map::Entry::Occupied(o) => {
tracing::warn!(
user_addr = format_args!("{:#x}", p.user_addr),
first_idx = *o.get(),
duplicate_idx = i,
"arena snapshot has duplicate user_addr; keeping first page",
);
}
}
}
index
}
struct AccessorMemReader<'a> {
kernel: &'a super::super::guest::GuestKernel,
arena_snapshot: Option<&'a super::super::arena::ArenaSnapshot>,
arena_page_index: &'a ArenaPageIndex,
num_cpus: u32,
}
impl MemReader for AccessorMemReader<'_> {
fn read_kva(&self, kva: u64, len: usize) -> Option<Vec<u8>> {
let walk = self.kernel.walk_context();
let pa = super::super::idr::translate_any_kva(
self.kernel.mem(),
walk.cr3_pa,
walk.page_offset,
kva,
walk.l5,
walk.tcr_el1,
)?;
let mut buf = vec![0u8; len];
let read = self.kernel.mem().read_bytes(pa, &mut buf);
if read == len { Some(buf) } else { None }
}
fn is_arena_addr(&self, addr: u64) -> bool {
let Some(snap) = self.arena_snapshot else {
return false;
};
if snap.user_vm_start == 0 {
return false;
}
let Some(end) = snap.user_vm_start.checked_add(1 << 32) else {
return false;
};
addr >= snap.user_vm_start && addr < end
}
fn read_arena(&self, addr: u64, len: usize) -> Option<Vec<u8>> {
let snap = self.arena_snapshot?;
let offset = (addr & 0xFFF) as usize;
let end = offset.checked_add(len)?;
if end > 4096 {
return None;
}
let page_addr = addr & !0xFFF;
if let Some(&idx) = self.arena_page_index.get(&page_addr) {
let page = &snap.pages[idx];
if end <= page.bytes.len() {
return Some(page.bytes[offset..end].to_vec());
}
}
if snap.kern_vm_start == 0 {
return None;
}
let kva = snap.kern_vm_start.wrapping_add(addr & 0xFFFF_FFFF);
self.read_kva(kva, len)
}
fn nr_cpu_ids(&self) -> u32 {
self.num_cpus
}
}
impl<'a> GuestMemMapAccessor<'a> {
pub(crate) fn mem_reader(
&self,
arena_snapshot: Option<&'a super::super::arena::ArenaSnapshot>,
arena_page_index: &'a ArenaPageIndex,
num_cpus: u32,
) -> impl MemReader + 'a {
AccessorMemReader {
kernel: self.kernel(),
arena_snapshot,
arena_page_index,
num_cpus,
}
}
}
#[derive(Debug, Clone)]
pub(super) struct SdtAllocMeta {
pub(super) allocator_name: String,
pub(super) elem_size: u64,
pub(super) header_size: usize,
pub(super) payload_btf_type_id: u32,
pub(super) kern_vm_start: u64,
}
pub(super) fn select_sdt_alloc_meta<'a>(
metas: &'a [SdtAllocMeta],
map_name: &str,
) -> Option<&'a SdtAllocMeta> {
if metas.is_empty() {
return None;
}
if metas.len() == 1 {
return Some(&metas[0]);
}
let stem = |name: &str| -> String {
let s = name.strip_suffix("_allocator").unwrap_or(name);
s.strip_prefix("scx_").unwrap_or(s).to_string()
};
metas
.iter()
.filter(|m| {
let s = stem(&m.allocator_name);
!s.is_empty() && map_name.contains(&s)
})
.max_by_key(|m| stem(&m.allocator_name).len())
}
pub(super) struct RenderMapCtx<'a> {
pub(super) accessor: &'a GuestMemMapAccessor<'a>,
pub(super) btf: Option<&'a Btf>,
pub(super) num_cpus: u32,
pub(super) arena_offsets: Option<&'a BpfArenaOffsets>,
pub(super) shared_arena: Option<(&'a ArenaSnapshot, u64)>,
pub(super) arena_page_index: &'a ArenaPageIndex,
pub(super) sdt_alloc_metas: &'a [SdtAllocMeta],
}
pub(super) fn render_value_or_hex(
btf: Option<&Btf>,
type_id: u32,
bytes: &[u8],
mem_reader: &dyn MemReader,
) -> RenderedValue {
match (btf, type_id) {
(Some(b), id) if id != 0 => render_value_with_mem(b, id, bytes, mem_reader),
_ => RenderedValue::Bytes {
hex: hex_dump(bytes),
},
}
}
pub(super) fn render_key_optional(
btf: Option<&Btf>,
type_id: u32,
bytes: &[u8],
mem_reader: &dyn MemReader,
) -> Option<RenderedValue> {
match (btf, type_id) {
(Some(b), id) if id != 0 => Some(render_value_with_mem(b, id, bytes, mem_reader)),
_ => None,
}
}
const MODIFIER_PEEL_LIMIT: usize = 32;
fn peel_modifiers(btf: &Btf, start: btf_rs::Type) -> Option<btf_rs::Type> {
use btf_rs::Type;
let mut t = start;
for _ in 0..MODIFIER_PEEL_LIMIT {
match t {
Type::Typedef(_)
| Type::Const(_)
| Type::Volatile(_)
| Type::Restrict(_)
| Type::TypeTag(_) => {
let inner = t.as_btf_type()?;
t = btf.resolve_chained_type(inner).ok()?;
}
_ => return Some(t),
}
}
None
}
pub(super) fn find_sdt_data_field_offset(btf: &Btf, value_type_id: u32) -> Option<usize> {
use btf_rs::Type;
if value_type_id == 0 {
return None;
}
let value_ty = peel_modifiers(btf, btf.resolve_type_by_id(value_type_id).ok()?)?;
let s = match value_ty {
Type::Struct(s) | Type::Union(s) => s,
_ => return None,
};
for member in &s.members {
let bit_off = member.bit_offset() as usize;
if !bit_off.is_multiple_of(8) {
continue;
}
let byte_off = bit_off / 8;
let Ok(member_ty) = btf.resolve_chained_type(member) else {
continue;
};
let Some(t) = peel_modifiers(btf, member_ty) else {
continue;
};
let ptr = match t {
Type::Ptr(p) => p,
_ => continue,
};
let Ok(pointee_chained) = btf.resolve_chained_type(&ptr) else {
continue;
};
let Some(pointee) = peel_modifiers(btf, pointee_chained) else {
continue;
};
let pointee_name: Option<String> = match pointee {
Type::Struct(ref s) => btf.resolve_name(s).ok(),
Type::Fwd(ref fwd) if fwd.is_struct() => btf.resolve_name(fwd).ok(),
_ => continue,
};
if matches!(pointee_name.as_deref(), Some("sdt_data")) {
return Some(byte_off);
}
}
None
}
fn resolve_struct_ops_payload_type_id(btf: &Btf, wrapper_type_id: u32) -> Option<u32> {
use btf_rs::{BtfType, Type};
if wrapper_type_id == 0 {
return None;
}
let wrapper_ty = peel_modifiers(btf, btf.resolve_type_by_id(wrapper_type_id).ok()?)?;
let s = match wrapper_ty {
Type::Struct(s) => s,
_ => return None,
};
for member in &s.members {
let Ok(name) = btf.resolve_name(member) else {
continue;
};
if name != "data" {
continue;
}
let raw_id = member.get_type_id().ok()?;
let chained = btf.resolve_type_by_id(raw_id).ok()?;
let inner = peel_modifiers(btf, chained)?;
return Some(match inner {
Type::Struct(_) | Type::Union(_) | Type::Int(_) | Type::Enum(_) | Type::Enum64(_) => {
let mut id = raw_id;
let mut t = btf.resolve_type_by_id(id).ok()?;
for _ in 0..MODIFIER_PEEL_LIMIT {
match t {
Type::Typedef(_)
| Type::Const(_)
| Type::Volatile(_)
| Type::Restrict(_)
| Type::TypeTag(_) => {
id = t.as_btf_type()?.get_type_id().ok()?;
t = btf.resolve_type_by_id(id).ok()?;
}
_ => break,
}
}
id
}
_ => return None,
});
}
None
}
pub(super) fn chase_sdt_data_payload(
btf: Option<&Btf>,
field_offset: Option<usize>,
meta: Option<&SdtAllocMeta>,
value_bytes: &[u8],
mem_reader: &dyn MemReader,
) -> Option<RenderedValue> {
let btf = btf?;
let off = field_offset?;
let meta = meta?;
if meta.payload_btf_type_id == 0 {
return None;
}
if meta.elem_size as usize <= meta.header_size {
return None;
}
if meta.kern_vm_start == 0 {
return None;
}
let end = off.checked_add(8)?;
let slice = value_bytes.get(off..end)?;
let mut buf = [0u8; 8];
buf.copy_from_slice(slice);
let data_ptr = u64::from_le_bytes(buf);
if data_ptr == 0 {
return None;
}
let kva = meta.kern_vm_start.wrapping_add(data_ptr & 0xFFFF_FFFF);
let elem_bytes = mem_reader.read_kva(kva, meta.elem_size as usize)?;
let payload = elem_bytes.get(meta.header_size..)?;
Some(render_value_with_mem(
btf,
meta.payload_btf_type_id,
payload,
mem_reader,
))
}
fn is_str_literal_section(map_name: &str) -> bool {
map_name.ends_with(".rodata.str1.1")
}
fn ascii_str_dump(bytes: &[u8]) -> String {
use std::fmt::Write;
let mut s = String::with_capacity(bytes.len());
for &b in bytes {
if (0x20..=0x7E).contains(&b) {
s.push(b as char);
} else {
let _ = write!(s, "\\x{b:02x}");
}
}
s
}
pub(super) fn map_type_name(map_type: u32) -> Option<&'static str> {
Some(match map_type {
BPF_MAP_TYPE_HASH => "hash",
BPF_MAP_TYPE_ARRAY => "array",
BPF_MAP_TYPE_PROG_ARRAY => "prog_array",
BPF_MAP_TYPE_PERF_EVENT_ARRAY => "perf_event_array",
BPF_MAP_TYPE_PERCPU_HASH => "percpu_hash",
BPF_MAP_TYPE_PERCPU_ARRAY => "percpu_array",
BPF_MAP_TYPE_STACK_TRACE => "stack_trace",
BPF_MAP_TYPE_CGROUP_ARRAY => "cgroup_array",
BPF_MAP_TYPE_LRU_HASH => "lru_hash",
BPF_MAP_TYPE_LRU_PERCPU_HASH => "lru_percpu_hash",
BPF_MAP_TYPE_LPM_TRIE => "lpm_trie",
BPF_MAP_TYPE_ARRAY_OF_MAPS => "array_of_maps",
BPF_MAP_TYPE_HASH_OF_MAPS => "hash_of_maps",
BPF_MAP_TYPE_DEVMAP => "devmap",
BPF_MAP_TYPE_SOCKMAP => "sockmap",
BPF_MAP_TYPE_CPUMAP => "cpumap",
BPF_MAP_TYPE_XSKMAP => "xskmap",
BPF_MAP_TYPE_SOCKHASH => "sockhash",
BPF_MAP_TYPE_CGROUP_STORAGE => "cgroup_storage",
BPF_MAP_TYPE_REUSEPORT_SOCKARRAY => "reuseport_sockarray",
BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE => "percpu_cgroup_storage",
BPF_MAP_TYPE_QUEUE => "queue",
BPF_MAP_TYPE_STACK => "stack",
BPF_MAP_TYPE_SK_STORAGE => "sk_storage",
BPF_MAP_TYPE_DEVMAP_HASH => "devmap_hash",
BPF_MAP_TYPE_STRUCT_OPS => "struct_ops",
BPF_MAP_TYPE_RINGBUF => "ringbuf",
BPF_MAP_TYPE_INODE_STORAGE => "inode_storage",
BPF_MAP_TYPE_TASK_STORAGE => "task_storage",
BPF_MAP_TYPE_BLOOM_FILTER => "bloom_filter",
BPF_MAP_TYPE_USER_RINGBUF => "user_ringbuf",
BPF_MAP_TYPE_CGRP_STORAGE => "cgrp_storage",
BPF_MAP_TYPE_ARENA => "arena",
BPF_MAP_TYPE_INSN_ARRAY => "insn_array",
_ => return None,
})
}
pub(super) const MAP_TYPE_EXPLANATIONS: &[(u32, &str)] = &[
(
BPF_MAP_TYPE_CGROUP_STORAGE,
"deprecated cgroup-attached storage; use CGRP_STORAGE on newer kernels",
),
(
BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE,
"deprecated cgroup-attached storage; use CGRP_STORAGE on newer kernels",
),
(
BPF_MAP_TYPE_QUEUE,
"QUEUE/STACK are destructive (peek shows only the head; pop consumes); \
no enumeration API",
),
(
BPF_MAP_TYPE_STACK,
"QUEUE/STACK are destructive (peek shows only the head; pop consumes); \
no enumeration API",
),
(
BPF_MAP_TYPE_BLOOM_FILTER,
"BLOOM_FILTER is a probabilistic set; no key enumeration is possible",
),
(
BPF_MAP_TYPE_LPM_TRIE,
"LPM_TRIE walker not implemented (keyed by prefixlen + data); \
use bpf(2) BPF_MAP_GET_NEXT_KEY for live-host iteration",
),
(
BPF_MAP_TYPE_INSN_ARRAY,
"INSN_ARRAY stores BPF instruction targets used by the verifier \
for indirect jumps; values are kernel-side program data",
),
];
pub(super) fn render_map(ctx: &RenderMapCtx<'_>, info: &BpfMapInfo) -> FailureDumpMap {
let &RenderMapCtx {
accessor,
btf,
num_cpus,
arena_offsets,
shared_arena,
arena_page_index,
sdt_alloc_metas,
} = ctx;
let mem_reader = accessor.mem_reader(
shared_arena.map(|(snap, _map_kva)| snap),
arena_page_index,
num_cpus,
);
let info_name = info.name();
let sdt_alloc_meta: Option<SdtAllocMeta> =
select_sdt_alloc_meta(sdt_alloc_metas, &info_name).cloned();
let mut out = FailureDumpMap {
name: info_name.into_owned(),
map_type: info.map_type,
value_size: info.value_size,
max_entries: info.max_entries,
value: None,
entries: Vec::new(),
percpu_entries: Vec::new(),
percpu_hash_entries: Vec::new(),
arena: None,
ringbuf: None,
stack_trace: None,
fd_array: None,
error: None,
};
match info.map_type {
BPF_MAP_TYPE_ARRAY => {
match accessor.read_value(info, 0, info.value_size as usize) {
Some(bytes) => {
out.value = Some(if is_str_literal_section(&out.name) {
RenderedValue::Bytes {
hex: ascii_str_dump(&bytes),
}
} else {
render_value_or_hex(btf, info.btf_value_type_id, &bytes, &mem_reader)
});
}
None => {
out.error = Some("ARRAY value region unreadable (unmapped page?)".into());
}
}
if out.error.is_none() && info.max_entries > 1 {
out.error = Some(format!(
"multi-entry ARRAY: only key 0 of {} shown",
info.max_entries
));
}
}
BPF_MAP_TYPE_HASH | BPF_MAP_TYPE_LRU_HASH => {
let raw_entries = accessor.iter_hash_map(info);
let truncated = raw_entries.len() > MAX_HASH_ENTRIES;
let sdt_data_field = btf
.zip(sdt_alloc_meta.as_ref())
.and_then(|(b, _)| find_sdt_data_field_offset(b, info.btf_value_type_id));
for (k, v) in raw_entries.into_iter().take(MAX_HASH_ENTRIES) {
let key = render_key_optional(btf, info.btf_key_type_id, &k, &mem_reader);
let value = render_key_optional(btf, info.btf_value_type_id, &v, &mem_reader);
let payload = chase_sdt_data_payload(
btf,
sdt_data_field,
sdt_alloc_meta.as_ref(),
&v,
&mem_reader,
);
out.entries.push(FailureDumpEntry {
key,
key_hex: hex_dump(&k),
value,
value_hex: hex_dump(&v),
payload,
});
}
if truncated {
out.error = Some(format!("hash map truncated at {MAX_HASH_ENTRIES} entries"));
}
}
BPF_MAP_TYPE_PERCPU_HASH | BPF_MAP_TYPE_LRU_PERCPU_HASH => {
let raw_entries = accessor.iter_percpu_hash_map(info, num_cpus);
let truncated = raw_entries.len() > MAX_HASH_ENTRIES;
for (k, per_cpu_bytes) in raw_entries.into_iter().take(MAX_HASH_ENTRIES) {
let key = render_key_optional(btf, info.btf_key_type_id, &k, &mem_reader);
let per_cpu = per_cpu_bytes
.into_iter()
.map(|maybe_bytes| {
maybe_bytes.map(|b| {
render_value_or_hex(btf, info.btf_value_type_id, &b, &mem_reader)
})
})
.collect();
out.percpu_hash_entries.push(FailureDumpPercpuHashEntry {
key,
key_hex: hex_dump(&k),
per_cpu,
});
}
if truncated {
out.error = Some(format!(
"percpu hash map truncated at {MAX_HASH_ENTRIES} entries"
));
}
}
BPF_MAP_TYPE_PERCPU_ARRAY => {
let limit = info.max_entries.min(MAX_PERCPU_KEYS);
for key in 0..limit {
let per_cpu_bytes = accessor.read_percpu_array(info, key, num_cpus);
let per_cpu = per_cpu_bytes
.into_iter()
.map(|maybe_bytes| {
maybe_bytes.map(|b| {
render_value_or_hex(btf, info.btf_value_type_id, &b, &mem_reader)
})
})
.collect();
out.percpu_entries
.push(FailureDumpPercpuEntry { key, per_cpu });
}
if info.max_entries > MAX_PERCPU_KEYS {
out.error = Some(format!(
"PERCPU_ARRAY truncated at {MAX_PERCPU_KEYS} keys (max_entries={})",
info.max_entries,
));
}
}
BPF_MAP_TYPE_ARENA => {
match arena_offsets {
Some(off) => {
let snap = match shared_arena {
Some((shared_snap, shared_map_kva)) if shared_map_kva == info.map_kva => {
shared_snap.clone()
}
_ => snapshot_arena(accessor.kernel(), info, off),
};
out.arena = Some(snap);
}
None => {
out.error = Some(
"arena BTF offsets unavailable (kernel lacks struct bpf_arena?)".into(),
);
}
}
}
BPF_MAP_TYPE_STRUCT_OPS => {
let Some(so) = accessor.offsets().struct_ops_offsets.as_ref() else {
out.error = Some(
"STRUCT_OPS value unreadable: bpf_struct_ops_map BTF offsets unresolved \
(kernel without struct_ops support, or vmlinux BTF stripped of \
bpf_struct_ops_map / bpf_struct_ops_value)."
.into(),
);
return out;
};
let data_off = so.value_data;
let data_len = (info.value_size as usize).saturating_sub(data_off);
let payload_type_id = if info.btf_value_type_id != 0 {
info.btf_value_type_id
} else if info.btf_vmlinux_value_type_id != 0 {
btf.and_then(|b| {
resolve_struct_ops_payload_type_id(b, info.btf_vmlinux_value_type_id)
})
.unwrap_or(0)
} else {
0
};
match accessor.read_value(info, 0, data_len) {
Some(bytes) => {
out.value = Some(render_value_or_hex(
btf,
payload_type_id,
&bytes,
&mem_reader,
));
}
None => {
out.error = Some(
"STRUCT_OPS value unreadable: value region unmapped. Live-host \
backend reads via BPF_MAP_LOOKUP_ELEM at key=0."
.into(),
);
}
}
}
BPF_MAP_TYPE_TASK_STORAGE
| BPF_MAP_TYPE_INODE_STORAGE
| BPF_MAP_TYPE_SK_STORAGE
| BPF_MAP_TYPE_CGRP_STORAGE => {
let raw_entries = accessor.iter_task_storage(info);
let truncated = raw_entries.len() > MAX_HASH_ENTRIES;
let sdt_data_field = btf
.zip(sdt_alloc_meta.as_ref())
.and_then(|(b, _)| find_sdt_data_field_offset(b, info.btf_value_type_id));
for (k, v) in raw_entries.into_iter().take(MAX_HASH_ENTRIES) {
let owner_kva = u64::from_le_bytes(k.as_slice().try_into().unwrap_or([0u8; 8]));
let key = Some(RenderedValue::Ptr {
value: owner_kva,
deref: None,
deref_skipped_reason: None,
});
let value = Some(render_value_or_hex(
btf,
info.btf_value_type_id,
&v,
&mem_reader,
));
let payload = chase_sdt_data_payload(
btf,
sdt_data_field,
sdt_alloc_meta.as_ref(),
&v,
&mem_reader,
);
out.entries.push(FailureDumpEntry {
key,
key_hex: hex_dump(&k),
value,
value_hex: hex_dump(&v),
payload,
});
}
if truncated {
out.error = Some(format!(
"local_storage map truncated at {MAX_HASH_ENTRIES} entries"
));
}
}
BPF_MAP_TYPE_RINGBUF | BPF_MAP_TYPE_USER_RINGBUF => {
match render_ringbuf_state(accessor, info) {
Ok(rb) => {
out.ringbuf = Some(rb);
}
Err(reason) => {
out.error = Some(reason);
}
}
}
BPF_MAP_TYPE_STACK_TRACE => {
match render_stack_traces(accessor, info) {
Ok(st) => {
out.stack_trace = Some(st);
}
Err(reason) => {
out.error = Some(reason);
}
}
}
BPF_MAP_TYPE_PROG_ARRAY
| BPF_MAP_TYPE_PERF_EVENT_ARRAY
| BPF_MAP_TYPE_CGROUP_ARRAY
| BPF_MAP_TYPE_ARRAY_OF_MAPS
| BPF_MAP_TYPE_HASH_OF_MAPS
| BPF_MAP_TYPE_DEVMAP
| BPF_MAP_TYPE_DEVMAP_HASH
| BPF_MAP_TYPE_SOCKMAP
| BPF_MAP_TYPE_SOCKHASH
| BPF_MAP_TYPE_CPUMAP
| BPF_MAP_TYPE_XSKMAP
| BPF_MAP_TYPE_REUSEPORT_SOCKARRAY => {
out.fd_array = Some(render_fd_array_slots(accessor, info));
}
other => {
out.error = Some(
MAP_TYPE_EXPLANATIONS
.iter()
.find(|(t, _)| *t == other)
.map(|(_, msg)| (*msg).to_string())
.unwrap_or_else(|| {
format!(
"unknown map_type {other} (kernel newer than dump renderer; \
update render_map dispatch)"
)
}),
);
}
}
out
}
pub(super) fn render_ringbuf_state(
accessor: &GuestMemMapAccessor<'_>,
info: &BpfMapInfo,
) -> Result<FailureDumpRingbuf, String> {
let Some(rb_offs) = accessor.offsets().ringbuf_offsets.as_ref() else {
return Err(
"RINGBUF state unreadable: BTF lacks bpf_ringbuf_map / bpf_ringbuf \
(kernel built without ringbuf, or BTF stripped). Wire format \
remains consumer_pos/producer_pos/mask/pending_pos but the \
struct field offsets are not resolved on this kernel."
.to_string(),
);
};
let kernel = accessor.kernel();
let mem = kernel.mem();
let walk = kernel.walk_context();
let map_pa = super::super::idr::translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
info.map_kva,
walk.l5,
walk.tcr_el1,
)
.ok_or_else(|| {
"RINGBUF map_kva unmapped during freeze (concurrent destruction race?)".to_string()
})?;
let rb_kva = mem.read_u64(map_pa, rb_offs.rbm_rb);
if rb_kva == 0 {
return Err(
"RINGBUF rb pointer NULL: bpf_ringbuf_alloc failed at map create time \
(out of memory or numa mismatch); the map exists but has no backing \
ring data area"
.to_string(),
);
}
let read_at = |off: usize| -> Option<u64> {
let kva = rb_kva.wrapping_add(off as u64);
let pa = super::super::idr::translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
kva,
walk.l5,
walk.tcr_el1,
)?;
Some(mem.read_u64(pa, 0))
};
let mask = read_at(rb_offs.rb_mask).ok_or_else(|| {
"RINGBUF rb->mask unmapped during freeze (rb pointer torn or unmapped)".to_string()
})?;
let consumer_pos = read_at(rb_offs.rb_consumer_pos)
.ok_or_else(|| "RINGBUF rb->consumer_pos unmapped (consumer page absent)".to_string())?;
let producer_pos = read_at(rb_offs.rb_producer_pos)
.ok_or_else(|| "RINGBUF rb->producer_pos unmapped (producer page absent)".to_string())?;
let pending_pos = read_at(rb_offs.rb_pending_pos)
.ok_or_else(|| "RINGBUF rb->pending_pos unmapped (producer page absent)".to_string())?;
if mask == u64::MAX {
return Err("RINGBUF rb->mask = u64::MAX (capacity would wrap to 0); \
likely a corrupted read of bpf_ringbuf.mask"
.to_string());
}
let capacity = mask.wrapping_add(1);
if capacity == 0 {
return Err("RINGBUF capacity = 0 (mask + 1 wrapped); rb->mask read \
produced a non-power-of-two value"
.to_string());
}
Ok(FailureDumpRingbuf {
capacity,
consumer_pos,
producer_pos,
pending_pos,
pending_bytes: producer_pos.wrapping_sub(consumer_pos),
})
}
pub(super) fn render_stack_traces(
accessor: &GuestMemMapAccessor<'_>,
info: &BpfMapInfo,
) -> Result<FailureDumpStackTrace, String> {
let Some(sm_offs) = accessor.offsets().stackmap_offsets.as_ref() else {
return Err(
"STACK_TRACE bucket array unreadable: BTF lacks bpf_stack_map / \
stack_map_bucket. Each non-null bucket pointer would carry `nr` \
u64 PCs (or bpf_stack_build_id records when BPF_F_STACK_BUILD_ID \
is set on the map); resolve the offsets to render."
.to_string(),
);
};
let kernel = accessor.kernel();
let mem = kernel.mem();
let walk = kernel.walk_context();
let map_pa = super::super::idr::translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
info.map_kva,
walk.l5,
walk.tcr_el1,
)
.ok_or_else(|| "STACK_TRACE map_kva unmapped during freeze".to_string())?;
let n_buckets = mem.read_u32(map_pa, sm_offs.smap_n_buckets);
let scan_buckets = n_buckets.min(MAX_STACK_TRACE_BUCKETS);
const BPF_F_STACK_BUILD_ID: u32 = 1 << 5;
const STACK_BUILD_ID_SIZE: u32 = 32;
let build_id_mode = (info.map_flags & BPF_F_STACK_BUILD_ID) != 0;
let entry_size: u32 = if build_id_mode {
STACK_BUILD_ID_SIZE
} else {
8
};
let mut entries = Vec::new();
let mut any_truncated = false;
for bucket_id in 0..scan_buckets {
let slot_kva = info
.map_kva
.wrapping_add(sm_offs.smap_buckets as u64)
.wrapping_add((bucket_id as u64) * 8);
let Some(slot_pa) = super::super::idr::translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
slot_kva,
walk.l5,
walk.tcr_el1,
) else {
continue;
};
let bucket_kva = mem.read_u64(slot_pa, 0);
if bucket_kva == 0 {
continue;
}
let Some(bucket_pa) = super::super::idr::translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
bucket_kva,
walk.l5,
walk.tcr_el1,
) else {
continue;
};
let nr = mem.read_u32(bucket_pa, sm_offs.smb_nr);
let nr_to_read = nr.min(MAX_STACK_TRACE_PCS);
if nr > MAX_STACK_TRACE_PCS {
any_truncated = true;
}
let data_byte_len = (nr_to_read as usize).saturating_mul(entry_size as usize);
let data_kva = bucket_kva.wrapping_add(sm_offs.smb_data as u64);
let mut data_bytes = vec![0u8; data_byte_len];
const PAGE: u64 = 4096;
let mut consumed: u64 = 0;
let total = data_byte_len as u64;
let mut data_ok = true;
while consumed < total {
let cur_kva = data_kva.wrapping_add(consumed);
let Some(pa) = super::super::idr::translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
cur_kva,
walk.l5,
walk.tcr_el1,
) else {
data_ok = false;
break;
};
let page_end = (cur_kva & !(PAGE - 1)).wrapping_add(PAGE);
let chunk_len = page_end.wrapping_sub(cur_kva).min(total - consumed) as usize;
let dst = &mut data_bytes[consumed as usize..consumed as usize + chunk_len];
let n = mem.read_bytes(pa, dst);
if n != chunk_len {
data_ok = false;
break;
}
consumed += chunk_len as u64;
}
if !data_ok {
data_bytes.clear();
}
let mut pcs = Vec::new();
if !build_id_mode {
for chunk in data_bytes.chunks_exact(8) {
pcs.push(u64::from_le_bytes(chunk.try_into().unwrap()));
}
}
entries.push(FailureDumpStackTraceEntry {
bucket_id,
nr,
pcs,
data_hex: hex_dump(&data_bytes),
});
}
Ok(FailureDumpStackTrace {
n_buckets,
entries,
truncated: any_truncated || n_buckets > MAX_STACK_TRACE_BUCKETS,
})
}
pub(super) fn render_fd_array_slots(
accessor: &GuestMemMapAccessor<'_>,
info: &BpfMapInfo,
) -> FailureDumpFdArray {
let kernel = accessor.kernel();
let mem = kernel.mem();
let walk = kernel.walk_context();
let array_value_off = accessor.offsets().array_value;
let scan = info.max_entries.min(MAX_FD_ARRAY_SLOTS);
let truncated = info.max_entries > MAX_FD_ARRAY_SLOTS;
let mut populated: u32 = 0;
let mut indices: Vec<u32> = Vec::new();
let hash_shaped = matches!(
info.map_type,
BPF_MAP_TYPE_SOCKHASH | BPF_MAP_TYPE_DEVMAP_HASH | BPF_MAP_TYPE_HASH_OF_MAPS
);
if hash_shaped {
return FailureDumpFdArray {
populated: 0,
scanned: 0,
indices: Vec::new(),
truncated: false,
indices_truncated: false,
};
}
for idx in 0..scan {
let slot_kva = info
.map_kva
.wrapping_add(array_value_off as u64)
.wrapping_add((idx as u64) * 8);
let Some(slot_pa) = super::super::idr::translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
slot_kva,
walk.l5,
walk.tcr_el1,
) else {
continue;
};
let ptr = mem.read_u64(slot_pa, 0);
if ptr != 0 {
populated += 1;
if indices.len() < MAX_FD_ARRAY_INDICES {
indices.push(idx);
}
}
}
FailureDumpFdArray {
populated,
scanned: scan,
indices_truncated: indices.len() < populated as usize,
indices,
truncated,
}
}