RE: [PATCH v4 07/11] gdb: Handle shadow stack pointer register unwinding for amd64 linux.

Schimpe, Christina
2025-06-23T14:55:57Z

> -----Original Message-----
> From: Thiago Jung Bauermann <thiago.bauermann@linaro.org>
> Sent: Friday, June 20, 2025 3:43 AM
> To: Luis Machado <luis.machado@arm.com>
> Cc: Schimpe, Christina <christina.schimpe@intel.com>; gdb-
> patches@sourceware.org; eliz@gnu.org
> Subject: Re: [PATCH v4 07/11] gdb: Handle shadow stack pointer register
> unwinding for amd64 linux.
>
> Luis Machado <luis.machado@arm.com> writes:
>
> > On 6/17/25 13:11, Christina Schimpe wrote:
> >> + 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);
> >> +
> >> + /* If NEW_SSP points to the end of or before (<=) the current
> >> + shadow stack memory range we consider NEW_SSP as valid (but
> >> + empty). */
> >
> > I couldn't quite understand the difference between the empty case and
> > the unavailable case. But maybe I just don't fully understand the feature.
> >
> > Would it be possible to make the comment a bit more clear?
>
> I understood it to mean that if new_ssp == range.second, then it points to
> the top of the stack and there aren't any elements.
>
> Whereas if new_ssp points outside of the shadow stack area, then it's
> garbage and we failed to unwind it, hence the unavailable value.
>
> Christina, please correct me if I'm wrong.
>
> But now looking at this again, I think there's an off-by-one error:
> range.second is the first address outside of the memory range, so the
> comparison needs to be strictly less than. And the shadow stack will be
> empty if new_ssp == range.second - 1 (there's no need to check for that,
> though).
>
> >> + 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;
> >> +}

Hi Thiago,

> Whereas if new_ssp points outside of the shadow stack area, then it's
> garbage and we failed to unwind it, hence the unavailable value.

Yes, this is how it should be. 😊

But I think the current implementation should be correct.
I wrote a small testprogram, which enables shadow stack manually using ARCH_PRCTL in enable_ssp:

~~~

int call ()
{
return 0;
}

int enable_ssp ()
{
int ret;
if (ARCH_PRCTL(ARCH_SHSTK_ENABLE, ARCH_SHSTK_SHSTK)) {
printf("[FAIL]\tEnabling shadow stack failed\n");
return 1;
}

ret = call (); // stop in enable_ssp

if (ARCH_PRCTL(ARCH_SHSTK_DISABLE, ARCH_SHSTK_SHSTK)) {
ret = 1;
printf("[FAIL]\tDisabling shadow stack failed\n");
}
return ret;
}

int main ()
{
return enable_ssp ();
}

~~~

If we stop in enable_ssp, ("ret = call ()"), we have the scenario that shadow stack is enabled,
but no call instruction has been executed yet.
So we'll have an empty shadow stack, but SSP is valid and pointing to the end address
(==range.second) of the shadow stack address space:

~~~
(gdb) r
Starting program: /tmp/main
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, enable_ssp () at main.c:52
52 ret = call (); // stop in enable_ssp
(gdb) p $pl3_ssp
$1 = (void *) 0x7ffff7c00000
(gdb) info proc mappings
process 94192
Mapped address spaces:

Start Addr End Addr Size Offset Perms File
[...]
0x0000555555558000 0x0000555555559000 0x1000 0x3000 rw-p /tmp/main
0x00007ffff7400000 0x00007ffff7c00000 0x800000 0x0 rw-p
[...]

~~~

Checking the unwinding we see that one frame above SSP becomes unavailable:
~~~
(gdb) up
#1 0x00005555555551d4 in main () at main.c:63
63 return enable_ssp ();
(gdb) p $pl3_ssp
$3 = <unavailable>
(gdb) down
#0 enable_ssp () at main.c:52
52 ret = call (); // stop in enable_ssp
(gdb) p $pl3_ssp
$4 = (void *) 0x7ffff7c00000
~~~

Or unwinding from call:
~~~
Breakpoint 1, call () at main.c:41
41 return 0;
(gdb) p $pl3_ssp
$1 = (void *) 0x7ffff7bffff8
(gdb) up
#1 0x000055555555518a in enable_ssp () at main.c:52
52 ret = call (); // stop in enable_ssp
(gdb) p $pl3_ssp
$2 = (void *) 0x7ffff7c00000
(gdb) up
#2 0x00005555555551d4 in main () at main.c:63
63 return enable_ssp ();
(gdb) p $pl3_ssp
$3 = <unavailable>
(gdb) down
#1 0x000055555555518a in enable_ssp () at main.c:52
52 ret = call (); // stop in enable_ssp
(gdb) p $pl3_ssp
$4 = (void *) 0x7ffff7c00000
(gdb) down
#0 call () at main.c:41
41 return 0;
(gdb) p $pl3_ssp
$5 = (void *) 0x7ffff7bffff8
~~~

So the check

"if (new_ssp <= range.second)"

seems correct to me.

Christina

Intel Deutschland GmbH
Registered Address: Am Campeon 10, 85579 Neubiberg, Germany
Tel: +49 89 99 8853-0, www.intel.de
Managing Directors: Sean Fennelly, Jeffrey Schneiderman, Tiffany Doon Silva
Chairperson of the Supervisory Board: Nicole Lau
Registered Office: Munich
Commercial Register: Amtsgericht Muenchen HRB 186928