From e1e8b4bd74eebed0305115c87ef3aaed38fb3b72 Mon Sep 17 00:00:00 2001 From: SickleFire Date: Sat, 4 Jul 2026 22:59:06 +0800 Subject: [PATCH 1/2] Refactor walk_heap to read from /proc/maps --- src/os/linux.rs | 140 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 105 insertions(+), 35 deletions(-) diff --git a/src/os/linux.rs b/src/os/linux.rs index 632d736..b22e2d0 100644 --- a/src/os/linux.rs +++ b/src/os/linux.rs @@ -78,45 +78,115 @@ pub fn walk_regions(pid: u32) -> Vec { /// Reads `/proc//smaps` and returns heap blocks for the process. pub fn walk_heap(pid: u32) -> Vec { - 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 } From 39af268fd83fbb38f17c6d8044d87fe76dedcbe8 Mon Sep 17 00:00:00 2001 From: SickleFire Date: Sun, 5 Jul 2026 00:04:42 +0800 Subject: [PATCH 2/2] update leak_tui net growth calculation --- src/core/scan.rs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/core/scan.rs b/src/core/scan.rs index a80f48e..14f1c46 100644 --- a/src/core/scan.rs +++ b/src/core/scan.rs @@ -519,30 +519,36 @@ pub fn leak_command_tui(pid: u32, interval: u64) -> (Vec>, 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) }