mirror of
https://github.com/bminor/binutils-gdb.git
synced 2025-12-06 07:33:08 +00:00
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:
@@ -48,6 +48,8 @@
|
|||||||
#include "arch/amd64-linux-tdesc.h"
|
#include "arch/amd64-linux-tdesc.h"
|
||||||
#include "inferior.h"
|
#include "inferior.h"
|
||||||
#include "x86-tdep.h"
|
#include "x86-tdep.h"
|
||||||
|
#include "dwarf2/frame.h"
|
||||||
|
#include "frame-unwind.h"
|
||||||
|
|
||||||
/* The syscall's XML filename for i386. */
|
/* The syscall's XML filename for i386. */
|
||||||
#define XML_SYSCALL_FILENAME_AMD64 "syscalls/amd64-linux.xml"
|
#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 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
|
static void
|
||||||
amd64_linux_init_abi_common (struct gdbarch_info info, struct gdbarch *gdbarch,
|
amd64_linux_init_abi_common (struct gdbarch_info info, struct gdbarch *gdbarch,
|
||||||
int num_disp_step_buffers)
|
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
|
set_gdbarch_remove_non_address_bits_watchpoint
|
||||||
(gdbarch, amd64_linux_remove_non_address_bits_watchpoint);
|
(gdbarch, amd64_linux_remove_non_address_bits_watchpoint);
|
||||||
|
dwarf2_frame_set_init_reg (gdbarch, amd64_init_reg);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
#include "gdbsupport/unordered_map.h"
|
#include "gdbsupport/unordered_map.h"
|
||||||
|
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
/* This enum represents the values that the user can choose when
|
/* This enum represents the values that the user can choose when
|
||||||
informing the Linux kernel about which memory mappings will be
|
informing the Linux kernel about which memory mappings will be
|
||||||
@@ -96,6 +97,10 @@ struct smaps_vmflags
|
|||||||
/* Memory map has memory tagging enabled. */
|
/* Memory map has memory tagging enabled. */
|
||||||
|
|
||||||
unsigned int memory_tagging : 1;
|
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
|
/* 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;
|
v->shared_mapping = 1;
|
||||||
else if (strcmp (s, "mt") == 0)
|
else if (strcmp (s, "mt") == 0)
|
||||||
v->memory_tagging = 1;
|
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);
|
" 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
|
/* To be called from the various GDB_OSABI_LINUX handlers for the
|
||||||
various GNU/Linux architectures and machine types.
|
various GNU/Linux architectures and machine types.
|
||||||
|
|
||||||
|
|||||||
@@ -113,4 +113,11 @@ extern CORE_ADDR linux_get_hwcap2 (const std::optional<gdb::byte_vector> &auxv,
|
|||||||
|
|
||||||
extern CORE_ADDR linux_get_hwcap2 ();
|
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 */
|
#endif /* GDB_LINUX_TDEP_H */
|
||||||
|
|||||||
88
gdb/testsuite/gdb.arch/amd64-shadow-stack-cmds.exp
Normal file
88
gdb/testsuite/gdb.arch/amd64-shadow-stack-cmds.exp
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,8 +15,21 @@
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
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
|
int
|
||||||
main ()
|
main ()
|
||||||
{
|
{
|
||||||
|
call1 (); /* break main. */
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user