gdb: Handle shadow stack pointer register unwinding for amd64 linux.

Unwind the $pl3_ssp register.
We now have an updated value for the shadow stack pointer when
moving up or down the frame level.  Note that $pl3_ssp can
become unavailable when moving to a frame before the shadow
stack enablement.  In the example below, shadow stack is enabled
in the function 'call1'.  Thus, when moving to a frame level above
the function, $pl3_ssp will become unavaiable.
Following the restriction of the linux kernel, implement the unwinding
for amd64 linux only.

Before this patch:
~~~
Breakpoint 1, call2 (j=3) at sample.c:44
44	  return 42;
(gdb) p $pl3_ssp
$1 = (void *) 0x7ffff79ffff8
(gdb) up
55	  call2 (3);
(gdb) p $pl3_ssp
$2 = (void *) 0x7ffff79ffff8
(gdb) up
68	  call1 (43);
(gdb) p $pl3_ssp
$3 = (void *) 0x7ffff79ffff8
~~~

After this patch:
~~~
Breakpoint 1, call2 (j=3) at sample.c:44
44	  return 42;
(gdb) p $pl3_ssp
$1 = (void *) 0x7ffff79ffff8
(gdb) up
55	  call2 (3);
(gdb) p $pl3_ssp
$2 = (void *) 0x7ffff7a00000
(gdb) up
68	  call1 (43i);
(gdb) p $pl3_ssp
$3 = <unavailable>
~~~

As we now have an updated value for each selected frame, the
return command is now enabled for shadow stack enabled programs, too.

We therefore add a test for the return command and shadow stack support,
and for an updated shadow stack pointer after a frame level change.

Reviewed-By: Thiago Jung Bauermann <thiago.bauermann@linaro.org>
Approved-By: Luis Machado <luis.machado@arm.com>
Approved-By: Andrew Burgess <aburgess@redhat.com>
This commit is contained in:
Christina Schimpe
2024-04-08 11:57:34 -04:00
parent e07c03e47a
commit a4011720d4
5 changed files with 240 additions and 0 deletions

View File

@@ -48,6 +48,8 @@
#include "arch/amd64-linux-tdesc.h"
#include "inferior.h"
#include "x86-tdep.h"
#include "dwarf2/frame.h"
#include "frame-unwind.h"
/* The syscall's XML filename for i386. */
#define XML_SYSCALL_FILENAME_AMD64 "syscalls/amd64-linux.xml"
@@ -1921,6 +1923,88 @@ amd64_linux_get_tls_dtv_addr (struct gdbarch *gdbarch, ptid_t ptid,
return dtv_addr;
}
/* Return the number of bytes required to update the shadow stack pointer
by one element. For x32 the shadow stack elements are still 64-bit
aligned. Thus, gdbarch_addr_bit cannot be used to compute the new
stack pointer. */
static inline int
amd64_linux_shadow_stack_element_size_aligned (gdbarch *gdbarch)
{
const bfd_arch_info *binfo = gdbarch_bfd_arch_info (gdbarch);
return (binfo->bits_per_word / binfo->bits_per_byte);
}
/* Implement shadow stack pointer unwinding. For each new shadow stack
pointer check if its address is still in the shadow stack memory range.
If it's outside the range set the returned value to unavailable,
otherwise return a value containing the new shadow stack pointer. */
static value *
amd64_linux_dwarf2_prev_ssp (const frame_info_ptr &this_frame,
void **this_cache, int regnum)
{
value *v = frame_unwind_got_register (this_frame, regnum, regnum);
gdb_assert (v != nullptr);
gdbarch *gdbarch = get_frame_arch (this_frame);
if (v->entirely_available () && !v->optimized_out ())
{
int size = register_size (gdbarch, regnum);
bfd_endian byte_order = gdbarch_byte_order (gdbarch);
CORE_ADDR ssp = extract_unsigned_integer (v->contents_all ().data (),
size, byte_order);
/* Using /proc/PID/smaps we can only check if the current shadow
stack pointer SSP points to shadow stack memory. Only if this is
the case a valid previous shadow stack pointer can be
calculated. */
std::pair<CORE_ADDR, CORE_ADDR> range;
if (linux_address_in_shadow_stack_mem_range (ssp, &range))
{
/* The shadow stack grows downwards. To compute the previous
shadow stack pointer, we need to increment SSP. */
CORE_ADDR new_ssp
= ssp + amd64_linux_shadow_stack_element_size_aligned (gdbarch);
/* There can be scenarios where we have a shadow stack pointer
but the shadow stack is empty, as no call instruction has
been executed yet. If NEW_SSP points to the end of or before
(<=) the current shadow stack memory range we consider
NEW_SSP as valid (but empty). */
if (new_ssp <= range.second)
return frame_unwind_got_address (this_frame, regnum, new_ssp);
}
}
/* Return a value which is marked as unavailable in case we could not
calculate a valid previous shadow stack pointer. */
value *retval
= value::allocate_register (get_next_frame_sentinel_okay (this_frame),
regnum, register_type (gdbarch, regnum));
retval->mark_bytes_unavailable (0, retval->type ()->length ());
return retval;
}
/* Implement the "init_reg" dwarf2_frame_ops method. */
static void
amd64_init_reg (gdbarch *gdbarch, int regnum, dwarf2_frame_state_reg *reg,
const frame_info_ptr &this_frame)
{
if (regnum == gdbarch_pc_regnum (gdbarch))
reg->how = DWARF2_FRAME_REG_RA;
else if (regnum == gdbarch_sp_regnum (gdbarch))
reg->how = DWARF2_FRAME_REG_CFA;
else if (regnum == AMD64_PL3_SSP_REGNUM)
{
reg->how = DWARF2_FRAME_REG_FN;
reg->loc.fn = amd64_linux_dwarf2_prev_ssp;
}
}
static void
amd64_linux_init_abi_common (struct gdbarch_info info, struct gdbarch *gdbarch,
int num_disp_step_buffers)
@@ -1978,6 +2062,7 @@ amd64_linux_init_abi_common (struct gdbarch_info info, struct gdbarch *gdbarch,
set_gdbarch_remove_non_address_bits_watchpoint
(gdbarch, amd64_linux_remove_non_address_bits_watchpoint);
dwarf2_frame_set_init_reg (gdbarch, amd64_init_reg);
}
static void

View File

@@ -47,6 +47,7 @@
#include "gdbsupport/unordered_map.h"
#include <ctype.h>
#include <algorithm>
/* This enum represents the values that the user can choose when
informing the Linux kernel about which memory mappings will be
@@ -96,6 +97,10 @@ struct smaps_vmflags
/* Memory map has memory tagging enabled. */
unsigned int memory_tagging : 1;
/* Memory map used for shadow stack. */
unsigned int shadow_stack_memory : 1;
};
/* Data structure that holds the information contained in the
@@ -537,6 +542,8 @@ decode_vmflags (char *p, struct smaps_vmflags *v)
v->shared_mapping = 1;
else if (strcmp (s, "mt") == 0)
v->memory_tagging = 1;
else if (strcmp (s, "ss") == 0)
v->shadow_stack_memory = 1;
}
}
@@ -3032,6 +3039,46 @@ show_dump_excluded_mappings (struct ui_file *file, int from_tty,
" flag is %s.\n"), value);
}
/* See linux-tdep.h. */
bool
linux_address_in_shadow_stack_mem_range
(CORE_ADDR addr, std::pair<CORE_ADDR, CORE_ADDR> *range)
{
if (!target_has_execution () || current_inferior ()->fake_pid_p)
return false;
const int pid = current_inferior ()->pid;
std::string smaps_file = string_printf ("/proc/%d/smaps", pid);
gdb::unique_xmalloc_ptr<char> data
= target_fileio_read_stralloc (nullptr, smaps_file.c_str ());
if (data == nullptr)
return false;
const std::vector<smaps_data> smaps
= parse_smaps_data (data.get (), std::move (smaps_file));
auto find_addr_mem_range = [&addr] (const smaps_data &map)
{
bool addr_in_mem_range
= (addr >= map.start_address && addr < map.end_address);
return (addr_in_mem_range && map.vmflags.shadow_stack_memory);
};
auto it = std::find_if (smaps.begin (), smaps.end (), find_addr_mem_range);
if (it != smaps.end ())
{
range->first = it->start_address;
range->second = it->end_address;
return true;
}
return false;
}
/* To be called from the various GDB_OSABI_LINUX handlers for the
various GNU/Linux architectures and machine types.

View File

@@ -113,4 +113,11 @@ extern CORE_ADDR linux_get_hwcap2 (const std::optional<gdb::byte_vector> &auxv,
extern CORE_ADDR linux_get_hwcap2 ();
/* Returns true if ADDR belongs to a shadow stack memory range. If this
is the case, assign the shadow stack memory range to RANGE
[start_address, end_address). */
extern bool linux_address_in_shadow_stack_mem_range
(CORE_ADDR addr, std::pair<CORE_ADDR, CORE_ADDR> *range);
#endif /* GDB_LINUX_TDEP_H */

