#691

[Bug] iOS 18.6.2 guest (cloudOS 26.1): paravirtual Metal driver fails to load, so Safari and every WKWebView render blank pages

OpenUnownPkmnopened 12d ago

Summary

On an iOS 18.6.2 guest paired with cloudOS 26.1, every page in Safari is blank: white, or just the page's background colour. Networking, loading, layout and JavaScript all work. The problem is compositing.

The chain:

  1. The GPU driver bundle that fw prepare stages from cloudOS 26.1 (AppleParavirtGPUMetalIOGPUFamily.bundle, built for iOS 26.1) fails to dlopen on the iOS 18.6.2 userland. It imports _OBJC_IVAR_$_IOGPUMetalHeap._device, which iOS 18's IOGPU.framework doesn't export.
  2. No process gets a Metal device. backboardd falls back to the CoreAnimation software renderer.
  3. The CA software renderer can't composite 10-bit extended-range IOSurfaces (w30r, b3a8).
  4. WebKit allocates all of its layer backing stores in exactly those formats, because the guest reports an extended-colour (P3) display (HasExtendedColorDisplay = true).
  5. Every web layer is dropped. UIKit content (8-bit BGRA) still draws, so the Safari chrome looks normal.

The iOS 26.6.2 and 27.0.1 guests (cloudOS 26.4) on the same Mac render web pages normally. See Comparison with a working guest.

Environment

HostMacBookPro18,1 (Apple M1 Pro), macOS 15.8.1 (24H32)
VPhone.bundle / Launchpad2.10.1 (bootChain 2.9.5, guestEnvironment 2.10.1)
GuestiPhone17,3_18.6.2_22G100 + cloudOS 26.1-23B85, preset standard, variant jb, network nat
GPU driver installed in guestAppleParavirtGPUMetalIOGPUFamily.bundle from 26.1-23B85: CFBundleVersion 63.2.2, DTPlatformVersion 26.1, MinimumOSVersion 26.1, LC_BUILD_VERSION minos 26.1 sdk 26.1, binary SHA-256 754717559817ae7e31411f04cd59d6ba549aeba71f196025dba22ab891b5e4f3

Steps to reproduce

  1. Create an iOS 18.6.2 (22G100) + cloudOS 26.1 (23B85) VM with the standard preset and boot it.
  2. Open Safari on any site (https://example.com, https://www.google.com, https://www.wikipedia.org).

Expected: the page renders. Actual: the content area stays white. The URL bar shows the site, the reload button is shown (the load finished), and no crash report is written.

Root cause

1. The Metal driver doesn't load (every process, from boot)

From backboardd at boot (same error in SpringBoard, SBRendererService, cameracaptured, mediaplaybackd, MobileSafari, com.apple.WebKit.GPU, PosterBoard extensions, nebulad…):

backboardd[67] [com.apple.CFBundle:loading] Error loading /System/Library/Extensions/AppleParavirtGPUMetalIOGPUFamily.bundle/AppleParavirtGPUMetalIOGPUFamily (99):
  dlopen(…, 0x0109): Symbol not found: _OBJC_IVAR_$_IOGPUMetalHeap._device
  Referenced from: <5ABC78D1-6760-32D4-B29A-3F6A77231E73> /System/Library/Extensions/AppleParavirtGPUMetalIOGPUFamily.bundle/AppleParavirtGPUMetalIOGPUFamily (built for iOS 26.1 which is newer than running OS)
  Expected in:     <44BC1A1D-96BD-30E8-A70D-4FFDDA7811B3> /System/Library/PrivateFrameworks/IOGPU.framework/IOGPU
backboardd[67] [com.apple.coreanimation:WindowServer] CoreAnimation: Software renderer! Simulating crash!
backboardd[67] [com.apple.coreanimation:WindowServer] CoreAnimation: Failed to initialize Metal or OpenGLES contexts!

In an app on the guest, MTLCreateSystemDefaultDevice() returns nil and MTLCopyAllDevices() returns an empty array.

2. It's a single missing export

I resolved every IOGPU/Metal import of the driver against the guest's frameworks (Frida, Module.findExportByName after loading IOGPU.framework):

  • 26.1 driver: 89 imports checked, only _OBJC_IVAR_$_IOGPUMetalHeap._device is missing.
  • 26.4 driver (26.4-23E5207q, 64.4.4): 102 imports checked, the same single symbol is missing. Swapping in the newer driver would not help.

The ivar itself exists on iOS 18.6.2. Only its offset symbol isn't exported:

IOGPUMetalHeap ivars (iOS 18.6.2 22G100): _resource@0x38, _device@0x40, _allocator@0x48, _mutex@0x70, _size@0xb0

In the 26.1 driver the symbol has one GOT slot (__got 0x583B0) and two uses, both reading heap->_device:

0xd050  adrp  x8, 0x58000
0xd054  ldr   x8, [x8, #0x3b0]   ; _OBJC_IVAR_$_IOGPUMetalHeap._device
0xd058  ldrsw x8, [x8]
0xd05c  ldr   x0, [x20, x8]

0xd180  adrp  x8, 0x58000
0xd184  ldr   x8, [x8, #0x3b0]   ; _OBJC_IVAR_$_IOGPUMetalHeap._device
0xd188  ldrsw x8, [x8]
0xd18c  ldr   x0, [x21, x8]

3. The software renderer drops WebKit's 10-bit backing stores

With CA in software mode, backboardd logs this whenever WebKit content (or a snapshot of it) has to be composited:

backboardd[67] [com.apple.coreanimation:OGL] SW renderer invalid destination format w30r
backboardd[67] [com.apple.coreanimation:OGL] SW renderer invalid destination format b3a8

w30r / b3a8 are WebKit's PixelFormat::RGB10 / RGB10A8. RemoteLayerBackingStore picks them when screenSupportsExtendedColor() is true, which comes from MobileGestalt HasExtendedColorDisplay (Aixt/MEN2O2B7f+8m4TxUA). It is 1 on this guest (iPhone 16 profile, ArtworkDisplayGamut = P3).

Comparison with a working guest on the same host

Same host, same bundle (2.10.1), both standard, same probe in each guest:

iOS 18.6.2 (22G100) + cloudOS 26.1iOS 27.0.1 (24A446) + cloudOS 26.4
GPU driver in guest26.1 (63.2.2, minos 26.1)26.4 (64.4.4, minos 26.4)
Driver dlopenfails: _OBJC_IVAR_$_IOGPUMetalHeap._deviceloads
MTLCreateSystemDefaultDevice()nilApple Paravirtual device GPU (Apple3, Common3)
backboarddSoftware renderer! / Failed to initialize Metal or OpenGLES contexts!no renderer errors
MGGetBoolAnswer("HasExtendedColorDisplay")11
Safari on wikipedia.orgblankrenders

Both guests report an extended-colour display, so WebKit picks the 10-bit backing-store formats on both. Only the guest whose driver loads (and so composites on Metal) can draw them. The userland/driver mismatch is the difference, not the host and not WebKit.

What I ruled out

  • Network: curl from the guest gets 200 from google.com, wikipedia.org and apple.com over IPv4. WebKit's log shows didReceiveResponse (httpStatusCode=200), didFinishResourceLoad (length=122197), didCommitLoadForFrame, didFinishLoadForFrame, and -[WKWebView _didCommitLayerTree:] commits arriving in the UI process.
  • DOM / JS: on wikipedia.org the search field gets focus. The caret and the form-assist bar show over the blank page.
  • Site-specific or JIT: example.com is blank too. So is a typed data: URL with no script: data:text/html,<body style=background:red><div style=background:blue;height:300px;font-size:60px>BLUE</div><div style=background:lime;height:300px;transform:translateZ(0)>GREEN</div><h1>RED</h1>. Only the red page background shows (it reaches the UI process as a plain layer backgroundColor). The painted blue block, the composited green layer and all the text are missing.
  • GPU-process DOM rendering: turning UseGPUProcessForDOMRenderingEnabled off changes the white to the page background colour, but painted content is still missing. Both paths produce IOSurface contents in the same formats.
  • WebKit crashes: none. No WebContent/GPU/Networking crash reports, and the processes stay alive.

Confirmation

Spawning Safari under Frida with MGGetBoolAnswer("HasExtendedColorDisplay") and WebCore::screenSupportsExtendedColor() forced to return false makes WebKit use 8-bit BGRA backing stores. With nothing else changed, example.com, google.com (consent dialog) and wikipedia.org all render correctly on the same guest. Script: safari-no-extended-color.js below.

Suggested fixes

  1. Make the 26.x driver load on an 18.x userland. The ABI gap at bind time is one ivar-offset symbol. On an iOS 18 base, cfw install could patch the two ldr x8,[x8,#GOT]; ldrsw x8,[x8] sites to a constant offset of IOGPUMetalHeap._device (0x40 on 22G100, best derived from the guest's own IOGPU ObjC metadata, not hard-coded), and drop or weak-import the bind. The tooling is already there (retargetGPUBundleToTVOS, MachOBuildPlatform, the heap_index_offset driver patch). After that, Metal on 18.x needs a runtime check for other behavioural differences between a 26.x driver and the 18.x Metal/IOGPU frameworks.
  2. Guardrail. VPhonePCCGPUDriver.validateBundle only compares DTPlatformVersion with the cloudOS version. It never checks the driver's MinimumOSVersion (26.1) against the iPhone userland (18.6.2). fw prepare / cfw install / vm info should warn that the guest will run without Metal and with CoreAnimation in software mode.
  3. Mitigation while Metal is unavailable. When the guest has no Metal device, make WebKit clients use 8-bit backing stores. For example, a SystemHook hook that returns false for MGGetBoolAnswer("HasExtendedColorDisplay") in WebKit client processes. That restores Safari, SFSafariViewController and every in-app WKWebView.
  4. Docs. compatibility.md still lists 17,3_18.6.2_22G100 + 26.1-23B85 under earlier combinations. It should note that this pairing currently has no GPU acceleration and blank web content.

Workaround script

Keep the Frida session open while browsing (--eternalize didn't keep the hooks in my test):

frida -U -f com.apple.mobilesafari -l safari-no-extended-color.js
const extendedColorKeys = ["HasExtendedColorDisplay", "Aixt/MEN2O2B7f+8m4TxUA"];

const mobileGestalt = Process.getModuleByName("libMobileGestalt.dylib");
Interceptor.attach(mobileGestalt.getExportByName("MGGetBoolAnswer"), {
    onEnter(args) {
        this.key = args[0].isNull() ? null : new ObjC.Object(args[0]).toString();
    },
    onLeave(result) {
        if (extendedColorKeys.includes(this.key) && result.toInt32() !== 0) {
            result.replace(0);
        }
    },
});

function hookWebCore() {
    const webCore = Process.findModuleByName("WebCore");
    if (webCore === null) {
        return false;
    }
    const screenSupportsExtendedColor = webCore.findExportByName("_ZN7WebCore27screenSupportsExtendedColorEPNS_6WidgetE");
    if (screenSupportsExtendedColor !== null) {
        Interceptor.attach(screenSupportsExtendedColor, { onLeave(result) { result.replace(0); } });
    }
    return true;
}

if (!hookWebCore()) {
    const timer = setInterval(() => { if (hookWebCore()) clearInterval(timer); }, 5);
}

Notes

  • The guest also has a RootHide bootstrap (SSL Kill Switch 3 is injected into MobileSafari but reports "Hook disabled"). It isn't involved: the driver load fails in backboardd at boot.
  • I only tested 18.6.2 (22G100). Every iOS 18.x userland probably hits the same missing export with any cloudOS 26.x driver.
  • The same host's iOS 26.6.2 guest also renders Safari normally (reported by the VM's owner). I took the measurements in the comparison table on the iOS 27.0.1 guest.

Activity

0

No activity yet.