CVE-2026-7950
Overview
Background
- X11 `GetProperty`
- an X protocol request whose reply carries a window-property value plus a
formatfield that must be 8, 16, or 32 (bits per element) and avalue_lencount. - `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
memcpyfrom the reply’svalue->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.
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
- Control the GetProperty reply
A malicious or compromised X server (or attacker-controlled window-property data) returns a
GetPropertyreply to Chrome’s X11 code with a craftedformatandvalue_len. - Bypass the weak check
The reply sets
format = 39; the oldformat / 8u != sizeof(T)test (forsizeof(T) == 4) computes4, matches, and does not reject the reply. - Mismatched length vs allocation
The handler resizes the output to
value_lenelements (value_len * sizeof(T)bytes) but computesbyte_len = value_len * 39 / 8, which is larger. - OOB read and write via `memcpy`
The
UNSAFE_TODO(memcpy(...))copiesbyte_lenbytes, 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
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
| Function | Change | Notes |
|---|---|---|
ifui/gfx/x/connection.h |
modified | |
TESTui/gfx/x/connection_unittest.cc |
modified | |
ifui/gfx/x/generated_protos/xproto.cc |
modified |
Files Changed
ui/gfx/x/connection.hui/gfx/x/connection_unittest.ccui/gfx/x/gen_xproto.pyui/gfx/x/generated_protos/xproto.cc
Audit Directions
- Enum/format fields used in size mathAudit 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 mismatchLook for
resize(N)paired with amemcpywhose length is computed from a different untrusted quantity than the allocation. - Integer-division validationFlag comparisons like
x / k == expectedon attacker input, which accept a whole range ofx; the X11, Wayland, and other wire-protocol readers are prime candidates.
Patch
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);
Regression Test / PoC
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
Original Bug Report
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.hui/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).
- The attacker sets up a malicious X11 server.
- The victim launches Chrome, which connects to the X display.
- Chrome requests a standard property, such as
WM_NORMAL_HINTS, which invokesGetArrayProperty<uint32_t>. - The malicious server responds to the
GetPropertyRequestwith:format = 39value_len = 1000length = 1000(to satisfy the parser’s expected 4000-byte payload length)
- The parser successfully processes the 4000-byte payload.
GetArrayPropertyevaluates the guardresponse->format / 8u != sizeof(T). Since39 / 8 == 4 == sizeof(uint32_t), the check passes.byte_lenis calculated as 4875. Thememcpyreads 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
- Strict Format Validation: The most robust fix is to strictly validate that the
formatfield provided by the X server is a standard value (i.e., exactly8,16, or32). If a non-standard format is encountered, the reply should be rejected early. - Consistent Arithmetic: Update
GetArrayPropertyto calculate the copy size exactly as the parser does, or derive it directly from the allocatedsizeof(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.