# macOS's System Integrity Protection sanitizes your environment

tags:

macOS added System Integrity Protection in El Capitan. It’s supposed to limit what root users can do and prevent changes to system files. Noble goals, especially for non-developers.

But its more than. Some programs sanitize the environment before they start child processes so I can’t substitute my own, potentially malicious libraries. I’m being generous when I say that this is lightly documented. Specifically, environment variables starting with DYLD_ and LD_ are unset for child process started by system programs. This works in the Apple ecosystem, but some third-party tools still rely on it.

That macOS would do this makes sense for most people. These environment variables were supposed to be about debugging so you could compile a library and try it with a program. As with many things, how people used it was different. As a developer, I’m doing things virtually no one else in the world is doing. I don’t want to use these variables, but other things I want to use wants to use these variables.

## Debugging hell

Imagine trying to debug something like this in a bizarro world in which I swear I’ve set the variable, I’ve stared at it for minutes to ensure I’ve spelled it correctly instead of DLYD_LIRARY_PATH, and I can see it in small test programs. I swear it’s set up, but when I go back to the big situation, it’s gone.

I discovered this because I was testing some Postgres stuff and setting different values on the command line:

$env PGSSLMODE=require ...  My Perl program then couldn’t find /Library/PostgreSQL/12/lib/libssl.1.1.dylib and I’d get an error that says it can’t find the library: Library not loaded: libssl.1.1.dylib  Googling did not help that much. Most answers were just cargo-culting advice about Homebrew that is essentially “have you tried reinstalling it?” As an aside, I don’t mind that people use convenience tools such as brew. However, you should understand what it does and how it does it if you want to be a developer. You can see the trail of destruction and wasted time from people who’ve neglected to learn their tools—they can’t even diagnose the problem. ## DYLD_LIBRARY_PATH I can set the DYLD_LIBRARY_PATH environment variable to tell processes where it can find dynamic libraries, and I can see that I’ve set it: $ export DYLD_LIBRARY_PATH=/Library/PostgreSQL/12/lib
$echo$DYLD_LIBRARY_PATH
/Library/PostgreSQL/12/lib


With a perl that I’ve compiled and installed myself (not the macOS system Perl), I can see the value from a Perl program, but from the system perl I can’t:

$which perl /Users/brian/bin/perl$ perl -le 'print $ENV{DYLD_LIBRARY_PATH}' /Library/PostgreSQL/12/lib$ /usr/bin/perl -le 'print $ENV{DYLD_LIBRARY_PATH}'$


But, if I run the same Perl one-liner under env, I can’t see it:

$env which perl /Users/brian/bin/perl$ env perl -le 'print $ENV{DYLD_LIBRARY_PATH}'$


This isn’t a Perl thing. Here’s the same thing in Ruby, which I installed myself:

$which ruby /usr/local/bin/ruby$ ruby -e 'puts ENV["DYLD_LIBRARY_PATH"]'
/Library/PostgreSQL/12/lib
$env ruby -e 'puts ENV["DYLD_LIBRARY_PATH"]'$


Even worse, just running env means I won’t see all of the environment variables. The variable is set and I can see it with echo, but env doesn’t even know it exists:

$env | grep DYLD$


And here’s a small Makefile to show the value of DYLD_LIBRARY_PATH:

all:
@ echo "DYLD_LIBRARY_PATH=" $(DYLD_LIBRARY_PATH)  It also sanitizes the environment when I use the XTools make (and I’d rather not muddy the waters with a different set of tools): $ which make
/usr/bin/make
$echo$DYLD_LIBRARY_PATH
/Library/PostgreSQL/12/lib
$make DYLD_LIBRARY_PATH=  ## What can I do? As the nuclear option, I can turn off SIP. I have to boot into Recovery Mode and disable SIP from the terminal, then reboot into normal mode: $ csrutil disable


I don’t really want to do that though. I’d rather leave my base system as close to pristine as I can. The more I diverge from the normal case, the less I develop for the normal case. Something works accidentally for me because I have a special. more omniscient system.

I can change my Makefile to set a default (assign with ?=) with a safe environment variable name and re-export it. Without the export, the echo sees the Makefile variable, but perl would not see an environment variable:

export DYLD_LIBRARY_PATH ?= $(MY_DYLD_LIBRARY_PATH) all: echo "DYLD_LIBRARY_PATH="$(DYLD_LIBRARY_PATH)
perl -le 'print ENV{DYLD_LIBRARY_PATH}'


This requires me to set the extra environment variable, which is easy enough in a startup file. The hard part is adjusting foreign code to use this. This is where I think most developers would stop because it’s achievable and we’re used to working around obstacles when we can’t get through them.

However, I didn’t give up. There must be a better way. If I’m not supposed to use DYLD_LIBRARY_PATH, how does Apple expect me to do it?

After I compiled DBD::Pg, I looked at the .bundle file it created. otool can show you which libraries it wants:

$otool -L ./blib/arch/auto/DBD/Pg/Pg.bundle ./blib/arch/auto/DBD/Pg/Pg.bundle: libssl.1.1.dylib (compatibility version 1.1.0, current version 1.1.0) libcrypto.1.1.dylib (compatibility version 1.1.0, current version 1.1.0) libpq.5.dylib (compatibility version 5.0.0, current version 5.12.0) /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.100.1)  Three of those paths are relative, which is why it needs something like DYLD_LIBRARY_PATH to find them. The other one is an absolute paths. Huh. If I update those to absolute paths, I don’t need to search through directories for them. The install_name_change tool (virtually completely undocumented) does this. There are two ways to go here based on where this library is. If I can’t tell the root directory for the library, I can use the RPATH infrastructure. However, in this case, I know that the directory is going to be /Library/PostgreSQL/12/lib, so I can use absolute paths for these libraries. The install_name_change lets me update the library paths for either RPATH or absolute paths (Fun with rpath, otool, and install_name_tool is a nice read): $ install_name_tool -change libssl.1.1.dylib /Library/PostgreSQL/12/lib/libssl.1.1.dylib ./blib/arch/auto/DBD/Pg/Pg.bundle
$install_name_tool -change libcrypto.1.1.dylib /Library/PostgreSQL/12/lib/libcrypto.1.1.dylib ./blib/arch/auto/DBD/Pg/Pg.bundle$ install_name_tool -change libpq.5.dylib /Library/PostgreSQL/12/lib/libpq.5.dylib ./blib/arch/auto/DBD/Pg/Pg.bundle


I check with otool again to make sure it took:

\$ otool -L ./blib/arch/auto/DBD/Pg/Pg.bundle
./blib/arch/auto/DBD/Pg/Pg.bundle:
/Library/PostgreSQL/12/lib/libssl.1.1.dylib (compatibility version 1.1.0, current version 1.1.0)
/Library/PostgreSQL/12/lib/libcrypto.1.1.dylib (compatibility version 1.1.0, current version 1.1.0)
/Library/PostgreSQL/12/lib/libpq.5.dylib (compatibility version 5.0.0, current version 5.12.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.100.1)


And that works. I’ve filed an issue on DBD::Pg, but I haven’t worked on fixing the module installer.

## A list of ignored variables:

SIP strips out any environment variables starting with DYLD_ or LD_, but here are the ones for the search engines:

• DYLD_LIBRARY_PATH
• DYLD_FALLBACK_LIBRARY_PATH
• LD_LIBRARY_PATH
• DYLD_INSERT_LIBRARIES