MacOS System Integrity Protection and {DY,}DL_LIBRARY_PATH

It seems that Apple’s System Integrity Protection (SIP) causes (among other restrictions) {DY,}LD_LIBRARY_PATH to be ignored in many circumstances.

Geant4 is a framework which clients use by writing their own code which depends on the Geant4 libraries, being expected to specify the location of these libraries by setting DL_LIBRARY_PATH.

nixpgks.geant4 works like a charm on Linux, but gives mysterious crashes on Darwin. When using this derivation on Linux, DL_LIBRARY_PATH contains locations of Geant4 in the nix store; when using it on
MacOS, {DY,}DL_LIBRARY_PATH are empty.

As I am not an apple user, and do not have interactive access to a MacOS machine, getting to the bottom of this is difficult. Specifically, I do not know under exactly what conditions these environment variables are empty; I do vaguely recall some evidence of their being set correctly in some circumstances. I suspect that this is related to the exact circumstances under which SIP sanitizes these PATHs.

Besides completely disabling SIP on the system (something which users would reasonably decline to do), what can be done make the nixpgs.geant4 usable on MacOS?

I’m unsure from your description when/where the envs are set and when/where they are empty.

Someone on Matrix pointed out this Apple doc: Runtime Protections.

It says:

Spawning children processes of processes restricted by System Integrity Protection, such as by launching a helper process in a bundle with NSTask or calling the exec(2) command, resets the Mach special ports of that child process. Any dynamic linker (dyld ) environment variables, such as DYLD_LIBRARY_PATH , are purged when launching protected processes.

Do you know if there’s a system executable that might be in the execution chain via PATH or abspath?

Same person on IRC also pointed out an issue suggesting that the upstream may not be using DLYD_LIBRARY_PATH any more?

Given a flake which contains a devShell which has geant4 and{all,of,them} in packages, in the shell created by nix develop: LD_LIBRARY_PATH is set on Linux, but empty on macOS (as is DYLD_LIBRARY_PATH). However all of the envvars which point at the data directories (such as G4LEDATA) are set on both Linux and macOS.

IOW, the flake is setting G4-related envvars on both systems, but on macOS {DY,}LD_LIBRARY_PATH are empty.

Here is


in nix develop on macOS GitHub Actions

… but now I notice there’s something else even weirder going on, which I thought we had sidestepped already, but apparently not … I need to dig a bit more.

Ugh. There are so many ways in which this can be broken on macOS. Ideally, I’d find a working example of a flake that successfully uses nixpkgs.geant4 on macOS, to make sure that I’m not barking up the wrong tree.

1 Like

