diff --git a/Changelog.md b/Changelog.md index f1b15dcb..3c047268 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,11 @@ WhateverGreen Changelog ======================= +#### v1.7.2 +- Fixed a boot panic during PCI/USB device enumeration on macOS 26 (Tahoe) caused by an unrelocatable IOReg getProperty prologue +- Stopped routing IOReg getProperty unless CFG/PP/CAIL property overrides are actually present, added `-radnoprop` to disable the IOReg property routes outright +- Fixed calls through unresolved trampolines in the setProperty wrapper and in connector autofix, which panicked on macOS 26 where getAtomObjectTableForType no longer exists +- Bounded the framebuffer back-copy and zero-fill by the mapped VRAM length instead of the console geometry + #### v1.7.1 - Added support for routing IOReg getProperty in other kexts diff --git a/WhateverGreen.xcodeproj/project.pbxproj b/WhateverGreen.xcodeproj/project.pbxproj index 10cda549..7d98bb2d 100644 --- a/WhateverGreen.xcodeproj/project.pbxproj +++ b/WhateverGreen.xcodeproj/project.pbxproj @@ -650,7 +650,7 @@ MODULE_NAME = as.vit9696.WhateverGreen; MODULE_START = "$(PRODUCT_NAME)_kern_start"; MODULE_STOP = "$(PRODUCT_NAME)_kern_stop"; - MODULE_VERSION = 1.7.1; + MODULE_VERSION = 1.7.2; OTHER_CFLAGS = ( "-mmmx", "-msse", @@ -700,7 +700,7 @@ MODULE_NAME = as.vit9696.WhateverGreen; MODULE_START = "$(PRODUCT_NAME)_kern_start"; MODULE_STOP = "$(PRODUCT_NAME)_kern_stop"; - MODULE_VERSION = 1.7.1; + MODULE_VERSION = 1.7.2; OTHER_CFLAGS = ( "-mmmx", "-msse", @@ -841,7 +841,7 @@ MODULE_NAME = as.vit9696.WhateverGreen; MODULE_START = "$(PRODUCT_NAME)_kern_start"; MODULE_STOP = "$(PRODUCT_NAME)_kern_stop"; - MODULE_VERSION = 1.7.1; + MODULE_VERSION = 1.7.2; OTHER_CFLAGS = ( "-mmmx", "-msse", diff --git a/WhateverGreen/kern_rad.cpp b/WhateverGreen/kern_rad.cpp index 3e8d9ea3..4a31e1f1 100644 --- a/WhateverGreen/kern_rad.cpp +++ b/WhateverGreen/kern_rad.cpp @@ -6,6 +6,7 @@ // #include +#include #include #include #include @@ -118,6 +119,11 @@ void RAD::init(bool enableNavi10Bkl) { // Fix codec PID to be spoofed PID if requested forceCodecInfo = checkKernelArgument("-radcodec"); + // Escape hatch for the kernel-wide IORegistry property routes, which are the most invasive + // thing this submodule does and the first thing worth ruling out when the kernel dies while + // enumerating devices. + disablePropertyRoutes = checkKernelArgument("-radnoprop"); + // To support overriding connectors and -radvesa mode we need to patch AMDSupport. lilu.onKextLoadForce(&kextRadeonSupport); // Mojave dropped legacy GPU support (5xxx and 6xxx). @@ -145,14 +151,49 @@ void RAD::deinit() { } +bool RAD::hasPropertyMergeOverrides(IORegistryEntry *device) { + // Mirrors the prefixes mergeProperties looks for. Anything else cannot reach the merge. + static const char *prefixes[] { "CFG,", "PP,", "CAIL," }; + + auto dict = device->getPropertyTable(); + if (!dict) + return false; + + auto iterator = OSCollectionIterator::withCollection(dict); + if (!iterator) + return false; + + bool found = false; + OSSymbol *propname; + while (!found && (propname = OSDynamicCast(OSSymbol, iterator->getNextObject())) != nullptr) { + auto name = propname->getCStringNoCopy(); + if (!name) continue; + for (size_t i = 0; i < arrsize(prefixes); i++) { + auto len = strlen(prefixes[i]); + if (propname->getLength() > len && !strncmp(name, prefixes[i], len)) { + DBGLOG("rad", "found property merge override %s", name); + found = true; + break; + } + } + } + + iterator->release(); + return found; +} + void RAD::processKernel(KernelPatcher &patcher, DeviceInfo *info) { bool hasAMD = false; + bool needsPropertyMerge = false; for (size_t i = 0; i < info->videoExternal.size(); i++) { if (info->videoExternal[i].vendor == WIOKit::VendorID::ATIAMD) { if (!hasAMD) { hasAMD = true; } + if (!needsPropertyMerge && hasPropertyMergeOverrides(info->videoExternal[i].video)) + needsPropertyMerge = true; + if (info->videoExternal[i].video->getProperty("enable-gva-support")) enableGvaSupport = true; @@ -170,15 +211,26 @@ void RAD::processKernel(KernelPatcher &patcher, DeviceInfo *info) { if (PE_parse_boot_argn("radgva", &gva, sizeof(gva))) enableGvaSupport = gva != 0; - KernelPatcher::RouteRequest requests[] { - KernelPatcher::RouteRequest("__ZN15IORegistryEntry11setPropertyEPKcPvj", wrapSetProperty, orgSetProperty), - KernelPatcher::RouteRequest("__ZNK15IORegistryEntry11getPropertyEPKc", wrapGetProperty, orgGetProperty), - }; - + // getProperty is the hottest function in IOKit: the kernel calls it for every property of + // every device it enumerates, and the wrapper reaches back into the registry through + // getParentEntry while that same walk is in progress. It can only ever do useful work when + // CFG/PP/CAIL overrides were actually injected, so leave it alone when nothing needs merging. + bool routeGetProperty = needsPropertyMerge && !disablePropertyRoutes; + if (hasAMD && !routeGetProperty) + DBGLOG("rad", "skipping getProperty route, no property merge overrides present"); + + KernelPatcher::RouteRequest setPropertyRequest {"__ZN15IORegistryEntry11setPropertyEPKcPvj", wrapSetProperty, orgSetProperty}; + KernelPatcher::RouteRequest getPropertyRequest {"__ZNK15IORegistryEntry11getPropertyEPKc", wrapGetProperty, orgGetProperty}; + if (getKernelVersion() >= KernelVersion::Catalina) { - patcher.routeMultipleLong(KernelPatcher::KernelID, requests, arrsize(requests)); - } else { - patcher.routeMultiple(KernelPatcher::KernelID, requests); + // Kernel-wide routes: let each one pick a jump type whose prologue relocation is safe. + if (!disablePropertyRoutes) + routeKernelFunctionSafely(patcher, setPropertyRequest); + if (routeGetProperty) + routeKernelFunctionSafely(patcher, getPropertyRequest); + } else if (!disablePropertyRoutes) { + KernelPatcher::RouteRequest requests[] { setPropertyRequest, getPropertyRequest }; + patcher.routeMultiple(KernelPatcher::KernelID, requests, routeGetProperty ? 2 : 1); } if (useCustomAgdpDecision && info->firmwareVendor == DeviceInfo::FirmwareVendor::Apple) @@ -194,6 +246,73 @@ void RAD::processKernel(KernelPatcher &patcher, DeviceInfo *info) { } } +bool RAD::isPrologueRelocatable(mach_vm_address_t addr, size_t min) { + size_t total = 0; + + while (total < min) { + Disassembler::hde_t hs {}; + auto len = Disassembler::hdeDisasm(addr + total, &hs); + + if (len == 0 || (hs.flags & F_ERROR)) { + DBGLOG("rad", "prologue decoding failed at offset %lu", total); + return false; + } + + // Relative branches and RIP-relative operands are encoded against the address of the + // instruction itself. Lilu copies the prologue into the trampoline byte for byte, so + // once moved they resolve against the trampoline and point at unrelated memory. + if (hs.flags & F_RELATIVE) { + DBGLOG("rad", "prologue has a relative operand at offset %lu", total); + return false; + } + + total += len; + } + + return true; +} + +bool RAD::routeKernelFunctionSafely(KernelPatcher &patcher, KernelPatcher::RouteRequest &request) { + // Mirrors the jump sizes in KernelPatcher, which are private to Lilu. The number of + // prologue bytes Lilu relocates is the size of the jump it writes, rounded up to an + // instruction boundary. + static constexpr size_t SmallJump {1 + sizeof(int32_t)}; + static constexpr size_t LongJump {6 + sizeof(uintptr_t)}; + + auto from = patcher.solveSymbol(KernelPatcher::KernelID, request.symbol); + if (!from) { + SYSLOG("rad", "failed to solve %s, err %d", request.symbol, patcher.getError()); + patcher.clearError(); + return false; + } + + // An absolute jump is what Lilu needs whenever the callback is out of relative reach, and + // it is also the variant that relocates the most prologue. Take it when the prologue can + // survive being moved, since it is the only variant that works for a distant callback. + if (isPrologueRelocatable(from, LongJump)) { + if (patcher.routeMultipleLong(KernelPatcher::KernelID, &request, 1)) + return true; + + SYSLOG("rad", "failed to long route %s, err %d", request.symbol, patcher.getError()); + patcher.clearError(); + return false; + } + + // Otherwise only a relative jump is safe, because it relocates far less. routeMultipleShort + // refuses to patch at all when the callback is out of reach instead of quietly widening the + // jump, which is what makes it usable as a fallback here. + if (isPrologueRelocatable(from, SmallJump) && + patcher.routeMultipleShort(KernelPatcher::KernelID, &request, 1)) + return true; + + // Losing the property merge costs injected CFG/PP/CAIL overrides on this GPU. Routing anyway + // would hand the AMD drivers a broken trampoline on a path the kernel takes for every device + // it enumerates, which panics long before anything can report why. + SYSLOG("rad", "skipping %s, its prologue cannot be relocated safely", request.symbol); + patcher.clearError(); + return false; +} + void RAD::updatePwmMaxBrightnessFromInternalDisplay() { OSDictionary * matching = IOService::serviceMatching("AppleBacklightDisplay"); if (matching == nullptr) { @@ -713,7 +832,12 @@ void RAD::updateConnectorsInfo(void *atomutils, t_getAtomObjectTableForType gett DBGLOG("rad", "getConnectorsInfo conoverrides have invalid type"); } } else { - if (atomutils) { + // gettable is resolved separately from the route that leads here and is absent on some + // releases, macOS 26 among them, so it cannot be assumed to be present. + if (atomutils && !gettable) + DBGLOG("rad", "getConnectorsInfo cannot autofix connectors without getAtomObjectTableForType"); + + if (atomutils && gettable) { DBGLOG("rad", "getConnectorsInfo attempting to autofix connectors"); uint8_t sHeader = 0, displayPathNum = 0, connectorObjectNum = 0; auto baseAddr = static_cast(gettable(atomutils, AtomObjectTableType::Common, &sHeader)) - sizeof(uint32_t); @@ -1028,7 +1152,12 @@ bool RAD::wrapSetProperty(IORegistryEntry *that, const char *aKey, void *bytes, if (length > 10 && aKey && reinterpret_cast(aKey)[0] == 'edom' && reinterpret_cast(aKey)[2] == 'l') { DBGLOG("rad", "SetProperty caught model %u (%.*s)", length, length, static_cast(bytes)); if (*static_cast(bytes) == ' DMA' || *static_cast(bytes) == ' ITA' || *static_cast(bytes) == 'edaR') { - if (FunctionCast(wrapGetProperty, callbackRAD->orgGetProperty)(that, aKey)) { + // getProperty is only routed when there is something to merge, so orgGetProperty is + // null in the common case. Call the real thing instead of through a dead trampoline. + auto existing = callbackRAD->orgGetProperty ? + FunctionCast(wrapGetProperty, callbackRAD->orgGetProperty)(that, aKey) : + that->getProperty(aKey); + if (existing) { DBGLOG("rad", "SetProperty ignored setting %s to %s", aKey, static_cast(bytes)); return true; } diff --git a/WhateverGreen/kern_rad.hpp b/WhateverGreen/kern_rad.hpp index 8f95f8a0..6c7d3ff8 100644 --- a/WhateverGreen/kern_rad.hpp +++ b/WhateverGreen/kern_rad.hpp @@ -65,6 +65,45 @@ class RAD { */ static RAD *callbackRAD; + /** + * Route a kernel function picking a jump type whose prologue relocation is safe + * + * Lilu copies the prologue bytes it overwrites into a trampoline verbatim, so any + * instruction with a relative operand inside that range silently breaks. How many + * bytes get relocated depends on the jump type Lilu ends up choosing, so pick the + * request's jump type from what the target's prologue actually tolerates. + * + * @param patcher KernelPatcher instance + * @param request route request, solved and routed in place + * + * @return true if the function was routed + */ + static bool routeKernelFunctionSafely(KernelPatcher &patcher, KernelPatcher::RouteRequest &request); + + /** + * Check that the first `min` bytes of a function can be moved to another address + * + * @param addr function address + * @param min minimum number of bytes that will be relocated + * + * @return true if no instruction in the relocated range has a relative operand + */ + static bool isPrologueRelocatable(mach_vm_address_t addr, size_t min); + + /** + * Check whether a GPU carries properties the getProperty wrapper could merge + * + * @param device PCI device to inspect + * + * @return true if any CFG/PP/CAIL prefixed property is present + */ + static bool hasPropertyMergeOverrides(IORegistryEntry *device); + + /** + * Disable the kernel-wide IORegistry property routes entirely (-radnoprop) + */ + bool disablePropertyRoutes {false}; + /** * Original set property function */ diff --git a/WhateverGreen/kern_weg.cpp b/WhateverGreen/kern_weg.cpp index bd94fe62..d4b9522f 100644 --- a/WhateverGreen/kern_weg.cpp +++ b/WhateverGreen/kern_weg.cpp @@ -721,18 +721,29 @@ void WEG::wrapFramebufferInit(IOFramebuffer *fb) { if (!backCopy) *callbackWEG->gIOFBVerboseBootPtr = verboseBoot; // Finish the framebuffer initialisation by filling with black or copying the image back. - if (FramebufferViewer::getVramMap(fb)) { + if (auto vramMap = FramebufferViewer::getVramMap(fb)) { auto src = reinterpret_cast(callbackWEG->consoleBuffer); - auto dst = reinterpret_cast(FramebufferViewer::getVramMap(fb)->getVirtualAddress()); + auto dst = reinterpret_cast(vramMap->getVirtualAddress()); + + // The size comes from the console vinfo while the mapping belongs to the framebuffer. The + // display mode check above only compares against the mode's pixel information, which is not + // the same thing as the mapping, so bound the write by what is actually mapped. + size_t size = static_cast(info.v_rowbytes) * info.v_height; + size_t mapped = vramMap->getLength(); + if (size > mapped) { + DBGLOG("weg", "console image is %lu bytes but only %lu are mapped, clamping", size, mapped); + size = mapped; + } + if (backCopy) { DBGLOG("weg", "attempting to copy..."); // Here you can actually draw at your will, but looks like only on Intel. // On AMD you technically can draw too, but it happens for a very short while, and is not worth it. - lilu_os_memcpy(dst, src, info.v_rowbytes * info.v_height); + lilu_os_memcpy(dst, src, size); } else if (zeroFill) { // On AMD we do a zero-fill to ensure no visual glitches. DBGLOG("weg", "doing zero-fill..."); - memset(dst, 0, info.v_rowbytes * info.v_height); + memset(dst, 0, size); } } }