feat: enable ASLR for PIE guest binaries - #1655
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces ASLR for PIE guest ELF binaries by randomizing the guest virtual base address of the code region, while preserving correct behavior for non-PIE binaries that must load at their declared ELF virtual addresses. It threads the chosen code_virt_base through snapshot creation/restore paths and updates tests and build infrastructure to cover non-PIE guests.
Changes:
- Randomize PIE guest code virtual base and validate it against other guest mappings.
- Preserve
code_virt_baseacross snapshot/restore and use it for code-region GVA mappings and entrypoint computation. - Add non-PIE guest build targets and an integration test exercising non-PIE ELF VA mapping.
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/hyperlight_testing/src/lib.rs | Adds helper to locate the non-PIE simpleguest binary. |
| src/hyperlight_host/tests/integration_test.rs | Adds an integration test that runs a non-PIE guest end-to-end. |
| src/hyperlight_host/src/sandbox/snapshot/mod.rs | Computes/stores code_virt_base, uses it for loading, mapping, and entrypoint calculation. |
| src/hyperlight_host/src/sandbox/snapshot/file/mod.rs | Wires new Snapshot field for file-loaded snapshots (currently placeholder value). |
| src/hyperlight_host/src/sandbox/snapshot/file/config.rs | Relaxes entrypoint validation to allow non-identity-mapped code GVAs. |
| src/hyperlight_host/src/sandbox/initialized_multi_use.rs | Updates trace-guest GVA tests to use code_virt_base. |
| src/hyperlight_host/src/mem/mgr.rs | Threads code_virt_base through the memory manager so it survives restore/evolve flows. |
| src/hyperlight_host/src/mem/layout.rs | Implements PIE code VA randomization and conflict validation for code mapping. |
| src/hyperlight_host/src/mem/exe.rs | Exposes ExeInfo::is_pie() for ELF type classification. |
| src/hyperlight_host/src/mem/elf.rs | Tracks PIE-ness (ET_DYN) in ELF metadata. |
| src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs | Updates internal tests for the expanded memory manager constructor. |
| Justfile | Adds recipes to build and stage non-PIE guest binaries. |
| .gitignore | Ignores the new non-PIE guest build target directory. |
| .github/workflows/dep_build_guests.yml | Builds and stages non-PIE guests in CI guest-build workflow. |
| /// For PIE binaries (`is_pie == true`), the code is identity-mapped so | ||
| /// the virtual base equals the physical load address and no conflict | ||
| /// is possible by construction. | ||
| /// | ||
| /// For non-PIE binaries, the code appears at the ELF's declared | ||
| /// virtual address (`elf_base_va`), which may differ from the physical | ||
| /// load address. This method checks that the resulting virtual range | ||
| /// `[elf_base_va, elf_base_va + loaded_size)` does not overlap any | ||
| /// non-Code region. |
| let min_page = 0x1000_u64; // 0x1000 * PAGE_SIZE = 0x1000000 | ||
| let max_page = 0x7_FFFF_FFFF_u64 - code_size_pages; | ||
| let page_number = rng.random_range(min_page..max_page); | ||
| page_number * PAGE_SIZE_USIZE as u64 | ||
| }; | ||
|
|
||
| // Verify the code mapping does not conflict with other mappings | ||
| // (both non-PIE with declared VA and PIE with randomized ASLR base). | ||
| let code_virt_end = virt_base + loaded_size; | ||
| for rgn in self.get_memory_regions_::<GuestMemoryRegion>(())?.iter() { | ||
| if rgn.region_type == MemoryRegionType::Code { | ||
| continue; | ||
| } | ||
| let rgn_start = rgn.guest_region.start as u64; | ||
| let rgn_end = rgn_start + rgn.guest_region.len() as u64; | ||
| if virt_base < rgn_end && rgn_start < code_virt_end { |
| sregs: None, | ||
| entrypoint: NextAction::Initialise(load_addr + entrypoint_va - base_va), | ||
| code_virt_base, | ||
| entrypoint: NextAction::Initialise(code_virt_base + entrypoint_offset), | ||
| snapshot_generation: 0, |
| // Entrypoint address must point inside the guest snapshot | ||
| // region `[BASE_ADDRESS, BASE_ADDRESS + snapshot_size)`. The | ||
| // address is a GVA, bounded by the same range because guests | ||
| // identity-map the snapshot region at low VAs. A guest | ||
| // dispatching from a non-identity-mapped VA must relax this | ||
| // check. | ||
| let snap_lo = SandboxMemoryLayout::BASE_ADDRESS as u64; | ||
| let snap_hi = snap_lo | ||
| .checked_add(self.layout.snapshot_size as u64) | ||
| .ok_or_else(|| { | ||
| crate::new_error!( | ||
| "snapshot layout overflow: BASE_ADDRESS + snapshot_size ({}) does not fit in u64", | ||
| self.layout.snapshot_size | ||
| ) | ||
| })?; | ||
| if self.entrypoint_addr < snap_lo || self.entrypoint_addr >= snap_hi { | ||
| // `entrypoint_addr` is a GVA that will be loaded into RIP. For | ||
| // identity-mapped guests it falls inside the snapshot region | ||
| // `[BASE_ADDRESS, BASE_ADDRESS + snapshot_size)`. For ASLR or | ||
| // non-PIE guests the code may be mapped at a non-identity VA, |
| // Deserialized snapshots don't record the original code_virt_base; | ||
| // use 0 as a placeholder (trace_guest tests won't rely on it here). | ||
| code_virt_base: 0, |
| build-rust-guests-non-pie target=default-target: (ensure-cargo-hyperlight) | ||
| cd src/tests/rust_guests/simpleguest && cargo hyperlight build --target-dir ../target-non-pie --profile={{ if target == "debug" { "dev" } else { target } }} | ||
| {{ if os() == "windows" { "$env:RUSTC_BOOTSTRAP=1; $env:RUSTFLAGS='--sysroot=' + (Resolve-Path src/tests/rust_guests/target-non-pie/sysroot).Path + ' -C relocation-model=static -C link-args=--no-pie -C link-args=--image-base=0x1000000 --cfg=hyperlight --check-cfg=cfg(hyperlight) -Clink-args=-eentrypoint';" } else { "" } }} cd src/tests/rust_guests/simpleguest && {{ if os() == "windows" { "" } else { "RUSTC_BOOTSTRAP=1 RUSTFLAGS=\"--sysroot=$(cd .. && pwd)/target-non-pie/sysroot -C relocation-model=static -C link-args=--no-pie -C link-args=--image-base=0x1000000 --cfg=hyperlight --check-cfg=cfg(hyperlight) -Clink-args=-eentrypoint\"" } }} cargo build --target x86_64-hyperlight-none --target-dir ../target-non-pie/build --profile={{ if target == "debug" { "dev" } else { target } }} | ||
|
|
||
| non_pie_guests_target := "src/tests/rust_guests/target-non-pie/build/x86_64-hyperlight-none" |
| - name: Build non-PIE Rust guests | ||
| run: | | ||
| just build-rust-guests-non-pie ${{ inputs.config }} | ||
| just move-rust-guests-non-pie ${{ inputs.config }} | ||
|
|
09e0700 to
7f2b79e
Compare
|
How does this interact with snapshots and restoring snapshots into existing sandboxes? Is it transparent? |
78c6463 to
8c610e3
Compare
c55b6f5 to
52dae22
Compare
Add support for running non-PIE (ET_EXEC) guest binaries by mapping code at the ELF's declared virtual address rather than assuming identity mapping (physical == virtual). Changes: - Add is_pie() and base_va() methods to ExeInfo/ElfInfo to detect ET_DYN vs ET_EXEC binaries and extract the base virtual address - Add SandboxMemoryLayout::code_virt_base() to compute the correct virtual base for the code region and validate it doesn't conflict with other memory regions - Update snapshot creation to use non-identity virtual mapping for non-PIE code regions - Add non-PIE guest build step to CI (cargo hyperlight with -C relocation-model=static -C link-args=--no-pie) - Add integration test verifying non-PIE guest execution - Add test helper for locating non-PIE guest binaries Signed-off-by: cshung <3410332+cshung@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6f20a05d-6bee-4e2e-b320-12f8d9759bbc Signed-off-by: cshung <3410332+cshung@users.noreply.github.com>
Change GuestMemoryRegion::HostBaseType from () to usize so that GuestMemoryRegion becomes a proper mapping: host_region carries guest physical addresses (GPA) and guest_region carries guest virtual addresses (GVA). For identity-mapped regions both are the same. For non-PIE code the Code region's guest_region is overridden to the ELF-declared virtual address. Remove the guest_virt_addr field from MemoryRegion_ since its role is now served by the guest_region/host_region split in GuestMemoryRegion. Use checked_add for the code VA overlap check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6f20a05d-6bee-4e2e-b320-12f8d9759bbc Signed-off-by: cshung <3410332+cshung@users.noreply.github.com>
Rename get_memory_regions_ to get_memory_regions and remove the generic type parameter. All callers use GuestMemoryRegion, so the generic is unnecessary. The host_base argument is now always BASE_ADDRESS, supplied internally. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6f20a05d-6bee-4e2e-b320-12f8d9759bbc Signed-off-by: cshung <3410332+cshung@users.noreply.github.com>
Randomize the virtual base address for PIE guest code regions instead of using identity mapping. This provides address space layout randomization (ASLR) for PIE guests, making the code region virtual address unpredictable across sandbox instantiations. The random base is chosen from a page-aligned range within 47-bit canonical user space [0x1000000, max - code_size). Non-PIE binaries continue to use their declared ELF base VA. Changes: - layout.rs: code_virt_base() now randomizes VA for PIE guests and always validates against memory region conflicts - mgr.rs: thread code_virt_base through SandboxMemoryManager - snapshot/mod.rs: store code_virt_base in Snapshot, use it for relocation processing in exe_info.load() - config.rs: relax entrypoint validation to allow non-identity-mapped virtual addresses (ASLR / non-PIE) - initialized_multi_use.rs: trace_guest tests use code_virt_base instead of assuming GVA == GPA Signed-off-by: cshung <3410332+cshung@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6f20a05d-6bee-4e2e-b320-12f8d9759bbc Signed-off-by: cshung <3410332+cshung@users.noreply.github.com>
52dae22 to
d73e7e5
Compare
I believe from discussions we had in the past that this only does randomisation at the time of the elf -> initial snapshot creation, as it's quite difficult to do after that without some way to walk the heap, which is one reason I'm not sure whether it makes sense to move forward with this. |
Summary
Randomize the virtual base address for PIE guest code regions instead of using identity mapping. This provides address space layout randomization (ASLR) for PIE guests, making the code region virtual address unpredictable across sandbox instantiations.
Changes
and::rng(), choosing a page-aligned address within 47-bit canonical user space. Conflict validation now applies to both PIE and non-PIE.
Design
Testing
Dependencies
This PR is based on #1530 (non-PIE ELF loading support).