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        tracing::info!("loading uefi nvram from storage");
159
160        let nvram_buf = self
161            .storage
162            .restore()
163            .await
164            .map_err(|e| NvramStorageError::Load(e.into()))?
165            .unwrap_or_default();
166
167        if nvram_buf.len() > MAXIMUM_NVRAM_SIZE {
168            return Err(NvramStorageError::Load(
169                format!(
170                    "Existing nvram state exceeds MAXIMUM_NVRAM_SIZE ({} > {})",
171                    nvram_buf.len(),
172                    MAXIMUM_NVRAM_SIZE
173                )
174                .into(),
175            ));
176        }
177
178        // load state into memory
179        self.in_memory.clear();
180        self.nvram_buf = nvram_buf;
181        let mut buf = self.nvram_buf.as_slice();
182        // TODO: zerocopy: error propagation (https://github.com/microsoft/openvmm/issues/759)
183        while let Ok((header, _)) = format::NvramHeader::read_from_prefix(buf) {
184            if buf.len() < header.length as usize {
185                return Err(NvramStorageError::Load(
186                    format!(
187                        "unexpected EOF. expected at least {} more bytes, but only found {}",
188                        header.length,
189                        buf.len()
190                    )
191                    .into(),
192                ));
193            }
194
195            let entry_buf = {
196                let (entry_buf, remaining) = buf.split_at(header.length as usize);
197                buf = remaining;
198                entry_buf
199            };
200
201            match header.header_type {
202                format::NvramHeaderType::VARIABLE => {}
203                _ => {
204                    return Err(NvramStorageError::Load(
205                        format!("unknown header type: {:?}", header.header_type).into(),
206                    ));
207                }
208            }
209
210            // validation check above ensures that at this point, entry_buf
211            // corresponds to a VARIABLE entry
212
213            let (var_header, var_name, var_data) = {
214                // TODO: zerocopy: error propagation (https://github.com/microsoft/openvmm/issues/759)
215                // TODO: zerocopy: manual fix - review carefully! (https://github.com/microsoft/openvmm/issues/759)
216                let (var_header, var_length_data) =
217                    format::NvramVariable::read_from_prefix(entry_buf)
218                        .map_err(|_| NvramStorageError::Load("variable entry too short".into()))?;
219
220                if var_length_data.len()
221                    != var_header.name_bytes as usize + var_header.data_bytes as usize
222                {
223                    return Err(NvramStorageError::Load(
224                        "mismatch between header length and variable data size".into(),
225                    ));
226                }
227
228                let (var_name, var_data) = var_length_data.split_at(var_header.name_bytes as usize);
229
230                (var_header, var_name, var_data)
231            };
232
233            if var_name.len() > EFI_MAX_VARIABLE_NAME_SIZE {
234                return Err(NvramStorageError::Load(
235                    format!(
236                        "variable name too big. {} > {}",
237                        var_name.len(),
238                        EFI_MAX_VARIABLE_NAME_SIZE
239                    )
240                    .into(),
241                ));
242            }
243
244            if var_data.len() > EFI_MAX_VARIABLE_DATA_SIZE {
245                return Err(NvramStorageError::Load(
246                    format!(
247                        "variable data too big. {} > {}",
248                        var_data.len(),
249                        EFI_MAX_VARIABLE_DATA_SIZE
250                    )
251                    .into(),
252                ));
253            }
254
255            let name = match Ucs2LeSlice::from_slice_with_nul(var_name) {
256                Ok(name) => name,
257                Err(e) => {
258                    if self.quirks.skip_corrupt_vars_with_missing_null_term {
259                        let var = {
260                            let mut var = var_name.to_vec();
261                            var.push(0);
262                            var.push(0);
263                            ucs2::Ucs2LeVec::from_vec_with_nul(var)
264                        };
265                        tracing::warn!(
266                            CVM_ALLOWED,
267                            "skipping corrupt nvram var (missing null term)"
268                        );
269                        tracing::warn!(
270                            CVM_CONFIDENTIAL,
271                            ?var,
272                            "skipping corrupt nvram var (missing null term)"
273                        );
274                        continue;
275                    } else {
276                        return Err(NvramStorageError::Load(e.into()));
277                    }
278                }
279            };
280
281            self.in_memory
282                .set_variable(
283                    name,
284                    var_header.vendor,
285                    var_header.attributes,
286                    var_data.to_vec(),
287                    var_header.timestamp,
288                )
289                .await?;
290        }
291
292        if !buf.is_empty() {
293            return Err(NvramStorageError::Load(
294                "existing nvram state contains excess data".into(),
295            ));
296        }
297
298        self.loaded = true;
299        Ok(())
300    }
301
302    /// Dump in-memory nvram to the underlying storage device.
303    async fn flush_storage(&mut self) -> Result<(), NvramStorageError> {
304        tracing::info!("flushing uefi nvram to storage");
305        self.nvram_buf.clear();
306
307        for in_memory::VariableEntry {
308            vendor,
309            name,
310            data,
311            timestamp,
312            attr,
313        } in self.in_memory.iter()
314        {
315            self.nvram_buf.extend_from_slice(
316                format::NvramVariable {
317                    header: format::NvramHeader {
318                        header_type: format::NvramHeaderType::VARIABLE,
319                        length: (size_of::<format::NvramVariable>()
320                            + name.as_bytes().len()
321                            + data.len()) as u32,
322                    },
323                    attributes: attr,
324                    timestamp,
325                    vendor,
326                    name_bytes: name.as_bytes().len() as u16,
327                    data_bytes: data.len() as u16,
328                }
329                .as_bytes(),
330            );
331            self.nvram_buf.extend_from_slice(name.as_bytes());
332            self.nvram_buf.extend_from_slice(data);
333        }
334
335        // callers make sure that any operations that add/append to vars will
336        // not result in file size exceeding MAXIMUM_NVRAM_SIZE
337        assert!(self.nvram_buf.len() < MAXIMUM_NVRAM_SIZE);
338
339        self.storage
340            .persist(self.nvram_buf.clone())
341            .await
342            .map_err(|e| NvramStorageError::Commit(e.into()))?;
343
344        Ok(())
345    }
346
347    /// Iterate over the NVRAM entries. This function asynchronously loads the
348    /// NVRAM contents into memory from the backing storage if necessary.
349    pub async fn iter(
350        &mut self,
351    ) -> Result<impl Iterator<Item = in_memory::VariableEntry<'_>>, NvramStorageError> {
352        self.lazy_load_from_storage().await?;
353        Ok(self.in_memory.iter())
354    }
355}
356
357#[async_trait::async_trait]
358impl<S: StorageBackend> NvramStorage for HclCompatNvram<S> {
359    async fn get_variable(
360        &mut self,
361        name: &Ucs2LeSlice,
362        vendor: Guid,
363    ) -> Result<Option<(u32, Vec<u8>, EFI_TIME)>, NvramStorageError> {
364        self.lazy_load_from_storage().await?;
365
366        if name.as_bytes().len() > EFI_MAX_VARIABLE_NAME_SIZE {
367            return Err(NvramStorageError::VariableNameTooLong);
368        }
369
370        self.in_memory.get_variable(name, vendor).await
371    }
372
373    async fn set_variable(
374        &mut self,
375        name: &Ucs2LeSlice,
376        vendor: Guid,
377        attr: u32,
378        data: Vec<u8>,
379        timestamp: EFI_TIME,
380    ) -> Result<(), NvramStorageError> {
381        self.lazy_load_from_storage().await?;
382
383        if name.as_bytes().len() > EFI_MAX_VARIABLE_NAME_SIZE {
384            return Err(NvramStorageError::VariableNameTooLong);
385        }
386
387        if data.len() > EFI_MAX_VARIABLE_DATA_SIZE {
388            return Err(NvramStorageError::VariableDataTooLong);
389        }
390
391        // don't overshoot MAXIMUM_NVRAM_SIZE
392        {
393            let new_file_size = match self.in_memory.get_variable(name, vendor).await? {
394                Some((_, existing_data, _)) => {
395                    self.nvram_buf.len() - existing_data.len() + data.len()
396                }
397                None => {
398                    self.nvram_buf.len()
399                        + name.as_bytes().len()
400                        + data.len()
401                        + size_of::<format::NvramVariable>()
402                }
403            };
404
405            if new_file_size > MAXIMUM_NVRAM_SIZE {
406                return Err(NvramStorageError::OutOfSpace);
407            }
408        }
409
410        self.in_memory
411            .set_variable(name, vendor, attr, data, timestamp)
412            .await?;
413        self.flush_storage().await?;
414
415        Ok(())
416    }
417
418    async fn append_variable(
419        &mut self,
420        name: &Ucs2LeSlice,
421        vendor: Guid,
422        data: Vec<u8>,
423        timestamp: EFI_TIME,
424    ) -> Result<bool, NvramStorageError> {
425        self.lazy_load_from_storage().await?;
426
427        if name.as_bytes().len() > EFI_MAX_VARIABLE_NAME_SIZE {
428            return Err(NvramStorageError::VariableNameTooLong);
429        }
430
431        if let Some((_, existing_data, _)) = self.in_memory.get_variable(name, vendor).await? {
432            if existing_data.len() + data.len() > EFI_MAX_VARIABLE_DATA_SIZE {
433                return Err(NvramStorageError::VariableDataTooLong);
434            }
435
436            let new_file_size = self.nvram_buf.len() + data.len();
437
438            if new_file_size > MAXIMUM_NVRAM_SIZE {
439                return Err(NvramStorageError::OutOfSpace);
440            }
441        }
442
443        let found = self
444            .in_memory
445            .append_variable(name, vendor, data, timestamp)
446            .await?;
447        self.flush_storage().await?;
448
449        Ok(found)
450    }
451
452    async fn remove_variable(
453        &mut self,
454        name: &Ucs2LeSlice,
455        vendor: Guid,
456    ) -> Result<bool, NvramStorageError> {
457        self.lazy_load_from_storage().await?;
458
459        if name.as_bytes().len() > EFI_MAX_VARIABLE_NAME_SIZE {
460            return Err(NvramStorageError::VariableNameTooLong);
461        }
462
463        let removed = self.in_memory.remove_variable(name, vendor).await?;
464        self.flush_storage().await?;
465
466        Ok(removed)
467    }
468
469    async fn next_variable(
470        &mut self,
471        name_vendor: Option<(&Ucs2LeSlice, Guid)>,
472    ) -> Result<NextVariable, NvramStorageError> {
473        self.lazy_load_from_storage().await?;
474
475        if let Some((name, _)) = name_vendor {
476            if name.as_bytes().len() > EFI_MAX_VARIABLE_NAME_SIZE {
477                return Err(NvramStorageError::VariableNameTooLong);
478            }
479        }
480
481        self.in_memory.next_variable(name_vendor).await
482    }
483}
484
485#[cfg(feature = "save_restore")]
486mod save_restore {
487    use super::*;
488    use vmcore::save_restore::RestoreError;
489    use vmcore::save_restore::SaveError;
490    use vmcore::save_restore::SaveRestore;
491
492    impl<S: StorageBackend> SaveRestore for HclCompatNvram<S> {
493        type SavedState = <in_memory::InMemoryNvram as SaveRestore>::SavedState;
494
495        fn save(&mut self) -> Result<Self::SavedState, SaveError> {
496            self.in_memory.save()
497        }
498
499        fn restore(&mut self, state: Self::SavedState) -> Result<(), RestoreError> {
500            if state.nvram.is_some() {
501                self.in_memory.restore(state)?;
502                self.loaded = true;
503            }
504            Ok(())
505        }
506    }
507}
508
509#[cfg(test)]
510mod test {
511    use super::storage_backend::StorageBackend;
512    use super::storage_backend::StorageBackendError;
513    use super::*;
514    use pal_async::async_test;
515    use ucs2::Ucs2LeVec;
516    use uefi_nvram_storage::in_memory::impl_agnostic_tests;
517    use wchar::wchz;
518
519    /// An ephemeral implementation of [`StorageBackend`] backed by an in-memory
520    /// buffer. Useful for tests, stateless VM scenarios.
521    #[derive(Default)]
522    pub struct EphemeralStorageBackend(Option<Vec<u8>>);
523
524    #[async_trait::async_trait]
525    impl StorageBackend for EphemeralStorageBackend {
526        async fn persist(&mut self, data: Vec<u8>) -> Result<(), StorageBackendError> {
527            self.0 = Some(data);
528            Ok(())
529        }
530
531        async fn restore(&mut self) -> Result<Option<Vec<u8>>, StorageBackendError> {
532            Ok(self.0.clone())
533        }
534    }
535
536    #[async_test]
537    async fn test_single_variable() {
538        let mut storage = EphemeralStorageBackend::default();
539        let mut nvram = HclCompatNvram::new(&mut storage, None);
540        impl_agnostic_tests::test_single_variable(&mut nvram).await;
541    }
542
543    #[async_test]
544    async fn test_multiple_variable() {
545        let mut storage = EphemeralStorageBackend::default();
546        let mut nvram = HclCompatNvram::new(&mut storage, None);
547        impl_agnostic_tests::test_multiple_variable(&mut nvram).await;
548    }
549
550    #[async_test]
551    async fn test_next() {
552        let mut storage = EphemeralStorageBackend::default();
553        let mut nvram = HclCompatNvram::new(&mut storage, None);
554        impl_agnostic_tests::test_next(&mut nvram).await;
555    }
556
557    #[async_test]
558    async fn boundary_conditions() {
559        let mut storage = EphemeralStorageBackend::default();
560        let mut nvram = HclCompatNvram::new(&mut storage, None);
561
562        let vendor = Guid::new_random();
563        let attr = 0x1234;
564        let data = vec![0x1, 0x2, 0x3, 0x4, 0x5];
565        let timestamp = EFI_TIME::default();
566
567        let name_ok = Ucs2LeVec::from_vec_with_nul(
568            std::iter::repeat_n([0, b'a'], (EFI_MAX_VARIABLE_NAME_SIZE / 2) - 1)
569                .chain(Some([0, 0]))
570                .flat_map(|x| x.into_iter())
571                .collect(),
572        )
573        .unwrap();
574        let name_too_big = Ucs2LeVec::from_vec_with_nul(
575            std::iter::repeat_n([0, b'a'], EFI_MAX_VARIABLE_NAME_SIZE / 2)
576                .chain(Some([0, 0]))
577                .flat_map(|x| x.into_iter())
578                .collect(),
579        )
580        .unwrap();
581
582        nvram
583            .set_variable(&name_ok, vendor, attr, data.clone(), timestamp)
584            .await
585            .unwrap();
586
587        let res = nvram
588            .set_variable(&name_too_big, vendor, attr, data.clone(), timestamp)
589            .await;
590        assert!(matches!(res, Err(NvramStorageError::VariableNameTooLong)));
591
592        nvram
593            .set_variable(
594                &name_ok,
595                vendor,
596                attr,
597                vec![0xff; EFI_MAX_VARIABLE_DATA_SIZE],
598                timestamp,
599            )
600            .await
601            .unwrap();
602
603        let res = nvram
604            .set_variable(
605                &name_ok,
606                vendor,
607                attr,
608                vec![0xff; EFI_MAX_VARIABLE_DATA_SIZE + 1],
609                timestamp,
610            )
611            .await;
612        assert!(matches!(res, Err(NvramStorageError::VariableDataTooLong)));
613
614        // make sure we can hit the max-memory error
615        loop {
616            let res = nvram
617                .set_variable(
618                    &name_ok,
619                    Guid::new_random(), // different guids = different vars
620                    attr,
621                    vec![0xff; EFI_MAX_VARIABLE_DATA_SIZE],
622                    timestamp,
623                )
624                .await;
625
626            match res {
627                Ok(()) => {}
628                Err(NvramStorageError::OutOfSpace) => break,
629                Err(_) => panic!(),
630            }
631        }
632    }
633
634    #[async_test]
635    async fn load_reload() {
636        let mut storage = EphemeralStorageBackend::default();
637
638        let vendor1 = Guid::new_random();
639        let name1 = Ucs2LeSlice::from_slice_with_nul(wchz!(u16, "var1").as_bytes()).unwrap();
640        let vendor2 = Guid::new_random();
641        let name2 = Ucs2LeSlice::from_slice_with_nul(wchz!(u16, "var2").as_bytes()).unwrap();
642        let vendor3 = Guid::new_random();
643        let name3 = Ucs2LeSlice::from_slice_with_nul(wchz!(u16, "var3").as_bytes()).unwrap();
644        let attr = 0x1234;
645        let data = vec![0x1, 0x2, 0x3, 0x4, 0x5];
646        let timestamp = EFI_TIME::default();
647
648        let mut nvram = HclCompatNvram::new(&mut storage, None);
649        nvram
650            .set_variable(name1, vendor1, attr, data.clone(), timestamp)
651            .await
652            .unwrap();
653        nvram
654            .set_variable(name2, vendor2, attr, data.clone(), timestamp)
655            .await
656            .unwrap();
657        nvram
658            .set_variable(name3, vendor3, attr, data.clone(), timestamp)
659            .await
660            .unwrap();
661
662        drop(nvram);
663
664        // reload
665        let mut nvram = HclCompatNvram::new(&mut storage, None);
666
667        let (result_attr, result_data, result_timestamp) =
668            nvram.get_variable(name1, vendor1).await.unwrap().unwrap();
669        assert_eq!(result_attr, attr);
670        assert_eq!(result_data, data);
671        assert_eq!(result_timestamp, timestamp);
672
673        let (result_attr, result_data, result_timestamp) =
674            nvram.get_variable(name2, vendor2).await.unwrap().unwrap();
675        assert_eq!(result_attr, attr);
676        assert_eq!(result_data, data);
677        assert_eq!(result_timestamp, timestamp);
678
679        let (result_attr, result_data, result_timestamp) =
680            nvram.get_variable(name3, vendor3).await.unwrap().unwrap();
681        assert_eq!(result_attr, attr);
682        assert_eq!(result_data, data);
683        assert_eq!(result_timestamp, timestamp);
684    }
685}