On 8/4/26 4:59 PM, Edgecombe, Rick P wrote:
On Tue, 2026-08-04 at 15:43 -0500, Bill Roberts wrote:
IIRC CRIU needed to unlock shadow stack during the save. Because it's tricks
for
saving the process involved messing with the stack. So it needs to be able
to
unlock but *only* via ptrace. Apps cannot be allowed to unlock themselves.
Because, well, obviously..

    As MUSL and
others get patches, it would be much simpler to have one interface to do
this in, and right now
unlock is unused (even in the test suite :-p).
I think I raised this on the other arch's APIs originally. x86 went first,
and then later arm/riscv wanted a unified approach, but didn't solve this
ptrace unlock part. So we didn't join. AFAICT the situation hasn't changed.
So I'm worried it's too soon to unify. Or we might need to do multiple steps
of unifications.
At this point, the only remaining semantic difference is support for
unlocking a locked shadow stack during ptrace/CRIU restore. Arm64 and RISC-V
don't currently expose a generic unlock operation because they use
architecture-specific ptrace mechanisms for state restoration. If those
architectures eventually converge on a common unlock model for CRIU, I think
it would make sense to unify that interface as well. Until then, I don't think
the lack of a generic unlock should prevent unifying the parts of the ABI that
already have equivalent semantics. Especially considering how it unifies the
loader/libc side of the house.
The locking needs to be done after the linked DSOs are checked for shadow stack
compatibility.
Correction, "Locking *may* be done after...", this is a policy based on per-arch tunables.

    Also looking forward to understanding the security module problem
statement  better.
Right now all the controls are opt in, and requires support in glibc, via
per arch tunable flags for both enabling and locking.
x86 glibc was originally supposed to have two modes for shadow stack. A normal
mode and a permissive mode. The normal mode would first check all the ldd DSOs
for shadow stack compatibility. If they all have shadow stack, it enables shadow
stack and does the lock on the main thread, which then gets inherited by all the
child tasks. If dlopen() is later made with a DSO that does not shadow stack,
the dlopen() call is failed.

In permissive mode, it did the enablement checks like the normal mode, except it
did not lock shadow stack. Then if dlopen() call was made with a non supporting
DSO, it would disable shadow stack on the calling thread. This didn't really
work, because the other threads in the app still had shadow stack enabled. Why
didn't it just disable it shadow stack for the full app? Lots of races with apps
doing shadow stack specific stuff while the support gets disabled out from under
them.

To fix permissive mode, there was a proposal (I don't know if it ever made it
into glibc) where the shadow stack would get disabled on the first dlopen() to a
non-compatible shadow stack OR the first thread was spawned. This was
questionable to me. But there was a desire to minimize the compatibility hit
first so it could be enabled widely. Then tighten it up over time.

But in any case, lock is still needed for the normal mode right? You need to
decide in the loader whether you want to have shadow stack for the app. Or we
are talking about another mode, like a forced-on mode where the app will do
something else if it finds the linked DSOs don't support shadow stack. Like exit
with an error or something. But if that's the case, it's a decent amount of new
stuff to me, so please explain.

Everything you mentioned here is correct AFAIK. Besides background, I am not
seeing how this is relevant here. Some of this is more a conversation about
policy vs mechanism. Am I missing something you're trying to convey?

A lot of this conversation is about the other things tangential to the patch
which is good, I just want to make sure we don't get too off topic here.

  The glibc folks are
starting to balk at the amount of flags. Additionally, it would be nice to
have MAC controls on them, and the best way for that is via LSM into
SE Linux. Normal applications can be placed in a domain where
they can never remove protections, and things like CRIU will have these
elevated
perms, if they are ever needed, since right now they go through a
different channel.
This sounds like a new paradigm. If we have enough shadow stack support out
there, seems interesting to me. But I would like to hear more details about how
it works. I'm just speculating above.

No, it's not a new paradigm, the "permissions" around shadow stack are granted to domains, ie groups of processes, based on a collection of tunables. You need an
elevated permission domain granted by CAP_SYS_PTRACE to unlock and then a
call to disable the feature, like shadow stack.

The only difference is, that we can do this with SE Linux controls. Plus
we can also limit what can have capability ptrace to even touch that. In SE Linux policy, it could be something like this: neverallow { domain - criu } self:process disable_shadow_stack; allow criu self:process disable_shadow_stack;

The LSM actually removes the need for lock flags in essence, but yeah we
can't count
on that.
?

In selinux, after my changes, nothing on the system will have the ability to disable shadow stack via prctl or arch_prctl *except for things explicitly granted these perms*. Because of that, it functions as an implicit call to a lock. However, that doesn't mean
we don't support the locked feature via prctl/arch_prctl, we need to support
non-lsm and non-selinux systems.

So that begs the question, why hook it via LSMs and add support to SE Linux?

LSMs - Opting out of shadow stack is a security decision, so LSMs should be able to reason
about these state changes.

SE Linux - Central policy, auditablity, logs, least privilege, etc, all the good benefits that it provides.

Now that we have multiple arch's in tree doing this, with a generic interface, it makes sense, and good
hygiene, to rectify this division.

The lore is here:
https://sourceware.org/pipermail/libc-alpha/2025-December/173636.html
This seems to be about other shadow stack knobs. Stack size, etc.

For the following LSM piece, there is no hard dependency on this. I can
put the LSM hooks into arch_prctl for x86 and things would work fine, in fact
there is no getting around that or the arch_prctl interface for x86 wouldn't
get the LSM protections. I want x86 to get the LSM protections. I also want to
coalesce what we can around the around the current generic interfaces for
glibc. Work could be done there to start dropping arch tunables around this
too. I am sitting on these patches right now, because they have a soft
dependency on conflicts.

Ill spin out a v2 with the comments, but the one thing left to be
addressed, afaik, is the testing patch
do you want that? I'll drop it, but I think that's a bad approach.
Yea, I hear your point. It is good to test more, but #including c files with
pre-processor switches seems too hacky. Is there anything we could do to
make it
in one c file? Like not the templated tricks to generate two test binaries?
For
example embed the switch between the two APIs in a macros in the single
file.
Like say we have SHSTK_ENABLE(). Inside it does either ARCH_PRCTL() or
regular.
Then have a global or something that switches between the two behaviors.
Pull
out the parts in main() into something that could be run twice?

Anything is possible, but is the juice worth the squeeze here? This
approach is used
already in tree:
- bpf/progs/arena_htab_asm.c
- bpf/progs/test_cls_redirect_subprogs.c
- bpf/progs/test_sk_assign_libbpf.c
- kvm/lib/rbtree.c
- kvm/rseq_test.c
- livepatch/test_modules/test_klp_state3.c
- powerpc/vphn/test-vphn.c
- x86/test_shadow_stack_prctl.c
Uhh... that is your own changes. :)
I literally went back and forth including my change... I just included it as a,
"See I am joining a long list".
I am generally a fan of doing the simplest, most obvious thing first
that follows existing conventions
unless a really big reason can be made to not do it. I can refactor it
if it's a show stopper, but IMHO
not worth it here. But if you want it refactored, sure.
We'll we can leave it to x86 maintainers to weigh in on the style thing. I don't
like it, but won't throw a fit.

Could you nudge one of em?

But lastly, I just want to get back to your non-structural/style concerns around this patch series, which to paraphrase
and summarize poorly are:

What about UNLOCK and how does that fit in with the future and whats the risk of not being able to rectify
that gap?

To answer that, unlock can be easily rectified by mirroring the arch_prctl interface into prctl when/if needed. Considering that this split between APIs using arch_prctl and prctl was initially discussed when adding the generic prctl interface, and the arch_prctl interface was very easy to map onto prctl (in this PR), I don't see any reason why
unlock would be a problem if we need it.

In full disclosure, unlock will remain a blind spot to LSMs, and things with ptrace capability, will be able to unlock feature bits. But this is no different then the current state now (and it's idempotent with an LSM involved), and we gain the ability to see/control what domains in the system need to actually twiddle the shadow stack bits off or
make it write able.

But back to this patch series, it merely accomplishes a unified prctl interface for shadow stack locking, status getting and enabling. From there, libc's and other consumers can start to unify around generic interfaces, which will help promote things like generic tunables in the case of glibc while preserving all backwards compatible interfaces.

If I don't hear back with concerns that would block this, ill spin a V2 later this week if that works for you?


Reply via email to