Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

c18n: [Draft] Save caller's stack pointer in trusted frame #2060

Closed
wants to merge 6 commits into from
Closed

Conversation

dpgao
Copy link
Contributor

@dpgao dpgao commented Mar 19, 2024

The first commit (5094160) actually makes the change. The rest of the commits are rebased from #2054.

Previously, the caller's stack pointer is only saved at the bottom of
the caller's stack during domain transition. This means that the act of
unwinding a trusted frame relies on external state, namely the value at
the bottom of the caller's stack.

We now also save the caller's stack pointer in the trusted frame so that
unwinding can be stateless, i.e., inspecting the content of the trusted
frame alone is sufficient for restoring the stack pointer of the caller.
Load a compartment policy file via the environment variable
LD_C18N_COMPARTMENT_POLICY.

Currently, the policy allows one to
(a) define logical compartments comprising multiple libraries,
(b) specify which symbols are trusted by a particular compartment, and
(c) specify which libraries are allowed to link against any particular
symbol.
Previously, after a process calls vfork, the child process then calls
execve. This call causes a domain transition that changes the top of the
stack as recorded by RTLD. This change is visible in the parent process,
which then wakes up and sees that the top of the stack has been altered,
which is something that should never happen.

Trust execve so that calling it does not cause a domain transition.
Let trampoline hooks consume the trampoline header and trusted stack
directly.

Remove the dependency on mempcpy.
uint8_t ret_args : 2;
/*
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is libunwind using this? GDB doesn't use the cookie, it matches on the instruction sequence just like for signal frames. That is, I don't understand how cookie is different from pc?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

libunwind uses this to detect if the current return address is a trampoline and we need to unwind from the trusted frame.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pc is what the trampoline should return to. cookie is where the callee should return to. It is an address within the trampoline.

Checking the Executive bit of the return address to determine whether we are at a compartment boundary is unreliable because we might be returning into RTLD directly. Checking that the return address matches the cookie gives full assurance, and it also works under the benchmark ABI where everything is Executive.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess what I don't understand is that since the cookie is already on the trusted stack, doesn't libunwind have to know before it can even see the cookie that it needs to read from the trusted stack (ECSP) instead of the normal stack (RCSP)? That is, when libunwind is unwinding from the uppermost frame of a compartment back out to the compartment switch, it gets a CLR (and thus PCC) value that points into a trampoline. How does it then decide that for this new frame that it is a compartment frame that is a compartment boundary thus requiring it to go looking in ECSP instead or RCSP for its saved frame? If this were DWARF based, you would look up the FDE/CFI based on the unwound PC value and it would tell you the register for CFA and the offsets of saved registers relative to the CFA. For custom unwinders in GDB including signal frames, GDB does a pattern match against known instructions (e.g. the first 3 instructions CLR points to) to locate a custom unwinder. Does CFP in this case store a scalar address back into the trusted stack (yes, that appears to be true) that libunwind notices is out of the bounds of RCSP and thus assume it might be on the trusted stack? If you aren't doing the bounds check, how do you avoid potential false positives if the stack garbage at the offset of cookie in a "normal" frame happens to match PC? If you are doing the bounds check, isn't that alone sufficient to know you are on an alternate stack? (And you could even check if it is in bounds of the trusted stack which libunwind presumably has access to.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't libunwind have to know before it can even see the cookie that it needs to read from the trusted stack (ECSP) instead of the normal stack (RCSP)?

libunwind always just blindly checks whether the CLR matches the cookie of the top trusted frame to decide if it's at a compartment boundary. (See https://github.com/CTSRD-CHERI/llvm-project/pull/731/files#diff-6dd9111398b6f61443d5eec566d751eb857a8ee262fe9c5f38c192881826300dR363 which is being used at https://github.com/CTSRD-CHERI/llvm-project/pull/731/files#diff-6dd9111398b6f61443d5eec566d751eb857a8ee262fe9c5f38c192881826300dR602)

The cookie feature was implemented before I started to properly set the CFP value, and your suggestion of checking if CFP is in bounds does seem like a viable idea. @dstolfa What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds like it could work, I'll prototype it to check.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I'm after is if we no longer need the cookie value, then we can remove it from the trusted_frame and with a bit of reorganizing have room for the full ecsp in place of the current ptraddr_t for csp.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In particular, it is pretty unusual to restore "part" of a register on a stack frame in an unwinder. DWARF CFI annotations don't (afaik) have a way to describe this kind of case currently for example as stack frames in general save/restore entire registers. It's true that we can't use DWARF to describe this frame today for multiple reasons (dynamically allocated trampolines; no DWARF register number for RCSP and ECSP, just CSP; etc.), but I do think we should aim to create a frame that is generally compatible with how other stack frames generally work as we are less likely to run afoul of assumptions in other unwinders.

@dpgao dpgao changed the title c18n: Save caller's stack pointer in trusted frame c18n: [Draft] Save caller's stack pointer in trusted frame Mar 19, 2024
@dpgao
Copy link
Contributor Author

dpgao commented Mar 20, 2024

Superseded by #2061.

@dpgao dpgao closed this Mar 20, 2024
@dpgao dpgao deleted the c18n-nsp branch March 20, 2024 15:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants