Skip to main content

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}