Mirror of the gdb-patches mailing list
 help / color / mirror / Atom feed
From: simon.marchi@polymtl.ca
To: gdb-patches@sourceware.org
Cc: Simon Marchi <simon.marchi@polymtl.ca>
Subject: [PATCH] gdb: fix out-of-bounds read when printing a pointer with a format
Date: Sun,  4 Oct 2026 22:46:46 -0400	[thread overview]
Message-ID: <20261005024658.927684-1-simon.marchi@polymtl.ca> (raw)

From: Simon Marchi <simon.marchi@polymtl.ca>

I stumbled on this while investigating bug 34638 ("Multi-register-reading
oddity / anachronism"), but this patch is not otherwise related to that
bug.

When the value being printed is a pointer, print_scalar_formatted
overrides the number of bytes it reads from the value buffer:

  if (type->code () == TYPE_CODE_PTR)
    len = gdbarch_addr_bit (gdbarch) / TARGET_CHAR_BIT;

VALADDR only holds type->length() bytes though, and for a pointer type
built by GDB that is `gdbarch_ptr_bit / TARGET_CHAR_BIT` bytes.  On targets
where gdbarch_addr_bit is larger than gdbarch_ptr_bit, we therefore read
past the end of the buffer.

AVR is such a target: gdbarch_ptr_bit is 16 while gdbarch_addr_bit is 32,
so printing any pointer with a format reads 4 bytes out of a 2-byte buffer.
With a GDB built with AddressSanitizer:

    (gdb) p/x $sp
    =================================================================
    ==433709==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7c11d1239e33 at pc 0x55eb8e8438f0 bp 0x7ffc17c69050 sp 0x7ffc17c69040

It can be reproduced without running a program, simply with a GDB that has
AVR support, by forcing the architecture to be AVR:

    (gdb) set architecture avr
    The target architecture is set to "avr".
    (gdb) p/x (void *) 0x1234
    =================================================================
    ==434356==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7bbca0e21293 at pc 0x55f5d74d08f0 bp 0x7ffe74cba960 sp 0x7ffe74cba950

"info registers SP" on an AVR target goes through the same path, so hits
the same crash:

    (gdb) c
    Continuing.

    Breakpoint 1, main () at test.c:16
    16        int local = 42;
    (gdb) info registers SP
    =================================================================
    ==432390==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7b764403b413 at pc 0x55e8ef8628f0 bp 0x7fff13994110 sp 0x7fff13994100

The override comes with this comment:

  /* If the value is a pointer, and pointers and addresses are not the
     same, then at this point, the value's length (in target bytes) is
     gdbarch_addr_bit/TARGET_CHAR_BIT, not type->length ().  */

This was true when the override was introduced, in commit ef166cf46a1b
(2001).  At the time, print_scalar_formatted started by converting the
value to a LONGEST:

  val_long = unpack_long (type, valaddr);

For pointers, unpack_long goes through extract_typed_address, then
gdbarch_pointer_to_address, which converts the pointer to a CORE_ADDR
gdbarch_addr_bit wide.  On AVR, for instance, data pointer 0x1234 (2 bytes
long) becomes address 0x801234 (4 bytes long).  "The value" in the comment
refers to this VAL_LONG, not to the bytes at VALADDR.  LEN was then only
used to mask VAL_LONG, and the override made sure that the mask did not
truncate the converted address to gdbarch_ptr_bit bits.  VALADDR (2 bytes)
was never read using LEN (4 bytes), so there was no out-of-bounds read.

Commit d9109c8080ac ("Simplify print_scalar_formatted", 2017) changed the
function to print the value using the print_*_chars functions, which
operate directly on the bytes at VALADDR, and removed the conversion to
LONGEST.  From then on, the value printed became the raw pointer
representation, type->length() bytes long (2 bytes), not the converted
address (4 bytes).  The length override was left in place, and this is
when the out-of-bounds read was introduced.

Note that commit d9109c8080ac therefore changed how `print (void *) 0x1234`
is printed for architectures like AVR, it went from

    $1 = 0x801234

to just

    $1 = 0x1234

I don't think it's a mistake in itself, but I am not sure it was fully
understood at the time.

Since the LEN override no longer has a reason to be, remove it.

With LEN now always equal to type->length(), the following block becomes
dead code, so remove it.  The comment above it is also a relic of the
VAL_LONG time.

I started by adding a very simple test to
gdb.base/all-architectures.exp.tcl to try to print a formatted pointer with
each arch.  I then thought it would be useful to test printing a pointer
without formatting, and then the various kinds of pointers (data vs code).
The result is the somewhat complicated procedure in this patch.  It did not
find additional bugs, but I think it's still useful to have in the
testsuite, as it captures what GDB does today.  If a change in the pointer
printing code ever changes (notably in the address <-> pointer conversion),
we will notice it.

Change-Id: I287eb515a019941b63325e3a258b1eb1b5225e4a
---
 gdb/printcmd.c                                | 16 ----
 .../gdb.base/all-architectures.exp.tcl        | 76 +++++++++++++++++++
 2 files changed, 76 insertions(+), 16 deletions(-)

diff --git a/gdb/printcmd.c b/gdb/printcmd.c
index 4fd369c90c06..09b22ae5010c 100644
--- a/gdb/printcmd.c
+++ b/gdb/printcmd.c
@@ -373,22 +373,6 @@ print_scalar_formatted (const gdb_byte *valaddr, struct type *type,
   /* String printing should go through val_print_scalar_formatted.  */
   gdb_assert (options.format != 's');
 
-  /* If the value is a pointer, and pointers and addresses are not the
-     same, then at this point, the value's length (in target bytes) is
-     gdbarch_addr_bit/TARGET_CHAR_BIT, not type->length ().  */
-  if (type->code () == TYPE_CODE_PTR)
-    len = gdbarch_addr_bit (gdbarch) / TARGET_CHAR_BIT;
-
-  /* If we are printing it as unsigned, truncate it in case it is actually
-     a negative signed value (e.g. "print/u (short)-1" should print 65535
-     (if shorts are 16 bits) instead of 4294967295).  */
-  if (options.format != 'c'
-      && (options.format != 'd' || type->is_unsigned ()))
-    {
-      if (len < type->length () && byte_order == BFD_ENDIAN_BIG)
-	valaddr += type->length () - len;
-    }
-
   /* Allow LEN == 0, and in this case, don't assume that VALADDR is
      valid.  */
   const gdb_byte zero = 0;
diff --git a/gdb/testsuite/gdb.base/all-architectures.exp.tcl b/gdb/testsuite/gdb.base/all-architectures.exp.tcl
index 7d2979476286..79f13ba7efa1 100644
--- a/gdb/testsuite/gdb.base/all-architectures.exp.tcl
+++ b/gdb/testsuite/gdb.base/all-architectures.exp.tcl
@@ -130,10 +130,86 @@ proc print_floats {} {
     gdb_test_internal "print 1.0f" " = 1" "print, float"
 }
 
+# Exercise printing various kinds of data and code pointers, with and
+# without a format, on architecture ARCH.  This is not necessarily a
+# source of truth, the test just matches what GDB happened to output
+# for each arch at the time of writing.
+
+proc print_pointers {arch} {
+    # Without a format, the pointer is converted to an address using
+    # gdbarch_pointer_to_address, which is sometimes in a different
+    # representation.
+    set data_addr 0x1234
+    set code_addr 0x1234
+
+    if {[string match "avr*" $arch]} {
+	# Data pointers point to SRAM, which starts at 0x800000 in the
+	# imaginary unified address space.
+	#
+	# Code pointers are word-addressed and are converted to a byte address
+	# in flash.
+	set data_addr 0x801234
+	set code_addr 0x2468
+    } elseif {[string match "ft32*" $arch]} {
+	# Data pointers point to RAM, which starts at 0x800000.
+	#
+	# Pointers with the flash address class (see the @flash
+	# pointers below) point to flash, so start at 0.
+	#
+	# Function pointers and pointers to @code behave like data pointers,
+	# not sure if it's a bug or not (maybe they should behave like @flash
+	# pointers).
+	set data_addr 0x801234
+	set code_addr 0x801234
+    } elseif {$arch == "rl78"} {
+	# Data pointers point to the 64 KiB region starting at
+	# 0xf0000.
+	set data_addr 0xf1234
+    } elseif {$arch == "iq2000" || $arch == "iq10"} {
+	# Code pointers have the 0x80000000 bit set.
+	set code_addr 0x80001234
+    }
+
+    # Each element is: test prefix, type, type regexp as printed by
+    # GDB, expected address.
+    #
+    # The @code and @data qualifiers apply to the pointed-to type, and
+    # are understood by all architectures.
+    set pointers [list \
+		      [list "data pointer" "void *" "void \\*" $data_addr] \
+		      [list "code pointer" "void (*) (void)"  "void \\(\\*\\)\\(void\\)" $code_addr] \
+		      [list "@data pointer" "void @data *" "@data void \\*" $data_addr] \
+		      [list "@code pointer" "void @code *" "@code void \\*" $code_addr]]
+
+    # AVR and FT32 have a "flash" address class, which applies to the
+    # pointer type itself.
+    if {[string match "avr*" $arch] || [string match "ft32*" $arch]} {
+	lappend pointers \
+	    [list "@flash data pointer" "void * @flash" "void \\* @flash" 0x1234] \
+	    [list "@flash code pointer" "void (* @flash) (void)" "void \\(\\* @flash\\)\\(void\\)" 0x1234]
+    }
+
+    foreach ptr $pointers {
+	lassign $ptr kind type_name type_name_re addr
+
+	with_test_prefix $kind {
+	    gdb_test_internal "print ($type_name) 0x1234" \
+		" = \\($type_name_re\\) $addr" "print"
+
+	    # With a format, the pointer's bytes are printed as is.
+	    gdb_test_internal "print/x ($type_name) 0x1234" " = 0x1234" "print/x"
+	    gdb_test_internal "print/d ($type_name) 0x1234" " = 4660" "print/d"
+	    gdb_test_internal "print/o ($type_name) 0x1234" " = 011064" "print/o"
+	    gdb_test_internal "print/t ($type_name) 0x1234" " = 1001000110100" "print/t"
+	}
+    }
+}
+
 # Run tests on the current architecture ARCH.
 
 proc do_arch_tests {arch} {
     print_floats
+    print_pointers $arch
 
     # When we disassemble using the default architecture then we
     # expect that the only error we should get from the disassembler

base-commit: 2ce6c3190773eb22f467446c4fdc7edb123ce82f
-- 
2.55.0


             reply	other threads:[~2026-10-05  2:47 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-05  2:46 simon.marchi [this message]
2026-10-15 10:42 ` Andrew Burgess
2026-10-15 12:52   ` Simon Marchi

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261005024658.927684-1-simon.marchi@polymtl.ca \
    --to=simon.marchi@polymtl.ca \
    --cc=gdb-patches@sourceware.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox