Skip to content

Emit LC_BUILD_VERSION in Mach-O metadata objects - #267

Merged
Shnatsel merged 3 commits into
rust-secure-code:masterfrom
darkhonor:macho-build-version
Sep 13, 2026
Merged

Shnatsel merged 3 commits into
rust-secure-code:masterfrom
darkhonor:macho-build-version

Conversation

@darkhonor

Copy link
Copy Markdown
Contributor

Fixes #266.

Mach-O object files are expected to carry an LC_BUILD_VERSION load command naming the platform they were built for. The dependency-list object did not have one, so ld had nothing to read the platform from, guessed, and reported the guess on stderr. Rust 1.97 began surfacing linker output through the linker_messages lint, which is what made it visible.

src/object_file.rs notes that it is adapted from rustc's compiler/rustc_codegen_ssa/src/back/metadata.rs. The upstream version sets the build version — macho_object_build_version_for_target, called at metadata.rs:222 — and that is the piece that did not come across.

The change

One call on the BinaryFormat::MachO path, plus a helper that derives the platform from target_os and target_abi: 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 unaided, so they are no worse off than today and still get a load command to read.

minos and sdk are left at zero deliberately. This object carries 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 reasoning.

object 0.37 already exposes set_macho_build_version and MachOBuildVersion, so there is no dependency change.

Verification

macOS 26.6.2, Rust 1.98.1 aarch64-apple-darwin, ld-1267. Measured before and after on a project that reproduces the warning, rebuilding the binary each time so the link actually reruns:

before (0.7.5) : 1 occurrence of "no platform load command"
after          : 0

The emitted object now looks like this, and rust-audit-info still reads the embedded metadata:

$ otool -l target/release/deps/<crate>_audit_data.o | grep -E '^ *cmd |platform|minos|sdk '
      cmd LC_SEGMENT_64
      cmd LC_BUILD_VERSION
 platform 1
    minos 0.0
      sdk n/a
      cmd LC_SYMTAB
      cmd LC_DYSYMTAB

One thing worth flagging for whoever reviews: the warning does not reproduce everywhere. Building this repo with stock 0.7.5 in a scratch copy produced an object with no LC_BUILD_VERSION and yet no warning, so visibility appears to depend on the link context rather than on the object alone. I checked the before/after against a project that does reproduce it rather than relying on a tree that does not, since the latter would have shown a clean result either way.

Added unit tests for the platform mapping and for minos/sdk staying unset. cargo fmt --check, cargo clippy -- -D warnings, the three cargo check variants and cargo test --all-features --workspace all pass locally (the full test suite needs the wasm32-unknown-unknown target installed for test_wasm).

I have not been able to test the non-macOS Apple targets on real hardware — the platform mapping for those is derived from rustc's and covered only by the unit test.

darkhonor and others added 2 commits September 13, 2026 22:06
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 rust-secure-code#266

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
@darkhonor

Copy link
Copy Markdown
Contributor Author

Thanks — I opened this a few minutes after you replied on the issue and hadn't seen your comment yet, so let me address it directly. I've pushed one change on the back of it.

On not hardcoding values that drift from rustc: agreed, and that is why there are no version numbers here at all. minos and sdk are both left at zero. There is nothing to keep in step with a deployment target default, an SDK release, or rustc's own choices, because the object states no version — only which platform it is for. rustc omits the SDK version for the same reason, and the reasoning extends to minos in this case: the object carries the dependency list and no code, so any minimum it declared would be a claim about something it does not constrain.

On falling back to inserting nothing: you were right and my first version wasn't doing that. It fell back to PLATFORM_MACOS for Apple targets the mapping doesn't recognise, on the grounds that it matches what ld assumes anyway. That is the wrong trade for exactly the reason you gave — it is the case most likely to arise from a platform added after this code was written, and a platform stated in the load command that we cannot verify is worse than no load command. 3a03bc1 changes it to emit nothing for unrecognised targets, so the behaviour there is what it is today, warning included, and a test pins it.

What remains is the target_os/target_abiPLATFORM_* mapping. Those are Apple ABI constants that come from the object crate rather than from rustc, and in practice that list is additive: new platforms get added, existing ones do not change meaning. If it goes stale the failure is now a missing load command and the old warning, not wrong data.

On calling into the SDK: I'd suggest not, unless you want the version for another reason. It would be needed only to populate minos/sdk, and the warning goes away without them — the linker's complaint is that it cannot tell which platform the object targets, not that it lacks a version. Avoiding the call also avoids a build-time dependency on xcrun being present and well-behaved, and the failure path that comes with it. That said, if you'd rather have real versions with a fall back to inserting nothing when the lookup fails, I'm happy to add it — it slots into the same function.

Verification, re-run after the fallback change: on a project that reproduces the warning, 1 occurrence before and 0 after, with rust-audit-info still reading the metadata. cargo fmt --check, cargo clippy -- -D warnings, the three cargo check variants and cargo test --all-features --workspace all pass.

