Rust · 19953 bytes Raw Blame History
1 //! Binary Mach-O dynamic library reader (`MH_DYLIB`).
2 //!
3 //! Sprint 5 lifts the dylib's self-identification (`LC_ID_DYLIB`), its
4 //! dependency chain (`LC_LOAD_DYLIB` / `LC_LOAD_WEAK_DYLIB` /
5 //! `LC_REEXPORT_DYLIB` / `LC_LOAD_UPWARD_DYLIB`), its runtime search paths
6 //! (`LC_RPATH`), and the export trie. Sprint 6 layers TBD text stubs onto
7 //! this same `DylibFile` surface so callers don't care whether a dylib came
8 //! from a real `.dylib` or a `.tbd` fixture.
9
10 use std::path::PathBuf;
11
12 use super::constants::*;
13 use super::exports::{ExportEntry, ExportKind, Exports};
14 use super::reader::{
15 parse_commands, parse_header, LoadCommand, MachHeader64, ReadError, SymtabCmd,
16 };
17 use super::tbd::{parse_version, SymbolLists, Target, Tbd};
18
19 const DEFAULT_TBD_VERSION: u32 = 1 << 16;
20
21 /// How a consumer loaded this dylib. The filetype of the dylib itself is
22 /// always `MH_DYLIB`; this kind captures the *relationship*.
23 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
24 pub enum DylibLoadKind {
25 Normal,
26 Weak,
27 Reexport,
28 Upward,
29 }
30
31 impl DylibLoadKind {
32 pub fn from_cmd(cmd: u32) -> Option<Self> {
33 match cmd {
34 LC_LOAD_DYLIB => Some(DylibLoadKind::Normal),
35 LC_LOAD_WEAK_DYLIB => Some(DylibLoadKind::Weak),
36 LC_REEXPORT_DYLIB => Some(DylibLoadKind::Reexport),
37 LC_LOAD_UPWARD_DYLIB => Some(DylibLoadKind::Upward),
38 _ => None,
39 }
40 }
41
42 pub fn load_cmd(self) -> u32 {
43 match self {
44 DylibLoadKind::Normal => LC_LOAD_DYLIB,
45 DylibLoadKind::Weak => LC_LOAD_WEAK_DYLIB,
46 DylibLoadKind::Reexport => LC_REEXPORT_DYLIB,
47 DylibLoadKind::Upward => LC_LOAD_UPWARD_DYLIB,
48 }
49 }
50 }
51
52 /// One dylib this file depends on. Ordinals match the two-level namespace
53 /// convention: they're 1-based positions in command-line / load-command
54 /// order, encoded into undefined symbols' `n_desc` high byte so dyld knows
55 /// which dylib to bind from.
56 #[derive(Debug, Clone, PartialEq, Eq)]
57 pub struct DylibDependency {
58 pub kind: DylibLoadKind,
59 pub install_name: String,
60 pub current_version: u32,
61 pub compatibility_version: u32,
62 /// 1-based ordinal into the dependency list.
63 pub ordinal: u16,
64 }
65
66 #[derive(Debug)]
67 pub struct DylibFile {
68 pub path: PathBuf,
69 pub header: MachHeader64,
70 pub commands: Vec<LoadCommand>,
71 pub install_name: String,
72 pub current_version: u32,
73 pub compatibility_version: u32,
74 pub dependencies: Vec<DylibDependency>,
75 pub rpaths: Vec<String>,
76 pub symtab: Option<SymtabCmd>,
77 pub exports: Exports,
78 }
79
80 impl DylibFile {
81 /// Parse an MH_DYLIB from its raw bytes.
82 pub fn parse(path: impl Into<PathBuf>, file_bytes: &[u8]) -> Result<Self, ReadError> {
83 let path = path.into();
84 let header = parse_header(file_bytes)?;
85 if header.filetype != MH_DYLIB {
86 return Err(ReadError::BadCmdsize {
87 cmd: 0,
88 cmdsize: header.filetype,
89 at_offset: 0,
90 reason: "DylibFile::parse expects MH_DYLIB filetype",
91 });
92 }
93 let commands = parse_commands(&header, file_bytes)?;
94
95 let mut install_name = String::new();
96 let mut current_version = 0u32;
97 let mut compatibility_version = 0u32;
98 let mut dependencies: Vec<DylibDependency> = Vec::new();
99 let mut rpaths: Vec<String> = Vec::new();
100 let mut symtab: Option<SymtabCmd> = None;
101
102 for cmd in &commands {
103 match cmd {
104 LoadCommand::Dylib(d) if d.cmd == LC_ID_DYLIB => {
105 install_name = d.name.clone();
106 current_version = d.current_version;
107 compatibility_version = d.compatibility_version;
108 }
109 LoadCommand::Dylib(d) => {
110 if let Some(kind) = DylibLoadKind::from_cmd(d.cmd) {
111 let ordinal = (dependencies.len() + 1) as u16;
112 dependencies.push(DylibDependency {
113 kind,
114 install_name: d.name.clone(),
115 current_version: d.current_version,
116 compatibility_version: d.compatibility_version,
117 ordinal,
118 });
119 }
120 }
121 LoadCommand::Rpath(r) => rpaths.push(r.path.clone()),
122 LoadCommand::Symtab(s) => symtab = Some(*s),
123 _ => {}
124 }
125 }
126
127 let exports = locate_exports(&commands, file_bytes)?;
128
129 Ok(DylibFile {
130 path,
131 header,
132 commands,
133 install_name,
134 current_version,
135 compatibility_version,
136 dependencies,
137 rpaths,
138 symtab,
139 exports,
140 })
141 }
142 }
143
144 /// Locate the export-trie bytes in either `LC_DYLD_INFO_ONLY.export_*` or
145 /// `LC_DYLD_EXPORTS_TRIE` (chained-fixups era). Dylibs built by older
146 /// toolchains may have no export trie; in that case return an empty
147 /// `Exports::Flat(vec![])` so downstream `entries()` works uniformly.
148 fn locate_exports(commands: &[LoadCommand], file_bytes: &[u8]) -> Result<Exports, ReadError> {
149 for cmd in commands {
150 match cmd {
151 LoadCommand::DyldInfoOnly(d) if d.export_size != 0 => {
152 return trie_slice(file_bytes, d.export_off, d.export_size);
153 }
154 LoadCommand::DyldExportsTrie(l) if l.datasize != 0 => {
155 return trie_slice(file_bytes, l.dataoff, l.datasize);
156 }
157 _ => {}
158 }
159 }
160 Ok(Exports::empty())
161 }
162
163 fn trie_slice(file_bytes: &[u8], off: u32, size: u32) -> Result<Exports, ReadError> {
164 let start = off as usize;
165 let end = start
166 .checked_add(size as usize)
167 .ok_or(ReadError::Truncated {
168 need: usize::MAX,
169 have: file_bytes.len(),
170 context: "export trie (offset + size overflows)",
171 })?;
172 if end > file_bytes.len() {
173 return Err(ReadError::Truncated {
174 need: end,
175 have: file_bytes.len(),
176 context: "export trie",
177 });
178 }
179 Ok(Exports::from_trie_bytes(&file_bytes[start..end]))
180 }
181
182 impl DylibFile {
183 /// Materialize a TBD document as a `DylibFile` specialized for a single
184 /// target. Scoped lists (`exports`, `reexported-libraries`, `rpaths`
185 /// via parent-umbrella) narrow to entries whose target set includes
186 /// `target`. Symbol kinds map to ObjC-class / ObjC-eh-type / ObjC-ivar /
187 /// thread-local / weak / regular in `ExportKind::Regular` form —
188 /// addresses are zero because TBD stubs don't commit to them.
189 pub fn from_tbd(path: impl Into<PathBuf>, tbd: &Tbd, target: &Target) -> Self {
190 let mut entries: Vec<ExportEntry> = Vec::new();
191 for scoped in &tbd.exports {
192 if !scope_matches(&scoped.targets, target) {
193 continue;
194 }
195 append_entries(&scoped.value, &mut entries);
196 }
197 // re-exported symbols from peer dylibs surface the same way —
198 // dyld treats them as part of this dylib's export surface.
199 for scoped in &tbd.reexports {
200 if !scope_matches(&scoped.targets, target) {
201 continue;
202 }
203 append_entries(&scoped.value, &mut entries);
204 }
205
206 let mut dependencies = Vec::new();
207 let mut ordinal: u16 = 1;
208 for scoped in &tbd.reexported_libraries {
209 if !scope_matches(&scoped.targets, target) {
210 continue;
211 }
212 for install_name in &scoped.value {
213 dependencies.push(DylibDependency {
214 kind: DylibLoadKind::Reexport,
215 install_name: install_name.clone(),
216 current_version: 0,
217 compatibility_version: 0,
218 ordinal,
219 });
220 ordinal += 1;
221 }
222 }
223
224 DylibFile {
225 path: path.into(),
226 // TBDs have no binary header; synth a minimal one so downstream
227 // consumers that only read `install_name` / versions don't care
228 // whether they got a binary or a stub.
229 header: synthetic_header(),
230 commands: Vec::new(),
231 install_name: tbd.install_name.clone(),
232 current_version: tbd
233 .current_version
234 .as_deref()
235 .map(parse_version)
236 .unwrap_or(DEFAULT_TBD_VERSION),
237 compatibility_version: tbd
238 .compatibility_version
239 .as_deref()
240 .map(parse_version)
241 .unwrap_or(DEFAULT_TBD_VERSION),
242 dependencies,
243 rpaths: Vec::new(),
244 symtab: None,
245 exports: Exports::from_entries(entries),
246 }
247 }
248 }
249
250 fn scope_matches(targets: &[Target], wanted: &Target) -> bool {
251 targets.iter().any(|t| t.matches_requested(wanted))
252 }
253
254 fn append_entries(lists: &SymbolLists, out: &mut Vec<ExportEntry>) {
255 for n in &lists.symbols {
256 out.push(regular_entry(n, 0));
257 }
258 for n in &lists.weak_symbols {
259 out.push(weak_entry(n));
260 }
261 for n in &lists.thread_local_symbols {
262 out.push(tls_entry(n));
263 }
264 // ObjC classes ship as `_OBJC_CLASS_$_<name>` externs in a real dylib.
265 for n in &lists.objc_classes {
266 let full = format!("_OBJC_CLASS_$_{n}");
267 out.push(regular_entry(&full, 0));
268 }
269 for n in &lists.objc_eh_types {
270 let full = format!("_OBJC_EHTYPE_$_{n}");
271 out.push(regular_entry(&full, 0));
272 }
273 for n in &lists.objc_ivars {
274 let full = format!("_OBJC_IVAR_$_{n}");
275 out.push(regular_entry(&full, 0));
276 }
277 }
278
279 fn regular_entry(name: &str, flags: u64) -> ExportEntry {
280 ExportEntry {
281 name: name.to_string(),
282 flags,
283 kind: ExportKind::Regular { address: 0 },
284 }
285 }
286
287 fn weak_entry(name: &str) -> ExportEntry {
288 ExportEntry {
289 name: name.to_string(),
290 flags: EXPORT_SYMBOL_FLAGS_WEAK_DEFINITION,
291 kind: ExportKind::Regular { address: 0 },
292 }
293 }
294
295 fn tls_entry(name: &str) -> ExportEntry {
296 ExportEntry {
297 name: name.to_string(),
298 flags: EXPORT_SYMBOL_FLAGS_KIND_THREAD_LOCAL,
299 kind: ExportKind::ThreadLocal { address: 0 },
300 }
301 }
302
303 fn synthetic_header() -> MachHeader64 {
304 MachHeader64 {
305 magic: MH_MAGIC_64,
306 cputype: CPU_TYPE_ARM64,
307 cpusubtype: 0,
308 filetype: MH_DYLIB,
309 ncmds: 0,
310 sizeofcmds: 0,
311 flags: MH_DYLDLINK | MH_TWOLEVEL,
312 reserved: 0,
313 }
314 }
315
316 /// Look up the 1-based ordinal of a dependency by its install name. Used by
317 /// Sprint 14's symbol-table writer when encoding each undefined symbol's
318 /// two-level-namespace library ordinal into its `n_desc` high byte.
319 pub fn dependency_ordinal(deps: &[DylibDependency], install_name: &str) -> Option<u16> {
320 deps.iter()
321 .find(|d| d.install_name == install_name)
322 .map(|d| d.ordinal)
323 }
324
325 #[cfg(test)]
326 mod tests {
327 use super::*;
328 use crate::macho::reader::{write_commands, write_header, DylibCmd, RpathCmd};
329
330 fn make_dylib_image(commands: Vec<LoadCommand>) -> Vec<u8> {
331 let sizeofcmds: u32 = commands.iter().map(|c| c.cmdsize()).sum();
332 let ncmds = commands.len() as u32;
333 let hdr = MachHeader64 {
334 magic: MH_MAGIC_64,
335 cputype: CPU_TYPE_ARM64,
336 cpusubtype: 0,
337 filetype: MH_DYLIB,
338 ncmds,
339 sizeofcmds,
340 flags: MH_DYLDLINK | MH_TWOLEVEL,
341 reserved: 0,
342 };
343 let mut image = Vec::new();
344 write_header(&hdr, &mut image);
345 write_commands(&commands, &mut image);
346 image
347 }
348
349 fn dylib_cmd(kind: u32, name: &str) -> DylibCmd {
350 DylibCmd {
351 cmd: kind,
352 name: name.into(),
353 timestamp: 2,
354 current_version: 1 << 16,
355 compatibility_version: 1 << 16,
356 }
357 }
358
359 #[test]
360 fn parse_dylib_extracts_install_name_and_versions() {
361 let image = make_dylib_image(vec![LoadCommand::Dylib(DylibCmd {
362 cmd: LC_ID_DYLIB,
363 name: "@rpath/libfoo.dylib".into(),
364 timestamp: 2,
365 current_version: (1 << 16) | (2 << 8) | 3,
366 compatibility_version: 1 << 16,
367 })]);
368 let dy = DylibFile::parse("/tmp/libfoo.dylib", &image).unwrap();
369 assert_eq!(dy.install_name, "@rpath/libfoo.dylib");
370 assert_eq!(dy.current_version, (1 << 16) | (2 << 8) | 3);
371 assert_eq!(dy.compatibility_version, 1 << 16);
372 }
373
374 #[test]
375 fn parse_dylib_assigns_ordinals_to_dependencies_in_order() {
376 let image = make_dylib_image(vec![
377 LoadCommand::Dylib(dylib_cmd(LC_ID_DYLIB, "@rpath/libself.dylib")),
378 LoadCommand::Dylib(dylib_cmd(LC_LOAD_DYLIB, "/usr/lib/libSystem.B.dylib")),
379 LoadCommand::Dylib(dylib_cmd(LC_LOAD_WEAK_DYLIB, "/usr/lib/libobjc.A.dylib")),
380 LoadCommand::Dylib(dylib_cmd(LC_REEXPORT_DYLIB, "/usr/lib/libc++abi.dylib")),
381 ]);
382 let dy = DylibFile::parse("/tmp/x.dylib", &image).unwrap();
383 assert_eq!(dy.install_name, "@rpath/libself.dylib");
384 assert_eq!(dy.dependencies.len(), 3);
385 assert_eq!(dy.dependencies[0].kind, DylibLoadKind::Normal);
386 assert_eq!(dy.dependencies[0].ordinal, 1);
387 assert_eq!(dy.dependencies[1].kind, DylibLoadKind::Weak);
388 assert_eq!(dy.dependencies[1].ordinal, 2);
389 assert_eq!(dy.dependencies[2].kind, DylibLoadKind::Reexport);
390 assert_eq!(dy.dependencies[2].ordinal, 3);
391
392 assert_eq!(
393 dependency_ordinal(&dy.dependencies, "/usr/lib/libSystem.B.dylib"),
394 Some(1)
395 );
396 assert_eq!(
397 dependency_ordinal(&dy.dependencies, "/usr/lib/libc++abi.dylib"),
398 Some(3)
399 );
400 assert_eq!(dependency_ordinal(&dy.dependencies, "missing"), None);
401 }
402
403 #[test]
404 fn parse_dylib_collects_rpaths_in_source_order() {
405 let image = make_dylib_image(vec![
406 LoadCommand::Dylib(dylib_cmd(LC_ID_DYLIB, "@rpath/libself.dylib")),
407 LoadCommand::Rpath(RpathCmd {
408 path: "@executable_path/../lib".into(),
409 }),
410 LoadCommand::Rpath(RpathCmd {
411 path: "/opt/local/lib".into(),
412 }),
413 ]);
414 let dy = DylibFile::parse("/tmp/x.dylib", &image).unwrap();
415 assert_eq!(dy.rpaths, vec!["@executable_path/../lib", "/opt/local/lib"]);
416 }
417
418 // ----- DylibFile::from_tbd tests -----
419
420 use crate::macho::tbd::{parse_tbd, Arch, Platform};
421
422 fn arm64_macos() -> Target {
423 Target {
424 arch: Arch::Arm64,
425 platform: Platform::MacOs,
426 }
427 }
428
429 #[test]
430 fn from_tbd_filters_exports_by_target() {
431 let src = "--- !tapi-tbd\n\
432 tbd-version: 4\n\
433 targets: [ arm64-macos, x86_64-macos ]\n\
434 install-name: '/usr/lib/libdemo.dylib'\n\
435 current-version: 1.2.3\n\
436 exports:\n\
437 \x20 - targets: [ arm64-macos ]\n\
438 \x20 symbols: [ _arm_only ]\n\
439 \x20 - targets: [ x86_64-macos ]\n\
440 \x20 symbols: [ _x86_only ]\n\
441 \x20 - targets: [ arm64-macos, x86_64-macos ]\n\
442 \x20 symbols: [ _shared_sym ]\n";
443 let tbd = &parse_tbd(src).unwrap()[0];
444 let dy = DylibFile::from_tbd("/stub/libdemo.tbd", tbd, &arm64_macos());
445 let names: Vec<String> = dy
446 .exports
447 .entries()
448 .unwrap()
449 .into_iter()
450 .map(|e| e.name)
451 .collect();
452 assert!(names.contains(&"_arm_only".to_string()));
453 assert!(names.contains(&"_shared_sym".to_string()));
454 assert!(!names.contains(&"_x86_only".to_string()));
455 }
456
457 #[test]
458 fn from_tbd_arm64_uses_arm64e_scopes() {
459 let src = "--- !tapi-tbd\n\
460 tbd-version: 4\n\
461 targets: [ arm64e-macos ]\n\
462 install-name: '/usr/lib/libdemo.dylib'\n\
463 exports:\n\
464 \x20 - targets: [ arm64e-macos ]\n\
465 \x20 symbols: [ _umbrella_only ]\n";
466 let tbd = &parse_tbd(src).unwrap()[0];
467 let dy = DylibFile::from_tbd("/stub/libdemo.tbd", tbd, &arm64_macos());
468 let names: Vec<String> = dy
469 .exports
470 .entries()
471 .unwrap()
472 .into_iter()
473 .map(|e| e.name)
474 .collect();
475 assert_eq!(names, vec!["_umbrella_only".to_string()]);
476 }
477
478 #[test]
479 fn from_tbd_includes_reexported_libraries_as_dependencies() {
480 let src = "--- !tapi-tbd\n\
481 tbd-version: 4\n\
482 targets: [ arm64-macos ]\n\
483 install-name: '/usr/lib/libSystem.B.dylib'\n\
484 reexported-libraries:\n\
485 \x20 - targets: [ arm64-macos ]\n\
486 \x20 libraries: [ '/usr/lib/system/libcache.dylib', '/usr/lib/system/libxpc.dylib' ]\n";
487 let tbd = &parse_tbd(src).unwrap()[0];
488 let dy = DylibFile::from_tbd("/stub/libSystem.tbd", tbd, &arm64_macos());
489 assert_eq!(dy.dependencies.len(), 2);
490 assert_eq!(dy.dependencies[0].kind, DylibLoadKind::Reexport);
491 assert_eq!(dy.dependencies[0].ordinal, 1);
492 assert_eq!(
493 dy.dependencies[0].install_name,
494 "/usr/lib/system/libcache.dylib"
495 );
496 assert_eq!(dy.dependencies[1].ordinal, 2);
497 }
498
499 #[test]
500 fn from_tbd_decodes_objc_symbols_with_prefix() {
501 let src = "--- !tapi-tbd\n\
502 tbd-version: 4\n\
503 targets: [ arm64-macos ]\n\
504 install-name: '/usr/lib/libobjc.A.dylib'\n\
505 exports:\n\
506 \x20 - targets: [ arm64-macos ]\n\
507 \x20 objc-classes: [ NSObject, NSArray ]\n\
508 \x20 objc-eh-types: [ NSException ]\n";
509 let tbd = &parse_tbd(src).unwrap()[0];
510 let dy = DylibFile::from_tbd("/stub/libobjc.tbd", tbd, &arm64_macos());
511 let names: Vec<String> = dy
512 .exports
513 .entries()
514 .unwrap()
515 .into_iter()
516 .map(|e| e.name)
517 .collect();
518 assert!(names.contains(&"_OBJC_CLASS_$_NSObject".to_string()));
519 assert!(names.contains(&"_OBJC_CLASS_$_NSArray".to_string()));
520 assert!(names.contains(&"_OBJC_EHTYPE_$_NSException".to_string()));
521 }
522
523 #[test]
524 fn from_tbd_parses_current_version_into_packed_u32() {
525 let src = "--- !tapi-tbd\n\
526 tbd-version: 4\n\
527 targets: [ arm64-macos ]\n\
528 install-name: '/usr/lib/libfoo.dylib'\n\
529 current-version: 14.2.3\n";
530 let tbd = &parse_tbd(src).unwrap()[0];
531 let dy = DylibFile::from_tbd("/stub", tbd, &arm64_macos());
532 assert_eq!(dy.current_version, (14 << 16) | (2 << 8) | 3);
533 }
534
535 #[test]
536 fn parse_dylib_rejects_non_dylib_filetype() {
537 // MH_OBJECT image — should fail DylibFile::parse with a clear error.
538 let hdr = MachHeader64 {
539 magic: MH_MAGIC_64,
540 cputype: CPU_TYPE_ARM64,
541 cpusubtype: 0,
542 filetype: MH_OBJECT,
543 ncmds: 0,
544 sizeofcmds: 0,
545 flags: 0,
546 reserved: 0,
547 };
548 let mut image = Vec::new();
549 write_header(&hdr, &mut image);
550 let err = DylibFile::parse("/tmp/obj.o", &image).unwrap_err();
551 assert!(matches!(err, ReadError::BadCmdsize { reason, .. } if reason.contains("MH_DYLIB")));
552 }
553 }
554