Skip to content

.NET 9.0.1 Breaking Change in macOS native library resolution #112080

Open

Description

@yaakov-h

Description

.NET runtime 9.0.1 seems to have introduced a breaking change with regards to how dyld on macOS resolves relative paths to system frameworks.

I suspect this will be related to the change in pipelines from macos-12 to macos-13:

        # OSX Public Build Pool (we don't have on-prem OSX BuildPool).
        ${{ if and(in(parameters.osGroup, 'osx', 'maccatalyst', 'ios', 'iossimulator', 'tvos', 'tvossimulator'), eq(variables['System.TeamProject'], 'public')) }}:
-          vmImage: 'macos-12'
+          vmImage: 'macos-13'

        # OSX Internal Pool
        ${{ if and(in(parameters.osGroup, 'osx', 'maccatalyst', 'ios', 'iossimulator', 'tvos', 'tvossimulator'), ne(variables['System.TeamProject'], 'public')) }}:
          name: "Azure Pipelines"
-          vmImage: 'macOS-12'
+          vmImage: 'macOS-13'
          os: macOS

        # Official Build Windows Pool

(I don't know enough about the .NET build engineering side so I could be wrong here, but that is definitely sus.)

Is there a way to restore the old behaviour for the .NET 9 lifecycle?

If not, should this at least be documented somewhere? I didn't see this in any of the release notes.

See SteamRE/SteamKit#1501 and Homebrew/homebrew-core#206331 (comment) for details.

Reproduction Steps

Attempt to DllImport/LibraryImport a system framework using a relative path on macOS e.g. CoreFoundation.framework/CoreFoundation

Expected behavior

The P/Invoke function is loaded and executed.

Actual behavior

System.DllNotFoundException : Unable to load shared library 'CoreFoundation.framework/CoreFoundation' or one of its dependencies. In order to help diagnose loading problems, consider setting the DYLD_PRINT_LIBRARIES environment variable: 
dlopen(/Users/runner/.dotnet/shared/Microsoft.NETCore.App/9.0.1/CoreFoundation.framework/CoreFoundation.dylib, 0x0001): tried: '/Users/runner/.dotnet/shared/Microsoft.NETCore.App/9.0.1/CoreFoundation.framework/CoreFoundation.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/Users/runner/.dotnet/shared/Microsoft.NETCore.App/9.0.1/CoreFoundation.framework/CoreFoundation.dylib' (no such file), '/Users/runner/.dotnet/shared/Microsoft.NETCore.App/9.0.1/CoreFoundation.framework/CoreFoundation.dylib' (no such file)
dlopen(/Users/runner/work/SteamKit/SteamKit/SteamKit2/Tests/bin/Debug/net8.0/CoreFoundation.framework/CoreFoundation.dylib, 0x0001): tried: '/Users/runner/work/SteamKit/SteamKit/SteamKit2/Tests/bin/Debug/net8.0/CoreFoundation.framework/CoreFoundation.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/Users/runner/work/SteamKit/SteamKit/SteamKit2/Tests/bin/Debug/net8.0/CoreFoundation.framework/CoreFoundation.dylib' (no such file), '/Users/runner/work/SteamKit/SteamKit/SteamKit2/Tests/bin/Debug/net8.0/CoreFoundation.framework/CoreFoundation.dylib' (no such file)
dlopen(CoreFoundation.framework/CoreFoundation.dylib, 0x0001): tried: 'CoreFoundation.framework/CoreFoundation.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OSCoreFoundation.framework/CoreFoundation.dylib' (no such file), '/usr/lib/CoreFoundation.framework/CoreFoundation.dylib' (no such file, not in dyld cache), 'CoreFoundation.framework/CoreFoundation.dylib' (no such file)
dlopen(/Users/runner/.dotnet/shared/Microsoft.NETCore.App/9.0.1/CoreFoundation.framework/CoreFoundation, 0x0001): tried: '/Users/runner/.dotnet/shared/Microsoft.NETCore.App/9.0.1/CoreFoundation.framework/CoreFoundation' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/Users/runner/.dotnet/shared/Microsoft.NETCore.App/9.0.1/CoreFoundation.framework/CoreFoundation' (no such file), '/Users/runner/.dotnet/shared/Microsoft.NETCore.App/9.0.1/CoreFoundation.framework/CoreFoundation' (no such file)
dlopen(/Users/runner/work/SteamKit/SteamKit/SteamKit2/Tests/bin/Debug/net8.0/CoreFoundation.framework/CoreFoundation, 0x0001): tried: '/Users/runner/work/SteamKit/SteamKit/SteamKit2/Tests/bin/Debug/net8.0/CoreFoundation.framework/CoreFoundation' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/Users/runner/work/SteamKit/SteamKit/SteamKit2/Tests/bin/Debug/net8.0/CoreFoundation.framework/CoreFoundation' (no such file), '/Users/runner/work/SteamKit/SteamKit/SteamKit2/Tests/bin/Debug/net8.0/CoreFoundation.framework/CoreFoundation' (no such file)
dlopen(CoreFoundation.framework/CoreFoundation, 0x0001): tried: 'CoreFoundation.framework/CoreFoundation' (no such file), '/System/Volumes/Preboot/Cryptexes/OSCoreFoundation.framework/CoreFoundation' (no such file), '/usr/lib/CoreFoundation.framework/CoreFoundation' (no such file, not in dyld cache), 'CoreFoundation.framework/CoreFoundation' (no such file)

Regression?

This worked in .NET 8 and seems to also work in .NET 9.0.0. It fails in .NET 9.0.1.

Known Workarounds

Workaround 1: Launch with the environment variable DYLD_FRAMEWORK_PATH=/System/Library/Frameworks preset.

Workaround 2: Recompile and redeploy binaries with DllImport/LibraryImport set to an absolute path to the framework binaries, rather than a relative path.

Configuration

macos-14 and macos-latest on GitHub Actions, as well as my MacBook Pro (M2 Pro) with Sequoia 15.3 (24D60) arm64

.NET 9.0.1

Other information

cc @dotnet/compat

Activity

  1. ghost added
    needs-area-labelAn area label is needed to ensure this gets routed to the appropriate area owners
    on Feb 3, 2025
  2. changed the title [-].NET 9.0.1 Breaking Change in macOS assembly resolution[/-] [+].NET 9.0.1 Breaking Change in macOS native library resolution[/+] on Feb 3, 2025
  3. removed
    needs-area-labelAn area label is needed to ensure this gets routed to the appropriate area owners
    on Feb 3, 2025
  4. dotnet-policy-service commented on Feb 3, 2025

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @dotnet/interop-contrib
    See info in area-owners.md if you want to be subscribed.

  5. AaronRobinsonMSFT commented on Feb 3, 2025

    @AaronRobinsonMSFT
    Member

    @elinor-fung Thoughts on what might have changed here?

  6. yaakov-h commented on Feb 3, 2025

    @yaakov-h
    MemberAuthor

    FWIW I'm also seeing this now on .NET 8.0.12.

    If I run the failing unit test with DOTNET_ROLL_FORWARD=disable it passes, with DOTNET_ROLL_FORWARD=LatestPatch it fails.

    I suspect that with <RollForward>LatestMajor</RollForward> in the test .csproj it was rolling forward to 9.0 so I'm not sure which patch of .NET 8 introduced the same bug as I tried to set several but they would have all been skipped due to rollforward behaviour.

    Though given that the same vmImage update occurred between .NET 8.0.11 and .NET 8.0.12, I would put my money on that...

  7. agocke commented on Feb 3, 2025

    @agocke
    Member

    I don’t think we changed anything in the code, so I agree that if something changed the OS image update looks notable. But I have no idea what could have changed in 13 to cause this type of code change.

  8. added this to the 9.0.x milestone on Feb 3, 2025
  9. removed
    untriagedNew issue has not been triaged by the area owner
    on Feb 3, 2025
  10. ejhg commented on Feb 22, 2025

    @ejhg

    I am seeing this issue in sdk 8.0.405 and 8.0.406. (Issue is absent in 8.0.404 and prior.)

  11. DennisDyallo commented on Mar 6, 2025

    @DennisDyallo

    We're seeing this issue in the Yubico.NET.SDK which loads some native libraries in order to do some native work (connecting to USB-devices, CFString, etc.)

    We came across the error when upgrading our integration test and example projects to .net8 and .net9 and running on Mac (Sequoia 15.2, 15.3.1)

    While these workarounds work fine in our own integration testing, it wont work well downstream with the end users of our SDK/3rd party client apps.

    Would like to see a confirmation from MSFT on the issue, the intended fix and forward guidance.

  12. DennisDyallo commented on Apr 22, 2025

    @DennisDyallo

    Any news?

  13. elinor-fung commented on Sep 29, 2025

    @elinor-fung
    Member

    I suspect this will be related to the change in pipelines from macos-12 to macos-13

    if something changed the OS image update looks notable. But I have no idea what could have changed in 13 to cause this type of code change.

    I couldn't find anything official like release notes or some breaking change notice, but there are are various (non-.NET-related) reports about macOS library load behaviour changing after a tools version update - for example, https://developer.apple.com/forums/thread/737920

    The dyld man page at https://github.com/apple-oss-distributions/dyld/blob/main/doc/man/man1/dyld.1 does indicate there was some change around this.

    DYLD_FALLBACK_FRAMEWORK_PATH

    For new binaries (Fall 2023 or later) there is no default fallback. For older binaries,
    there is a default fallback search path of: /Library/Frameworks:/System/Library/Frameworks

    DYLD_FALLBACK_LIBRARY_PATH

    For new binaries (Fall 2023 or later) there is no default. For older binaries,
    there is a default fallback search path of: /usr/local/lib:/usr/lib.

    Assuming this new behaviour is intentional on the macOS side, I don't know that the .NET runtime should try to do anything explicit to go back to the old behaviour. This step of our native library load is basically 'use default OS load'.

    @agocke do we have contacts on the Apple side we could ask about this? And if they have guidance for folks that were relying on the previous fallbacks?

  14. agocke commented on Sep 30, 2025

    @agocke
    Member

    Yup, I can start a conversation with Apple to get confirmation that this is intended.

  15. CamJN commented on Oct 3, 2025

    @CamJN

    Assuming that the loading behaviour of macOS has changed, how should a C# library add paths to load from on macOS? I've tried reading about it, but not found anything practical. For example this seems to only handle the executable case, not the case of a library providing bindings to a native library.

  16. jkotas commented on Oct 3, 2025

    @jkotas
    Member

    You can use absolute paths in DllImport.

    If it is not possible or desirable, the library that provides binding to native library can register DllImport resolver for itself. For example, including the following file in the library project is going to register the resolver before any code in that library gets executed:

    using System.Reflection;
    using System.Runtime.CompilerServices;
    using System.Runtime.InteropServices;
    
    internal static class NativeLibraryResolver
    {
        [ModuleInitializer]
        internal static void Initialize()
        {
            if (OperatingSystem.IsMacOS())
            {
                NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), DllImportResolver);
            }
        }
    
        private static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
        {
            if (libraryName == "nativedep")
            {
                return NativeLibrary.Load("/absolute/path/to/load/library.dylib");
            }
    
            return IntPtr.Zero;
        }
    }
  17. CamJN commented on Oct 3, 2025

    @CamJN

    @jkotas thanks so much!

  18. agocke commented on Oct 3, 2025

    @agocke
    Member

    In particular, if you're trying to load a library that you know is part of the system install, potentially in a path like /System/Library/Frameworks, then I would definitely recommend loading via absolute path. In that case you know where the library should be and where you want to load from.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    • Status
      No status

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions