vmgstool/
uefi_nvram.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Functions for interacting with the BIOS_NVRAM file in a VMGS file
5
6use crate::Error;
7use crate::FilePathArg;
8use crate::KeyPathArg;
9use crate::OpenMode;
10use crate::storage_backend::VmgsStorageBackend;
11use crate::vmgs_file_open;
12use crate::vmgs_json;
13use anyhow::Result;
14use clap::Args;
15use clap::Subcommand;
16use fs_err::File;
17use guid::Guid;
18use hcl_compat_uefi_nvram_storage::HclCompatNvram;
19use std::io::Write;
20use std::ops::Deref;
21use std::path::Path;
22use std::path::PathBuf;
23use std::str::FromStr;
24use ucs2::Ucs2LeVec;
25use uefi_nvram_specvars::ParsedNvramEntry;
26use uefi_nvram_specvars::boot_order;
27use uefi_nvram_specvars::parse_nvram_entry;
28use uefi_nvram_specvars::signature_list::SignatureList;
29use uefi_nvram_storage::NvramStorage;
30use uefi_specs::uefi::nvram::vars::EFI_GLOBAL_VARIABLE;
31use uefi_specs::uefi::time::EFI_TIME;
32use vmgs::Vmgs;
33
34#[derive(Args)]
35pub(crate) struct OutputArgs {
36    /// Output file path (defaults to terminal)
37    #[clap(short = 'o', long, alias = "outpath")]
38    output_path: Option<PathBuf>,
39    /// Only print about one line's worth of bytes of each entry
40    #[clap(short = 't', long)]
41    truncate: bool,
42}
43
44#[derive(Subcommand)]
45pub(crate) enum UefiNvramOperation {
46    /// Dump/Read UEFI NVRAM variables
47    Dump {
48        #[command(flatten)]
49        file_path: FilePathArg,
50        #[command(flatten)]
51        key_path: KeyPathArg,
52        #[command(flatten)]
53        output: OutputArgs,
54    },
55    /// Dump/Read UEFI NVRAM variables from a JSON file generated by
56    /// HvGuestState from a VMGSv1 file
57    DumpFromJson {
58        /// JSON file path
59        #[clap(short = 'f', long, alias = "filepath")]
60        file_path: PathBuf,
61        #[command(flatten)]
62        output: OutputArgs,
63    },
64    /// Attempt to repair boot by deleting all boot entries from the UEFI NVRAM
65    RemoveBootEntries {
66        #[command(flatten)]
67        file_path: FilePathArg,
68        #[command(flatten)]
69        key_path: KeyPathArg,
70        /// Don't actually delete anything, just print the boot entries
71        #[clap(short = 'n', long)]
72        dry_run: bool,
73    },
74    /// Remove a UEFI NVRAM variable
75    RemoveEntry {
76        #[command(flatten)]
77        file_path: FilePathArg,
78        #[command(flatten)]
79        key_path: KeyPathArg,
80        /// Name of the NVRAM entry
81        #[clap(short = 'n', long)]
82        name: String,
83        /// Vendor GUID of the NVRAM entry
84        #[clap(short = 'v', long)]
85        vendor: String,
86    },
87}
88
89pub(crate) async fn do_command(operation: UefiNvramOperation) -> Result<(), Error> {
90    match operation {
91        UefiNvramOperation::Dump {
92            file_path,
93            key_path,
94            output,
95        } => {
96            vmgs_file_dump_nvram(
97                file_path.file_path,
98                output.output_path,
99                key_path.key_path,
100                output.truncate,
101            )
102            .await
103        }
104        UefiNvramOperation::DumpFromJson { file_path, output } => {
105            dump_nvram_from_json(file_path, output.output_path, output.truncate)
106        }
107        UefiNvramOperation::RemoveBootEntries {
108            file_path,
109            key_path,
110            dry_run,
111        } => vmgs_file_remove_boot_entries(file_path.file_path, key_path.key_path, dry_run).await,
112        UefiNvramOperation::RemoveEntry {
113            file_path,
114            key_path,
115            name,
116            vendor,
117        } => {
118            vmgs_file_remove_nvram_entry(file_path.file_path, key_path.key_path, name, vendor).await
119        }
120    }
121}
122
123/// Get UEFI variables from the VMGS file, and write to `data_path`.
124async fn vmgs_file_dump_nvram(
125    file_path: impl AsRef<Path>,
126    output_path: Option<impl AsRef<Path>>,
127    key_path: Option<impl AsRef<Path>>,
128    truncate: bool,
129) -> Result<(), Error> {
130    let mut nvram_storage =
131        vmgs_file_open_nvram(file_path, key_path, OpenMode::ReadOnlyWarn).await?;
132
133    let mut out: Box<dyn Write + Send> = if let Some(path) = output_path {
134        Box::new(File::create(path.as_ref()).map_err(Error::DataFile)?)
135    } else {
136        Box::new(std::io::stdout())
137    };
138
139    dump_nvram(&mut nvram_storage, &mut out, truncate).await
140}
141
142async fn dump_nvram(
143    nvram_storage: &mut HclCompatNvram<VmgsStorageBackend>,
144    out: &mut impl Write,
145    truncate: bool,
146) -> Result<(), Error> {
147    let mut count = 0;
148    for entry in nvram_storage.iter().await? {
149        let meta = NvramEntryMetadata {
150            vendor: entry.vendor.to_string(),
151            name: entry.name.to_string(),
152            timestamp: Some(entry.timestamp),
153            attr: entry.attr,
154            size: entry.data.len(),
155        };
156        let entry = parse_nvram_entry(&meta.name, entry.data)?;
157        print_nvram_entry(out, &meta, &entry, truncate).map_err(Error::DataFile)?;
158        count += 1;
159    }
160
161    tracing::info!("Retrieved {count} NVRAM entries");
162    Ok(())
163}
164
165/// Get UEFI variables from a JSON file and write to `output_path`.
166fn dump_nvram_from_json(
167    file_path: impl AsRef<Path>,
168    output_path: Option<impl AsRef<Path>>,
169    truncate: bool,
170) -> Result<(), Error> {
171    tracing::info!("Opening JSON file: {}", file_path.as_ref().display());
172    let file = File::open(file_path.as_ref()).map_err(Error::VmgsFile)?;
173
174    let runtime_state: vmgs_json::RuntimeState = serde_json::from_reader(file)?;
175
176    let nvram_state = runtime_state
177        .devices
178        .get(vmgs_json::BIOS_LOADER_DEVICE_ID)
179        .ok_or(Error::Json("Missing BIOS_LOADER_DEVICE_ID".to_string()))?
180        .states
181        .get("Nvram")
182        .ok_or(Error::Json("Missing Nvram".to_string()))?;
183
184    let vendors = match nvram_state {
185        vmgs_json::State::Nvram { vendors, .. } => vendors,
186        _ => return Err(Error::Json("Nvram state invalid".to_string())),
187    };
188
189    let mut out: Box<dyn Write> = if let Some(path) = output_path {
190        Box::new(File::create(path.as_ref()).map_err(Error::DataFile)?)
191    } else {
192        Box::new(std::io::stdout())
193    };
194
195    let mut count = 0;
196    for (vendor, val) in vendors.iter() {
197        for (name, var) in val.variables.iter() {
198            let meta = NvramEntryMetadata {
199                vendor: vendor.clone(),
200                name: name.clone(),
201                timestamp: None,
202                attr: var.attributes,
203                size: var.data.len(),
204            };
205            let entry = parse_nvram_entry(&meta.name, &var.data)?;
206            print_nvram_entry(&mut out, &meta, &entry, truncate).map_err(Error::DataFile)?;
207            count += 1;
208        }
209    }
210
211    tracing::info!("Retrieved {count} NVRAM entries");
212    Ok(())
213}
214
215/// Similar to [`uefi_nvram_storage::in_memory::VariableEntry`], but with metadata
216/// members that are easier to manipulate
217struct NvramEntryMetadata {
218    pub vendor: String,
219    pub name: String,
220    pub timestamp: Option<EFI_TIME>,
221    pub attr: u32,
222    pub size: usize,
223}
224
225fn print_nvram_entry(
226    out: &mut impl Write,
227    meta: &NvramEntryMetadata,
228    entry: &ParsedNvramEntry<'_>,
229    truncate: bool,
230) -> std::io::Result<()> {
231    const LINE_WIDTH: usize = 80;
232
233    write!(
234        out,
235        "Vendor: {:?}\nName: {:?}\nAttributes: {:#x}\nSize: {:#x}\n",
236        meta.vendor, meta.name, meta.attr, meta.size,
237    )?;
238
239    if let Some(timestamp) = meta.timestamp {
240        writeln!(out, "Timestamp: {}", timestamp)?;
241    }
242
243    match entry {
244        ParsedNvramEntry::BootOrder(boot_order) => {
245            write!(out, "Boot Order:")?;
246            for x in boot_order {
247                write!(out, " {}", x)?;
248            }
249            writeln!(out)?;
250        }
251        ParsedNvramEntry::Boot(load_option) => {
252            writeln!(
253                out,
254                "Load Option: attributes: {:x}, description: {}",
255                load_option.attributes, load_option.description
256            )?;
257            for path in &load_option.device_paths {
258                writeln!(out, "  - {:x?}", path)?;
259            }
260            if let Some(opt) = load_option.opt {
261                let prefix = "  - opt: ";
262                write!(out, "{}", prefix)?;
263                print_hex_compact(out, opt, truncate.then(|| LINE_WIDTH - prefix.len()))?;
264                writeln!(out)?;
265            }
266        }
267        ParsedNvramEntry::SignatureList(sig_lists) => {
268            writeln!(out, "Signature Lists:")?;
269            for sig in sig_lists {
270                match sig {
271                    SignatureList::Sha256(list) => {
272                        writeln!(out, "  - [Sha256]")?;
273                        for sig in list {
274                            let prefix = format!(
275                                "      - Signature Owner: {} Data: ",
276                                sig.header.signature_owner
277                            );
278                            write!(out, "{}", &prefix)?;
279                            print_hex_compact(
280                                out,
281                                sig.data.0.deref(),
282                                truncate.then(|| LINE_WIDTH - prefix.len()),
283                            )?;
284                            writeln!(out)?;
285                        }
286                    }
287                    SignatureList::X509(sig) => {
288                        let prefix = format!(
289                            "  - [X509] Signature Owner: {} Data: ",
290                            sig.header.signature_owner
291                        );
292                        write!(out, "{}", &prefix)?;
293                        print_hex_compact(
294                            out,
295                            sig.data.0.deref(),
296                            truncate.then(|| LINE_WIDTH - prefix.len()),
297                        )?;
298                        writeln!(out)?;
299                    }
300                }
301            }
302        }
303        ParsedNvramEntry::Unknown(data) => {
304            let prefix = "data: ";
305            write!(out, "{}", prefix)?;
306            print_hex_compact(out, data, truncate.then(|| LINE_WIDTH - prefix.len()))?;
307            writeln!(out)?;
308        }
309    }
310
311    writeln!(out)?;
312
313    Ok(())
314}
315
316fn print_hex_compact(
317    out: &mut impl Write,
318    data: &[u8],
319    truncate: Option<usize>,
320) -> std::io::Result<()> {
321    if let Some(truncate) = truncate {
322        let ellipsis = "...";
323        let num_bytes = (truncate - ellipsis.len()) / 2;
324        for byte in data.iter().take(num_bytes) {
325            write!(out, "{:02x}", byte)?;
326        }
327        if data.len() > num_bytes {
328            write!(out, "{}", ellipsis)?;
329        }
330    } else {
331        for byte in data {
332            write!(out, "{:02x}", byte)?;
333        }
334    }
335
336    Ok(())
337}
338
339async fn vmgs_file_open_nvram(
340    file_path: impl AsRef<Path>,
341    key_path: Option<impl AsRef<Path>>,
342    open_mode: OpenMode,
343) -> Result<HclCompatNvram<VmgsStorageBackend>, Error> {
344    let vmgs = vmgs_file_open(file_path, key_path, open_mode).await?;
345    let encrypted = vmgs.encrypted();
346
347    open_nvram(vmgs, encrypted)
348}
349
350fn open_nvram(vmgs: Vmgs, encrypted: bool) -> Result<HclCompatNvram<VmgsStorageBackend>, Error> {
351    let nvram_storage = HclCompatNvram::new(
352        VmgsStorageBackend::new(vmgs, vmgs::FileId::BIOS_NVRAM, encrypted)
353            .map_err(Error::VmgsStorageBackend)?,
354        None,
355    );
356
357    Ok(nvram_storage)
358}
359
360/// Delete all boot entries in the BIOS NVRAM VMGS file in an attempt to repair a VM that is failing to boot.
361/// This will trigger UEFI to attempt a default boot of all installed devices until one succeeds.
362async fn vmgs_file_remove_boot_entries(
363    file_path: impl AsRef<Path>,
364    key_path: Option<impl AsRef<Path>>,
365    dry_run: bool,
366) -> Result<(), Error> {
367    let mut nvram_storage =
368        vmgs_file_open_nvram(file_path, key_path, OpenMode::ReadWriteRequire).await?;
369
370    if dry_run {
371        tracing::info!("Printing Boot Entries (Dry-run)");
372    } else {
373        tracing::info!("Deleting Boot Entries");
374    }
375
376    let name = Ucs2LeVec::from("BootOrder".to_string());
377    let (_, boot_order_bytes, _) = nvram_storage
378        .get_variable(&name, EFI_GLOBAL_VARIABLE)
379        .await?
380        .ok_or(Error::MissingNvramEntry(name.clone()))?;
381    let boot_order = boot_order::parse_boot_order(&boot_order_bytes)
382        .map_err(uefi_nvram_specvars::ParseError::BootOrder)?;
383
384    if !dry_run {
385        if !nvram_storage
386            .remove_variable(&name, EFI_GLOBAL_VARIABLE)
387            .await?
388        {
389            return Err(Error::MissingNvramEntry(name));
390        }
391    }
392
393    for (i, boot_option_num) in boot_order.enumerate() {
394        let name = Ucs2LeVec::from(format!("Boot{:04x}", boot_option_num));
395        let (_, boot_option_bytes, _) = nvram_storage
396            .get_variable(&name, EFI_GLOBAL_VARIABLE)
397            .await?
398            .ok_or(Error::MissingNvramEntry(name.clone()))?;
399        let boot_option = boot_order::EfiLoadOption::parse(&boot_option_bytes)
400            .map_err(uefi_nvram_specvars::ParseError::BootOrder)?;
401
402        println!("{i}: {}: {:x?}", &name, boot_option);
403
404        if !dry_run {
405            if !nvram_storage
406                .remove_variable(&name, EFI_GLOBAL_VARIABLE)
407                .await?
408            {
409                return Err(Error::MissingNvramEntry(name));
410            }
411        }
412    }
413
414    Ok(())
415}
416
417/// Remove an entry from the BIOS NVRAM VMGS file
418async fn vmgs_file_remove_nvram_entry(
419    file_path: impl AsRef<Path>,
420    key_path: Option<impl AsRef<Path>>,
421    name: String,
422    vendor: String,
423) -> Result<(), Error> {
424    let mut nvram_storage =
425        vmgs_file_open_nvram(file_path, key_path, OpenMode::ReadWriteRequire).await?;
426
427    tracing::info!("Removing variable with name {name} and vendor {vendor}");
428
429    let name = Ucs2LeVec::from(name);
430    let vendor = Guid::from_str(&vendor)?;
431
432    if !nvram_storage.remove_variable(&name, vendor).await? {
433        return Err(Error::MissingNvramEntry(name));
434    }
435
436    Ok(())
437}