High patch-now ✔︎ verified CVSS 8.3 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in ANGLE
DescriptionHeap buffer overflow in ANGLE
ComponentANGLE
Sub-componentVulkan
Bug ClassOOB
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
Reviewer notes: Mechanism matches diff. Minor: commit frames it as streaming more than the allocated (destination) buffer; source over-read is secondary. Path is Vulkan-backend CPU conversion only, so 'default' holds only where ANGLE uses Vulkan.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker496503799
Fix commita96c6e3e7e3a (angle/angle) +69/-2
CWECWE-122 (Heap buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedAnonymous
Reported2026-03-26 · bounty $16000 (per vendor advisory)
Fix landed2026-03-31 (public git)
Disclosed2026-05-05
Patch gap35 days — fix public in source before the advisory

Background

`VertexArrayVk::convertVertexBufferCPU`
the ANGLE Vulkan-backend routine that, when a vertex format cannot be used directly by the GPU, converts vertex attribute data on the CPU into a newly allocated conversion buffer.
`StreamVertexData`
the helper that reads source vertices (via a vertexLoadFunction and srcStride) and writes the converted result into the conversion buffer at dstOffset.
`numVertices` vs. `maxNumVertices`
maxNumVertices is the full capacity of the attribute/conversion buffer, while numVertices is the smaller count of vertices actually covered by the current dirty sub-range computed by CalculateOffsetAndVertexCountForDirtyRange.
Dirty-range partial update
when only part of a vertex buffer changes (e.g. via glBufferSubData), ANGLE converts just that sub-range rather than the whole buffer.

Root Cause Analysis

When converting only a dirty sub-range, convertVertexBufferCPU computed the number of bytes to copy as bytesToCopy = maxNumVertices * dstFormat.pixelBytes and passed maxNumVertices as the vertex count to StreamVertexData, even though CalculateOffsetAndVertexCountForDirtyRange had just produced the smaller numVertices and a srcOffset into the source. For a partial update near the end of the buffer, srcBytes = src + srcOffset is advanced well into the buffer, yet the code still streamed a full maxNumVertices worth of vertices from that advanced pointer, reading past the end of the source data and writing/streaming more than the sub-range should — a heap-buffer-overflow, as the commit states it “streams more data than buffer it allocates.” The invariant violated is that the amount copied must match the dirty sub-range’s own vertex count and extent, not the full buffer capacity.

The fix recomputes bytesToCopy = numVertices * dstFormat.pixelBytes and passes numVertices to StreamVertexData, and adds ASSERT(numVertices <= maxNumVertices) to enforce the relationship. The regression test BufferDataTest.UnalignedVertexAttribPointer uses an 8MB buffer with an unaligned stride/offset and a glBufferSubData near the end to reach this CPU-conversion path and expose the overflow.

Key insight
The core mistake was sizing the copy and the stream count by the whole-buffer maxNumVertices while processing only a dirty sub-range starting at srcOffset; the fix uses the sub-range’s actual numVertices for both bytesToCopy and the StreamVertexData count.

Attack Path

  1. Allocate a large vertex buffer The client creates a buffer big enough to force the non-sub-allocation CPU-conversion path (the test uses 8MB).
  2. Force CPU conversion with unaligned geometry It configures glVertexAttribPointer with an unaligned stride/offset, which cannot be fed to the GPU directly and routes through convertVertexBufferCPU.
  3. Establish the conversion buffer A first draw builds the conversion buffer sized for the data, and glFinish prevents reallocation.
  4. Dirty a sub-range near the end A glBufferSubData near the buffer’s end (large enough to defeat the dirty-range short-circuit) creates a dirty range with a nonzero srcOffset and small numVertices.
  5. Overflow on redraw The next draw re-enters convertVertexBufferCPU, which pre-patch copies maxNumVertices * pixelBytes from src + srcOffset, reading beyond the source and overflowing the heap during streaming (the heap-buffer-overflow the test triggers).

Impact Assessment

A client issuing WebGL/GLES draws with an unaligned vertex attribute that forces CPU vertex conversion can trigger a heap-buffer-overflow in ANGLE’s Vulkan backend, running in the GPU process. The vendor advisory rates this high severity (heap buffer overflow in ANGLE) and does not report exploited-in-the-wild status; whether the out-of-bounds access is exploitable beyond a crash (e.g. into memory corruption usable for code execution) is inferred and not demonstrated by the diff. Preconditions are the ability to submit draws with an unaligned stride/offset against a buffer large enough to take the CPU-conversion path, with a partial sub-range update.

Files Changed

  • src/libANGLE/renderer/vulkan/VertexArrayVk.cpp
  • src/tests/angle_end2end_tests_expectations.txt
  • src/tests/gl_tests/BufferDataTest.cpp

Audit Directions

  • Capacity vs. active-count mismatches
    Flag any copy where the length is derived from a buffer’s full capacity (max*) while the loop/offset is driven by a smaller active or dirty count.
  • Dirty-range / partial-update paths
    Audit conversion and streaming routines that handle sub-ranges, verifying the byte count and element count both use the sub-range’s values alongside its srcOffset.
  • Unaligned-attribute CPU fallbacks
    Review other backend CPU-conversion fallbacks (VertexArray*, index/vertex streaming) reached by unaligned stride/offset for similar size-vs-offset inconsistencies.
From a96c6e3e7e3abb396109be762c06c2b2784fbde7 Mon Sep 17 00:00:00 2001
From: Charlie Lao <[email protected]>
Date: Mon, 30 Mar 2026 12:01:45 -0700
Subject: [PATCH] Vulkan: Fix heap-buffer-overflow in convertVertexBufferCPU

There was a bug in vulkan backend
VertexArrayVk::convertVertexBufferCPU() that it streams more data than
buffer it allocates. A test has been added to expose the
heap-buffer-overflow bug. The bug is also fixed in the CL.

Bug: b/496503799
Change-Id: I79f8619699a9c82ea7d35e4849edfb91866c3623
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7712217
Reviewed-by: Amirali Abdolrashidi <[email protected]>
Reviewed-by: Shahbaz Youssefi <[email protected]>
Commit-Queue: Charlie Lao <[email protected]>
---

diff --git a/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp b/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp
index c23d424..fe9cc5a 100644
--- a/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp
+++ b/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp
@@ -855,17 +855,21 @@
                 continue;
             }
 
+            // Use numVertices instead of maxNumVertices to calculate bytesToCopy to avoid buffer
+            // overrun.
             uint32_t srcOffset, dstOffset, numVertices;
             CalculateOffsetAndVertexCountForDirtyRange(srcBuffer, conversion, srcFormat, dstFormat,
                                                        dirtyRange, &srcOffset, &dstOffset,
                                                        &numVertices);
+            ASSERT(numVertices <= maxNumVertices);
 
             if (numVertices > 0)
             {
                 const uint8_t *srcBytes = src + srcOffset;
-                size_t bytesToCopy      = maxNumVertices * dstFormat.pixelBytes;
+
+                size_t bytesToCopy = numVertices * dstFormat.pixelBytes;
                 ANGLE_TRY(StreamVertexData(contextVk, conversion->getBuffer(), srcBytes,
-                                           bytesToCopy, dstOffset, maxNumVertices, srcStride,
+                                           bytesToCopy, dstOffset, numVertices, srcStride,
                                            vertexLoadFunction));
             }
         }
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 09f5279..61f2e99 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -2651,6 +2651,8 @@
 
 // WebGPU does not support BGRA
 42267264 WGPU : ClearTest.*BGRA* = SKIP
+// WebGPU error BufferOffset (5242862) is not a multiple of 4.
+496503799 WGPU : BufferDataTest.UnalignedVertexAttribPointer/* = SKIP
 
 // Fails on Mac & OpenGL & NVIDIA
 406807990 MAC NVIDIA OPENGL : TextureUploadFormatTest_ES3.AllWithPBO/* = SKIP
diff --git a/src/tests/gl_tests/BufferDataTest.cpp b/src/tests/gl_tests/BufferDataTest.cpp
index 97a8fba..45a3fc6 100644
--- a/src/tests/gl_tests/BufferDataTest.cpp
+++ b/src/tests/gl_tests/BufferDataTest.cpp
@@ -1376,6 +1376,67 @@
     EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::cyan);
 }
 
+// Tests unaligned vertex attribute pointer, which should get to convertVertexBufferCPU in vulkan
+// backend.
+TEST_P(BufferDataTest, UnalignedVertexAttribPointer)
+{
+    ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::UniformColor());
+    glUseProgram(program);
+
+    GLint positionLocation = glGetAttribLocation(program, essl1_shaders::PositionAttrib());
+    ASSERT_NE(positionLocation, -1);
+    glEnableVertexAttribArray(positionLocation);
+
+    GLint colorUniformLocation =
+        glGetUniformLocation(program, angle::essl1_shaders::ColorUniform());
+    ASSERT_NE(colorUniformLocation, -1);
+
+    // Allocate 8MB buffer to force non sub-allocation path
+    constexpr GLsizeiptr kBufferSize = 8 * 1024 * 1024;
+    std::vector<uint8_t> initialData(kBufferSize, 0);
+
+    GLBuffer positionBuffer;
+    glBindBuffer(GL_ARRAY_BUFFER, positionBuffer);
+    glBufferData(GL_ARRAY_BUFFER, kBufferSize, initialData.data(), GL_DYNAMIC_DRAW);
+
+    // Trigger CPU downgrade conversion with unaligned stride/offset
+    const GLsizei stride      = 13;
+    const GLintptr offset     = 1;
+    const GLint components    = 3;
+    const GLsizei typeSize    = sizeof(GLfloat);
+    const GLsizei elementSize = components * typeSize;  // 12
+
+    // Calculate vertexCount equivalent to Math.floor in JS
+    GLsizei vertexCount     = (kBufferSize - offset - elementSize) / stride + 1;
+    GLsizei quadVertexCount = (vertexCount / 6) * 6;
+    GLsizei firstVertex     = vertexCount - quadVertexCount;
+
+    glVertexAttribPointer(positionLocation, components, GL_FLOAT, GL_FALSE, stride,
+                          reinterpret_cast<const void *>(offset));
+
+    // First draw: establishes the conversion buffer
+    glDrawArrays(GL_TRIANGLES, firstVertex, quadVertexCount);
+    // CRITICAL: glFinish() forces GPU pipeline flush, preventing buffer reallocation
+    glFinish();
+
+    // SubData near the end - must be >= 12 bytes to break ANGLE's dirty-range short-circuit
+    const std::array<Vector3, 6> &quadVertices = GetQuadVertices();
+    std::vector<uint8_t> updateData(stride * 6);
+    for (int vertexIndex = 0; vertexIndex < 6; vertexIndex++)
+    {
+        memcpy(updateData.data() + stride * vertexIndex, quadVertices[vertexIndex].data(),
+               quadVertices[vertexIndex].size() * sizeof(float));
+    }
+    GLintptr lastQuadVertexStartPtr = offset + (vertexCount - 6) * stride;
+    glBufferSubData(GL_ARRAY_BUFFER, lastQuadVertexStartPtr, updateData.size(), updateData.data());
+
+    // Draw green quad. This should trigger partial update but should not access out of bounds
+    glClear(GL_COLOR_BUFFER_BIT);
+    glUniform4fv(colorUniformLocation, 1, &kFloatGreen.R);
+    glDrawArrays(GL_TRIANGLES, firstVertex, quadVertexCount);
+    EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth(), getWindowHeight(), GLColor::green);
+}
+
 // Verify that previous draws are not affected when a buffer is respecified with null data
 // and updated by calling map.
 TEST_P(BufferDataTestES3, BufferDataWithNullFollowedByMap)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 09f5279..61f2e99 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -2651,6 +2651,8 @@
 
 // WebGPU does not support BGRA
 42267264 WGPU : ClearTest.*BGRA* = SKIP
+// WebGPU error BufferOffset (5242862) is not a multiple of 4.
+496503799 WGPU : BufferDataTest.UnalignedVertexAttribPointer/* = SKIP
 
 // Fails on Mac & OpenGL & NVIDIA
 406807990 MAC NVIDIA OPENGL : TextureUploadFormatTest_ES3.AllWithPBO/* = SKIP
diff --git a/src/tests/gl_tests/BufferDataTest.cpp b/src/tests/gl_tests/BufferDataTest.cpp
index 97a8fba..45a3fc6 100644
--- a/src/tests/gl_tests/BufferDataTest.cpp
+++ b/src/tests/gl_tests/BufferDataTest.cpp
@@ -1376,6 +1376,67 @@
     EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::cyan);
 }
 
+// Tests unaligned vertex attribute pointer, which should get to convertVertexBufferCPU in vulkan
+// backend.
+TEST_P(BufferDataTest, UnalignedVertexAttribPointer)
+{
+    ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::UniformColor());
+    glUseProgram(program);
+
+    GLint positionLocation = glGetAttribLocation(program, essl1_shaders::PositionAttrib());
+    ASSERT_NE(positionLocation, -1);
+    glEnableVertexAttribArray(positionLocation);
+
+    GLint colorUniformLocation =
+        glGetUniformLocation(program, angle::essl1_shaders::ColorUniform());
+    ASSERT_NE(colorUniformLocation, -1);
+
+    // Allocate 8MB buffer to force non sub-allocation path
+    constexpr GLsizeiptr kBufferSize = 8 * 1024 * 1024;
+    std::vector<uint8_t> initialData(kBufferSize, 0);
+
+    GLBuffer positionBuffer;
+    glBindBuffer(GL_ARRAY_BUFFER, positionBuffer);
+    glBufferData(GL_ARRAY_BUFFER, kBufferSize, initialData.data(), GL_DYNAMIC_DRAW);
+
+    // Trigger CPU downgrade conversion with unaligned stride/offset
+    const GLsizei stride      = 13;
+    const GLintptr offset     = 1;
+    const GLint components    = 3;
+    const GLsizei typeSize    = sizeof(GLfloat);
+    const GLsizei elementSize = components * typeSize;  // 12
+
+    // Calculate vertexCount equivalent to Math.floor in JS
+    GLsizei vertexCount     = (kBufferSize - offset - elementSize) / stride + 1;
+    GLsizei quadVertexCount = (vertexCount / 6) * 6;
+    GLsizei firstVertex     = vertexCount - quadVertexCount;
+
+    glVertexAttribPointer(positionLocation, components, GL_FLOAT, GL_FALSE, stride,
+                          reinterpret_cast<const void *>(offset));
+
+    // First draw: establishes the conversion buffer
+    glDrawArrays(GL_TRIANGLES, firstVertex, quadVertexCount);
+    // CRITICAL: glFinish() forces GPU pipeline flush, preventing buffer reallocation
+    glFinish();
+
+    // SubData near the end - must be >= 12 bytes to break ANGLE's dirty-range short-circuit
+    const std::array<Vector3, 6> &quadVertices = GetQuadVertices();
+    std::vector<uint8_t> updateData(stride * 6);
+    for (int vertexIndex = 0; vertexIndex < 6; vertexIndex++)
+    {
+        memcpy(updateData.data() + stride * vertexIndex, quadVertices[vertexIndex].data(),
+               quadVertices[vertexIndex].size() * sizeof(float));
+    }
+    GLintptr lastQuadVertexStartPtr = offset + (vertexCount - 6) * stride;
+    glBufferSubData(GL_ARRAY_BUFFER, lastQuadVertexStartPtr, updateData.size(), updateData.data());
+
+    // Draw green quad. This should trigger partial update but should not access out of bounds
+    glClear(GL_COLOR_BUFFER_BIT);
+    glUniform4fv(colorUniformLocation, 1, &kFloatGreen.R);
+    glDrawArrays(GL_TRIANGLES, firstVertex, quadVertexCount);
+    EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth(), getWindowHeight(), GLColor::green);
+}
+
 // Verify that previous draws are not affected when a buffer is respecified with null data
 // and updated by calling map.
 TEST_P(BufferDataTestES3, BufferDataWithNullFollowedByMap)
Loading diff…

Original Bug Report

reported by [email protected]

heap-buffer-overflow in ANGLE VertexArrayVk::convertVertexBufferCPU

VULNERABILITY DETAILS

A heap-buffer-overflow exists in the ANGLE Vulkan backend’s CPU-side vertex format conversion path. When a partial buffer update (via bufferSubData) dirties only a small range of a large vertex buffer, the subsequent convertVertexBufferCPU function computes the copy length using the total buffer capacity instead of the dirty range size, causing an out-of-bounds write into the conversion buffer.

In convertVertexBufferCPU, when the conversion buffer is not entirely dirty, the function iterates over dirty ranges [0]:

        const std::vector<RangeDeviceSize> &dirtyRanges = conversion->getDirtyBufferRanges();
        for (const RangeDeviceSize &dirtyRange : dirtyRanges) // [0]
        {
            if (dirtyRange.empty())
            {
                // consolidateDirtyRanges may end up with invalid range if it gets merged.
                continue;
            }

            uint32_t srcOffset, dstOffset, numVertices;
            CalculateOffsetAndVertexCountForDirtyRange(srcBuffer, conversion, srcFormat, dstFormat,
                                                       dirtyRange, &srcOffset, &dstOffset,
                                                       &numVertices);

            if (numVertices > 0)
            {
                const uint8_t *srcBytes = src + srcOffset;
                size_t bytesToCopy      = maxNumVertices * dstFormat.pixelBytes; // [1]
                ANGLE_TRY(StreamVertexData(contextVk, conversion->getBuffer(), srcBytes, // [2]
                                           bytesToCopy, dstOffset, maxNumVertices, srcStride,
                                           vertexLoadFunction));
            }
        }

CalculateOffsetAndVertexCountForDirtyRange computes dstOffset based on where the dirty range begins within the buffer — when the dirty range is near the end of the buffer, dstOffset points near the end of the conversion buffer.

However, bytesToCopy at [1] is computed from maxNumVertices, which represents the total vertex count derived from the entire buffer capacity. The conversion buffer is allocated with exactly maxNumVertices * dstFormat.pixelBytes bytes. When StreamVertexData is called at [2], vertexLoadFunction copies maxNumVertices vertices starting from dstOffset into the conversion buffer, writing far past the buffer’s end.

StreamVertexData writes directly to the destination buffer at the given offset [3]:

    uint8_t *dst = dstBufferHelper->getMappedMemory() + dstOffset;

    if (vertexLoadFunction != nullptr)
    {
        vertexLoadFunction(srcData, srcStride, vertexCount, dst); // [3]
    }

The conversion buffer is allocated with maxNumVertices * dstStride bytes via CalculateMaxVertexCountForConversion [4]:

    ANGLE_TRY(contextVk->initBufferForVertexConversion(conversion, maxNumVertices * dstStride, // [4]
                                                       hostVisible));

So when dstOffset is near the end and the copy length equals the full buffer size, the write overflows the conversion buffer heap allocation.

To reach the vulnerable CPU conversion path, the vertex attribute stride or offset must be unaligned with the format’s component size, causing bindingIsAligned to evaluate to false. This triggers the CPU fallback path in syncNeedsConversionAttrib [5]:

        if (bindingIsAligned)
        {
            ANGLE_TRY(
                convertVertexBufferGPU(contextVk, bufferVk, conversion, srcFormat, dstFormat));
        }
        else
        {
            ANGLE_VK_PERF_WARNING(contextVk, GL_DEBUG_SEVERITY_HIGH,
                                  "GPU stall due to vertex format conversion of unaligned data");

            ANGLE_TRY(convertVertexBufferCPU(contextVk, bufferVk, conversion, srcFormat, dstFormat, // [5]
                                             vertexFormat.getVertexLoadFunction()));
        }

WebGL contexts enforce stride/offset alignment via ValidateWebGLVertexAttribPointer, blocking this path. However, a compromised renderer can request an OPENGLES2 context type instead of CONTEXT_TYPE_WEBGL1/2 when creating the GPU command buffer — CONTEXT_TYPE_OPENGLES2 is a legitimate context type that is fully supported by the IPC serialization layer and GPU service. With isWebGL() returning false, the alignment validation is bypassed, and unaligned stride/offset values reach the Vulkan backend.

[0] https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp;drc=216f1f0264f2b729c45c6aa7a038dc592c4551cc;l=850

[1] https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp;drc=216f1f0264f2b729c45c6aa7a038dc592c4551cc;l=866

[2] https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp;drc=216f1f0264f2b729c45c6aa7a038dc592c4551cc;l=867

[3] https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp;drc=216f1f0264f2b729c45c6aa7a038dc592c4551cc;l=236

[4] https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp;drc=216f1f0264f2b729c45c6aa7a038dc592c4551cc;l=370

[5] https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp;drc=216f1f0264f2b729c45c6aa7a038dc592c4551cc;l=1323

BISECTION

Introduced by ANGLE commit [0] which added fine-grained dirty range tracking for vertex buffer conversion but incorrectly used the full-buffer vertex count (maxNumVertices) instead of the dirty-range vertex count (numVertices) when computing the copy size.

This was rolled into Chromium in commit [1].

[0] https://chromium.googlesource.com/angle/angle/+/53476d6ff2740267db0c0573644378621c4e7d78

[1] https://chromium.googlesource.com/chromium/src/+/2580fab69bf8f219b97a3e812122dca566c525e0

VERSION

Chrome Version: HEAD

Operating System: Linux

REPRODUCTION CASE

This vulnerability requires a compromised renderer. The attached renderer.patch modifies the renderer process to request CONTEXT_TYPE_OPENGLES2 instead of WebGL context types, bypassing WebGL alignment checks.

  1. Apply the renderer patch and build Chromium with ASan.
  2. Host poc.html on an HTTP server.
  3. Run Chrome against the PoC.
$ git apply renderer.patch && autoninja -C out/asan chrome
$ python3 -m http.server
$ ./out/asan/chrome --use-angle=vulkan "http://localhost:8000/poc.html"

CRASH INFORMATION

Type of crash: GPU process

Crash log: see the attached asan.txt ASan trace.

CREDIT INFORMATION

Reporter credit: Anonymous

View on issue tracker