From 8d1adc2a1eecbf134bba6685e92838a1fac5f03c Mon Sep 17 00:00:00 2001 From: Eshaan Gupta <146680427+Eshaan-byte@users.noreply.github.com> Date: Wed, 3 Dec 2025 18:00:45 +1100 Subject: [PATCH] Fix plist to support pointer-sized integer fields (#3448) (#3453) * Fix plist to support pointer-sized integer fields like size_t Resolves #3448 The plist command previously rejected fields that were not pointer types, even if they were pointer-sized integers (like size_t, uintptr_t) used to store addresses. Changes: - Accept integer types with size equal to architecture pointer size - Handle type validation for pointer-sized integers - Assume pointer-sized integers point to outer structure type This allows plist to work with structs like: struct node { size_t next; // Previously rejected, now works size_t prev; }; * Strip typedefs before checking if field is pointer-sized int size_t and similar types are TypeCode.TYPEDEF, not TypeCode.INT. We need to call strip_typedefs() to get the underlying type (e.g., size_t -> unsigned long) before checking the type code. Thanks to @jackmisbach for catching this issue! * Add test cases for plist with size_t fields - Add test struct with size_t next pointer to linked-lists.c - Add test_command_plist_size_t_field() to both GDB and DBG test suites - Tests verify plist correctly handles typedef-wrapped pointer-sized integers * Fix include placement in linked-lists.c Move stdint.h include to top of file with other includes * Set dereference-limit in size_t test cases Ensure the test runs with a sufficient dereference limit to traverse all 3 nodes * Use explicit count flag instead of dereference-limit in size_t test Use -c 3 flag to explicitly request 3 nodes, matching the pattern used in other plist tests * Simplify size_t test to use -f value flag Use -f value flag to only display the value field, avoiding issues with size_t formatting. This matches the pattern in test_command_plist_flat_field and provides a cleaner, more focused test of the core functionality. --- pwndbg/commands/plist.py | 30 +++++++++++++++++-- tests/binaries/host/linked-lists.c | 10 +++++++ tests/library/dbg/tests/test_command_plist.py | 19 ++++++++++++ tests/library/gdb/tests/test_command_plist.py | 18 +++++++++++ 4 files changed, 74 insertions(+), 3 deletions(-) diff --git a/pwndbg/commands/plist.py b/pwndbg/commands/plist.py index fe638ea02..8f41fd0b4 100644 --- a/pwndbg/commands/plist.py +++ b/pwndbg/commands/plist.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse from typing import Optional +import pwndbg.aglib.arch import pwndbg.aglib.memory import pwndbg.aglib.symbol import pwndbg.chain @@ -272,8 +273,21 @@ def plist( 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 != pwndbg.dbg_mod.TypeCode.POINTER: - print(message.error(f"{path}{sep}{next_ptr_name} is not a pointer")) + + # Check if the field is a pointer type, or an integer type with pointer size + # (e.g., size_t, uintptr_t, unsigned long on 64-bit systems) + # Strip typedefs to get the underlying type (e.g., size_t -> unsigned long) + underlying_type = next_ptr.type.strip_typedefs() + is_pointer_type = underlying_type.code == pwndbg.dbg_mod.TypeCode.POINTER + is_pointer_sized_int = ( + underlying_type.code == pwndbg.dbg_mod.TypeCode.INT + and underlying_type.sizeof == pwndbg.aglib.arch.ptrsize + ) + + if not (is_pointer_type or is_pointer_sized_int): + print( + message.error(f"{path}{sep}{next_ptr_name} is not a pointer or pointer-sized integer") + ) return # If the user wants a specific field to be displayed, resolve it. @@ -359,7 +373,17 @@ def plist( # 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: + + # For pointer-sized integers (like size_t), we can't validate the target type + # since they don't have a target() method. We assume they point to the outer structure. + if is_pointer_sized_int: + # For integer types, we assume pointee_offset is 0 (pointing to outer structure) + # unless we have an inner structure, in which case the user needs to use --inner + # and we can't automatically determine the offset. + if inner is not None: + # We can't determine if it points to inner or outer type, so assume outer + pointee_offset = 0 + elif 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. diff --git a/tests/binaries/host/linked-lists.c b/tests/binaries/host/linked-lists.c index 705cf29e9..eb09b4b01 100644 --- a/tests/binaries/host/linked-lists.c +++ b/tests/binaries/host/linked-lists.c @@ -1,4 +1,5 @@ #include /* For NULL. */ +#include /* For size_t. */ /* Linked list in which the pointer to the next element in inside the node * structure itself. */ @@ -39,6 +40,15 @@ 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 } }; +/* Linked list using size_t for next pointer to test pointer-sized integer support. */ +struct size_t_node { + int value; + size_t next; +}; +struct size_t_node size_t_node_c = { 42, 0 }; +struct size_t_node size_t_node_b = { 21, (size_t)&size_t_node_c }; +struct size_t_node size_t_node_a = { 10, (size_t)&size_t_node_b }; + void break_here(void) {} int main(void) diff --git a/tests/library/dbg/tests/test_command_plist.py b/tests/library/dbg/tests/test_command_plist.py index a61fc41cf..0fe923dfd 100644 --- a/tests/library/dbg/tests/test_command_plist.py +++ b/tests/library/dbg/tests/test_command_plist.py @@ -309,3 +309,22 @@ async def test_command_plist_nested_indirect(ctrl: Controller): result_str = await ctrl.execute_and_capture("plist inner_a_node_a -i inner next") assert expected_out.match(result_str) is not None + + +@pwndbg_test +async def test_command_plist_size_t_field(ctrl: Controller): + """ + Tests the plist command with size_t fields (pointer-sized integers) + """ + await startup(ctrl) + + expected_out = re.compile( + """\ +0[xX][0-9a-fA-F]+ :.* 10\\s* +0[xX][0-9a-fA-F]+ :.* 21\\s* +0[xX][0-9a-fA-F]+ :.* 42\\s* +""" + ) + + result_str = await ctrl.execute_and_capture("plist size_t_node_a next -f value -c 3") + assert expected_out.match(result_str) is not None diff --git a/tests/library/gdb/tests/test_command_plist.py b/tests/library/gdb/tests/test_command_plist.py index c7be38c1b..f5029e73b 100644 --- a/tests/library/gdb/tests/test_command_plist.py +++ b/tests/library/gdb/tests/test_command_plist.py @@ -297,3 +297,21 @@ def test_command_plist_nested_indirect(start_binary): result_str = gdb.execute("plist inner_a_node_a -i inner next", to_string=True) assert expected_out.match(result_str) is not None + + +def test_command_plist_size_t_field(start_binary): + """ + Tests the plist command with size_t fields (pointer-sized integers) + """ + startup(start_binary) + + expected_out = re.compile( + """\ +0[xX][0-9a-fA-F]+ : 10\\s* +0[xX][0-9a-fA-F]+ : 21\\s* +0[xX][0-9a-fA-F]+ : 42\\s* +""" + ) + + result_str = gdb.execute("plist size_t_node_a next -f value -c 3", to_string=True) + assert expected_out.match(result_str) is not None