Sunday, February 28, 2021

How do I make newer Unity games backwards-compatible with OS X 10.9 “Mavericks”?

The crash log indicates that the game is looking for a function called getattrlistbulk. Because this function doesn't exist in Mavericks, the game doesn't know what to do, and crashes. Ergo, in order for the game to run, we'll have to give it what it wants—a copy of the getattrlistbulk function.

(In other words, we need to write some code. Make sure you have Apple's Xcode Command Line Tools installed!)

getattrlistbulk is a part of the kernel, which means I definitely don't understand what it does. But, what if I don't have to—what if Unity games don't actually need getattrlistbulk for anything important, and/or have a fallback codepath for unexpected values? If that was the case, it might be that getattrlistbulk doesn't need to actually do anything for the game to run, it merely needs to exist.

Let's give this code a try:

#include <sys/attr.h>

int getattrlistbulk(int dirfd, struct attrlist * attrList, void * attrBuf, size_t attrBufSize, uint64_t options) {
    return 0;
}

This copy of getattrlistbulk will always return 0, no matter what.

If you save this code as UnityFixer.m, you can compile it with:

clang -compatibility_version 9999 -o UnityFixer.dylib -dynamiclib UnityFixer.m

Now, we need to make the game load this library, which should be easy to do with the DYLD_INSERT_LIBRARIES environment variable. Run in the Terminal:

DYLD_INSERT_LIBRARIES=UnityFixer.dylib Sayonara\ Wild\ Hearts.app/Contents/MacOS/Sayonara\ Wild\ Hearts

...and watch the game crash the exact same way it did before:

Dyld Error Message:
  Symbol not found: _getattrlistbulk
  Referenced from: /Users/USER/Desktop/Sayonara Wild Hearts.app/Contents/MacOS/../Frameworks/UnityPlayer.dylib
  Expected in: /usr/lib/libSystem.B.dylib

Why can't UnityPlayer.dylib find our fancy new getattrlistbulk function?

Let's look at the crash report again. UnityPlayer isn't expecting getattrlistbulk to be just anywhere, it's expecting it to be in /usr/lib/libSystem.B.dylib, and so isn't looking inside of our UnityFixer library. This is due to a concept called two-level namespaces, which you can and should read more about here. And, although two-level namespacing can be turned off via DYLD_FORCE_FLAT_NAMESPACE=1, Unity games won't work without it.

Let's try something else. What if we made the game load our library, UnityFixer.dylib, in place of libSystem.B.dylib? Apple's install_name_tool command makes this easy! Copy UnityFixer.dylib into the application bundle's Contents/Frameworks directory, and then run:

install_name_tool -change /usr/lib/libSystem.B.dylib @executable_path/../Frameworks/UnityFixer.dylib Sayonara\ Wild\ Hearts.app/Contents/Frameworks/UnityPlayer.dylib

(Replace Sayonara\ Wild\ Hearts with the name of your game.)

Now, try launching the game again, and...

Application Specific Information:
dyld: launch, loading dependent libraries

Dyld Error Message:
  Symbol not found: dyld_stub_binder
  Referenced from: /Users/USER/Desktop/Sayonara Wild Hearts.app/Contents/MacOS/Sayonara Wild Hearts
  Expected in: /Users/USER/Desktop/Sayonara Wild Hearts.app/Contents/MacOS/../Frameworks/UnityFixer.dylib
 in /Users/USER/Desktop/Sayonara Wild Hearts.app/Contents/MacOS/Sayonara Wild Hearts

Hey, at least the crash log changed this time! You can probably already guess why this didn't work. libSystem.B.dylib is essentially the Mac equivalent of Linux's libc, and as such, it contains many, many functions. UnityFixer only contains getattrlistbulk. And so although the game now has access to getattrlistbulk, it's missing everything else!

What we want is for our UnityFixer library to also provide all of the other libSystem functions in addition to its own—which is to say, we should make libSystem.B.dylib a sub-library of UnityFixer.dylib.

I did this using optool:

optool install -c reexport -p /usr/lib/libSystem.B.dylib -t Sayonara\ Wild\ Hearts.app/Contents/Frameworks/UnityFixer.dylib

(There's probably a cleaner way to link this at compile-time, but I couldn't figure out how, and optool worked.)

Now, try launching the game again, and...

It works!


Theoretical FAQs

(Follow-up questions no one has asked, but which they theoretically could.)

Q: Is the return value of 0 important?

A: Yes. I originally tried 1, but that caused some games to allocate all available memory and crash once none was left.

Q: Will this make all Unity games work properly in Mavericks?

A: No. As far as I'm aware, it will allow any Unity 2018/2019 game to start up, but no one said anything about working properly! Games are complex, and these ones have clearly never been tested on Mavericks before, so they may have other glitches. Timelie is one example of a game which technically works with this fix, but has very severe graphical problems.

Many other games, however, really do seem to run perfectly.

Q: Is it possible to actually reimplement getattrlistbulk instead of using a stub function?

A: Maybe? The Internet™ says getattrlistbulk is a replacement for getdirentriesattr. So, at one point I tried:

#include <sys/attr.h>
#include <unistd.h>

int getattrlistbulk(int dirfd, struct attrlist * attrList, void * attrBuf, size_t attrBufSize, uint64_t options) {
    
    unsigned int count;
    unsigned int basep;
    unsigned int newState;
    
    return getdirentriesattr(dirfd, &attrList, &attrBuf, attrBufSize, &count, &basep, &newState, options);
}

This was written by pattern-matching the example code for getattrlistbulk and getdirentriesattr in the developer documentation. It does work (insofar as games run), but then, so does return 0, so I really have no way to test the code. Under the circumstances, return 0 seems safer.

Q: Can I get a tl;dr?

A: Sure:

  1. Copy the first code block in this answer into a file named UnityFixer.m.
  2. clang -compatibility_version 9999 -o /Path/To/Game.app/Contents/Frameworks/UnityFixer.dylib -dynamiclib /Path/To/UnityFixer.m
  3. install_name_tool -change /usr/lib/libSystem.B.dylib @executable_path/../Frameworks/UnityFixer.dylib /Path/To/Game.app/Contents/Frameworks/UnityPlayer.dylib
  4. Download optool.
  5. /Path/To/optool install -c reexport -p /usr/lib/libSystem.B.dylib -t /Path/To/Game.app/Contents/Frameworks/UnityFixer.dylib


from Hacker News https://ift.tt/2OaJS1z

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.