Updating macOS versions will not modify your SIP configuration — this has been the case since SIP’s introduction all the way back in OS X 10.11 (〜2015).
※ If you are using an Intel/AMD (x86_64) machine, the rest of this post does not apply to you, and you are free to ignore it.
Anyway, if you are using an Apple Silicon (arm64e/AArch64) machine and have been using the “partially weakened” SIP configuration mentioned in the documentation, you will have to go back to 1TR (One True recoveryOS) and disable NVRAM protections in addition to the other two (debug
and fs
).
If you have been using the “fully disabled” SIP configuration (csrutil disable
), then you will not have to make any other changes.
The reason for this is because you will need to add a new boot-arg
to your NVRAM configuration (-arm64e_preview_abi
) to use TotalFinder on macOS 13.
Not to worry though — I’m currently working on the onboarding experience for this to make it as easy as possible for you all.
Basically, TotalFinder will detect and appropriately help walk you through the process of:
- Disabling SIP NVRAM protections if you still have them on
- Adding the
-arm64e_preview_abi
boot-arg either automatically using one button press, or instructing you on how to do it manually for those that prefer that, and then prompting for a reboot
… But wait, why is that required now?
The reason for this change is due to Apple introducing a new(-ish) dyld
feature in macOS 13 (and iOS 16) called “page-in linking”, which is explained in detail in Apple’s WWDC2022 video “Link fast: Improve build and launch times” at around 25m02s. (※ See also: Ueyama Rui’s analysis of this feature with regards to their excellent “mold
” linker.)
In the interest of trying to not go on yet another multi-paragraph lecture, here’s a really over-simplified explanation.
(Hi, Future Karen here! I uh… definitely failed at that goal.)
Traditionally, when loading a dynamically-linked library (or a “dylib”) on Apple OSes, something called dyld
handles the entire process of loading the dylib into a random empty space in memory and then “translating” all of the relevant memory addresses so that they’d correctly point to the wherever the dylib is actually located in memory.
(※ I’m sorry to all the developers that are currently yelling at their screens while reading this.)
Apple — in their ever-eternal pursuit of making their OSes as fast and efficient as possible — started using a technique almost a decade ago that they call “page-in linking” for dynamic libraries that are shipped as part of the system — specifically, those that are loaded from something called the dyld_shared_cache
(DSC).
By letting the kernel (XNU/Darwin) handle this operation instead of dyld
, the “time and memory cost” of loading a dylib is greatly lessened, leading to much faster application launch times, higher system responsiveness, lower memory usage, better security, and of course, greater power efficiency.
Basically, you can say that the full name of “page-in linking” is actually “kernel page-in linking”.
(Again, if you want to know more, I highly recommend watching the video linked above. This explanation is extremely simplified and completely disregards a lot of important aspects. See also: XNU/Darwin source code for map_with_linking_np()
.)
Okay, but like, you said that was almost a decade ago. What changed now?
The major change that Apple made in macOS 13 / iOS 16 is that now “page-in linking” is used for all dynamic libraries, not just ones shipped with the system (in the DSC).
… Or, well, it tries to. Not every dylib can be loaded this way, as only dylibs that are built with chained fixups enabled (LC_DYLD_CHAINED_FIXUPS
) are eligible.
Basically, that means anything targetting macOS 12 / iOS 15 as a minimum version.
“But Karen,” I hear you ask. “TotalFinder currently targets macOS 10.15 as a minimum version! Doesn’t this mean it’s exempt?”
Not quite. The important part thing that is affecting TotalFinder (and other macOS tweaks like it) is the “it tries to” part.
Instead of the traditional behaviour seen in macOS 12 / iOS 15 and below where loading dylibs is pretty much almost solely handled by dyld
, macOS 13 / iOS 16 appears to now always get the kernel involved to some extent in this process, regardless of whether or not the dylib works with page-in linking.
Apple’s arm64e
ABI
Apple’s platform-binaries shipped as part of their OSes are all compiled using the arm64e
architecture (arm64
+ pointer authentication). Naturally, this includes Finder.
In order to inject code into another process, the architecture of the code you want to inject must match the architecture of the original process. (※ There are slight exceptions to this regarding minor compatible subtypes such as armv7s
vs. armv7
, or x86_64h
vs. x86_64
.)
As a result, the TotalFinder dynamic library is compiled for arm64e
(and x86_64
), so it can be injected into Finder, an arm64e
process.
The problem here is that Apple considers their arm64e
ABI to be not yet ready for production use by anyone that isn’t themselves.
In other words, they consider the ABI “unstable” — in that the specifications of the ABI are subject to breaking changes in future OS versions at Apple’s leisure, without any notice or support given.
For this reason, Apple actually prevents you from running arm64e
executables by default.
Instead, you have to manually add a boot argument in NVRAM called -arm64e_preview_abi
, which you have to disable SIP NVRAM protections in order to do so.
This has been true since macOS 11, the first version of macOS to support Apple Silicon.
… But I don’t need that boot-arg
on my Apple Silicon machine running macOS 12 or 11! What changed in macOS 13?
Ah.
So here’s a fun little implementation detail.
The XNU/Darwin kernel is where the “Is this arm64e
binary a platform-binary
?” check is performed. (※ See below for the actual code excerpt.)
dyld
on the other hand, will happily load anything you give it.
Including an non-platform-binary arm64e
dynamic library. Such as TotalFinder.
But because macOS 13 now passes all dylib load requests through the kernel, TotalFinder is now suddenly affected by the arm64e restrictions, and thus now requires the -arm64e_preview_abi
boot-arg in order to successfully inject.
Aren’t there any workarounds?
Yes, actually!
I read through the source code for the XNU/Darwin kernel to see if I could find out how the arm64e validation is being performed.
After doing that, I managed to find… one potential workaround.
It involves manually modifying the Mach-O header to bypass the arm64e check in the kernel.
If you don’t know what that means, let’s just say it’s a really horrible no good very bad super ultra you should never do this idea.
So… yeah. I’m not doing that.
For the curious: Basically, if you modify your Mach-O header’s cpusubtype
in such a way that CPU_SUBTYPE_ARM64_PTR_AUTH_VERSION
(which uses CPU_SUBTYPE_ARM64_PTR_AUTH_MASK
) would result in some value that isn’t 0
, the XNU/Darwin kernel will just… happily allow your binaries to run.
This is because it quite literally only checks to see if the value is exactly 0, and nothing else.
(Please, please never ship software that does this.)
Final notes
- Unlike typical macOS / iOS tweaks, TotalFinder is technically not really a “dynamic library” or a “dylib” in the Apple Mach-O
MH_DYLIB
sense. Rather, it is actually a “loadable bundle”, or anMH_BUNDLE
.- That being said, it is still, for all intents and purposes, a dynamic library (the generic terminology), and is therefore loaded and treated like one.
- For more information, you can run
otool -hvvvvvv ${PATH_TO_WHATEVER_YOU_WANT_TO_ANALYSE}
.
- For some reason, it’s been found that macOS installs from China in particular (and maybe some other Asian countries? Definitely not Japan, though…) have arm64e ABI support enabled by default. No one really seems to know why this is the case, how this mechanism is triggered, or even how to detect it.
- My best guesses for how this is triggered is perhaps a flag in the SMBIOS? Or maybe detection based on regional hardware model IDs like what triggers camera shutter sounds in Japanese and South Korean iPhones? Honestly, it’s anyone’s guess, and Apple sure doesn’t have this behaviour documented anywhere (or even in the XNU/Darwin source code).
I absolutely failed at my original goal of trying not to write a massive essay. I’m sorry. ;P Hopefully this was interesting, at least!