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
99/// "Quirks" to take into account when loading/storing nvram blob data.
100#[cfg_attr(feature = "inspect", derive(inspect::Inspect))]
101pub struct HclCompatNvramQuirks {
102    /// When loading nvram variables from storage, don't fail the entire load
103    /// process when encountering variables that are missing null terminators in
104    /// their name. Instead, skip loading any such variables, and continue on
105    /// with the load.
106    ///
107    /// # Context
108    ///
109    /// Due to a (now fixed) bug in a previous version of Microsoft HCL, it was
110    /// possible for non-null-terminated nvram variables to slip-through
111    /// validation and get persisted to disk.
112    ///
113    /// Enabling this quirk will allow "salvaging" the rest of the non-corrupt
114    /// nvram variables, which may be preferable over having the VM fail to boot
115    /// at all.
116    pub skip_corrupt_vars_with_missing_null_term: bool,
117}
118
119impl<S: StorageBackend> HclCompatNvram<S> {
120    /// Create a new [`HclCompatNvram`]
121    pub async fn new(
122        storage: S,
123        quirks: Option<HclCompatNvramQuirks>,
124        is_restoring: bool,
125    ) -> Result<Self, NvramStorageError> {
126        let mut nvram = Self {
127            quirks: quirks.unwrap_or(HclCompatNvramQuirks {
128                skip_corrupt_vars_with_missing_null_term: false,
129            }),
130
131            storage,
132
133            in_memory: in_memory::InMemoryNvram::new(),
134
135            nvram_buf: Vec::new(),
136        };
137        if !is_restoring {
138            nvram.load_from_storage().await?;
139        }
140        Ok(nvram)
141    }
142
143    async fn load_from_storage(&mut self) -> Result<(), NvramStorageError> {
144        tracing::info!("loading uefi nvram from storage");
145        let res = self.load_from_storage_inner().await;
146        if let Err(e) = &res {
147            tracing::error!(CVM_ALLOWED, "storage contains corrupt nvram state");
148            tracing::error!(
149                CVM_CONFIDENTIAL,
150                error = e as &dyn std::error::Error,
151                "storage contains corrupt nvram state"
152            );
153        }
154        res
155    }
156
157    async fn load_from_storage_inner(&mut self) -> Result<(), NvramStorageError> {
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        Ok(())
297    }
298
299    /// Dump in-memory nvram to the underlying storage device.
300    async fn flush_storage(&mut self) -> Result<(), NvramStorageError> {
301        tracing::info!("flushing uefi nvram to storage");
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 fn iter(&mut self) -> impl Iterator<Item = in_memory::VariableEntry<'_>> {
347        self.in_memory.iter()
348    }
349}
350
351#[async_trait::async_trait]
352impl<S: StorageBackend> NvramStorage for HclCompatNvram<S> {
353    async fn get_variable(
354        &mut self,
355        name: &Ucs2LeSlice,
356        vendor: Guid,
357    ) -> Result<Option<(u32, Vec<u8>, EFI_TIME)>, NvramStorageError> {
358        if name.as_bytes().len() > EFI_MAX_VARIABLE_NAME_SIZE {
359            return Err(NvramStorageError::VariableNameTooLong);
360        }
361
362        self.in_memory.get_variable(name, vendor).await
363    }
364
365    async fn set_variable(
366        &mut self,
367        name: &Ucs2LeSlice,
368        vendor: Guid,
369        attr: u32,
370        data: Vec<u8>,
371        timestamp: EFI_TIME,
372    ) -> Result<(), NvramStorageError> {
373        if name.as_bytes().len() > EFI_MAX_VARIABLE_NAME_SIZE {
374            return Err(NvramStorageError::VariableNameTooLong);
375        }
376
377        if data.len() > EFI_MAX_VARIABLE_DATA_SIZE {
378            return Err(NvramStorageError::VariableDataTooLong);
379        }
380
381        // don't overshoot MAXIMUM_NVRAM_SIZE
382        {
383            let new_file_size = match self.in_memory.get_variable(name, vendor).await? {
384                Some((_, existing_data, _)) => {
385                    self.nvram_buf.len() - existing_data.len() + data.len()
386                }
387                None => {
388                    self.nvram_buf.len()
389                        + name.as_bytes().len()
390                        + data.len()
391                        + size_of::<format::NvramVariable>()
392                }
393            };
394
395            if new_file_size > MAXIMUM_NVRAM_SIZE {
396                return Err(NvramStorageError::OutOfSpace);
397            }
398        }
399
400        self.in_memory
401            .set_variable(name, vendor, attr, data, timestamp)
402            .await?;
403        self.flush_storage().await?;
404
405        Ok(())
406    }
407
408    async fn append_variable(
409        &mut self,
410        name: &Ucs2LeSlice,
411        vendor: Guid,
412        data: Vec<u8>,
413        timestamp: EFI_TIME,
414    ) -> Result<bool, NvramStorageError> {
415        if name.as_bytes().len() > EFI_MAX_VARIABLE_NAME_SIZE {
416            return Err(NvramStorageError::VariableNameTooLong);
417        }
418
419        if let Some((_, existing_data, _)) = self.in_memory.get_variable(name, vendor).await? {
420            if existing_data.len() + data.len() > EFI_MAX_VARIABLE_DATA_SIZE {
421                return Err(NvramStorageError::VariableDataTooLong);
422            }
423
424            let new_file_size = self.nvram_buf.len() + data.len();
425
426            if new_file_size > MAXIMUM_NVRAM_SIZE {
427                return Err(NvramStorageError::OutOfSpace);
428            }
429        }
430
431        let found = self
432            .in_memory
433            .append_variable(name, vendor, data, timestamp)
434            .await?;
435        self.flush_storage().await?;
436
437        Ok(found)
438    }
439
440    async fn remove_variable(
441        &mut self,
442        name: &Ucs2LeSlice,
443        vendor: Guid,
444    ) -> Result<bool, NvramStorageError> {
445        if name.as_bytes().len() > EFI_MAX_VARIABLE_NAME_SIZE {
446            return Err(NvramStorageError::VariableNameTooLong);
447        }
448
449        let removed = self.in_memory.remove_variable(name, vendor).await?;
450        self.flush_storage().await?;
451
452        Ok(removed)
453    }
454
455    async fn next_variable(
456        &mut self,
457        name_vendor: Option<(&Ucs2LeSlice, Guid)>,
458    ) -> Result<NextVariable, NvramStorageError> {
459        if let Some((name, _)) = name_vendor {
460            if name.as_bytes().len() > EFI_MAX_VARIABLE_NAME_SIZE {
461                return Err(NvramStorageError::VariableNameTooLong);
462            }
463        }
464
465        self.in_memory.next_variable(name_vendor).await
466    }
467}
468
469#[cfg(feature = "save_restore")]
470mod save_restore {
471    use super::*;
472    use vmcore::save_restore::RestoreError;
473    use vmcore::save_restore::SaveError;
474    use vmcore::save_restore::SaveRestore;
475
476    impl<S: StorageBackend> SaveRestore for HclCompatNvram<S> {
477        type SavedState = <in_memory::InMemoryNvram as SaveRestore>::SavedState;
478
479        fn save(&mut self) -> Result<Self::SavedState, SaveError> {
480            self.in_memory.save()
481        }
482
483        fn restore(&mut self, state: Self::SavedState) -> Result<(), RestoreError> {
484            self.in_memory.restore(state)
485        }
486    }
487}
488
489#[cfg(test)]
490mod test {
491    use super::storage_backend::StorageBackend;
492    use super::storage_backend::StorageBackendError;
493    use super::*;
494    use pal_async::async_test;
495    use ucs2::Ucs2LeVec;
496    use uefi_nvram_storage::in_memory::impl_agnostic_tests;
497    use wchar::wchz;
498
499    /// An ephemeral implementation of [`StorageBackend`] backed by an in-memory
500    /// buffer. Useful for tests, stateless VM scenarios.
501    #[derive(Default)]
502    pub struct EphemeralStorageBackend(Option<Vec<u8>>);
503
504    #[async_trait::async_trait]
505    impl StorageBackend for EphemeralStorageBackend {
506        async fn persist(&mut self, data: Vec<u8>) -> Result<(), StorageBackendError> {
507            self.0 = Some(data);
508            Ok(())
509        }
510
511        async fn restore(&mut self) -> Result<Option<Vec<u8>>, StorageBackendError> {
512            Ok(self.0.clone())
513        }
514    }
515
516    #[async_test]
517    async fn test_single_variable() {
518        let mut storage = EphemeralStorageBackend::default();
519        let mut nvram = HclCompatNvram::new(&mut storage, None, false)
520            .await
521            .unwrap();
522        impl_agnostic_tests::test_single_variable(&mut nvram).await;
523    }
524
525    #[async_test]
526    async fn test_multiple_variable() {
527        let mut storage = EphemeralStorageBackend::default();
528        let mut nvram = HclCompatNvram::new(&mut storage, None, false)
529            .await
530            .unwrap();
531        impl_agnostic_tests::test_multiple_variable(&mut nvram).await;
532    }
533
534    #[async_test]
535    async fn test_next() {
536        let mut storage = EphemeralStorageBackend::default();
537        let mut nvram = HclCompatNvram::new(&mut storage, None, false)
538            .await
539            .unwrap();
540        impl_agnostic_tests::test_next(&mut nvram).await;
541    }
542
543    #[async_test]
544    async fn boundary_conditions() {
545        let mut storage = EphemeralStorageBackend::default();
546        let mut nvram = HclCompatNvram::new(&mut storage, None, false)
547            .await
548            .unwrap();
549
550        let vendor = Guid::new_random();
551        let attr = 0x1234;
552        let data = vec![0x1, 0x2, 0x3, 0x4, 0x5];
553        let timestamp = EFI_TIME::default();
554
555        let name_ok = Ucs2LeVec::from_vec_with_nul(
556            std::iter::repeat_n([0, b'a'], (EFI_MAX_VARIABLE_NAME_SIZE / 2) - 1)
557                .chain(Some([0, 0]))
558                .flat_map(|x| x.into_iter())
559                .collect(),
560        )
561        .unwrap();
562        let name_too_big = Ucs2LeVec::from_vec_with_nul(
563            std::iter::repeat_n([0, b'a'], EFI_MAX_VARIABLE_NAME_SIZE / 2)
564                .chain(Some([0, 0]))
565                .flat_map(|x| x.into_iter())
566                .collect(),
567        )
568        .unwrap();
569
570        nvram
571            .set_variable(&name_ok, vendor, attr, data.clone(), timestamp)
572            .await
573            .unwrap();
574
575        let res = nvram
576            .set_variable(&name_too_big, vendor, attr, data.clone(), timestamp)
577            .await;
578        assert!(matches!(res, Err(NvramStorageError::VariableNameTooLong)));
579
580        nvram
581            .set_variable(
582                &name_ok,
583                vendor,
584                attr,
585                vec![0xff; EFI_MAX_VARIABLE_DATA_SIZE],
586                timestamp,
587            )
588            .await
589            .unwrap();
590
591        let res = nvram
592            .set_variable(
593                &name_ok,
594                vendor,
595                attr,
596                vec![0xff; EFI_MAX_VARIABLE_DATA_SIZE + 1],
597                timestamp,
598            )
599            .await;
600        assert!(matches!(res, Err(NvramStorageError::VariableDataTooLong)));
601
602        // make sure we can hit the max-memory error
603        loop {
604            let res = nvram
605                .set_variable(
606                    &name_ok,
607                    Guid::new_random(), // different guids = different vars
608                    attr,
609                    vec![0xff; EFI_MAX_VARIABLE_DATA_SIZE],
610                    timestamp,
611                )
612                .await;
613
614            match res {
615                Ok(()) => {}
616                Err(NvramStorageError::OutOfSpace) => break,
617                Err(_) => panic!(),
618            }
619        }
620    }
621
622    #[async_test]
623    async fn load_reload() {
624        let mut storage = EphemeralStorageBackend::default();
625
626        let vendor1 = Guid::new_random();
627        let name1 = Ucs2LeSlice::from_slice_with_nul(wchz!(u16, "var1").as_bytes()).unwrap();
628        let vendor2 = Guid::new_random();
629        let name2 = Ucs2LeSlice::from_slice_with_nul(wchz!(u16, "var2").as_bytes()).unwrap();
630        let vendor3 = Guid::new_random();
631        let name3 = Ucs2LeSlice::from_slice_with_nul(wchz!(u16, "var3").as_bytes()).unwrap();
632        let attr = 0x1234;
633        let data = vec![0x1, 0x2, 0x3, 0x4, 0x5];
634        let timestamp = EFI_TIME::default();
635
636        let mut nvram = HclCompatNvram::new(&mut storage, None, false)
637            .await
638            .unwrap();
639        nvram
640            .set_variable(name1, vendor1, attr, data.clone(), timestamp)
641            .await
642            .unwrap();
643        nvram
644            .set_variable(name2, vendor2, attr, data.clone(), timestamp)
645            .await
646            .unwrap();
647        nvram
648            .set_variable(name3, vendor3, attr, data.clone(), timestamp)
649            .await
650            .unwrap();
651
652        drop(nvram);
653
654        // reload
655        let mut nvram = HclCompatNvram::new(&mut storage, None, false)
656            .await
657            .unwrap();
658
659        let (result_attr, result_data, result_timestamp) =
660            nvram.get_variable(name1, vendor1).await.unwrap().unwrap();
661        assert_eq!(result_attr, attr);
662        assert_eq!(result_data, data);
663        assert_eq!(result_timestamp, timestamp);
664
665        let (result_attr, result_data, result_timestamp) =
666            nvram.get_variable(name2, vendor2).await.unwrap().unwrap();
667        assert_eq!(result_attr, attr);
668        assert_eq!(result_data, data);
669        assert_eq!(result_timestamp, timestamp);
670
671        let (result_attr, result_data, result_timestamp) =
672            nvram.get_variable(name3, vendor3).await.unwrap().unwrap();
673        assert_eq!(result_attr, attr);
674        assert_eq!(result_data, data);
675        assert_eq!(result_timestamp, timestamp);
676    }
677}