Skip to main content

igvmfilegen/
firmware_dll.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Read an IGVM image from either a raw IGVM file or a `vmfirmwareigvm`
5//! resource DLL.
6//!
7//! Production OpenHCL IGVM files are shipped encapsulated in a Windows
8//! resource-only DLL (`vmfirmwareigvm.dll` / `vmfirmwarecvm.dll`), where the
9//! IGVM payload is stored as a custom `VMFW` resource with id `1` -- see the
10//! `1 VMFW <igvm>` entry in `openhcl/vmfirmwareigvm_dll/resources.rc`. To let
11//! `dump` / `dump-corim` operate directly on those shipped DLLs (e.g. to
12//! confirm a CoRIM is present in the packaged firmware), this module detects a
13//! PE input and extracts the embedded IGVM from it.
14
15use anyhow::Context;
16use std::path::Path;
17
18/// Custom PE resource *type* name under which the IGVM payload is stored in a
19/// `vmfirmwareigvm` resource DLL (the `1 VMFW <igvm>` entry in `resources.rc`).
20const VMFW_RESOURCE_TYPE: &str = "VMFW";
21
22/// Read an IGVM image from `path`, transparently extracting it from a
23/// `vmfirmwareigvm` resource DLL when `path` points at a PE/DLL rather than a
24/// raw IGVM file.
25///
26/// A resource DLL is a PE image beginning with the `MZ` signature; a raw IGVM
27/// file does not. When a PE is detected, the embedded `VMFW` resource is
28/// returned; otherwise the file bytes are returned unchanged.
29pub fn read_igvm_image(path: &Path) -> anyhow::Result<Vec<u8>> {
30    let bytes = fs_err::read(path).context("reading input file")?;
31    if bytes.starts_with(b"MZ") {
32        extract_vmfw_resource(&bytes).with_context(|| {
33            format!(
34                "extracting embedded IGVM ({VMFW_RESOURCE_TYPE} resource) from resource DLL {}",
35                path.display()
36            )
37        })
38    } else {
39        Ok(bytes)
40    }
41}
42
43/// Extract the embedded IGVM payload (the `VMFW` resource) from a PE/DLL image.
44fn extract_vmfw_resource(pe_bytes: &[u8]) -> anyhow::Result<Vec<u8>> {
45    use object::read::pe::PeFile64;
46    use object::read::pe::ResourceDirectoryEntryData;
47    use object::read::pe::ResourceNameOrId;
48
49    let pe =
50        PeFile64::parse(pe_bytes).context("parsing PE image (expected a 64-bit resource DLL)")?;
51    let sections = pe.section_table();
52    let dir = pe
53        .data_directories()
54        .resource_directory(pe_bytes, &sections)
55        .context("reading resource directory")?
56        .context("PE image has no resource directory")?;
57    let root = dir.root().context("reading resource directory root")?;
58
59    // The resource tree has three levels: type -> id/name -> language -> data.
60    // Locate the custom `VMFW` resource type, then descend to the first (and
61    // only) id and language entry to reach the IGVM payload.
62    for type_entry in root.entries {
63        let ResourceNameOrId::Name(name) = type_entry.name_or_id() else {
64            // The IGVM is stored under a named type (`VMFW`), not a numeric one.
65            continue;
66        };
67        let raw_name = name.raw_data(dir).context("reading resource type name")?;
68        if !utf16le_eq(raw_name, VMFW_RESOURCE_TYPE) {
69            continue;
70        }
71
72        let ResourceDirectoryEntryData::Table(id_table) = type_entry
73            .data(dir)
74            .context("reading VMFW resource id table")?
75        else {
76            anyhow::bail!("VMFW resource type entry is not a subdirectory");
77        };
78        let id_entry = id_table
79            .entries
80            .first()
81            .context("VMFW resource type has no entries")?;
82
83        let ResourceDirectoryEntryData::Table(lang_table) = id_entry
84            .data(dir)
85            .context("reading VMFW resource language table")?
86        else {
87            anyhow::bail!("VMFW resource id entry is not a subdirectory");
88        };
89        let lang_entry = lang_table
90            .entries
91            .first()
92            .context("VMFW resource has no language entries")?;
93
94        let ResourceDirectoryEntryData::Data(data) = lang_entry
95            .data(dir)
96            .context("reading VMFW resource data entry")?
97        else {
98            anyhow::bail!("VMFW resource language entry is not a data entry");
99        };
100
101        // The data entry references the payload by RVA + size; resolve it via
102        // the section table and take exactly `size` bytes.
103        let rva = data.offset_to_data.get(object::LittleEndian);
104        let size = data.size.get(object::LittleEndian) as usize;
105        let payload = sections
106            .pe_data_at(pe_bytes, rva)
107            .context("resolving VMFW resource RVA in section table")?
108            .get(..size)
109            .context("VMFW resource size extends past its section")?;
110        return Ok(payload.to_vec());
111    }
112
113    anyhow::bail!(
114        "no '{VMFW_RESOURCE_TYPE}' resource found; input does not look like a vmfirmwareigvm resource DLL"
115    )
116}
117
118/// Compare a little-endian UTF-16 PE resource name to an ASCII string.
119fn utf16le_eq(utf16le: &[u8], ascii: &str) -> bool {
120    if utf16le.len() != ascii.len() * 2 {
121        return false;
122    }
123    utf16le
124        .chunks_exact(2)
125        .zip(ascii.chars())
126        .all(|(pair, c)| u16::from_le_bytes([pair[0], pair[1]]) == c as u16)
127}
128
129#[cfg(test)]
130mod tests {
131    use super::utf16le_eq;
132
133    fn utf16le(s: &str) -> Vec<u8> {
134        s.encode_utf16().flat_map(u16::to_le_bytes).collect()
135    }
136
137    #[test]
138    fn utf16le_eq_matches() {
139        assert!(utf16le_eq(&utf16le("VMFW"), "VMFW"));
140    }
141
142    #[test]
143    fn utf16le_eq_rejects_mismatch_and_length() {
144        assert!(!utf16le_eq(&utf16le("VMFX"), "VMFW"));
145        assert!(!utf16le_eq(&utf16le("VMF"), "VMFW"));
146        assert!(!utf16le_eq(&utf16le("VMFWX"), "VMFW"));
147        // Odd-length (truncated) UTF-16 buffers never match.
148        assert!(!utf16le_eq(&[0x56, 0x00, 0x4d], "VM"));
149    }
150}