-
-
Notifications
You must be signed in to change notification settings - Fork 779
kernel/platform/mpu: make MPU an unsafe trait, change mock impl from () to NoMPU
#4602
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
base: master
Are you sure you want to change the base?
Conversation
| /// This is an `unsafe trait`, as it is crucial to uphold Tock's isolation | ||
| /// properties, and thus safety of the Tock kernel. Users of this trait must be | ||
| /// able to rely on its implementations being correct, and implementing the | ||
| /// exact semantics as documented on its associated types and methods. | ||
| pub unsafe trait MPU { |
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'm curious what the specific soundness concern is for the MPU. At some level, every line of code must be correct for Tock to be correct. Any kernel function (and chips, etc.) could just have an unsafe block and do whatever, and the caller has to trust that any function or trait impl in a yes_unsafe crate is implemented correctly.
For STV it more or less makes sense to me because the only purpose of that type is to address Rust safety/soundness issues with static mut. But MPU is quite different.
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 some level, every line of code must be correct for Tock to be correct.
I think there is a more nuanced distinction around Rust's soundness.
The soundness of Tock's basic kernel and architecture implementations relies on the fact that applications cannot access kernel memory. This is provided by the MPU, which trusted components (i.e., the core kernel) consume an externally provided implementation of (impl MPU), and subsequently use to enforce this isolation.
However, if any arbitrary, semi-trusted component can provide an implementation of the MPU trait, then the core kernel cannot rely on any guarantees and claims made on the implementation of that trait for upholding memory isolation, vital for overall soundness. This is especially true given that the kernel accepts an implementation of this trait, passed in from another crate.
This is different from other correctness concerns. While, for instance, a ring buffer implementation could be incorrect and discard elements, this is not inherently unsound as per Rust's soundness definition. If an incorrect ring buffer implementation means, that, e.g., a userspace application will no longer be scheduled by a round-robin scheduler, that is fine as far as Rust's soundness goes.
Only if the kernel relies on the correctness of that implementation to uphold Rust's strict soundness requirements would a RingBuffer implementation have to stem from the same crate, or the trait would have to be unsafe, or a method accepting an impl RingBuffer (under the assumption that it can rely on it being a correct implementation to prevent undefined behavior) be an unsafe method.
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.
The soundness of Tock's basic kernel and architecture implementations relies on the fact that applications cannot access kernel memory.
That should be in the comment.
It sounds like it isn't really that important that the entire implementation is correct, just that it protects kernel memory.
While, for instance, a ring buffer implementation could be incorrect and discard elements, this is not inherently unsound as per Rust's soundness definition.
I was thinking something like ringbuffer needs to store a new element, so it decides to use staticref (or similar) to arbitrarily choose some location in memory it thinks would be a good place to store it, without any concern for if that memory happens to alias with some other Tock kernel object.
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 should be in the comment.
Sure, always happy to improve the docs.
I was thinking something like ringbuffer needs to store a new element, so it decides to use staticref (or similar) to arbitrarily choose some location in memory it thinks would be a good place to store it, without any concern for if that memory happens to alias with some other Tock kernel object.
There's a difference between a component being itself unsound, or a component relying on a correct implementation of another component (abstracted through a trait) for the former component's soundness.
We have the latter case here: the kernel relies on the MPU to correctly configure the hardware, for the kernel's own soundness. This case can only be solved by not accepting any arbitrary implementations passed in from other crates (especially from reverse-dependencies), or making the trait itself unsafe, or making the method accepting this trait instance unsafe.
Or, in other words, even a memory- and type-safe MPU implementation can break the kernel's soundness, hence we must use unsafe to place additional semantic restrictions of what implementations of this trait can do.
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 sounds like it isn't really that important that the entire implementation is correct, just that it protects kernel memory.
Disentangling these concerns seems hard to do in practice, but yes, to uphold soundness I believe it is sufficient if an MPU implementation would uphold a subset of its API contract, namely that applications don't have access to kernel-owned memory.
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.
Or, in other words, even a memory- and type-safe MPU implementation can break the kernel's soundness, hence we must use unsafe to place additional semantic restrictions of what implementations of this trait can do.
I think this is the most clear explanation.
I agree with the change in this PR (I'm pretty sure). However, it still seems wrong to me, because the unsafe is in the wrong place. The unsafe operation isn't implementing something wrong. The unsafe operation is using a bad implementation. This PR addresses this even with the NoMPU constructor being unsafe.
What does it mean that the same PR has an implementation which explicitly breaks the unsafe guarantees being added? One way to resolve this I think is that using an unsafe trait is the wrong mechanism. Instead, it should be some new thing akin to capabilities. Sensitive trait implementations should require a newcapability to construct (ie to call new()). Which newcapability depends on the implementation. NoMPU would require an unsafe newcapability (that requires unsafe to construct) called Dangerous or NotForProduction or something. Normal MPU implementations would require either no newcapability or some benign, easy to create newcapability.
However, I'm not sure there is any way to actually do this with what rust provides.
I'm not sure it is ok for us to include NoMPU in the kernel. I agree it is useful. But if we are going to make safety requirements to implement traits, I'm not sure we should immediately break those requirements.
| /// in applications having unrestricted access to kernel memory. As such, | ||
| /// constructing this type is an `unsafe` operation. | ||
| // By having this type contain a private field, we prevent constructing it from | ||
| // outside this crate, except through the `unsafe fn new()` constructor. |
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.
When would someone use NopMPU? Is this for hardware that lacks an MPU?
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 have previously used it for board bring-up and debugging purposes. It may potentially be used for hardware lacking an MPU, although hopefully the unsafe constructor should make clear that this effectively opts out of any safety and soundness promises that Tock makes otherwise.
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.
The empty default is a legacy from when very-early Tock had some cortex-m0 (not cortex-m0+) boards (specifically nRF51822 IIRC) in-tree, which does not have an MPU.
It remains a useful escape hatch for limited hardware, but should definitely be made harder to use.
() to NopMPU() to NoMPU
| /// This type does not meet the safety requirements outlined on the `MPU` trait, | ||
| /// hence constructing it is also an unsafe (and unsound) operation. | ||
| unsafe impl MPU for NopMPU { |
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'm concerned that we cannot do this. I agree that making the constructor unsafe is a good companion, but I don't think it is valid to blatantly ignore the safety requirement.
Pull Request Overview
This PR changes
MPUto be an unsafe trait. It is vital for Tock's isolation guarantees, and consumers of this trait must be able to rely on its implementations adhering to the trait's API contract.It also changes the mock MPU implementation from the unit type
()to a newtypeNoMPUthat itself is unsafe to construct, as doing so (and then using this type as an implementation ofMPU) is unsound.Testing Strategy
N/A
TODO or Help Wanted
N/A
Documentation Updated
/docs, or no updates are required.Formatting
make prepush.