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 = vmgs_file_open_nvram(file_path, key_path, OpenMode::ReadOnly).await?;
131
132    let mut out: Box<dyn Write + Send> = if let Some(path) = output_path {
133        Box::new(File::create(path.as_ref()).map_err(Error::DataFile)?)
134    } else {
135        Box::new(std::io::stdout())
136    };
137
138    dump_nvram(&mut nvram_storage, &mut out, truncate).await
139}
140
141async fn dump_nvram(
142    nvram_storage: &mut HclCompatNvram<VmgsStorageBackend>,
143    out: &mut impl Write,
144    truncate: bool,
145) -> Result<(), Error> {
146    let mut count = 0;
147    for entry in nvram_storage.iter() {
148        let meta = NvramEntryMetadata {
149            vendor: entry.vendor.to_string(),
150            name: entry.name.to_string(),
151            timestamp: Some(entry.timestamp),
152            attr: entry.attr,
153            size: entry.data.len(),
154        };
155        let entry = parse_nvram_entry(&meta.name, entry.data)?;
156        print_nvram_entry(out, &meta, &entry, truncate).map_err(Error::DataFile)?;
157        count += 1;
158    }
159
160    eprintln!("Retrieved {count} NVRAM entries");
161    Ok(())
162}
163
164/// Get UEFI variables from a JSON file and write to `output_path`.
165fn dump_nvram_from_json(
166    file_path: impl AsRef<Path>,
167    output_path: Option<impl AsRef<Path>>,
168    truncate: bool,
169) -> Result<(), Error> {
170    eprintln!("Opening JSON file: {}", file_path.as_ref().display());
171    let file = File::open(file_path.as_ref()).map_err(Error::VmgsFile)?;
172
173    let runtime_state: vmgs_json::RuntimeState = serde_json::from_reader(file)?;
174
175    let nvram_state = runtime_state
176        .devices
177        .get(vmgs_json::BIOS_LOADER_DEVICE_ID)
178        .ok_or(Error::Json("Missing BIOS_LOADER_DEVICE_ID".to_string()))?
179        .states
180        .get("Nvram")
181        .ok_or(Error::Json("Missing Nvram".to_string()))?;
182
183    let vendors = match nvram_state {
184        vmgs_json::State::Nvram { vendors, .. } => vendors,
185        _ => return Err(Error::Json("Nvram state invalid".to_string())),
186    };
187
188    let mut out: Box<dyn Write> = if let Some(path) = output_path {
189        Box::new(File::create(path.as_ref()).map_err(Error::DataFile)?)
190    } else {
191        Box::new(std::io::stdout())
192    };
193
194    let mut count = 0;
195    for (vendor, val) in vendors.iter() {
196        for (name, var) in val.variables.iter() {
197            let meta = NvramEntryMetadata {
198                vendor: vendor.clone(),
199                name: name.clone(),
200                timestamp: None,
201                attr: var.attributes,
202                size: var.data.len(),
203            };
204            let entry = parse_nvram_entry(&meta.name, &var.data)?;
205            print_nvram_entry(&mut out, &meta, &entry, truncate).map_err(Error::DataFile)?;
206            count += 1;
207        }
208    }
209
210    eprintln!("Retrieved {count} NVRAM entries");
211    Ok(())
212}
213
214/// Similar to [`uefi_nvram_storage::in_memory::VariableEntry`], but with metadata
215/// members that are easier to manipulate
216struct NvramEntryMetadata {
217    pub vendor: String,
218    pub name: String,
219    pub timestamp: Option<EFI_TIME>,
220    pub attr: u32,
221    pub size: usize,
222}
223
224fn print_nvram_entry(
225    out: &mut impl Write,
226    meta: &NvramEntryMetadata,
227    entry: &ParsedNvramEntry<'_>,
228    truncate: bool,
229) -> std::io::Result<()> {
230    const LINE_WIDTH: usize = 80;
231
232    write!(
233        out,
234        "Vendor: {:?}\nName: {:?}\nAttributes: {:#x}\nSize: {:#x}\n",
235        meta.vendor, meta.name, meta.attr, meta.size,
236    )?;
237
238    if let Some(timestamp) = meta.timestamp {
239        writeln!(out, "Timestamp: {}", timestamp)?;
240    }
241
242    match entry {
243        ParsedNvramEntry::BootOrder(boot_order) => {
244            write!(out, "Boot Order:")?;
245            for x in boot_order {
246                write!(out, " {}", x)?;
247            }
248            writeln!(out)?;
249        }
250        ParsedNvramEntry::Boot(load_option) => {
251            writeln!(
252                out,
253                "Load Option: attributes: {:x}, description: {}",
254                load_option.attributes, load_option.description
255            )?;
256            for path in &load_option.device_paths {
257                writeln!(out, "  - {:x?}", path)?;
258            }
259            if let Some(opt) = load_option.opt {
260                let prefix = "  - opt: ";
261                write!(out, "{}", prefix)?;
262                print_hex_compact(out, opt, truncate.then(|| LINE_WIDTH - prefix.len()))?;
263                writeln!(out)?;
264            }
265        }
266        ParsedNvramEntry::SignatureList(sig_lists) => {
267            writeln!(out, "Signature Lists:")?;
268            for sig in sig_lists {
269                match sig {
270                    SignatureList::Sha256(list) => {
271                        writeln!(out, "  - [Sha256]")?;
272                        for sig in list {
273                            let prefix = format!(
274                                "      - Signature Owner: {} Data: ",
275                                sig.header.signature_owner
276                            );
277                            write!(out, "{}", &prefix)?;
278                            print_hex_compact(
279                                out,
280                                sig.data.0.deref(),
281                                truncate.then(|| LINE_WIDTH - prefix.len()),
282                            )?;
283                            writeln!(out)?;
284                        }
285                    }
286                    SignatureList::X509(sig) => {
287                        let prefix = format!(
288                            "  - [X509] Signature Owner: {} Data: ",
289                            sig.header.signature_owner
290                        );
291                        write!(out, "{}", &prefix)?;
292                        print_hex_compact(
293                            out,
294                            sig.data.0.deref(),
295                            truncate.then(|| LINE_WIDTH - prefix.len()),
296                        )?;
297                        writeln!(out)?;
298                    }
299                }
300            }
301        }
302        ParsedNvramEntry::Unknown(data) => {
303            let prefix = "data: ";
304            write!(out, "{}", prefix)?;
305            print_hex_compact(out, data, truncate.then(|| LINE_WIDTH - prefix.len()))?;
306            writeln!(out)?;
307        }
308    }
309
310    writeln!(out)?;
311
312    Ok(())
313}
314
315fn print_hex_compact(
316    out: &mut impl Write,
317    data: &[u8],
318    truncate: Option<usize>,
319) -> std::io::Result<()> {
320    if let Some(truncate) = truncate {
321        let ellipsis = "...";
322        let num_bytes = (truncate - ellipsis.len()) / 2;
323        for byte in data.iter().take(num_bytes) {
324            write!(out, "{:02x}", byte)?;
325        }
326        if data.len() > num_bytes {
327            write!(out, "{}", ellipsis)?;
328        }
329    } else {
330        for byte in data {
331            write!(out, "{:02x}", byte)?;
332        }
333    }
334
335    Ok(())
336}
337
338async fn vmgs_file_open_nvram(
339    file_path: impl AsRef<Path>,
340    key_path: Option<impl AsRef<Path>>,
341    open_mode: OpenMode,
342) -> Result<HclCompatNvram<VmgsStorageBackend>, Error> {
343    let vmgs = vmgs_file_open(file_path, key_path, open_mode).await?;
344    let encrypted = vmgs.is_encrypted();
345
346    open_nvram(vmgs, encrypted).await
347}
348
349async fn open_nvram(
350    vmgs: Vmgs,
351    encrypted: bool,
352) -> Result<HclCompatNvram<VmgsStorageBackend>, Error> {
353    Ok(HclCompatNvram::new(
354        VmgsStorageBackend::new(vmgs, vmgs::FileId::BIOS_NVRAM, encrypted)
355            .map_err(Error::VmgsStorageBackend)?,
356        None,
357        false,
358    )
359    .await?)
360}
361
362/// Delete all boot entries in the BIOS NVRAM VMGS file in an attempt to repair a VM that is failing to boot.
363/// This will trigger UEFI to attempt a default boot of all installed devices until one succeeds.
364async fn vmgs_file_remove_boot_entries(
365    file_path: impl AsRef<Path>,
366    key_path: Option<impl AsRef<Path>>,
367    dry_run: bool,
368) -> Result<(), Error> {
369    let mut nvram_storage = vmgs_file_open_nvram(file_path, key_path, OpenMode::ReadWrite).await?;
370
371    if dry_run {
372        eprintln!("Printing Boot Entries (Dry-run)");
373    } else {
374        eprintln!("Deleting Boot Entries");
375    }
376
377    let name = Ucs2LeVec::from("BootOrder".to_string());
378    let (_, boot_order_bytes, _) = nvram_storage
379        .get_variable(&name, EFI_GLOBAL_VARIABLE)
380        .await?
381        .ok_or(Error::MissingNvramEntry(name.clone()))?;
382    let boot_order = boot_order::parse_boot_order(&boot_order_bytes)
383        .map_err(uefi_nvram_specvars::ParseError::BootOrder)?;
384
385    if !dry_run {
386        if !nvram_storage
387            .remove_variable(&name, EFI_GLOBAL_VARIABLE)
388            .await?
389        {
390            return Err(Error::MissingNvramEntry(name));
391        }
392    }
393
394    for (i, boot_option_num) in boot_order.enumerate() {
395        let name = Ucs2LeVec::from(format!("Boot{:04x}", boot_option_num));
396        let (_, boot_option_bytes, _) = nvram_storage
397            .get_variable(&name, EFI_GLOBAL_VARIABLE)
398            .await?
399            .ok_or(Error::MissingNvramEntry(name.clone()))?;
400        let boot_option = boot_order::EfiLoadOption::parse(&boot_option_bytes)
401            .map_err(uefi_nvram_specvars::ParseError::BootOrder)?;
402
403        println!("{i}: {}: {:x?}", &name, boot_option);
404
405        if !dry_run {
406            if !nvram_storage
407                .remove_variable(&name, EFI_GLOBAL_VARIABLE)
408                .await?
409            {
410                return Err(Error::MissingNvramEntry(name));
411            }
412        }
413    }
414
415    Ok(())
416}
417
418/// Remove an entry from the BIOS NVRAM VMGS file
419async fn vmgs_file_remove_nvram_entry(
420    file_path: impl AsRef<Path>,
421    key_path: Option<impl AsRef<Path>>,
422    name: String,
423    vendor: String,
424) -> Result<(), Error> {
425    let mut nvram_storage = vmgs_file_open_nvram(file_path, key_path, OpenMode::ReadWrite).await?;
426
427    eprintln!("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}