View File

@@ -0,0 +1,88 @@
# Copyright 2024-2025 Free Software Foundation, Inc.
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Test shadow stack enabling for frame level update and the return command.
require allow_ssp_tests
standard_testfile amd64-shadow-stack.c
save_vars { ::env(GLIBC_TUNABLES) } {
append_environment GLIBC_TUNABLES "glibc.cpu.hwcaps" "SHSTK"
if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile} \
{debug additional_flags="-fcf-protection=return"}] } {
return -1
}
clean_restart ${binfile}
if { ![runto_main] } {
return -1
}
set call1_line [ gdb_get_line_number "break call1" ]
set call2_line [ gdb_get_line_number "break call2" ]
# Extract shadow stack pointer inside main, call1 and call2 function.
gdb_breakpoint $call1_line
gdb_breakpoint $call2_line
set ssp_main [get_valueof /x "\$pl3_ssp" 0 "get value of ssp in main"]
gdb_continue_to_breakpoint "break call1" ".*break call1.*"
set ssp_call1 [get_valueof /x "\$pl3_ssp" 0 "get value of ssp in call1"]
gdb_continue_to_breakpoint "break call2" ".*break call2.*"
set ssp_call2 [get_valueof /x "\$pl3_ssp" 0 "get value of ssp in call2"]
with_test_prefix "test frame level update" {
gdb_test "up" "call1.*" "move to frame 1"
gdb_test "print /x \$pl3_ssp" "= $ssp_call1" "check pl3_ssp of frame 1"
gdb_test "up" "main.*" "move to frame 2"
gdb_test "print /x \$pl3_ssp" "= $ssp_main" "check pl3_ssp of frame 2"
gdb_test "frame 0" "call2.*" "move to frame 0"
gdb_test "print /x \$pl3_ssp" "= $ssp_call2" "check pl3_ssp of frame 0"
}
with_test_prefix "test return from current frame" {
gdb_test "return (int) 1" "#0.*call1.*" \
"Test shadow stack return from current frame" \
"Make.*return now\\? \\(y or n\\) " "y"
# Potential CET violations often only occur after resuming normal execution.
# Therefore, it is important to test normal program continuation after
# testing the return command.
gdb_continue_to_end
}
clean_restart ${binfile}
if { ![runto_main] } {
return -1
}
with_test_prefix "test return from past frame" {
gdb_breakpoint $call2_line
gdb_continue_to_breakpoint "break call2" ".*break call2.*"
gdb_test "frame 1" ".*in call1.*"
gdb_test "return (int) 1" "#0.*main.*" \
"Test shadow stack return from past frame" \
"Make.*return now\\? \\(y or n\\) " "y"
# Potential CET violations often only occur after resuming normal execution.
# Therefore, it is important to test normal program continuation after
# testing the return command.
gdb_continue_to_end
}
}

View File

@@ -15,8 +15,21 @@
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
static int
call2 ()
{
return 42; /* break call2. */
}
static int
call1 ()
{
return call2 (); /* break call1. */
}
int
main ()
{
call1 (); /* break main. */
return 0;
}