Skip to main content

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