Lack of consideration for jmp_buf ABI
jrtc27 opened this issue · comments
The current shadow stack spec suggests saving the shadow stack pointer to the jmp_buf in setjmp so that longjmp can unwind it back to where it was. However, jmp_buf is an exposed type that is part of the ABI, and so extending it with such state would be an ABI break. There would be no way to incrementally and conditionally adopt it like on other architectures. Unless there is another way to implement setjmp/longjmp without changing anything about jmp_buf I cannot sign off on the extension with my ABI hat on.
Could the jmp_buf be not adopted as done on other architectures:
https://github.com/lattera/glibc/blob/master/sysdeps/x86_64/setjmp.S#L73
That only worked on x86 in glibc because there was existing padding that could be repurposed bminor/glibc@d6cc182 bminor/glibc@f33632c
Other systems may not be so lucky
Could the same technique of using space from the 1024 entry array in __sigset_t __saved_mask be used? I think _NSIG
is still only 64 as of 6.5.
In glibc? Maybe. But not every setjmp implementation is glibc. FreeBSD’s does happen to have space, but for different reasons:
- On x86, space for sigset_t is allocated in units of long, but the struct is in units of int, so on amd64 (but not i386) it allocates double the space needed for it, giving 2 spare longs
- On riscv, there’s an absurd amount of padding due to a misunderstanding that predates my involvement
But just because glibc and FreeBSD both happen to have wasted space in their jmp_bufs today does not mean that other libcs have such luxury. For example, musl appears to have no unused space when on lp64d.
I tried to read other libc implementations.
Please see if I got any of this wrong.
- musl, seems similar to glibc in the jmp_buf (unsigned long __ss[128/sizeof(long)]);
- bionic, seems to define a 32 entry jmp_buf, and presently to use 30 entries. Maybe 2 entries are available.
If shadow stack is in use, the unmodifed longjmp
function would have caused an exception (ra mismatch with SS's ra), and it doesn't seem to make much sense to consider ABI compatibility? Because they're incompatible whether they are the same size or not?
It is possible to calculate the number of pops that need with the unwind, but this seems to lead to greater costs and easy to attack. Plus, unlikely to have that in an embedded environment where size matters.
When shadow stack is in use, the longjmp
cannot be an unmodified function. But it is also a point to note that an unmodified longjmp
would also not have instructions like sspopchk
to check ra
mismatch with return address obtained from shadow stack - but that is besides the point. As shown above for the cases of linux glibc, bsd libc, bionic libc, and musl libc we may not need to grow the size of the jump buffer.
The point is not about longjmp needing to be modified. That doesn't matter. What matters is whether jmp_buf needs to be modified, because that's the difference between being able to have an incremental migration where shadow stack objects and non-shadow stack objects are ABI-compatible (just run without a shadow stack) or not.
Agree. For the major implementations I think we may be able to get by without needing to grow the jmp_buf (phew!).
- bionic, seems to define a 32 entry jmp_buf, and presently to use 30 entries. Maybe 2 entries are available.
(since Android's riscv64 ABI isn't final yet, i doubled the size in https://android-review.googlesource.com/c/platform/bionic/+/2719577 just to give us more room to maneuver in future if necessary.)