@@ -358,9 +358,11 @@ def normalize_single_architecture(bundle: Path, architecture: str,
358358 "enabled" : True ,
359359 "requestedArchitecture" : architecture ,
360360 "files" : results ,
361+ "installNames" : [],
361362 "summary" : {
362363 "machoFiles" : len (results ),
363364 "thinnedFiles" : sum (item ["action" ] == "thinned" for item in results ),
365+ "relocatedInstallNames" : 0 ,
364366 "originalBytes" : original_bytes ,
365367 "resultBytes" : result_bytes ,
366368 "savedBytes" : original_bytes - result_bytes ,
@@ -369,6 +371,93 @@ def normalize_single_architecture(bundle: Path, architecture: str,
369371 }
370372
371373
374+ def normalize_absolute_install_names (
375+ bundle : Path ,
376+ maximum_entries : int = DEFAULT_MAXIMUM_ENTRIES ,
377+ system_prefixes : tuple [str , ...] = DEFAULT_SYSTEM_PREFIXES ,
378+ runner = run_tool ) -> list [dict ]:
379+ """Relocate producer-machine LC_ID_DYLIB values without relaxing the audit."""
380+ bundle = bundle .resolve ()
381+ before = snapshot_tree (bundle , maximum_entries )
382+ candidates = []
383+ for relative in sorted (path for path , entry in before .items ()
384+ if entry ["type" ] == "file" and entry .get ("macho" )):
385+ entry = before [relative ]
386+ if os .name != "nt" and entry ["links" ] != 1 :
387+ raise AuditError (f"Mach-O has multiple hard links: { relative } " )
388+ path = bundle / relative
389+ architectures = parse_architectures (runner (["lipo" , "-archs" , str (path )]))
390+ identifiers = []
391+ for architecture in architectures :
392+ identifier = parse_load_commands (
393+ runner (["otool" , "-arch" , architecture , "-l" , str (path )])
394+ )["identifier" ]
395+ identifiers .append (identifier )
396+ distinct = set (identifiers )
397+ if len (distinct ) > 1 :
398+ raise AuditError (f"Mach-O slices have different dylib install names: { relative } " )
399+ identifier = identifiers [0 ]
400+ if identifier is None or not identifier .startswith ("/" ):
401+ continue
402+ if any (identifier .startswith (prefix ) for prefix in system_prefixes ):
403+ continue
404+ basename = PurePosixPath (identifier ).name
405+ if not basename or basename in ("." , ".." ):
406+ raise AuditError (f"absolute dylib install name has no basename: { relative } " )
407+ relocated = "@rpath/" + basename
408+ validate_install_name (relocated , "relocated dylib install name" , system_prefixes )
409+ candidates .append ({
410+ "path" : relative ,
411+ "architectures" : architectures ,
412+ "original" : identifier ,
413+ "result" : relocated ,
414+ "originalSha256" : sha256 (path ),
415+ })
416+
417+ results = []
418+ for candidate in candidates :
419+ relative = candidate ["path" ]
420+ path = bundle / relative
421+ original = before [relative ]
422+ if (stable_normalization_metadata (file_metadata (path ))
423+ != stable_normalization_metadata (original )
424+ or sha256 (path ) != candidate ["originalSha256" ]):
425+ raise AuditError (f"Mach-O mutated before install-name normalization: { relative } " )
426+ runner (["install_name_tool" , "-id" , candidate ["result" ], str (path )])
427+ current = file_metadata (path )
428+ if (current ["type" ] != "file" or not current .get ("macho" )
429+ or (os .name != "nt" and (
430+ current ["links" ] != 1
431+ or current ["mode" ] != original ["mode" ]
432+ or current ["uid" ] != original ["uid" ]
433+ or current ["gid" ] != original ["gid" ]))):
434+ raise AuditError (f"install-name normalization made Mach-O unsafe: { relative } " )
435+ for architecture in candidate ["architectures" ]:
436+ identifier = parse_load_commands (
437+ runner (["otool" , "-arch" , architecture , "-l" , str (path )])
438+ )["identifier" ]
439+ if identifier != candidate ["result" ]:
440+ raise AuditError (
441+ f"install-name normalization failed for { relative } ({ architecture } )"
442+ )
443+ result_hash = sha256 (path )
444+ if result_hash == candidate ["originalSha256" ]:
445+ raise AuditError (f"install-name normalization did not modify { relative } " )
446+ results .append ({** candidate , "resultSha256" : result_hash })
447+
448+ after = snapshot_tree (bundle , maximum_entries )
449+ if set (before ) != set (after ):
450+ changed = sorted (set (before ) ^ set (after ))
451+ raise AuditError (f"bundle inventory changed during install-name normalization: { changed [0 ]} " )
452+ changed_paths = {item ["path" ] for item in results }
453+ for relative in sorted (before ):
454+ if relative in changed_paths :
455+ continue
456+ if stable_metadata (before [relative ]) != stable_metadata (after [relative ]):
457+ raise AuditError (f"bundle mutated during install-name normalization: { relative } " )
458+ return results
459+
460+
372461def parse_architectures (output : str ) -> list [str ]:
373462 architectures = output .strip ().split ()
374463 if not architectures or any (
@@ -717,9 +806,11 @@ def audit_bundle(bundle: Path, executable: str, architectures: list[str],
717806 "enabled" : False ,
718807 "requestedArchitecture" : None ,
719808 "files" : [],
809+ "installNames" : [],
720810 "summary" : {
721811 "machoFiles" : len (binaries ),
722812 "thinnedFiles" : 0 ,
813+ "relocatedInstallNames" : 0 ,
723814 "originalBytes" : sum (item ["bytes" ] for item in binaries ),
724815 "resultBytes" : sum (item ["bytes" ] for item in binaries ),
725816 "savedBytes" : 0 ,
0 commit comments