Draft: explore LA57-aware virtual address validity - #599
Conversation
Add sealed fixed LA48, fixed LA57, and runtime virtual-address validity policies, with const construction for fixed-width addresses and runtime validation against CR4.LA57. Propagate address validity through the directly affected descriptor, interrupt, page, range, TLB, and register APIs while keeping existing four-level page-table traversal semantics explicit. Preserve the crate's Rust 1.59 configurations and architectural structure layouts, and add coverage for canonicality, arithmetic, conversions, and generic API behavior.
1. The
|
2. Types and functions that depend on
|
3. Prototype status and validationThe prototype implements all three validity models and propagates them widely enough to expose the affected API surface. It has been checked across the relevant feature, target, documentation, lint, and Rust 1.59 configurations. A branch of my own OS program builds against the current prototype and it works as expected. I would prefer to settle these questions before expanding the implementation to five-level page-table traversal. I am happy to revise the prototype in whichever direction reaches consensus. |
Freax13
left a comment
There was a problem hiding this comment.
Thank your for your contribution!
Sorry for the late review. This is a massive PR and haven't had the time to do a full review yet. Here are some first comments.
If have a couple of concerns with the generic parameter approach:
- Not everyone will use LA57, but with the current design everyone will have to explicitly mention the validity type in a lot of places. This adds quite a bit of boilerplate even though the vast majority of users never want anything other than LA48.
- To my understanding one of the advantages of this approach is that users can use all of the validity types within the same binary and are not locked into any validity type (48 vs 57 vs RT). This is in contrast to the cargo feature based approach where the validity type/address space size is set at compile time. My concern with this is that I still don't understand what use cases require multiple validity types within the same binary. Are users not expected to use the same validity type everywhere? If there are use-cases expected to use several validity types, do we consider those important enough to potentially justify worse ergonomics for everyone else who doesn't need this?
For my own curiosity: Do you have a public project making use of this code and what does a migration from the old API to the new one look like?
| pub unsafe fn flush_pcid(command: InvPcidCommand) { | ||
| unsafe { flush_pcid_inner(command) } | ||
| } | ||
|
|
||
| #[inline] | ||
| unsafe fn flush_pcid_inner<V: VirtAddrValidity>(command: InvPcidCommand<V>) { |
There was a problem hiding this comment.
Shouldn't flush_pcid have a generic parameter for the validity?
What's the purpose of flush_pcid_inner?
| }; | ||
|
|
||
| /// A Mapper implementation that relies on a PhysAddr to VirtAddr conversion function. | ||
| /// A Mapper implementation that relies on a PhysAddr to VirtAddr48 conversion function. |
There was a problem hiding this comment.
Can we make this (and the other mapper/translate types/traits) work with VirtAddr57 and VirtAddrRT?
| @@ -159,9 +159,10 @@ pub trait Mapper<S: PageSize> { | |||
| /// # Mapper, Page, PhysFrame, FrameAllocator, | |||
| /// # Size4KiB, OffsetPageTable, page_table::PageTableFlags | |||
| /// # }; | |||
| /// # use x86_64::FixedValidity; | |||
There was a problem hiding this comment.
| /// # use x86_64::FixedValidity; | |
| /// # use x86_64::structures::paging::{ | |
| /// # Mapper, Page, PhysFrame, FrameAllocator, | |
| /// # Size4KiB, OffsetPageTable, page_table::PageTableFlags, | |
| ///# FixedValidity | |
| /// # }; |
| /// Returns the page that contains the given fixed-width virtual address. | ||
| #[inline] | ||
| #[rustversion::attr(since(1.61), const)] | ||
| pub fn containing_address_const(address: VirtAddr<FixedValidity<BITS>>) -> Self { |
There was a problem hiding this comment.
I'm not a fan of having separate const functions though I understand that we may not have much of a choice until const traits are stabilized.
| /// Returns the page that contains the given fixed-width virtual address. | ||
| #[inline] | ||
| #[rustversion::attr(since(1.61), const)] | ||
| pub fn containing_address_const(address: VirtAddr<FixedValidity<BITS>>) -> Self { |
There was a problem hiding this comment.
Do we really need to re-check the validity of the aligned address? Given that we know that S::SIZE is smaller than 1<<47, isn't it guaranteed that the down-aligned address is always valid regardless of the address space size?
| } | ||
|
|
||
| impl<S: NotGiantPageSize> Page<S> { | ||
| impl<S: NotGiantPageSize, V: VirtAddrValidity> Page<S, V> { |
There was a problem hiding this comment.
We should add impls for the other validity types.
| /// A validity policy for which address-producing arithmetic is available. | ||
| pub(crate) trait VirtAddrArithmeticValidity: VirtAddrValidity {} |
There was a problem hiding this comment.
Isn't that just all validity types?
| } | ||
|
|
||
| impl GlobalDescriptorTable { | ||
| impl GlobalDescriptorTable<8, RuntimeValidity> { |
There was a problem hiding this comment.
| impl GlobalDescriptorTable<8, RuntimeValidity> { | |
| impl GlobalDescriptorTable { |
| /// Creates an empty GDT which can hold `MAX` number of [`Entry`]s. | ||
| #[inline] | ||
| pub const fn empty() -> Self { | ||
| pub const fn empty_with_validity() -> Self { |
There was a problem hiding this comment.
It seems we now have empty and empty_with_validity. AFAICT we don't do that in most other places. Why are we doing it here?
Hi Freax13 and thanks for your reviewing and comments! I was (until my graduation this June) a graduate student of Tsinghua University and a developer of the ArceOS project, where this great For your first concern, I agree that compatibility should be of the primary priority when adding LA57 support. For now I think the best solution is to make And for your second concern, I think OS kernels (and other bare metal applications) designed to be adaptive to different VA bits may find it useful to have access to all 3 validities. There's another thing to consider, that VirtAddrRT is by definition not possible to construct in const contexts, therefore VirtAddrRT-users still need VirtAddr48/VirtAddr57 for constants. And for ergonomics, I think the type alias mentioned above could (with careful consideration and design) be compatible and convenient enough for users that do not need and care about VirtAddr57/RT? In my opinion, cargo features may not be a good choice here because features are additive and adding a dependency may change the final feature list in a completely unexpected way (e.g. an indirect dependency just enables a feature and you cannot disable it). It would be a serious problem if we use features to control the behavior of the As for other types changed in this PR, maybe we should discuss them later? After we have a first-step agreement on how should VirtAddr looks like? I'm going to push a newer version to make VirtAddr48 the default, maybe later this week. |
|
I missed one thing in the last comment, the cost of CR4 read. I'm not sure which option is better now, maybe we should just postpone the validation until the value is used? Or should we cache it in som way? |
This works for
You raise a good point about const contexts.
I disagree with this. Yes, if dependencies start randomly enabling the feature for this, that'd be a problem, but they just shouldn't do that unless the library crate cannot work without one of LA48/LA57 for some weird reason. These features should also be enabled by the binary crate. The concept of "features that should only be enabled by the top level binary crate" isn't new. For example, sqlx uses features to chose the underlying async runtime and tracing has features to control the minimum log level. In both cases, bad things will happen if a library crate enables features that disagree with the choice by the top level binary crate, but there's no reason for library crates to do so.
Sure, I'm fine with working on this step by step.
I'm not sure what you mean by postponing the validation. Caching seems like a straightforward solution. |
It's actually a little bit tricky. Ideally we should use default generic parameters like fn foo() {
// Failed to compile, trying to infer the generic parameter.
let addr = VirtAddr::new(0x4200_0000);
// It actually infers `V` from addr, but it compiles as well for methods that does not have any arg with V in it.
let page = Page::<Size4KiB>::from_start_address(addr);
}Therefore, I chose to use type aliases for types. That does create many boilerplate aliases (may be simplified by an internal macro?) but that's the only way I can find to support different validities while preserving compatibility. For traits, I have just checked them:
I must say that maybe the best way is to introduce an unchecked
I understand your point and that makes total sense. I prefer to keep [features]
# new features, none of them enabled by default
virt_addr_57 = []
virt_addr_rt = []
default_virt_addr_57 = ["virt_addr_57"] # Optional, useful but a little bit dangerousThis preserves compatibility and flexibility.
There are pros and cons for both approaches:
Caching seems to be better but I'm not 100% sure. I haven't benchmarked the cost of CR4 seriously but reading it so often is (I feel too) worrisome. But doubling the size of |
|
The more I think about all of this the more I come to the conclusion that there just isn't a clean solution. I guess if we want to support LA-57 we have to be content with a solution that may be a bit rough around the edges.
I'm not suggesting that we cache the value inside the
IMO I think we should avoid a
We can make breaking changes for PRs targeting the |
…, add features gating `VirtAddr57` and `VirtAddrRT`
I agree...It won't be easy and clean.
I get it. Maybe we should add a function like
I've been giving this some more thought on this. For VirtAddr48/57-only users, we actually don't need to modify things other than |
|
@Freax13 I've pushed an updated version, containing some designs we have talked above. Could you please take a look at the For other types/methods, the current version is still temporary and I'll update them later. |
Freax13
left a comment
There was a problem hiding this comment.
@Freax13 I've pushed an updated version, containing some designs we have talked above. Could you please take a look at the
addrmodule (the updatedVirtAddr)? Thanks.For other types/methods, the current version is still temporary and I'll update them later.
Yeah, doing this piece by piece is a very good idea.
Here's a review for the the changes in the addr module. It's mostly smaller concerns. Overall, I think I'm happy with the direction it's taking so far.
| #[cfg(not(feature = "default_virt_addr_57"))] | ||
| pub type VirtAddr = VirtAddr48; |
There was a problem hiding this comment.
How well do this work with type inference? Is this sufficient to let the compiler figure out the generic parameter? Ideally, I'd like to see some code migrated to this new API to see how it impacts users (i.e. what changes they have to make during the transition).
| /// Tries to create a new canonical virtual address with the given number of bits. | ||
| #[inline] | ||
| #[rustversion::attr(since(1.61), const)] | ||
| fn try_new_with_bits<V: VirtAddrValidity>( |
There was a problem hiding this comment.
This function needs to be marked as unsafe. There's no guarantee that bits is correct. Passing in an incorrect value (i.e. 64) would allow creating uncanonical addresses.
| /// Returns the 9-bit level 4 page table index. | ||
| #[inline] | ||
| #[rustversion::attr(since(1.61), const)] | ||
| pub fn p4_index(self) -> PageTableIndex { |
There was a problem hiding this comment.
We should add a p5_index getter.
| PageOffset::new_truncate(self.0 as u16) | ||
| #[cfg_attr( | ||
| not(all(feature = "instructions", target_arch = "x86_64")), | ||
| allow(dead_code) |
There was a problem hiding this comment.
| allow(dead_code) | |
| expect(dead_code) |
| /// Enabled fixed validity policies always support arithmetic. `RuntimeValidity` supports | ||
| /// arithmetic when the `instructions` feature is enabled and the target is `x86_64`. |
There was a problem hiding this comment.
Should we even allow using RuntimeValidity if that's not the case? RuntimeValidity can't be used if that's not the case, can it?
There was a problem hiding this comment.
Only a small subset of methods like new_unsafe, zero, as_u64, etc. I actually cannot imagine a scenario where VirtAddrRT should be used when instructions is not enabled or the target is not x86_64, but I think it's also not something bad to allow it.
| return None; | ||
| } | ||
| _ => {} | ||
| let mask = (1u64 << <V>::bits()) - 1; |
There was a problem hiding this comment.
| let mask = (1u64 << <V>::bits()) - 1; | |
| let mask = (1u64 << V::bits()) - 1; |
Here and elsewhere.
| #[cfg(feature = "virt_addr_57")] | ||
| impl TryFrom<VirtAddr57> for VirtAddrRT { |
There was a problem hiding this comment.
Let's move this conversion impl to the other ones.
There was a problem hiding this comment.
It's here because it's #[cfg(all(feature = "instructions", target_arch = "x86_64"))].
| #[inline] | ||
| pub fn new(addr: u64) -> Self { |
There was a problem hiding this comment.
Duplicating methods like this isn't great (both for usability and maintainability), but it might be the best we can do.
If/when const traits ever get stabilized, we can probably merge to two implementations again.
There was a problem hiding this comment.
Yes, this is the best we can do for now.
Very happy to receive your review. I have made some fixes. Can we split the |
Purpose of this draft
This is a working prototype for LA57-aware virtual address types. It is meant to make the API choices and their effects reviewable in code, not to claim that all of those choices are final.
The work is inspired by #435 and #586. In particular, it agrees with the central idea in #586 that address validity should be represented by a generic parameter. It explores some additional questions that became visible while propagating that design through the crate and integrating it into my own OS program.
The current branch implements the runtime-valid-by-default end of the design space. That is useful as a prototype because it demonstrates what is required for a program to use the same address types while running in either LA48 or LA57 mode. It is not necessarily the best compatibility choice for the final API.
This PR is organized as a list of technical decisions. I have marked them as:
Scope
This prototype covers:
structures, basic page/range types, TLB operations, and register accessors;
It deliberately does not add P5 page-table traversal, P5 indices, or LA57 mapping support. Page-table APIs should be handled separately after the address model is settled. It also does not generalize
PhysAddr: LA57 changes virtual address canonicality, not the architectural physical-address width.