nub_build/arch_x86.rs
1//! Cross-compile a nub bare-metal Arch guest-kernel crate for a stable
2//! bare-metal target (today: `x86_64-unknown-none`) and return the path
3//! to the resulting ELF.
4//!
5//! This is the ring-0 substrate that *hosts* the engine inside a
6//! KVM/Hyperlight sandbox — not a PVM2 program. For those, see
7//! [`crate::pvm2`].
8//!
9//! Today's only consumer is `javm-guest-x86` (the Javm personality
10//! crate over the generic `nub-arch-x86` kernel lib). As we add more
11//! bare-metal Arch backends (e.g. an arm/riscv guest), they live here
12//! too.
13//!
14//! Two simplifications versus the PVM2 recipe:
15//!
16//! 1. **No custom target JSON.** We use the upstream stable target
17//! `x86_64-unknown-none` (shipped since Rust 1.71). The
18//! `core`/`alloc`/`compiler_builtins` come pre-built; no
19//! `-Zbuild-std` needed.
20//!
21//! 2. **No C toolchain.** Hyperlight guests that link picolibc need
22//! a full cross-clang setup (which is why `cargo-hyperlight`
23//! exists). Our guests use `hyperlight-guest-bin` with
24//! `default-features = false`, dropping the picolibc dependency
25//! entirely — pure Rust, no cc-rs, no bindgen.
26//!
27//! What `cargo-hyperlight` does that we replicate as `RUSTFLAGS`:
28//!
29//! - `--cfg=hyperlight` + `--check-cfg=cfg(hyperlight)` — the
30//! hyperlight guest crates gate some code on this cfg.
31//! - `-Clink-args=-eentrypoint` — make the symbol `entrypoint` the
32//! ELF entry point.
33//!
34//! What `cargo-hyperlight` does that we skip:
35//!
36//! - Building the sysroot (`-Zbuild-std`) — unnecessary for stable
37//! `x86_64-unknown-none`.
38//! - Setting `CC_…`, `AR_…`, `CFLAGS_…` — unnecessary without C.
39
40use std::path::PathBuf;
41use std::process::Command;
42
43const TARGET_TRIPLE: &str = "x86_64-unknown-none";
44
45/// Cross-compile a hyperlight guest crate. Returns the path to the
46/// resulting ELF binary, suitable for `include_bytes!` or for
47/// passing to `hyperlight_host::GuestBinary::FilePath`.
48///
49/// `manifest_dir` is relative to the calling `build.rs`'s
50/// `CARGO_MANIFEST_DIR`. `bin_name` is the `[[bin]]` name to build.
51/// `features` is forwarded to cargo as `--features <comma-joined>`;
52/// pass `&[]` for no extras.
53///
54/// Emits `cargo:rerun-if-changed` for the guest crate's `src/` and
55/// `Cargo.toml`, plus `cargo:rerun-if-env-changed` for
56/// `SKIP_GUEST_BUILD`. Respects the `BUILD_CRATE_GUEST_BUILD` env
57/// var as a recursion guard (mirrors [`crate::pvm2`]).
58pub fn build(manifest_dir: &str, bin_name: &str, features: &[&str]) -> PathBuf {
59 let manifest_dir = build_crate::resolve_manifest_dir(manifest_dir);
60 let manifest_path = manifest_dir.join("Cargo.toml");
61
62 build_crate::emit_rerun_for_dir(&manifest_dir.join("src"));
63 println!("cargo:rerun-if-changed={}", manifest_path.display());
64 println!("cargo:rerun-if-env-changed=SKIP_GUEST_BUILD");
65
66 let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
67 let target_dir = PathBuf::from(&out_dir).join("nub-arch-guest-build");
68 let elf_path = target_dir
69 .join(TARGET_TRIPLE)
70 .join("release")
71 .join(bin_name);
72
73 if std::env::var("SKIP_GUEST_BUILD").is_ok() && elf_path.exists() {
74 return elf_path;
75 }
76
77 // Custom linker script. Adjacent to the guest crate's `src/`.
78 let link_script = manifest_dir.join("link.x");
79 let link_script_arg = format!("-Clink-args=-T{}", link_script.display());
80 // The guest is a PIE (DYN ELF) linked at VA 0. The host loader
81 // (`nub-host-kvm/src/mem/elf.rs::load_at`) patches each
82 // `R_X86_64_RELATIVE` entry with `runtime_base_va + addend`, where
83 // `runtime_base_va = guest_va_base() + KERNEL_OFFSET` from
84 // `nub-host-common::layout` — env-overridable on Linux, dynamic
85 // on macOS. So the kernel boots wherever the host reserves the
86 // per-process GUEST_VA range, no hardcoded link base required.
87 let rustflags = [
88 "--cfg=hyperlight",
89 "--check-cfg=cfg(hyperlight)",
90 "-Clink-args=-eentrypoint",
91 // Force PIE output (DYN ELF) so absolute references emit
92 // `R_X86_64_RELATIVE` entries the host can patch at load
93 // time with the runtime base GVA. Without `-pie`, lld
94 // produces an EXEC binary with statically-resolved (to 0)
95 // absolute references, which would dereference garbage at
96 // runtime once mapped at a non-zero kernel base.
97 "-Clink-args=-pie",
98 link_script_arg.as_str(),
99 // PIC (not static): the linker -pie flag produces a DYN ELF
100 // with `R_X86_64_RELATIVE` entries for absolute references;
101 // the compiler must agree by emitting PIC-style code (so
102 // text-segment relocations can be rewritten as RELATIVE).
103 "-Crelocation-model=pic",
104 // x86_64-unknown-none defaults to the `kernel` code model,
105 // which assumes the kernel sits in the high-half
106 // (`0xFFFF_FFFF_8000_0000+`) where R_X86_64_32S sign-extension
107 // does the right thing. We load the guest at a low-half VA
108 // (typically `0x5001_4000_0000`), which is too far above 2 GiB
109 // for the small/kernel models — switch to `large` to emit
110 // 64-bit absolute relocations everywhere (the linker rewrites
111 // them as `R_X86_64_RELATIVE` in the PIE output).
112 "-Ccode-model=large",
113 // Smallest valid panic strategy for no_std bin
114 "-Cpanic=abort",
115 ]
116 .join("\x1f");
117
118 let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
119 let mut cmd = Command::new(cargo);
120 cmd.arg("build")
121 .arg("--release")
122 .arg("--manifest-path")
123 .arg(&manifest_path)
124 .arg("--target")
125 .arg(TARGET_TRIPLE)
126 .arg("--bin")
127 .arg(bin_name)
128 // NOTE: enabling LTO here (`--config profile.release.lto=…`) fails:
129 // the guest forces `-Ccode-model=large` (it loads at a low-half GVA,
130 // too far above 2 GiB for the default `kernel` model), but the
131 // *precompiled* `core`/`alloc` use the target-default `kernel`
132 // model, and LTO (fat *and* thin) refuses to merge modules with
133 // conflicting `Code Model` flags (`i32 4` vs `i32 2`). Making LTO
134 // work would require rebuilding std with the matching code model via
135 // `-Zbuild-std`, i.e. a nightly toolchain (or `RUSTC_BOOTSTRAP=1`) —
136 // a departure from this crate's deliberate stable / no-build-std
137 // design. Left disabled pending that decision.
138 .env("CARGO_TARGET_DIR", &target_dir)
139 .env("BUILD_CRATE_GUEST_BUILD", "1")
140 .env("CARGO_ENCODED_RUSTFLAGS", rustflags);
141 if !features.is_empty() {
142 cmd.arg("--features").arg(features.join(","));
143 }
144
145 let output = cmd
146 .output()
147 .expect("failed to spawn cargo for nub arch guest");
148
149 if !output.status.success() {
150 let stderr = String::from_utf8_lossy(&output.stderr);
151 let stdout = String::from_utf8_lossy(&output.stdout);
152 panic!(
153 "nub arch guest build failed for {}:\n--- stderr ---\n{}\n--- stdout ---\n{}",
154 manifest_dir.display(),
155 stderr,
156 stdout
157 );
158 }
159
160 assert!(
161 elf_path.exists(),
162 "Expected ELF artifact not found at: {}",
163 elf_path.display()
164 );
165 elf_path
166}