Skip to content

Commit e629f02

Browse files
authored
Add option to resolve type when dumping heap summary. (#1030)
This change adds `--resolve` option to resolve the type and categorize the chunks, using vtable in the chunk. Since multiple types of objects could share the same size, also it is impossible for anyone to remember the size of all the objects in the system, categorize and output the chunks by object type will become very handy when debugging issues like memory leaks.
1 parent 663d4a2 commit e629f02

File tree

3 files changed

+49
-18
lines changed

3 files changed

+49
-18
lines changed

docs/commands/heap.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,17 @@ gef➤ heap chunks --summary
7878

7979
![heap-chunks-summary](https://i.imgur.com/3HTgtwX.png)
8080

81+
Sometimes, multiple types of objects could have the same size, hence it might not be enough only
82+
knowing the chunk size when debugging issues like memory leaks. GEF supports using the vtable to
83+
determine the type of the object stored in the chunk. To enable this feature, use `--resolve` along
84+
with the `--summary` flag.
85+
86+
```text
87+
gef➤ heap chunks --summary --resolve
88+
```
89+
90+
![heap-chunks-summary-resolve](https://i.imgur.com/2Mm0JF6.png)
91+
8192
Heap chunk command also supports filtering chunks by their size. To do so, simply provide the
8293
`--min-size` or `--max-size` argument:
8394

gef.py

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1710,6 +1710,15 @@ def psprint(self) -> str:
17101710
msg.append(f"\n\n{self._str_pointers()}")
17111711
return "\n".join(msg) + "\n"
17121712

1713+
def resolve_type(self) -> str:
1714+
ptr_data = gef.memory.read_integer(self.data_address)
1715+
if ptr_data != 0:
1716+
sym = gdb_get_location_from_symbol(ptr_data)
1717+
if sym is not None and "vtable for" in sym[0]:
1718+
return sym[0].replace("vtable for ", "")
1719+
1720+
return ""
1721+
17131722

17141723
class GlibcFastChunk(GlibcChunk):
17151724

@@ -1999,7 +2008,6 @@ def gdb_lookup_symbol(sym: str) -> Optional[Tuple[Optional[str], Optional[Tuple[
19992008
except gdb.error:
20002009
return None
20012010

2002-
20032011
@lru_cache(maxsize=512)
20042012
def gdb_get_location_from_symbol(address: int) -> Optional[Tuple[str, int]]:
20052013
"""Retrieve the location of the `address` argument from the symbol table.
@@ -6302,7 +6310,8 @@ def do_invoke(self, _: List[str], **kwargs: Any) -> None:
63026310

63036311

63046312
class GlibcHeapChunkSummary:
6305-
def __init__(self):
6313+
def __init__(self, desc = ""):
6314+
self.desc = desc
63066315
self.count = 0
63076316
self.total_bytes = 0
63086317

@@ -6312,7 +6321,8 @@ def process_chunk(self, chunk: GlibcChunk) -> None:
63126321

63136322

63146323
class GlibcHeapArenaSummary:
6315-
def __init__(self) -> None:
6324+
def __init__(self, resolve_type = False) -> None:
6325+
self.resolve_symbol = resolve_type
63166326
self.size_distribution = {}
63176327
self.flag_distribution = {
63186328
"PREV_INUSE": GlibcHeapChunkSummary(),
@@ -6321,10 +6331,12 @@ def __init__(self) -> None:
63216331
}
63226332

63236333
def process_chunk(self, chunk: GlibcChunk) -> None:
6324-
per_size_summary = self.size_distribution.get(chunk.size, None)
6334+
chunk_type = "" if not self.resolve_symbol else chunk.resolve_type()
6335+
6336+
per_size_summary = self.size_distribution.get((chunk.size, chunk_type), None)
63256337
if per_size_summary is None:
6326-
per_size_summary = GlibcHeapChunkSummary()
6327-
self.size_distribution[chunk.size] = per_size_summary
6338+
per_size_summary = GlibcHeapChunkSummary(desc=chunk_type)
6339+
self.size_distribution[(chunk.size, chunk_type)] = per_size_summary
63286340
per_size_summary.process_chunk(chunk)
63296341

63306342
if chunk.has_p_bit():
@@ -6336,9 +6348,9 @@ def process_chunk(self, chunk: GlibcChunk) -> None:
63366348

63376349
def print(self) -> None:
63386350
gef_print("== Chunk distribution by size ==")
6339-
gef_print("{:<10s}\t{:<10s}\t{:s}".format("ChunkBytes", "Count", "TotalBytes"))
6340-
for chunk_size, chunk_summary in sorted(self.size_distribution.items(), key=lambda x: x[1].total_bytes, reverse=True):
6341-
gef_print("{:<10d}\t{:<10d}\t{:<d}".format(chunk_size, chunk_summary.count, chunk_summary.total_bytes))
6351+
gef_print("{:<10s}\t{:<10s}\t{:15s}\t{:s}".format("ChunkBytes", "Count", "TotalBytes", "Description"))
6352+
for chunk_info, chunk_summary in sorted(self.size_distribution.items(), key=lambda x: x[1].total_bytes, reverse=True):
6353+
gef_print("{:<10d}\t{:<10d}\t{:<15d}\t{:s}".format(chunk_info[0], chunk_summary.count, chunk_summary.total_bytes, chunk_summary.desc))
63426354

63436355
gef_print("\n== Chunk distribution by flag ==")
63446356
gef_print("{:<15s}\t{:<10s}\t{:s}".format("Flag", "TotalCount", "TotalBytes"))
@@ -6352,7 +6364,7 @@ class GlibcHeapChunksCommand(GenericCommand):
63526364
the base address of a different arena can be passed"""
63536365

63546366
_cmdline_ = "heap chunks"
6355-
_syntax_ = f"{_cmdline_} [-h] [--all] [--allow-unaligned] [--summary] [--min-size MIN_SIZE] [--max-size MAX_SIZE] [arena_address]"
6367+
_syntax_ = f"{_cmdline_} [-h] [--all] [--allow-unaligned] [--summary] [--min-size MIN_SIZE] [--max-size MAX_SIZE] [--resolve] [arena_address]"
63566368
_example_ = (f"\n{_cmdline_}"
63576369
f"\n{_cmdline_} 0x555555775000")
63586370

@@ -6361,24 +6373,24 @@ def __init__(self) -> None:
63616373
self["peek_nb_byte"] = (16, "Hexdump N first byte(s) inside the chunk data (0 to disable)")
63626374
return
63636375

6364-
@parse_arguments({"arena_address": ""}, {("--all", "-a"): True, "--allow-unaligned": True, "--min-size": 0, "--max-size": 0, ("--summary", "-s"): True})
6376+
@parse_arguments({"arena_address": ""}, {("--all", "-a"): True, "--allow-unaligned": True, "--min-size": 0, "--max-size": 0, ("--summary", "-s"): True, "--resolve": True})
63656377
@only_if_gdb_running
63666378
def do_invoke(self, _: List[str], **kwargs: Any) -> None:
63676379
args = kwargs["arguments"]
63686380
if args.all or not args.arena_address:
63696381
for arena in gef.heap.arenas:
6370-
self.dump_chunks_arena(arena, print_arena=args.all, allow_unaligned=args.allow_unaligned, min_size=args.min_size, max_size=args.max_size, summary=args.summary)
6382+
self.dump_chunks_arena(arena, print_arena=args.all, allow_unaligned=args.allow_unaligned, min_size=args.min_size, max_size=args.max_size, summary=args.summary, resolve_type=args.resolve)
63716383
if not args.all:
63726384
return
63736385
try:
63746386
arena_addr = parse_address(args.arena_address)
63756387
arena = GlibcArena(f"*{arena_addr:#x}")
6376-
self.dump_chunks_arena(arena, allow_unaligned=args.allow_unaligned, min_size=args.min_size, max_size=args.max_size, summary=args.summary)
6388+
self.dump_chunks_arena(arena, allow_unaligned=args.allow_unaligned, min_size=args.min_size, max_size=args.max_size, summary=args.summary, resolve_type=args.resolve)
63776389
except gdb.error:
63786390
err("Invalid arena")
63796391
return
63806392

6381-
def dump_chunks_arena(self, arena: GlibcArena, print_arena: bool = False, allow_unaligned: bool = False, min_size: int = 0, max_size: int = 0, summary: bool = False) -> None:
6393+
def dump_chunks_arena(self, arena: GlibcArena, print_arena: bool = False, allow_unaligned: bool = False, min_size: int = 0, max_size: int = 0, summary: bool = False, resolve_type: bool = False) -> None:
63826394
heap_addr = arena.heap_addr(allow_unaligned=allow_unaligned)
63836395
if heap_addr is None:
63846396
err("Could not find heap for arena")
@@ -6387,18 +6399,18 @@ def dump_chunks_arena(self, arena: GlibcArena, print_arena: bool = False, allow_
63876399
gef_print(str(arena))
63886400
if arena.is_main_arena():
63896401
heap_end = arena.top + GlibcChunk(arena.top, from_base=True).size
6390-
self.dump_chunks_heap(heap_addr, heap_end, arena, allow_unaligned=allow_unaligned, min_size=min_size, max_size=max_size, summary=summary)
6402+
self.dump_chunks_heap(heap_addr, heap_end, arena, allow_unaligned=allow_unaligned, min_size=min_size, max_size=max_size, summary=summary, resolve_type=resolve_type)
63916403
else:
63926404
heap_info_structs = arena.get_heap_info_list() or []
63936405
for heap_info in heap_info_structs:
6394-
if not self.dump_chunks_heap(heap_info.heap_start, heap_info.heap_end, arena, allow_unaligned=allow_unaligned, min_size=min_size, max_size=max_size, summary=summary):
6406+
if not self.dump_chunks_heap(heap_info.heap_start, heap_info.heap_end, arena, allow_unaligned=allow_unaligned, min_size=min_size, max_size=max_size, summary=summary, resolve_type=resolve_type):
63956407
break
63966408
return
63976409

6398-
def dump_chunks_heap(self, start: int, end: int, arena: GlibcArena, allow_unaligned: bool = False, min_size: int = 0, max_size: int = 0, summary: bool = False) -> bool:
6410+
def dump_chunks_heap(self, start: int, end: int, arena: GlibcArena, allow_unaligned: bool = False, min_size: int = 0, max_size: int = 0, summary: bool = False, resolve_type: bool = False) -> bool:
63996411
nb = self["peek_nb_byte"]
64006412
chunk_iterator = GlibcChunk(start, from_base=True, allow_unaligned=allow_unaligned)
6401-
heap_summary = GlibcHeapArenaSummary()
6413+
heap_summary = GlibcHeapArenaSummary(resolve_type=resolve_type)
64026414
for chunk in chunk_iterator:
64036415
heap_corrupted = chunk.base_address > end
64046416
should_process = self.should_process_chunk(chunk, min_size, max_size)

tests/commands/heap.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,14 @@ def test_cmd_heap_chunks_summary(self):
103103
self.assertIn("== Chunk distribution by size", res)
104104
self.assertIn("== Chunk distribution by flag", res)
105105

106+
def test_cmd_heap_chunks_summary_with_type_resolved(self):
107+
cmd = "heap chunks --summary --resolve"
108+
target = _target("class")
109+
res = gdb_run_silent_cmd(cmd, target=target, before=["b B<TraitA, TraitB>::Run()"])
110+
self.assertNoException(res)
111+
self.assertIn("== Chunk distribution by size", res)
112+
self.assertIn("B<TraitA, TraitB>", res)
113+
106114
def test_cmd_heap_chunks_min_size_filter(self):
107115
cmd = "heap chunks --min-size 16"
108116
target = _target("heap")

0 commit comments

Comments
 (0)