Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 1.7.0 — 2026-08-07

- Added a Big Sur-compatible direct MSI-to-MSI-X reallocation path while keeping a single Kext loadable on macOS 11–15.
- Moved the MSI-X request to early PCI matching on every supported release so Recovery and second-stage Installer polled commands are covered.
- Retained the macOS 14–15 interrupt-source route as a fallback.
- Hardware-validated macOS 11–14 Recovery and a complete macOS 15.6.1 installation.
- Verified PCIe x4 / 8.0 GT/s, TRIM support, S.M.A.R.T. status, and 2766.1/3005.9 MB/s measured sequential write/read performance on the tested PC711.

## 1.2.0 — 2026-08-07

- Added early, PC711-only MSI-X allocation for Darwin 20–22 without taking ownership away from Apple `IONVMeFamily`.
Expand Down
4 changes: 2 additions & 2 deletions Driver/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>KEXT</string>
<key>CFBundleShortVersionString</key>
<string>1.2.0</string>
<string>1.7.0</string>
<key>CFBundleVersion</key>
<string>1.2.0</string>
<string>1.7.0</string>
<key>IOKitPersonalities</key>
<dict>
<key>PC711EarlyMSIX</key>
Expand Down
219 changes: 211 additions & 8 deletions Driver/PC711Probe.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

#include <IOKit/IOService.h>
#include <IOKit/IOFilterInterruptEventSource.h>
#include <IOKit/IOPlatformExpert.h>
#include <IOKit/pci/IOPCIDevice.h>
#include <IOKit/pci/IOPCIFamilyDefinitions.h>
#include <libkern/c++/OSArray.h>
#include <libkern/c++/OSSymbol.h>

#include <Headers/kern_api.hpp>
#include <Headers/kern_patcher.hpp>
Expand All @@ -20,10 +23,62 @@ constexpr uint32_t kInterruptTypeMSIX {0x00020000U};
constexpr size_t kConfigureInterruptsVtableSlot {0x960 / sizeof(uintptr_t)};
constexpr size_t kControllerFlagsOffset {0x191};
constexpr uint8_t kLegacyMSIXPathFlag {0x10};
constexpr uint8_t kBigSurMSIXMode {0x01};

constexpr const char *kAllocateDeviceInterruptsSymbol {
"__ZN32IOPCIMessagedInterruptController24allocateDeviceInterruptsEP9IOServicejjPyPj"
};
constexpr const char *kDeallocateDeviceInterruptsSymbol {
"__ZN32IOPCIMessagedInterruptController26deallocateDeviceInterruptsEP9IOService"
};

using ConfigureInterrupts = IOReturn (*)(IOPCIDevice *device,
uint32_t interruptType, uint32_t numRequired, uint32_t numRequested,
IOOptionBits options);
using AllocateDeviceInterrupts = IOReturn (*)(void *controller,
IOService *device, uint32_t numVectors, uint32_t msiCapability,
uint64_t *msiAddress, uint32_t *msiData);
using DeallocateDeviceInterrupts = IOReturn (*)(void *controller,
IOService *device);

AllocateDeviceInterrupts bigSurAllocateDeviceInterrupts {nullptr};
DeallocateDeviceInterrupts bigSurDeallocateDeviceInterrupts {nullptr};

// Big Sur exports the five-argument allocator. Monterey and newer changed
// its C++ signature, so weak linkage keeps one binary loadable across 11-15.
extern "C" IOReturn directBigSurAllocateDeviceInterrupts(void *controller,
IOService *device, uint32_t numVectors, uint32_t msiCapability,
uint64_t *msiAddress, uint32_t *msiData)
__asm("__ZN32IOPCIMessagedInterruptController24allocateDeviceInterruptsEP9IOServicejjPyPj")
__attribute__((weak_import));
extern "C" IOReturn directBigSurDeallocateDeviceInterrupts(void *controller,
IOService *device)
__asm("__ZN32IOPCIMessagedInterruptController26deallocateDeviceInterruptsEP9IOService")
__attribute__((weak_import));