One caveat I'd flag for anyone testing this: the warning does not reproduce in every project. Building this repo with stock 0.7.5 in a scratch copy produced an object with no LC_BUILD_VERSION and no warning, so visibility seems to depend on the link context rather than the object alone. I measured before and after against a project that does reproduce it, since the other tree would have looked clean either way.

No hard feelings if this isn't the shape you want — happy to rework it, or to leave it with you if you'd rather take the idea and write it yourselves.

@Shnatsel

Copy link
Copy Markdown
Member

This looks good to me, but I'm not really familiar with mac OS. I asked Astra to review this out of abundance of caution and it flagged this:

[P2] Handle missing target_abi without assuming a device platform — [object_file.rs:72–77](

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,
). Older compilers, including Rust 1.74, omit target_abi from --print=cfg. When cargo-auditable wraps those compilers, this fallback incorrectly marks Mac Catalyst and simulator objects as device iOS objects. I reproduced a regression for x86_64-apple-ios-macabi: the base object links successfully, while the PR object fails with has platform iOS, which is different from target platform macCatalyst. Return None when the ABI is unavailable, or infer it from the target triple. Add a regression test using older rustc output.

We can probably skip emitting this field altogether on older compilers, since the warning is harmless and it won't be visible anyway.

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) <noreply@anthropic.com>
@darkhonor

Copy link
Copy Markdown
Contributor Author

That's a real bug and a good catch — thank you (and please pass that on to Astra). Fixed in c0542f5. I checked the claim rather than taking it on faith, and it holds: against a real 1.74.0 toolchain, --print=cfg omits target_abi for every Apple target, not just some.

$ rustup run 1.74.0 rustc --print=cfg --target=x86_64-apple-ios-macabi | grep target_abi
(nothing)
$ rustc --print=cfg --target=x86_64-apple-ios-macabi | grep target_abi     # 1.98.1
target_abi="macabi"

So on an older compiler x86_64-apple-ios-macabi and aarch64-apple-ios-sim both arrived indistinguishable from device iOS and my match fell through to PLATFORM_IOS. That is worse than the bug this PR fixes: a wrong platform is rejected outright rather than warned about, so it would have turned a harmless message into a build failure.

The device families now require target_abi to be present and emit nothing without it. There is also a distinction the tests now pin explicitly, because it is easy to miss: device targets report an EMPTY target_abi, not a missing one (aarch64-apple-iostarget_abi=""). Empty means device; absent means we cannot tell.

One deliberate divergence: macOS is exempt, and I think it has to be

Applying the rule uniformly would regress the common case. macOS is the only Apple OS with no ABI variants — every *-apple-darwin target reports an empty target_abi — so it stays unambiguous when the key is missing entirely. Requiring the key there would silently drop the load command on macOS for anyone wrapping an older compiler, which is precisely the situation this PR exists to fix.

Verified end to end rather than argued: driving the patched binary with the 1.74.0 toolchain on macOS still produces LC_BUILD_VERSION / platform 1 and zero warnings, while the device families correctly produce nothing.

This also matches your instinct — "skip emitting this field altogether on older compilers, since the warning is harmless" — everywhere the platform is genuinely ambiguous. I have just kept the one case where it is not.

On "or infer it from the target triple" — I would recommend against it

It looks like it would recover the old-compiler case, but it is wrong on real targets today:

i386-apple-ios      target_os=ios  target_abi=sim
x86_64-apple-ios    target_os=ios  target_abi=sim

Both are simulator targets whose triples carry no -sim suffix. Inferring from the triple would label them device iOS — reintroducing exactly the class of mislabelling we are fixing, and on targets that exist right now rather than hypothetical future ones. target_abi is the authoritative signal; where it is absent, the honest answer is that we do not know.

Tests

Four unit tests cover it, including the empty-vs-missing distinction and a macOS-without-target_abi case. cargo fmt --check, cargo clippy -- -D warnings, the three cargo check variants and cargo test --all-features --workspace all pass. (The only clippy output is the pre-existing virtual-workspace resolver = "1" notice, which is not from this change.)

I have not been able to link a real Mac Catalyst or simulator artifact to confirm the linker error text first-hand — that half comes from Astra's reproduction and I have taken it as reported. The platform mapping itself is derived from rustc's and is covered by the unit tests.

@Shnatsel
Shnatsel merged commit 3a5d363 into rust-secure-code:master Sep 13, 2026
11 checks passed
@Shnatsel

Copy link
Copy Markdown
Member

Published in v0.7.6

charliermarsh pushed a commit to astral-sh/uv that referenced this pull request Sep 15, 2026
cargo-auditable 0.7.6 adds platform metadata to its generated Mach-O
objects, fixing the "no platform load command found" warnings emitted by
recent Apple linkers. Update the release-build tool pin while retaining
embedded dependency data.

Upstream fix:
rust-secure-code/cargo-auditable#267.

<!-- codex-thread: 01a0a4cb-88c3-7b20-8580-ae6c4b8998f6 -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cargo auditable build emits linker warnings on macOS

2 participants