Reverse Engineering the MacOS AHCI Kernel Driver

Prologue

Now keep in mind, I’m writing this article now in 2023. It has been 2 years since this whole fiasco, even though the post says “posted in 2021.” That is a lie.

Storytime with Kevin

TWO years ago, I decided to turn my Dell laptop into a Hackintosh. The specific model was the Dell Inspiron 5567 (you can still see the serial number taped to the screen in the image below). However, there were some challenges – especially because I wanted to boot the MacOS image off an optical bay caddy. That’s right! I stuck a 1TB drive into the optical bay for more storage.

The problem with this (of course) is that the optical bay SATA connection was designed for a 3GBps (Gen2) connection, not a 6GBps (Gen3) connection. Aside from Windows, both Linux and MacOS had trouble detecting this drive. This was because I had stuck a Gen3 capable drive into a Gen2 capable link, and the OS simply assumed a Gen3 connection. Except for Windows – it seemed the Intel AHCI drivers were doing something fancy.

On Linux, this was easy to fix: just set the libata.force kernel parameter documented here and you’re good to go. On MacOS, you’re out of luck. Unless you’re crazy enough to reverse-engineer and patch the kernel!

Which I am.

But XNU is open source! Right?

So no. The drivers are not. Peeking at the IORegistryExplorer, AppleACHI is the driver responsible for SATA connections. This corresponded to the AppleAHCIPort.kext KEXT file. A Kernel EXTtension is analogous to the loadable kernel modules in Linux. However, we’re so deep in the kernel that this KEXT is not even an IOKit-derived driver. Instead, it probably is the backend of the IOKit ATA API, documented here.

I did thoroughly search the IOATAFamily.kextandIOAHCIFamily.kext` drivers but got nowhere. I still have no clue what they do. My methodology was to search for the text “6 gigabit” in the string tables because some driver somewhere was reporting that.

There were certainly a lot of hopes and prayers behind this.

Yippee! From this, we can see there are 2 cross-references of this symbol: one is in InitializePort and one is in ScanForDevices.

Two paths diverged in the woods

The segment from InitializePort looked like this:

We can see that the code is simply reading an already-initialized value from this + 0x140 and setting pcVar13 based off of that. Which is ultimately used in IOAHCIPort::SetAHCIProperty. A dead end. Well, what is “this”. It turns out “this” is part of CtlnaAHCIPort (AppleAHCIPort but a renamed version for use in BigSur because of this shenanigans).

Let’s rename this field and continue. In ScanForDevices, we see the following:

We see that the value in uVar9 is compared and a speed is assigned accordingly. Now, uVar9 is a monstrosity. How good are you with pointer arithmetic? Let’s break it down:

1
uint uVar9 = *(uint *)(*(long *)(this->unknown + 0x138) + 0x28)

To read this, employ the spiral rule!.

  1. First interpret this->unknown + 0x138 as long* and dereference
  2. Next, offset that long by 0x28
  3. Interpret as uint* and then dereference again!

You got it! Let’s rename this field and continue. For reference, this is what our struct AHCIPort looks like now. This is a screenshot of the structure editor of Ghidra, a very useful feature!

And full definition in C++:

1
2
3
4
5
6
7
8
9
10
11
struct AHCIPort {
uint8_t[0x138] unknown;
struct some_uint_ptr_struct* some_uint_ptr;
uint possibly_flags;
// Possibly more fields
};
struct some_uint_ptr_struct {
uint8_t[0x28] unknown;
uint gen;
// Possibly more fields
};

Now uVar9 becomes this->some_uint_ptr->gen. Much nicer!

It’s a bird… or a plane… or PxSSTS?

Buckle up and grab a fresh copy of the Intel Serial ATA AHCI Specification v1.3.1. If we just trace through the usages of this gen field in Ghidra, the very first method that comes up is AHCIPort::ReadPxSSTS. Now, in Section 3.3.10 of the AHCI specification, we see:

1
Offset 28h: PxSSTS – Port x Serial ATA Status (SCR0: SStatus)

and if you recall in AHCIPort::ScanForDevices, we read this register and mask it with 0xf0 to obtain the generation. Indeed, this is the Current Interface Speed (SPD) field of this register! Although this isn’t what we want – we want to set the current interface speed, not read it. Reading on, we see that the next register, PxSCTL, contains a field Speed Allowed (SPD) which does just that!

This is quite simple now! The struct some_uint_ptr_struct is actually the HBA registers and our some_uint_ptr field is actually a pointer to the HBA base. Let’s rename our structs:

1
2
3
4
5
6
7
8
9
10
11
struct AHCIPort { /* PlaceHolder Structure */
UInt8 unknown[312];
struct HBARegs * hba_regs_base; /* base address of HBA */
uint possibly_flags; /* masked by 0x0f00_0000 is the generation */
};

struct HBARegs {
char unknown[40];
uint PxSSTS;
uint PxSCTL;
};

and now we just search for all uses of PxSCTL and replace them so that they are OR’d with 0x2 (for Gen2 speeds). It turns out that Apple doesn’t do any sort of link training at all – and they don’t need to – since they assume all their PCBs will be designed to meet the reasonable specs of the drive.

In the end, I only patched AHCIPort::EnablePortOperation and AHCIPort::HandleComReset as those were the remaining functions dealt with SATA hot plugging, which I don’t care about. I hope you enjoyed this journey as much as I had! See you in the next article.