Repository: pwndbg/pwndbg

Issue:
Add a command to dump linked lists
A common issue I have is needing to dump linked lists in memory. This is possible in Python, but it's complicated:
```python
[print(pwndbg.gdblib.memory.poi(gdb.lookup_type('struct _IO_FILE'), x)) for x in pwndbg.chain.get(int(gdb.lookup_symbol('_IO_list_all')[0].value()['file'].address), 2, 104)]
```
Ideally this could be simplified into a command that would look like:
```
chain _IO_list_all.file _chain
```
as long as type information was available.


Discussion:

Resolving patch:
diff --git a/pwndbg/commands/__init__.py b/pwndbg/commands/__init__.py
--- a/pwndbg/commands/__init__.py
+++ b/pwndbg/commands/__init__.py
@@ -614,6 +614,7 @@ def load_commands() -> None:
     import pwndbg.commands.attachp
     import pwndbg.commands.auxv
     import pwndbg.commands.canary
+    import pwndbg.commands.chain
     import pwndbg.commands.checksec
     import pwndbg.commands.comments
     import pwndbg.commands.config
diff --git a/pwndbg/commands/chain.py b/pwndbg/commands/chain.py
new file mode 100644
--- /dev/null
+++ b/pwndbg/commands/chain.py
@@ -0,0 +1,393 @@
+import argparse
+
+import gdb
+
+import pwndbg.chain
+import pwndbg.commands
+import pwndbg.gdblib.memory
+from pwndbg.color import message
+
+parser = argparse.ArgumentParser(
+    formatter_class=argparse.RawTextHelpFormatter,
+    description="""Dumps the elements of a linked list.
+
+This command traverses the linked list beginning at a given element, dumping its
+contents and the contents of all the elements that come after it in the list.
+Traversal is configurable and can handle multiple types of chains, but will
+always stop when a cycle is detected.
+
+The path to the first element can be any GDB expression that evaluates to either
+the first element directly, or a to pointer to it. The next element is the name
+of the field containing the next pointer, in either the structure itself or in
+the structure given by --inner.
+
+An address value may be given with --sentinel that signals the end of the list.
+By default, the value used is NULL (0).
+
+If only one field inside each node is desired, it can be printed exclusively by
+specifying its name with --field.
+
+This command supports traversing three types of linked lists, classified by how
+the next pointer can be found in the structure and what type it is:
+    1 - Next pointer is field of structure, type is the same as structure.
+    2 - Next pointer is field of inner nested structure, pointed to type is the
+        same as outer structure.
+    3 - Next pointer is field of inner nested structure, pointed to type is the
+        same as inner structure.
+Types 2 and 3 require --inner to be specified.
+
+Example 1:
+
+```
+struct node {
+    int value;
+    struct node *next;
+};
+struct node node_c = { 2, NULL };
+struct node node_b = { 1, &node_c };
+struct node node_a = { 0, &node_b };
+```
+
+pwndbg> chain node_a next
+0x4000011050 <node_a>: {
+  value = 0,
+  next = 0x4000011040 <node_b>
+}
+0x4000011040 <node_b>: {
+  value = 1,
+  next = 0x4000011010 <node_c>
+}
+0x4000011010 <node_c>: {
+  value = 2,
+  next = 0x0
+}
+
+Example 2:
+
+```
+struct node_inner_a {
+    struct node_inner_a *next;
+};
+struct inner_a_node {
+    int value;
+    struct node_inner_a inner;
+};
+struct inner_a_node inner_a_node_c = { 2, { NULL } };
+struct inner_a_node inner_a_node_b = { 1, { &inner_a_node_c.inner } };
+struct inner_a_node inner_a_node_a = { 0, { &inner_a_node_b.inner } };
+```
+
+pwndbg> chain inner_a_node_a -i inner next
+0x4000011070 <inner_a_node_a>: {
+  value = 0,
+  inner = {
+    next = 0x4000011068 <inner_a_node_b+8>
+  }
+}
+0x4000011060 <inner_a_node_b>: {
+  value = 1,
+  inner = {
+    next = 0x4000011028 <inner_a_node_c+8>
+  }
+}
+0x4000011020 <inner_a_node_c>: {
+  value = 2,
+  inner = {
+    next = 0x0
+  }
+}
+
+Example 3:
+
+```
+struct inner_b_node;
+struct node_inner_b {
+    struct inner_b_node *next;
+};
+struct inner_b_node {
+    int value;
+    struct node_inner_b inner;
+};
+struct inner_b_node inner_b_node_c = { 2, { NULL } };
+struct inner_b_node inner_b_node_b = { 1, { &inner_b_node_c } };
+struct inner_b_node inner_b_node_a = { 0, { &inner_b_node_b } };
+```
+
+pwndbg> chain inner_b_node_a -i inner next
+0x4000011090 <inner_b_node_a>: {
+  value = 0,
+  inner = {
+    next = 0x4000011080 <inner_b_node_b>
+  }
+}
+0x4000011080 <inner_b_node_b>: {
+  value = 1,
+  inner = {
+    next = 0x4000011030 <inner_b_node_c>
+  }
+}
+0x4000011030 <inner_b_node_c>: {
+  value = 2,
+  inner = {
+    next = 0x0
+  }
+}
+
+""",
+)
+parser.add_argument(
+    "path",
+    type=str,
+    help="The first element of the chain",
+)
+parser.add_argument(
+    "next", type=str, help="The name of the field pointing to the next element in the chain"
+)
+parser.add_argument(
+    "-s",
+    "--sentinel",
+    dest="sentinel",
+    type=int,
+    default=0,
+    help="The address that stands in for an end of list marker in a non-cyclic list",
+)
+parser.add_argument(
+    "-i",
+    "--inner",
+    dest="inner_name",
+    type=str,
+    help="The name of the inner nested structure where the next pointer is stored",
+)
+parser.add_argument(
+    "-f",
+    "--field",
+    dest="field_name",
+    type=str,
+    help="The name of the field to be displayed, if only one is desired",
+)
+
+
+@pwndbg.commands.ArgparsedCommand(parser, command_name="chain")
+def chain(path, next, sentinel, inner_name, field_name) -> None:
+    # Have GDB parse the path for us and check if it's valid.
+    try:
+        first = gdb.parse_and_eval(path)
+    except gdb.error as e:
+        print(message.error(f"{e}"))
+        return
+
+    if first.is_optimized_out:
+        print(message.error(f"{path} has been optimized out"))
+        return
+
+    # We suport being passed either a pointer to the first structure or the
+    # structure itself, for the sake of convenience. But we don't bother with
+    # chains of pointers. Additionally, we pick the correct separator to use
+    # for the error mesages.
+    sep = "."
+    deref = ""
+    if first.type.code == gdb.TYPE_CODE_PTR:
+        sep = "->"
+        deref = "*"
+        try:
+            first = first.dereference()
+        except gdb.error as e:
+            print(message.error(f"Pointer at {path} could not be dereferenced: {e}"))
+            return
+
+    if first.type.code == gdb.TYPE_CODE_PTR:
+        print(message.error(f"{path} is not a value or a single pointer to one"))
+        return
+
+    if first.address is None:
+        print(message.error(f"{deref}{path} is not addressable"))
+        return
+
+    if first.is_optimized_out:
+        print(message.error(f"{deref}{path} has been optimized out"))
+        return
+
+    # If there is an inner element we have to use, find it.
+    inner = None
+    inner_sep = ""
+    if inner_name is not None:
+        try:
+            inner = first[inner_name]
+            inner_sep = "->"
+        except gdb.error as e:
+            print(message.error(f"Cannot find component {inner_name} in {path}: {e}"))
+            return
+        if inner.is_optimized_out:
+            print(message.error(f"{path}{sep}{inner_name} has been optimized out"))
+            return
+
+    # Resolve the pointer to the next structure, wherever it may be, and make
+    # sure that we can use it to traverse the chain.
+    next_ptr_loc = first
+    next_ptr_name = next
+    try:
+        if inner is None:
+            next_ptr = first[next]
+        else:
+            next_ptr = inner[next]
+            next_ptr_loc = inner
+            next_ptr_name = f"{inner_name}.{next}"
+    except gdb.error as e:
+        print(message.error(f"Cannot find component {next_ptr_name} in {path}: {e}"))
+        return
+
+    if next_ptr.is_optimized_out:
+        print(message.error(f"{path}{sep}{next_ptr_name} has been optimized out"))
+        return
+    if next_ptr.type.code != gdb.TYPE_CODE_PTR:
+        print(message.error(f"{path}{sep}{next_ptr_name} is not a pointer"))
+        return
+
+    # If the user wants a specific field to be displayed, resolve it.
+    field_offset = None
+    field_type = None
+    if field_name is not None:
+        try:
+            field = first[field_name]
+        except gdb.error as e:
+            print(message.error(f"Cannot find component {field_name} in {path}: {e}"))
+            return
+        field_type = field.type
+
+        bit_offset = bit_offset_of_field(first, field_name)
+        if bit_offset is None:
+            print(
+                message.error(
+                    f"{path}{sep}{field_name} has no known offset \
+                from {deref}{path}"
+                )
+            )
+            return
+        byte_offset = get_byte_offset(bit_offset)
+        if byte_offset is None:
+            print(
+                message.error(
+                    f"{field_name} is a non-whole number of bytes \
+                {bit_offset} bits) offset from {deref}{path}"
+                )
+            )
+            return
+        field_offset = byte_offset
+
+    # Figure out the offset of the inner structure, if any, in typeof(first).
+    inner_offset = 0
+    if inner is not None:
+        bit_offset = bit_offset_of_field(first, inner_name)
+        if bit_offset is None:
+            print(
+                message.error(
+                    f"{path}{sep}{inner_name} has no known offset \
+                from {deref}{path}"
+                )
+            )
+            return
+        byte_offset = get_byte_offset(bit_offset)
+        if byte_offset is None:
+            print(
+                message.error(
+                    f"{inner_name} is a non-whole number of bytes \
+                {bit_offset} bits) offset from {deref}{path}"
+                )
+            )
+            return
+        inner_offset = byte_offset
+
+    # Figure out the offset of the next pointer in its containing type.
+    bit_offset = bit_offset_of_field(next_ptr_loc, next)
+    if bit_offset is None:
+        print(
+            message.error(
+                f"{path}{sep}{next_ptr_name} has no known offset \
+            from {deref}{path}{inner_sep}{inner_name}"
+            )
+        )
+        return
+    byte_offset = get_byte_offset(bit_offset)
+    if byte_offset is None:
+        print(
+            message.error(
+                f"{path}{sep}{next_ptr_name} is a non-whole \
+            number of bytes {bit_offset} bits) offset from \
+            {deref}{path}{inner_sep}{inner_name}"
+            )
+        )
+        return
+    next_offset = byte_offset
+
+    # If the next pointer points to an intance of the inner structure, we will
+    # additionally have to do the equivalent of container_of(next, typeof(first),
+    # inner_name).
+    #
+    # Here, we figure out how many bytes to subtract from *typeof(inner) so that
+    # we can have a *typeof(first).
+    pointee_offset = 0
+    if inner is not None and next_ptr.type.target() == inner.type:
+        pointee_offset = inner_offset
+    elif next_ptr.type.target() == first.type:
+        # We've already got everything we need for this mode.
+        pass
+    else:
+        print(
+            message.error(
+                f"{deref}{path}{sep}{next_ptr_name} has a \
+            different type than {path}"
+            )
+        )
+        return
+
+    # Now, we follow the chain. We have to do this in two steps, as, because we
+    # have the address of the first outer structure, in case pointee_offset is
+    # not zero, the offset for the first element will not be the same as the one
+    # for all of the elements after it.
+    offset0 = inner_offset + next_offset
+    offset1 = offset0 - pointee_offset
+    addresses = pwndbg.chain.get(int(first.address), limit=1, offset=offset0)
+    if len(addresses) > 1:
+        addresses.extend(pwndbg.chain.get(addresses[1], offset=offset1, include_start=False))
+
+    # Finally, dump the information in the addresses we've just gathered.
+    for i, address in enumerate(addresses):
+        if address == sentinel:
+            break
+        try:
+            # Always make sure we have the address of the outer structure.
+            if i > 0:
+                address -= pointee_offset
+
+            # Read the data and print it out.
+            target_type = first.type
+            target_address = address
+            if field_offset is not None:
+                target_type = field_type
+                target_address = address + field_offset
+
+            value = pwndbg.gdblib.memory.poi(target_type, target_address)
+
+            symbol = pwndbg.gdblib.symbol.get(target_address)
+            symbol = f"<{symbol}>" if symbol else ""
+
+            print(f"{target_address:#x} {symbol}: {value}")
+        except gdb.error as e:
+            print(message.error(f"Cannot dereference 0x{address:#x} for chain link #{i + 1}: {e}"))
+            print(message.error("Is the chain corrupted or is the sentinel value wrong?"))
+            return
+
+
+# Helper functions to resolve the offsets of fields inside a structure.
+def bit_offset_of_field(struct, field_name, inner_name=None):
+    offset = None
+    for field in struct.type.fields():
+        if field.name == field_name:
+            offset = field.bitpos
+    return offset
+
+
+def get_byte_offset(bit_offset):
+    if bit_offset % 8 != 0:
+        return None
+    return bit_offset // 8


