[aarch64] Enable pointer authentication support for aarch64 bare metal/kernel mode...
authorLuis Machado <luis.machado@arm.com>
Sun, 11 Sep 2022 19:47:18 +0000 (20:47 +0100)
committerLuis Machado <luis.machado@arm.com>
Tue, 21 Feb 2023 09:09:24 +0000 (09:09 +0000)
At the moment GDB only handles pointer authentication (pauth) for userspace
addresses and if we're debugging a Linux-hosted program.

The Linux Kernel can be configured to use pauth instructions for some
additional security hardening, but GDB doesn't handle this well.

To overcome this limitation, GDB needs a couple things:

1 - The target needs to advertise pauth support.
2 - The hook to remove non-address bits from a pointer needs to be registered
    in aarch64-tdep.c as opposed to aarch64-linux-tdep.c.

There is a patch for QEMU that addresses the first point, and it makes
QEMU's gdbstub expose a couple more pauth mask registers, so overall we will
have up to 4 pauth masks (2 masks or 4 masks):

pauth_dmask
pauth_cmask
pauth_dmask_high
pauth_cmask_high

pauth_dmask and pauth_cmask are the masks used to remove pauth signatures
from userspace addresses. pauth_dmask_high and pauth_cmask_high masks are used
to remove pauth signatures from kernel addresses.

The second point is easily addressed by moving code around.

When debugging a Linux Kernel built with pauth with an unpatched GDB, we get
the following backtrace:

 #0  __fput (file=0xffff0000c17a6400) at /repos/linux/fs/file_table.c:296
 #1  0xffff8000082bd1f0 in ____fput (work=<optimized out>) at /repos/linux/fs/file_table.c:348
 #2  0x30008000080ade30 [PAC] in ?? ()
 #3  0x30d48000080ade30 in ?? ()
 Backtrace stopped: previous frame identical to this frame (corrupt stack?)

With a patched GDB, we get something a lot more meaningful:

 #0  __fput (file=0xffff0000c1bcfa00) at /repos/linux/fs/file_table.c:296
 #1  0xffff8000082bd1f0 in ____fput (work=<optimized out>) at /repos/linux/fs/file_table.c:348
 #2  0xffff8000080ade30 [PAC] in task_work_run () at /repos/linux/kernel/task_work.c:179
 #3  0xffff80000801db90 [PAC] in resume_user_mode_work (regs=0xffff80000a96beb0) at /repos/linux/include/linux/resume_user_mode.h:49
 #4  do_notify_resume (regs=regs@entry=0xffff80000a96beb0, thread_flags=4) at /repos/linux/arch/arm64/kernel/signal.c:1127
 #5  0xffff800008fb9974 [PAC] in prepare_exit_to_user_mode (regs=0xffff80000a96beb0) at /repos/linux/arch/arm64/kernel/entry-common.c:137
 #6  exit_to_user_mode (regs=0xffff80000a96beb0) at /repos/linux/arch/arm64/kernel/entry-common.c:142
 #7  el0_svc (regs=0xffff80000a96beb0) at /repos/linux/arch/arm64/kernel/entry-common.c:638
 #8  0xffff800008fb9d34 [PAC] in el0t_64_sync_handler (regs=<optimized out>) at /repos/linux/arch/arm64/kernel/entry-common.c:655
 #9  0xffff800008011548 [PAC] in el0t_64_sync () at /repos/linux/arch/arm64/kernel/entry.S:586
 Backtrace stopped: Cannot access memory at address 0xffff80000a96c0c8

gdb/aarch64-linux-tdep.c
gdb/aarch64-tdep.c
gdb/aarch64-tdep.h
gdb/arch/aarch64.h

index 9d6ed358597ca00a6a2d44db94d0cc2e1b1701b5..0000b498f8959b44ed20404c834a4ad77fcea8d4 100644 (file)
@@ -1980,40 +1980,6 @@ aarch64_linux_decode_memtag_section (struct gdbarch *gdbarch,
   return tags;
 }
 
