From 96b73121699a9cd2420b08c2a48ff0e806bfaa64 Mon Sep 17 00:00:00 2001
From: Karan Kurani <karankurani3k@gmail.com>
Date: Fri, 24 Jul 2026 13:15:37 +0000
Subject: [PATCH 1/2] libdw: Add checked initial-length readers

The read_*_unaligned_inc macros leave it to each caller to have checked
that enough bytes are left before the read.  Add checked variants for the
four and eight byte incrementing reads that take the end of the buffer and
do that check themselves, so the check and the read cannot get separated.

__libdw_can_read compares the cursor against the end before subtracting
them, so it never forms a pointer outside the buffer and never computes
cursor plus width.  The readers return false when the complete value is
not available, leaving both the cursor and the result untouched, and reuse
the existing non-incrementing readers so the byte order handling is
unchanged.  They are static inline and internal, so there is no API or ABI
change.

Four byte reads keep a uint32_t result.  Callers that need a wider value
widen explicitly.

tests/read_unaligned.c gains coverage for the boundaries: exactly enough
bytes, one byte short, cursor at the end, cursor past the end within the
same array, both byte orders, and a failing read after a successful one.

Signed-off-by: Karan Kurani <karankurani3k@gmail.com>
---
 libdw/memory-access.h  | 33 +++++++++++++++++++
 tests/read_unaligned.c | 73 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 106 insertions(+)

diff --git a/libdw/memory-access.h b/libdw/memory-access.h
index 6d79343c..f59c26f9 100644
--- a/libdw/memory-access.h
+++ b/libdw/memory-access.h
@@ -404,6 +404,39 @@ read_3ubyte_unaligned (Dwarf *dbg, const unsigned char *p)
      Addr = (__typeof (Addr)) (((uintptr_t) (Addr)) + 3);		      \
      t_; })
 
+static inline bool
+__libdw_can_read (const unsigned char *addr, const unsigned char *end,
+		  size_t size)
+{
+  return addr <= end && size <= (size_t) (end - addr);
+}
+
+static inline bool
+read_4ubyte_unaligned_inc_checked (Dwarf *dbg, const unsigned char **addrp,
+				   const unsigned char *end, uint32_t *result)
+{
+  const unsigned char *addr = *addrp;
+  if (unlikely (! __libdw_can_read (addr, end, 4)))
+    return false;
+
+  *result = read_4ubyte_unaligned (dbg, addr);
+  *addrp = addr + 4;
+  return true;
+}
+
+static inline bool
+read_8ubyte_unaligned_inc_checked (Dwarf *dbg, const unsigned char **addrp,
+				   const unsigned char *end, uint64_t *result)
+{
+  const unsigned char *addr = *addrp;
+  if (unlikely (! __libdw_can_read (addr, end, 8)))
+    return false;
+
+  *result = read_8ubyte_unaligned (dbg, addr);
+  *addrp = addr + 8;
+  return true;
+}
+
 #define read_addr_unaligned_inc(Nbytes, Dbg, Addr)			\
   (assert ((Nbytes) == 4 || (Nbytes) == 8),				\
     ((Nbytes) == 4 ? read_4ubyte_unaligned_inc (Dbg, Addr)		\
diff --git a/tests/read_unaligned.c b/tests/read_unaligned.c
index 15e0c002..270d2666 100644
--- a/tests/read_unaligned.c
+++ b/tests/read_unaligned.c
@@ -332,6 +332,76 @@ static unsigned char be_mem[] =
     0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe,
   };
 
+static void
+check_checked_reads (Dwarf *dbg_le, Dwarf *dbg_be)
+{
+  const unsigned char bytes[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+  const unsigned char *p;
+  uint32_t u32;
+  uint64_t u64;
+
+  /* Four byte reads, both byte orders, with exactly four bytes left.  */
+  p = bytes;
+  assert (read_4ubyte_unaligned_inc_checked (dbg_le, &p, bytes + 4, &u32));
+  assert (u32 == 0x04030201 && p == bytes + 4);
+  p = bytes;
+  assert (read_4ubyte_unaligned_inc_checked (dbg_be, &p, bytes + 4, &u32));
+  assert (u32 == 0x01020304 && p == bytes + 4);
+
+  /* Eight byte reads, both byte orders, with exactly eight bytes left.  */
+  p = bytes;
+  assert (read_8ubyte_unaligned_inc_checked (dbg_le, &p, bytes + 8, &u64));
+  assert (u64 == UINT64_C (0x0807060504030201) && p == bytes + 8);
+  p = bytes;
+  assert (read_8ubyte_unaligned_inc_checked (dbg_be, &p, bytes + 8, &u64));
+  assert (u64 == UINT64_C (0x0102030405060708) && p == bytes + 8);
+
+  /* One byte short.  Neither the cursor nor the result may change.  */
+  p = bytes;
+  u32 = UINT32_MAX;
+  assert (! read_4ubyte_unaligned_inc_checked (dbg_le, &p, bytes + 3, &u32));
+  assert (u32 == UINT32_MAX && p == bytes);
+
+  p = bytes;
+  u64 = UINT64_MAX;
+  assert (! read_8ubyte_unaligned_inc_checked (dbg_le, &p, bytes + 7, &u64));
+  assert (u64 == UINT64_MAX && p == bytes);
+
+  /* Cursor at the end, and cursor past the end.  Both positions are
+     inside bytes[], so no unrelated pointers are compared.  */
+  p = bytes + 4;
+  u32 = UINT32_MAX;
+  assert (! read_4ubyte_unaligned_inc_checked (dbg_le, &p, bytes + 4, &u32));
+  assert (u32 == UINT32_MAX && p == bytes + 4);
+
+  p = bytes + 5;
+  u32 = UINT32_MAX;
+  assert (! read_4ubyte_unaligned_inc_checked (dbg_le, &p, bytes + 4, &u32));
+  assert (u32 == UINT32_MAX && p == bytes + 5);
+
+  /* A failing read after a successful one keeps the position reached by
+     the successful read.  */
+  p = bytes;
+  assert (read_4ubyte_unaligned_inc_checked (dbg_le, &p, bytes + 6, &u32));
+  assert (p == bytes + 4);
+  uint32_t kept = u32;
+  assert (! read_4ubyte_unaligned_inc_checked (dbg_le, &p, bytes + 6, &u32));
+  assert (u32 == kept && p == bytes + 4);
+
+  /* __libdw_can_read rejects an ordering it cannot subtract safely.  */
+  assert (! __libdw_can_read (bytes + 5, bytes + 4, 1));
+  assert (__libdw_can_read (bytes, bytes + 4, 4));
+  assert (! __libdw_can_read (bytes, bytes + 4, 5));
+
+  /* The DWARF initial length widening the converted parsers perform.  */
+  p = bytes;
+  uint32_t initial_length;
+  assert (read_4ubyte_unaligned_inc_checked (dbg_le, &p, bytes + 4,
+					     &initial_length));
+  uint64_t unit_length = initial_length;
+  assert (unit_length == UINT64_C (0x04030201));
+}
+
 int
 main (int argc, char **argv __attribute__((unused)))
 {
@@ -354,6 +424,9 @@ main (int argc, char **argv __attribute__((unused)))
   Dwarf dbg_le = { .other_byte_order = !is_le };
   Dwarf dbg_be = { .other_byte_order = is_le };
 
+  if (! write)
+    check_checked_reads (&dbg_le, &dbg_be);
+
   unsigned char *p_le = le_mem;
   unsigned char *p_be = be_mem;
 
-- 
2.44.0

