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

soundness improvements around hypervisor-shared memory #451

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

Freax13
Copy link
Contributor

@Freax13 Freax13 commented Aug 30, 2024

This PR improves the soundness of code around hypervisor-shared memory.

The first patch, 174274d, is blocked on google/zerocopy#1601. Let me know if you want me to drop that patch if we don't want to wait on a new zerocopy release. I used the following patch to override zerocopy for testing:

diff --git a/Cargo.toml b/Cargo.toml
index b7bdb46..d87444d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -49,6 +49,9 @@ zerocopy = { version = "0.7.32", features = ["alloc", "derive"] }
 # other repos
 packit = { git = "https://github.com/coconut-svsm/packit", version = "0.1.1" }
 
+[patch.crates-io]
+zerocopy = { git = "https://github.com/Freax13/zerocopy.git", rev = "68e1cc8" }
+
 [workspace.lints.rust]
 future_incompatible = { level = "deny", priority = 127 }
 nonstandard_style = { level = "deny", priority = 126 }

@Freax13 Freax13 force-pushed the more-zerocopy branch 3 times, most recently from bfe8671 to a084a9a Compare August 30, 2024 07:24
@joergroedel
Copy link
Member

This PR improves the soundness of code around hypervisor-shared memory.

The first patch, 174274d, is blocked on google/zerocopy#1601. Let me know if you want me to drop that patch if we don't want to wait on a new zerocopy release.

Please move that patch to a separate draft-PR, which you can then "undraft" once all blockers are solved.

In general I like these changes, especially the SharedBox implementation. That will simplify a lot of things.

Once updated this needs testing by @msft-jlange and possibly also a review by @cclaudio .

@Freax13
Copy link
Contributor Author

Freax13 commented Sep 12, 2024

Please move that patch to a separate draft-PR, which you can then "undraft" once all blockers are solved.

Done.

@joergroedel joergroedel added the wait-for-review PR needs for approval by reviewers label Sep 16, 2024
This makes it possible to implement get_aad_slice without any unsafe
code.

Signed-off-by: Tom Dohrmann <erbse.13@gmx.de>
Given that the hypervisor has write access to that memory, we need to
treat the memory as interiorly mutable.

Signed-off-by: Tom Dohrmann <erbse.13@gmx.de>
@Freax13
Copy link
Contributor Author

Freax13 commented Sep 18, 2024

Just rebased onto main. I resolved the TODOs by switching to the functions in crate::cpu::mem.

this_cpu()
.shutdown()
.expect("Failed to shut down percpu data (including GHCB)");
unsafe fn shutdown_percpu() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this function unsafe? At a minimum, there should be a Safety comment here. But there is also nothing about the function declaration that suggests that it can only be called from unsafe code. My understanding of the convention we have been using is that a function should only be declared unsafe if it is not possible to call it from safe code due to its parameters or return value (this was the position advocated by @00xc as I always understood it).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why is this function unsafe? At a minimum, there should be a Safety comment here.

I added a safety section.

But there is also nothing about the function declaration that suggests that it can only be called from unsafe code. My understanding of the convention we have been using is that a function should only be declared unsafe if it is not possible to call it from safe code due to its parameters or return value (this was the position advocated by @00xc as I always understood it).

No, that's not how unsafe is used in Rust: A function must be marked as unsafe if (and usually only if) safe code using the function can cause undefined behavior (see the Rustonomicon). Whether or not a function must be marked as unsafe has very little to do with its signature and much more to do with what it actually does (you could make any unsafe function with any signature safe by replacing it's body with loop {}). In this case, the caller must ensure that the PerCpu instance isn't used after it's been dropped because doing so is UB. This isn't something that the compiler can enforce, so we have to mark the function as unsafe and shift the responsibility of upholding this to the user. This isn't a convention that's specified by us in this project, those are the rules for writing unsafe code in Rust.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with your perspective on the use of unsafe. However, I once submitted a PR to this project that declared a function unsafe for similar reasons, and received feedback from @00xc that the function should not be declared this way, for the reasons I included in my earlier comment. I'm happy with your approach now that you've added a safety comment, and will leave it to @00xc to comment on whether this use of unsafe is appropriate.

/// This function has all the safety requirements of `core::ptr::read` except
/// that data races are explicitly permitted.
#[inline(always)]
pub unsafe fn is_clear(src: usize, size: usize) -> bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

This appears to be used only by test code. Should it therefore be within the test module so it is not accidentally referenced by non-test code?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This appears to be used only by test code.

This function is also referenced by SharedBox::is_clear which itself is used by send_extended_guest_request.

Copy link
Contributor

Choose a reason for hiding this comment

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

If this function is required for production code, then why is it written in assembly? In the interest of minimizing the use of unsafe code, it seems like we don't want to write anything in assembly unless it has to be done that way, and at a glance it appears that this function could be implemented in Rust instead of asm. If you have a strong justification for the use of assembly, then can you include that justification in the safety comments?

If performance is your primary concern, then there are many ways this loop could be improved (switching to SCASQ at a minimum, and better yet would be to use XMM). But if performance is not a primary concern, then avoiding assembly altogether would be best.

/// # Safety
///
/// Converting the memory at `vaddr` must be safe within Rust's memory model.
/// Notably any objects at `vaddr` must tolerate unsychronized writes of any
Copy link
Contributor

Choose a reason for hiding this comment

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

Typo: unsychronized => unsynchronized.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed, thanks!

.expect("Could not re-encrypt page");

// Unregister GHCB PA
register_ghcb_gpa_msr(PhysAddr::null()).expect("Could not unregister GHCB");
Copy link
Contributor