-/* AArch64 implementation of the remove_non_address_bits gdbarch hook.  Remove
-   non address bits from a pointer value.  */
-
-static CORE_ADDR
-aarch64_remove_non_address_bits (struct gdbarch *gdbarch, CORE_ADDR pointer)
-{
-  aarch64_gdbarch_tdep *tdep = gdbarch_tdep<aarch64_gdbarch_tdep> (gdbarch);
-
-  /* By default, we assume TBI and discard the top 8 bits plus the VA range
-     select bit (55).  */
-  CORE_ADDR mask = AARCH64_TOP_BITS_MASK;
-
-  if (tdep->has_pauth ())
-    {
-      /* Fetch the PAC masks.  These masks are per-process, so we can just
-        fetch data from whatever thread we have at the moment.
-
-        Also, we have both a code mask and a data mask.  For now they are the
-        same, but this may change in the future.  */
-      struct regcache *regs = get_current_regcache ();
-      CORE_ADDR cmask, dmask;
-
-      if (regs->cooked_read (tdep->pauth_reg_base, &dmask) != REG_VALID)
-       dmask = mask;
-
-      if (regs->cooked_read (tdep->pauth_reg_base + 1, &cmask) != REG_VALID)
-       cmask = mask;
-
-      mask |= aarch64_mask_from_pac_registers (cmask, dmask);
-    }
-
-  return aarch64_remove_top_bits (pointer, mask);
-}
-
 static void
 aarch64_linux_init_abi (struct gdbarch_info info, struct gdbarch *gdbarch)
 {
@@ -2066,12 +2032,6 @@ aarch64_linux_init_abi (struct gdbarch_info info, struct gdbarch *gdbarch)
   /* Syscall record.  */
   tdep->aarch64_syscall_record = aarch64_linux_syscall_record;
 
-  /* The top byte of a user space address known as the "tag",
-     is ignored by the kernel and can be regarded as additional
-     data associated with the address.  */
-  set_gdbarch_remove_non_address_bits (gdbarch,
-                                      aarch64_remove_non_address_bits);
-
   /* MTE-specific settings and hooks.  */
   if (tdep->has_mte ())
     {
index 0bf6085c43ae21d60354b3c4e9d7587c75bdfaea..5b1b9921f87e588f8251a77d858f8f312be1e5ac 100644 (file)
@@ -137,10 +137,14 @@ static const char *const aarch64_sve_register_names[] =
 
 static const char *const aarch64_pauth_register_names[] =
 {
-  /* Authentication mask for data pointer.  */
+  /* Authentication mask for data pointer, low half/user pointers.  */
   "pauth_dmask",
-  /* Authentication mask for code pointer.  */
-  "pauth_cmask"
+  /* Authentication mask for code pointer, low half/user pointers.  */
+  "pauth_cmask",
+  /* Authentication mask for data pointer, high half / kernel pointers.  */
+  "pauth_dmask_high",
+  /* Authentication mask for code pointer, high half / kernel pointers.  */
+  "pauth_cmask_high"
 };
 
 static const char *const aarch64_mte_register_names[] =
@@ -228,9 +232,19 @@ aarch64_frame_unmask_lr (aarch64_gdbarch_tdep *tdep,
       && frame_unwind_register_unsigned (this_frame,
                                         tdep->ra_sign_state_regnum))
     {
-      int cmask_num = AARCH64_PAUTH_CMASK_REGNUM (tdep->pauth_reg_base);
-      CORE_ADDR cmask = frame_unwind_register_unsigned (this_frame, cmask_num);
-      addr = addr & ~cmask;
+      /* VA range select (bit 55) tells us whether to use the low half masks
+        or the high half masks.  */
+      int cmask_num;
+      if (tdep->pauth_reg_count > 2 && addr & VA_RANGE_SELECT_BIT_MASK)
+       cmask_num = AARCH64_PAUTH_CMASK_HIGH_REGNUM (tdep->pauth_reg_base);
+      else
+       cmask_num = AARCH64_PAUTH_CMASK_REGNUM (tdep->pauth_reg_base);
+
+      /* By default, we assume TBI and discard the top 8 bits plus the VA range
+        select bit (55).  */
+      CORE_ADDR mask = AARCH64_TOP_BITS_MASK;
+      mask |= frame_unwind_register_unsigned (this_frame, cmask_num);
+      addr = aarch64_remove_top_bits (addr, mask);
 
       /* Record in the frame that the link register required unmasking.  */
       set_frame_previous_pc_masked (this_frame);
@@ -1326,8 +1340,8 @@ aarch64_dwarf2_frame_init_reg (struct gdbarch *gdbarch, int regnum,
          reg->loc.exp.len = 1;
          return;
        }
-      else if (regnum == AARCH64_PAUTH_DMASK_REGNUM (tdep->pauth_reg_base)
-              || regnum == AARCH64_PAUTH_CMASK_REGNUM (tdep->pauth_reg_base))
+      else if (regnum >= tdep->pauth_reg_base
+              && regnum < tdep->pauth_reg_base + tdep->pauth_reg_count)
        {
          reg->how = DWARF2_FRAME_REG_SAME_VALUE;
          return;
@@ -3507,8 +3521,8 @@ aarch64_cannot_store_register (struct gdbarch *gdbarch, int regnum)
     return 0;
 
   /* Pointer authentication registers are read-only.  */
-  return (regnum == AARCH64_PAUTH_DMASK_REGNUM (tdep->pauth_reg_base)
-         || regnum == AARCH64_PAUTH_CMASK_REGNUM (tdep->pauth_reg_base));
+  return (regnum >= tdep->pauth_reg_base
+         && regnum < tdep->pauth_reg_base + tdep->pauth_reg_count);
 }
 
 /* Implement the stack_frame_destroyed_p gdbarch method.  */
@@ -3536,6 +3550,51 @@ aarch64_stack_frame_destroyed_p (struct gdbarch *gdbarch, CORE_ADDR pc)
   return streq (inst.opcode->name, "ret");
 }
 
+/* AArch64 implementation of the remove_non_address_bits gdbarch hook.  Remove
+   non address bits from a pointer value.  */
+
+static CORE_ADDR
+aarch64_remove_non_address_bits (struct gdbarch *gdbarch, CORE_ADDR pointer)
+{
+  aarch64_gdbarch_tdep *tdep = gdbarch_tdep<aarch64_gdbarch_tdep> (gdbarch);
+
+  /* By default, we assume TBI and discard the top 8 bits plus the VA range
+     select bit (55).  */
+  CORE_ADDR mask = AARCH64_TOP_BITS_MASK;
+
+  if (tdep->has_pauth ())
+    {
+      /* Fetch the PAC masks.  These masks are per-process, so we can just
+        fetch data from whatever thread we have at the moment.
+
+        Also, we have both a code mask and a data mask.  For now they are the
+        same, but this may change in the future.  */
+      struct regcache *regs = get_current_regcache ();
+      CORE_ADDR cmask, dmask;
+      int dmask_regnum = AARCH64_PAUTH_DMASK_REGNUM (tdep->pauth_reg_base);
+      int cmask_regnum = AARCH64_PAUTH_CMASK_REGNUM (tdep->pauth_reg_base);
+
+      /* If we have a kernel address and we have kernel-mode address mask
+        registers, use those instead.  */
+      if (tdep->pauth_reg_count > 2
+         && pointer & VA_RANGE_SELECT_BIT_MASK)
+       {
+         dmask_regnum = AARCH64_PAUTH_DMASK_HIGH_REGNUM (tdep->pauth_reg_base);
+         cmask_regnum = AARCH64_PAUTH_CMASK_HIGH_REGNUM (tdep->pauth_reg_base);
+       }
+
+      if (regs->cooked_read (dmask_regnum, &dmask) != REG_VALID)
+       dmask = mask;
+
+      if (regs->cooked_read (cmask_regnum, &cmask) != REG_VALID)
+       cmask = mask;
+
+      mask |= aarch64_mask_from_pac_registers (cmask, dmask);
+    }
+
+  return aarch64_remove_top_bits (pointer, mask);
+}
+
 /* Initialize the current architecture based on INFO.  If possible,
    re-use an architecture from ARCHES, which is a list of
    architectures already created during this debugging session.
@@ -3674,19 +3733,35 @@ aarch64_gdbarch_init (struct gdbarch_info info, struct gdbarch_list *arches)
     }
 
   /* Add the pauth registers.  */
+  int pauth_masks = 0;
   if (feature_pauth != NULL)
     {
       first_pauth_regnum = num_regs;
       ra_sign_state_offset = num_pseudo_regs;
+
+      /* Size of the expected register set with all 4 masks.  */
+      int set_size = ARRAY_SIZE (aarch64_pauth_register_names);
+
+      /* QEMU exposes a couple additional masks for the high half of the
+        address.  We should either have 2 registers or 4 registers.  */
+      if (tdesc_unnumbered_register (feature_pauth,
+                                    "pauth_dmask_high") == 0)
+       {
+         /* We did not find pauth_dmask_high, assume we only have
+            2 masks.  We are not dealing with QEMU/Emulators then.  */
+         set_size -= 2;
+       }
+
       /* Validate the descriptor provides the mandatory PAUTH registers and
         allocate their numbers.  */
-      for (i = 0; i < ARRAY_SIZE (aarch64_pauth_register_names); i++)
+      for (i = 0; i < set_size; i++)
        valid_p &= tdesc_numbered_register (feature_pauth, tdesc_data.get (),
                                            first_pauth_regnum + i,
                                            aarch64_pauth_register_names[i]);
 
       num_regs += i;
       num_pseudo_regs += 1;    /* Count RA_STATE pseudo register.  */
+      pauth_masks = set_size;
     }
 
   /* Add the MTE registers.  */
@@ -3722,6 +3797,7 @@ aarch64_gdbarch_init (struct gdbarch_info info, struct gdbarch_list *arches)
   tdep->jb_elt_size = 8;
   tdep->vq = vq;
   tdep->pauth_reg_base = first_pauth_regnum;
+  tdep->pauth_reg_count = pauth_masks;
   tdep->ra_sign_state_regnum = -1;
   tdep->mte_reg_base = first_mte_regnum;
   tdep->tls_regnum_base = first_tls_regnum;
@@ -3838,6 +3914,11 @@ aarch64_gdbarch_init (struct gdbarch_info info, struct gdbarch_list *arches)
   if (tdep->has_pauth ())
     tdep->ra_sign_state_regnum = ra_sign_state_offset + num_regs;
 
+  /* Architecture hook to remove bits of a pointer that are not part of the
+     address, like memory tags (MTE) and pointer authentication signatures.  */
+  set_gdbarch_remove_non_address_bits (gdbarch,
+                                      aarch64_remove_non_address_bits);
+
   /* Add standard register aliases.  */
   for (i = 0; i < ARRAY_SIZE (aarch64_register_aliases); i++)
     user_reg_add (gdbarch, aarch64_register_aliases[i].name,
index 97b8d0c379311299b5f3fcab93b16bd72f46928d..ae38327ffab27918ed2a18ac7aa240b650314be9 100644 (file)
@@ -94,6 +94,8 @@ struct aarch64_gdbarch_tdep : gdbarch_tdep_base
   }
 
   int pauth_reg_base = 0;
+  /* Number of pauth masks.  */
+  int pauth_reg_count = 0;
   int ra_sign_state_regnum = 0;
 
   /* Returns true if the target supports pauth.  */
index de4461e8be17cce299f9698e9fea1018b383b47a..4b3f1b8eff8cb836f3b9a057486f77a09dc94c9e 100644 (file)
@@ -132,6 +132,12 @@ enum aarch64_regnum
 
 #define AARCH64_PAUTH_DMASK_REGNUM(pauth_reg_base) (pauth_reg_base)
 #define AARCH64_PAUTH_CMASK_REGNUM(pauth_reg_base) (pauth_reg_base + 1)
+/* The high versions of these masks are used for bare metal/kernel-mode pointer
+   authentication support.  */
+#define AARCH64_PAUTH_DMASK_HIGH_REGNUM(pauth_reg_base) (pauth_reg_base + 2)
+#define AARCH64_PAUTH_CMASK_HIGH_REGNUM(pauth_reg_base) (pauth_reg_base + 3)
+
+/* This size is only meant for Linux, not bare metal.  QEMU exposes 4 masks.  */
 #define AARCH64_PAUTH_REGS_SIZE (16)
 
 #define AARCH64_X_REGS_NUM 31