Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions src/core/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -519,30 +519,36 @@ pub fn leak_command_tui(pid: u32, interval: u64) -> (Vec<Line<'static>>, LeakDel
let dur = Duration::new(interval, 0);
sleep(dur);
let snapshot2 = heap_mode(pid).unwrap_or_default();
let growth = diff_heap_size(&snapshot1, &snapshot2);

let new_allocs = diff_snapshots(&snapshot1, &snapshot2);
let new_allocated_bytes: usize = new_allocs.iter().map(|(_, size)| size).sum();

let freed = diff_freed_memory(&snapshot1, &snapshot2);
let new_freed_memory: usize = freed.iter().map(|(_, size)| size).sum();

let leak_delta = LeakDelta {
freed_bytes: new_freed_memory,
allocated_bytes: growth,
allocated_bytes: new_allocated_bytes,
};
let net = leak_delta.net_change();

let leak_delta_output = leak_delta.get_diagnostic_line();
output.push(Line::raw(leak_delta_output.0.to_string()));
output.push(Line::raw(format!(
"heap growth: {}",
format_bytes(growth as u64)
"net change: {}{}",
if net >= 0 { "+" } else { "-" },
format_bytes(net.unsigned_abs() as u64)
)));
if growth > 0 {

if net > 0 {
output.push(Line::from(Span::styled(
format!(
"leak suspected — heap grew by {}",
format_bytes(growth as u64)
),
format!("leak suspected — heap grew by {}", format_bytes(net as u64)),
Style::default().fg(Color::Red),
)));
} else {
output.push(Line::raw("no leak detected".to_string()));
}

(output, leak_delta)
}

Expand Down
140 changes: 105 additions & 35 deletions src/os/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,45 +78,115 @@ pub fn walk_regions(pid: u32) -> Vec<Region> {

/// Reads `/proc/<pid>/smaps` and returns heap blocks for the process.
pub fn walk_heap(pid: u32) -> Vec<HeapBlock> {
let path = format!("/proc/{}/smaps", pid);
let content = fs::read_to_string(path).expect("failed to read maps");
use std::io::{Read, Seek, SeekFrom};

let mut blocks = Vec::new();
let mut current_start = 0usize;
let mut in_heap = false;
let mut protect: RegionProtect;

for line in content.lines() {
if line.contains("[heap]") {
in_heap = true;
let range = line.split_whitespace().next().unwrap_or("");
let mut parts = range.split('-');
current_start = usize::from_str_radix(parts.next().unwrap_or("0"), 16).unwrap_or(0);
} else if in_heap && line.starts_with("Size:") {
let perms = line.split_whitespace().nth(1).unwrap_or("");
if perms.contains('x') {
protect = RegionProtect::Execute;
} else if perms.contains('w') {
protect = RegionProtect::ReadWrite;
} else if perms.contains('r') {
protect = RegionProtect::Readonly;
} else {
protect = RegionProtect::NoAccess;
};
let kb: usize = line
.split_whitespace()
.nth(1)
.unwrap_or("0")
.parse()
.unwrap_or(0);
blocks.push(HeapBlock {
address: current_start,
size: kb * 1024,
is_free: false,
vm_protect: protect,
});
in_heap = false;
let maps_path = format!("/proc/{}/maps", pid);
let maps_content = match fs::read_to_string(&maps_path) {
Ok(c) => c,
Err(_) => return blocks,
};

let mut heap_start = 0usize;
let mut heap_end = 0usize;
let mut vm_protect = RegionProtect::NoAccess;

for line in maps_content.lines() {
if !line.contains("[heap]") {
continue;
}

let mut parts = line.splitn(6, ' ');
let range = parts.next().unwrap_or("");
let perms = parts.next().unwrap_or("");

let mut range_parts = range.split('-');
heap_start = usize::from_str_radix(range_parts.next().unwrap_or("0"), 16).unwrap_or(0);
heap_end = usize::from_str_radix(range_parts.next().unwrap_or("0"), 16).unwrap_or(0);

vm_protect = if perms.contains('x') {
RegionProtect::Execute
} else if perms.contains('w') {
RegionProtect::ReadWrite
} else if perms.contains('r') {
RegionProtect::Readonly
} else {
RegionProtect::NoAccess
};

break; // only one [heap] entry exists, stop looking
}

if heap_start == 0 || heap_end <= heap_start {
return blocks; // no heap found, nothing to walk
}

let mem_path = format!("/proc/{}/mem", pid);
let mut mem = match std::fs::File::open(&mem_path) {
Ok(f) => f,
Err(_) => return blocks,
};

const HEADER_SIZE: usize = 16;
const PREV_INUSE: usize = 0x1;
const SIZE_MASK: usize = !0x7; // clears the low 3 flag bits, keeps real size

let read_header = |mem: &mut std::fs::File, addr: usize| -> io::Result<(usize, usize)> {
let mut buf = [0u8; HEADER_SIZE];
mem.seek(SeekFrom::Start(addr as u64))?;
mem.read_exact(&mut buf)?;
let prev_size = usize::from_le_bytes(buf[0..8].try_into().unwrap());
let size = usize::from_le_bytes(buf[8..16].try_into().unwrap());
Ok((prev_size, size))
};

let mut addr = heap_start;

// read the very first chunk so we have something to inspect each loop
let (_, mut current_size_field) = match read_header(&mut mem, addr) {
Ok(h) => h,
Err(_) => return blocks, // can't read heap memory at all
};

loop {
let chunk_size = current_size_field & SIZE_MASK;

// safety valve: if size is garbage, stop instead of looping forever
if chunk_size < HEADER_SIZE {
break;
}

let next_addr = addr + chunk_size;

// reached the end of the heap region — this last chunk is the "top
// chunk" (glibc's remaining unused arena space), not a real
// allocation, so we stop here rather than reporting it as a block.
if next_addr >= heap_end {
break;
}

// read the NEXT chunk's header, because its PREV_INUSE bit tells us
// whether THIS chunk (at `addr`) is free or in use
let (_, next_size_field) = match read_header(&mut mem, next_addr) {
Ok(h) => h,
Err(_) => break,
};
let is_free = next_size_field & PREV_INUSE == 0;

// usable memory starts right after this chunk's own header
blocks.push(HeapBlock {
address: addr + HEADER_SIZE,
size: chunk_size - HEADER_SIZE,
is_free,
vm_protect: vm_protect.clone(),
});

// move to the next chunk and repeat
addr = next_addr;
current_size_field = next_size_field;
}

blocks
}

Expand Down
Loading