Choose a reason for hiding this comment

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

This is not guaranteed to succeed, because the VMM is not required to accept NULL as a valid GHCB page.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's true, but this PR didn't introduce this code, it merely moved it around. We were already panicking before this PR. We should fix this in a later PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

That is a reasonable position.

@@ -160,7 +162,28 @@ impl GhcbPage {

impl Drop for GhcbPage {
fn drop(&mut self) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any scenario for which dropping a GHCB is not associated with a fatal termination of the SVSM? The process of restoring a GHCB page to the private state is fragile, and I believe it would be best to avoid any attempt to do so unless we were aware of a valid use case for this. If we cannot come up with one, then it would be simplest for GhcbPage::drop() simply to panic, since we shouldn't get here anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there any scenario for which dropping a GHCB is not associated with a fatal termination of the SVSM? The process of restoring a GHCB page to the private state is fragile, and I believe it would be best to avoid any attempt to do so unless we were aware of a valid use case for this. If we cannot come up with one, then it would be simplest for GhcbPage::drop() simply to panic, since we shouldn't get here anyway.

Yes, calling the shutdown_percpu function will cause the GhcbPage to be dropped. We do this to release the GHCB used by stage2 shortly before entering the svsm kernel. That said, this code wasn't introduced by this PR, this PR just moved that code around.

If we didn't need this, I'd would totally be on your side, we shouldn't write fragile code when we shouldn't need it in practice.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'll take another look at this flow after your PR merges to see if we can be more robust here, but I accept your point that your PR isn't really changing the logic that exists today.

pub fn clear(&self) {
// Clear valid bitmap
self.valid_bitmap.set([0, 0]);
self.valid_bitmap[0].store(0, Ordering::SeqCst);
Copy link
Contributor

@msft-jlange msft-jlange Sep 19, 2024

Choose a reason for hiding this comment

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

The GHCB page is not manipulated across processors, so there is no reason for expensive memory barriers when modifying its contents. The only race conditions we might expect are between the SVSM environment and the host running on the same processor; this possibility for interruption means that atomic operations are necessary, but Ordering::Relaxed will be sufficient for all such operations. That is true everywhere throughout this file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, sounds reasonable.

@@ -78,3 +91,139 @@ pub fn make_page_private(vaddr: VirtAddr) -> Result<(), SvsmError> {

Ok(())
}

/// SharedBox is a safe wrapper around memory pages shared with the host.
pub struct SharedBox<T> {
Copy link
Contributor

Choose a reason for hiding this comment

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

This structure appears to have nothing to do with GHCB - the GhcbPage structure doesn't even make use of it - and thus it should be in its own source file and not stuck in the GHCB sources. In addition, the notion of SharedBox will be used on other confidential platforms (like TDX) and definitely should not be in the SEV sources directory.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm a little confused, SharedBox isn't declared in a file that has anything to do with the GHCB or SEV specific sources. It's declared in the same file as two other functions that a related to page visibility and that file is already platform independent.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, somehow I observed this code to be in kernel/src/sev/ghcb.rs. The current location is great.

}
/// Allocates a new HV doorbell page and registers it on the hypervisor
/// using the given GHCB.
pub fn allocate_hv_doorbell_page(ghcb: &GHCB) -> Result<&'static HVDoorbell, SvsmError> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why does this function not use SharedBox? It seems that SharedBox is designed to do exactly the sort of assignment and visibility management expected here, and unlike the GHCB, there is no chicken-and-egg problem because SharedBox only requires the existence of a GHCB, not of a doorbell page.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea! I changed the function to use SharedBox.

Cell doesn't allow concurrent accesses. This is a problem because we
share the memory with the host and the host could write to the memory
while we're reading it. Use atomic accesses instead. Atomic accesses
can tolerate concurrent writes.

Signed-off-by: Tom Dohrmann <erbse.13@gmx.de>
This method makes it possible to check if memory is clear without
having to worry about data races.

Signed-off-by: Tom Dohrmann <erbse.13@gmx.de>
SharedBox is a safe wrapper around memory pages shared with the host.

Signed-off-by: Tom Dohrmann <erbse.13@gmx.de>
HVDoorbellPage was only used in one place and leak was immediately
called on it. Given that we don't ever need to free up a doorbell page
let's just implement this in a single function returning a static
reference.

Signed-off-by: Tom Dohrmann <erbse.13@gmx.de>
This is better for a couple of reasons:
1. drop_in_place destroys the object rather than mutating it to release
   resources. The downside with simply mutating but not destroying is
   that the object still has to be in a valid state and this limits the
   shutdown code (for example it can't release the memory associated
   with a PageBox)
2. After the object has been dropped, it can't be accessed anymore.
   This means that the shutdown code doesn't have to worry about later
   accesses like the previous code had to.
3. All resources are freed, not just the GHCB.

This also fixes a soundness issue where if the shutdown were to be
called twice on the same GHCB that would result in a double-pvalidate
bug.

Signed-off-by: Tom Dohrmann <erbse.13@gmx.de>
This impl is unused. It is also unsound because we can never have
unique ownership over the GHCB as long as it is shared with the host.

Signed-off-by: Tom Dohrmann <erbse.13@gmx.de>
Now that the shutdown code is only called from the Drop impl we might
as well move it in there. This also makes it impossible to call
shutdown more than once (or to call shutdown and the Drop the
GhcbPage).

Signed-off-by: Tom Dohrmann <erbse.13@gmx.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
wait-for-review PR needs for approval by reviewers
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants