Use physical pointer abstraction in LVBS#817
Conversation
29c61b5 to
e42c3a0
Compare
8b9b1f7 to
3a833f2
Compare
c6fd071 to
24e1d87
Compare
24e1d87 to
9b0f0f0
Compare
wdcui
left a comment
There was a problem hiding this comment.
LGTM. I left some comments. Thanks!
| where | ||
| T: FromBytes, | ||
| { | ||
| if values.is_empty() { |
There was a problem hiding this comment.
Yes. This aligns with the litebox's RawConstPointer.
| const MAX_SPAN_PAGES: usize = 16; | ||
| // If `buf` would force the start page into the span twice, vmap rejects the | ||
| // duplicate. Hand off to the two-write slow path before truncating `buf`. | ||
| if buf.len() < size && write_offset % PAGE_SIZE + buf.len() > size { |
There was a problem hiding this comment.
I don't understand this line. Why do we modulo PAGE_SIZE then add buf.len() to compare with size?
There was a problem hiding this comment.
Yes, this is confusing. Basically, it figures out whether wraparound requires duplicated mapping, which our page mapper doesn't allow.
PAGE_SIZE = 4096
size = 4096
write_offset = 4090
buf.len() = 16
Evaluate the condition:
buf.len() < size
16 < 4096
true
Something like below would be more readable. Let me revise it.
let page_count = size / PAGE_SIZE;
let page_offset = write_offset % PAGE_SIZE;
let span_pages = (page_offset + buf.len()).div_ceil(PAGE_SIZE);
if span_pages > page_count {
return write_slow(rb_pa, size, write_offset, buf);
}
lschuermann
left a comment
There was a problem hiding this comment.
I took a pass over the entirety of physical_pointers.rs, not just the diff of this PR. In summary, I think this is close to a neat and (in the future) sound abstraction with rich pointer semantics over dynamically mapped physical memory.
Happy to discuss my comments below, I'll go over the remaining changes soon. I don't think any of these is critical to be addressed in this PR, instead I'm happy to implement the changes in a follow-up PR!
| #[repr(C)] | ||
| pub struct PhysMutPtr<T: Clone, const ALIGN: usize> { | ||
| pub struct PhysMutPtr<T: Clone, const ALIGN: usize, V: GlobalVmapManager<ALIGN>> { | ||
| pages: alloc::boxed::Box<[PhysPageAddr<ALIGN>]>, |
There was a problem hiding this comment.
pages is currently effectively an array of usizes with alignment constraints. For strict provenance support, this should be wrapping raw pointers instead. Not critical here.
| if core::mem::size_of::<T>() == 0 { | ||
| return Err(PhysPointerError::UnsupportedZeroSizedType); | ||
| } |
There was a problem hiding this comment.
I'm wondering why ZSTs are not supported here. Given that this type can already be used to read n items out of foreign memory, shouldn't the implementation for ZSTs be "easy" in the sense that it would just allow reading / writing an infinite amount of ZSTs to/from any valid foreign address?
Perhaps there's an edge case that I'm missing here. But it would be nice if we could get rid of this runtime error. :)
There was a problem hiding this comment.
I think this function can be made safe (along with all the other readers and writers in the file).
While is true that this can read corrupted memory---memcpy_fallible can race with another writer, but is implemented in assembly and therefore outside the Rust Abstract Machine and allowed to race---because T: FromBytes, reading any corrupted memory is actually safe. There's a concern about semantic correctness, but not about Rust soundness.
What would be unsound, however, is if these read and write operations are operating on memory that overlaps with any valid Rust allocation. That's discharged through the V::manager().validate_owned() call (provided it gets turned into an unsafe trait and its # Safety doc gets extended).
| pub unsafe fn read_slice_at_offset( | ||
| &mut self, | ||
| count: usize, | ||
| values: &mut [T], |
There was a problem hiding this comment.
We'll need to carefully reason about why this is safe. It could potentially leave T in a partially copied state. Given that it's T: FromBytes this is probably fine.
| impl<T: Clone, const ALIGN: usize> PhysMutPtr<T, ALIGN> { | ||
| impl<T: Clone, const ALIGN: usize, V> PhysMutPtr<T, ALIGN, V> | ||
| where | ||
| V: GlobalVmapManager<ALIGN>, |
There was a problem hiding this comment.
The soundness of this implementation relies on a correct implementation of the -> #824GlobalVmapManager, specifically that it must return the appropriate, active VmapManager for the current system. That means that GlobalVmapManager should be an unsafe trait.
Similarly, the soundness of this implementation relies on the fact that -> #824V::validate_unowned is correct. Therefore, VmapManager should also be an unsafe trait.
The documentation of validate_unowned should additionally carry a safety invariant that states that any address range for which this function every returns Ok(()) must never be used to store any Rust allocation (i.e., never be part of the Rust heap) in the future. Otherwise, the soundness implications of https://github.com/microsoft/litebox/pull/817/changes#r3417155237 apply.
| if offset >= ALIGN { | ||
| return Err(PhysPointerError::InvalidBaseOffset(offset, ALIGN)); | ||
| } |
There was a problem hiding this comment.
Should the function ensure that (ALIGN + offset) % std::mem::align_of::<T>() == 0? I.e., that Ts in this memory region are located at well-aligned pointers?
The answer is probably: no. This function uses an unaligned memcpy underneath; it never creates an (ephemeral) Rust reference to any foreign memory. Also, the above restriction would mean that Ts with large alignment constraints would be unsupported.
The constructor should then get a comment documenting that reads/writes of T don't have to be aligned, and why that's safe.
| if size < core::mem::size_of::<T>() { | ||
| return Err(PhysPointerError::InsufficientPhysicalPages( | ||
| size, | ||
| core::mem::size_of::<T>(), | ||
| )); | ||
| } |
There was a problem hiding this comment.
Why this early error path? Given that the address range can contain a slice of types T, wouldn't it be easier to also allow constructing PhysMutPtr if zero instances of the type fit? Is there a good reason to ensure that this can never capture "zero elements of type T"?
| /// Create a new `PhysMutPtr` from the given contiguous physical address and length. | ||
| /// | ||
| /// This is a shortcut for | ||
| /// `PhysMutPtr::new([align_down(pa), align_down(pa) + ALIGN, ..., align_up(pa + bytes) - ALIGN], pa % ALIGN)`. | ||
| /// This function assumes that `pa`, ..., `pa+bytes` are both physically and virtually contiguous. If not, | ||
| /// later accesses through `PhysMutPtr` may read/write data in a wrong order. | ||
| pub fn with_contiguous_pages(pa: usize, bytes: usize) -> Result<Self, PhysPointerError> { |
There was a problem hiding this comment.
Should clarify that "this function assumes" means "this function will return an error if not".
There was a problem hiding this comment.
This is the thorny part.
Something that would be a big soundness risk here: if another core unmaps this memory in parallel, but the memory is then re-mapped carrying some Rust allocation, then this write races with Rust accesses.
We can shift the burden of proof onto GlobalVmapManager and (by extension) VmapManager. By having them both be unsafe traits, and extending the safety documentation to promise that if it ever returns Ok(()) for validate_unowned this memory will never used as backing for any Rust allocation in the future, this asm!-backed memcpy can be unconditionally safe.
That is, of course, ignoring semantic correctness constraints that this function should document. But those don't affect the soundness of this function.
There was a problem hiding this comment.
I guess, another way to ensure that this does not end up touching Rust memory would be adding a guard to the TLB shootdown handler, which detects whether the local core is currently in the memcpy_fallible assembly and checks if the memcpy would be affected by the new page table state. This could be used to guarantee that all concurrent memcpys operating on a now-revoked page terminate, before the virtual address is ever reused.
That seems hard though. It might be easier and cleaner to simply guarantee that such VAs are never re-used for Rust allocations.
lschuermann
left a comment
There was a problem hiding this comment.
Reviewed the rest of the PR. I realize some of my feedback might be obsolete due to #824, so I'll check that out next. Should I create a PR to address the left-over comments targeting this same sanghle/lvbs/vmap_copy branch?
| static RINGBUFFER_ONCE: Once<Mutex<RingBuffer>> = Once::new(); | ||
| pub(crate) fn set_ringbuffer(pa: PhysAddr, size: usize) -> &'static Mutex<RingBuffer> { | ||
| RINGBUFFER_ONCE.call_once(|| { | ||
| let ring_buffer = RingBuffer::new(pa, size); | ||
| Mutex::new(ring_buffer) | ||
| }) | ||
| } | ||
|
|
||
| pub(crate) fn ringbuffer() -> Option<&'static Mutex<RingBuffer>> { | ||
| RINGBUFFER_ONCE.get() | ||
| } | ||
|
|
||
| impl fmt::Write for RingBuffer { | ||
| fn write_str(&mut self, s: &str) -> fmt::Result { | ||
| self.write(s.as_bytes()); | ||
| Ok(()) | ||
| } | ||
| } |
There was a problem hiding this comment.
I was initially confused by this, because it seemed weird that we'd just have one instance of this generic RingBuffer. Maybe it could help calling this DEBUG_RINGBUFFER?
There was a problem hiding this comment.
Is there a documentation of the expected ring buffer semantics somewhere? I can see it's used for debug printing, but don't know what the consumer side looks like. Depending on the consumer, some of the semantics seem interesting (like, it resets the write_offset to zero after a large write).
| fn validate_unowned(&self, _pages: &PhysPageAddrArray<ALIGN>) -> Result<(), PhysPointerError> { | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
This default implementation should probably be removed. For (unsafe) traits that are load-bearing for soundness, there should either not be default implementations, or they should be conservative and unconditionally sound. If an implementer forgets this validate_unowned method, it will cause any address to be marked as unowned and eligible to be written to through the PhysPtr abstraction.
| ) -> Result<(), PhysPointerError> { | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
Same here, default impl should be removed or made conservative.
| /// This function apply the given `HekiPatch` patch data to VTL0 text. | ||
| /// It assumes the caller has confirmed the validity of `HekiPatch` by invoking the `is_valid()` member function. | ||
| fn apply_vtl0_text_patch(heki_patch: HekiPatch) -> Result<(), VsmError> { |
There was a problem hiding this comment.
Worth making this into a type state, perhaps?
I don't think heki_patch.is_valid() == false will allow this function to invoke undefined behavior. But if it does, it would need to either perform this check internally or be marked unsafe. A type state would avoid this.
| #[derive(Clone, Copy, Debug, Default)] | ||
| pub struct Vmap; |
There was a problem hiding this comment.
| #[derive(Clone, Copy, Debug, Default)] | |
| pub struct Vmap; | |
| #[derive(Clone, Copy, Debug, Default)] | |
| pub enum Vmap {}; |
Might as well make this a variant-less enum? That's the idiomatic way to have the compiler guarantee this type is erased during compilation and not present in the final binary (or any artifacts associated with it, like vtables).
| #[derive(Clone, Copy, Debug, Default)] | ||
| pub struct Vmap; |
There was a problem hiding this comment.
Same here, maybe worth making variant-less enum instead.
This PR refactors the physical pointer API. --------- Co-authored-by: Sangho Lee <sanghle@microsoft.com>
fefb293 to
0e0e307
Compare
|
🤖 SemverChecks 🤖 Click for details |
This PR lets LVBS use physical pointer abstraction to access VTL0 memory. This is equivalent to the OP-TEE shim's normal-world memory access such that it is not only safer than the legacy memory copy functions but also supporting non-contiguous physical pages. This PR and physical address range lock (next PR) will enable memory-safe VTL0 memory access (i.e., drop
unsafefrom read/write methods).