RFC: "Raw" pointer improvements
- Dominant language
- Rust
- Stars
- 2.7k
- Forks
- 144
- Avg merge
- 12h 21m
- Merged PRs (30d)
- 146
Description
LiteBox has evolved such that "raw" pointers essentially mean "guest"/"user" pointers--they represent untrusted addresses under user control, outside of the Rust memory model. Although one can imagine uses of LiteBox where the pointers _can_ be trusted, no one writing generic shim code can _assume_ this.
As such, there are several challenges with the current design:
## Safety
Many of the pointer trait methods are `unsafe`, but the safety contract is unclear and/or unenforceable. These pointers are inherently untrusted--they come from code on the other side of the security barrier (the Linux process, the OPTEE TA).
And there is no mechanism that the shim could use to validate a address ahead of time, before constructing a pointer, since in most cases this would be a TOCTOU bug--the user process can change the page mappings between pointer validation and pointer use. The best we can do is validate that the address is outside of the kernel address range... but we might as well defer that to access time if we're going to perform the check at all (on some architectures, it can be combined with the access).
We should fix this so that all trait methods on raw pointers are safe, relying on the platform to ensure this. For userland platforms, where this is not practical and there is no security boundary, this will have to be best-effort.
### COW
The pointer trait methods provide ways to get borrowed `Cow` references to the user data but, as discussed above, this is always unsafe and undefined behavior--guest memory lives outside the Rust memory model. We should eliminate this altogether.
### Safe transmutability
Currently, the pointer trait access methods do not require that the underlying data can be safely transmuted to/from bytes--it's up to the caller to not read an `Arc` or `&'static Platform` from guest memory. We can leverage the `zerocopy` crate to provide useful bounds while still allowing safe access of user types.
### Atomic accesses
Some code (currently, just the futex manager) needs a guarantee that a given memory access is performed atomically. There is no mechanism for this today, and so code that has this requirement currently transmutes the address to an `&AtomicU32` and hopes for the best.
## Ergonomics
The current pointers are difficult to use correctly for some use cases.
* There are multiple overlapping methods to read/write data, but no simple `read`/`write` methods for the common case where you just want to access the pointed-to data.
* It is difficult to cast a pointer between types or to construct a pointer that is offset from another one. You can cast through `usize`, but the traits warn you not to do this.
* Accessing a slice of guest memory requires extra care since you are responsible for all bounds checks. There's no mechanism to create a "raw slice".
* Because LiteBox code uses the platform pointer objects directly, it can be awkward to describe the pointer type in generic code. See uses of `IoReadVec` for an example.
# Proposal
To address all these, we should split the guest pointer infrastructure into two parts:
1. A simple `RawPointer` trait implemented by the platform to provide low-level memory access in a safe way.
2. Rich `XxxPointer` types provided by `litebox` that the shims use to actually access memory.
The low-level trait would look something like:
```rust
trait RawPointer {
// Safe if `T` is safe to read. Guaranteed to be atomic if `T` is 1,2,4,8 byes and aligned.
unsafe fn read(self) -> Result;
// Safe if `T` is safe to write.
unsafe fn write_ref(self, v: &T) -> Result<(), Fault>;
// Always safe.
fn read_bytes(self, dst: &mut [u8]) -> Result<(), Fault>;
fn write_bytes(self, src: &[u8]) -> Result<(), Fault>;
fn from_addr(_: usize) -> Self;
fn addr(self) -> usize;
fn offset(self, offset: isize) -> Self;
}
```
And the high-level objects would look like:
```rust
struct MutPointer(Platform::RawPointer, PhantomData<*mut T>);
impl MutPointer {
fn read(self) -> Result where T: zerocopy::FromBytes { ... }
fn write(self, v: T) -> Result<(), Fault> where T: zerocopy::IntoBytes {...}
fn from_addr(_: usize) -> Self {...}
fn offset(self, count: isize) -> Self {...}
}
impl MutPointer<[T]> {
fn from_addr_and_len(addr: usize, len: usize) -> Self {...}
fn copy_from_slice(self, data: &[T]) -> Result<(), Fault> where T: zerocopy::FromBytes {...}
fn copy_into_slice(self, buf: &mut [T]) -> Result<(), Fault> where T: zerocopy::IntoBytes {...}
fn len(self) -> usize;
fn get(self, range: I) -> Option {...}
}
impl MutPointer {
fn cast_const(self) -> ConstPointer {...}
fn cast(self) -> MutPointer {...}
fn addr(self) -> usize {...}
}
```
This approach clearly delineates the responsibilities between the platform and LiteBox:
* The platform is responsible for ensuring the correctness, safety, and performance of the raw operations, e.g., ensuring that the memory accesses are really to guest memory, that aligned accesses are atomic, etc.
* LiteBox is responsible for ensuring Rust-level safety (adding in the `zerocopy` bounds, for example) and ergonomics (generic for avoiding pointer confusion, support for slice types, etc.).
Contributor guide
Research direction
No implementation files or tests are named. Start by tracing the existing platform pointer traits and the uses of IoReadVec, then map how guest-memory access is divided between platform code and LiteBox. Done would require an agreed design and implementation covering the proposed RawPointer and high-level pointer responsibilities, including zerocopy bounds and atomic access guarantees.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- operating-systems, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100