linux_memutils/
agesa.rs

1// SPDX-FileCopyrightText: Benedikt Vollmerhaus <benedikt@vollmerhaus.org>
2// SPDX-License-Identifier: MIT
3/*!
4Utilities for finding the [AGESA] version in physical memory (on AMD Zen).
5
6# AGESA
7
8AGESA is a procedure library by AMD embedded into the UEFI firmware of AMD
9platforms up to and including Zen 5. It performs _Platform Initialization_,
10so it is responsible for CPU startup, memory training, I/O (including PCIe)
11configuration, and more.
12
13Because of AGESA's importance for stability _and security_, one may want to
14inspect its version, ideally from user space on a running system. Alas, the
15Linux kernel does not provide a straightforward interface for this; however,
16AGESA's version marker is generally located somewhere in extended memory and
17can thus be obtained via a brute-force search as implemented by this module.
18
19<div class="warning">
20
21  [Per coreboot], there are two documented iterations of AGESA:
22
23  * **v5** (or [Arch2008]) for CPU families before Zen (< `17h`)
24  * **v9** for Zen and later (≥ `17h`)
25
26  This module supports both, but **v5** was not yet comprehensively tested.
27
28</div>
29
30[AGESA]: https://en.wikipedia.org/wiki/AGESA
31[Per coreboot]: https://doc.coreboot.org/soc/amd/family17h.html#introduction
32[Arch2008]: https://www.amd.com/content/dam/amd/en/documents/processor-tech-docs/specifications/44065_Arch2008.pdf
33*/
34use std::fs::File;
35use std::io::{self, Read, Seek};
36
37use heapless::HistoryBuffer;
38use thiserror::Error;
39
40use crate::iomem::{MemoryRegion, MemoryRegionType, parse_proc_iomem};
41use crate::reader::SkippingBufReader;
42
43/// An AGESA version found in physical memory.
44#[derive(PartialEq)]
45pub struct AgesaVersion {
46    /// The complete version string (may include trailing whitespace).
47    pub version_string: String,
48    /// The absolute start address of this version in physical memory.
49    pub absolute_address: usize,
50    /// The memory region this version is located in.
51    pub surrounding_region: MemoryRegion,
52}
53
54impl AgesaVersion {
55    /// Return this version's offset within its surrounding memory region.
56    #[must_use]
57    pub fn offset_in_region(&self) -> usize {
58        self.absolute_address - self.surrounding_region.start_address
59    }
60}
61
62#[derive(Error, Debug)]
63pub enum SearchError {
64    #[error("could not open `/dev/mem`")]
65    DevMemUnopenable(#[source] io::Error),
66
67    #[error("could not read memory map from `/proc/iomem`")]
68    IomemUnreadable(#[source] io::Error),
69
70    #[error("could not read byte in physical memory from `/dev/mem`")]
71    ByteUnreadable(#[source] io::Error),
72}
73
74pub type SearchResult = Result<Option<AgesaVersion>, SearchError>;
75
76/// Search for the AGESA version within all `Reserved` memory regions.
77///
78/// # Errors
79///
80/// This function will return an error if no memory map could be obtained.
81/// It will also return errors for reading from physical memory according
82/// to [`find_agesa_version_in_memory_region`].
83pub fn find_agesa_version() -> SearchResult {
84    let possible_regions =
85        get_reserved_regions_in_extended_memory().map_err(SearchError::IomemUnreadable)?;
86
87    for region in possible_regions {
88        log::info!("Searching memory region: {region}");
89        let maybe_found_version = find_agesa_version_in_memory_region(region)?;
90        if maybe_found_version.is_some() {
91            return Ok(maybe_found_version);
92        }
93    }
94
95    Ok(None)
96}
97
98/// Search for the AGESA version within the given memory region.
99///
100/// # Errors
101///
102/// This function will return an error if `/dev/mem` could not be opened
103/// or an unexpected read error occurred during the search.
104pub fn find_agesa_version_in_memory_region(region: MemoryRegion) -> SearchResult {
105    let file = File::open("/dev/mem").map_err(SearchError::DevMemUnopenable)?;
106    let buf_reader = SkippingBufReader::new(file, region.start_address, Some(region.end_address));
107
108    if let Some((version_string, absolute_address)) = find_agesa_version_in_reader(buf_reader)? {
109        return Ok(Some(AgesaVersion {
110            version_string,
111            absolute_address,
112            surrounding_region: region,
113        }));
114    }
115
116    Ok(None)
117}
118
119/// The possible states of an ongoing search.
120enum SearchState {
121    Searching,
122    SignatureFound,
123    VersionStartFound,
124}
125
126/// The signature indicating the start of an AGESA v9 version in memory.
127const SIGNATURE_V9: &[u8] = b"AGESA!V";
128const SIGNATURE_LENGTH: usize = SIGNATURE_V9.len();
129
130/// The signature indicating the start of an AGESA v5 version in memory.
131///
132/// Per the Arch2008 spec, this is should be `!!AGESA `; however, on the
133/// [Kaveri platform] I tested, it is `!!!AGESA` immediately followed by
134/// the version string. The specified prefix should work for both cases.
135///
136/// [Kaveri platform]: https://github.com/fishbaoz/KaveriPI/blob/master/AGESA/AMD.h#L260
137const SIGNATURE_V5: &[u8; SIGNATURE_LENGTH] = b"!!AGESA";
138
139/// Search for the AGESA version within the given buffered reader.
140///
141/// This returns the found version string and its offset in `buf_reader`.
142///
143/// # Errors
144///
145/// This function will return an error if a byte in the reader could not
146/// be read (except in case of already-handled permission errors).
147pub fn find_agesa_version_in_reader<R: Read + Seek>(
148    mut buf_reader: SkippingBufReader<R>,
149) -> Result<Option<(String, usize)>, SearchError> {
150    let mut version_string = Vec::new();
151
152    let mut search_state = SearchState::Searching;
153    let mut search_window: HistoryBuffer<u8, SIGNATURE_LENGTH> = HistoryBuffer::new();
154
155    let mut buffer = [0; 1024];
156    loop {
157        let bytes_read = buf_reader
158            .read(&mut buffer)
159            .map_err(SearchError::ByteUnreadable)?;
160        if bytes_read == 0 {
161            break;
162        }
163
164        for (i, &byte) in buffer[..bytes_read].iter().enumerate() {
165            match search_state {
166                SearchState::Searching => {
167                    search_window.write(byte);
168
169                    if search_window.oldest_ordered().eq(SIGNATURE_V9) {
170                        // AGESA!V9␀CezannePI-FP6 1.0.1.1␀
171                        //       ^
172                        search_state = SearchState::SignatureFound;
173                    } else if search_window.oldest_ordered().eq(SIGNATURE_V5) {
174                        // !!!AGESAKaveriPI        V1.1.0.7    ␀
175                        //        ^
176                        // For AGESA v5, the version string starts right after
177                        // the signature, so there is no null byte to skip to
178                        search_state = SearchState::VersionStartFound;
179                    }
180                }
181                SearchState::SignatureFound => {
182                    if byte == b'\0' {
183                        // AGESA!V9␀CezannePI-FP6 1.0.1.1␀
184                        //         ^
185                        search_state = SearchState::VersionStartFound;
186                    }
187                }
188                SearchState::VersionStartFound if byte == b'\0' => {
189                    // AGESA!V9␀CezannePI-FP6 1.0.1.1␀
190                    //                               ^
191                    let trimmed_version = version_string.trim_ascii_start();
192                    let current_offset = buf_reader.position_in_file() - (bytes_read - i);
193                    let version_offset = current_offset - trimmed_version.len();
194
195                    return Ok(Some((
196                        String::from_utf8_lossy(trimmed_version).into(),
197                        version_offset,
198                    )));
199                }
200                SearchState::VersionStartFound => {
201                    version_string.push(byte);
202                }
203            }
204        }
205    }
206
207    Ok(None)
208}
209
210/// The start address of extended memory.
211const EXTENDED_MEM_START: usize = 0x0000_0000_0010_0000;
212
213/// Find and return all `Reserved` regions in [extended memory] (> 1 MiB).
214///
215/// Testing on a few machines showed that at least one `Reserved` region in
216/// extended memory reliably contains the AGESA version – usually the first
217/// one at that. Even `Usable` regions may occasionally include it, but the
218/// initial (generally small) `Reserved` regions are much faster to search.
219///
220/// [extended memory]: https://wiki.osdev.org/Memory_Map_(x86)#Extended_Memory_(%3E_1_MiB)
221///
222/// # Errors
223///
224/// This function will return an error if `/proc/iomem` could not be read.
225pub fn get_reserved_regions_in_extended_memory() -> io::Result<Vec<MemoryRegion>> {
226    let all_regions = parse_proc_iomem()?;
227    let reserved_high_mem_regions: Vec<MemoryRegion> = all_regions
228        .into_iter()
229        .filter(|r| r.region_type == MemoryRegionType::Reserved)
230        .filter(|r| r.start_address >= EXTENDED_MEM_START)
231        .collect();
232
233    Ok(reserved_high_mem_regions)
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    mod found_version {
241        use super::*;
242
243        #[test]
244        fn offset_in_region_returns_expected_offset() {
245            let version = AgesaVersion {
246                version_string: "CezannePI-FP6 1.0.1.1".into(),
247                absolute_address: 20,
248                surrounding_region: MemoryRegion {
249                    start_address: 5,
250                    end_address: 100,
251                    region_type: MemoryRegionType::Reserved,
252                },
253            };
254            assert_eq!(version.offset_in_region(), 15);
255        }
256    }
257
258    mod find_agesa_version_in_reader {
259        use super::*;
260        use indoc::formatdoc;
261        use rstest::rstest;
262        use std::io::Cursor;
263
264        #[rstest]
265        #[case::agesa_v9_signature(
266            "AGESA!V9\0CezannePI-FP6 1.0.1.1\0",
267            "CezannePI-FP6 1.0.1.1",
268            37
269        )]
270        #[case::agesa_v5_signature_arch2008(
271            "!!AGESA KaveriPI        V1.1.0.7    \0",
272            "KaveriPI        V1.1.0.7    ",
273            36
274        )]
275        #[case::agesa_v5_signature_alternative(
276            "!!!AGESAKaveriPI        V1.1.0.7    \0",
277            "KaveriPI        V1.1.0.7    ",
278            36
279        )]
280        fn returns_expected_version_string_and_absolute_address(
281            #[case] version_in_memory: String,
282            #[case] expected_version_string: String,
283            #[case] expected_absolute_address: usize,
284        ) {
285            let file = Cursor::new(formatdoc! {"
286                PreceedingUnrelated\0Bytes%p
287                {version_in_memory}
288                \0SubsequentUnrelatedBytes\0
289            "});
290            let buf_reader = SkippingBufReader::new(file, 0, None);
291
292            let result = find_agesa_version_in_reader(buf_reader).unwrap();
293            assert_eq!(
294                result,
295                Some((expected_version_string, expected_absolute_address))
296            );
297        }
298
299        #[test]
300        fn returns_none_if_no_agesa_signature_is_present() {
301            let file = Cursor::new(b"AESA!V9\0CezannePI-FP6 1.0.1.1\0");
302            let buf_reader = SkippingBufReader::new(file, 0, None);
303
304            let result = find_agesa_version_in_reader(buf_reader).unwrap();
305            assert_eq!(result, None);
306        }
307
308        #[test]
309        fn returns_none_if_version_string_does_not_end() {
310            let file = Cursor::new(b"AGESA!V9\0CezannePI-FP6 1.0.1.1");
311            let buf_reader = SkippingBufReader::new(file, 0, None);
312
313            let result = find_agesa_version_in_reader(buf_reader).unwrap();
314            assert_eq!(result, None);
315        }
316    }
317}