Skip to content

feat: i686 page tables, snapshot compaction, and CoW (standalone)#1385

Open
danbugs wants to merge 12 commits intomainfrom
nanvix-platform
Open

feat: i686 page tables, snapshot compaction, and CoW (standalone)#1385
danbugs wants to merge 12 commits intomainfrom
nanvix-platform

Conversation

@danbugs
Copy link
Copy Markdown
Contributor

@danbugs danbugs commented Apr 16, 2026

Summary

The cleaned-up version of #1381: drops the dependencies on #1379 and #1380 so this PR can land on its own.

Commits

  • refactor: replace nanvix-unstable with i686-guest and guest-counter features
  • feat: i686 protected-mode boot and unified restore path
  • feat: i686 page tables, snapshot compaction, and CoW support
  • fixup: address PR #1381 review comments

What this adds

  • i686 guest support on the x86_64 host: 32-bit protected-mode boot, unified restore path, 2-level page-table walking and snapshot compaction with CoW-resolved PTE reads.
  • Feature rename — the prior nanvix-unstable gates split into i686-guest (PT walker, protected-mode boot, compaction) and guest-counter (scratch counter plumbing). No behavior change for consumers using the old feature flag; Cargo.toml / Justfile / build.rs updated to match.

What it does not include (vs #1381)

Both can land separately; #1381 happened to stack on top of them.

Review items from #1381 addressed here

  • PTE decode uses u32::from_le_bytes (previously from_ne_bytes) — PTEs are little-endian by arch spec.
  • i686-guest feature guarded with a compile_error! on targets that are neither x86 (guest) nor x86_64 (host).
  • restore_snapshot rewrites the scratch PD-roots bookkeeping (SCRATCH_TOP_PD_ROOTS_{COUNT,ARRAY}_OFFSET) so a subsequent snapshot() doesn't see a stale zero count. Snapshot persists the root count; root GPAs are deterministic (layout.get_pt_base_gpa() + i * PAGE_SIZE).

Verification

Built with cargo check -p hyperlight-{common,host,guest} --features kvm,mshv3,executable_heap,i686-guest,guest-counter — clean. End-to-end tested against nanvix@e61306676: Hello 5/5 with NANVIX_REPEAT=4 through the restore+call loop.

ludfjig and others added 4 commits April 16, 2026 20:09
…eatures

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>

Co-authored-by: danbugs <danilochiarlone@gmail.com>
Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>

Co-authored-by: danbugs <danilochiarlone@gmail.com>
Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>

Co-authored-by: danbugs <danilochiarlone@gmail.com>
- sandbox/snapshot.rs: decode i686 PTEs with `u32::from_le_bytes`
  instead of `from_ne_bytes`. Page-table entries are defined as
  little-endian by arch spec; `from_ne_bytes` is incorrect on
  big-endian hosts and inconsistent with surrounding helpers that
  already use `from_le_bytes`.

- hyperlight_common/vmem.rs: guard the `i686-guest` feature with a
  `compile_error!` on targets that are neither `x86` (the guest
  itself) nor `x86_64` (the host that runs the guest). Previously
  enabling the feature on e.g. aarch64 would silently compile but
  downstream crates would hit confusing missing-item errors when
  they reached for `vmem::i686_guest`.

- mem/mgr.rs, sandbox/snapshot.rs: persist the per-process
  PD-roots count in `Snapshot` and rewrite the scratch bookkeeping
  area (`SCRATCH_TOP_PD_ROOTS_{COUNT,ARRAY}_OFFSET`) during
  `restore_snapshot`. Scratch is zeroed on restore, so without
  this a subsequent `snapshot()` call would read count=0 through
  `read_pd_roots_from_scratch` and fail. Root `i`'s compacted
  GPA is deterministic (`layout.get_pt_base_gpa() + i * PAGE_SIZE`
  — same layout `compact_i686_snapshot` used when building the
  rebuilt PDs), so we only need to store the count.

Signed-off-by: danbugs <danilochiarlone@gmail.com>
@danbugs danbugs added the kind/enhancement For PRs adding features, improving functionality, docs, tests, etc. label Apr 16, 2026
Copy link
Copy Markdown
Member

@simongdavies simongdavies left a comment

Choose a reason for hiding this comment

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

Reviewers: Opus, Gemini,ChatGPT

All three reviewers agree this is a well-structured PR that cleanly separates the nanvix-unstable feature into i686-guest and guest-counter, and replaces the TODO-laden real-mode boot with proper 32-bit protected mode + paging. The code quality is generally good — comments are thorough, unsafe blocks are contained, and the architecture is sensible.

However, all three independently flagged critical concerns around the CoW flag logic, potential scratch memory layout overlap, and the complete absence of tests for the i686 code path.

🟢 Good Stuff

  • Unified restore_all_state(): Removing the TODO-laden nanvix-unstable branch with its "this is probably not correct" comment is a significant improvement. The CR3-based restore is now clean and shared.
  • compile_error! guard for i686-guest on non-x86 — nice defensive programming.
  • OOB-safe read_entry implementations: Both Snapshot and SharedMemoryPageTableBuffer return 0 (not-present) for OOB addresses rather than panicking. Exactly right for defensively walking guest-controlled page tables.
  • Clean feature flag split: i686-guest + guest-counter are genuinely independent concerns.
  • Thorough doc comments on Snapshot fields (n_pd_roots, separate_pt_bytes).
  • Scratch bookkeeping factoring: Extracting copy_pt_to_scratch() from update_scratch_bookkeeping() is a clean refactor.

🔴 Additional findings (on lines outside the diff)

Snapshot impl of TableReadOps always reads 8-byte entries (snapshot.rs line 131, Flagged by: 2/3)

The impl TableReadOps for Snapshot at line 131 of snapshot.rs unconditionally reads 8 bytes for a PTE. While it appears to only be used in the #[cfg(not(feature = "i686-guest"))] path currently, it's not gated by #[cfg]. If anyone ever calls virt_to_phys with a Snapshot on the i686 path, it will read 8-byte entries from a 4-byte PTE table — reading cross-entry data and producing garbage mappings. Either gate with #[cfg(not(feature = "i686-guest"))] or add a comment + compile_error! guard.

Endianness inconsistency (snapshot.rs line 149, Flagged by: 2/3)

Line 149 of snapshot.rs uses u64::from_ne_bytes() (native endian), while the i686 SharedMemoryPageTableBuffer::read_entry() at line 273 correctly uses u32::from_le_bytes() with a comment noting "Page-table entries are little-endian by arch spec". The x86_64 path happens to work on LE hosts but is technically incorrect by the same reasoning. Should use from_le_bytes consistently.

See inline comments for the remaining findings.

Comment thread src/hyperlight_host/src/sandbox/snapshot.rs Outdated
Comment thread src/hyperlight_common/src/layout.rs Outdated
Comment thread src/hyperlight_host/src/sandbox/snapshot.rs
Comment thread src/hyperlight_host/src/sandbox/snapshot.rs Outdated
Comment thread src/hyperlight_host/src/sandbox/snapshot.rs Outdated
Comment thread src/hyperlight_host/src/hypervisor/regs/x86_64/special_regs.rs Outdated
Comment thread src/hyperlight_host/build.rs Outdated
@andreiltd andreiltd force-pushed the nanvix-platform branch 2 times, most recently from cd065b1 to 105974d Compare April 17, 2026 12:05
Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com>
Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com>
Copy link
Copy Markdown
Member

@syntactically syntactically left a comment

Choose a reason for hiding this comment

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

The high-level feature here (supporting x86-32 VA spaces instead of the pretending-to-be-real-mode #[cfg(feature = "nanvix-unstable")]) is great.

It seems like all of the i686 page table code is scattered around in various places and does not follow the same structure or architecture-independent API as the amd64 and aarch64 code. I don't think that's at all maintainable. Please try to use the same structures and APIs as the amd64 code, having i686 implementations in hyperlight_common/src/arch/i686/vmem.rs that implement the architecture-independent re-exports of hyperlight_common/src/vmem.rs and allowing most/all of the downstream code to use the same APIs and patterns. (It may be sensible to extract out the page table iterator code that is currently amd64 only, in which case I would expect it to not require much extra code to support i686 page tables (although, if you do this, please check that inlining the iterators still works, at least in release builds---e.g. vmem::map should inline down to basically the same thing as a handwritten loop in a single function)).

I would expect the changes to src/hyperlight_host/src/sandbox/snapshot.rs in particular to be limited to basically

  • Make Snapshot::new iterate through several different VA spaces worth of mappings, instead of just one (there ought to be no need to do anything special to avoid duplicated backing pages as long as the existing phys_seen map is kept)
  • (If necessary) minor changes to support keeping the PTs generated for the new mappings in a separate vec in the snapshot, instead of at the end of the snapshot memory where they are now on all architectures. Ideally, rather than being #[cfg] items directly in the crate, this would be controlled by some constant e.g. hyperlight_common::vmem::PA_SPACE_IS_SMALL: bool (with the naming reflecting the actual motivation for making this different across architectures).
    • It occurs to me that this may not even need any changes here---perhaps it could be achieved simply by changing the mem mgr or hypervisor manager code to avoid mapping the end of the snapshot region into the guest PA space on i686.

I have left a few other minor inline comments, but the above issue is the only thing that I think is actually quite important.

I didn't review any of the i686 address space manipulation code yet, because it seems like it will need to be refactored and generalised a little to be able to provide the same APIs that we use on other architectures. I think it is quite important that all the guest architectures implement the same APIs, so that most of the code can be architecture-independent.

/// page tables in guest memory. The `arch/i686/vmem.rs` module only compiles
/// for `target_arch = "x86"` (the guest side), so the host-side walker lives
/// here, gated behind the feature flag.
#[cfg(feature = "i686-guest")]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it would make more sense to have the i686/vmem.rs module used when feature = i686-guest---we should understand that the arch in hyperlight_common is always about the guest arch.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

moved

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It seems like this for some reason still pulls in a bunch of amd64 code and then exports another sub-module for i686? I am somewhat confused by what is going on, but I really think that this should follow the same pattern as amd64 and arm64 guests---all the exports from the architecture-independent vmem are the i686 values when we are compiling for an i686 guest.

Comment thread src/hyperlight_common/src/arch/amd64/vmem.rs Outdated
ExeInfo::Elf(elf) => Offset::from(elf.entrypoint_va()),
}
}
/// Returns the base virtual address of the loaded binary (lowest PT_LOAD p_vaddr).
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This whole base va sliding is a hack because of some executables we used to have + the lack of virtual memory in the guest. I don't think that any of the usual guests end up computing anything other than 0 here, so I was thinking it was worth getting rid of entirely. Do the nanvix binaries actually end up with something here? If so, can we replace the unpleasant semantics-breaking relocation with just changing the initial page tables slightly to get things mapped to the VAs that they ask for?

(Not relevant for merging this PR---just curious about answers to these questions for the future).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes nanvix requires this

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What does it actually end up being out of curiosity? And, is the binary actually PIC or not? I don't see any added code for processing textrels or anything, so if it's not PIC I would expect shifting text around like this not to work, and if it is PIC, I wouldn't expect this to be necessary at all since generally PIEs are based at 0.

If so, can we replace the unpleasant semantics-breaking relocation with just changing the initial page tables slightly to get things mapped to the VAs that they ask for

I still think this is the right way to go, although for PIC binaries that look like they start from 0 we should move them somewhere e.g. 0x1000. It can be a different PR, however.

Comment thread src/hyperlight_host/src/mem/mgr.rs
abort_buffer: Vec::new(), // Guest doesn't need abort buffer
};
host_mgr.update_scratch_bookkeeping()?;
host_mgr.copy_pt_to_scratch()?;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does this need to make the same differentiation between the i686 and x86-64 sources of the initial page table information?

Comment thread src/hyperlight_host/src/mem/mgr.rs Outdated
Comment thread src/hyperlight_host/src/sandbox/initialized_multi_use.rs Outdated
/// CoW resolution map: maps snapshot GPAs to their CoW'd scratch GPAs.
/// Built by walking the kernel PD to find pages that were CoW'd during boot.
#[cfg(feature = "i686-guest")]
cow_map: Option<&'a std::collections::HashMap<u64, u64>>,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is this information necessary beyond the scope of one address space traversal? And, why is this a useful form of this information?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

it's used in a couple of places and TableReadOps::read_entry takes a &self...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What are the places, and why is it used when we didn't need a similar structure on amd64/arm64?

u64::from_ne_bytes(n)
let memoff = access_gpa(self.snap, self.scratch, self.layout, addr);
// For i686 guests, page table entries are 4 bytes; for x86_64 they
// are 8 bytes. Read the correct size based on the feature flag.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This can just use mem::size_of::<hyperlight_common::vmem::PageTableEntry>(). Or, if it can't because of some limitation on the const-ness of that expression, it should just be another constant and/or type synonym defined in the same place.

There should ideally be little-to-no arch-specific code in this module: the hyperlight_common::vmem and related APIs should abstract over the architecture-of-the-guest differences well enough that this code can be more-or-less entirely generic. (If there are problems with the vmem api assuming to much, we should fix them, rather than introduce more #[cfg] here).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

please take another look, I believe it's mostly fixed

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There is still a bunch of #[cfg] gated code that looks identical apart from some sizes. I don't see why this can't just directly be something like

let Some(pte_bytes) = memoff.and_then(|(mem, off)| mem.get(off..off+mem::size_of::<vmem::PageTableEntry>())) else {
  return 0;
};
let n: [u8; mem::size_of::<vmem::PageTableEntry>()] = pte_bytes.try_into().unwrap();
vmem::PageTableEntry::from_le_bytes(n)

Comment thread src/hyperlight_host/src/sandbox/snapshot.rs Outdated
Refactor i686 page table code to follow the same architecture-independent
API as amd64/aarch64, per reviewer feedback.

hyperlight_common changes:
- Extract shared page table iterators (modify_ptes, read_pte_if_present,
  require_pte_exist, write_entry_updating) to vmem.rs with PTE_SHIFT
  const generic for reuse across architectures.
- Implement i686/vmem.rs with proper map()/virt_to_phys() using the
  shared iterators, replacing stubs and the host-side i686_guest
  submodule that was in amd64/vmem.rs.
- Remove PAGE_USER from pte_for_table (PDEs are supervisor-only by
  default); user-space PDEs get PAGE_USER via post-processing.
- Remove SCRATCH_TOP_PD_ROOTS_* and MAX_PD_ROOTS constants (replaced
  by set_pt_root_finder callback).

hyperlight_host changes:
- Unify GuestPageTableBuffer<PTE_BYTES> for both architectures,
  removing i686_pt::Builder, build_initial_i686_page_tables, and
  compact_i686_snapshot (~500 lines removed from snapshot.rs).
- Add root_offset to GuestPageTableBuffer for targeting per-process
  PDs, with finalize_multi_root() to replicate kernel PDEs, map
  scratch, and set PAGE_USER across all roots.
- Snapshot::new uses filtered_mappings with root-index tagging for
  per-process PD isolation in a single pass (no double PT walk).
- Add set_pt_root_finder callback on MultiUseSandbox, replacing the
  scratch-based PD roots bookkeeping.
- Add CR0 named constants (CR0_PE, CR0_ET, CR0_WP, CR0_PG).
- Add compaction_kind() helper to deduplicate kind conversion.
- Add 5 i686 unit tests for map/virt_to_phys.
- Detailed comments on separate_pt_bytes explaining the map_file_cow
  GPA overlap constraint.

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
@ludfjig ludfjig requested a review from Copilot April 21, 2026 01:18
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds standalone i686 guest support (protected-mode boot, 2-level PT walking, CoW-aware PTE reads) and snapshot compaction, while splitting the old nanvix-unstable gating into i686-guest and guest-counter.

Changes:

  • Introduces i686 page-table walking/mapping and updates snapshot creation/restore to support multi-root compaction + CoW resolution.
  • Adds an optional PtRootFinder callback to snapshot multiple page-table roots (e.g., per-process PD roots).
  • Renames feature gating across host/guest/common (nanvix-unstablei686-guest + guest-counter) and updates build tooling.

Reviewed changes

Copilot reviewed 29 out of 29 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
src/hyperlight_host/src/sandbox/uninitialized_evolve.rs Updates feature gate for publishing scratch HostSharedMemory for guest counter.
src/hyperlight_host/src/sandbox/uninitialized.rs Renames feature gate and propagates guest-counter gating through GuestCounter + tests.
src/hyperlight_host/src/sandbox/snapshot.rs Implements i686-aware PT reading, CoW resolution, multi-root walking, and separate PT storage for i686 snapshots.
src/hyperlight_host/src/sandbox/mod.rs Re-exports PtRootFinder alongside MultiUseSandbox.
src/hyperlight_host/src/sandbox/initialized_multi_use.rs Adds PtRootFinder callback and uses it to supply multiple PT roots to snapshotting.
src/hyperlight_host/src/mem/shared_mem.rs Renames feature gate for test-only HostSharedMemory view.
src/hyperlight_host/src/mem/mgr.rs Generalizes GuestPageTableBuffer over PTE size, adds i686 multi-root finalization, and updates restore PT copying logic.
src/hyperlight_host/src/mem/memory_region.rs Updates cfg attributes to the new i686-guest feature naming.
src/hyperlight_host/src/mem/layout.rs Removes nanvix-unstable-specific base address split; updates cfg attrs to i686-guest.
src/hyperlight_host/src/mem/exe.rs Exposes ELF base VA to compute entrypoint offsets correctly.
src/hyperlight_host/src/lib.rs Re-exports GuestCounter under guest-counter feature.
src/hyperlight_host/src/hypervisor/virtual_machine/whp.rs Updates test gating from nanvix-unstable to i686-guest.
src/hyperlight_host/src/hypervisor/virtual_machine/mshv/x86_64.rs Updates test gating from nanvix-unstable to i686-guest.
src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs Updates XSAVE constant/test gating from nanvix-unstable to i686-guest.
src/hyperlight_host/src/hypervisor/virtual_machine/kvm/x86_64.rs Updates test gating from nanvix-unstable to i686-guest.
src/hyperlight_host/src/hypervisor/regs/x86_64/special_regs.rs Adds i686 protected-mode paging defaults and reorganizes shared constants.
src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs Switches to i686 paging defaults when i686-guest is enabled and unifies CR3 handling.
src/hyperlight_host/build.rs Adjusts unshared_snapshot_mem documentation/logic ordering around feature flags.
src/hyperlight_host/Cargo.toml Adds i686-guest and guest-counter features; keeps nanvix-unstable as a compatibility alias.
src/hyperlight_guest/src/layout.rs Renames guest counter gating to guest-counter.
src/hyperlight_guest/Cargo.toml Adds i686-guest and guest-counter features.
src/hyperlight_common/src/vmem.rs Adds shared PT iterator infrastructure, i686 guest module export, and i686-guest target validation.
src/hyperlight_common/src/layout.rs Switches x86_64 layout selection based on i686-guest; renames guest-counter constant gating.
src/hyperlight_common/src/arch/i686/vmem.rs Implements real i686 2-level paging map + walk support (incl. CoW tag via AVL bits).
src/hyperlight_common/src/arch/i686/layout.rs Implements real i686 min scratch size calculation.
src/hyperlight_common/src/arch/amd64/vmem.rs Refactors amd64 PT code to use shared iterator infrastructure + shared UpdateParent logic.
src/hyperlight_common/src/arch/aarch64/vmem.rs Updates TableMovability impls to match new shared trait shape.
src/hyperlight_common/Cargo.toml Adds i686-guest and guest-counter features; keeps nanvix-unstable as alias.
Justfile Updates i686 checks and replaces nanvix-unstable checks with i686-guest where appropriate.

Comment on lines +368 to +370
use std::collections::HashSet;
let mut all_mappings = Vec::new();
let mut seen_phys = HashSet::new();
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

Deduplicating by phys_base here can drop valid alias mappings (multiple virt_base values mapping to the same phys_base). That would cause the rebuilt page tables to miss some virtual mappings in the compacted snapshot. A safer approach is to keep all mappings (or at most dedupe by (effective_root, virt_base)), and rely on the later phys_seen compaction map to dedupe page contents.

Copilot uses AI. Check for mistakes.
Comment on lines +419 to +421
if seen_phys.insert(m.phys_base) {
all_mappings.push((effective_root, m));
}
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

Deduplicating by phys_base here can drop valid alias mappings (multiple virt_base values mapping to the same phys_base). That would cause the rebuilt page tables to miss some virtual mappings in the compacted snapshot. A safer approach is to keep all mappings (or at most dedupe by (effective_root, virt_base)), and rely on the later phys_seen compaction map to dedupe page contents.

Copilot uses AI. Check for mistakes.
Comment thread src/hyperlight_host/src/sandbox/snapshot.rs Outdated
// doesn't make this any more panic-y.
#[allow(clippy::unwrap_used)]
let n: [u8; 8] = pte_bytes.try_into().unwrap();
u64::from_ne_bytes(n)
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

This still decodes 8-byte page-table entries using from_ne_bytes. Page table entries are little-endian by architecture spec (including x86_64), so this should use u64::from_le_bytes to avoid host-endianness dependence.

Suggested change
u64::from_ne_bytes(n)
// Page-table entries are little-endian by arch spec;
// use `from_le_bytes` so host endianness doesn't leak in.
u64::from_le_bytes(n)

Copilot uses AI. Check for mistakes.
Comment thread src/hyperlight_host/src/sandbox/snapshot.rs Outdated
Comment on lines +243 to +246
let page_gpa = addr & 0xFFFFF000;
if let Some(map) = self.cow_map {
if let Some(&scratch_gpa) = map.get(&page_gpa) {
scratch_gpa + (addr & 0xFFF)
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

These hard-coded masks duplicate PAGE_SIZE knowledge. Using PAGE_SIZE-derived masks (e.g., !(PAGE_SIZE as u64 - 1) / (PAGE_SIZE as u64 - 1)) would make the intent clearer and avoid accidental inconsistencies if constants ever change.

Suggested change
let page_gpa = addr & 0xFFFFF000;
if let Some(map) = self.cow_map {
if let Some(&scratch_gpa) = map.get(&page_gpa) {
scratch_gpa + (addr & 0xFFF)
let page_gpa = addr & !(PAGE_SIZE as u64 - 1);
if let Some(map) = self.cow_map {
if let Some(&scratch_gpa) = map.get(&page_gpa) {
scratch_gpa + (addr & (PAGE_SIZE as u64 - 1))

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +109
vm.set_sregs(&CommonSpecialRegisters::standard_32bit_paging_defaults(
_pml4_addr,
))
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

Under i686-guest, _pml4_addr is passed as the 32-bit page directory base. Renaming this variable to something architecture-neutral (e.g., root_pt_addr/root_table_addr) would reduce confusion now that the same path supports both amd64 and i686.

Copilot uses AI. Check for mistakes.
MappingKind::Unmapped => MappingKind::Unmapped,
}
}

Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The new multi-root + dedup/collection logic in filtered_mappings is central to snapshot correctness but isn’t directly covered by the existing tests in this module. Adding a unit test that creates two distinct VAs mapping to the same PA (aliasing) and verifies both VAs remain mapped after compaction would help prevent regressions (especially given the new dedup behavior).

Suggested change
#[cfg(test)]
fn filtered_mapping_dedup_key(root_index: usize, virt_base: u64) -> (usize, u64) {
(root_index, virt_base)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn compaction_dedup_preserves_aliasing_virtual_addresses() {
let root_index = 0usize;
let virt_a = 0x1000_u64;
let virt_b = 0x2000_u64;
let shared_phys = 0x3000_u64;
let kind = MappingKind::Basic(BasicMapping {
readable: true,
writable: true,
executable: false,
});
let mut collected = HashMap::new();
collected.insert(
filtered_mapping_dedup_key(root_index, virt_a),
(shared_phys, compaction_kind(&kind)),
);
collected.insert(
filtered_mapping_dedup_key(root_index, virt_b),
(shared_phys, compaction_kind(&kind)),
);
assert_eq!(collected.len(), 2);
assert_eq!(
collected
.get(&filtered_mapping_dedup_key(root_index, virt_a))
.map(|(phys, _)| *phys),
Some(shared_phys)
);
assert_eq!(
collected
.get(&filtered_mapping_dedup_key(root_index, virt_b))
.map(|(phys, _)| *phys),
Some(shared_phys)
);
assert!(matches!(
collected
.get(&filtered_mapping_dedup_key(root_index, virt_a))
.map(|(_, kind)| kind),
Some(MappingKind::Cow(_))
));
assert!(matches!(
collected
.get(&filtered_mapping_dedup_key(root_index, virt_b))
.map(|(_, kind)| kind),
Some(MappingKind::Cow(_))
));
}
#[test]
fn compaction_dedup_preserves_same_virtual_address_across_roots() {
let virt = 0x4000_u64;
let phys = 0x5000_u64;
let kind = MappingKind::Basic(BasicMapping {
readable: true,
writable: false,
executable: false,
});
let mut collected = HashMap::new();
collected.insert(filtered_mapping_dedup_key(0, virt), (phys, compaction_kind(&kind)));
collected.insert(filtered_mapping_dedup_key(1, virt), (phys, compaction_kind(&kind)));
assert_eq!(collected.len(), 2);
assert!(collected.contains_key(&filtered_mapping_dedup_key(0, virt)));
assert!(collected.contains_key(&filtered_mapping_dedup_key(1, virt)));
assert!(matches!(
collected.get(&filtered_mapping_dedup_key(0, virt)).map(|(_, kind)| kind),
Some(MappingKind::Basic(BasicMapping {
readable: true,
writable: false,
executable: false,
}))
));
}
}

Copilot uses AI. Check for mistakes.
return None;
root_pts: &[u64],
#[cfg(feature = "i686-guest")] cow_map: &std::collections::HashMap<u64, u64>,
) -> Vec<(usize, Mapping, &'a [u8])> {
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The new multi-root + dedup/collection logic in filtered_mappings is central to snapshot correctness but isn’t directly covered by the existing tests in this module. Adding a unit test that creates two distinct VAs mapping to the same PA (aliasing) and verifies both VAs remain mapped after compaction would help prevent regressions (especially given the new dedup behavior).

Copilot uses AI. Check for mistakes.
Comment thread src/hyperlight_host/build.rs Outdated
// temporarily!) need to use writable/un-shared snapshot
// memories, and so can't share
unshared_snapshot_mem: { any(feature = "nanvix-unstable", feature = "gdb") },
// gdb needs writable snapshot memory for debug access.
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The comment mentions only gdb, but the condition also includes nanvix-unstable. Either update the comment to reflect both reasons, or (if intended) migrate this to the new feature split (i686-guest / guest-counter) so the build-time behavior matches the new feature naming.

Suggested change
// gdb needs writable snapshot memory for debug access.
// Writable snapshot memory is required for gdb debug access and nanvix-unstable builds.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

@syntactically syntactically left a comment

Choose a reason for hiding this comment

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

This is definitely a big step in the right direction!

I still have some significant concerns about divergences in code flow between the i686 guests and other architectures. Really, this PR has (at least) two parts:

  • Support multiple virtual address spaces in the guest (this should all be totally architecture-independent, in order to keep the high-level model of hyperlight the same across architectures---if it is not, bugs will certainly creep in)
  • Support i686 guests. (This should ideally be pretty much limited to files in arch/i686 and a few places where some new architecture-specific logic is unavoidable or code sharing opportunities are too good to ignore (e.g. the hypervisor driver x86_64.rs files, which are now I guess somewhat misnamed).

I say "at least two" because it seems to me that there are some other smaller features as well, such as adding support for round-tripping some extra permission information (acknowledging the use of two protection levels in the guest), etc. These should also to the maximum extent possible be split into architecture-independent and architecture-dependent portions---for example, whatever extra generic architecture-independent semantic information about mappings needs to be added to track whether or not they are user should go in the architecture-independent vmem::Mapping structures, but it's probably fine if the architecture-dependent code in arch/i686/vmem.rs is the only thing that actually produces or consumes mappings with those flags.

Comment thread src/hyperlight_common/src/vmem.rs Outdated
pub use arch::PAGE_SIZE;
#[cfg(all(feature = "i686-guest", target_arch = "x86_64"))]
#[path = "arch/i686/vmem.rs"]
pub mod i686_guest;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why isn't this just mod arch like it is on the other architectures? This implies that on i686 we pull in the amd64 arch module and the i686 one? Which doesn't make sense to me.


/// Extract bits `[HIGH_BIT:LOW_BIT]` (inclusive) from a u64.
#[inline(always)]
pub(crate) fn bits<const HIGH_BIT: u8, const LOW_BIT: u8>(x: u64) -> u64 {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Probably all of these can be pub(self) or something so that they are accessible by the mod arch but not the rest of the crate?

/// `entry_ptr` must point to a valid page table entry.
#[inline(always)]
#[allow(clippy::useless_conversion)]
pub(crate) unsafe fn read_pte_if_present<Op: TableReadOps>(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should not be in the arch-independent code in this module, since it depends on an architecture-dependent feature (bit 0 being the present bit).


/// Parent is the root (e.g. CR3). `MayMoveTable` impl lives in each arch module.
#[derive(Copy, Clone)]
pub struct UpdateParentRoot {}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Similarly, since the actual impl for this has to be in each architecture-specific module, I would expect the type to be there as well. If some architecture needed to actually have state here (which some might) the use of the trait fn to construct this was beneficial.

Comment thread src/hyperlight_common/src/vmem.rs Outdated
/// Iterates over PTEs at one level of the page table hierarchy.
///
/// `HIGH_BIT`/`LOW_BIT` select which VA bits index this level.
/// `PTE_SHIFT` is log2(PTE byte size) (3 for 8-byte, 2 for 4-byte).
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there a reason to take this here rather than use mem::size_of::<arch::PageTableEntry>()?

Some(&resolved.as_ref()[..PAGE_SIZE])
}

fn map_specials(pt_buf: &GuestPageTableBuffer, scratch_size: usize) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is this inlined? It looks to me as though the equivalent operation is still needed in both places it originally was, and it is a convenient place to centralise special mappings---e.g. at some point the snapshot pt mapping will actually come back, and when that is supported on a platform this is a nice single point to make sure it gets done.

ExeInfo::Elf(elf) => Offset::from(elf.entrypoint_va()),
}
}
/// Returns the base virtual address of the loaded binary (lowest PT_LOAD p_vaddr).
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What does it actually end up being out of curiosity? And, is the binary actually PIC or not? I don't see any added code for processing textrels or anything, so if it's not PIC I would expect shifting text around like this not to work, and if it is PIC, I wouldn't expect this to be necessary at all since generally PIEs are based at 0.

If so, can we replace the unpleasant semantics-breaking relocation with just changing the initial page tables slightly to get things mapped to the VAs that they ask for

I still think this is the right way to go, although for PIC binaries that look like they start from 0 we should move them somewhere e.g. 0x1000. It can be a different PR, however.

);
}

// Phase 4 (i686 only): replicate kernel PDEs and scratch
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If you just refrain from filtering out the kernel PDEs earlier you will not have to replicate them here (which is important: this code introduces an assumption that all guest kernels will behave precisely as Nanvix does in terms of what kinds of task virtual address layouts they use, which definitely does not belong in Hyperlight core).

},
);
#[cfg(feature = "i686-guest")]
hyperlight_common::vmem::i686_guest::map(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This shouldn't need to be architecture-specific here---the whole point of exporting an architecture-independent veneer of an interface from hyperlight_common::vmem is to remove the need for this sort of thing.

// We do not need the original regions anymore, as any uses of
// them in the guest have been incorporated into the snapshot
// properly.
#[cfg(not(feature = "i686-guest"))]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is the explanatory comment removed, and why is this now cfg-guarded?

…rom guest memory slot

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
ludfjig added 4 commits April 21, 2026 13:59
…d user_accessible to Mapping

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
…arch constants, remove PTE_SHIFT, restore comments

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/enhancement For PRs adding features, improving functionality, docs, tests, etc.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants