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() {}