Test patch:
diff --git a/tests/gdb-tests/tests/binaries/linked-lists.c b/tests/gdb-tests/tests/binaries/linked-lists.c
new file mode 100644
--- /dev/null
+++ b/tests/gdb-tests/tests/binaries/linked-lists.c
@@ -0,0 +1,46 @@
+#include <stddef.h> /* For NULL. */
+
+/* Linked list in which the pointer to the next element in inside the node 
+ * structure itself. */
+struct node {
+    int value;
+    struct node *next;
+};
+struct node node_c = { 2, NULL };
+struct node node_b = { 1, &node_c };
+struct node node_a = { 0, &node_b };
+
+/* Linked list in which the nodes are inner structures of a larger structure. */
+struct node_inner_a {
+    struct node_inner_a *next;
+};
+struct inner_a_node {
+    int value;
+    struct node_inner_a inner;
+};
+struct inner_a_node inner_a_node_c = { 2, { NULL } };
+struct inner_a_node inner_a_node_b = { 1, { &inner_a_node_c.inner } };
+struct inner_a_node inner_a_node_a = { 0, { &inner_a_node_b.inner } };
+
+/* Linked list in which the pointer to the next element is nested inside the 
+ * structure. */
+struct inner_b_node;
+struct node_inner_b {
+    struct inner_b_node *next;
+};
+struct inner_b_node {
+    int value;
+    struct node_inner_b inner;
+};
+struct inner_b_node inner_b_node_c = { 2, { NULL } };
+struct inner_b_node inner_b_node_b = { 1, { &inner_b_node_c } };
+struct inner_b_node inner_b_node_a = { 0, { &inner_b_node_b } };
+
+
+void break_here(void) {}
+int main(void)
+{
+    break_here();
+    return 0;
+}
+
diff --git a/tests/gdb-tests/tests/binaries/makefile b/tests/gdb-tests/tests/binaries/makefile
--- a/tests/gdb-tests/tests/binaries/makefile
+++ b/tests/gdb-tests/tests/binaries/makefile
@@ -38,7 +38,7 @@ GLIBC_2_33=$(PWD)/glibcs/2.33
 
 .PHONY : all clean
 
-CUSTOM_TARGETS = reference_bin_pie.out reference_bin_nopie.out symbol_1600_and_752.out initialized_heap_x64.out initialized_heap_i386_big.out
+CUSTOM_TARGETS = reference_bin_pie.out reference_bin_nopie.out symbol_1600_and_752.out initialized_heap_x64.out initialized_heap_i386_big.out linked_lists.out
 
 all: $(LINKED) $(LINKED_ASM) $(COMPILED_GO) $(CUSTOM_TARGETS)
 
@@ -134,6 +134,9 @@ clean :
 	@echo "[+] Cleaning stuff"
 	@rm -f $(COMPILED) $(LINKED) $(COMPILED_ASM) $(LINKED_ASM) $(COMPILED_GO) *.out *.o
 
+linked_lists.out: linked-lists.c
+	@echo "[+] Building $<"
+	${ZIGCC} -fpie -g -o $@ $<
 
 reference_bin_pie.out: reference-binary.c
 	@echo "[+] Building reference_bin_pie.out"
diff --git a/tests/gdb-tests/tests/test_command_chain.py b/tests/gdb-tests/tests/test_command_chain.py
new file mode 100644
--- /dev/null
+++ b/tests/gdb-tests/tests/test_command_chain.py
@@ -0,0 +1,146 @@
+import re
+
+import gdb
+
+import tests
+
+LINKED_LISTS_BINARY = tests.binaries.get("linked-lists.out")
+
+
+def startup(start_binary):
+    start_binary(LINKED_LISTS_BINARY)
+
+    gdb.execute("break break_here")
+    gdb.execute("run")
+    gdb.execute("up")
+
+
+def test_command_chain_flat_no_flags(start_binary):
+    """
+    Tests the chain for a non-nested linked list
+    """
+    startup(start_binary)
+
+    expected_out = re.compile(
+        """\
+0[xX][0-9a-fA-F]+ <node_a>: {\\s*
+  value = 0,\\s*
+  next = 0[xX][0-9a-fA-F]+ <node_b>\\s*
+}\\s*
+0[xX][0-9a-fA-F]+ <node_b>: {\\s*
+  value = 1,\\s*
+  next = 0[xX][0-9a-fA-F]+ <node_c>\\s*
+}\\s*
+0[xX][0-9a-fA-F]+ <node_c>: {\\s*
+  value = 2,\\s*
+  next = 0x0\\s*
+}"""
+    )
+
+    result_str = gdb.execute("chain node_a next", to_string=True)
+    assert expected_out.match(result_str) is not None
+
+
+def test_command_chain_flat_field(start_binary):
+    """
+    Tests the chain command for a non-nested linked list with field flag
+    """
+    startup(start_binary)
+
+    expected_out = re.compile(
+        """\
+0[xX][0-9a-fA-F]+ <node_a>: 0\\s*
+0[xX][0-9a-fA-F]+ <node_b>: 1\\s*
+0[xX][0-9a-fA-F]+ <node_c>: 2\\s*
+"""
+    )
+
+    result_str = gdb.execute("chain node_a next -f value", to_string=True)
+    assert expected_out.match(result_str) is not None
+
+
+def test_command_chain_flat_sentinel(start_binary):
+    """
+    Tests the chain command for a non-nested linked list with field flag
+    """
+    startup(start_binary)
+
+    sentinel = int(gdb.lookup_symbol("node_c")[0].value().address)
+    expected_out = re.compile(
+        """\
+0[xX][0-9a-fA-F]+ <node_a>: {\\s*
+  value = 0,\\s*
+  next = 0[xX][0-9a-fA-F]+ <node_b>\\s*
+}\\s*
+0[xX][0-9a-fA-F]+ <node_b>: {\\s*
+  value = 1,\\s*
+  next = 0[xX][0-9a-fA-F]+ <node_c>\\s*
+}"""
+    )
+
+    result_str = gdb.execute(f"chain node_a next -s {sentinel}", to_string=True)
+    assert expected_out.match(result_str) is not None
+
+
+def test_command_chain_nested_direct(start_binary):
+    """
+    Tests the chain for a nested linked list pointing to the outer structure
+    """
+    startup(start_binary)
+
+    expected_out = re.compile(
+        """\
+0[xX][0-9a-fA-F]+ <inner_b_node_a>: {\\s*
+  value = 0,\\s*
+  inner = {\\s*
+    next = 0[xX][0-9a-fA-F]+ <inner_b_node_b>\\s*
+  }\\s*
+}\\s*
+0[xX][0-9a-fA-F]+ <inner_b_node_b>: {\\s*
+  value = 1,\\s*
+  inner = {\\s*
+    next = 0[xX][0-9a-fA-F]+ <inner_b_node_c>\\s*
+  }\\s*
+}\\s*
+0[xX][0-9a-fA-F]+ <inner_b_node_c>: {\\s*
+  value = 2,\\s*
+  inner = {\\s*
+    next = 0x0\\s*
+  }\\s*
+}"""
+    )
+
+    result_str = gdb.execute("chain inner_b_node_a -i inner next", to_string=True)
+    assert expected_out.match(result_str) is not None
+
+
+def test_command_chain_nested_indirect(start_binary):
+    """
+    Tests the chain for a nested linked list pointing to the inner structure
+    """
+    startup(start_binary)
+
+    expected_out = re.compile(
+        """\
+0[xX][0-9a-fA-F]+ <inner_a_node_a>: {\\s*
+  value = 0,\\s*
+  inner = {\\s*
+    next = 0[xX][0-9a-fA-F]+ <inner_a_node_b\\+8>\\s*
+  }\\s*
+}\\s*
+0[xX][0-9a-fA-F]+ <inner_a_node_b>: {\\s*
+  value = 1,\\s*
+  inner = {\\s*
+    next = 0[xX][0-9a-fA-F]+ <inner_a_node_c\\+8>\\s*
+  }\\s*
+}\\s*
+0[xX][0-9a-fA-F]+ <inner_a_node_c>: {\\s*
+  value = 2,\\s*
+  inner = {\\s*
+    next = 0x0\\s*
+  }\\s*
+}"""
+    )
+
+    result_str = gdb.execute("chain inner_a_node_a -i inner next", to_string=True)
+    assert expected_out.match(result_str) is not None