Repository: pwndbg/pwndbg

Issue:
gdb.MemoryError: Cannot access memory at address While search main_arena
I have try to run `vis` at ARM arch with strippted GLIBC In this [line](https://github.com/pwndbg/pwndbg/blob/b9c9f12efcec8e5e11c8cd8fcedc69c856d1716a/pwndbg/heap/ptmalloc.py#L1414)

```
Traceback (most recent call last):
  File "/home/USER/pwndbg/pwndbg/commands/__init__.py", line 184, in __call__
    return self.function(*args, **kwargs)
  File "/home/USER/pwndbg/pwndbg/commands/__init__.py", line 315, in _OnlyWhenRunning
    return function(*a, **kw)
  File "/home/USER/pwndbg/pwndbg/commands/__init__.py", line 409, in _OnlyWithResolvedHeapSyms
    return _try2run_heap_command(function, a, kw)
  File "/home/USER/pwndbg/pwndbg/commands/__init__.py", line 377, in _try2run_heap_command
    raise err
  File "/home/USER/pwndbg/pwndbg/commands/__init__.py", line 358, in _try2run_heap_command
    return function(*a, **kw)
  File "/home/USER/pwndbg/pwndbg/commands/__init__.py", line 340, in _OnlyWhenHeapIsInitialized
    return function(*a, **kw)
  File "/home/USER/pwndbg/pwndbg/commands/heap.py", line 673, in vis_heap_chunks
    heap_region = arena.active_heap
  File "/home/USER/pwndbg/pwndbg/heap/ptmalloc.py", line 591, in active_heap
    self._active_heap = Heap(self.top, arena=self)
  File "/home/USER/pwndbg/pwndbg/heap/ptmalloc.py", line 352, in __init__
    main_arena = allocator.main_arena
  File "/home/USER/pwndbg/pwndbg/heap/ptmalloc.py", line 1414, in main_arena
    tmp_next = int(tmp_arena["next"])
gdb.MemoryError: Cannot access memory at address 0xXXXXXXXX (I remove the original address)

If that is an issue, you can report it on https://github.com/pwndbg/pwndbg/issues
(Please don't forget to search if it hasn't been reported before)
To generate the report and open a browser, you may run `bugreport --run-browser`
PS: Pull requests are welcome
> /home/USER/pwndbg/pwndbg/heap/ptmalloc.py(1414)main_arena()
-> tmp_next = int(tmp_arena["next"])
```

The address 0xXXXXXXXX  really relate for some driver , not relate for heap
And maybe it will be better to check the address before access


Discussion:

Resolving patch:
diff --git a/pwndbg/gdblib/memory.py b/pwndbg/gdblib/memory.py
--- a/pwndbg/gdblib/memory.py
+++ b/pwndbg/gdblib/memory.py
@@ -114,6 +114,22 @@ def peek(address):
     return None
 
 
+@pwndbg.lib.memoize.reset_on_stop
+def is_readable_address(address):
+    """is_readable_address(address) -> bool
+
+    Check if the address can be read by GDB.
+
+    Arguments:
+        address(int): Address to read
+
+    Returns:
+        :class:`bool`: Whether the address is readable.
+    """
+    # We use vmmap to check before `peek()` because accessing memory for embedded targets might be slow and expensive.
+    return pwndbg.gdblib.vmmap.find(address) is not None and peek(address) is not None
+
+
 def poke(address) -> bool:
     """poke(address)
 
diff --git a/pwndbg/heap/ptmalloc.py b/pwndbg/heap/ptmalloc.py
--- a/pwndbg/heap/ptmalloc.py
+++ b/pwndbg/heap/ptmalloc.py
@@ -12,6 +12,7 @@
 import pwndbg.disasm
 import pwndbg.gdblib.config
 import pwndbg.gdblib.events
+import pwndbg.gdblib.memory
 import pwndbg.gdblib.symbol
 import pwndbg.gdblib.tls
 import pwndbg.gdblib.typeinfo
@@ -323,7 +324,7 @@ def next_chunk(self):
             return None
 
         next = Chunk(self.address + self.real_size, arena=self.arena)
-        if pwndbg.gdblib.memory.peek(next.address):
+        if pwndbg.gdblib.memory.is_readable_address(next.address):
             return next
         else:
             return None
@@ -1381,7 +1382,7 @@ def main_arena(self):
                                 self._main_arena_addr = offset + instr.address + 4
 
             # Try to search main_arena in .data of libc if we can't find it via above trick
-            if not self._main_arena_addr or pwndbg.gdblib.vmmap.find(self._main_arena_addr) is None:
+            if not pwndbg.gdblib.memory.is_readable_address(self._main_arena_addr):
                 start = pwndbg.gdblib.symbol.address("_IO_2_1_stdin_")
                 end = pwndbg.gdblib.symbol.address("_IO_list_all")
                 # If we didn't have these symbols, we try to find them in the possible page
@@ -1398,11 +1399,16 @@ def main_arena(self):
                     for addr in range(start, end, pwndbg.gdblib.arch.ptrsize):
                         found = False
                         tmp_arena = self.malloc_state(addr)
-                        tmp_next = int(tmp_arena["next"])
+                        try:
+                            tmp_next = int(tmp_arena["next"])
+                        except gdb.MemoryError:
+                            # the memory after tmp_arena is not valid, stop searching
+                            break
                         # check if the `next` pointer of tmp_arena will point to the same address we guess
                         # e.g. when our process is single-threaded, &tmp_arena->next == &main_arena
                         # when our process is multi-threaded, &tmp_arena->next->...->next == &main_arena
-                        while tmp_next > 0:
+                        while tmp_next > 0 and tmp_next % pwndbg.gdblib.arch.ptrsize == 0:
+                            # tmp_next should align with ptrsize
                             if tmp_next == addr:
                                 self._main_arena_addr = addr
                                 found = True
@@ -1410,16 +1416,19 @@ def main_arena(self):
                             tmp_arena = self.malloc_state(tmp_next)
                             if (
                                 pwndbg.gdblib.vmmap.find(tmp_arena.get_field_address("next"))
-                                is not None
+                                is None
                             ):
-                                tmp_next = int(tmp_arena["next"])
-                            else:
                                 # if `&tmp_arena->next` is not valid, the linked list is broken, break this while loop and try `addr+pwndbg.gdblib.arch.ptrsize` again
+                                # Note: We use vmmap to check before accessing the memory because accessing memory for embedded targets might be slow and expensive.
+                                break
+                            try:
+                                tmp_next = int(tmp_arena["next"])
+                            except gdb.MemoryError:
                                 break
                         if found:
                             break
 
-        if self._main_arena_addr and pwndbg.gdblib.vmmap.find(self._main_arena_addr):
+        if pwndbg.gdblib.memory.is_readable_address(self._main_arena_addr):
             self._main_arena = Arena(self._main_arena_addr)
             return self._main_arena
 
@@ -1445,7 +1454,7 @@ def thread_arena(self):
                 if pwndbg.gdblib.tls.is_thread_local_variable(thread_arena_via_symbol):
                     thread_arena_struct_addr = pwndbg.gdblib.memory.u(thread_arena_via_symbol)
                     # Check &thread_arena is a valid address or not to avoid false positive.
-                    if pwndbg.gdblib.vmmap.find(thread_arena_struct_addr):
+                    if pwndbg.gdblib.memory.is_readable_address(thread_arena_struct_addr):
                         return Arena(thread_arena_struct_addr)
 
             if not self._thread_arena_offset and pwndbg.gdblib.symbol.address("__libc_calloc"):
@@ -1575,7 +1584,7 @@ def thread_arena(self):
                 tls_base = pwndbg.gdblib.tls.address
                 if tls_base:
                     thread_arena_struct_addr = tls_base + self._thread_arena_offset
-                    if pwndbg.gdblib.vmmap.find(thread_arena_struct_addr):
+                    if pwndbg.gdblib.memory.is_readable_address(thread_arena_struct_addr):
                         return Arena(pwndbg.gdblib.memory.pvoid(thread_arena_struct_addr))
 
             raise SymbolUnresolvableError("thread_arena")
@@ -1601,7 +1610,7 @@ def thread_cache(self):
             # Check &tcache is nearby TLS base or not to avoid false positive.
             if pwndbg.gdblib.tls.is_thread_local_variable(thread_cache_via_symbol):
                 thread_cache_struct_addr = pwndbg.gdblib.memory.u(thread_cache_via_symbol)
-                if pwndbg.gdblib.vmmap.find(thread_cache_struct_addr):
+                if pwndbg.gdblib.memory.is_readable_address(thread_cache_struct_addr):
                     self._thread_cache = self.tcache_perthread_struct(thread_cache_struct_addr)
                     return self._thread_cache
 
@@ -1610,7 +1619,7 @@ def thread_cache(self):
         if not self.multithreaded:
             try:
                 thread_cache_struct_addr = self.get_sbrk_heap_region().vaddr + 0x10
-                if pwndbg.gdblib.vmmap.find(thread_cache_struct_addr):
+                if pwndbg.gdblib.memory.is_readable_address(thread_cache_struct_addr):
                     self._thread_cache = self.tcache_perthread_struct(thread_cache_struct_addr)
                     return self._thread_cache
             except SymbolUnresolvableError:
@@ -1759,7 +1768,7 @@ def thread_cache(self):
                 thread_cache_struct_addr = pwndbg.gdblib.memory.pvoid(
                     tls_base + self._thread_cache_offset
                 )
-                if pwndbg.gdblib.vmmap.find(thread_cache_struct_addr):
+                if pwndbg.gdblib.memory.is_readable_address(thread_cache_struct_addr):
                     self._thread_cache = self.tcache_perthread_struct(thread_cache_struct_addr)
                     return self._thread_cache
 
@@ -1892,8 +1901,7 @@ def mp(self):
 
         # can't find the reference about mp_ in __libc_free, try to find it with heap boundaries of main_arena
         if (
-            not self._mp_addr
-            or pwndbg.gdblib.vmmap.find(self._mp_addr) is None
+            not pwndbg.gdblib.memory.is_readable_address(self._mp_addr)
             and self.possible_page_of_symbols
         ):
             libc_page = self.possible_page_of_symbols
@@ -1927,7 +1935,7 @@ def mp(self):
                 except StopIteration:
                     pass
 
-        if self._mp_addr and pwndbg.gdblib.vmmap.find(self._mp_addr) is not None:
+        if pwndbg.gdblib.memory.is_readable_address(self._mp_addr):
             self._mp = self.malloc_par(self._mp_addr)
             return self._mp
 
@@ -2060,7 +2068,7 @@ def global_max_fast(self):
                     elif instr.mnemonic == "ldr" and "[pc" in instr.op_str:
                         ldr_instr = instr
 
-        if self._global_max_fast_addr and pwndbg.gdblib.vmmap.find(self._global_max_fast_addr):
+        if pwndbg.gdblib.memory.is_readable_address(self._global_max_fast_addr):
             self._global_max_fast = pwndbg.gdblib.memory.u(self._global_max_fast_addr)
             return self._global_max_fast
 


Test patch:
diff --git a/tests/gdb-tests/tests/heap/test_heap.py b/tests/gdb-tests/tests/heap/test_heap.py
--- a/tests/gdb-tests/tests/heap/test_heap.py
+++ b/tests/gdb-tests/tests/heap/test_heap.py
@@ -6,10 +6,12 @@
 import pwndbg.gdblib.memory
 import pwndbg.gdblib.symbol
 import pwndbg.gdblib.typeinfo
+import pwndbg.gdblib.vmmap
 import pwndbg.glibc
 import pwndbg.heap
 import tests
 from pwndbg.heap.ptmalloc import SymbolUnresolvableError
+from pwndbg.lib.memory import Page
 
 HEAP_MALLOC_CHUNK = tests.binaries.get("heap_malloc_chunk.out")
 
@@ -175,7 +177,9 @@ def test_malloc_chunk_command_heuristic(start_binary):
 
 
 class mock_for_heuristic:
-    def __init__(self, mock_symbols=[], mock_all=False, mess_up_memory=False):
+    def __init__(
+        self, mock_symbols=[], mock_all=False, mess_up_memory=False, test_memory_parsing=False
+    ):
         self.mock_symbols = (
             mock_symbols  # every symbol's address in the list will be mocked to `None`
         )
@@ -187,10 +191,23 @@ def __init__(self, mock_symbols=[], mock_all=False, mess_up_memory=False):
         )
         # We mess up the memory in the page of the symbols, to make sure that the heuristic will not succeed by parsing the memory
         self.mess_up_memory = mess_up_memory
+
+        # Some addresses can be found by `pwndbg.gdblib.vmmap.find()`, but it is not a valid memory address to access (e.g. the address in [vsyscall])
+        # This option is to make sure that the heuristic will not affect by this
+        self.test_memory_parsing = test_memory_parsing
+
         if mess_up_memory:
             # Save all the memory before we mess it up
             self.page = pwndbg.heap.current.possible_page_of_symbols
             self.saved_memory = pwndbg.gdblib.memory.read(self.page.vaddr, self.page.memsz)
+        if test_memory_parsing:
+
+            def fake_vmmap_find(addr):
+                # The heuristics should work without vmmap working
+                return Page(0, 0xFFFFFFFFFFFFFFFF, 4, 0, "[deadbeaf]")
+
+            fake_vmmap_find.original = pwndbg.gdblib.vmmap.find
+            pwndbg.gdblib.vmmap.find = fake_vmmap_find
 
     def __enter__(self):
         def mock(original):
@@ -212,6 +229,8 @@ def _mock(symbol, *args, **kwargs):
         if self.mess_up_memory:
             # Fill the page with `0xff`
             pwndbg.gdblib.memory.write(self.page.vaddr, b"\xff" * self.page.memsz)
+        if self.test_memory_parsing:
+            pwndbg.gdblib.vmmap.find = pwndbg.gdblib.vmmap.find.original
 
     def __exit__(self, exc_type, exc_value, traceback):
         # Restore `pwndbg.gdblib.symbol.address` and `pwndbg.gdblib.symbol.static_linkage_symbol_address`
@@ -261,13 +280,15 @@ def test_main_arena_heuristic(start_binary):
     pwndbg.heap.current = type(pwndbg.heap.current)()  # Reset the heap object of pwndbg
 
     # Level 3: We check we can get the address of `main_arena` by parsing the memory
-    for _ in range(2):
-        with mock_for_heuristic(mock_all=True):
-            # Check the address of `main_arena` is correct
-            assert pwndbg.heap.current.main_arena.address == main_arena_addr_via_debug_symbol
+    with mock_for_heuristic(mock_all=True, test_memory_parsing=True):
+        # Check the address of `main_arena` is correct
+        assert pwndbg.heap.current.main_arena.address == main_arena_addr_via_debug_symbol
+        pwndbg.heap.current = type(pwndbg.heap.current)()  # Reset the heap object of pwndbg
+
         # Check if it works when there's more than one arena
         gdb.execute("continue")
         assert gdb.selected_thread().num == 2
+        assert pwndbg.heap.current.main_arena.address == main_arena_addr_via_debug_symbol
 
 
 def test_mp_heuristic(start_binary):
@@ -300,7 +321,7 @@ def test_mp_heuristic(start_binary):
     pwndbg.heap.current = type(pwndbg.heap.current)()  # Reset the heap object of pwndbg
 
     # Level 3: We check we can get the address of `mp_` by parsing the memory
-    with mock_for_heuristic(mock_all=True):
+    with mock_for_heuristic(mock_all=True, test_memory_parsing=True):
         # Check the address of `mp_` is correct
         assert pwndbg.heap.current.mp.address == mp_addr_via_debug_symbol