Repository: pwndbg/pwndbg

Issue:
find_fake_fast --glibc-fastbin-bug option is broken
<!--
Before reporting a new issue, make sure that we do not have any duplicates already open.
If there is one it might be good to take part in the discussion there.

Please make sure you have checked that the issue persists on LATEST pwndbg version.

Below is a template for BUG REPORTS.
Don't include it if this is a FEATURE REQUEST.
-->


### Description

<!--
Briefly describe the problem you are having in a few paragraphs.
-->
Using the `find_fake_fast` option `--glibc-fastbin-bug` always results in an error, at least on 64-bit platforms.
This seems to be because the option causes [only 4 bytes](https://github.com/pwndbg/pwndbg/blob/8d0ccbc3eb94ee91b25c76a1b9e3201e7a70ad82/pwndbg/commands/heap.py#L686) to be read for the size, but then that gets [passed to `unpack()`](https://github.com/pwndbg/pwndbg/blob/8d0ccbc3eb94ee91b25c76a1b9e3201e7a70ad82/pwndbg/commands/heap.py#L761) which expects 8 bytes.


### Steps to reproduce

<!--
What do we have to do to reproduce the problem?
If this is connected to particular C/asm code, 
please provide the smallest C code that reproduces the issue.
-->

Run `find_fake_fast -b <address>` in a 64-bit environment and observe error.

### My setup

<!--
Show us your gdb/python/pwndbg/OS/IDA Pro version (depending on your case).

NOTE: We are currently supporting only Ubuntu installations.
It is known that pwndbg is not fully working e.g. on Arch Linux (the heap stuff is not working there).
If you would like to change this situation - help us improving pwndbg and supporting other distros!

This can be displayed in pwndbg through `version` command.

If it is somehow unavailable, use:
* `show version` - for gdb
* `py import sys; print(sys.version)` - for python
* pwndbg version/git commit id
-->

```
Gdb:      12.1
Python:   3.10.6 (main, May 29 2023, 11:10:38) [GCC 11.3.0]
Pwndbg:   2023.03.19 build: 79ade42
Capstone: 4.0.1024
Unicorn:  2.0.1
```


Discussion:

Resolving patch:
diff --git a/pwndbg/commands/heap.py b/pwndbg/commands/heap.py
--- a/pwndbg/commands/heap.py
+++ b/pwndbg/commands/heap.py
@@ -758,7 +758,7 @@ def find_fake_fast(
         candidate = search_region[i : i + size_field_width]
 
         if len(candidate) == size_field_width:
-            size_field = pwndbg.gdblib.arch.unpack(candidate)
+            size_field = pwndbg.gdblib.arch.unpack_size(candidate, size_field_width)
             size_field &= ~(allocator.malloc_align_mask)
 
             if size_field < min_chunk_size or size_field > max_candidate_size:
diff --git a/pwndbg/lib/arch.py b/pwndbg/lib/arch.py
--- a/pwndbg/lib/arch.py
+++ b/pwndbg/lib/arch.py
@@ -3,6 +3,9 @@
 
 from typing_extensions import Literal
 
+FMT_LITTLE_ENDIAN = {1: "B", 2: "<H", 4: "<I", 8: "<Q"}
+FMT_BIG_ENDIAN = {1: "B", 2: ">H", 4: ">I", 8: ">Q"}
+
 
 class Arch:
     def __init__(self, arch_name: str, ptrsize: int, endian: Literal["little", "big"]) -> None:
@@ -20,9 +23,8 @@ def update(self, arch_name: str, ptrsize: int, endian: Literal["little", "big"])
         self.ptrmask = (1 << 8 * ptrsize) - 1
         self.endian = endian
 
-        self.fmt = {(4, "little"): "<I", (4, "big"): ">I", (8, "little"): "<Q", (8, "big"): ">Q"}[
-            (self.ptrsize, self.endian)
-        ]
+        self.fmts = FMT_LITTLE_ENDIAN if endian == "little" else FMT_BIG_ENDIAN
+        self.fmt = self.fmts[self.ptrsize]
 
         if self.name == "arm" and self.endian == "big":
             self.qemu = "armeb"
@@ -36,3 +38,9 @@ def pack(self, integer: int) -> bytes:
 
     def unpack(self, data: bytes) -> int:
         return struct.unpack(self.fmt, data)[0]
+
+    def pack_size(self, integer: int, size: int) -> bytes:
+        return struct.pack(self.fmts[size], integer & self.ptrmask)
+
+    def unpack_size(self, data: bytes, size: int) -> int:
+        return struct.unpack(self.fmts[size], data)[0]


Test patch:
diff --git a/tests/gdb-tests/tests/binaries/heap_find_fake_fast.c b/tests/gdb-tests/tests/binaries/heap_find_fake_fast.c
--- a/tests/gdb-tests/tests/binaries/heap_find_fake_fast.c
+++ b/tests/gdb-tests/tests/binaries/heap_find_fake_fast.c
@@ -128,4 +128,9 @@ int main(void) {
     // range of the target address
     setup_mem(0x100, 0x100);
     break_here();
+
+    // A fastbin chunk with a size that has top 4 bytes clobbered, due to glibc
+    // fastbin size bug, this is still valid
+    setup_mem(0xAABBCCDD00000020, 0x8);
+    break_here();
 }
diff --git a/tests/gdb-tests/tests/heap/test_find_fake_fast.py b/tests/gdb-tests/tests/heap/test_find_fake_fast.py
--- a/tests/gdb-tests/tests/heap/test_find_fake_fast.py
+++ b/tests/gdb-tests/tests/heap/test_find_fake_fast.py
@@ -150,3 +150,11 @@ def test_find_fake_fast_command(start_binary):
     result = gdb.execute("find_fake_fast &target_address 0x100", to_string=True)
     check_no_results(result)
     gdb.execute("continue")
+
+    # setup_mem(0xAABBCCDD00000020, 0x8)
+    result = gdb.execute("find_fake_fast &target_address", to_string=True)
+    check_no_results(result)
+
+    result = gdb.execute("find_fake_fast &target_address --glibc-fastbin-bug", to_string=True)
+    check_result(result, 0xAABBCCDD00000020)
+    gdb.execute("continue")