Fix boot panic on macOS 26 (Tahoe) with AMD GPUs - #127
Conversation
With an AMD GPU present, WhateverGreen panicked the kernel while it was enumerating PCI and USB devices. Four separate defects were involved, each of which only became reachable once the previous one was fixed. Corrupt trampoline. Lilu copies the prologue bytes it displaces into a trampoline verbatim, so an instruction with a relative operand inside that range resolves against the trampoline once moved. On macOS 26, IORegistryEntry::getProperty(const char *) has 13 bytes of movable prologue before a rel32 call into OSSymbol::withCString, so the absolute route added in ed5e710 swallowed that call and retargeted it at unrelated memory. The route now picks a jump type the prologue can survive, falling back to routeMultipleShort, which refuses to patch rather than widening the jump, and skipping the route entirely when neither variant is safe. setProperty (exactly 14 movable bytes) and cs_validate_page (16) were audited the same way and are unaffected. Wrapper re-entrancy. Even with a correct trampoline, wrapGetProperty calls getParentEntry on every dictionary-valued lookup, re-entering the registry while a walk of that same registry is in progress. It can only ever do useful work when CFG/PP/CAIL overrides were injected, so it is no longer routed unless one is present. -radnoprop disables both property routes. Calls through unresolved trampolines. Making getProperty conditional left orgGetProperty null for wrapSetProperty, which called through it whenever a model property was set. updateConnectorsInfo had the same defect via orgGetAtomObjectTableForType, a symbol that does not exist on macOS 26 at all. Both are now checked. Unbounded framebuffer write. The back-copy and zero-fill sized their write from the console vinfo but wrote into the framebuffer's VRAM mapping, with the guard above them comparing against the display mode rather than the mapping. The write is now clamped to the mapped length. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 32c8815)
|
Does this issue need to be added to Acidanthera's bugtracker here? |
|
Hi guys, I would just like to confirm this PR succeessfully helped me get up and running on Tahoe 26.6.1 (25G76) with an RX5700XT, using Thanks for the PR @olwimo! |
|
The correct way to use WEG on macOS 26 is Fixing other modes on macOS 26 is unlikely necessary in the first place, but I might consider accepting some of the changes if they make sense. Claude hallucinations presented in this patch make little sense, because they give no proof the assumption made is actually correct. Please start with posting a panic log with keepsyms=1. Let's iterate from there. |
|
@vit9696 yes, this pr is Claude-ed, and certainly shouldn't be merged without proper (human) evaluation. The thing is, I have a cand.scient. in computer science, but I also have a heavier workload than ever, because everything is moving so fast with ai assistance, and two sons who need me as a parent and they tell me they can't make do with a robot-dad yet - so I don't have time to follow through on this hobby project, no matter how much I'd like to. |
|
So...: With WEG disabled and agdpmod boot-arg removed it boots fine - but after sleep it gives this kp: With WEG built from this PR and boot-args agdpmod=pikera and unfairgva=1 it boots, it sleeps and it wakes. I hope that's useful in some way. |
|
Sorry for a slow reply, but it took me some time to get here. Let me go step by step on this PR:
To sum it up, for this problem on the following is true:
The way the problem is described makes no sense to me:
Nevertheless the garbage, the suggested change ignores the fact that CFG, / PP, / CAIL, overrides may not be visible at a time routing happens. This exact reason is why getProperty patch is unconditional, and this is why the suggested change is wrong.
DetailsOSObject *RAD::wrapGetProperty(IORegistryEntry *that, const char *aKey) {
auto obj = FunctionCast(wrapGetProperty, callbackRAD->orgGetProperty)(that, aKey);
auto props = OSDynamicCast(OSDictionary, obj);
if (props && aKey) {
const char *prefix {nullptr};
if (aKey[0] == 'a') {
if (!strcmp(aKey, "aty_config"))
prefix = "CFG,";
else if (!strcmp(aKey, "aty_properties"))
prefix = "PP,";
} else if (aKey[0] == 'c' && !strcmp(aKey, "cail_properties")) {
prefix = "CAIL,";
}
if (prefix) {
auto provider = OSDynamicCast(IOService, that->getParentEntry(gIOServicePlane));
if (provider) {
DBGLOG("rad", "GetProperty discovered property merge request for %s", aKey);
auto rawProps = props->copyCollection();
if (rawProps) {
auto newProps = OSDynamicCast(OSDictionary, rawProps);
if (newProps) {
callbackRAD->mergeProperties(newProps, prefix, provider);
that->setProperty(aKey, newProps);
obj = newProps;
}
rawProps->release();
}
}
}
}
return obj;
}
I do not think any of that makes sense besides the fact that Claude broke
This change looks suspicious. I suspect this in fact could be the main reason it broke. However, more understanding is needed before any change is made. Basically your patch says "let's not write more than is allocated". This is ok, but we write to a framebuffer, which must be allocated there. If it is not allocated there, well, we are doomed and have a memory corruption before that code even runs. Could you maybe trigger a panic when mapped memory is less than framebuffer memory and perhaps print:
At least this way we can compare numbers. The original code is a bit weird, and I need to remember why it is written this way in the first place. TL;DR
|
|
I'll get to it asap, I haven't done systems programming at all since school, and that was only on a minix-like for mips(I think?) arch, so... not really anything relevant. Safe to say I'll still have to rely quite a bit on claude, but I will spend some real time looking at the code it writes and questioning the reasoning this time. Anyway, just one correction: I do have Lilu 1.7.2 installed and running: % grep -A1 'CFBundleShortVersionString' /Volumes/EFI/EFI/OC/Kexts/Lilu.kext/Contents/Info.plist % diff Lilu.kext/Contents/MacOS/Lilu /Volumes/EFI/EFI/OC/Kexts/Lilu.kext/Contents/MacOS/Lilu (Downloaded 1.7.2 fresh off the github release page, diff returned empty) ... and the decisive test: ... so, yeah. But I'll try to revert/rewrite the changes and test them, as well as I can and according to your comment, in a few moments. |
|
Ok, tl;dr: I'm an idiot, these patches do nothing for me, I'd close this at once if it wasn't for @PsychoTea 's report. Slightly longer (and embarrassing) story: Claude immediately ceded that reverting on 1 and 3 should do nothing; and it didn't. Neither did reverting most of 2 - or all of it and reverting 4 as well, running a functional copy of master with just a few diagnostics helpers added. At this point Claude noticed that I had upgraded from 26.6.1 to 26.6.2 since I last observed the boot failure, and was trying to talk me into re-installing that version to investigate further, and I was quite frankly not happy about that proposition. But then it clicked: oh, yeah, but did I actually run the weg master when I couldn't boot? 'Cause it was from a different repo, the acidanthera, not my recent fork. And yeah, cd to that repo and: % git status ... I'm sorry for wasting your time. I blame senile dementia, and will seek out a doctor for a proper treatment asap - or maybe just ask Claude for his professional opinion as a medical advisor. @PsychoTea are you sure these patches work better than master for you?... because if they do, that's great, but completely accidental. |
|
Hi @olwimo Put simply, it wasn’t booting with 1.7.0 from the releases, and worked after building and installing this PR. It’s very possible there was some other small change I made when installing this PR (ie. changing a boot arg), meaning my test may not have fully isolated this build. Taking a look at the commit history there is also ed5e710 which was not made available in releases. It’s possible this change alone fixed whatever issue I was having. If it would be beneficial, I can go back and test 1.7.0, latest master, and this PR to try and isolate things. |