We’ve reduced it to a minimal example.

  1. We can compile hello-world with plain clang++ and run the resulting executable on macOS.

  2. We can compile and run this same program with cmake.

  3. As soon as we add

    find_package(Geant4 REQUIRED)

    to CMakeLists.txt, our hello-world program segfaults, when trying to access the standard library:

    Process 7764 stopped
    * thread #1, queue = '', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
        frame #0: 0x0000000000000000
    error: memory read failed for 0x0
    Target 0: (exampleB1) stopped.
    (lldb) up
    frame #1: 0x0000000102849bb8 libc++.1.0.dylib`std::__1::__stdinbuf<char>::imbue(std::__1::locale const&) + 52
    ->  0x102849bb8 <+52>: str    w0, [x19, #0x58]
        0x102849bbc <+56>: ldr    x0, [x19, #0x48]
        0x102849bc0 <+60>: ldr    x8, [x0]
        0x102849bc4 <+64>: ldr    x8, [x8, #0x38]
    rest of stack, for context
    (lldb) up
    frame #2: 0x000000010284941c libc++.1.0.dylib`std::__1::DoIOSInit::DoIOSInit() + 148
    ->  0x10284941c <+148>: add    x0, sp, #0x10
        0x102849420 <+152>: bl     0x10284b034               ; std::__1::locale::~locale()
        0x102849424 <+156>: nop    
        0x102849428 <+160>: ldr    x8, #0x6ed40              ; (void *)0x00000001031743d0: vtable for std::__1::basic_istream<char, std::__1::char_traits<char> >
    frame #3: 0x000000010284a9d4 libc++.1.0.dylib`_GLOBAL__I_000100 + 72
    ->  0x10284a9d4 <+72>: adr    x0, #-0xf58               ; std::__1::DoIOSInit::~DoIOSInit()
        0x10284a9d8 <+76>: nop    
        0x10284a9dc <+80>: adr    x2, #-0x369dc
        0x10284a9e0 <+84>: nop    
    frame #4: 0x00000001a0b4c1e0 dyld`invocation function for block in dyld4::Loader::findAndRunAllInitializers(dyld4::RuntimeState&) const::$_0::operator()() const + 168
    dyld`invocation function for block in dyld4::Loader::findAndRunAllInitializers(dyld4::RuntimeState&) const::$_0::operator()() const:
    ->  0x1a0b4c1e0 <+168>: add    x0, sp, #0x10
        0x1a0b4c1e4 <+172>: bl     0x1a0b2ba90               ; dyld3::ScopedTimer::endTimer()
        0x1a0b4c1e8 <+176>: ldp    x29, x30, [sp, #0xa0]
        0x1a0b4c1ec <+180>: ldp    x20, x19, [sp, #0x90]
    frame #5: 0x00000001a0b8dc60 dyld`invocation function for block in dyld3::MachOAnalyzer::forEachInitializer(Diagnostics&, dyld3::MachOAnalyzer::VMAddrConverter const&, void (unsigned int) block_pointer, void const*) const + 172
    dyld`invocation function for block in dyld3::MachOAnalyzer::forEachInitializer(Diagnostics&, dyld3::MachOAnalyzer::VMAddrConverter const&, void (unsigned int) block_pointer, void const*) const:
    ->  0x1a0b8dc60 <+172>: add    x21, x21, #0x8
        0x1a0b8dc64 <+176>: cmp    x21, x22
        0x1a0b8dc68 <+180>: b.lo   0x1a0b8dbf0               ; <+60>
        0x1a0b8dc6c <+184>: b      0x1a0b8dd2c               ; <+376>
    frame #6: 0x00000001a0b811a4 dyld`invocation function for block in dyld3::MachOFile::forEachSection(void (dyld3::MachOFile::SectionInfo const&, bool, bool&) block_pointer) const + 528
    dyld`invocation function for block in dyld3::MachOFile::forEachSection(void (dyld3::MachOFile::SectionInfo const&, bool, bool&) block_pointer) const:
    ->  0x1a0b811a4 <+528>: ldrb   w8, [x19]
        0x1a0b811a8 <+532>: cbnz   w8, 0x1a0b81308           ; <+884>
        0x1a0b811ac <+536>: add    x8, x25, #0x50
        0x1a0b811b0 <+540>: cmp    x25, x23
    frame #7: 0x00000001a0b2c2d8 dyld`dyld3::MachOFile::forEachLoadCommand(Diagnostics&, void (load_command const*, bool&) block_pointer) const + 296
    ->  0x1a0b2c2d8 <+296>: ldrb   w8, [sp, #0x3f]
        0x1a0b2c2dc <+300>: cbnz   w8, 0x1a0b2c25c           ; <+172>
        0x1a0b2c2e0 <+304>: add    w22, w22, #0x1
        0x1a0b2c2e4 <+308>: ldr    w8, [x20, #0x10]
    frame #8: 0x00000001a0b801cc dyld`dyld3::MachOFile::forEachSection(void (dyld3::MachOFile::SectionInfo const&, bool, bool&) block_pointer) const + 192
    ->  0x1a0b801cc <+192>: sub    x0, x29, #0x38
        0x1a0b801d0 <+196>: bl     0x1a0b2c354               ; Diagnostics::assertNoError() const
        0x1a0b801d4 <+200>: add    x0, sp, #0x48
        0x1a0b801d8 <+204>: mov    w1, #0x8
    frame #9: 0x00000001a0b82cfc dyld`dyld3::MachOFile::forEachInitializerPointerSection(Diagnostics&, void (unsigned int, unsigned int, bool&) block_pointer) const + 160
    ->  0x1a0b82cfc <+160>: ldp    x29, x30, [sp, #0x60]
        0x1a0b82d00 <+164>: ldp    x20, x19, [sp, #0x50]
        0x1a0b82d04 <+168>: ldp    x22, x21, [sp, #0x40]
        0x1a0b82d08 <+172>: add    sp, sp, #0x70
    frame #10: 0x00000001a0b8d904 dyld`dyld3::MachOAnalyzer::forEachInitializer(Diagnostics&, dyld3::MachOAnalyzer::VMAddrConverter const&, void (unsigned int) block_pointer, void const*) const + 432
    ->  0x1a0b8d904 <+432>: mov    x8, sp
        0x1a0b8d908 <+436>: adrp   x16, 374721
        0x1a0b8d90c <+440>: add    x16, x16, #0xed8          ; _NSConcreteStackBlock
        0x1a0b8d910 <+444>: mov    x17, x8
    frame #11: 0x00000001a0b48864 dyld`dyld4::Loader::findAndRunAllInitializers(dyld4::RuntimeState&) const + 448
    ->  0x1a0b48864 <+448>: ldrb   w8, [x21, #0x21]
        0x1a0b48868 <+452>: cbz    w8, 0x1a0b48924           ; <+640>
        0x1a0b4886c <+456>: add    x8, sp, #0x68
        0x1a0b48870 <+460>: add    x8, x8, #0x8
    frame #12: 0x00000001a0b48c18 dyld`dyld4::Loader::runInitializersBottomUp(dyld4::RuntimeState&, dyld3::Array<dyld4::Loader const*>&) const + 220
    ->  0x1a0b48c18 <+220>: ldp    x29, x30, [sp, #0x40]
        0x1a0b48c1c <+224>: ldp    x20, x19, [sp, #0x30]
        0x1a0b48c20 <+228>: ldp    x22, x21, [sp, #0x20]
        0x1a0b48c24 <+232>: ldp    x24, x23, [sp, #0x10]
    frame #13: 0x00000001a0b48bf4 dyld`dyld4::Loader::runInitializersBottomUp(dyld4::RuntimeState&, dyld3::Array<dyld4::Loader const*>&) const + 184
    ->  0x1a0b48bf4 <+184>: add    w23, w23, #0x1
        0x1a0b48bf8 <+188>: cmp    w23, w22
        0x1a0b48bfc <+192>:   0x1a0b48b80               ; <+68>
        0x1a0b48c00 <+196>: mov    x0, x19
    frame #14: 0x00000001a0b4c26c dyld`dyld4::Loader::runInitializersBottomUpPlusUpwardLinks(dyld4::RuntimeState&) const::$_1::operator()() const + 112
    dyld`dyld4::Loader::runInitializersBottomUpPlusUpwardLinks(dyld4::RuntimeState&) const::$_1::operator()() const:
    ->  0x1a0b4c26c <+112>: ldr    x1, [x19]
        0x1a0b4c270 <+116>: ldr    x8, [x1, #0x30]
        0x1a0b4c274 <+120>: lsl    x12, x8, #3
        0x1a0b4c278 <+124>: add    x9, x12, #0x8
    frame #15: 0x00000001a0b48d98 dyld`dyld4::Loader::runInitializersBottomUpPlusUpwardLinks(dyld4::RuntimeState&) const + 304
    ->  0x1a0b48d98 <+304>: ldrb   w8, [x19, #0x21]
        0x1a0b48d9c <+308>: cbz    w8, 0x1a0b48e58           ; <+496>
        0x1a0b48da0 <+312>: add    x8, sp, #0x8
        0x1a0b48da4 <+316>: add    x8, x8, #0x8
    frame #16: 0x00000001a0b6c984 dyld`dyld4::APIs::runAllInitializersForMain() + 468
    ->  0x1a0b6c984 <+468>: mov    x0, x20
        0x1a0b6c988 <+472>: mov    x1, x19
        0x1a0b6c98c <+476>: bl     0x1a0b44b04               ; dyld4::Loader::analyzer(dyld4::RuntimeState&) const
        0x1a0b6c990 <+480>: bl     0x1a0b7fea0               ; dyld3::MachOFile::isMainExecutable() const
    frame #17: 0x00000001a0b312d0 dyld`dyld4::prepare(dyld4::APIs&, dyld3::MachOAnalyzer const*) + 3480
    ->  0x1a0b312d0 <+3480>: bl     0x1a0b6aa7c               ; dyld4::notifyMonitoringDyldMain()
        0x1a0b312d4 <+3484>: mov    w0, #0x4
        0x1a0b312d8 <+3488>: movk   w0, #0x1f07, lsl #16
        0x1a0b312dc <+3492>: bl     0x1a0b2bdc0               ; dyld3::kdebug_trace_dyld_enabled(unsigned int)
    frame #18: 0x00000001a0b2fe18 dyld`start + 1964
    ->  0x1a0b2fe18 <+1964>: mov    x19, x0
        0x1a0b2fe1c <+1968>: ldrb   w8, [sp, #0xb1]
        0x1a0b2fe20 <+1972>: cbnz   w8, 0x1a0b2fe88           ; <+2076>
        0x1a0b2fe24 <+1976>: ldrb   w8, [sp, #0xb0]
    error: Already at the top of the stack.

    This stack trace comes from a machine running Apple Silicon, but we get essentially the same on an Intel machine. (No problems whatsoever on Linux.)

So it seems that merely adding the (nix flake-provided) Geant4 libraries as dependencies to the hello-world executable, prevents the latter from being able to find the standard libraries at run-time.

1 Like

Is that the flake being used? When I run the example, I get a segmentation fault. Based on your debugger trace, I knew what it was.

$ otool -L ./g4-examples/B1/build/exampleB1 | rg 'libc++|Qt'
	/nix/store/zca5zd4mc9121a3r4sarm03gb3xwy4bd-qtbase-5.15.9/lib/libQt5OpenGL.5.dylib (compatibility version 5.15.0, current version 5.15.9)
	/nix/store/zca5zd4mc9121a3r4sarm03gb3xwy4bd-qtbase-5.15.9/lib/libQt5PrintSupport.5.dylib (compatibility version 5.15.0, current version 5.15.9)
	/nix/store/zca5zd4mc9121a3r4sarm03gb3xwy4bd-qtbase-5.15.9/lib/libQt5Widgets.5.dylib (compatibility version 5.15.0, current version 5.15.9)
	/nix/store/zca5zd4mc9121a3r4sarm03gb3xwy4bd-qtbase-5.15.9/lib/libQt5Gui.5.dylib (compatibility version 5.15.0, current version 5.15.9)
	/nix/store/zca5zd4mc9121a3r4sarm03gb3xwy4bd-qtbase-5.15.9/lib/libQt5Core.5.dylib (compatibility version 5.15.0, current version 5.15.9)
	/nix/store/1wd5dggwpcnrjjlfjykl6hf2x03v6q6i-libcxxabi-16.0.1/lib/libc++abi.1.dylib (compatibility version 1.0.0, current version 1.0.0)
	/nix/store/mwpcf9kxw8k7w6wx9x3qjrj2rmjcmrhc-libcxx-16.0.1/lib/libc++.1.0.dylib (compatibility version 1.0.0, current version 1.0.0)
$ otool -L /nix/store/zca5zd4mc9121a3r4sarm03gb3xwy4bd-qtbase-5.15.9/lib/libQt5Core.5.dylib | rg 'libc++'
	/nix/store/ldjmd9ry137lb5f4s7lklp6scij0j9b5-libcxxabi-11.1.0/lib/libc++abi.1.dylib (compatibility version 1.0.0, current version 1.0.0)
	/nix/store/29dxsv48zqzb6w65jdb0agjhryhl0vyx-libcxx-11.1.0/lib/libc++.1.0.dylib (compatibility version 1.0.0, current version 1.0.0)

You’re linking two versions of libc++ into the same binary (Qt and geant4 are both linking against libc++ 11 while the example is linking against libc++ 16). That’s what is causing the crash.

There is work in progress to update the LLVM used by the Darwin stdenv. In the meantime, the easiest fix is to use on Darwin instead of clang_16. Otherwise, you’ll need to make sure all of your dependencies are linking against libc++ 16 instead of the one in the Darwin stdenv.


Thank you. We had got as far as getting otool to show us multiple versions on libc++, but then we didn’t really know what to do with it.

This seems to solve the problem.

How can I keep clang on Linux, without too much code repetition?

I don’t see how something{Darwin,Linux} can be used to distinguish between these two versions:

devShell = pkgs.mkShell.override { stdenv = pkgs.clang_16.stdenv; } { ... }
devShell = pkgs.mkShell.override {                                } { ... }

or these two:

devShell = pkgs.mkShell.override { stdenv = pkgs.clang_16.stdenv; } { ... }
devShell = pkgs.mkShell                                             { ... }

without having to repeat the contents of the { ... }.

Even with repetition, is{Darwin,Linux} return empty or non-empty arrays, and devShell needs a single value.

1 Like

This works for me to use clang 11 on Darwin and clang 16 on Linux.

diff --git a/flake.nix b/flake.nix
index fb26f92..01c8156 100644
--- a/flake.nix
+++ b/flake.nix
@@ -21,10 +21,12 @@
           enablePython         = false;
           enableRaytracerX11   = false;
+        mkShell = pkgs.mkShell.override {
+          stdenv = if pkgs.stdenv.isDarwin then pkgs.stdenv else pkgs.llvmPackages_16.stdenv;
+        };
       in {
-        devShell = pkgs.mkShell.override { stdenv = pkgs.clang_16.stdenv; } {
+        devShell = mkShell {
           name = "G4-examples-devenv";
           packages = with pkgs; [
@@ -36,7 +38,6 @@
-            clang_16

If you want to use clang 16 on Darwin anyway (before the stdenv update lands), it is possible to use libc++ 11 with clang 16. It’s not pretty though.

diff --git a/flake.nix b/flake.nix
index fb26f92..d1320c9 100644
--- a/flake.nix
+++ b/flake.nix
@@ -22,9 +22,28 @@
           enableRaytracerX11   = false;
+        clang_16 = if pkgs.stdenv.isDarwin
+          then pkgs.llvmPackages_16.clang.override rec {
+            libc = pkgs.darwin.apple_sdk.Libsystem;
+            bintools = pkgs.bintools.override { inherit libc; };
+            inherit (pkgs.llvmPackages) libcxx;
+            extraPackages = [
+              pkgs.llvmPackages.libcxxabi
+              # Use the compiler-rt associated with clang, but use the libc++abi from the stdenv
+              # to avoid linking against two different versions (for the same reasons as above).
+              (pkgs.llvmPackages_16.compiler-rt.override {
+                inherit (pkgs.llvmPackages) libcxxabi;
+              })
+            ];
+          }
+          else pkgs.llvmPackages.clang;
+        mkShell = pkgs.mkShell.override {
+          stdenv = pkgs.overrideCC pkgs.llvmPackages_16.stdenv clang_16;
+        };
       in {
-        devShell = pkgs.mkShell.override { stdenv = pkgs.clang_16.stdenv; } {
+        devShell = mkShell {
           name = "G4-examples-devenv";
           packages = with pkgs; [
@@ -36,7 +55,6 @@
-            clang_16

Many thanks for your lucid diagnosis and your clear and explicit instructions on how to fix things.

We have wasted many hours on grappling with this whole problem, and you’ve got us past something that had us completely stymied.

Much appreciated.


I tried this earlier, but it turns out that other changes resulted in it not being evaluated at all, so I mistakenly believed that it had worked.

It looks like there is a slight error: the apple_sdk in darwin.apple_sdk.Libsystem should not be there.

1 Like

I made those changes to the master branch and followed the instructions in the repo to do nix develop and build the example. The clang I got was the requested version, and I was able to build and run the example.

The libc stuff may not be needed (I’ve gotten used to doing it defensively while working on the stdenv), but it shouldn’t cause any errors. Is that an evaluation error or an error while building or at runtime?

1 Like


I get an evaluation error. Poking around in nix repl shows that pkgs.darwin.apple_sdk.Libsystem does not exist, while pkgs.darwin.Libsystem does. So I removed the apple_sdk and everything worked.

Here is an example of the error appearing on GHA.


That is weird. I don’t know why I was able to build it locally. Glad you were able to find a solution.

I gladly suffer the injuries to my forehead caused banging my head against Nix, largely to avoid the it-worked-on-my-machine problem, so it always freaks me out a bit when it happens on Nix. And it does. Many orders of magnitude less than anywhere else, but it still does.

Anyway, many thanks again for your help. Very useful.