CVE-2026-113003
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fservices/device/hid/hid_connection_impl_unittest.cc |
modified | |
TEST_Fservices/device/hid/hid_connection_unittest.cc |
modified |
Files Changed
services/device/public/cpp/device_features.hservices/device/public/cpp/device_features.ccservices/device/hid/hid_connection.ccservices/device/hid/hid_connection_unittest.ccservices/device/hid/hid_connection_impl_unittest.cc
Patch
From 584159394114a18259b1af8327df3868b36b22ac Mon Sep 17 00:00:00 2001
From: Alvin Ji <alvinji@chromium.org>
Date: Mon, 14 Sep 2026 17:09:22 -0700
Subject: [PATCH] hid: Validate feature report buffer size in HidConnection
HidConnection::Write validates that the provided buffer size does not
exceed the device's declared maximum output report size plus the report
ID prefix. However, HidConnection::SendFeatureReport was missing an
equivalent check against the maximum feature report size.
This change adds a check in HidConnection::SendFeatureReport to verify
that the buffer size does not exceed max_feature_report_size() + 1 before
forwarding the report to platform-specific implementations.
TAG=agy
CONV=afacb408-897a-4dba-bf05-3be896e28c31
Bug: 505024593
Change-Id: I961309c62a7524c525b2bf563a015377f4361541
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8400910
Commit-Queue: Alvin Ji <alvinji@chromium.org>
Reviewed-by: Matt Reynolds <mattreynolds@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1697654}
---
diff --git a/services/device/hid/hid_connection.cc b/services/device/hid/hid_connection.cc
index 7fa17b8f..179e174d 100644
--- a/services/device/hid/hid_connection.cc
+++ b/services/device/hid/hid_connection.cc
@@ -139,6 +139,14 @@
std::move(callback).Run(false);
return;
}
+ if (base::FeatureList::IsEnabled(features::kHidValidateFeatureReportSize) &&
+ buffer->size() > device_info_->max_feature_report_size() + 1) {
+ HID_LOG(USER) << "Feature report buffer too long (" << buffer->size()
+ << " > " << (device_info_->max_feature_report_size() + 1)
+ << ").";
+ std::move(callback).Run(false);
+ return;
+ }
DCHECK_GE(buffer->size(), 1u);
uint8_t report_id = buffer->data()[0];
if (device_info_->has_report_id() != (report_id != 0)) {
diff --git a/services/device/public/cpp/device_features.cc b/services/device/public/cpp/device_features.cc
index 4fa3c767..ee538fca 100644
--- a/services/device/public/cpp/device_features.cc
+++ b/services/device/public/cpp/device_features.cc
@@ -133,6 +133,11 @@
// security bypasses (e.g., nested keyboards or FIDO keys).
BASE_FEATURE(kWebHidRecursiveFiltering, base::FEATURE_ENABLED_BY_DEFAULT);
+// When enabled, feature reports are checked to verify that the buffer size does
+// not exceed the device's maximum feature report size plus the report ID
+// prefix.
+BASE_FEATURE(kHidValidateFeatureReportSize, base::FEATURE_ENABLED_BY_DEFAULT);
+
const base::FeatureParam<device::mojom::LocationProviderManagerMode>::Option
location_provider_manager_mode_options[] = {
{device::mojom::LocationProviderManagerMode::kNetworkOnly,
diff --git a/services/device/public/cpp/device_features.h b/services/device/public/cpp/device_features.h
index e2c1876..16e92673 100644
--- a/services/device/public/cpp/device_features.h
+++ b/services/device/public/cpp/device_features.h
@@ -55,6 +55,7 @@
#endif // !BUILDFLAG(IS_ANDROID)
DEVICE_FEATURES_EXPORT BASE_DECLARE_FEATURE(kWebHidRecursiveFiltering);
+DEVICE_FEATURES_EXPORT BASE_DECLARE_FEATURE(kHidValidateFeatureReportSize);
extern const DEVICE_FEATURES_EXPORT
base::FeatureParam<device::mojom::LocationProviderManagerMode>
diff --git a/services/device/hid/hid_connection_impl_unittest.cc b/services/device/hid/hid_connection_impl_unittest.cc
index 6c44be1..1b716c9 100644
--- a/services/device/hid/hid_connection_impl_unittest.cc
+++ b/services/device/hid/hid_connection_impl_unittest.cc
@@ -8,12 +8,14 @@
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted_memory.h"
+#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "build/build_config.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "services/device/device_service_test_base.h"
+#include "services/device/public/cpp/device_features.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -375,4 +377,64 @@
EXPECT_THAT(feature_buffer->as_vector(), ElementsAre(kTestReportId));
}
+TEST_F(HidConnectionImplTest, WriteOutputReportBufferTooLong) {
+ auto hid_connection = CreateHidConnection(/*with_connection_client=*/false);
+ EXPECT_CALL(mock_connection(), PlatformWrite).Times(0);
+ WriteFuture write_future;
+ hid_connection->Write(
+ kTestReportId,
+ /*buffer=*/std::vector<uint8_t>(kMaxReportSizeBytes + 1, 0x42),
+ write_future.GetCallback());
+ EXPECT_FALSE(write_future.Get());
+}
+
+TEST_F(HidConnectionImplTest, SendFeatureReport) {
+ auto hid_connection = CreateHidConnection(/*with_connection_client=*/false);
+ scoped_refptr<base::RefCountedBytes> feature_buffer;
+ EXPECT_CALL(mock_connection(), PlatformSendFeatureReport)
+ .WillOnce([&feature_buffer](scoped_refptr<base::RefCountedBytes> buffer,
+ HidConnectionImpl::WriteCallback callback) {
+ feature_buffer = buffer;
+ std::move(callback).Run(/*success=*/true);
+ });
+ WriteFuture write_future;
+ std::vector<uint8_t> buffer(kMaxReportSizeBytes, 0x42);
+ hid_connection->SendFeatureReport(kTestReportId, buffer,
+ write_future.GetCallback());
+ EXPECT_TRUE(write_future.Get());
+ ASSERT_TRUE(feature_buffer);
+ EXPECT_EQ(feature_buffer->size(), kMaxReportSizeBytes + 1);
+ EXPECT_EQ(feature_buffer->as_vector()[0], kTestReportId);
+}
+
+TEST_F(HidConnectionImplTest, SendFeatureReportBufferTooLong) {
+ auto hid_connection = CreateHidConnection(/*with_connection_client=*/false);
+ EXPECT_CALL(mock_connection(), PlatformSendFeatureReport).Times(0);
+ WriteFuture write_future;
+ hid_connection->SendFeatureReport(
+ kTestReportId,
+ /*buffer=*/std::vector<uint8_t>(kMaxReportSizeBytes + 1, 0x42),
+ write_future.GetCallback());
+ EXPECT_FALSE(write_future.Get());
+}
+
+TEST_F(HidConnectionImplTest, SendFeatureReportBufferTooLongFeatureDisabled) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndDisableFeature(
+ features::kHidValidateFeatureReportSize);
+
+ auto hid_connection = CreateHidConnection(/*with_connection_client=*/false);
+ EXPECT_CALL(mock_connection(), PlatformSendFeatureReport)
+ .WillOnce([](scoped_refptr<base::RefCountedBytes> buffer,
+ HidConnection::WriteCallback callback) {
+ std::move(callback).Run(/*success=*/true);
+ });
+ WriteFuture write_future;
+ hid_connection->SendFeatureReport(
+ kTestReportId,
+ /*buffer=*/std::vector<uint8_t>(kMaxReportSizeBytes + 1, 0x42),
+ write_future.GetCallback());
+ EXPECT_TRUE(write_future.Get());
+}
+
} // namespace device
diff --git a/services/device/hid/hid_connection_unittest.cc b/services/device/hid/hid_connection_unittest.cc
index 2bd2e5c..3f6c733 100644
--- a/services/device/hid/hid_connection_unittest.cc
+++ b/services/device/hid/hid_connection_unittest.cc
@@ -746,4 +746,238 @@
}
}
+TEST_F(HidConnectionProtectedReportTest, OutputReportBufferTooLong) {
+ constexpr uint8_t kTestReportId = 1;
+ constexpr size_t kMaxOutputReportSize = 2;
+ auto collection = mojom::HidCollectionInfo::New();
+ collection->usage = mojom::HidUsageAndPage::New(mojom::kGenericDesktopGamePad,
+ mojom::kPageGenericDesktop);
+ collection->collection_type = mojom::kHIDCollectionTypeApplication;
+ collection->report_ids.push_back(kTestReportId);
+ auto report = mojom::HidReportDescription::New();
+ report->report_id = kTestReportId;
+ collection->output_reports.push_back(std::move(report));
+
+ auto device_info =
+ CreateHidDeviceInfo(std::move(collection),
+ /*max_input_report_size=*/0, kMaxOutputReportSize,
+ /*max_feature_report_size=*/0);
+ CreateConnection(device_info);
+
+ // A buffer matching the maximum report size (1 byte report ID + 2 bytes data)
+ // succeeds.
+ {
+ TestFuture<bool> write_future;
+ auto valid_buffer = base::MakeRefCounted<base::RefCountedBytes>(
+ std::vector<uint8_t>{kTestReportId, 0x01, 0x02});
+ connection().Write(valid_buffer, write_future.GetCallback());
+ EXPECT_TRUE(write_future.Get());
+ }
+
+ // A buffer exceeding the maximum report size (1 byte report ID + 3 bytes
+ // data) fails.
+ {
+ TestFuture<bool> write_future;
+ auto long_buffer = base::MakeRefCounted<base::RefCountedBytes>(
+ std::vector<uint8_t>{kTestReportId, 0x01, 0x02, 0x03});
+ connection().Write(long_buffer, write_future.GetCallback());
+ EXPECT_FALSE(write_future.Get());
+ }
+
+ connection().Close();
+}
+
+TEST_F(HidConnectionProtectedReportTest,
+ OutputReportBufferTooLongWithoutReportId) {
+ constexpr size_t kMaxOutputReportSize = 2;
+ auto collection = mojom::HidCollectionInfo::New();
+ collection->usage = mojom::HidUsageAndPage::New(mojom::kGenericDesktopGamePad,
+ mojom::kPageGenericDesktop);
+ collection->collection_type = mojom::kHIDCollectionTypeApplication;
+ auto report = mojom::HidReportDescription::New();
+ report->report_id = 0;
+ collection->output_reports.push_back(std::move(report));
+
+ auto device_info =
+ CreateHidDeviceInfo(std::move(collection),
+ /*max_input_report_size=*/0, kMaxOutputReportSize,
+ /*max_feature_report_size=*/0);
+ CreateConnection(device_info);
+
+ // A buffer matching the maximum report size (1 byte report ID 0 + 2 bytes
+ // data) succeeds.
+ {
+ TestFuture<bool> write_future;
+ auto valid_buffer = base::MakeRefCounted<base::RefCountedBytes>(
+ std::vector<uint8_t>{0, 0x01, 0x02});
+ connection().Write(valid_buffer, write_future.GetCallback());
+ EXPECT_TRUE(write_future.Get());
+ }
+
+ // A buffer exceeding the maximum report size (1 byte report ID 0 + 3 bytes
+ // data) fails.
+ {
+ TestFuture<bool> write_future;
+ auto long_buffer = base::MakeRefCounted<base::RefCountedBytes>(
+ std::vector<uint8_t>{0, 0x01, 0x02, 0x03});
+ connection().Write(long_buffer, write_future.GetCallback());
+ EXPECT_FALSE(write_future.Get());
+ }
+
+ connection().Close();
+}
+
+TEST_F(HidConnectionProtectedReportTest, FeatureReportBufferTooLong) {
+ constexpr uint8_t kTestReportId = 1;
+ constexpr size_t kMaxFeatureReportSize = 2;
+ auto collection = mojom::HidCollectionInfo::New();
+ collection->usage = mojom::HidUsageAndPage::New(mojom::kGenericDesktopGamePad,
+ mojom::kPageGenericDesktop);
+ collection->collection_type = mojom::kHIDCollectionTypeApplication;
+ collection->report_ids.push_back(kTestReportId);
+ auto report = mojom::HidReportDescription::New();
+ report->report_id = kTestReportId;
+ collection->feature_reports.push_back(std::move(report));
+
+ auto device_info =
+ CreateHidDeviceInfo(std::move(collection),
+ /*max_input_report_size=*/0,
+ /*max_output_report_size=*/0, kMaxFeatureReportSize);
+ CreateConnection(device_info);
+
+ // A buffer matching the maximum report size (1 byte report ID + 2 bytes data)
+ // succeeds.
+ {
+ TestFuture<bool> send_feature_future;
+ auto valid_buffer = base::MakeRefCounted<base::RefCountedBytes>(
+ std::vector<uint8_t>{kTestReportId, 0x01, 0x02});
+ connection().SendFeatureReport(valid_buffer,
+ send_feature_future.GetCallback());
+ EXPECT_TRUE(send_feature_future.Get());
+ }
+
+ // A buffer exceeding the maximum report size (1 byte report ID + 3 bytes
+ // data) fails.
+ {
+ TestFuture<bool> send_feature_future;
+ auto long_buffer = base::MakeRefCounted<base::RefCountedBytes>(
+ std::vector<uint8_t>{kTestReportId, 0x01, 0x02, 0x03});
+ connection().SendFeatureReport(long_buffer,
+ send_feature_future.GetCallback());
+ EXPECT_FALSE(send_feature_future.Get());
+ }
+
+ connection().Close();
+}
+
+TEST_F(HidConnectionProtectedReportTest,
+ FeatureReportBufferTooLongFeatureDisabled) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndDisableFeature(
+ features::kHidValidateFeatureReportSize);
+
+ constexpr uint8_t kTestReportId = 1;
+ constexpr size_t kMaxFeatureReportSize = 2;
+ auto collection = mojom::HidCollectionInfo::New();
+ collection->usage = mojom::HidUsageAndPage::New(mojom::kGenericDesktopGamePad,
+ mojom::kPageGenericDesktop);
+ collection->collection_type = mojom::kHIDCollectionTypeApplication;
+ collection->report_ids.push_back(kTestReportId);
+ auto report = mojom::HidReportDescription::New();
+ report->report_id = kTestReportId;
+ collection->feature_reports.push_back(std::move(report));
+
+ auto device_info =
+ CreateHidDeviceInfo(std::move(collection),
+ /*max_input_report_size=*/0,
+ /*max_output_report_size=*/0, kMaxFeatureReportSize);
+ CreateConnection(device_info);
+
+ // When kHidValidateFeatureReportSize is disabled, an oversized buffer is
+ // permitted.
+ TestFuture<bool> send_feature_future;
+ auto long_buffer = base::MakeRefCounted<base::RefCountedBytes>(
+ std::vector<uint8_t>{kTestReportId, 0x01, 0x02, 0x03});
+ connection().SendFeatureReport(long_buffer,
+ send_feature_future.GetCallback());
+ EXPECT_TRUE(send_feature_future.Get());
+
+ connection().Close();
+}
+
+TEST_F(HidConnectionProtectedReportTest,
+ FeatureReportBufferTooLongWithoutReportId) {
+ constexpr size_t kMaxFeatureReportSize = 2;
+ auto collection = mojom::HidCollectionInfo::New();
+ collection->usage = mojom::HidUsageAndPage::New(mojom::kGenericDesktopGamePad,
+ mojom::kPageGenericDesktop);
+ collection->collection_type = mojom::kHIDCollectionTypeApplication;
+ auto report = mojom::HidReportDescription::New();
+ report->report_id = 0;
+ collection->feature_reports.push_back(std::move(report));
+
+ auto device_info =
+ CreateHidDeviceInfo(std::move(collection),
+ /*max_input_report_size=*/0,
+ /*max_output_report_size=*/0, kMaxFeatureReportSize);
+ CreateConnection(device_info);
+
+ // A buffer matching the maximum report size (1 byte report ID 0 + 2 bytes
+ // data) succeeds.
+ {
+ TestFuture<bool> send_feature_future;
+ auto valid_buffer = base::MakeRefCounted<base::RefCountedBytes>(
+ std::vector<uint8_t>{0, 0x01, 0x02});
+ connection().SendFeatureReport(valid_buffer,
+ send_feature_future.GetCallback());
+ EXPECT_TRUE(send_feature_future.Get());
+ }
+
+ // A buffer exceeding the maximum report size (1 byte report ID 0 + 3 bytes
+ // data) fails.
+ {
+ TestFuture<bool> send_feature_future;
+ auto long_buffer = base::MakeRefCounted<base::RefCountedBytes>(
+ std::vector<uint8_t>{0, 0x01, 0x02, 0x03});
+ connection().SendFeatureReport(long_buffer,
+ send_feature_future.GetCallback());
+ EXPECT_FALSE(send_feature_future.Get());
+ }
+
+ connection().Close();
+}
+
+TEST_F(HidConnectionProtectedReportTest,
+ FeatureReportBufferTooLongWithoutReportIdFeatureDisabled) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndDisableFeature(
+ features::kHidValidateFeatureReportSize);
+
+ constexpr size_t kMaxFeatureReportSize = 2;
+ auto collection = mojom::HidCollectionInfo::New();
+ collection->usage = mojom::HidUsageAndPage::New(mojom::kGenericDesktopGamePad,
+ mojom::kPageGenericDesktop);
+ collection->collection_type = mojom::kHIDCollectionTypeApplication;
+ auto report = mojom::HidReportDescription::New();
+ report->report_id = 0;
+ collection->feature_reports.push_back(std::move(report));
+
+ auto device_info =
+ CreateHidDeviceInfo(std::move(collection),
+ /*max_input_report_size=*/0,
+ /*max_output_report_size=*/0, kMaxFeatureReportSize);
+ CreateConnection(device_info);
+
+ // When kHidValidateFeatureReportSize is disabled, an oversized buffer is
+ // permitted.
+ TestFuture<bool> send_feature_future;
+ auto long_buffer = base::MakeRefCounted<base::RefCountedBytes>(
+ std::vector<uint8_t>{0, 0x01, 0x02, 0x03});
+ connection().SendFeatureReport(long_buffer,
+ send_feature_future.GetCallback());
+ EXPECT_TRUE(send_feature_future.Get());
+
+ connection().Close();
+}
+
} // namespace device
Regression Test / PoC
diff --git a/services/device/hid/hid_connection_impl_unittest.cc b/services/device/hid/hid_connection_impl_unittest.cc
index 6c44be1..1b716c9 100644
--- a/services/device/hid/hid_connection_impl_unittest.cc
+++ b/services/device/hid/hid_connection_impl_unittest.cc
@@ -8,12 +8,14 @@
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted_memory.h"
+#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "build/build_config.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "services/device/device_service_test_base.h"
+#include "services/device/public/cpp/device_features.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -375,4 +377,64 @@
EXPECT_THAT(feature_buffer->as_vector(), ElementsAre(kTestReportId));
}
+TEST_F(HidConnectionImplTest, WriteOutputReportBufferTooLong) {
+ auto hid_connection = CreateHidConnection(/*with_connection_client=*/false);
+ EXPECT_CALL(mock_connection(), PlatformWrite).Times(0);
+ WriteFuture write_future;
+ hid_connection->Write(
+ kTestReportId,
+ /*buffer=*/std::vector<uint8_t>(kMaxReportSizeBytes + 1, 0x42),
+ write_future.GetCallback());
+ EXPECT_FALSE(write_future.Get());
+}
+
+TEST_F(HidConnectionImplTest, SendFeatureReport) {
+ auto hid_connection = CreateHidConnection(/*with_connection_client=*/false);
+ scoped_refptr<base::RefCountedBytes> feature_buffer;
+ EXPECT_CALL(mock_connection(), PlatformSendFeatureReport)
+ .WillOnce([&feature_buffer](scoped_refptr<base::RefCountedBytes> buffer,
+ HidConnectionImpl::WriteCallback callback) {
+ feature_buffer = buffer;
+ std::move(callback).Run(/*success=*/true);
+ });
+ WriteFuture write_future;
+ std::vector<uint8_t> buffer(kMaxReportSizeBytes, 0x42);
+ hid_connection->SendFeatureReport(kTestReportId, buffer,
+ write_future.GetCallback());
+ EXPECT_TRUE(write_future.Get());
+ ASSERT_TRUE(feature_buffer);
+ EXPECT_EQ(feature_buffer->size(), kMaxReportSizeBytes + 1);
+ EXPECT_EQ(feature_buffer->as_vector()[0], kTestReportId);
+}
+
+TEST_F(HidConnectionImplTest, SendFeatureReportBufferTooLong) {
+ auto hid_connection = CreateHidConnection(/*with_connection_client=*/false);
+ EXPECT_CALL(mock_connection(), PlatformSendFeatureReport).Times(0);
+ WriteFuture write_future;
+ hid_connection->SendFeatureReport(
+ kTestReportId,
+ /*buffer=*/std::vector<uint8_t>(kMaxReportSizeBytes + 1, 0x42),
+ write_future.GetCallback());
+ EXPECT_FALSE(write_future.Get());
+}
+
+TEST_F(HidConnectionImplTest, SendFeatureReportBufferTooLongFeatureDisabled) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndDisableFeature(
+ features::kHidValidateFeatureReportSize);
+
+ auto hid_connection = CreateHidConnection(/*with_connection_client=*/false);
+ EXPECT_CALL(mock_connection(), PlatformSendFeatureReport)
+ .WillOnce([](scoped_refptr<base::RefCountedBytes> buffer,
+ HidConnection::WriteCallback callback) {
+ std::move(callback).Run(/*success=*/true);
+ });
+ WriteFuture write_future;
+ hid_connection->SendFeatureReport(
+ kTestReportId,
+ /*buffer=*/std::vector<uint8_t>(kMaxReportSizeBytes + 1, 0x42),
+ write_future.GetCallback());
+ EXPECT_TRUE(write_future.Get());
+}
+
} // namespace device
diff --git a/services/device/hid/hid_connection_unittest.cc b/services/device/hid/hid_connection_unittest.cc
index 2bd2e5c..3f6c733 100644
--- a/services/device/hid/hid_connection_unittest.cc
+++ b/services/device/hid/hid_connection_unittest.cc
@@ -746,4 +746,238 @@
}
}
+TEST_F(HidConnectionProtectedReportTest, OutputReportBufferTooLong) {
+ constexpr uint8_t kTestReportId = 1;
+ constexpr size_t kMaxOutputReportSize = 2;
+ auto collection = mojom::HidCollectionInfo::New();
+ collection->usage = mojom::HidUsageAndPage::New(mojom::kGenericDesktopGamePad,
+ mojom::kPageGenericDesktop);
+ collection->collection_type = mojom::kHIDCollectionTypeApplication;
+ collection->report_ids.push_back(kTestReportId);
+ auto report = mojom::HidReportDescription::New();
+ report->report_id = kTestReportId;
+ collection->output_reports.push_back(std::move(report));
+
+ auto device_info =
+ CreateHidDeviceInfo(std::move(collection),
+ /*max_input_report_size=*/0, kMaxOutputReportSize,
+ /*max_feature_report_size=*/0);
+ CreateConnection(device_info);
+
+ // A buffer matching the maximum report size (1 byte report ID + 2 bytes data)
+ // succeeds.
+ {
+ TestFuture<bool> write_future;
+ auto valid_buffer = base::MakeRefCounted<base::RefCountedBytes>(
+ std::vector<uint8_t>{kTestReportId, 0x01, 0x02});
+ connection().Write(valid_buffer, write_future.GetCallback());
+ EXPECT_TRUE(write_future.Get());
+ }
+
+ // A buffer exceeding the maximum report size (1 byte report ID + 3 bytes
+ // data) fails.
+ {
+ TestFuture<bool> write_future;
+ auto long_buffer = base::MakeRefCounted<base::RefCountedBytes>(
+ std::vector<uint8_t>{kTestReportId, 0x01, 0x02, 0x03});
+ connection().Write(long_buffer, write_future.GetCallback());
+ EXPECT_FALSE(write_future.Get());
+ }
+
+ connection().Close();
+}
+
+TEST_F(HidConnectionProtectedReportTest,
+ OutputReportBufferTooLongWithoutReportId) {
+ constexpr size_t kMaxOutputReportSize = 2;
+ auto collection = mojom::HidCollectionInfo::New();
+ collection->usage = mojom::HidUsageAndPage::New(mojom::kGenericDesktopGamePad,
+ mojom::kPageGenericDesktop);
+ collection->collection_type = mojom::kHIDCollectionTypeApplication;
+ auto report = mojom::HidReportDescription::New();
+ report->report_id = 0;
+ collection->output_reports.push_back(std::move(report));
+
+ auto device_info =
+ CreateHidDeviceInfo(std::move(collection),
+ /*max_input_report_size=*/0, kMaxOutputReportSize,
+ /*max_feature_report_size=*/0);
+ CreateConnection(device_info);
+
+ // A buffer matching the maximum report size (1 byte report ID 0 + 2 bytes
+ // data) succeeds.
+ {
+ TestFuture<bool> write_future;
+ auto valid_buffer = base::MakeRefCounted<base::RefCountedBytes>(
+ std::vector<uint8_t>{0, 0x01, 0x02});
+ connection().Write(valid_buffer, write_future.GetCallback());
+ EXPECT_TRUE(write_future.Get());
+ }
+
+ // A buffer exceeding the maximum report size (1 byte report ID 0 + 3 bytes
+ // data) fails.
+ {
+ TestFuture<bool> write_future;
+ auto long_buffer = base::MakeRefCounted<base::RefCountedBytes>(
+ std::vector<uint8_t>{0, 0x01, 0x02, 0x03});
+ connection().Write(long_buffer, write_future.GetCallback());
+ EXPECT_FALSE(write_future.Get());
+ }
+
+ connection().Close();
+}
+
+TEST_F(HidConnectionProtectedReportTest, FeatureReportBufferTooLong) {
+ constexpr uint8_t kTestReportId = 1;
+ constexpr size_t kMaxFeatureReportSize = 2;
+ auto collection = mojom::HidCollectionInfo::New();
+ collection->usage = mojom::HidUsageAndPage::New(mojom::kGenericDesktopGamePad,
+ mojom::kPageGenericDesktop);
+ collection->collection_type = mojom::kHIDCollectionTypeApplication;
+ collection->report_ids.push_back(kTestReportId);
+ auto report = mojom::HidReportDescription::New();
+ report->report_id = kTestReportId;
+ collection->feature_reports.push_back(std::move(report));
+
+ auto device_info =
+ CreateHidDeviceInfo(std::move(collection),
+ /*max_input_report_size=*/0,
+ /*max_output_report_size=*/0, kMaxFeatureReportSize);
+ CreateConnection(device_info);
+
+ // A buffer matching the maximum report size (1 byte report ID + 2 bytes data)
+ // succeeds.
+ {
+ TestFuture<bool> send_feature_future;
+ auto valid_buffer = base::MakeRefCounted<base::RefCountedBytes>(
+ std::vector<uint8_t>{kTestReportId, 0x01, 0x02});
+ connection().SendFeatureReport(valid_buffer,
+ send_feature_future.GetCallback());
+ EXPECT_TRUE(send_feature_future.Get());
+ }
+
+ // A buffer exceeding the maximum report size (1 byte report ID + 3 bytes
+ // data) fails.
+ {
+ TestFuture<bool> send_feature_future;
+ auto long_buffer = base::MakeRefCounted<base::RefCountedBytes>(
+ std::vector<uint8_t>{kTestReportId, 0x01, 0x02, 0x03});
+ connection().SendFeatureReport(long_buffer,
+ send_feature_future.GetCallback());
+ EXPECT_FALSE(send_feature_future.Get());
+ }
+
+ connection().Close();
+}
+
+TEST_F(HidConnectionProtectedReportTest,
+ FeatureReportBufferTooLongFeatureDisabled) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndDisableFeature(
+ features::kHidValidateFeatureReportSize);
+
+ constexpr uint8_t kTestReportId = 1;
+ constexpr size_t kMaxFeatureReportSize = 2;
+ auto collection = mojom::HidCollectionInfo::New();
+ collection->usage = mojom::HidUsageAndPage::New(mojom::kGenericDesktopGamePad,
+ mojom::kPageGenericDesktop);
+ collection->collection_type = mojom::kHIDCollectionTypeApplication;
+ collection->report_ids.push_back(kTestReportId);
+ auto report = mojom::HidReportDescription::New();
+ report->report_id = kTestReportId;
+ collection->feature_reports.push_back(std::move(report));
+
+ auto device_info =
+ CreateHidDeviceInfo(std::move(collection),
+ /*max_input_report_size=*/0,
+ /*max_output_report_size=*/0, kMaxFeatureReportSize);
+ CreateConnection(device_info);
+
+ // When kHidValidateFeatureReportSize is disabled, an oversized buffer is
+ // permitted.
+ TestFuture<bool> send_feature_future;
+ auto long_buffer = base::MakeRefCounted<base::RefCountedBytes>(
+ std::vector<uint8_t>{kTestReportId, 0x01, 0x02, 0x03});
+ connection().SendFeatureReport(long_buffer,
+ send_feature_future.GetCallback());
+ EXPECT_TRUE(send_feature_future.Get());
+
+ connection().Close();
+}
+
+TEST_F(HidConnectionProtectedReportTest,
+ FeatureReportBufferTooLongWithoutReportId) {
+ constexpr size_t kMaxFeatureReportSize = 2;
+ auto collection = mojom::HidCollectionInfo::New();
+ collection->usage = mojom::HidUsageAndPage::New(mojom::kGenericDesktopGamePad,
+ mojom::kPageGenericDesktop);
+ collection->collection_type = mojom::kHIDCollectionTypeApplication;
+ auto report = mojom::HidReportDescription::New();
+ report->report_id = 0;
+ collection->feature_reports.push_back(std::move(report));
+
+ auto device_info =
+ CreateHidDeviceInfo(std::move(collection),
+ /*max_input_report_size=*/0,
+ /*max_output_report_size=*/0, kMaxFeatureReportSize);
+ CreateConnection(device_info);
+
+ // A buffer matching the maximum report size (1 byte report ID 0 + 2 bytes
+ // data) succeeds.
+ {
+ TestFuture<bool> send_feature_future;
+ auto valid_buffer = base::MakeRefCounted<base::RefCountedBytes>(
+ std::vector<uint8_t>{0, 0x01, 0x02});
+ connection().SendFeatureReport(valid_buffer,
+ send_feature_future.GetCallback());
+ EXPECT_TRUE(send_feature_future.Get());
+ }
+
+ // A buffer exceeding the maximum report size (1 byte report ID 0 + 3 bytes
+ // data) fails.
+ {
+ TestFuture<bool> send_feature_future;
+ auto long_buffer = base::MakeRefCounted<base::RefCountedBytes>(
+ std::vector<uint8_t>{0, 0x01, 0x02, 0x03});
+ connection().SendFeatureReport(long_buffer,
+ send_feature_future.GetCallback());
+ EXPECT_FALSE(send_feature_future.Get());
+ }
+
+ connection().Close();
+}
+
+TEST_F(HidConnectionProtectedReportTest,
+ FeatureReportBufferTooLongWithoutReportIdFeatureDisabled) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndDisableFeature(
+ features::kHidValidateFeatureReportSize);
+
+ constexpr size_t kMaxFeatureReportSize = 2;
... (truncated)
Original Bug Report
WebHID: SendFeatureReport sends oversized buffers to OS HID driver (size check missing vs Write())
Report description
WebHID: SendFeatureReport sends oversized buffers to OS HID driver (size check missing vs Write())
Bug location
Where do you want to report your vulnerability?
Chrome VRP – Report security issues affecting the Chrome browser. See program rules
Which URL (or repository) have you found the vulnerability in?
https://chromium.googlesource.com/chromium/
The problem
Please describe the technical details of the vulnerability
HidConnection::Write validates the buffer size against max_output_report_size before calling PlatformWrite. If the buffer is too large, the call is rejected early. HidConnection::SendFeatureReport doesn’t have this check. The buffer goes straight to PlatformSendFeatureReport without any size validation against max_feature_report_size.
Compare both functions in services/device/hid/hid_connection.cc:
// Write - has the check
void HidConnection::Write(...) {
if (buffer->size() > device_info_->max_output_report_size() + 1) {
HID_LOG(USER) << "Output report buffer too long (" << buffer->size()
<< " > " << (device_info_->max_output_report_size() + 1)
<< ").";
std::move(callback).Run(false);
return;
}
// ...
PlatformWrite(buffer, std::move(callback));
}
// SendFeatureReport - no check
void HidConnection::SendFeatureReport(...) {
// NO validation against max_feature_report_size()
DCHECK_GE(buffer->size(), 1u);
// ...
PlatformSendFeatureReport(buffer, std::move(callback));
}
The full call chain from a web page:
- JS calls
device.sendFeatureReport(reportId, data)with arbitrary size HidConnectionImpl::SendFeatureReportprepends the report ID, no size checkHidConnection::SendFeatureReportforwards to platform, no size check- Platform sends to kernel:
The kernel (hidraw_send_report) only checks against HID_MAX_BUFFER_SIZE (16384), not against the device’s declared max_feature_report_size. So a 4096-byte buffer goes through for a device that declares 184 bytes max.
Reproduction
Prerequisites: any HID device with feature report support. Tested with MSI Mystic Light (vendorId=0x1462, productId=0x7d27, max_feature_report_size=184 bytes).
- Open the attached PoC HTML in Chrome
- Click “Connect”, select the HID device
- Click “Write normal” (184 bytes of 0xAA, within bounds)
- Click “Write oversized” (4096 bytes of 0xBB, 22x the declared max)
- Both writes succeed. Chromium accepts both without distinction.
Kernel-side confirmation via bpftrace:
$ sudo bpftrace -e 'kprobe:hidraw_send_report { printf("count=%d report_type=%d\n", arg2, arg3); }'
count=185 report_type=2 <- reset (184 + 1 byte report ID)
count=185 report_type=2 <- normal write
count=4097 report_type=2 <- oversized write (4096 + 1), should have been rejected
Impact analysis
Any web page granted WebHID device access can send feature reports larger than the device’s declared maximum directly to the OS HID driver. The browser doesn’t validate the size, and the kernel doesn’t check against the device’s report descriptor either.
On USB hidraw (Linux), hidraw_send_report allocates dynamically via memdup_user so there’s no kernel heap overflow on this specific path. But Chromium can’t assume all transport drivers do this. I2C-HID and Bluetooth HID transport drivers often use fixed-size buffers based on the report descriptor, where an oversized write could cause a kernel heap overflow.
The cause
What version of Chrome have you found the security issue in?
147.0.7727.101 stable ; code still in main
Is the security issue related to a crash?
No, it is not related to a crash.
Choose the type of vulnerability
Other
How would you like to be publicly acknowledged for your report?
Florian Pradines