Repository: pwndbg/pwndbg

Issue:
The disasm context cache is not cleared when memory changes
TL;DR: memory change event should probably clear cache for disassembled instructions?

```
pwndbg> ctx disasm
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────[ DISASM / x86-64 / set emulate off ]─────────────────────────────────────────────
 ► 0x55555555aab0    endbr64
   0x55555555aab4    xor    ebp, ebp
   0x55555555aab6    mov    r9, rdx
   0x55555555aab9    pop    rsi
   0x55555555aaba    mov    rdx, rsp
   0x55555555aabd    and    rsp, 0xfffffffffffffff0
   0x55555555aac1    push   rax
   0x55555555aac2    push   rsp
   0x55555555aac3    xor    r8d, r8d
   0x55555555aac6    xor    ecx, ecx
   0x55555555aac8    lea    rdi, [rip - 0x1daf]
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> spray $rip
pwndbg> ctx disasm
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────[ DISASM / x86-64 / set emulate off ]─────────────────────────────────────────────
 ► 0x55555555aab0    endbr64
   0x55555555aab4    xor    ebp, ebp
   0x55555555aab6    mov    r9, rdx
   0x55555555aab9    pop    rsi
   0x55555555aaba    mov    rdx, rsp
   0x55555555aabd    and    rsp, 0xfffffffffffffff0
   0x55555555aac1    push   rax
   0x55555555aac2    push   rsp
   0x55555555aac3    xor    r8d, r8d
   0x55555555aac6    xor    ecx, ecx
   0x55555555aac8    lea    rdi, [rip - 0x1daf]
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> memoize
Caching is now OFF (pwndbg will work slower, use only for debugging pwndbg)
pwndbg> ctx disasm
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────[ DISASM / x86-64 / set emulate off ]─────────────────────────────────────────────
Invalid instructions at 0x55555555aab0










───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/10xg $rip
0x55555555aab0:	0x6161616161616161	0x6161616161616162
0x55555555aac0:	0x6161616161616163	0x6161616161616164
0x55555555aad0:	0x6161616161616165	0x6161616161616166
0x55555555aae0:	0x6161616161616167	0x6161616161616168
0x55555555aaf0:	0x6161616161616169	0x616161616161616a
pwndbg>
```


Discussion:
This is cached by this code and caching decorator:
https://github.com/pwndbg/pwndbg/blob/627115709084d67f3a895c172d97309a706d0841/pwndbg/disasm/__init__.py#L152-L161

Resolving patch:
diff --git a/pwndbg/gdblib/hooks.py b/pwndbg/gdblib/hooks.py
--- a/pwndbg/gdblib/hooks.py
+++ b/pwndbg/gdblib/hooks.py
@@ -62,7 +62,11 @@ def on_stop() -> None:
         "exit": (pwndbg.gdblib.events.exit,),
         "objfile": (pwndbg.gdblib.events.new_objfile,),
         "start": (pwndbg.gdblib.events.start,),
-        "cont": (pwndbg.gdblib.events.cont,),
+        "cont": (
+            pwndbg.gdblib.events.cont,
+            pwndbg.gdblib.events.mem_changed,
+            pwndbg.gdblib.events.reg_changed,
+        ),
         "thread": (pwndbg.gdblib.events.thread,),
         "prompt": (pwndbg.gdblib.events.before_prompt,),
         "forever": (),
diff --git a/pwndbg/gdblib/memory.py b/pwndbg/gdblib/memory.py
--- a/pwndbg/gdblib/memory.py
+++ b/pwndbg/gdblib/memory.py
@@ -96,6 +96,12 @@ def write(addr, data) -> None:
     # Throws an exception if can't access memory
     gdb.selected_inferior().write_memory(addr, data)
 
+    # Clear caches which are hooked to gdb.MemoryChanged events
+    # We do this explicitly because `write_memory` does not fire off
+    # the gdb.MemoryChanged event (see #1818)
+    pwndbg.lib.cache.clear_cache("stop")
+    pwndbg.lib.cache.clear_cache("cont")
+
 
 def peek(address):
     """peek(address) -> str
diff --git a/pwndbg/lib/cache.py b/pwndbg/lib/cache.py
--- a/pwndbg/lib/cache.py
+++ b/pwndbg/lib/cache.py
@@ -62,7 +62,7 @@ def __init__(self) -> None:
 
     def connect_event_hooks(self, event_hooks) -> None:
         """
-        A given cache until event may require multiple debugger events
+        A given _CacheUntilEvent object may require multiple debugger events
         to be handled properly. E.g. our `stop` cache needs to be handled
         by `stop`, `mem_changed` and `reg_changed` events.
         """


Test patch:
diff --git a/tests/gdb-tests/tests/test_context_commands.py b/tests/gdb-tests/tests/test_context_commands.py
--- a/tests/gdb-tests/tests/test_context_commands.py
+++ b/tests/gdb-tests/tests/test_context_commands.py
@@ -254,3 +254,40 @@ def assert_att(out):
         out[1] == "──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────"
     )
     assert_att(out)
+
+
+@pytest.mark.parametrize("patch_or_api", (True, False))
+def test_context_disasm_proper_render_on_mem_change_issue_1818(start_binary, patch_or_api):
+    start_binary(SYSCALLS_BINARY)
+
+    old = gdb.execute("context disasm", to_string=True).split("\n")
+
+    # Just a sanity check
+    assert old[0] == "LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA"
+    assert "mov    eax, 0" in old[2]
+    assert "mov    edi, 0x1337" in old[3]
+    assert "mov    esi, 0xdeadbeef" in old[4]
+    assert "mov    ecx, 0x10" in old[5]
+    assert "syscall" in old[6]
+
+    # 5 bytes because 'mov eax, 0' is 5 bytes long
+    if patch_or_api:
+        gdb.execute("patch $rip nop;nop;nop;nop;nop", to_string=True)
+    else:
+        # Do the same, but through write API
+        pwndbg.gdblib.memory.write(pwndbg.gdblib.regs.rip, b"\x90" * 5)
+
+    # Actual test: we expect the read memory to be different now ;)
+    # (and not e.g. returned incorrectly from a not cleared cache)
+    new = gdb.execute("context disasm", to_string=True).split("\n")
+
+    assert new[0] == "LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA"
+    assert "nop" in new[2]
+    assert "nop" in new[3]
+    assert "nop" in new[4]
+    assert "nop" in new[5]
+    assert "nop" in new[6]
+    assert "mov    edi, 0x1337" in new[7]
+    assert "mov    esi, 0xdeadbeef" in new[8]
+    assert "mov    ecx, 0x10" in new[9]
+    assert "syscall" in new[10]