hcl_compat_uefi_nvram_storage/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! HCL-compatible UEFI nvram variable storage format.
5//!
6//! Stores Nvram variables as a _packed_ byte-buffer of structs + associated
7//! variable length data, in the same format as the earlier Microsoft HCL
8//! versions.
9//!
10//! # A brief comment about the data representation
11//!
12//! Because variables are stored in the buffer back-to-back with no padding, the
13//! UTF-16 encoded `name` field is _not_ guaranteed to be properly aligned,
14//! which means it's invalid to reference it as a `&[u16]`, or any similar
15//! wrapper type (e.g: `widestring::U16CStr`).
16
17#![forbid(unsafe_code)]
18
19pub mod storage_backend;
20
21use cvm_tracing::CVM_ALLOWED;
22use cvm_tracing::CVM_CONFIDENTIAL;
23use guid::Guid;
24use std::fmt::Debug;
25use storage_backend::StorageBackend;
26use ucs2::Ucs2LeSlice;
27use uefi_nvram_storage::EFI_TIME;
28use uefi_nvram_storage::NextVariable;
29use uefi_nvram_storage::NvramStorage;
30use uefi_nvram_storage::NvramStorageError;
31use uefi_nvram_storage::in_memory;
32use zerocopy::FromBytes;
33use zerocopy::Immutable;
34use zerocopy::IntoBytes;
35use zerocopy::KnownLayout;
36
37const EFI_MAX_VARIABLE_NAME_SIZE: usize = 2 * 1024;
38const EFI_MAX_VARIABLE_DATA_SIZE: usize = 32 * 1024;
39
40// Max size allows two re-sizings, max size of 128K
41// TODO: how big required for secure boot with db/dbx?
42const INITIAL_NVRAM_SIZE: usize = 32768;
43const MAXIMUM_NVRAM_SIZE: usize = INITIAL_NVRAM_SIZE * 4;
44
45mod format {
46    use super::*;
47    use open_enum::open_enum;
48    use static_assertions::const_assert_eq;
49
50    open_enum! {
51        #[derive(IntoBytes, Immutable, KnownLayout, FromBytes)]
52        pub enum NvramHeaderType: u32 {
53            VARIABLE = 0,
54        }
55    }
56
57    #[repr(C)]
58    #[derive(Copy, Clone, Debug, IntoBytes, Immutable, KnownLayout, FromBytes)]
59    pub struct NvramHeader {
60        pub header_type: NvramHeaderType,
61        pub length: u32, // Total length of the variable, in bytes. Includes the header.
62    }
63
64    const_assert_eq!(8, size_of::<NvramHeader>());
65
66    #[repr(C)]
67    #[derive(Copy, Clone, Debug, IntoBytes, Immutable, KnownLayout, FromBytes)]
68    pub struct NvramVariable {
69        pub header: NvramHeader, // Set to type NvramVariable
70        pub attributes: u32,
71        pub timestamp: EFI_TIME, // Only used by authenticated variables
72        pub vendor: Guid,
73        pub name_bytes: u16, // max name size of 2K, in _bytes_ not number of characters
74        pub data_bytes: u16, // max data size of 32K
75                             // std::uint16_t Name[];
76                             // std::uint8_t Data[]; // Follows after Name.
77    }
78    const_assert_eq!(48, size_of::<NvramVariable>());
79}
80
81/// Stores Nvram variables in files as a _packed_ byte-buffer of structs +
82/// associated variable length data.
83#[cfg_attr(feature = "inspect", derive(inspect::Inspect))]
84pub struct HclCompatNvram<S> {
85    quirks: HclCompatNvramQuirks,
86
87    #[cfg_attr(feature = "inspect", inspect(skip))]
88    storage: S,
89
90    in_memory: in_memory::InMemoryNvram,
91
92    // reuse the same allocation for the nvram_buf, trading off steady-state
93    // memory usage for a more consistent (albeit larger) memory footprint, and
94    // reduced allocator pressure
95    #[cfg_attr(feature = "inspect", inspect(skip))] // internal bookkeeping - not worth inspecting
96    nvram_buf: Vec<u8>,
97
98    // whether the NVRAM has been loaded, either from storage or saved state
99    loaded: bool,
100}
101
102/// "Quirks" to take into account when loading/storing nvram blob data.
103#[cfg_attr(feature = "inspect", derive(inspect::Inspect))]
104pub struct HclCompatNvramQuirks {
105    /// When loading nvram variables from storage, don't fail the entire load
106    /// process when encountering variables that are missing null terminators in
107    /// their name. Instead, skip loading any such variables, and continue on
108    /// with the load.
109    ///
110    /// # Context
111    ///
112    /// Due to a (now fixed) bug in a previous version of Microsoft HCL, it was
113    /// possible for non-null-terminated nvram variables to slip-through
114    /// validation and get persisted to disk.
115    ///
116    /// Enabling this quirk will allow "salvaging" the rest of the non-corrupt
117    /// nvram variables, which may be preferable over having the VM fail to boot
118    /// at all.
119    pub skip_corrupt_vars_with_missing_null_term: bool,
120}
121
122impl<S: StorageBackend> HclCompatNvram<S> {
123    /// Create a new [`HclCompatNvram`]
124    pub fn new(storage: S, quirks: Option<HclCompatNvramQuirks>) -> Self {
125        Self {
126            quirks: quirks.unwrap_or(HclCompatNvramQuirks {
127                skip_corrupt_vars_with_missing_null_term: false,
128            }),
129
130            storage,
131
132            in_memory: in_memory::InMemoryNvram::new(),
133
134            nvram_buf: Vec::new(),
135
136            loaded: false,
137        }
138    }
139
140    async fn lazy_load_from_storage(&mut self) -> Result<(), NvramStorageError> {
141        let res = self.lazy_load_from_storage_inner().await;
142        if let Err(e) = &res {
143            tracing::error!(CVM_ALLOWED, "storage contains corrupt nvram state");
144            tracing::error!(
145                CVM_CONFIDENTIAL,
146                error = e as &dyn std::error::Error,
147                "storage contains corrupt nvram state"
148            );
149        }
150        res
151    }
152
153    async fn lazy_load_from_storage_inner(&mut self) -> Result<(), NvramStorageError> {
154        if self.loaded {
155            return Ok(());
156        }
157
158        let nvram_buf = self
159            .storage
160            .restore()
161            .await
162            .map_err(|e| NvramStorageError::Load(e.into()))?
163            .unwrap_or_default();
164
165        if nvram_buf.len() > MAXIMUM_NVRAM_SIZE {
166            return Err(NvramStorageError::Load(
167                format!(
168                    "Existing nvram state exceeds MAXIMUM_NVRAM_SIZE ({} > {})",
169                    nvram_buf.len(),
170                    MAXIMUM_NVRAM_SIZE
171                )
172                .into(),
173            ));
174        }
175
176        // load state into memory
177        self.in_memory.clear();
178        self.nvram_buf = nvram_buf;
179        let mut buf = self.nvram_buf.as_slice();
180        // TODO: zerocopy: error propagation (https://github.com/microsoft/openvmm/issues/759)
181        while let Ok((header, _)) = format::NvramHeader::read_from_prefix(buf) {
182            if buf.len() < header.length as usize {
183                return Err(NvramStorageError::Load(
184                    format!(
185                        "unexpected EOF. expected at least {} more bytes, but only found {}",
186                        header.length,
187                        buf.len()
188                    )
189                    .into(),
190                ));
191            }
192
193            let entry_buf = {
194                let (entry_buf, remaining) = buf.split_at(header.length as usize);
195                buf = remaining;
196                entry_buf
197            };
198
199            match header.header_type {
200                format::NvramHeaderType::VARIABLE => {}
201                _ => {
202                    return Err(NvramStorageError::Load(
203                        format!("unknown header type: {:?}", header.header_type).into(),
204                    ));
205                }
206            }
207
208            // validation check above ensures that at this point, entry_buf
209            // corresponds to a VARIABLE entry
210
211            let (var_header, var_name, var_data) = {
212                // TODO: zerocopy: error propagation (https://github.com/microsoft/openvmm/issues/759)
213                // TODO: zerocopy: manual fix - review carefully! (https://github.com/microsoft/openvmm/issues/759)
214                let (var_header, var_length_data) =
215                    format::NvramVariable::read_from_prefix(entry_buf)
216                        .map_err(|_| NvramStorageError::Load("variable entry too short".into()))?;
217
218                if var_length_data.len()
219                    != var_header.name_bytes as usize + var_header.data_bytes as usize
220                {
221                    return Err(NvramStorageError::Load(
222                        "mismatch between header length and variable data size".into(),
223                    ));
224                }
225
226                let (var_name, var_data) = var_length_data.split_at(var_header.name_bytes as usize);
227
228                (var_header, var_name, var_data)
229            };
230
231            if var_name.len() > EFI_MAX_VARIABLE_NAME_SIZE {
232                return Err(NvramStorageError::Load(
233                    format!(
234                        "variable name too big. {} > {}",
235                        var_name.len(),
236                        EFI_MAX_VARIABLE_NAME_SIZE
237                    )
238                    .into(),
239                ));
240            }
241
242            if var_data.len() > EFI_MAX_VARIABLE_DATA_SIZE {
243                return Err(NvramStorageError::Load(
244                    format!(
245                        "variable data too big. {} > {}",
246                        var_data.len(),
247                        EFI_MAX_VARIABLE_DATA_SIZE
248                    )
249                    .into(),
250                ));
251            }
252
253            let name = match Ucs2LeSlice::from_slice_with_nul(var_name) {
254                Ok(name) => name,
255                Err(e) => {
256                    if self.quirks.skip_corrupt_vars_with_missing_null_term {
257                        let var = {
258                            let mut var = var_name.to_vec();
259                            var.push(0);
260                            var.push(0);
261                            ucs2::Ucs2LeVec::from_vec_with_nul(var)
262                        };
263                        tracing::warn!(
264                            CVM_ALLOWED,
265                            "skipping corrupt nvram var (missing null term)"
266                        );
267                        tracing::warn!(
268                            CVM_CONFIDENTIAL,
269                            ?var,
270                            "skipping corrupt nvram var (missing null term)"
271                        );
272                        continue;
273                    } else {
274                        return Err(NvramStorageError::Load(e.into()));
275                    }
276                }
277            };
278
279            self.in_memory
280                .set_variable(
281                    name,
282                    var_header.vendor,
283                    var_header.attributes,
284                    var_data.to_vec(),
285                    var_header.timestamp,
286                )
287                .await?;
288        }
289
290        if !buf.is_empty() {
291            return Err(NvramStorageError::Load(
292                "existing nvram state contains excess data".into(),
293            ));
294        }
295
296        self.loaded = true;
297        Ok(())
298    }
299
300    /// Dump in-memory nvram to the underlying storage device.
301    async fn flush_storage(&mut self) -> Result<(), NvramStorageError> {
302        self.nvram_buf.clear();
303
304        for in_memory::VariableEntry {
305            vendor,
306            name,
307            data,
308            timestamp,
309            attr,
310        } in self.in_memory.iter()
311        {
312            self.nvram_buf.extend_from_slice(
313                format::NvramVariable {
314                    header: format::NvramHeader {
315                        header_type: format::NvramHeaderType::VARIABLE,
316                        length: (size_of::<format::NvramVariable>()
317                            + name.as_bytes().len()
318                            + data.len()) as u32,
319                    },
320                    attributes: attr,
321                    timestamp,
322                    vendor,
323                    name_bytes: name.as_bytes().len() as u16,
324                    data_bytes: data.len() as u16,
325                }
326                .as_bytes(),
327            );
328            self.nvram_buf.extend_from_slice(name.as_bytes());
329            self.nvram_buf.extend_from_slice(data);
330        }
331
332        // callers make sure that any operations that add/append to vars will
333        // not result in file size exceeding MAXIMUM_NVRAM_SIZE
334        assert!(self.nvram_buf.len() < MAXIMUM_NVRAM_SIZE);
335
336        self.storage
337            .persist(self.nvram_buf.clone())
338            .await
339            .map_err(|e| NvramStorageError::Commit(e.into()))?;
340
341        Ok(())
342    }
343
344    /// Iterate over the NVRAM entries. This function asynchronously loads the
345    /// NVRAM contents into memory from the backing storage if necessary.
346    pub async fn iter(
347        &mut self,
348    ) -> Result<impl Iterator<Item = in_memory::VariableEntry<'_>>, NvramStorageError> {
349        self.lazy_load_from_storage().await?;
350        Ok(self.in_memory.iter())
351    }
352}
353
354#[async_trait::async_trait]
355impl<S: StorageBackend> NvramStorage for HclCompatNvram<S> {
356    async fn get_variable(
357        &mut self,
358        name: &Ucs2LeSlice,
359        vendor: Guid,
360    ) -> Result<Option<(u32, Vec<u8>, EFI_TIME)>, NvramStorageError> {
361        self.lazy_load_from_storage().await?;
362
363        if name.as_bytes().len() > EFI_MAX_VARIABLE_NAME_SIZE {
364            return Err(NvramStorageError::VariableNameTooLong);
365        }
366
367        self.in_memory.get_variable(name, vendor).await
368    }
369
370    async fn set_variable(
371        &mut self,
372        name: &Ucs2LeSlice,
373        vendor: Guid,
374        attr: u32,
375        data: Vec<u8>,
376        timestamp: EFI_TIME,
377    ) -> Result<(), NvramStorageError> {
378        self.lazy_load_from_storage().await?;
379
380        if name.as_bytes().len() > EFI_MAX_VARIABLE_NAME_SIZE {
381            return Err(NvramStorageError::VariableNameTooLong);
382        }
383
384        if data.len() > EFI_MAX_VARIABLE_DATA_SIZE {
385            return Err(NvramStorageError::VariableDataTooLong);
386        }
387
388        // don't overshoot MAXIMUM_NVRAM_SIZE
389        {
390            let new_file_size = match self.in_memory.get_variable(name, vendor).await? {
391                Some((_, existing_data, _)) => {
392                    self.nvram_buf.len() - existing_data.len() + data.len()
393                }
394                None => {
395                    self.nvram_buf.len()
396                        + name.as_bytes().len()
397                        + data.len()
398                        + size_of::<format::NvramVariable>()
399                }
400            };
401
402            if new_file_size > MAXIMUM_NVRAM_SIZE {
403                return Err(NvramStorageError::OutOfSpace);
404            }
405        }
406
407        self.in_memory
408            .set_variable(name, vendor, attr, data, timestamp)
409            .await?;
410        self.flush_storage().await?;
411
412        Ok(())
413    }
414
415    async fn append_variable(
416        &mut self,
417        name: &Ucs2LeSlice,
418        vendor: Guid,
419        data: Vec<u8>,
420        timestamp: EFI_TIME,
421    ) -> Result<bool, NvramStorageError> {
422        self.lazy_load_from_storage().await?;
423
424        if name.as_bytes().len() > EFI_MAX_VARIABLE_NAME_SIZE {
425            return Err(NvramStorageError::VariableNameTooLong);
426        }
427
428        if let Some((_, existing_data, _)) = self.in_memory.get_variable(name, vendor).await? {
429            if existing_data.len() + data.len() > EFI_MAX_VARIABLE_DATA_SIZE {
430                return Err(NvramStorageError::VariableDataTooLong);
431            }
432
433            let new_file_size = self.nvram_buf.len() + data.len();
434
435            if new_file_size > MAXIMUM_NVRAM_SIZE {
436                return Err(NvramStorageError::OutOfSpace);
437            }
438        }
439
440        let found = self
441            .in_memory
442            .append_variable(name, vendor, data, timestamp)
443            .await?;
444        self.flush_storage().await?;
445
446        Ok(found)
447    }
448
449    async fn remove_variable(
450        &mut self,
451        name: &Ucs2LeSlice,
452        vendor: Guid,
453    ) -> Result<bool, NvramStorageError> {
454        self.lazy_load_from_storage().await?;
455
456        if name.as_bytes().len() > EFI_MAX_VARIABLE_NAME_SIZE {
457            return Err(NvramStorageError::VariableNameTooLong);
458        }
459
460        let removed = self.in_memory.remove_variable(name, vendor).await?;
461        self.flush_storage().await?;
462
463        Ok(removed)
464    }
465
466    async fn next_variable(
467        &mut self,
468        name_vendor: Option<(&Ucs2LeSlice, Guid)>,
469    ) -> Result<NextVariable, NvramStorageError> {
470        self.lazy_load_from_storage().await?;
471
472        if let Some((name, _)) = name_vendor {
473            if name.as_bytes().len() > EFI_MAX_VARIABLE_NAME_SIZE {
474                return Err(NvramStorageError::VariableNameTooLong);
475            }
476        }
477
478        self.in_memory.next_variable(name_vendor).await
479    }
480}
481
482#[cfg(feature = "save_restore")]
483mod save_restore {
484    use super::*;
485    use vmcore::save_restore::RestoreError;
486    use vmcore::save_restore::SaveError;
487    use vmcore::save_restore::SaveRestore;
488
489    impl<S: StorageBackend> SaveRestore for HclCompatNvram<S> {
490        type SavedState = <in_memory::InMemoryNvram as SaveRestore>::SavedState;
491
492        fn save(&mut self) -> Result<Self::SavedState, SaveError> {
493            self.in_memory.save()
494        }
495
496        fn restore(&mut self, state: Self::SavedState) -> Result<(), RestoreError> {
497            if state.nvram.is_some() {
498                self.in_memory.restore(state)?;
499                self.loaded = true;
500            }
501            Ok(())
502        }
503    }
504}
505
506#[cfg(test)]
507mod test {
508    use super::storage_backend::StorageBackend;
509    use super::storage_backend::StorageBackendError;
510    use super::*;
511    use pal_async::async_test;
512    use ucs2::Ucs2LeVec;
513    use uefi_nvram_storage::in_memory::impl_agnostic_tests;
514    use wchar::wchz;
515
516    /// An ephemeral implementation of [`StorageBackend`] backed by an in-memory
517    /// buffer. Useful for tests, stateless VM scenarios.
518    #[derive(Default)]
519    pub struct EphemeralStorageBackend(Option<Vec<u8>>);
520
521    #[async_trait::async_trait]
522    impl StorageBackend for EphemeralStorageBackend {
523        async fn persist(&mut self, data: Vec<u8>) -> Result<(), StorageBackendError> {
524            self.0 = Some(data);
525            Ok(())
526        }
527
528        async fn restore(&mut self) -> Result<Option<Vec<u8>>, StorageBackendError> {
529            Ok(self.0.clone())
530        }
531    }
532
533    #[async_test]
534    async fn test_single_variable() {
535        let mut storage = EphemeralStorageBackend::default();
536        let mut nvram = HclCompatNvram::new(&mut storage, None);
537        impl_agnostic_tests::test_single_variable(&mut nvram).await;
538    }
539
540    #[async_test]
541    async fn test_multiple_variable() {
542        let mut storage = EphemeralStorageBackend::default();
543        let mut nvram = HclCompatNvram::new(&mut storage, None);
544        impl_agnostic_tests::test_multiple_variable(&mut nvram).await;
545    }
546
547    #[async_test]
548    async fn test_next() {
549        let mut storage = EphemeralStorageBackend::default();
550        let mut nvram = HclCompatNvram::new(&mut storage, None);
551        impl_agnostic_tests::test_next(&mut nvram).await;
552    }
553
554    #[async_test]
555    async fn boundary_conditions() {
556        let mut storage = EphemeralStorageBackend::default();
557        let mut nvram = HclCompatNvram::new(&mut storage, None);
558
559        let vendor = Guid::new_random();
560        let attr = 0x1234;
561        let data = vec![0x1, 0x2, 0x3, 0x4, 0x5];
562        let timestamp = EFI_TIME::default();
563
564        let name_ok = Ucs2LeVec::from_vec_with_nul(
565            std::iter::repeat_n([0, b'a'], (EFI_MAX_VARIABLE_NAME_SIZE / 2) - 1)
566                .chain(Some([0, 0]))
567                .flat_map(|x| x.into_iter())
568                .collect(),
569        )
570        .unwrap();
571        let name_too_big = Ucs2LeVec::from_vec_with_nul(
572            std::iter::repeat_n([0, b'a'], EFI_MAX_VARIABLE_NAME_SIZE / 2)
573                .chain(Some([0, 0]))
574                .flat_map(|x| x.into_iter())
575                .collect(),
576        )
577        .unwrap();
578
579        nvram
580            .set_variable(&name_ok, vendor, attr, data.clone(), timestamp)
581            .await
582            .unwrap();
583
584        let res = nvram
585            .set_variable(&name_too_big, vendor, attr, data.clone(), timestamp)
586            .await;
587        assert!(matches!(res, Err(NvramStorageError::VariableNameTooLong)));
588
589        nvram
590            .set_variable(
591                &name_ok,
592                vendor,
593                attr,
594                vec![0xff; EFI_MAX_VARIABLE_DATA_SIZE],
595                timestamp,
596            )
597            .await
598            .unwrap();
599
600        let res = nvram
601            .set_variable(
602                &name_ok,
603                vendor,
604                attr,
605                vec![0xff; EFI_MAX_VARIABLE_DATA_SIZE + 1],
606                timestamp,
607            )
608            .await;
609        assert!(matches!(res, Err(NvramStorageError::VariableDataTooLong)));
610
611        // make sure we can hit the max-memory error
612        loop {
613            let res = nvram
614                .set_variable(
615                    &name_ok,
616                    Guid::new_random(), // different guids = different vars
617                    attr,
618                    vec![0xff; EFI_MAX_VARIABLE_DATA_SIZE],
619                    timestamp,
620                )
621                .await;
622
623            match res {
624                Ok(()) => {}
625                Err(NvramStorageError::OutOfSpace) => break,
626                Err(_) => panic!(),
627            }
628        }
629    }
630
631    #[async_test]
632    async fn load_reload() {
633        let mut storage = EphemeralStorageBackend::default();
634
635        let vendor1 = Guid::new_random();
636        let name1 = Ucs2LeSlice::from_slice_with_nul(wchz!(u16, "var1").as_bytes()).unwrap();
637        let vendor2 = Guid::new_random();
638        let name2 = Ucs2LeSlice::from_slice_with_nul(wchz!(u16, "var2").as_bytes()).unwrap();
639        let vendor3 = Guid::new_random();
640        let name3 = Ucs2LeSlice::from_slice_with_nul(wchz!(u16, "var3").as_bytes()).unwrap();
641        let attr = 0x1234;
642        let data = vec![0x1, 0x2, 0x3, 0x4, 0x5];
643        let timestamp = EFI_TIME::default();
644
645        let mut nvram = HclCompatNvram::new(&mut storage, None);
646        nvram
647            .set_variable(name1, vendor1, attr, data.clone(), timestamp)
648            .await
649            .unwrap();
650        nvram
651            .set_variable(name2, vendor2, attr, data.clone(), timestamp)
652            .await
653            .unwrap();
654        nvram
655            .set_variable(name3, vendor3, attr, data.clone(), timestamp)
656            .await
657            .unwrap();
658
659        drop(nvram);
660
661        // reload
662        let mut nvram = HclCompatNvram::new(&mut storage, None);
663
664        let (result_attr, result_data, result_timestamp) =
665            nvram.get_variable(name1, vendor1).await.unwrap().unwrap();
666        assert_eq!(result_attr, attr);
667        assert_eq!(result_data, data);
668        assert_eq!(result_timestamp, timestamp);
669
670        let (result_attr, result_data, result_timestamp) =
671            nvram.get_variable(name2, vendor2).await.unwrap().unwrap();
672        assert_eq!(result_attr, attr);
673        assert_eq!(result_data, data);
674        assert_eq!(result_timestamp, timestamp);
675
676        let (result_attr, result_data, result_timestamp) =
677            nvram.get_variable(name3, vendor3).await.unwrap().unwrap();
678        assert_eq!(result_attr, attr);
679        assert_eq!(result_data, data);
680        assert_eq!(result_timestamp, timestamp);
681    }
682}