Skip to main content

microvms_domain/
provision.rs

1// SPDX-License-Identifier: Apache-2.0
2//! The checks over a daemon binary's bytes: is it an aarch64 ELF (#234).
3//!
4//! Fetching, caching and digest verification are `microvms_core::provision`, which
5//! re-exports these under the same names. The checks take the bytes, so the caller reads the
6//! file.
7
8use crate::error::{Error, ErrorKind};
9
10/// The `e_machine` of an aarch64 ELF (`EM_AARCH64`).
11pub const REQUIRED_ELF_MACHINE: u16 = 0xB7;
12
13/// The `e_machine` field of an ELF header, or `None` if `bytes` does not start with one.
14///
15/// Twenty bytes: the four-byte magic, `EI_DATA` at offset 5 deciding the byte order, then
16/// the two-byte `e_machine` at 18. Reading the byte order rather than assuming little is
17/// not pedantry: a big-endian binary would otherwise report machine `0xB700` and be
18/// rejected with a number nobody can look up.
19pub fn elf_machine(bytes: &[u8]) -> Option<u16> {
20    let header = bytes.get(..20)?;
21    if &header[..4] != b"\x7fELF" {
22        return None;
23    }
24    let field = [header[18], header[19]];
25    Some(if header[5] == 1 {
26        u16::from_le_bytes(field)
27    } else {
28        u16::from_be_bytes(field)
29    })
30}
31
32/// Why `bytes` is not an aarch64 ELF, or `None` when it is.
33pub fn not_aarch64(bytes: &[u8]) -> Option<String> {
34    match elf_machine(bytes) {
35        Some(REQUIRED_ELF_MACHINE) => None,
36        Some(machine) => Some(format!(
37            "ELF machine 0x{machine:x}, not aarch64 (0x{REQUIRED_ELF_MACHINE:x})"
38        )),
39        None => Some("not an ELF binary at all".to_string()),
40    }
41}
42
43/// Refuses `bytes` as an image's daemon unless they are an aarch64 ELF (#257, BIND-20).
44///
45/// `ERR_PRECONDITION`, the kind provisioning gives the same finding: nothing has reached AWS,
46/// and the fix is another file. The image's build checks what these bytes are only by running
47/// them, which is why a caller learns it here rather than from the build.
48pub fn require_aarch64(bytes: &[u8]) -> Result<(), Error> {
49    match not_aarch64(bytes) {
50        None => Ok(()),
51        Some(why) => Err(Error::new(
52            ErrorKind::Precondition,
53            format!(
54                "the daemon binary is {why}. MicroVMs are ARM64-only, and a wrong-architecture \
55                 daemon fails as a run-hook timeout 45 minutes into a build.\n  - build one: \
56                 cargo build --release -p agentd --target aarch64-unknown-linux-musl"
57            ),
58        )),
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    fn elf_header(machine: u16) -> Vec<u8> {
67        let mut header = vec![0u8; 20];
68        header[..4].copy_from_slice(b"\x7fELF");
69        header[5] = 1;
70        header[18..20].copy_from_slice(&machine.to_le_bytes());
71        header
72    }
73
74    /// The ELF parser, both byte orders, and everything shorter than a header.
75    #[test]
76    fn the_elf_header_is_read_in_its_own_byte_order() {
77        assert_eq!(elf_machine(&elf_header(0xB7)), Some(0xB7));
78        let mut big = elf_header(0);
79        big[5] = 2;
80        big[18..20].copy_from_slice(&0xB7u16.to_be_bytes());
81        assert_eq!(elf_machine(&big), Some(0xB7));
82        assert_eq!(elf_machine(b"#!/bin/sh"), None);
83        assert_eq!(elf_machine(&elf_header(0xB7)[..19]), None);
84        assert_eq!(not_aarch64(&elf_header(0xB7)), None);
85    }
86
87    #[test]
88    fn a_non_aarch64_binary_is_refused_with_its_machine() {
89        assert!(not_aarch64(&elf_header(0x3E)).is_some_and(|why| why.contains("0x3e")));
90        assert!(not_aarch64(b"#!/bin/sh").is_some_and(|why| why.contains("not an ELF")));
91    }
92
93    /// The refusal an image's daemon gets: a precondition naming what the bytes are, what a
94    /// wrong-architecture daemon costs, and the build that makes one that passes.
95    #[test]
96    fn an_images_daemon_must_be_an_aarch64_elf() {
97        assert!(require_aarch64(&elf_header(0xB7)).is_ok());
98        for (bytes, why) in [
99            (elf_header(0x3E), "ELF machine 0x3e, not aarch64 (0xb7)"),
100            (b"#!/bin/sh".to_vec(), "not an ELF binary at all"),
101            (Vec::new(), "not an ELF binary at all"),
102        ] {
103            let error = require_aarch64(&bytes).expect_err(why);
104            assert_eq!(error.kind(), ErrorKind::Precondition, "{error}");
105            let message = error.to_string();
106            assert!(message.contains(why), "{message}");
107            assert!(message.contains("ARM64-only"), "{message}");
108            assert!(
109                message.contains("--target aarch64-unknown-linux-musl"),
110                "{message}"
111            );
112        }
113    }
114}