CVE-2026-11160
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forui/events/devices/x11/device_data_manager_x11.cc |
modified | |
ifui/events/devices/x11/device_data_manager_x11.cc |
modified | |
whileui/events/devices/x11/device_data_manager_x11.cc |
modified |
Files Changed
remoting/host/input_monitor/local_input_monitor_x11_common.ccui/base/x/x11_user_input_monitor.ccui/events/devices/x11/device_data_manager_x11.ccui/events/devices/x11/touch_factory_x11.cc
Patch
From fb19e42e9bcf7a8a675e8691ee6f2de9792b8718 Mon Sep 17 00:00:00 2001 From: Andrew Paseltiner <[email protected]> Date: Tue, 14 Apr 2026 17:07:26 -0700 Subject: [PATCH] Fix heap out-of-bounds read in DeviceDataManagerX11 Refactor XInput mask utility functions to use base::span for safer memory access and explicit bounds checking. Previously, these functions used raw pointers without size information, leading to potential out-of-bounds reads when processing truncated XInput event masks sent by the X server. Specific changes: - Update SetXinputMask and IsXinputMaskSet in xinput_util.h to accept base::span<uint8_t> and base::span<const uint8_t>. - Use base::as_byte_span, base::as_writable_byte_span, or base::byte_span_from_ref at all call sites (including DeviceDataManagerX11, TouchFactory, and various test utilities) to pass masks safely. - Add unit tests in xinput_util_unittest.cc verifying safe bounds handling for both IsXinputMaskSet and SetXinputMask, using EXPECT_DEATH_IF_SUPPORTED for the latter. - Add regression test in device_data_manager_x11_unittest.cc for GetEventData with truncated masks. Fixed: 501862016 Change-Id: Ia68255583bdaf944e786dea2a64cde937e761e4c Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7759573 Reviewed-by: Thomas Anderson <[email protected]> Reviewed-by: Joe Downing <[email protected]> Commit-Queue: Andrew Paseltiner <[email protected]> Reviewed-by: Jonathan Ross <[email protected]> Cr-Commit-Position: refs/heads/main@{#1614784} --- diff --git a/remoting/host/input_monitor/local_input_monitor_x11_common.cc b/remoting/host/input_monitor/local_input_monitor_x11_common.cc index 8ae0ad0..cc40c32 100644 --- a/remoting/host/input_monitor/local_input_monitor_x11_common.cc +++ b/remoting/host/input_monitor/local_input_monitor_x11_common.cc @@ -4,13 +4,18 @@ #include "remoting/host/input_monitor/local_input_monitor_x11_common.h" +#include "base/containers/span.h" + namespace remoting { x11::Input::XIEventMask CommonXIEventMaskForRootWindow() { x11::Input::XIEventMask mask{}; - ui::SetXinputMask(&mask, x11::Input::RawDeviceEvent::RawKeyPress); - ui::SetXinputMask(&mask, x11::Input::RawDeviceEvent::RawKeyRelease); - ui::SetXinputMask(&mask, x11::Input::RawDeviceEvent::RawMotion); + ui::SetXinputMask(base::byte_span_from_ref(mask), + x11::Input::RawDeviceEvent::RawKeyPress); + ui::SetXinputMask(base::byte_span_from_ref(mask), + x11::Input::RawDeviceEvent::RawKeyRelease); + ui::SetXinputMask(base::byte_span_from_ref(mask), + x11::Input::RawDeviceEvent::RawMotion); return mask; } diff --git a/ui/base/x/x11_user_input_monitor.cc b/ui/base/x/x11_user_input_monitor.cc index 9cd9b69..0d02cbe7 100644 --- a/ui/base/x/x11_user_input_monitor.cc +++ b/ui/base/x/x11_user_input_monitor.cc @@ -4,6 +4,7 @@ #include "ui/base/x/x11_user_input_monitor.h" +#include "base/containers/span.h" #include "base/logging.h" #include "base/task/single_thread_task_runner.h" #include "ui/events/devices/x11/xinput_util.h" @@ -78,8 +79,10 @@ } x11::Input::XIEventMask mask{}; - SetXinputMask(&mask, x11::Input::RawDeviceEvent::RawKeyPress); - SetXinputMask(&mask, x11::Input::RawDeviceEvent::RawKeyRelease); + SetXinputMask(base::byte_span_from_ref(mask), + x11::Input::RawDeviceEvent::RawKeyPress); + SetXinputMask(base::byte_span_from_ref(mask), + x11::Input::RawDeviceEvent::RawKeyRelease); connection_->xinput().XISelectEvents( {connection_->default_root(), {{x11::Input::DeviceId::AllMaster, {mask}}}}); diff --git a/ui/events/devices/x11/device_data_manager_x11.cc b/ui/events/devices/x11/device_data_manager_x11.cc index 0447e08d6..0fd92a2 100644 --- a/ui/events/devices/x11/device_data_manager_x11.cc +++ b/ui/events/devices/x11/device_data_manager_x11.cc @@ -13,6 +13,7 @@ #include "base/at_exit.h" #include "base/command_line.h" #include "base/compiler_specific.h" +#include "base/containers/span.h" #include "base/functional/bind.h" #include "base/functional/callback_helpers.h" #include "base/logging.h" @@ -288,7 +289,7 @@ data->clear(); auto valuators_iter = xiev->axisvalues.begin(); for (int i = 0; i <= valuator_count_[sourceid]; ++i) { - if (IsXinputMaskSet(xiev->valuator_mask.data(), i)) { + if (IsXinputMaskSet(base::as_byte_span(xiev->valuator_mask), i)) { int type = data_type_lookup_[sourceid][i]; if (type != DT_LAST_ENTRY) { double valuator = Fp3232ToDouble(*valuators_iter); @@ -337,11 +338,13 @@ int val_index = valuator_lookup_[sourceid][type].number; int slot = 0; if (val_index >= 0) { - if (IsXinputMaskSet(xiev->valuator_mask.data(), val_index)) { + if (IsXinputMaskSet(base::as_byte_span(xiev->valuator_mask), val_index)) { auto valuators_iter = xiev->axisvalues.begin(); while (val_index--) { - if (IsXinputMaskSet(xiev->valuator_mask.data(), val_index)) + if (IsXinputMaskSet(base::as_byte_span(xiev->valuator_mask), + val_index)) { ++valuators_iter; + } } *value = Fp3232ToDouble(*valuators_iter); if (IsTouchDataType(type)) { @@ -409,11 +412,13 @@ int horizontal_id = scroll_data_.at(sourceid).horizontal.number; int vertical_id = scroll_data_.at(sourceid).vertical.number; return (horizontal_id != -1 && - IsXinputMaskSet(xievent->valuator_mask.data(), horizontal_id) + IsXinputMaskSet(base::as_byte_span(xievent->valuator_mask), + horizontal_id) ? SCROLL_TYPE_HORIZONTAL : 0) | (vertical_id != -1 && - IsXinputMaskSet(xievent->valuator_mask.data(), vertical_id) + IsXinputMaskSet(base::as_byte_span(xievent->valuator_mask), + vertical_id) ? SCROLL_TYPE_VERTICAL : 0); } @@ -450,7 +455,8 @@ return false; } const int idx = valuator_lookup_.at(sourceid)[type].number; - return (idx >= 0) && IsXinputMaskSet(xiev->valuator_mask.data(), idx); + return (idx >= 0) && + IsXinputMaskSet(base::as_byte_span(xiev->valuator_mask), idx); } bool DeviceDataManagerX11::IsScrollEvent(const x11::Event& x11_event) const { @@ -539,8 +545,9 @@ const int vertical_number = info->vertical.number; for (int i = 0; i <= valuator_count_[sourceid]; ++i) { - if (!IsXinputMaskSet(xiev->valuator_mask.data(), i)) + if (!IsXinputMaskSet(base::as_byte_span(xiev->valuator_mask), i)) { continue; + } auto valuator = Fp3232ToDouble(*valuators_iter); if (i == horizontal_number) *x_offset = ExtractAndUpdateScrollOffset(&info->horizontal, valuator); @@ -704,14 +711,15 @@ double value) { auto device = devev->deviceid; int index = valuator_lookup_[device][type].number; - CHECK(!IsXinputMaskSet(devev->valuator_mask.data(), index)); + CHECK(!IsXinputMaskSet(base::as_byte_span(devev->valuator_mask), index)); CHECK(index >= 0 && index < valuator_count_[device]); - SetXinputMask(devev->valuator_mask.data(), index); + SetXinputMask(base::as_writable_byte_span(devev->valuator_mask), index); x11::Input::Fp3232* valuators = devev->axisvalues.data(); for (int i = 0; i < index; ++i) { - if (IsXinputMaskSet(devev->valuator_mask.data(), i)) + if (IsXinputMaskSet(base::as_byte_span(devev->valuator_mask), i)) { UNSAFE_TODO(valuators++); + } } for (int i = DT_LAST_ENTRY - 1; i > valuators - devev->axisvalues.data(); --i) { diff --git a/ui/events/devices/x11/touch_factory_x11.cc b/ui/events/devices/x11/touch_factory_x11.cc index b99a2b1..847d70d9 100644 --- a/ui/events/devices/x11/touch_factory_x11.cc +++ b/ui/events/devices/x11/touch_factory_x11.cc @@ -11,6 +11,7 @@ #include "base/command_line.h" #include "base/compiler_specific.h" +#include "base/containers/span.h" #include "base/logging.h" #include "base/memory/singleton.h" #include "base/strings/string_number_conversions.h" @@ -206,27 +207,27 @@ x11::Input::EventMask mask{x11::Input::DeviceId::AllMaster}; mask.mask.push_back({}); - auto* mask_data = mask.mask.data(); + auto mask_span = base::as_writable_byte_span(mask.mask); - SetXinputMask(mask_data, x11::Input::CrossingEvent::Enter); - SetXinputMask(mask_data, x11::Input::CrossingEvent::Leave);
Regression Test / PoC
diff --git a/ui/events/test/events_test_utils_x11.cc b/ui/events/test/events_test_utils_x11.cc
index ab6ba12c..cac3297 100644
--- a/ui/events/test/events_test_utils_x11.cc
+++ b/ui/events/test/events_test_utils_x11.cc
@@ -10,6 +10,7 @@
#include <vector>
#include "base/check_op.h"
+#include "base/containers/span.h"
#include "base/notreached.h"
#include "ui/events/devices/x11/touch_factory_x11.h"
#include "ui/events/devices/x11/xinput_util.h"
@@ -207,7 +208,8 @@
dev_event->detail = XButtonEventButton(type, flags);
dev_event->event_x = ToFp1616(location.x()),
dev_event->event_y = ToFp1616(location.y()),
- SetXinputMask(dev_event->button_mask.data(), XButtonEventButton(type, flags));
+ SetXinputMask(base::as_writable_byte_span(dev_event->button_mask),
+ XButtonEventButton(type, flags));
// Setup an empty valuator list for generic button events.
SetUpValuators(std::vector<Valuator>());
diff --git a/ui/ozone/platform/x11/test/device_data_manager_x11_unittest.cc b/ui/ozone/platform/x11/test/device_data_manager_x11_unittest.cc
index 15bbd53..648a69c 100644
--- a/ui/ozone/platform/x11/test/device_data_manager_x11_unittest.cc
+++ b/ui/ozone/platform/x11/test/device_data_manager_x11_unittest.cc
@@ -6,6 +6,7 @@
#include <vector>
+#include "base/containers/span.h"
#include "base/memory/raw_ptr.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/events/devices/device_hotplug_event_observer.h"
@@ -176,5 +177,35 @@
EXPECT_EQ(2u, devices.size());
}
+// Tests that GetEventData handles cases where the event's valuator_mask is
+// shorter than the registered valuator's index.
+// Regression test for crbug.com/501862016.
+TEST_F(DeviceDataManagerX11Test, GetEventDataShortMask) {
+ DeviceDataManagerX11* manager = DeviceDataManagerX11::GetInstance();
+ const auto device_id = static_cast<x11::Input::DeviceId>(1);
+
+ // Initialize a device with some valuators. SetDeviceListForTest will
+ // initialize them with indices starting from 0.
+ // Touch major/minor/orientation/pressure/x/y/tracking_id/raw_timestamp
+ // are 8 valuators (indices 0-7).
+ manager->SetDeviceListForTest({static_cast<int>(device_id)}, {}, {});
+
+ // Manually create an event with an empty mask.
+ x11::Event event(false, x11::Input::DeviceEvent{
+ .opcode = x11::Input::DeviceEvent::Motion,
+ .deviceid = device_id,
+ .sourceid = device_id,
+ .valuator_mask = {},
+ .axisvalues = {},
+ });
+
+ double value = -1.0;
+ // This should not crash and should return false because any index is OOB
+ // for an empty mask.
+ EXPECT_FALSE(manager->GetEventData(
+ event, DeviceDataManagerX11::DT_TOUCH_MAJOR, &value));
+ EXPECT_EQ(value, -1.0);
+}
+
} // namespace test
} // namespace ui
diff --git a/ui/ozone/platform/x11/test/xinput_util_unittest.cc b/ui/ozone/platform/x11/test/xinput_util_unittest.cc
new file mode 100644
index 0000000..07d56f84
--- /dev/null
+++ b/ui/ozone/platform/x11/test/xinput_util_unittest.cc
@@ -0,0 +1,70 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ui/events/devices/x11/xinput_util.h"
+
+#include <vector>
+
+#include "base/containers/span.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/gfx/x/xinput.h"
+
+namespace ui {
+
+// Regression test for crbug.com/501862016.
+TEST(XInputUtilTest, IsXinputMaskSetBoundsCheck) {
+ std::vector<uint32_t> mask = {1}; // 4 bytes, covering opcodes 0-31
+ auto mask_bytes = base::as_byte_span(mask);
+
+ // Opcode 0 is set (first bit of 1)
+ EXPECT_TRUE(IsXinputMaskSet(mask_bytes, 0));
+
+ // Opcode 1 is not set
+ EXPECT_FALSE(IsXinputMaskSet(mask_bytes, 1));
+
+ // Opcode 31 is not set
+ EXPECT_FALSE(IsXinputMaskSet(mask_bytes, 31));
+
+ // Opcode 32 is out of bounds for a 4-byte mask
+ EXPECT_FALSE(IsXinputMaskSet(mask_bytes, 32));
+
+ // Opcode 1000 is way out of bounds
+ EXPECT_FALSE(IsXinputMaskSet(mask_bytes, 1000));
+}
+
+// Regression test for crbug.com/501862016.
+TEST(XInputUtilTest, SetXinputMask) {
+ std::vector<uint32_t> mask = {0}; // 4 bytes, covering opcodes 0-31
+
+ // Setting opcode 0 should work
+ SetXinputMask(base::as_writable_byte_span(mask), 0);
+ EXPECT_EQ(mask[0], 1u);
+ EXPECT_TRUE(IsXinputMaskSet(base::as_byte_span(mask), 0));
+}
+
+// Regression test for crbug.com/501862016.
+TEST(XInputUtilTest, SetXinputMaskDeathTest) {
+ std::vector<uint32_t> mask = {0}; // 4 bytes, covering opcodes 0-31
+
+ // Setting opcode 32 (out of bounds) should CHECK/crash
+ EXPECT_DEATH_IF_SUPPORTED(
+ SetXinputMask(base::as_writable_byte_span(mask), 32), "");
+}
+
+// Regression test for crbug.com/501862016.
+TEST(XInputUtilTest, XIEventMaskSpan) {
+ x11::Input::XIEventMask xi_mask{};
+
+ // Initially nothing set
+ EXPECT_FALSE(IsXinputMaskSet(base::byte_span_from_ref(xi_mask), 1));
+
+ // Set some bit
+ SetXinputMask(base::byte_span_from_ref(xi_mask), 1);
+ EXPECT_TRUE(IsXinputMaskSet(base::byte_span_from_ref(xi_mask), 1));
+
+ // Out of bounds for int-sized mask (usually 32 bits)
+ EXPECT_FALSE(IsXinputMaskSet(base::byte_span_from_ref(xi_mask), 100));
+}
+
+} // namespace ui
Original Bug Report
Potential Heap OOB Read and Info Leak in DeviceDataManagerX11
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team.
Overview: A potential heap out-of-bounds read exists in DeviceDataManagerX11 when processing XInput2 events on Linux. The code fails to bounds-check the valuator_mask size when iterating over valuators, allowing a malicious website to potentially leak 8 bytes of browser heap memory via JavaScript pointer events. This can be triggered during normal user interaction with a multi-touch device and does not require a compromised X server.
Affected files:
ui/events/devices/x11/device_data_manager_x11.ccui/events/x/events_x_utils.cc
Estimated timestamp from git blame: 2025-11-14
Summary
A potential heap out-of-bounds (OOB) read vulnerability exists in DeviceDataManagerX11 within the browser process. The issue stems from a lack of bounds checking when processing valuator data in X11 input events. Specifically, the code assumes the valuator_mask array provided by the X server is large enough to cover the highest possible valuator index registered for the device. However, XInput2 servers legitimately optimize this mask to only cover the valuators that were actually updated in a specific event. This mismatch allows an attacker to read out-of-bounds heap memory, which is then exposed to web content through standard event properties like radiusX.
Technical Details
- Device Initialization: When a multi-touch device is initialized,
DeviceDataManagerX11::UpdateValuatorClassDevicemaps specific data types (likeDT_TOUCH_MAJOR) to their hardware valuator indices. Advanced touch features often have high indices (e.g., 33). - Event Parsing: When the user moves their finger, the X server sends an
XI_Motionevent. If only low-indexed axes (like X and Y, indices 0 and 1) are updated, the server optimizes thevaluator_mask, sending a short array (e.g., 4 bytes, covering indices 0-31). - Vulnerable Access: To populate the touch radius,
ui::GetTouchRadiusXFromXEventcallsDeviceDataManagerX11::GetEventData(..., DT_TOUCH_MAJOR, ...). Indevice_data_manager_x11.cc:340, the code checks if the valuator is present by callingIsXinputMaskSet(xiev->valuator_mask.data(), val_index). Let’s assumeval_indexis 33. - OOB Read (Mask):
IsXinputMaskSet(xinput_util.h:24) calculates the byte offset directly (33 / 8 = 4) and performs an unchecked access. Since the mask is only 4 bytes long, this reads the first byte of heap memory immediately following thevaluator_maskvector. - Iterator Overrun: If that OOB heap byte happens to have the corresponding bit set (due to normal heap noise),
GetEventDataenters a loop (lines 342-345) to advancevaluators_iterfor every set bit up toval_index. Because it counts the OOB bit, it increments the iterator pastxiev->axisvalues.end(), asaxisvaluesis strictly sized to the valid mask. - OOB Read (Value): At line 346,
*value = Fp3232ToDouble(*valuators_iter);is executed. This performs an 8-byte OOB read from the heap following theaxisvaluesvector. - Information Leak: This leaked memory is converted to a highly precise
double, stored inPointerDetails::radius_x, and sent to the renderer, where it is accessible viaevent.radiusX.
Potential Steps to Trigger (Suggested)
- A victim using Chromium on Linux/X11 with a multi-touch device visits an attacker-controlled website.
- The website registers event listeners for
touchmoveorpointermoveand repeatedly readsevent.radiusXandevent.radiusY. - The victim normally interacts with the page (e.g., scrolling or moving their finger).
- The X server generates optimized motion events with short valuator masks.
- The OOB read is triggered naturally. By observing the variations in the
radiusXvalues over time, the attacker can recover browser heap data, potentially bypassing ASLR.
Note: Our analysis tooling cannot run code, so these are potential steps derived from source code analysis.
Suggested Fix
Add bounds checking to IsXinputMaskSet or ensure that val_index is validated against the actual size of the incoming valuator_mask before performing the bitwise check.
// ui/events/devices/x11/xinput_util.h
inline bool IsXinputMaskSet(const std::vector<uint32_t>& mask, unsigned int opcode) {
size_t byte_index = opcode / 8;
if (byte_index >= mask.size() * sizeof(uint32_t)) {
return false;
}
const auto bit = 1 << (opcode & 7);
return UNSAFE_TODO(reinterpret_cast<const uint8_t*>(mask.data())[byte_index]) & bit;
}
All usages of IsXinputMaskSet should be updated to pass the vector or its size alongside the data pointer.
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
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. And please feel free to reach out to me directly if you have concerns or feedback on the project.