From 500932213429ac0b3e31dd87cb34d56e1ef9eb8e Mon Sep 17 00:00:00 2001 From: Alex Ackerman <22874423+darkhonor@users.noreply.github.com> Date: Sun, 13 Sep 2026 22:06:44 +0900 Subject: [PATCH 1/3] Emit LC_BUILD_VERSION in Mach-O metadata objects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apple's linker expects Mach-O object files to carry an LC_BUILD_VERSION load command describing the platform they were built for. The object holding the dependency list did not have one, so ld fell back to guessing and reported the guess on stderr: ld: no platform load command found in '..._audit_data.o', assuming: macOS Since Rust 1.97 the compiler surfaces linker output through the linker_messages lint, which made that message visible on every `cargo auditable build` on macOS. rustc emits the load command for the same reason, in the file object_file.rs is adapted from — see macho_object_build_version_for_target in compiler/rustc_codegen_ssa/src/back/metadata.rs. That part did not come across when the code was adapted. The platform is derived from target_os and target_abi, covering macOS, iOS, tvOS, watchOS and visionOS along with their simulator and Mac Catalyst variants. Unrecognised Apple targets fall back to PLATFORM_MACOS, which is the same assumption ld makes on its own, so they are no worse off than before and still get a load command to read. minos and sdk are deliberately left at zero. This object carries only the dependency list and no code, so it constrains nothing at runtime, and declaring a minimum OS version it does not require risks conflicting with the deployment target of the binary it is linked into. rustc omits the SDK version for the same reason. object 0.37 already exposes set_macho_build_version and MachOBuildVersion, so no dependency change is needed. Fixes #266 Co-Authored-By: Claude Opus 5 (1M context) --- cargo-auditable/src/object_file.rs | 94 ++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/cargo-auditable/src/object_file.rs b/cargo-auditable/src/object_file.rs index 36dd8a4..6231a7f 100644 --- a/cargo-auditable/src/object_file.rs +++ b/cargo-auditable/src/object_file.rs @@ -47,6 +47,50 @@ pub fn create_metadata_file( Some(file.write().unwrap()) } +/// Mach-O object files are expected to carry an `LC_BUILD_VERSION` load command +/// describing the platform they were built for. Without it Apple's `ld` has nothing +/// to read the platform from, so it guesses and reports the guess on stderr: +/// +/// ```text +/// ld: no platform load command found in '..._audit_data.o', assuming: macOS +/// ``` +/// +/// Since Rust 1.97 the compiler surfaces linker output through the `linker_messages` +/// lint, which makes that message visible on every `cargo auditable build` on macOS. +/// +/// rustc emits the load command for the same reason, in the file this module is +/// adapted from: see `macho_object_build_version_for_target` in +/// `compiler/rustc_codegen_ssa/src/back/metadata.rs`. +/// +/// `minos` and `sdk` are deliberately left at zero. This object carries only the +/// dependency list and no code, so it constrains nothing at runtime, and declaring a +/// minimum OS version it does not actually require risks conflicting with the +/// deployment target of the binary it is linked into. rustc omits the SDK version for +/// the same reason. +fn macho_build_version(info: &RustcTargetInfo) -> write::MachOBuildVersion { + let target_os = info.get("target_os").map(String::as_str); + let target_abi = info.get("target_abi").map(String::as_str); + let platform = match (target_os, target_abi) { + (Some("macos"), _) => object::macho::PLATFORM_MACOS, + (Some("ios"), Some("macabi")) => object::macho::PLATFORM_MACCATALYST, + (Some("ios"), Some("sim")) => object::macho::PLATFORM_IOSSIMULATOR, + (Some("ios"), _) => object::macho::PLATFORM_IOS, + (Some("tvos"), Some("sim")) => object::macho::PLATFORM_TVOSSIMULATOR, + (Some("tvos"), _) => object::macho::PLATFORM_TVOS, + (Some("watchos"), Some("sim")) => object::macho::PLATFORM_WATCHOSSIMULATOR, + (Some("watchos"), _) => object::macho::PLATFORM_WATCHOS, + (Some("visionos"), Some("sim")) => object::macho::PLATFORM_XROSSIMULATOR, + (Some("visionos"), _) => object::macho::PLATFORM_XROS, + // Not a platform we recognise. macOS is the same assumption `ld` makes on + // its own, so this is no worse than the current behaviour and still gives + // the linker a load command to read. + _ => object::macho::PLATFORM_MACOS, + }; + let mut build_version = write::MachOBuildVersion::default(); + build_version.platform = platform; + build_version +} + fn create_object_file( info: &RustcTargetInfo, target_triple: &str, @@ -97,6 +141,9 @@ fn create_object_file( }; let mut file = write::Object::new(binary_format, architecture, endianness); + if binary_format == BinaryFormat::MachO { + file.set_macho_build_version(macho_build_version(info)); + } let e_flags = match architecture { Architecture::Mips => { // the original code matches on info we don't have to support pre-1999 MIPS variants: @@ -262,6 +309,53 @@ mod tests { use super::*; use crate::target_info::parse_rustc_target_info; + fn apple_target_info(target_os: &str, target_abi: Option<&str>) -> RustcTargetInfo { + let mut info = HashMap::from([ + ("target_vendor".to_owned(), "apple".to_owned()), + ("target_os".to_owned(), target_os.to_owned()), + ]); + if let Some(abi) = target_abi { + info.insert("target_abi".to_owned(), abi.to_owned()); + } + info + } + + #[test] + fn test_macho_platform_detection() { + use object::macho; + + let cases = [ + (("macos", None), macho::PLATFORM_MACOS), + (("ios", None), macho::PLATFORM_IOS), + (("ios", Some("sim")), macho::PLATFORM_IOSSIMULATOR), + (("ios", Some("macabi")), macho::PLATFORM_MACCATALYST), + (("tvos", None), macho::PLATFORM_TVOS), + (("tvos", Some("sim")), macho::PLATFORM_TVOSSIMULATOR), + (("watchos", None), macho::PLATFORM_WATCHOS), + (("watchos", Some("sim")), macho::PLATFORM_WATCHOSSIMULATOR), + (("visionos", None), macho::PLATFORM_XROS), + (("visionos", Some("sim")), macho::PLATFORM_XROSSIMULATOR), + ]; + for ((target_os, target_abi), expected) in cases { + let info = apple_target_info(target_os, target_abi); + assert_eq!( + macho_build_version(&info).platform, + expected, + "target_os={target_os} target_abi={target_abi:?}" + ); + } + } + + /// The minimum OS version and SDK version are deliberately left unset: this + /// object carries no code, so declaring a minimum it does not require could + /// conflict with the deployment target of the binary it is linked into. + #[test] + fn test_macho_build_version_leaves_minos_and_sdk_unset() { + let version = macho_build_version(&apple_target_info("macos", None)); + assert_eq!(version.minos, 0); + assert_eq!(version.sdk, 0); + } + #[test] fn test_riscv_abi_detection() { // real-world target with double floats From 3a03bc135c95ed89728941062d9f3c71e0be5214 Mon Sep 17 00:00:00 2001 From: Alex Ackerman <22874423+darkhonor@users.noreply.github.com> Date: Sun, 13 Sep 2026 22:14:27 +0900 Subject: [PATCH 2/3] Emit no build version for unrecognised Apple targets An Apple target this mapping does not know, most likely one added after it was written, now gets no LC_BUILD_VERSION at all rather than falling back to PLATFORM_MACOS. The warning is the current behaviour and is recoverable; a platform stated in the load command that we cannot verify is not. Co-Authored-By: Claude Opus 5 (1M context) --- cargo-auditable/src/object_file.rs | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/cargo-auditable/src/object_file.rs b/cargo-auditable/src/object_file.rs index 6231a7f..f330cee 100644 --- a/cargo-auditable/src/object_file.rs +++ b/cargo-auditable/src/object_file.rs @@ -67,7 +67,7 @@ pub fn create_metadata_file( /// minimum OS version it does not actually require risks conflicting with the /// deployment target of the binary it is linked into. rustc omits the SDK version for /// the same reason. -fn macho_build_version(info: &RustcTargetInfo) -> write::MachOBuildVersion { +fn macho_build_version(info: &RustcTargetInfo) -> Option { let target_os = info.get("target_os").map(String::as_str); let target_abi = info.get("target_abi").map(String::as_str); let platform = match (target_os, target_abi) { @@ -81,14 +81,15 @@ fn macho_build_version(info: &RustcTargetInfo) -> write::MachOBuildVersion { (Some("watchos"), _) => object::macho::PLATFORM_WATCHOS, (Some("visionos"), Some("sim")) => object::macho::PLATFORM_XROSSIMULATOR, (Some("visionos"), _) => object::macho::PLATFORM_XROS, - // Not a platform we recognise. macOS is the same assumption `ld` makes on - // its own, so this is no worse than the current behaviour and still gives - // the linker a load command to read. - _ => object::macho::PLATFORM_MACOS, + // An Apple target we do not recognise, most likely one added after this + // was written. Emit nothing rather than assert a platform we cannot + // verify: the warning is the current behaviour and is recoverable, a + // wrong platform in the load command is neither. + _ => return None, }; let mut build_version = write::MachOBuildVersion::default(); build_version.platform = platform; - build_version + Some(build_version) } fn create_object_file( @@ -142,7 +143,9 @@ fn create_object_file( let mut file = write::Object::new(binary_format, architecture, endianness); if binary_format == BinaryFormat::MachO { - file.set_macho_build_version(macho_build_version(info)); + if let Some(build_version) = macho_build_version(info) { + file.set_macho_build_version(build_version); + } } let e_flags = match architecture { Architecture::Mips => { @@ -339,7 +342,7 @@ mod tests { for ((target_os, target_abi), expected) in cases { let info = apple_target_info(target_os, target_abi); assert_eq!( - macho_build_version(&info).platform, + macho_build_version(&info).expect("known platform").platform, expected, "target_os={target_os} target_abi={target_abi:?}" ); @@ -351,11 +354,19 @@ mod tests { /// conflict with the deployment target of the binary it is linked into. #[test] fn test_macho_build_version_leaves_minos_and_sdk_unset() { - let version = macho_build_version(&apple_target_info("macos", None)); + let version = + macho_build_version(&apple_target_info("macos", None)).expect("known platform"); assert_eq!(version.minos, 0); assert_eq!(version.sdk, 0); } + /// An Apple target we do not recognise gets no load command at all, rather + /// than a platform we cannot verify. + #[test] + fn test_macho_build_version_absent_for_unknown_platform() { + assert!(macho_build_version(&apple_target_info("futureos", None)).is_none()); + } + #[test] fn test_riscv_abi_detection() { // real-world target with double floats From c0542f5cddc99604129c0554f9b5861a321d18fd Mon Sep 17 00:00:00 2001 From: Alex Ackerman <22874423+darkhonor@users.noreply.github.com> Date: Mon, 14 Sep 2026 00:32:41 +0900 Subject: [PATCH 3/3] Require target_abi before naming a device-family Apple platform Compilers that predate target_abi in --print=cfg omit the key entirely. Verified against a real 1.74.0 toolchain: every Apple target reports no target_abi at all, so x86_64-apple-ios-macabi and aarch64-apple-ios-sim arrive indistinguishable from device iOS. Treating that as a device target is not a cosmetic error. A stated platform that disagrees with the link target is rejected outright (ld: ... has platform iOS, which is different from target platform macCatalyst), which would turn today's harmless warning into a build failure. The device families now require target_abi to be present and emit nothing without it. macOS is deliberately exempt: it is the only Apple OS with no ABI variants (every *-apple-darwin target reports an empty target_abi), so it stays identifiable when the key is missing. Requiring it there would silently drop the load command on the most common platform for anyone wrapping an older compiler, which is the case this PR exists to fix. Note the distinction the tests now pin: device targets report an EMPTY target_abi, not a missing one. Reported by Astra in review; thanks. Co-Authored-By: Claude Opus 5 (1M context) --- cargo-auditable/src/object_file.rs | 82 +++++++++++++++++++++++++----- 1 file changed, 69 insertions(+), 13 deletions(-) diff --git a/cargo-auditable/src/object_file.rs b/cargo-auditable/src/object_file.rs index f330cee..8533492 100644 --- a/cargo-auditable/src/object_file.rs +++ b/cargo-auditable/src/object_file.rs @@ -71,20 +71,29 @@ fn macho_build_version(info: &RustcTargetInfo) -> Option object::macho::PLATFORM_MACOS, + // For every other Apple OS the ABI is precisely what separates device from + // simulator from Mac Catalyst, so an ABSENT `target_abi` is not "device" — + // it is "unknown", and must fall through to emitting nothing. (Some("ios"), Some("macabi")) => object::macho::PLATFORM_MACCATALYST, (Some("ios"), Some("sim")) => object::macho::PLATFORM_IOSSIMULATOR, - (Some("ios"), _) => object::macho::PLATFORM_IOS, + (Some("ios"), Some(_)) => object::macho::PLATFORM_IOS, (Some("tvos"), Some("sim")) => object::macho::PLATFORM_TVOSSIMULATOR, - (Some("tvos"), _) => object::macho::PLATFORM_TVOS, + (Some("tvos"), Some(_)) => object::macho::PLATFORM_TVOS, (Some("watchos"), Some("sim")) => object::macho::PLATFORM_WATCHOSSIMULATOR, - (Some("watchos"), _) => object::macho::PLATFORM_WATCHOS, + (Some("watchos"), Some(_)) => object::macho::PLATFORM_WATCHOS, (Some("visionos"), Some("sim")) => object::macho::PLATFORM_XROSSIMULATOR, - (Some("visionos"), _) => object::macho::PLATFORM_XROS, - // An Apple target we do not recognise, most likely one added after this - // was written. Emit nothing rather than assert a platform we cannot - // verify: the warning is the current behaviour and is recoverable, a - // wrong platform in the load command is neither. + (Some("visionos"), Some(_)) => object::macho::PLATFORM_XROS, + // An Apple target we cannot identify: either an OS added after this was + // written, or a device-family target built by a compiler too old to + // report `target_abi`. Emit nothing rather than assert a platform we + // cannot verify — the warning is the current behaviour and is + // recoverable, whereas a WRONG platform is a hard link failure + // (`ld: ... has platform iOS, which is different from target platform + // macCatalyst`). _ => return None, }; let mut build_version = write::MachOBuildVersion::default(); @@ -327,16 +336,19 @@ mod tests { fn test_macho_platform_detection() { use object::macho; + // Device targets report an EMPTY `target_abi`, which is what rustc + // actually emits (`aarch64-apple-ios` -> `target_abi=""`). A MISSING key + // is a different case entirely and is covered by its own test below. let cases = [ - (("macos", None), macho::PLATFORM_MACOS), - (("ios", None), macho::PLATFORM_IOS), + (("macos", Some("")), macho::PLATFORM_MACOS), + (("ios", Some("")), macho::PLATFORM_IOS), (("ios", Some("sim")), macho::PLATFORM_IOSSIMULATOR), (("ios", Some("macabi")), macho::PLATFORM_MACCATALYST), - (("tvos", None), macho::PLATFORM_TVOS), + (("tvos", Some("")), macho::PLATFORM_TVOS), (("tvos", Some("sim")), macho::PLATFORM_TVOSSIMULATOR), - (("watchos", None), macho::PLATFORM_WATCHOS), + (("watchos", Some("")), macho::PLATFORM_WATCHOS), (("watchos", Some("sim")), macho::PLATFORM_WATCHOSSIMULATOR), - (("visionos", None), macho::PLATFORM_XROS), + (("visionos", Some("")), macho::PLATFORM_XROS), (("visionos", Some("sim")), macho::PLATFORM_XROSSIMULATOR), ]; for ((target_os, target_abi), expected) in cases { @@ -367,6 +379,50 @@ mod tests { assert!(macho_build_version(&apple_target_info("futureos", None)).is_none()); } + /// Regression: a compiler too old to report `target_abi` must NOT be treated + /// as "device". rustc 1.74 omits the key entirely for every Apple target — + /// verified against a real 1.74.0 toolchain — so `x86_64-apple-ios-macabi` + /// and `aarch64-apple-ios-sim` arrive indistinguishable from device iOS. + /// Guessing `PLATFORM_IOS` there is not a cosmetic error: the linker rejects + /// the mismatch outright (`has platform iOS, which is different from target + /// platform macCatalyst`), turning today's harmless warning into a build + /// failure. Emit nothing instead. + #[test] + fn test_macho_build_version_absent_when_target_abi_is_unavailable() { + for os in ["ios", "tvos", "watchos", "visionos"] { + assert!( + macho_build_version(&apple_target_info(os, None)).is_none(), + "{os} without target_abi must not be assumed to be a device target" + ); + } + } + + /// ...but macOS is still identifiable without `target_abi`, because it is the + /// one Apple OS with no ABI variants (every `*-apple-darwin` target reports an + /// empty `target_abi`). Requiring the key here would silently drop the load + /// command on the most common platform whenever an older compiler is wrapped. + #[test] + fn test_macho_build_version_present_for_macos_without_target_abi() { + assert_eq!( + macho_build_version(&apple_target_info("macos", None)) + .expect("macOS is unambiguous without target_abi") + .platform, + object::macho::PLATFORM_MACOS + ); + } + + /// Device targets report an EMPTY `target_abi`, not a missing one — that is + /// what distinguishes them from the old-compiler case above. + #[test] + fn test_macho_build_version_empty_target_abi_is_a_device_target() { + assert_eq!( + macho_build_version(&apple_target_info("ios", Some(""))) + .expect("empty target_abi is a device target") + .platform, + object::macho::PLATFORM_IOS + ); + } + #[test] fn test_riscv_abi_detection() { // real-world target with double floats