Skip to content

--self-contained macOS publish does not include external native dependencies from source-built runtime packs #5632

Description

@ttrias

Description

A source-built .NET macOS runtime pack can contain a native runtime asset with dependencies outside the runtime pack. dotnet publish --self-contained copies the runtime asset but does not copy or validate that external native dependency closure, so the resulting application is not actually self-contained.

This occurs with Homebrew's Apple-silicon dotnet 10.0.302 bottle. Its Microsoft.NETCore.App.Runtime.osx-arm64 copy of libSystem.IO.Compression.Native.dylib links to Homebrew's separately installed Brotli libraries. A Developer ID-signed application can pass strict code-signature verification, notarization, stapling, and Gatekeeper assessment, but then abort in dyld before managed startup because those libraries are outside the application and have a different signing Team ID.

The corresponding runtime asset in Microsoft's official 10.0.302 SDK does not have external Brotli dependencies.

Homebrew discussion: https://github.com/orgs/Homebrew/discussions/6991

The Homebrew maintainer's position is that the formula builds .NET using the supplied build/install instructions and that dependency closure introduced by that build should be handled by .NET.

Reproduction Steps

The affected runtime-pack asset can be inspected without installing the formula:

brew update
brew fetch --formula dotnet
bottle="$(brew --cache --formula dotnet)"
work="$(mktemp -d)"
tar -xzf "$bottle" -C "$work"
runtime_lib="$(find "$work" -path '*Microsoft.NETCore.App.Runtime.osx-arm64*' -name libSystem.IO.Compression.Native.dylib -print -quit)"
otool -L "$runtime_lib"

For the ARM64 Tahoe bottle of dotnet 10.0.302 (SHA-256 a420ba0e0a6f87c5b62327a4f40c6d2130bf589af985ec6ed02f7c00d9067cec), the dependencies include:

@@HOMEBREW_PREFIX@@/opt/dotnet/libexec/shared/Microsoft.NETCore.App/10.0.10/libSystem.IO.Compression.Native.dylib
/usr/lib/libz.1.dylib
@@HOMEBREW_PREFIX@@/opt/brotli/lib/libbrotlidec.1.dylib
@@HOMEBREW_PREFIX@@/opt/brotli/lib/libbrotlienc.1.dylib
/usr/lib/libSystem.B.dylib

After installation, the placeholders resolve under /opt/homebrew.

Publish a net10.0/net10.0-macos application for osx-arm64 with --self-contained true. The published libSystem.IO.Compression.Native.dylib retains those absolute paths. In an application bundle it may appear under Contents/MonoBundle.

Sign the application with Developer ID and hardened runtime, then launch it. The observed failure is:

Termination Reason: Namespace DYLD, Code 1, Library missing
Library not loaded: /opt/homebrew/opt/brotli/lib/libbrotlidec.1.dylib
Referenced from: MyApp.app/Contents/MonoBundle/libSystem.IO.Compression.Native.dylib
Reason: mapping process and mapped file (non-platform) have different Team IDs

The same failure mode also makes the output non-portable to a clean Mac where /opt/homebrew/opt/brotli is absent.

For comparison, the corresponding native library from Microsoft's official .NET 10.0.302 SDK reports only:

@rpath/libSystem.IO.Compression.Native.dylib
/usr/lib/libSystem.B.dylib

Rebuilding the same application with that SDK produced no non-system absolute dependencies and launched successfully.

Expected behavior

A runtime pack used by --self-contained publication should be closed over its native runtime dependencies and remain relocatable.

Possible ownership models include:

  • source-build/runtime-pack construction packages the external native dependency closure in a relocatable form;
  • the build/install instructions prevent runtime-pack assets from acquiring dependencies outside the pack; or
  • publication recursively discovers, copies, and validates the native dependency closure.

The exact layer is a design decision, but a --self-contained output should not retain absolute dependencies on the build machine's package-manager prefix.

Actual behavior

dotnet publish --self-contained copies libSystem.IO.Compression.Native.dylib from the source-built runtime pack without copying its Brotli closure or rejecting its absolute load commands.

The output can therefore:

  • fail on a clean target where Homebrew Brotli is absent; or
  • fail hardened-runtime library validation when Homebrew Brotli is installed but signed by a different Team ID.

Standard signing and notarization checks did not detect the dependency because the referenced Brotli libraries remained outside the application bundle.

Regression?

Unknown. The issue is present in the Homebrew-built .NET 10.0.302 / Microsoft.NETCore.App 10.0.10 ARM64 runtime pack. Microsoft's official SDK of the same version does not reproduce it.

Known Workarounds

Using Microsoft's official SDK produces a relocatable runtime asset.

We also verified a narrow post-publish repair:

  1. Detect the exact Brotli links in libSystem.IO.Compression.Native.dylib.
  2. Copy libbrotlidec, libbrotlienc, and their transitive libbrotlicommon dependency into Contents/Frameworks.
  3. Rewrite load commands and dylib IDs to bundle-relative @loader_path/@rpath references.
  4. Preserve effective application entitlements (including com.apple.security.cs.allow-jit) and sign the copied/modified libraries and enclosing application with the same Developer ID identity.
  5. Include Brotli's license notice.

After this repair, all 14 Mach-O files in the tested bundle had only system or bundle-relative dependencies, strict signature verification passed, and both the application and the application extracted from a newly signed installer launched successfully. Brotli was the only external dependency family found in that bundle.

Configuration

Homebrew 6.0.15
dotnet formula 10.0.302
Microsoft.NETCore.App runtime 10.0.10
macOS 26.6 arm64
Xcode 26.6
Apple silicon; Rosetta not installed

Other information

This report intentionally does not assume whether dotnet/runtime, source-build, or the SDK publisher should implement the final correction. Please transfer or redirect it if another .NET repository owns the dependency-closure contract.

Disclosure: I used OpenAI Codex to help inspect the binaries and draft this report. I verified the commands and output and will answer maintainer questions myself.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status
    Backlog
    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions