Skip to main content

nub_arch_x86/
main.rs

1//! Nub Arch implementation for Hyperlight: a bare-metal guest binary
2//! that runs the PVM in-kernel JIT path on real CPU + MMU.
3//!
4//! Built with `nub-build` → `cargo build --target=x86_64-unknown-none`.
5//! Links against `nub-arch-guestbin` (our forked + trimmed
6//! `hyperlight-guest-bin`). Entry point is `entrypoint` (supplied by
7//! guestbin), which initialises the heap + GDT + IDT then calls
8//! `hyperlight_main`. We don't define `hyperlight_main` ourselves; the
9//! weak default in guestbin is fine.
10//!
11//! Guest functions are registered via `#[guest_function(fn_id = N)]`
12//! from `nub-host-guest-macro`, which slots them into a `linkme`
13//! distributed-slice (`GUEST_FUNCTION_TABLE`) at compile time. The
14//! host invokes them by `fn_id` via Hyperlight's `OUT`-port +
15//! shared-memory function-call ABI, with rkyv-encoded
16//! `Request` / `Response` envelopes.
17//!
18//! On host targets (target_os != "none") this crate compiles to a
19//! trivial empty `main` so `cargo build --workspace` succeeds without
20//! dragging Hyperlight-guest deps onto host platforms. Only
21//! `cargo build --target=x86_64-unknown-none` produces a real guest
22//! ELF.
23
24#![cfg_attr(target_os = "none", no_std)]
25#![cfg_attr(target_os = "none", no_main)]
26
27#[cfg(target_os = "none")]
28extern crate alloc;
29#[cfg(target_os = "none")]
30extern crate hyperlight_guest_bin;
31
32#[cfg(target_os = "none")]
33mod call_loop;
34#[cfg(target_os = "none")]
35mod jit_cache;
36#[cfg(target_os = "none")]
37mod jit_run;
38#[cfg(target_os = "none")]
39mod page_alloc;
40#[cfg(target_os = "none")]
41mod paging;
42#[cfg(target_os = "none")]
43mod ring3;
44#[cfg(target_os = "none")]
45mod segments;
46#[cfg(target_os = "none")]
47mod state_cache;
48
49#[cfg(target_os = "none")]
50mod guest {
51    use alloc::vec::Vec;
52    use hyperlight_guest_bin::guest_function;
53    use javm_cap::cap::Cap;
54    use javm_cap::wire::WireCap;
55    #[cfg(feature = "heap-diag")]
56    use nub_arch_x86_abi::FN_ID_NUB_HEAP_STATS;
57    use nub_arch_x86_abi::{
58        BootInfo, FN_ID_NUB_GET_BOOT_INFO, FN_ID_NUB_INVOKE_CACHED, FN_ID_NUB_PUT_CAP,
59        FN_ID_NUB_SMOKE, InvocationResult, InvokePacket,
60    };
61
62    /// Skeleton stand-in for the `Nub::invoke` RPC. The host's
63    /// `Nub::new_hyperlight().invoke(...)` calls into this; returns 42
64    /// to match `nub_arch_local::LocalArch`'s stubbed return value so
65    /// both backends look identical to the test harness.
66    #[guest_function(fn_id = FN_ID_NUB_SMOKE)]
67    pub fn nub_smoke(_input: &[u8]) -> Vec<u8> {
68        let v: u64 = 42;
69        rkyv::to_bytes::<rkyv::rancor::Error>(&v)
70            .expect("rkyv-encode u64")
71            .into_vec()
72    }
73
74    fn encode_result_error(exit_arg: u32) -> Vec<u8> {
75        let result = InvocationResult {
76            exit_reason: u32::MAX,
77            exit_arg,
78            return_value: 0,
79            gas_remaining: 0,
80        };
81        rkyv::to_bytes::<rkyv::rancor::Error>(&result)
82            .expect("rkyv-encode InvocationResult error")
83            .into_vec()
84    }
85
86    /// Cache-based RPC: read an `InvokePacket`, drive the in-kernel
87    /// CALL/HALT loop ([`crate::call_loop`]) — which spins up frames,
88    /// dispatches `derive_spawn` + `host_call` in-sandbox, and tears
89    /// each frame down on HALT.
90    ///
91    /// Memory regions live behind the per-invocation page-table for the
92    /// duration of one JIT entry; the call loop builds them fresh on
93    /// every push.
94    #[guest_function(fn_id = FN_ID_NUB_INVOKE_CACHED)]
95    pub fn nub_invoke_cached(packet_bytes: &[u8]) -> Vec<u8> {
96        let packet = match InvokePacket::from_bytes(packet_bytes) {
97            Some(p) => p,
98            None => return encode_result_error(10),
99        };
100
101        // Caps are resolved via the heap-resident `CACHE`
102        // (`CacheDirectory<FixedState>`) — see `crate::state_cache`.
103        let outcome = crate::call_loop::run_top(
104            &packet.instance_hash,
105            packet.endpoint_idx,
106            packet.args,
107            packet.initial_gas as i64,
108        );
109
110        // GC the transient instance entries that `derive_spawn`
111        // created during this RPC. By now `run_top` has dropped the
112        // call stack, so every frame's `CapHashOrRef::Ref(CapRef)`
113        // clone is gone — the only holder of each transient instance
114        // is the directory's own self-ref. `sweep_instances` walks
115        // the instances tier and removes entries where
116        // `Arc::strong_count(self_ref) == 1`, looping until stable.
117        // Without this, the bench's `sub_vm_data_recurse` OOMs the
118        // guest's talc heap within seconds.
119        crate::state_cache::CACHE.sweep_instances();
120
121        let result = match outcome {
122            Ok(o) => InvocationResult {
123                exit_reason: o.exit_reason,
124                exit_arg: o.exit_arg,
125                return_value: o.return_value,
126                gas_remaining: o.gas_remaining.max(0) as u64,
127            },
128            Err(code) => InvocationResult {
129                exit_reason: u32::MAX,
130                exit_arg: code,
131                return_value: 0,
132                gas_remaining: 0,
133            },
134        };
135
136        rkyv::to_bytes::<rkyv::rancor::Error>(&result)
137            .expect("rkyv-encode InvocationResult")
138            .into_vec()
139    }
140
141    /// Heap-resident cap-directory publisher. Decodes the
142    /// rkyv-archived [`WireCap`] payload, converts it back into a
143    /// [`Cap`], and inserts it into [`crate::state_cache::CACHE`] via
144    /// [`javm_cap::cache::CacheDirectory::put_cap`].
145    ///
146    /// On any decode/conversion failure we return a sentinel
147    /// `CapHash` of all-`0xFF`. The host's `MultiUseSandbox::put_cap`
148    /// helper compares against this sentinel and surfaces a typed
149    /// error.
150    #[guest_function(fn_id = FN_ID_NUB_PUT_CAP)]
151    pub fn nub_put_cap(payload: &[u8]) -> Vec<u8> {
152        // Lazy first-call boot-info patch. `init_directory_va` is
153        // idempotent + cheap; nicer than wiring a custom
154        // `hyperlight_main` for just this one publication.
155        crate::state_cache::init_directory_va();
156
157        let mut aligned = rkyv::util::AlignedVec::<16>::with_capacity(payload.len());
158        aligned.extend_from_slice(payload);
159
160        let wire: WireCap = match rkyv::from_bytes::<WireCap, rkyv::rancor::Error>(&aligned) {
161            Ok(w) => w,
162            Err(_) => return error_hash_sentinel(),
163        };
164        let cap: Cap = match wire.into_cap() {
165            Ok(c) => c,
166            Err(_) => return error_hash_sentinel(),
167        };
168        let hash = match crate::state_cache::CACHE.put_cap(&cap) {
169            Ok(h) => h,
170            Err(_) => return error_hash_sentinel(),
171        };
172        let mut out: Vec<u8> = Vec::with_capacity(32);
173        out.extend_from_slice(&hash);
174        out
175    }
176
177    /// Read the current `BootInfo` block out as raw bytes. Used by
178    /// the host as a fallback when ELF-section lookup fails. Payload
179    /// is empty.
180    #[guest_function(fn_id = FN_ID_NUB_GET_BOOT_INFO)]
181    pub fn nub_get_boot_info(_input: &[u8]) -> Vec<u8> {
182        // Patch the VA on first read if it wasn't already published.
183        crate::state_cache::init_directory_va();
184
185        // SAFETY: `BOOT_INFO` is `static mut`; we read it after the
186        // init hook above ran, and we publish bytes out via a fresh
187        // copy. Reads of a freshly-patched `directory_va` field are
188        // safe in this single-threaded boot context.
189        let info: BootInfo = unsafe {
190            let p = &raw const crate::state_cache::BOOT_INFO;
191            core::ptr::read(p)
192        };
193        let bytes = unsafe {
194            core::slice::from_raw_parts(
195                &info as *const BootInfo as *const u8,
196                core::mem::size_of::<BootInfo>(),
197            )
198        };
199        bytes.to_vec()
200    }
201
202    /// `nub_put_cap` failure sentinel — a `CapHash` of all `0xFF`. No
203    /// real cap hashes to this value (SSZ root + Union mix-in
204    /// selector mean a content hash collides with all-ones only
205    /// with negligible probability), so the host can use equality
206    /// against this constant as a reliable error flag.
207    fn error_hash_sentinel() -> Vec<u8> {
208        alloc::vec![0xFFu8; 32]
209    }
210
211    /// Diagnostic: report talc's current allocation state as 32 LE
212    /// bytes packing `[allocated_bytes, allocation_count,
213    /// fragment_count, available_bytes]` (four u64s). Used to detect
214    /// per-iter heap leaks — `allocated_bytes` growing monotonically
215    /// indicates a real leak; `allocated_bytes` oscillating with
216    /// `fragment_count` climbing indicates fragmentation.
217    ///
218    /// Gated on `heap-diag` because reading the counters requires
219    /// talc's `counters` feature, which adds a small per-alloc cost.
220    #[cfg(feature = "heap-diag")]
221    #[guest_function(fn_id = FN_ID_NUB_HEAP_STATS)]
222    pub fn nub_heap_stats(_input: &[u8]) -> Vec<u8> {
223        let counters = hyperlight_guest_bin::HEAP_ALLOCATOR
224            .lock()
225            .counters()
226            .clone();
227        let mut buf = alloc::vec![0u8; 32];
228        buf[0..8].copy_from_slice(&(counters.allocated_bytes as u64).to_le_bytes());
229        buf[8..16].copy_from_slice(&(counters.allocation_count as u64).to_le_bytes());
230        buf[16..24].copy_from_slice(&(counters.fragment_count as u64).to_le_bytes());
231        buf[24..32].copy_from_slice(&(counters.available_bytes as u64).to_le_bytes());
232        buf
233    }
234}
235
236/// Empty `main` so `cargo build --workspace` (host target) succeeds
237/// without including any of the bare-metal guest code. The real entry
238/// point on `x86_64-unknown-none` is `entrypoint` from the linked
239/// guestbin.
240#[cfg(not(target_os = "none"))]
241fn main() {}