Problem
On macOS 26 (Tahoe), WhateverGreen panics the kernel while it is enumerating PCI and USB devices whenever an AMD GPU is present. The panic lands before the panic log can reach disk, so it leaves nothing behind.
Four separate defects turned out to be involved. Each only became reachable once the previous one was fixed, which is why this is a single PR rather than four.
1. Corrupt trampoline on
IORegistryEntry::getPropertyKernelPatcher::createTrampolinecopies displaced prologue bytes verbatim — the comment there reads// Copy the prologue, assuming it is PIC. An instruction carrying a relative operand inside that range therefore resolves against the trampoline once moved.On Tahoe,
IORegistryEntry::getProperty(const char *)has only 13 bytes of movable prologue:Reaching the 14 bytes an absolute jump needs swallows that
callq. #122 switched this route torouteMultipleLong, which forces an absolute route, so the trampoline's call toOSSymbol::withCStringlands on unrelated memory and the kernel dies on the first property lookup.The route now selects a jump type the prologue can actually survive:
routeMultipleLongwhen a 14-byte displacement is clean, otherwiserouteMultipleShort— which refuses to patch when the callback is out of reach instead of quietly widening the jump — and skipping the route entirely when neither is safe.Every route WhateverGreen makes was audited the same way against the real Tahoe binaries, using Lilu's own hde64 decoder.
getPropertyis the only hazard;setProperty(exactly 14 movable bytes) andcs_validate_page(16) are unaffected.2. Re-entrancy in the
getPropertywrapperA valid trampoline was still fatal.
wrapGetPropertycallsgetParentEntryon every dictionary-valued lookup, re-entering the registry while a walk of that same registry is in progress — including the walk Lilu performs to buildDeviceInfo. On a machine with a long PCI device list it dies partway through.The wrapper can only ever do useful work when
CFG,/PP,/CAIL,overrides were injected, so it is no longer routed unless one is actually present.-radnopropdisables both property routes outright.3. Calls through unresolved trampolines
Making
getPropertyconditional leftorgGetPropertynull, andwrapSetPropertycalled straight through it whenever amodelproperty was set — whichAMDRadeonX6000does when it loads.updateConnectorsInfohas the same defect viaorgGetAtomObjectTableForType. That symbol, along withAtiAtomBiosDceInterface::getConnectorsInfo,AMDLegacyController::start,AppleGraphicsDevicePolicy::start,_dce_driver_set_backlightand_dce_panel_cntl_hw_init, does not exist on macOS 26 at all. Both call sites are now checked.4. Unbounded framebuffer write
wrapFramebufferInitsized its back-copy and zero-fill from the consolevinfobut wrote into the framebuffer's VRAM mapping:The guard above it compares against the display mode's pixel information, which is not the same thing as the mapping. The write is now clamped to
IOMemoryMap::getLength().Testing
Built for Release and Debug. Verified on a Radeon Pro W5700 (Navi 10,
1002:7312) running macOS 26.6 (25G72), booting from an unbootable starting state through to a full desktop on stock settings (agdpmod=pikera unfairgva=1), with each fix bisected by boot argument along the way.Only tested on that one machine and on Navi. The Intel and NVIDIA submodules were not exercised, though nothing here is specific to AMD except the gating in point 2.
Note for maintainers
This includes the
MODULE_VERSIONbump to 1.7.2 and its Changelog entries, since the Changelog text refers to the version. Happy to drop both commits' version handling if you would rather bump separately, as the history suggests you normally do.