mirror of
https://github.com/bminor/binutils-gdb.git
synced 2025-12-05 15:15:42 +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 "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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 */
|
||||
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user