Medium watch CVSS 5.4 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read and write in GFX
DescriptionOut of bounds read and write in GFX
ComponentGFX
Sub-componentX11
Bug ClassOOB
Action tierwatch — corruption primitive but gated (local) — not default-reachable
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.
Tracker496259890
Fix commit6c8a8e8cee99 (chromium/src) +47/-2
CWECWE-125, CWE-787 (Out-of-bounds read, Out-of-bounds write)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-25 (per vendor advisory)
Fix landed2026-04-06 (public git)
Disclosed2026-05-05
Patch gap29 days — fix public in source before the advisory

Background

X11 `GetProperty`
an X protocol request whose reply carries a window-property value plus a format field that must be 8, 16, or 32 (bits per element) and a value_len count.
`response->format` / `value_len`
attacker-influenceable fields from the X server’s reply describing element bit-width and element count.
`T` / `sizeof(T)`
the C++ element type the caller expects for the property value; the template reads the value into a std::vector<T>.
`UNSAFE_TODO(memcpy(...))`
a raw memcpy from the reply’s value->bytes() into the resized output vector, with no implicit bounds check beyond the computed length.

Root Cause Analysis

The vulnerable code is the templated GetProperty-reply handler in ui/gfx/x/connection.h. Pre-patch it validated only response->format / 8u != sizeof(T), which is an integer division: for sizeof(T) == 4 any format in [32, 39] yields 4 and passes the check, so a malformed format such as 39 is accepted. It then computed byte_len = response->value_len * response->format / 8u but resized the destination to value->resize(response->value_len) (i.e. value_len * sizeof(T) bytes), and memcpy’d byte_len bytes in. With format = 39, byte_len = value_len * 39 / 8 exceeds value_len * sizeof(T) = value_len * 4, so the memcpy writes past the end of the destination buffer (OOB write) and reads past the reply’s value buffer (OOB read).

The fix tightens the guard to require response->format be exactly 8, 16, or 32 and response->format / 8u == sizeof(T), and recomputes byte_len = response->value_len * sizeof(T) so the copy length always matches the allocation; a parallel check is generated into xproto.cc (and gen_xproto.py) rejecting the GetProperty value field when format is not 0/8/16/32.

Key insight
Validating format / 8u == sizeof(T) with integer division let out-of-range format values (e.g. 39) slip through, after which byte_len was computed from the untrusted format while the buffer was sized from sizeof(T); the fix pins format to {8,16,32} and derives byte_len from sizeof(T).

Attack Path

  1. Control the GetProperty reply A malicious or compromised X server (or attacker-controlled window-property data) returns a GetProperty reply to Chrome’s X11 code with a crafted format and value_len.
  2. Bypass the weak check The reply sets format = 39; the old format / 8u != sizeof(T) test (for sizeof(T) == 4) computes 4, matches, and does not reject the reply.
  3. Mismatched length vs allocation The handler resizes the output to value_len elements (value_len * sizeof(T) bytes) but computes byte_len = value_len * 39 / 8, which is larger.
  4. OOB read and write via `memcpy` The UNSAFE_TODO(memcpy(...)) copies byte_len bytes, reading beyond the reply’s value buffer and writing beyond the destination vector, the out-of-bounds read and write the advisory describes.

Impact Assessment

An attacker controlling X11 GetProperty replies could trigger an out-of-bounds read and write in Chrome’s GFX X11 code via a malformed format field; the advisory rates this medium and classifies it as an out-of-bounds read and write in GFX. The precondition is the ability to supply crafted X server replies/window-property data to the browser’s X11 connection. The advisory does not report in-the-wild exploitation, and while an OOB write is memory corruption, the patch does not demonstrate controlled code execution (inferred).

Changed Functions

FunctionChangeNotes
if
ui/gfx/x/connection.h
modified
TEST
ui/gfx/x/connection_unittest.cc
modified
if
ui/gfx/x/generated_protos/xproto.cc
modified

Files Changed

  • ui/gfx/x/connection.h
  • ui/gfx/x/connection_unittest.cc
  • ui/gfx/x/gen_xproto.py
  • ui/gfx/x/generated_protos/xproto.cc

Audit Directions

  • Enum/format fields used in size math
    Audit code that multiplies an untrusted width/format field into a byte length; validate against the exact allowed set, not a derived/divided value.
  • Allocation vs copy-length mismatch
    Look for resize(N) paired with a memcpy whose length is computed from a different untrusted quantity than the allocation.
  • Integer-division validation
    Flag comparisons like x / k == expected on attacker input, which accept a whole range of x; the X11, Wayland, and other wire-protocol readers are prime candidates.
From 6c8a8e8cee9925e1a404b606b7e1f58d6b60e5e2 Mon Sep 17 00:00:00 2001
From: Tom Anderson <[email protected]>
Date: Mon, 06 Apr 2026 11:50:12 -0700
Subject: [PATCH] x11: Validate GetProperty format and fix OOB access

R=thestig

Change-Id: I8426be3b2246375cc2cdff5fbaa2d9009d05020d
Fixed: 496259890
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7731969
Reviewed-by: Lei Zhang <[email protected]>
Auto-Submit: Thomas Anderson <[email protected]>
Commit-Queue: Lei Zhang <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1610384}
---

diff --git a/ui/gfx/x/connection.h b/ui/gfx/x/connection.h
index 766bd3f..7f2d9047 100644
--- a/ui/gfx/x/connection.h
+++ b/ui/gfx/x/connection.h
@@ -372,11 +372,14 @@
                 .long_length = static_cast<uint32_t>(
                     amount ? length : std::numeric_limits<lentype>::max())})
             .Sync();
-    if (!response || response->format / 8u != sizeof(T)) {
+    if (!response ||
+        (response->format != 8 && response->format != 16 &&
+         response->format != 32) ||
+        response->format / 8u != sizeof(T)) {
       return false;
     }
 
-    size_t byte_len = response->value_len * response->format / 8u;
+    size_t byte_len = response->value_len * sizeof(T);
     value->resize(response->value_len);
     if (byte_len > 0u) {
       UNSAFE_TODO(memcpy(value->data(), response->value->bytes(), byte_len));
diff --git a/ui/gfx/x/connection_unittest.cc b/ui/gfx/x/connection_unittest.cc
index 9116707..fc451c4f 100644
--- a/ui/gfx/x/connection_unittest.cc
+++ b/ui/gfx/x/connection_unittest.cc
@@ -123,4 +123,36 @@
   connection.QueryTree(root).Sync();
 }
 
+TEST(X11ConnectionTest, GetPropertyReplyValidation) {
+  // Simulate a malicious response with format 39.
+  // The reply length is in 4-byte units, starting from after the first 32
+  // bytes. A GetProperty reply has a fixed size of 32 bytes followed by the
+  // value.
+  std::vector<uint8_t> data(32, 0);
+  data[0] = 1;   // response_type: Reply
+  data[1] = 39;  // format: 39 (Invalid, should be 8, 16, or 32)
+  data[10] = 0;  // length: 0
+  data[11] = 0;
+
+  ReadBuffer buf(x11::ThrowAwaySizeRefCountedMemory::From(std::move(data)));
+  auto reply = detail::ReadReply<GetPropertyReply>(&buf);
+  EXPECT_FALSE(reply);
+}
+
+TEST(X11ConnectionTest, GetPropertyReplyValid) {
+  // Simulate a valid response with format 32.
+  std::vector<uint8_t> data(32, 0);
+  data[0] = 1;   // response_type: Reply
+  data[1] = 32;  // format: 32
+  data[10] = 0;  // length: 0
+  data[11] = 0;
+  data[16] = 0;  // type: None
+  data[24] = 0;  // value_len: 0
+
+  ReadBuffer buf(x11::ThrowAwaySizeRefCountedMemory::From(std::move(data)));
+  auto reply = detail::ReadReply<GetPropertyReply>(&buf);
+  ASSERT_TRUE(reply);
+  EXPECT_EQ(reply->format, 32);
+}
+
 }  // namespace x11
diff --git a/ui/gfx/x/gen_xproto.py b/ui/gfx/x/gen_xproto.py
index 92a4338..6043a8c 100644
--- a/ui/gfx/x/gen_xproto.py
+++ b/ui/gfx/x/gen_xproto.py
@@ -613,6 +613,13 @@
 
         if t.is_ref_counted_memory:
             if self.is_read:
+                if name == 'value' and field.parent and field.parent[1] == (
+                        'xcb', 'GetProperty'):
+                    with Indent(
+                            self,
+                            'if (format != 0 && format != 8 && format != 16 && format != 32) {',
+                            '}'):
+                        self.write('return nullptr;')
                 self.write('%s = buffer->ReadAndAdvance(%s);' % (name, size))
             elif t.is_sized:
                 self.write('buf.AppendSizedBuffer(%s);' % (name))
diff --git a/ui/gfx/x/generated_protos/xproto.cc b/ui/gfx/x/generated_protos/xproto.cc
index d5e2ce0..c2d7de4 100644
--- a/ui/gfx/x/generated_protos/xproto.cc
+++ b/ui/gfx/x/generated_protos/xproto.cc
@@ -4276,6 +4276,9 @@
   Pad(&buf, 12);
 
   // value
+  if (format != 0 && format != 8 && format != 16 && format != 32) {
+    return nullptr;
+  }
   value = buffer->ReadAndAdvance((value_len) * ((format) / (8)));
 
   Align(&buf, 4);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ui/gfx/x/connection_unittest.cc b/ui/gfx/x/connection_unittest.cc
index 9116707..fc451c4f 100644
--- a/ui/gfx/x/connection_unittest.cc
+++ b/ui/gfx/x/connection_unittest.cc
@@ -123,4 +123,36 @@
   connection.QueryTree(root).Sync();
 }
 
+TEST(X11ConnectionTest, GetPropertyReplyValidation) {
+  // Simulate a malicious response with format 39.
+  // The reply length is in 4-byte units, starting from after the first 32
+  // bytes. A GetProperty reply has a fixed size of 32 bytes followed by the
+  // value.
+  std::vector<uint8_t> data(32, 0);
+  data[0] = 1;   // response_type: Reply
+  data[1] = 39;  // format: 39 (Invalid, should be 8, 16, or 32)
+  data[10] = 0;  // length: 0
+  data[11] = 0;
+
+  ReadBuffer buf(x11::ThrowAwaySizeRefCountedMemory::From(std::move(data)));
+  auto reply = detail::ReadReply<GetPropertyReply>(&buf);
+  EXPECT_FALSE(reply);
+}
+
+TEST(X11ConnectionTest, GetPropertyReplyValid) {
+  // Simulate a valid response with format 32.
+  std::vector<uint8_t> data(32, 0);
+  data[0] = 1;   // response_type: Reply
+  data[1] = 32;  // format: 32
+  data[10] = 0;  // length: 0
+  data[11] = 0;
+  data[16] = 0;  // type: None
+  data[24] = 0;  // value_len: 0
+
+  ReadBuffer buf(x11::ThrowAwaySizeRefCountedMemory::From(std::move(data)));
+  auto reply = detail::ReadReply<GetPropertyReply>(&buf);
+  ASSERT_TRUE(reply);
+  EXPECT_EQ(reply->format, 32);
+}
+
 }  // namespace x11
Loading diff…

Original Bug Report

reported by [email protected]

Heap OOB Read/Write in X11 GetArrayProperty via Arithmetic Mismatch

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: An arithmetic mismatch in X11 property calculations between the generated parser and the GetArrayProperty consumer can lead to a heap out-of-bounds read and write. A malicious X server can return a crafted format value, causing the consumer to calculate a larger copy size than the allocated buffer. This allows an attacker to perform a controlled OOB memory corruption in the highly-privileged browser process.

Affected files:

  • ui/gfx/x/connection.h
  • ui/gfx/x/generated_protos/xproto.cc

Estimated timestamp from git blame: 2025-04-21

Summary

There is a potential heap out-of-bounds (OOB) write and read in Chrome’s X11 property handling logic within ui/gfx/x. The issue stems from a discrepancy in how the byte length of property data is calculated between the auto-generated protocol parser and the GetArrayProperty template.

Note: This report was generated by our setup through static code analysis. The steps below are theoretical and a working Proof-of-Concept has not yet been executed.

Technical Details

The vulnerability is caused by an integer arithmetic mismatch when calculating the byte length of an X11 property value based on its format (bits per item) and value_len (number of items) fields.

1. Parser Logic (ui/gfx/x/generated_protos/xproto.cc): When reading a GetPropertyReply, the auto-generated parser advances its read buffer using the following calculation (line 4279):

value = buffer->ReadAndAdvance((value_len) * ((format) / (8)));

Due to the parentheses, integer division of format by 8 occurs first, truncating any remainder. For a non-standard format like 39, 39 / 8 evaluates to 4.

2. Consumer Logic (ui/gfx/x/connection.h): The GetArrayProperty template processes the reply and calculates the copy length (line 379):

size_t byte_len = response->value_len * response->format / 8u;

Here, multiplication occurs before division. For value_len=1000 and format=39, this evaluates as (1000 * 39) / 8 = 4875.

3. The OOB Copy (ui/gfx/x/connection.h): The consumer allocates a destination std::vector<T> based on value_len and performs an unsafe memcpy (lines 380-382):

value->resize(response->value_len);
if (byte_len > 0u) {
  UNSAFE_TODO(memcpy(value->data(), response->value->bytes(), byte_len));
}

If T=uint32_t, the vector allocates 1000 * 4 = 4000 bytes. The memcpy then attempts to copy 4875 bytes. This reads 875 bytes past the end of the X11 reply buffer (OOB read) and writes 875 bytes past the end of the vector’s backing store (OOB write).

Potential Attacker Steps

To trigger this vulnerability, an attacker would need to control the X11 server the victim connects to (e.g., via malicious local X server, X11 forwarding over SSH, or a compromised proxy).

  1. The attacker sets up a malicious X11 server.
  2. The victim launches Chrome, which connects to the X display.
  3. Chrome requests a standard property, such as WM_NORMAL_HINTS, which invokes GetArrayProperty<uint32_t>.
  4. The malicious server responds to the GetPropertyRequest with:
    • format = 39
    • value_len = 1000
    • length = 1000 (to satisfy the parser’s expected 4000-byte payload length)
  5. The parser successfully processes the 4000-byte payload.
  6. GetArrayProperty evaluates the guard response->format / 8u != sizeof(T). Since 39 / 8 == 4 == sizeof(uint32_t), the check passes.
  7. byte_len is calculated as 4875. The memcpy reads 4875 bytes from the 4000-byte source and writes 4875 bytes to the 4000-byte vector, causing a heap OOB read and write.

Suggested Fix

  1. Strict Format Validation: The most robust fix is to strictly validate that the format field provided by the X server is a standard value (i.e., exactly 8, 16, or 32). If a non-standard format is encountered, the reply should be rejected early.
  2. Consistent Arithmetic: Update GetArrayProperty to calculate the copy size exactly as the parser does, or derive it directly from the allocated sizeof(T):
    size_t byte_len = response->value_len * sizeof(T);
    

Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. Please feel free to reach out to me if you have concerns or feedback.

View on issue tracker