Re: [PATCH 18/35] mm: Add guard pages around a shadow stack.

From: Edgecombe, Rick P
Date: Fri Feb 11 2022 - 19:10:36 EST


On Fri, 2022-02-11 at 09:54 -0800, Andy Lutomirski wrote:
> > Like just using VM_SHADOW_STACK | VM_GROWSDOWN to get a regular
> > stack
> > sized gap? I think it could work. It also simplifies the mm-
> > >stack_vm
> > accounting.
>
> Seems not crazy. Do we want automatically growing shadow stacks? I
> don't really like the historical unix behavior where the main thread
> has
> a sort-of-infinite stack and every other thread has a fixed stack.

Ah! I was scratching my head why glibc did that - it's historical. Yea,
probably it's not needed and adding strange behavior.

>
> >
> > It would no longer get a gap at the end though. I don't think it's
> > needed.
> >
>
> I may have missed something about the oddball way the mm code works,
> but
> it seems if you have a gap at one end of every shadow stack, you
> automatically have a gap at the other end.

Right, we only need one, and this patch added a gap on both ends.

Per Andy's comment about the "oddball way" the mm code does the gaps -
The previous version of this (PROT_SHADOW_STACK) had an issue where if
you started with writable memory, then mprotect() with
PROT_SHADOW_STACK, the internal rb tree would get confused over the
sudden appearance of a gap. This new version follows closer to how
MAP_STACK avoids the problem I saw. But the way these guard gaps work
seem to barely avoid problems when you do things like split the vma by
mprotect()ing the middle of one. I wasn't sure if it's worth a
refactor. I guess the solution is quite old and there hasn't been
problems. I'm not even sure what the change would be, but it does feel
like adding to something fragile. Maybe shadow stack should just place
a guard page manually and not add any special shadow stack logic to the
mm code...

Other than that I'm inclined to remove the end gap and justify this
patch better in the commit log. Something like this (borrowing some
from Dave's comments):

The architecture of shadow stack constrains the ability of
userspace to move the shadow stack pointer (ssp) in order to
prevent corrupting or switching to other shadow stacks. The
RSTORSSP can move the spp to different shadow stacks, but it
requires a specially placed token in order to switch shadow
stacks. However, the architecture does not prevent
incrementing or decrementing shadow stack pointer to wander
onto an adjacent shadow stack. To prevent this in software,
enforce guard pages at the beginning of shadow stack vmas, such
that t
here will always be a gap between adjacent shadow stacks.

Make the gap big enough so that no userspace ssp changing
operations (besides RSTORSSP), can move the ssp from one stack
to the next. The ssp can increment or decrement by CALL, RET
and INCSSP. CALL and RET can move the ssp by a maximum of 8
bytes, at which point the shadow stack would be accessed.

The INCSSP instruction can also increment the shadow stack
pointer. It is the shadow stack analog of an instruction like:

addq $0x80, %rsp

However, there is one important difference between an ADD on
%rsp and INCSSP. In addition to modifying SSP, INCSSP also
reads from the memory of the first and last elements that were
"popped". It can be thought of as acting like this:

READ_ONCE(ssp); // read+discard top element on stack
ssp += nr_to_pop * 8; // move the shadow stack
READ_ONCE(ssp-8); // read+discard last popped stack element

The maximum distance INCSSP can move the ssp is 2040 bytes,
before it would read the memory. There for a single page gap
will be enough to prevent any operation from shifting the ssp
to an adjacent gap, before the shadow stack would be read and
cause a fault.

This could be accomplished by using VM_GROWSDOWN, but this has
two downsides.
1. VM_GROWSDOWN will have a 1MB gap which is on the large
side for 32 bit address spaces
2. The behavior would allow shadow stack's to grow, which
is unneeded and adds a strange difference to how most
regular stacks work.