1use std::collections::{BTreeMap, BTreeSet};
5
6use object::read::archive::{ArchiveFile, ArchiveKind};
7use object::read::coff::{CoffBigFile, CoffFile, CoffHeader, ImageSymbol};
8use object::{FileKind, Object, ObjectSection, ObjectSymbol, SectionFlags, SymbolFlags};
9use serde::{Deserialize, Serialize};
10use sha2::{Digest, Sha256};
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct NativeOperatorObjectIdentity {
14 pub format: NativeOperatorObjectFormat,
15 pub class_bits: u8,
16 pub endianness: NativeOperatorObjectEndianness,
17 pub machine: u32,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum NativeOperatorObjectFormat {
23 Elf,
24 MachO,
25 Coff,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum NativeOperatorObjectEndianness {
31 Little,
32 Big,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct NativeOperatorObjectInspection {
37 pub identity: NativeOperatorObjectIdentity,
38 pub defined_symbols: Vec<String>,
39 pub strong_defined_symbols: Vec<String>,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct NativeOperatorArchiveMember {
44 pub name: String,
45 pub bytes: Vec<u8>,
46 pub sha256: String,
47 pub object: NativeOperatorObjectInspection,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct NativeOperatorArchiveInspection {
52 pub identity: NativeOperatorObjectIdentity,
53 pub members: Vec<NativeOperatorArchiveMember>,
54 pub defined_symbols: Vec<String>,
55 pub strong_defined_symbols: Vec<String>,
56 pub symbol_definition_counts: BTreeMap<String, usize>,
57 pub indexed_symbol_members: BTreeMap<String, Vec<String>>,
58}
59
60impl NativeOperatorArchiveInspection {
61 pub fn require_unique_exports(&self, exports: &[String]) -> Result<(), String> {
63 for symbol in exports {
64 if self.symbol_definition_counts.get(symbol).copied() != Some(1) {
65 return Err(format!(
66 "MSVC archive must define required export exactly once: {symbol}"
67 ));
68 }
69 let defining_member = self
70 .members
71 .iter()
72 .find(|member| member.object.defined_symbols.contains(symbol))
73 .expect("definition count checked");
74 if !self
75 .indexed_symbol_members
76 .get(symbol)
77 .is_some_and(|members| members.len() == 1 && members[0] == defining_member.name)
78 {
79 return Err(format!("MSVC linker index does not resolve required export to its real definition: {symbol}"));
80 }
81 }
82 Ok(())
83 }
84}
85
86pub fn inspect_msvc_object(
87 bytes: &[u8],
88 expected_target: &str,
89) -> Result<NativeOperatorObjectInspection, String> {
90 let host = ferrum_types::NativeOperatorHostAbi::for_target(expected_target)?;
91 if host.compiler_flavor != ferrum_types::NativeOperatorCompilerFlavor::Msvc {
92 return Err("COFF inspection requires an explicit supported MSVC target".into());
93 }
94 match FileKind::parse(bytes).map_err(|error| format!("invalid COFF object: {error}"))? {
95 FileKind::Coff => {
96 if bytes.get(16..18) != Some(&[0, 0][..]) {
99 return Err("COFF object has an image optional header".into());
100 }
101 let file: CoffFile<'_> = CoffFile::parse(bytes).map_err(|error| error.to_string())?;
102 inspect_coff(file)
103 }
104 FileKind::CoffBig => {
105 inspect_coff(CoffBigFile::parse(bytes).map_err(|error| error.to_string())?)
106 }
107 FileKind::CoffImport => {
108 Err("short COFF import library member is not a native implementation".into())
109 }
110 other => Err(format!(
111 "MSVC native implementation must be COFF/bigobj, got {other:?}"
112 )),
113 }
114}
115
116fn inspect_coff<'data, C: CoffHeader>(
117 file: CoffFile<'data, &'data [u8], C>,
118) -> Result<NativeOperatorObjectInspection, String> {
119 let header = file.coff_header();
120 if header.machine() != object::pe::IMAGE_FILE_MACHINE_AMD64 {
121 return Err(format!(
122 "COFF machine does not match x86_64-pc-windows-msvc: {:#x}",
123 header.machine()
124 ));
125 }
126 if header.number_of_sections() == 0 {
127 return Err("COFF implementation has no sections".into());
128 }
129 if header.characteristics()
130 & (object::pe::IMAGE_FILE_EXECUTABLE_IMAGE | object::pe::IMAGE_FILE_DLL)
131 != 0
132 {
133 return Err("COFF native member is marked as an executable image".into());
134 }
135 let mut comdat_sections = BTreeSet::new();
136 for section in file.sections() {
137 let name = section.name().map_err(|error| error.to_string())?;
138 let data = section.data().map_err(|error| error.to_string())?;
139 if name == ".idata" || name.starts_with(".idata$") {
143 return Err(format!("long COFF import library member contains {name}"));
144 }
145 if name == ".drectve" {
146 validate_runtime_directives(data)?;
147 }
148 if matches!(section.flags(), SectionFlags::Coff { characteristics }
149 if characteristics & object::pe::IMAGE_SCN_LNK_COMDAT != 0)
150 {
151 comdat_sections.insert(section.index().0);
152 }
153 section
154 .coff_relocations()
155 .map_err(|error| error.to_string())?;
156 }
157 let mut selections = BTreeMap::new();
158 let mut primary_symbols = BTreeSet::new();
159 for symbol in file.symbols() {
160 let raw = symbol.coff_symbol();
161 if symbol.index().0 + 1 + usize::from(raw.number_of_aux_symbols())
162 > header.number_of_symbols() as usize
163 {
164 return Err("COFF symbol has a truncated auxiliary record".into());
165 }
166 primary_symbols.insert(symbol.index().0);
167 let name = symbol.name().map_err(|error| error.to_string())?;
168 if name.starts_with("__IMPORT_DESCRIPTOR_")
169 || name == "__NULL_IMPORT_DESCRIPTOR"
170 || name.ends_with("_NULL_THUNK_DATA")
171 {
172 return Err(format!("long COFF import library marker: {name}"));
173 }
174 if let Some(index) = symbol.section_index() {
175 file.section_by_index(index)
176 .map_err(|error| error.to_string())?;
177 if let SymbolFlags::CoffSection {
178 selection,
179 associative_section,
180 } = symbol.flags()
181 {
182 if comdat_sections.contains(&index.0) {
183 if !(1..=7).contains(&selection)
184 || selections.insert(index.0, selection).is_some()
185 {
186 return Err("invalid or duplicate COFF COMDAT selection".into());
187 }
188 if selection == object::pe::IMAGE_COMDAT_SELECT_ASSOCIATIVE {
189 let associated =
190 associative_section.ok_or("associative COMDAT has no parent")?;
191 if associated == index || !comdat_sections.contains(&associated.0) {
192 return Err("associative COMDAT has an invalid parent".into());
193 }
194 }
195 }
196 }
197 }
198 }
199 if comdat_sections
200 .iter()
201 .any(|index| !selections.contains_key(index))
202 {
203 return Err("COFF COMDAT section has no valid selection record".into());
204 }
205 for section in file.sections() {
206 for relocation in section
207 .coff_relocations()
208 .map_err(|error| error.to_string())?
209 {
210 if relocation.typ.get(object::LittleEndian) != 0
211 && !primary_symbols
212 .contains(&(relocation.symbol_table_index.get(object::LittleEndian) as usize))
213 {
214 return Err("COFF relocation does not reference a real symbol".into());
215 }
216 }
217 }
218 let mut defined = BTreeSet::new();
219 let mut strong = BTreeSet::new();
220 for symbol in file.symbols() {
221 if !symbol.is_global() || !symbol.is_definition() {
222 continue;
223 }
224 let name = symbol.name().map_err(|error| error.to_string())?;
225 if name.is_empty() || !defined.insert(name.to_owned()) {
226 return Err(format!("empty or repeated COFF definition: {name}"));
227 }
228 let selection = symbol
229 .section_index()
230 .and_then(|index| selections.get(&index.0))
231 .copied();
232 if !symbol.is_weak() && matches!(selection, None | Some(1)) {
233 strong.insert(name.to_owned());
234 }
235 }
236 Ok(NativeOperatorObjectInspection {
237 identity: NativeOperatorObjectIdentity {
238 format: NativeOperatorObjectFormat::Coff,
239 class_bits: 64,
240 endianness: NativeOperatorObjectEndianness::Little,
241 machine: u32::from(header.machine()),
242 },
243 defined_symbols: defined.into_iter().collect(),
244 strong_defined_symbols: strong.into_iter().collect(),
245 })
246}
247
248fn validate_runtime_directives(bytes: &[u8]) -> Result<(), String> {
249 let text = std::str::from_utf8(bytes).map_err(|_| "COFF linker directives are not UTF-8")?;
250 let mut tokens = Vec::new();
251 let mut token = String::new();
252 let mut quoted = false;
253 for ch in text.chars() {
254 if ch == '"' {
255 quoted = !quoted;
256 } else if (ch.is_ascii_whitespace() || ch == '\0') && !quoted {
257 if !token.is_empty() {
258 tokens.push(std::mem::take(&mut token));
259 }
260 } else {
261 token.push(ch);
262 }
263 }
264 if quoted {
265 return Err("COFF linker directive contains an unterminated quote".into());
266 }
267 if !token.is_empty() {
268 tokens.push(token);
269 }
270 for token in tokens {
271 let token = token.to_ascii_lowercase();
272 if let Some(library) = token
273 .strip_prefix("/defaultlib:")
274 .or_else(|| token.strip_prefix("-defaultlib:"))
275 {
276 let library = library.rsplit(['/', '\\']).next().unwrap_or(library);
277 let library = library.strip_suffix(".lib").unwrap_or(library);
278 if matches!(
279 library,
280 "libcmt"
281 | "libcmtd"
282 | "libcpmt"
283 | "libcpmtd"
284 | "msvcrtd"
285 | "msvcprtd"
286 | "vcruntimed"
287 | "ucrtd"
288 | "libvcruntime"
289 | "libvcruntimed"
290 | "libucrt"
291 | "libucrtd"
292 ) {
293 return Err(format!(
294 "COFF runtime directive conflicts with release /MD: {token}"
295 ));
296 }
297 }
298 if let Some(mismatch) = token
299 .strip_prefix("/failifmismatch:")
300 .or_else(|| token.strip_prefix("-failifmismatch:"))
301 {
302 if let Some(runtime) = mismatch.strip_prefix("runtimelibrary=") {
303 if runtime != "md_dynamicrelease" {
304 return Err(format!(
305 "COFF RuntimeLibrary conflicts with release /MD: {token}"
306 ));
307 }
308 }
309 }
310 }
311 Ok(())
312}
313
314pub fn inspect_msvc_archive(
315 bytes: &[u8],
316 expected_target: &str,
317) -> Result<NativeOperatorArchiveInspection, String> {
318 let archive =
319 ArchiveFile::parse(bytes).map_err(|error| format!("invalid MSVC archive: {error}"))?;
320 if archive.is_thin() || !bytes.starts_with(b"!<arch>\n") {
321 return Err("MSVC native archive must be self-contained, not thin".into());
322 }
323 if archive.kind() != ArchiveKind::Coff {
324 return Err("MSVC library is missing its two COFF linker members".into());
325 }
326 let mut members = Vec::new();
327 let mut names = BTreeSet::new();
328 let mut counts = BTreeMap::new();
329 let mut strong = BTreeSet::new();
330 let mut offsets = BTreeMap::new();
331 for member in archive.members() {
332 let member = member.map_err(|error| error.to_string())?;
333 let name =
334 std::str::from_utf8(member.name()).map_err(|_| "archive member name is not UTF-8")?;
335 if name.is_empty() || name.contains('\0') || !names.insert(name.to_owned()) {
336 return Err(format!("empty or duplicate MSVC archive member: {name}"));
337 }
338 let data = member.data(bytes).map_err(|error| error.to_string())?;
339 let offset = member
340 .file_range()
341 .0
342 .checked_sub(60)
343 .ok_or("invalid COFF member offset")?;
344 offsets.insert(offset, name.to_owned());
345 let object = inspect_msvc_object(data, expected_target)
346 .map_err(|error| format!("{name}: {error}"))?;
347 for symbol in &object.defined_symbols {
348 *counts.entry(symbol.clone()).or_insert(0usize) += 1;
349 }
350 for symbol in &object.strong_defined_symbols {
351 if !strong.insert(symbol.clone()) {
352 return Err(format!(
353 "duplicate strong MSVC archive definition: {symbol}"
354 ));
355 }
356 }
357 members.push(NativeOperatorArchiveMember {
358 name: name.to_owned(),
359 bytes: data.to_vec(),
360 sha256: format!("{:x}", Sha256::digest(data)),
361 object,
362 });
363 }
364 let identity = members
365 .first()
366 .ok_or("MSVC native archive has no object members")?
367 .object
368 .identity
369 .clone();
370 let mut indexed_symbol_members: BTreeMap<String, Vec<String>> = BTreeMap::new();
371 for symbol in archive
372 .symbols()
373 .map_err(|error| error.to_string())?
374 .ok_or("MSVC archive has no linker symbol table")?
375 {
376 let symbol = symbol.map_err(|error| error.to_string())?;
377 let name =
378 std::str::from_utf8(symbol.name()).map_err(|_| "COFF linker symbol is not UTF-8")?;
379 if name.is_empty() {
380 return Err("COFF linker symbol is empty".into());
381 }
382 let member = offsets
383 .get(&symbol.offset().0)
384 .ok_or("COFF linker symbol points outside object members")?;
385 indexed_symbol_members
386 .entry(name.to_owned())
387 .or_default()
388 .push(member.clone());
389 }
390 Ok(NativeOperatorArchiveInspection {
391 identity,
392 members,
393 defined_symbols: counts.keys().cloned().collect(),
394 strong_defined_symbols: strong.into_iter().collect(),
395 symbol_definition_counts: counts,
396 indexed_symbol_members,
397 })
398}
399
400#[cfg(test)]
401#[path = "coff_tests.rs"]
402pub(crate) mod tests;