struct BigSurPCIMSIState {
uint8_t reserved0[28];
uint16_t msiCapability;
uint16_t msiControl;
uint16_t msiPhysVectorCount;
uint16_t msiVectorCount;
uint8_t msiMode;
uint8_t msiEnable;
uint8_t reserved1[2];
uint64_t msiTable;
uint64_t msiPBA;
void *msiVectors;
};

static_assert(offsetof(BigSurPCIMSIState, msiCapability) == 28,
"unexpected Big Sur PCI MSI state layout");
static_assert(offsetof(BigSurPCIMSIState, msiTable) == 40,
"unexpected Big Sur PCI MSI table layout");

class PC711PCIDeviceAccess : public IOPCIDevice {
public:
void *compatReserved() { return reserved; }
};

bool isPC711Device(IOPCIDevice *pci) {
if (!pci)
Expand All @@ -50,11 +105,119 @@ IOReturn configurePC711MSIX(IOPCIDevice *pci) {
kIOReturnUnsupported;
}

IOReturn reallocateBigSurPC711MSIX(IOPCIDevice *pci) {
if (pci && pci->getProperty("PC711CompatBigSurMSIXReallocated"))
return kIOReturnSuccess;

auto allocate = bigSurAllocateDeviceInterrupts ?
bigSurAllocateDeviceInterrupts : directBigSurAllocateDeviceInterrupts;
auto deallocate = bigSurDeallocateDeviceInterrupts ?
bigSurDeallocateDeviceInterrupts : directBigSurDeallocateDeviceInterrupts;
if (!pci || !allocate || !deallocate)
return kIOReturnNotReady;

IOByteCount msixCapability {0};
pci->extendedFindPCICapability(kIOPCIMSIXCapability, &msixCapability);
if (!msixCapability)
return kIOReturnUnsupported;

auto controllers = OSDynamicCast(OSArray,
pci->getProperty(gIOInterruptControllersKey));
auto specifiers = OSDynamicCast(OSArray,
pci->getProperty(gIOInterruptSpecifiersKey));
if (!controllers || !specifiers ||
controllers->getCount() != specifiers->getCount())
return kIOReturnNotReady;

void *messagedController {nullptr};
const OSSymbol *messagedName {nullptr};
for (unsigned int index = 0; index < controllers->getCount(); index++) {
auto name = OSDynamicCast(OSSymbol, controllers->getObject(index));
if (!name)
continue;
auto controller = IOService::getPlatform()->lookUpInterruptController(name);
if (controller && controller->metaCast("IOPCIMessagedInterruptController")) {
messagedController = controller;
messagedName = name;
break;
}
}
if (!messagedController || !messagedName)
return kIOReturnNotReady;

auto state = reinterpret_cast<BigSurPCIMSIState *>(
reinterpret_cast<PC711PCIDeviceAccess *>(pci)->compatReserved());
if (!state || !state->msiCapability)
return kIOReturnNotReady;

const uint16_t oldCapability = state->msiCapability;
const uint8_t oldMode = state->msiMode;
const auto deallocateResult = deallocate(messagedController, pci);
if (deallocateResult != kIOReturnSuccess)
return deallocateResult;

auto retainedControllers = OSArray::withCapacity(controllers->getCount());
auto retainedSpecifiers = OSArray::withCapacity(specifiers->getCount());
if (!retainedControllers || !retainedSpecifiers) {
OSSafeReleaseNULL(retainedControllers);
OSSafeReleaseNULL(retainedSpecifiers);
return kIOReturnNoMemory;
}

for (unsigned int index = 0; index < controllers->getCount(); index++) {
auto controllerName = controllers->getObject(index);
auto specifier = specifiers->getObject(index);
if (!controllerName || !specifier || controllerName->isEqualTo(messagedName))
continue;
retainedControllers->setObject(controllerName);
retainedSpecifiers->setObject(specifier);
}
pci->setProperty(gIOInterruptControllersKey, retainedControllers);
pci->setProperty(gIOInterruptSpecifiersKey, retainedSpecifiers);
retainedControllers->release();
retainedSpecifiers->release();

state->msiCapability = static_cast<uint16_t>(msixCapability);
state->msiControl = 0;
state->msiPhysVectorCount = 0;
state->msiVectorCount = 0;
state->msiMode = kBigSurMSIXMode;
state->msiEnable = 0;
state->msiTable = 0;
state->msiPBA = 0;
state->msiVectors = nullptr;

const auto allocateResult = allocate(
messagedController, pci, 0, static_cast<uint32_t>(msixCapability),
nullptr, nullptr);
pci->setProperty("PC711CompatBigSurMSIXReallocationResult",
static_cast<unsigned long long>(static_cast<uint32_t>(allocateResult)), 32);
if (allocateResult == kIOReturnSuccess) {
pci->setProperty("PC711CompatBigSurMSIXReallocated", true);
return allocateResult;
}

// Restore the original MSI allocation if MSI-X allocation failed.
state->msiCapability = oldCapability;
state->msiControl = 0;
state->msiPhysVectorCount = 0;
state->msiVectorCount = 0;
state->msiMode = oldMode;
state->msiEnable = 0;
state->msiTable = 0;
state->msiPBA = 0;
state->msiVectors = nullptr;
allocate(messagedController, pci, 0, oldCapability,
nullptr, nullptr);
return allocateResult;
}

} // namespace

// Darwin 20-22 may have resolved a different PCI interrupt allocation before
// IONVMeFamily creates its event source. Request MSI-X while the PC711 PCI nub
// is still being probed, then decline attachment so Apple's driver takes over.
// Recovery and installer environments may issue polled NVMe commands before
// IONVMeFamily creates its ordinary event source. Request MSI-X while the
// PC711 PCI nub is still being probed, then decline attachment so Apple's
// driver takes over on every supported macOS release.
class PC711EarlyMSIX : public IOService {
OSDeclareDefaultStructors(PC711EarlyMSIX)

Expand All @@ -65,14 +228,12 @@ class PC711EarlyMSIX : public IOService {
OSDefineMetaClassAndStructors(PC711EarlyMSIX, IOService)

IOService *PC711EarlyMSIX::probe(IOService *provider, SInt32 *) {
if (getKernelVersion() > KernelVersion::Ventura)
return nullptr;

auto pci = OSDynamicCast(IOPCIDevice, provider);
if (!isPC711Device(pci))
return nullptr;

const auto result = configurePC711MSIX(pci);
const auto result = getKernelVersion() == KernelVersion::BigSur ?
reallocateBigSurPC711MSIX(pci) : configurePC711MSIX(pci);
pci->setProperty("PC711CompatEarlyMSIXRequested", true);
pci->setProperty("PC711CompatEarlyConfigureInterruptsResult",
static_cast<unsigned long long>(static_cast<uint32_t>(result)), 32);
Expand Down Expand Up @@ -110,6 +271,8 @@ class PC711ProbePlugin {
IOService *provider);
static void processKext(void *context, KernelPatcher &patcher, size_t index,
mach_vm_address_t address, size_t size);
static void processPCIKext(void *context, KernelPatcher &patcher, size_t index,
mach_vm_address_t address, size_t size);
static IOFilterInterruptEventSource *wrapCreateDeviceInterrupt(void *controller,
IOInterruptEventAction action, IOFilterInterruptAction filter,
IOService *provider);
Expand All @@ -129,6 +292,19 @@ class PC711ProbePlugin {
{},
KernelPatcher::KextInfo::Unloaded
};

const char *pciKextPath {
"/System/Library/Extensions/IOPCIFamily.kext/Contents/MacOS/IOPCIFamily"
};

KernelPatcher::KextInfo pciKextInfo {
"com.apple.iokit.IOPCIFamily",
&pciKextPath,
1,
{true},
{},
KernelPatcher::KextInfo::Unloaded
};
};

PC711ProbePlugin plugin;
Expand Down Expand Up @@ -162,7 +338,11 @@ IOFilterInterruptEventSource *PC711ProbePlugin::wrapCreateDeviceInterrupt(
filter, provider) : nullptr;
}

const auto result = configurePC711MSIX(pci);
// On Big Sur the IOPCIFamily callback may not have resolved the private MSI
// allocator when the high-score PCI personality probes. Retry here, after
// IOPCIFamily is ready but before IONVMeFamily creates its event source.
const auto result = getKernelVersion() == KernelVersion::BigSur ?
reallocateBigSurPC711MSIX(pci) : configurePC711MSIX(pci);
pci->setProperty("PC711CompatMSIXRequested", true);
pci->setProperty("PC711CompatConfigureInterruptsResult",
static_cast<unsigned long long>(static_cast<uint32_t>(result)), 32);
Expand Down Expand Up @@ -192,6 +372,25 @@ IOFilterInterruptEventSource *PC711ProbePlugin::wrapCreateDeviceInterrupt(
return eventSource;
}

void PC711ProbePlugin::processPCIKext(void *context, KernelPatcher &patcher,
size_t index, mach_vm_address_t, size_t) {
auto instance = static_cast<PC711ProbePlugin *>(context);
if (!instance || index != instance->pciKextInfo.loadIndex ||
getKernelVersion() != KernelVersion::BigSur)
return;

bigSurAllocateDeviceInterrupts = reinterpret_cast<AllocateDeviceInterrupts>(
patcher.solveSymbol(index, kAllocateDeviceInterruptsSymbol));
bigSurDeallocateDeviceInterrupts = reinterpret_cast<DeallocateDeviceInterrupts>(
patcher.solveSymbol(index, kDeallocateDeviceInterruptsSymbol));
if (!bigSurAllocateDeviceInterrupts || !bigSurDeallocateDeviceInterrupts) {
SYSLOG("probe", "failed to resolve Big Sur MSI reallocation functions");
return;
}

SYSLOG("probe", "Big Sur PC711 MSI-X reallocation functions ready");
}

void PC711ProbePlugin::processKext(void *context, KernelPatcher &patcher,
size_t index, mach_vm_address_t address, size_t size) {
auto instance = static_cast<PC711ProbePlugin *>(context);
Expand Down Expand Up @@ -228,6 +427,10 @@ void PC711ProbePlugin::init() {
const auto error = lilu.onKextLoad(&kextInfo, 1, processKext, this);
if (error != LiluAPI::Error::NoError)
SYSLOG("probe", "failed to register IONVMeFamily load callback: %d", error);

const auto pciError = lilu.onKextLoad(&pciKextInfo, 1, processPCIKext, this);
if (pciError != LiluAPI::Error::NoError)
SYSLOG("probe", "failed to register IOPCIFamily load callback: %d", pciError);
}

const char *bootargOff[] {"-pc711poff"};
Expand Down
28 changes: 14 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,43 +15,43 @@ An automatic Lilu compatibility plugin that fixes an `IONVMeFamily` Identify-tim

## Verified result

PC711Probe has passed hardware boot tests on the same PC711 with macOS 13.4.1 and macOS 15.6.1 Recovery. Disk Utility opened, the model and all five existing partitions were enumerated, and the former NVMe command-timeout panic after roughly 75 seconds did not recur. macOS 11.6 still panics and is not currently supported.
PC711Probe 1.7.0 has booted the same physical PC711 across macOS 11–15. macOS 11–14 were verified in Recovery, while macOS 15.6.1 completed a full installation and booted from the PC711. The former Identify/command timeout panic after roughly 75 seconds did not recur.

![PC711 enumerated in macOS 15.6.1 Recovery](docs/images/recovery-success.jpg)
![PC711 running macOS 15.6.1 with TRIM, PCIe link details, and measured disk performance](docs/images/macos15-installed-performance.png)

| Item | Verified value |
|---|---|
| Controller | SK hynix `1C5C:174A`, NVMe class `01:08:02` |
| Model | `SKHynix_HFS512GDE9X084N` (PC711) |
| Firmware | `41010C22` |
| v1.2.0 verified | macOS 13.4.1, build 22F82, Darwin 22.5.0 |
| Previously verified | macOS 15.6.1, build 24G90, Darwin 24.6.0 |
| Booted normally here | macOS 12.5.1 (21G83) and macOS 14.6.1 (23G93) Recovery |
| Currently unsupported | macOS 11.6 (20G165), original NVMe timeout panic remains |
| Recovery boot verified | macOS 11.6 (20G165), 12.5.1 (21G83), 13.4.1 (22F82), 14.6.1 (23G93) |
| Full installation verified | macOS 15.6.1, build 24G90, Darwin 24.6.0 |
| macOS 15 link/status | PCIe 3.0 x4, 8.0 GT/s, TRIM: Yes, S.M.A.R.T.: Verified |
| macOS 15 measured result | 2766.1 MB/s write, 3005.9 MB/s read (Blackmagic Disk Speed Test) |
| Native OS | macOS 26.5.1, build 25F80, Darwin 25.5.0 |
| Boot environment | OpenCore 1.0.8, Lilu 1.7.3 |

## Automatic matching

Version 1.2.0 requires no activation argument. Once enabled in OpenCore, it automatically patches only controllers matching:
Version 1.7.0 requires no activation argument. Once enabled in OpenCore, it automatically patches only controllers matching:

- PCI Vendor/Device: `1C5C:174A`; and
- NVMe class: `01:08:02`.

The PC711 model string is not available until the first Identify succeeds, so the plugin uses its known PCI controller identity before that command. Different capacities and OEM model strings do not affect matching. NVMe controllers with other PCI IDs retain Apple's original behavior.

The declared automatic range is Darwin 20–24 (macOS 11–15). The plugin does not load on Darwin 25/macOS 26. macOS 11 is within the load range but still panics on the tested machine.
The declared automatic range is Darwin 20–24 (macOS 11–15). The plugin does not load on Darwin 25/macOS 26, where the tested PC711 works natively.

## How it works

On macOS 15.6.1, the PC711 controller reaches Ready state (`CSTS=1`), but the first Identify Controller command never returns through the older interrupt completion path and eventually panics.
Without the patch, the PC711 controller reaches Ready state (`CSTS=1`), but Identify or another early NVMe command may never complete through the older interrupt path and eventually panics after roughly 75 seconds.

Comparison of older Apple `IONVMeFamily` builds with macOS 26 showed that the newer OS requests one MSI-X vector before creating the interrupt source and removes an older MSI-X-specific path. For the matched PC711, PC711Probe:

1. requests one MSI-X vector during early PCI matching on macOS 11–13, then declines attachment;
2. leaves Apple `IONVMeFamily` as the actual NVMe driver;
3. requests MSI-X and clears the old interrupt-path selector during interrupt-source creation on macOS 14–15; and
4. leaves Identify, queues, namespaces, and storage I/O to Apple's driver.
1. requests MSI-X during early PCI matching on macOS 11–15, before Recovery or Installer can issue sensitive polled commands;
2. uses Big Sur's original PCI message-interrupt allocator on macOS 11 and `IOPCIDevice::configureInterrupts` on macOS 12–15;
3. keeps the interrupt-source route as a fallback and clears the old selector on macOS 14–15; and
4. declines attachment, leaving Identify, queues, namespaces, and all storage I/O to Apple `IONVMeFamily`.

[Read the concise development process](docs/DEVELOPMENT.en.md)

Expand Down Expand Up @@ -84,7 +84,7 @@ Output: `build/Debug/PC711Probe.kext`

## Current validation boundary

Controller initialization, Identify, namespace discovery, and partition publication are verified in macOS 13/15 Recovery; macOS 12/14 Recovery booted normally here. macOS 11 remains unresolved. Full installation, sustained I/O, TRIM, sleep/wake, other firmware, and other platforms have not completed hardware validation. Keep a rollback EFI and data backup for the first test.
Recovery boot is verified on macOS 11–14. A complete macOS 15.6.1 installation, normal system boot, namespace/partition publication, PCIe 3.0 x4 link, reported TRIM support, S.M.A.R.T. status, and a 2766.1/3005.9 MB/s write/read benchmark are verified on the tested PC711. Sleep/wake on macOS 11–15, long-duration stress, other firmware revisions, and other platforms remain outside the current validation boundary. Keep a rollback EFI and data backup for the first test.

## License

Expand Down
Loading
Loading