-
Notifications
You must be signed in to change notification settings - Fork 13
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
Improve compartment example security #55
Conversation
I'm more or less putting this here now to see if anyone has any ideas if we can break this model. I'll probably take a bit of time to tinker with that. I also have not yet implemented exact function bounds (as per #54), so that is one know vector of attack. But as is, I believe a compartment can now not access data regarding other compartments (which is available within |
* Currently, these involve the PCC/DDC of the `switch_compartment` function | ||
* (PCC to execute, DDC to access memory where we store information pertaining | ||
* to compartments; essentially, `switch_compartment` performs a limited | ||
* privelege escalation, but only over prepared data within `main`). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sp "privilege"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 3955d6a.
* | | | ||
* | HEAP | | ||
* | | | ||
* | saved caps | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is included in "saved caps"? I guess these are the "some capabilities of interest" mentioned earlier, but I must admit that I don't know precisely what that set might be!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is detailed in main.c:4
:
Currently, these involve the PCC/DDC of the
switch_compartment
function (PCC to execute, DDC to access memory where we store information pertaining to compartments
Further, there is a saved_caps
variable a bit down. I can add in an underscore to make the connection more obvious?
size_t id = 0; | ||
|
||
// Duplicate certain capabilities within the heap of each compartments; | ||
// currently we save DDC and PCC of `switch_compartment` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If another compartment can arbitrarily read/write to the compartment switcher I think, in general, that it can get the compartment switcher to do whatever it wants. But, if switch_compartment
is a sentry (which I think it is), we don't need its PCC to be stored outside? The DDC is an interesting question: I'm not sure how to handle that. Can a sentry also change the DDC?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I don't quite follow. I think there are three issues at play here:
- The minimal amount of data a malicious compartment requires in order to be able to "take control" of
switch_compartment
(sentence 1). - Requiring the PCC of
switch_compartment
to be "stored outside" (I don't understand where "outside" is). (sentence 2) - Issue of DDC integrity (sentence 3-4)
For 1, we need to balance the minimal information switch_compartment
needs in order to actually perform the switch. We obviously need something to be exposed in order for the function to be executable (at minimum, we can say the address of switch_compartment
, but of course that is useless without knowing what compartment we want to switch to, or where that compartment is in memory). If the minimal information needed to perform the switch and the minimal data needed to take control of switch_compartment
are not disjoint, then this is a design problem, and this design is infeasible.
For 2, I will clarify that the switch_compartment
available to the compartments is a sentry, which can only be branched to. I need further clarification regarding the "outside" thing. For 3, what does "a sentry [..] change the DDC" mean? A sentry is a sealed capability with it's value being the address of a function. By that definition, a sentry is changing the DDC, as the DDC swapping happens inside switch_compartment
(although the DDC is expected in a register, provided as an argument by the calling compartment).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
although the DDC is expected in a register, provided as an argument by the calling compartment
I think that's a concern. What stops the caller from providing an inappropriate DDC? However, in this case I think it can only cause a crash in the switcher (by providing an inadequate DDC). That might be acceptable in the (currently implicit) threat model.
If it's necessary to bind a pair (to prevent this), it's possible to do that using ldpblr
, as we've discussed before.
mov x12, sp | ||
add x11, x10, #COMP_OFFSET_STK_ADDR | ||
ldr x11, [x19, x11] | ||
// add x13, x10, #COMP_OFFSET_STK_LEN |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general we prefer to delete commented code (unless there's a really good reason to keep it around).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoops. Removed in 3955d6a.
After further talks, I believe we conclude that leaking the DDC of the compartment switcher is unacceptable. Therefore this design is inappropriate in its current incarnation, unless we can somehow communicate compartment information to the switcher function without needing to expose this DDC capability. |
I think that's a fair summary of our current thoughts. |
Would you say sealing the DDC (to be unsealed somehow in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'll need another pass for my own understanding. I'm not sure if I yet understand what guarantees each compartment (including the switcher) can reasonably make, for example.
@@ -0,0 +1,194 @@ | |||
/* In this example, we tighten security even further. For each compartment we |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which example does it follow on for? To what does "even further" refer? (I can guess, but it may not be obvious later.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clarified in 9fe7172.
|
||
/* Abstract representation of a compartment. Within 80 bytes we represent: | ||
* - an id (8B) | ||
* - address to the start of the compartment's stack (8B) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the "start" the lowest address, or the highest (initial) address?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clarified in db7617b.
void init_comps() | ||
{ | ||
void *__capability switch_cap = (void *__capability) switch_compartment; | ||
switch_cap = cheri_bounds_set(switch_cap, 80 * 4); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is that a guess at the size of the switch_compartment
function? You can make this automatic (and tight) with some assembly directives, though I'm not sure how to make it a constant. For now, perhaps comment or assign it to a named variable, so the intention is clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is related to #54 . I have not implemented this yet, but at least lifting it for the future would be good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A better (not necessarily good) way of getting function sizes has been added in db7617b.
asm("mov c19, %w0\n\t" | ||
"mov x0, #0\n\t" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to make sure (basically) that the C compiler doesn't mess with those registers between setting them, and reading them in switch_compartment
. The only safe way to do that is to make sure that there is no C code in between, which mean writing this whole function in assembly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the only thing following is the call to switch_compartment
, would it be safe to simply include that function call as part of the inline assembly, maybe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can, but you'll have to mark all potentially-clobbered registers as such, and there are a lot of them. (All the caller-saved registers, basically.) It can be done if that's what you need to do, but it's often easier to put the whole lot into a .s
(or .S
) file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. So the naive solution then is to move executive_switch
in our shared.S
file, or is that still insufficient?
asm("mov c19, %w0\n\t" | ||
"mov x0, #0\n\t" | ||
"msr CID_EL0, c0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More generally — and this requires some thought — does splitting the actual switching not represent a security concern? For example, what happens if I call switch_compartment
directly? Nothing seems to prevent me from doing so, and then I can provide whatever value in c19
that I want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that is a concern. The problem with the current design is that in order to access the compartment information (which we want to keep secure from compartments) in memory, we need to set the DDC over that region of memory. I can't think of a way to self-determine this in switch_compartment
, without some expected cooperation from the compartment we are transitioning from.
As for why you can't call switch_compartment
directly (assuming from within a compartment), it is that the PCC for the compartment you are in does not cover switch_compartment
, and the only way to transition is via the provided sentry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the time of writing, I would have argued that holding the sentry is sufficient to make the call, since a malicious caller could provide a mis-matched DDC (or ID from which it is derived, or similar). However, after our recent call I now think you've resolved that using ldpblr
anyway. Is that the case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is correct - the compartment would have the PCC (albeit sealed), so it could call switch_compartment
and give it a malformed value in c19
(now c29
), which could lead to something unwanted. Now, the compartment only is aware of a sealed capability pointing to some address (which happens to contain the unsealed switcher DDC), but that capability can only be used via lpb
-like instructions (including ldpblr
), with set parameters (i.e., the DDC/PCC combo set in memory, outside of the compartment's DDC bounds).
/* This function stores the compartment data in memory, so that it can be | ||
* accessed by the compartments as needed. | ||
*/ | ||
void executive_switch(struct comp c) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't see that this actually uses executive mode. What have I missed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This "executive" is not related to the notion of executive/restricted in Morello, just bad naming we could say. I will edit it to make sure there is no such confusion.
|
||
asm("mov c19, %w0\n\t" | ||
"mov x0, #0\n\t" | ||
"msr CID_EL0, c0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suspect that passing this in a general-purpose register might be better. It can be set in switch_compartment
, rather than set here and read there.
The real answer here rather depends on what the ID actually means.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, honestly, c19
is more a remnant of previous iterations. It can be anything, or if we can change the design of switch_compartment
to not require this argument be passed somehow...
If by ID you mean the contents of CID_EL0
, it represents the ID of the compartment we are jumping into (which would, of course, require some knowledge between compartments of what compartment corresponds to what ID). This currently corresponds to the index of the comps
array.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant CID_EL0
. One of the first things that switch_compartment
does is to read the value out of CID_EL0
(into c10
), to use it to index the comps
array, but then why not just leave it in a general-purpose register (c0
) and let switch_compartment
choose if or how to put it in CID_EL0
? That seems like a better abstraction to me, and a clearer interface because it looks more like a conventional function call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, there is no practical reason, beyond showing one way of using this "special-purpose-but-not-really" CID register. I can either add a comment to emphasise this, or we can use c0
or any other register, if we prefer to have a "cleaner" example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's good to set it, but we should do so in switch_compartment
. Here, we should just pass the value in c0
(or similar). I don't think using CID_EL0
as a parameter-passing register would be considered a good idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed the use of CID_EL0
as discussed in 47e39b7.
I have pushed a new commit making use of |
This looks very promising! Can we create some "negative examples" (for want of a better term) in [At the moment we lack a nice testing infrastructure on CHERI for this stuff. @drpdr used shell scripts elsewhere in the repo to automatically check which programs should succeed and which should fail -- that's still the best approach I can think of.] |
Another rather meaty update, fixing memory allocation within the compartment (again, for no current functional use), and a few other tidbits. I think the only thing left is to remove the usage of |
// When creating a compartment, store a local copy of the capability which | ||
// will allow us to call `switch_compartment` in the heap of the compartment. | ||
memcpy(new_comp.heap_addr, &switcher_call, sizeof(void *__capability)); | ||
void * heap_top = (void *) (_start_addr + total_comp_size - sizeof(void* __capability)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor formatting: s/void * heap_top
/void *heap_top
/ and s/void* __capability
/void *__capability
/.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These should be fixed once I apply clang-format
, which I have not quite yet. Should be shortly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Applied clang-format
in 5d529ed.
I haven't fully groked this yet, but my first impression is positive! Are you going to push a negative example or two to this PR? I think that might help me understand the details a bit better. |
I think that we should finalize this PR first, considering both its age and the amount of discussion around it, before adding negative examples in a follow-up PR. However, I have nothing against including those here, if we think that's reasonable. |
Let's add 1 simple negative example to this PR, just as a proof of concept, then I agree: we can merge, and the next PR can add lots more negative examples. |
Added a negative example, where we try to dereference a local capability for I believe at this point, the only unresolved issue [1] is moving [1] #55 (comment) |
Please squash as you see fit. |
I've also pushed the final change of moving Furthermore, I've tried running both this example, as well as some of the previous ones on our daily CHERI build (with @drpdr's help), but I encountered some issues. As of now, none of the examples I have added (after |
What issues? We should fix them before merging unless there's a very good reason not to. |
No issues with the infrastructure itself, but difficulties in trying to manually make use of it. The final step was to get the correct SSH key to connect to, which I'll bother Pino about at the some point. Just that, overall, trying to manually use our nightly builds could be more streamlined. The one "issue" I would say it has is that it runs a very limited set of examples, which need to be manually provided [1]. This includes not ensuring that a newly provided example does indeed run on the build (but it does check that it compiles, as I was told). I would put this with overall infrastructure improvements we could be making to the repo, as we should also improve the overall organization and build system. [1] https://github.com/capablevms/cheri-examples/blob/master/tests/run_tests.sh#L61 |
I agree that we can improve things: but we should hook this new example into |
I know how to add that code, but I have yet to be able to validate it locally. I can either try to get it working (at worst, I'll just update my dev environment), or we can just push it blindly, which I'm not keen on, but we can fix stuff once we see it's broken I guess. |
The good thing about pushing it is that bors won't merge it if it doesn't work! So go ahead. |
Good point. I've added this, and |
Excellent! Since you're not sure if they work or not I recommend you do a |
bors try |
Our standard practise in this situation is to keep pushing fix commits and doing |
bors try |
tryBuild succeeded: |
@0152la Time to squash? |
f41d67f
to
4c499b9
Compare
@ltratt I assumed, after our conversation, that I would integrate the failing example in this PR. I can do that when I add the rest of the examples in the CI, in a future PR then, or I can just update this PR tomorrow if we want the failing example in as well. |
Ah, sorry, I thought the failing example was in here! Let's link it in -- we might as well :) |
After trying lots of things, I decided, at the moment, to just have some duplication and move some stuff in their independent directories. I will do some tries now to see if I got everything right. |
bors try |
tryBuild failed: |
bors try |
tryBuild failed: |
bors try |
tryBuild succeeded: |
@ltratt @jacobbramley I think we're set. This final set of commits reorganized 3 examples: the main correct example of this PR, the negative example of this PR, and the one in #53. It also adds all these three examples to our CI. |
Unless @jacobbramley has any comments, please squash. |
In this example, we tighten security even further. We store the DDC/PCC of `switch_compartment` within its memory region. Then, for each compartment, we create a copy of a capability pointing to this DDC/PCC pair, sealed with `lpb`. This allows compartments to call `switch_compartment` without having access to any information pertaining to it. We provide an additional example (`shared_try_deref.S`), where a malicious compartment attempts to dereference its local copy, to gain access to `switch_compartment`.
d852147
to
7f1e1ea
Compare
Squashed; can of course address further comments, if there are any. |
I think it's best to get this in, and we can improve it in further PRs if necessary. bors r+ |
Build succeeded: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks roughly correct but I'll look more closely later. I think all the earlier threads are resolved.
// 40 is arbitary; meant to be the size of the executable function within | ||
// compartment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you addressed that with precise pointer arithmetic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I forgot to update this comment. I'm unsure if we can amend this PR as is, after merging, or should this be done as part of another PR? @ltratt
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is merged, so it can be done in another PR. I'm fine with small PRs that fix such issues!
In this example, we tighten security even further. For each compartment we
create, we store some capabilities of interest. Essentially, we setup
certain entry points, and make them available to each compartment.
Currently, these involve the PCC/DDC of the
switch_compartment
function(PCC to execute, DDC to access memory where we store information pertaining
to compartments; essentially,
switch_compartment
performs a limitedprivelege escalation, but only over prepared data within
main
).Additionally, we also seal the PCC of
switch_compartment
.