Skip to content

Fix boot panic on macOS 26 (Tahoe) with AMD GPUs - #127

Open
olwimo wants to merge 1 commit into
acidanthera:masterfrom
olwimo:tahoe-boot-fix
Open

Fix boot panic on macOS 26 (Tahoe) with AMD GPUs#127
olwimo wants to merge 1 commit into
acidanthera:masterfrom
olwimo:tahoe-boot-fix

Conversation

@olwimo

@olwimo olwimo commented Aug 4, 2026

Copy link
Copy Markdown

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::getProperty

KernelPatcher::createTrampoline copies 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:

0:  55           pushq %rbp
1:  4889e5       movq  %rsp, %rbp
4:  4156         pushq %r14
6:  53           pushq %rbx
7:  4889fb       movq  %rdi, %rbx
10: 4889f7       movq  %rsi, %rdi
13: e87edbfbff   callq OSSymbol::withCString    <-- rel32

Reaching the 14 bytes an absolute jump needs swallows that callq. #122 switched this route to routeMultipleLong, which forces an absolute route, so the trampoline's call to OSSymbol::withCString lands on unrelated memory and the kernel dies on the first property lookup.

The route now selects a jump type the prologue can actually survive: routeMultipleLong when a 14-byte displacement is clean, otherwise routeMultipleShort — 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. getProperty is the only hazard; setProperty (exactly 14 movable bytes) and cs_validate_page (16) are unaffected.

2. Re-entrancy in the getProperty wrapper

A valid trampoline was still fatal. wrapGetProperty calls getParentEntry on every dictionary-valued lookup, re-entering the registry while a walk of that same registry is in progress — including the walk Lilu performs to build DeviceInfo. 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. -radnoprop disables both property routes outright.

3. Calls through unresolved trampolines

Making getProperty conditional left orgGetProperty null, and wrapSetProperty called straight through it whenever a model property was set — which AMDRadeonX6000 does when it loads.

updateConnectorsInfo has the same defect via orgGetAtomObjectTableForType. That symbol, along with AtiAtomBiosDceInterface::getConnectorsInfo, AMDLegacyController::start, AppleGraphicsDevicePolicy::start, _dce_driver_set_backlight and _dce_panel_cntl_hw_init, does not exist on macOS 26 at all. Both call sites are now checked.

4. Unbounded framebuffer write

wrapFramebufferInit sized its back-copy and zero-fill from the console vinfo but wrote into the framebuffer's VRAM mapping:

memset(dst, 0, info.v_rowbytes * info.v_height);

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_VERSION bump 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.

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)
@TonyC5

TonyC5 commented Aug 7, 2026

Copy link
Copy Markdown

Does this issue need to be added to Acidanthera's bugtracker here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants