hyperlight_common/version_note.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025 The Hyperlight Authors.
3
4//! ELF note types for embedding hyperlight version metadata in guest binaries.
5//!
6//! Guest binaries built with `hyperlight-guest-bin` include a `.note.hyperlight-version`
7//! ELF note section containing the crate version they were compiled against.
8//! The host reads this section at load time to verify ABI compatibility.
9
10/// The ELF note section name used to embed the hyperlight-guest-bin version in guest binaries.
11pub const HYPERLIGHT_VERSION_SECTION: &str = ".note.hyperlight-version";
12
13/// The owner name used in the ELF note header for hyperlight version metadata.
14pub const HYPERLIGHT_NOTE_NAME: &str = "Hyperlight";
15
16/// The note type value used in the ELF note header for hyperlight version metadata.
17pub const HYPERLIGHT_NOTE_TYPE: u32 = 1;
18
19/// Size of the ELF note header (namesz + descsz + type, each u32).
20const NOTE_HEADER_SIZE: usize = 3 * size_of::<u32>();
21
22/// Compute the padded size of the name field for a 64-bit ELF note.
23///
24/// The name must be padded so that the descriptor starts at an 8-byte
25/// aligned offset from the start of the note entry:
26/// `(NOTE_HEADER_SIZE + padded_name) % 8 == 0`.
27pub const fn padded_name_size(name_len_with_nul: usize) -> usize {
28 let desc_offset = NOTE_HEADER_SIZE + name_len_with_nul;
29 let padding = (8 - (desc_offset % 8)) % 8;
30 name_len_with_nul + padding
31}
32
33/// Compute the padded size of the descriptor field for a 64-bit ELF note.
34///
35/// The descriptor must be padded so that the next note entry starts at
36/// an 8-byte aligned offset: `padded_desc % 8 == 0`.
37pub const fn padded_desc_size(desc_len_with_nul: usize) -> usize {
38 let padding = (8 - (desc_len_with_nul % 8)) % 8;
39 desc_len_with_nul + padding
40}
41
42/// An ELF note structure suitable for embedding in a `#[link_section]` static.
43///
44/// Follows the System V gABI note format as specified in
45/// <https://www.sco.com/developers/gabi/latest/ch5.pheader.html#note_section>.
46///
47/// `NAME_SZ` and `DESC_SZ` are the **padded** sizes of the name and descriptor
48/// arrays (including null terminator and alignment padding). Use
49/// [`padded_name_size`] and [`padded_desc_size`] to compute them from
50/// `str.len() + 1` (the null-terminated length).
51///
52/// The constructor enforces these constraints with compile-time assertions.
53#[repr(C, align(8))]
54pub struct ElfNote<const NAME_SZ: usize, const DESC_SZ: usize> {
55 namesz: u32,
56 descsz: u32,
57 n_type: u32,
58 // NAME_SZ includes the null terminator and padding to align `desc`
59 // to an 8-byte boundary. Must equal `padded_name_size(namesz)`.
60 // Enforced at compile time by `new()`.
61 name: [u8; NAME_SZ],
62 // DESC_SZ includes the null terminator and padding so the total
63 // note size is a multiple of 8. Must equal `padded_desc_size(descsz)`.
64 // Enforced at compile time by `new()`.
65 desc: [u8; DESC_SZ],
66}
67
68// SAFETY: ElfNote contains only plain data (`u32` and `[u8; N]`).
69// Required because ElfNote is used in a `static` (for `#[link_section]`),
70// and `static` values must be `Sync`.
71unsafe impl<const N: usize, const D: usize> Sync for ElfNote<N, D> {}
72
73impl<const NAME_SZ: usize, const DESC_SZ: usize> ElfNote<NAME_SZ, DESC_SZ> {
74 /// Create a new ELF note from a name string, descriptor string, and type.
75 ///
76 /// # Panics
77 ///
78 /// Panics at compile time if `NAME_SZ` or `DESC_SZ` don't match
79 /// `padded_name_size(name.len() + 1)` or `padded_desc_size(desc.len() + 1)`.
80 pub const fn new(name: &str, desc: &str, n_type: u32) -> Self {
81 // NAME_SZ and DESC_SZ must match the padded sizes.
82 assert!(
83 NAME_SZ == padded_name_size(name.len() + 1),
84 "NAME_SZ must equal padded_name_size(name.len() + 1)"
85 );
86 assert!(
87 DESC_SZ == padded_desc_size(desc.len() + 1),
88 "DESC_SZ must equal padded_desc_size(desc.len() + 1)"
89 );
90
91 // desc must start at an 8-byte aligned offset from the note start.
92 assert!(
93 core::mem::offset_of!(Self, desc).is_multiple_of(8),
94 "desc is not 8-byte aligned"
95 );
96
97 // Total note size must be a multiple of 8 for next-entry alignment.
98 assert!(
99 size_of::<Self>().is_multiple_of(8),
100 "total note size is not 8-byte aligned"
101 );
102
103 Self {
104 namesz: (name.len() + 1) as u32,
105 descsz: (desc.len() + 1) as u32,
106 n_type,
107 name: pad_str_to_array(name),
108 desc: pad_str_to_array(desc),
109 }
110 }
111}
112
113/// Copy a string into a zero-initialised byte array at compile time.
114const fn pad_str_to_array<const N: usize>(s: &str) -> [u8; N] {
115 let bytes = s.as_bytes();
116 let mut result = [0u8; N];
117 let mut i = 0;
118 while i < bytes.len() {
119 result[i] = bytes[i];
120 i += 1;
121 }
122 result
123}