Closed Bug 2022243 Opened 4 months ago Closed 4 months ago

Type confusion with EGLImageDescriptor in CreateTextureHostOGL

Categories

(Core :: Graphics, defect)

defect

Tracking

()

RESOLVED FIXED
150 Branch
Tracking Status
firefox-esr115 149+ fixed
firefox-esr140 149+ fixed
firefox148 --- wontfix
firefox149 + fixed
firefox150 + fixed

People

(Reporter: tschuster, Assigned: jnicol)

Details

(Keywords: ai-involved, csectype-sandbox-escape, sec-high, Whiteboard: [adv-main149+r][adv-ESR140.9+r][adv-ESR115.34+r])

Attachments

(6 files, 1 obsolete file)

Summary

Type confusion in the Firefox parent process via content-controlled pointer injection. /firefox/gfx/layers/ipc/LayersSurfaces.ipdlh defines EGLImageDescriptor with uintptr_t image and uintptr_t fence fields that cross IPC boundaries without validation. /firefox/gfx/layers/opengl/TextureHostOGL.cpp:84-89 casts these directly to EGLImage/EGLSync with no IsSameProcess() check (contrast TextureHost.cpp:310 which guards the analogous MemoryOrShmem::Tuintptr_t). When WebRender uses a GLX backend, /firefox/gfx/webrender_bindings/RenderEGLImageTextureHost.cpp:160 calls GLContextEGL::Cast() on a GLContextGLX*, whose MOZ_ASSERT-only guard (/firefox/gfx/gl/GLContextEGL.h:64) compiles out in release builds, yielding an unchecked downcast that reads a function pointer table through type-confused memory.

Affected Code

File: /firefox/gfx/layers/opengl/TextureHostOGL.cpp, lines 84–89

case SurfaceDescriptor::TEGLImageDescriptor: {
  const EGLImageDescriptor& desc = aDesc.get_EGLImageDescriptor();
  result = new EGLImageTextureHost(aFlags, (EGLImage)desc.image(),
                                   (EGLSync)desc.fence(), desc.size(),
                                   desc.hasAlpha());
  break;
}

This accepts raw uintptr_t values from an untrusted IPC peer and interprets them as pointers in the parent address space. The only legitimate sender is SharedSurface_EGLImage::ToSurfaceDescriptor() at /firefox/gfx/gl/SharedSurfaceEGL.cpp:123, which is designed for same-process sharing only — but nothing enforces this constraint.

File: /firefox/gfx/gl/GLContextEGL.h, lines 63–66

static GLContextEGL* Cast(GLContext* gl) {
  MOZ_ASSERT(gl->GetContextType() == GLContextType::EGL);
  return static_cast<GLContextEGL*>(gl);
}

MOZ_ASSERT is a no-op when built with --disable-debug. This becomes a raw static_cast with no runtime type check.

File: /firefox/gfx/webrender_bindings/RenderEGLImageTextureHost.cpp, lines 157–168

bool RenderEGLImageTextureHost::WaitSync() {
  bool syncSucceeded = true;
  if (mSync) {                                          // mSync = 0x4242..., non-zero
    const auto& gle = gl::GLContextEGL::Cast(mGL);      // TYPE CONFUSION: mGL is GLContextGLX*
    const auto& egl = gle->mEgl;                        // reads shared_ptr at wrong offset
    MOZ_ASSERT(egl->IsExtensionSupported(gl::EGLExtension::KHR_fence_sync));
    if (egl->IsExtensionSupported(gl::EGLExtension::KHR_wait_sync)) {
      syncSucceeded = egl->fWaitSync(mSync, 0) == LOCAL_EGL_TRUE;
    } else {
      syncSucceeded = egl->fClientWaitSync(mSync, 0, LOCAL_EGL_FOREVER) ==
                      LOCAL_EGL_CONDITION_SATISFIED;    // INDIRECT CALL THROUGH CONFUSED PTR CHAIN
    }

Exploit Chain

  1. Compromised content process calls PCompositorBridgeChild::CreateTexture() with SurfaceDescriptor = EGLImageDescriptor{image=0x4141414141414141, fence=0x4242424242424242, size={64,64}, hasAlpha=true}
  2. Parent ContentCompositorBridgeParent::AllocPTextureParentTextureHost::CreateCreateTextureHostOGL at /firefox/gfx/layers/opengl/TextureHostOGL.cpp:84 constructs EGLImageTextureHost with mImage=(void*)0x4141..., mSync=(void*)0x4242...
  3. Content sends OpPushExternalImageForTexture via WebRenderBridgeChild::SendUpdateResources
  4. Parent WebRenderBridgeParent::RecvUpdateResourcesPushExternalImageForTextureEGLImageTextureHost::CreateRenderTexture at /firefox/gfx/layers/opengl/TextureHostOGL.cpp:1000 creates wr::RenderEGLImageTextureHost(mImage=0x4141..., mSync=0x4242..., ...) and registers it with the Render thread
  5. Content injects PushImage for the forged image key into its display list
  6. Parent Render thread: WebRender renderer → wr_renderer_lock_external_imageRenderEGLImageTextureHost::Lock(aChannelIndex, aGL) at /firefox/gfx/webrender_bindings/RenderEGLImageTextureHost.cpp:36
  7. mGL = aGL — on GLX backend (gfx.x11-egl.force-disabled=true), aGL is a GLContextGLX*
  8. Lock()WaitSync(): mSync != 0GLContextEGL::Cast(mGL) performs unchecked downcast
  9. gle->mEgl reads 16 bytes at sizeof(GLContext)+0 in a GLContextGLX object as std::shared_ptr<EglDisplay>. The shared_ptr's element pointer aliases GLContextGLX::mContext (a Mesa GLXContext heap pointer)
  10. egl->IsExtensionSupported(KHR_wait_sync) reads a bit from [GLXContext + offsetof(EglDisplay, mAvailableExtensions)] — succeeds (Mesa struct is large enough). Bit happens to be 0 → fClientWaitSync branch
  11. egl->fClientWaitSync(mSync, 0, LOCAL_EGL_FOREVER)EglDisplay::fClientWaitSync at /firefox/gfx/gl/GLLibraryEGL.h:863mLib->fClientWaitSync(...): mLib read as RefPtr<GLLibraryEGL> from offset 0 of Mesa's __GLXcontextRec
  12. GLLibraryEGL::fClientWaitSync at /firefox/gfx/gl/GLLibraryEGL.h:431WRAP(fClientWaitSyncKHR(dpy, sync, flags, timeout))mSymbols.fClientWaitSyncKHR(dpy, sync, flags, timeout)indirect call through function pointer at [mLib + 0x1e0] with sync = 0x4242424242424242

IPC Path

  • IPDL protocol: PCompositorBridge (manages PTexture), PWebRenderBridge (carries OpUpdateResource[] including OpPushExternalImageForTexture, and SetDisplayList with injected PushImage items)
  • Parent actor: ContentCompositorBridgeParent / WebRenderBridgeParent — run in the main Firefox process (no GPU process on GLX-forced Linux)
  • Child actor: CompositorBridgeChild / WebRenderBridgeChild in the content process
  • Messages:
    1. PCompositorBridge::PTexture constructor with SurfaceDescriptor = EGLImageDescriptor union arm
    2. PWebRenderBridge::UpdateResources with OpPushExternalImageForTexture
    3. PWebRenderBridge::SetDisplayList with PushImage item referencing the forged image key

Security Impact

Severity: High / sec-high

Attacker capability: A compromised content process can inject arbitrary pointer values that are dereferenced through a 3-level chain in the parent process, terminating in an indirect function call. The specific faulting address depends on the first 8 bytes of the GLX driver's __GLXcontextRec structure — a layout entirely outside Firefox's control. On Mesa builds where this field is zero, the result is a crash. On drivers/versions where it is a valid pointer (vtable, next-pointer, etc.), the code loads and calls a function pointer from attacker-uncontrolled-but-variable memory with the attacker-controlled 0x4242... value as an argument — a control-flow hijack primitive.

Preconditions:

  • Compromised content process (e.g., via JS engine bug) — this is a sandbox escape vector
  • WebRender using GLX backend on Linux X11 — requires either gfx.x11-egl.force-disabled=true OR natural EGL init failure (old drivers, missing libEGL, distros without EGL). GLX was the historical default and remains the fallback path.
  • Does NOT require any user interaction beyond the attacker already having code execution in content

Secondary surface (even on EGL backends): The content-controlled mImage/mSync values are passed directly to fEGLImageTargetTexture2D(target, mImage) and egl->fWaitSync(mSync, 0). Mesa's EGL validates handles, but NVIDIA/AMD proprietary drivers and Android EGL implementations may dereference these as raw pointers.

Suggested Fix

Primary fix — reject cross-process raw pointers (/firefox/gfx/layers/opengl/TextureHostOGL.cpp):

case SurfaceDescriptor::TEGLImageDescriptor: {
  if (aDeallocator && !aDeallocator->IsSameProcess()) {
    gfxCriticalError() << "EGLImageDescriptor from remote process rejected "
                          "(raw pointer injection)";
    return nullptr;
  }
  const EGLImageDescriptor& desc = aDesc.get_EGLImageDescriptor();
  result = new EGLImageTextureHost(aFlags, (EGLImage)desc.image(),
                                   (EGLSync)desc.fence(), desc.size(),
                                   desc.hasAlpha());
  break;
}

The same guard should be applied to TSurfaceDescriptorSharedGLTexture in the same switch, which also casts (GLsync)desc.fence() from a cross-process uintptr_t.

Defense-in-depth (/firefox/gfx/gl/GLContextEGL.h):

static GLContextEGL* Cast(GLContext* gl) {
  MOZ_RELEASE_ASSERT(gl->GetContextType() == GLContextType::EGL);
  return static_cast<GLContextEGL*>(gl);
}
Attached patch exploit.patchSplinter Review
Assignee: nobody → tschuster
Attached file test.html
Assignee: tschuster → nobody
Attached file (secure)
Assignee: nobody → jnicol
Status: NEW → ASSIGNED
Severity: -- → S2

Comment on attachment 9551523 [details]
(secure)

Security Approval Request

  • How easily could an exploit be constructed based on the patch?: The patch quite clearly shows that there is a vulnerability when sending these specific types of SurfaceDescriptor cross process, and from that it's probably fairly obvious which specific fields could be used. The GLX-specific part is probably more difficult to figure out, even with the added assertion in the patch. But as the report notes, proprietary EGL drivers may be vulnerable too.
  • Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: No
  • Which branches (beta, release, and/or ESR) are affected by this flaw, and do the release status flags reflect this affected/unaffected state correctly?: All branches
  • If not all supported branches, which bug introduced the flaw?: None
  • Do you have backports for the affected branches?: No
  • If not, how different, hard to create, and risky will they be?: Patch applies cleanly to all branches
  • How likely is this patch to cause regressions; how much testing does it need?: Unlikely
  • Is the patch ready to land after security approval is given?: Yes
  • Is Android affected?: Yes
Attachment #9551523 - Flags: sec-approval?

Comment on attachment 9551523 [details]
(secure)

sec-approval+ to land now and start requesting uplifts

Attachment #9551523 - Flags: sec-approval?
Attachment #9551523 - Flags: sec-approval+
Attachment #9551523 - Flags: approval-mozilla-beta+
Attachment #9551523 - Flags: approval-mozilla-beta+

:jnicol fyi we need the uplift requests asap in order to roll this out with 149

Flags: needinfo?(jnicol)

firefox-beta Uplift Approval Request

  • User impact if declined: sandbox escape
  • Code covered by automated testing: no
  • Fix verified in Nightly: no
  • Needs manual QE test: no
  • Steps to reproduce for manual QE testing: N/A
  • Risk associated with taking this patch: low
  • Explanation of risk level: Fails when sending invalid textures being sent cross process. Failure path is handled safely
  • String changes made/needed: N/A
  • Is Android affected?: yes
Attachment #9552308 - Flags: approval-mozilla-beta?
Attached file (secure)

firefox-esr115 Uplift Approval Request

  • User impact if declined: sandbox escape
  • Code covered by automated testing: no
  • Fix verified in Nightly: no
  • Needs manual QE test: no
  • Steps to reproduce for manual QE testing: N/A
  • Risk associated with taking this patch: low
  • Explanation of risk level: Fails when sending invalid textures being sent cross process. Failure path is handled safely
  • String changes made/needed: N/A
  • Is Android affected?: yes
Attachment #9552309 - Flags: approval-mozilla-esr115?
Attached file (secure)

firefox-esr140 Uplift Approval Request

  • User impact if declined: sandbox escape
  • Code covered by automated testing: no
  • Fix verified in Nightly: no
  • Needs manual QE test: no
  • Steps to reproduce for manual QE testing: N/A
  • Risk associated with taking this patch: low
  • Explanation of risk level: Fails when sending invalid textures being sent cross process. Failure path is handled safely
  • String changes made/needed: N/A
  • Is Android affected?: yes
Attachment #9552310 - Flags: approval-mozilla-esr140?
Attached file (secure)

firefox-release Uplift Approval Request

  • User impact if declined: sandbox escape
  • Code covered by automated testing: no
  • Fix verified in Nightly: no
  • Needs manual QE test: no
  • Steps to reproduce for manual QE testing: N/A
  • Risk associated with taking this patch: low
  • Explanation of risk level: Fails when sending invalid textures being sent cross process. Failure path is handled safely
  • String changes made/needed: N/A
  • Is Android affected?: yes
Attachment #9552312 - Flags: approval-mozilla-release?
Attached file (secure) (obsolete) —
Flags: needinfo?(jnicol)
Pushed by jnicol@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/32fc2ed34ce5 https://hg.mozilla.org/integration/autoland/rev/7e17408fdc74 Avoid deserializing invalid surface descriptors when sent cross process. r=gfx-reviewers,bradwerth
Group: gfx-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 4 months ago
Resolution: --- → FIXED
Target Milestone: --- → 150 Branch
Attachment #9552308 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
Attachment #9552312 - Attachment is obsolete: true
Attachment #9552312 - Flags: approval-mozilla-release?
Attachment #9552309 - Flags: approval-mozilla-esr115? → approval-mozilla-esr115+
Attachment #9552310 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c150/b149]
Whiteboard: [adv-main149+r][adv-ESR140.9+r][adv-ESR115.34+r]
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: