Browse Source

chore: cherry-pick b041159d06ad from chromium (#37695)

* chore: [21-x-y] cherry-pick b041159d06ad from chromium

* chore: update patches

---------

Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com>
Co-authored-by: electron-patch-conflict-fixer[bot] <83340002+electron-patch-conflict-fixer[bot]@users.noreply.github.com>
Pedro Pontes 2 years ago
parent
commit
ee66a3e938
2 changed files with 507 additions and 0 deletions
  1. 1 0
      patches/chromium/.patches
  2. 506 0
      patches/chromium/cherry-pick-b041159d06ad.patch

+ 1 - 0
patches/chromium/.patches

@@ -155,6 +155,7 @@ cherry-pick-0407102d19b9.patch
 cherry-pick-38de42d2bbc3.patch
 cherry-pick-bfd926be8178.patch
 cherry-pick-d202ad3c6aeb.patch
+cherry-pick-b041159d06ad.patch
 m108-lts_do_not_register_browser_watcher_activity_report_with.patch
 cherry-pick-8731bd8a30f6.patch
 cherry-pick-1235110fce18.patch

+ 506 - 0
patches/chromium/cherry-pick-b041159d06ad.patch

@@ -0,0 +1,506 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Matt Reynolds <[email protected]>
+Date: Wed, 8 Mar 2023 23:55:10 +0000
+Subject: hid: Handle empty input reports
+
+It's possible for a HID device to define its report descriptor such that
+one or more reports have no data fields within the report. When receiving these reports, the report buffer should contain only the
+report ID byte and no other data.
+
+Ensure that we do not read past the end of the buffer when handling
+zero-length input reports.
+
+(cherry picked from commit c9d77da78bc66c135520ac77873d67b89cdcaee6)
+
+Bug: 1419718
+Change-Id: I51d32c20f6b16f0d2b0172e0a165469b6b79748c
+Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4296562
+Reviewed-by: Reilly Grant <[email protected]>
+Commit-Queue: Matt Reynolds <[email protected]>
+Cr-Original-Commit-Position: refs/heads/main@{#1112009}
+Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4320692
+Commit-Queue: Reilly Grant <[email protected]>
+Auto-Submit: Matt Reynolds <[email protected]>
+Cr-Commit-Position: refs/branch-heads/5481@{#1341}
+Cr-Branched-From: 130f3e4d850f4bc7387cfb8d08aa993d288a67a9-refs/heads/main@{#1084008}
+
+diff --git a/services/device/hid/hid_connection_impl.cc b/services/device/hid/hid_connection_impl.cc
+index 131eee19f578a4fc22ab01fec37c4d82af829d4d..508114a7626c59fd02601c1e7eed2409ea0817f8 100644
+--- a/services/device/hid/hid_connection_impl.cc
++++ b/services/device/hid/hid_connection_impl.cc
+@@ -54,11 +54,12 @@ void HidConnectionImpl::OnInputReport(
+     scoped_refptr<base::RefCountedBytes> buffer,
+     size_t size) {
+   DCHECK(client_);
+-  uint8_t report_id = buffer->data()[0];
+-  uint8_t* begin = &buffer->data()[1];
+-  uint8_t* end = buffer->data().data() + size;
+-  std::vector<uint8_t> data(begin, end);
+-  client_->OnInputReport(report_id, data);
++  DCHECK_GE(size, 1u);
++  std::vector<uint8_t> data;
++  if (size > 1) {
++    data = std::vector<uint8_t>(buffer->front() + 1, buffer->front() + size);
++  }
++  client_->OnInputReport(/*report_id=*/buffer->data()[0], data);
+ }
+ 
+ void HidConnectionImpl::Read(ReadCallback callback) {
+diff --git a/services/device/hid/hid_connection_impl_unittest.cc b/services/device/hid/hid_connection_impl_unittest.cc
+index 3acdee978dbfc0b0efd16fd36416bf016c02d256..6359714a7cf62774111cf4ea2c0cdbf251680e62 100644
+--- a/services/device/hid/hid_connection_impl_unittest.cc
++++ b/services/device/hid/hid_connection_impl_unittest.cc
+@@ -8,17 +8,28 @@
+ #include "base/callback_helpers.h"
+ #include "base/memory/raw_ptr.h"
+ #include "base/memory/ref_counted_memory.h"
++#include "base/test/repeating_test_future.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 "testing/gmock/include/gmock/gmock.h"
+ #include "testing/gtest/include/gtest/gtest.h"
+ 
+ namespace device {
+ 
+ namespace {
+ 
++using ::testing::ElementsAre;
++
++using ReadFuture = base::test::
++    TestFuture<bool, uint8_t, const absl::optional<std::vector<uint8_t>>&>;
++using WriteFuture = base::test::TestFuture<bool>;
++using GetFeatureFuture =
++    base::test::TestFuture<bool, const absl::optional<std::vector<uint8_t>>&>;
++
+ #if BUILDFLAG(IS_MAC)
+ const uint64_t kTestDeviceId = 123;
+ #elif BUILDFLAG(IS_WIN)
+@@ -30,46 +41,37 @@ const char* kTestDeviceId = "123";
+ // The report ID to use for reports sent to or received from the test device.
+ const uint8_t kTestReportId = 0x42;
+ 
+-// The max size of input and output reports for the test device. Feature reports
+-// are not used in this test.
++// The max size of reports for the test device.
+ const uint64_t kMaxReportSizeBytes = 10;
+ 
+-// A fake HidConnection implementation that allows the test to simulate an
+-// input report.
+-class FakeHidConnection : public HidConnection {
++// A mock HidConnection implementation that allows the test to simulate reports.
++class MockHidConnection : public HidConnection {
+  public:
+-  explicit FakeHidConnection(scoped_refptr<HidDeviceInfo> device)
++  explicit MockHidConnection(scoped_refptr<HidDeviceInfo> device)
+       : HidConnection(device,
+                       /*allow_protected_reports=*/false,
+                       /*allow_fido_reports=*/false) {}
+-  FakeHidConnection(const FakeHidConnection&) = delete;
+-  FakeHidConnection& operator=(const FakeHidConnection&) = delete;
++  MockHidConnection(const MockHidConnection&) = delete;
++  MockHidConnection& operator=(const MockHidConnection&) = delete;
+ 
+   // HidConnection implementation.
+   void PlatformClose() override {}
+-  void PlatformWrite(scoped_refptr<base::RefCountedBytes> buffer,
+-                     WriteCallback callback) override {
+-    std::move(callback).Run(true);
+-  }
+-  void PlatformGetFeatureReport(uint8_t report_id,
+-                                ReadCallback callback) override {
+-    NOTIMPLEMENTED();
+-  }
+-  void PlatformSendFeatureReport(scoped_refptr<base::RefCountedBytes> buffer,
+-                                 WriteCallback callback) override {
+-    NOTIMPLEMENTED();
+-  }
++  MOCK_METHOD2(PlatformWrite,
++               void(scoped_refptr<base::RefCountedBytes>, WriteCallback));
++  MOCK_METHOD2(PlatformGetFeatureReport, void(uint8_t, ReadCallback));
++  MOCK_METHOD2(PlatformSendFeatureReport,
++               void(scoped_refptr<base::RefCountedBytes>, WriteCallback));
+ 
+   void SimulateInputReport(scoped_refptr<base::RefCountedBytes> buffer) {
+     ProcessInputReport(buffer, buffer->size());
+   }
+ 
+  private:
+-  ~FakeHidConnection() override = default;
++  ~MockHidConnection() override = default;
+ };
+ 
+-// A test implementation of HidConnectionClient that signals once an input
+-// report has been received. The contents of the input report are saved.
++// An implementation of HidConnectionClient that enables the test to wait until
++// an input report is received.
+ class TestHidConnectionClient : public mojom::HidConnectionClient {
+  public:
+   TestHidConnectionClient() = default;
+@@ -81,76 +83,18 @@ class TestHidConnectionClient : public mojom::HidConnectionClient {
+     receiver_.Bind(std::move(receiver));
+   }
+ 
+-  // mojom::HidConnectionClient implementation.
+   void OnInputReport(uint8_t report_id,
+                      const std::vector<uint8_t>& buffer) override {
+-    report_id_ = report_id;
+-    buffer_ = buffer;
+-    run_loop_.Quit();
+-  }
+-
+-  void WaitForInputReport() { run_loop_.Run(); }
+-
+-  uint8_t report_id() { return report_id_; }
+-  const std::vector<uint8_t>& buffer() { return buffer_; }
+-
+- private:
+-  base::RunLoop run_loop_;
+-  mojo::Receiver<mojom::HidConnectionClient> receiver_{this};
+-  uint8_t report_id_ = 0;
+-  std::vector<uint8_t> buffer_;
+-};
+-
+-// A utility for capturing the state returned by mojom::HidConnection I/O
+-// callbacks.
+-class TestIoCallback {
+- public:
+-  TestIoCallback() = default;
+-  TestIoCallback(const TestIoCallback&) = delete;
+-  TestIoCallback& operator=(const TestIoCallback&) = delete;
+-  ~TestIoCallback() = default;
+-
+-  void SetReadResult(bool result,
+-                     uint8_t report_id,
+-                     const absl::optional<std::vector<uint8_t>>& buffer) {
+-    result_ = result;
+-    report_id_ = report_id;
+-    has_buffer_ = buffer.has_value();
+-    if (has_buffer_)
+-      buffer_ = *buffer;
+-    run_loop_.Quit();
+-  }
+-
+-  void SetWriteResult(bool result) {
+-    result_ = result;
+-    run_loop_.Quit();
+-  }
+-
+-  bool WaitForResult() {
+-    run_loop_.Run();
+-    return result_;
+-  }
+-
+-  mojom::HidConnection::ReadCallback GetReadCallback() {
+-    return base::BindOnce(&TestIoCallback::SetReadResult,
+-                          base::Unretained(this));
++    future_.AddValue(report_id, buffer);
+   }
+ 
+-  mojom::HidConnection::WriteCallback GetWriteCallback() {
+-    return base::BindOnce(&TestIoCallback::SetWriteResult,
+-                          base::Unretained(this));
++  std::pair<uint8_t, std::vector<uint8_t>> GetNextInputReport() {
++    return future_.Take();
+   }
+ 
+-  uint8_t report_id() { return report_id_; }
+-  bool has_buffer() { return has_buffer_; }
+-  const std::vector<uint8_t>& buffer() { return buffer_; }
+-
+  private:
+-  base::RunLoop run_loop_;
+-  bool result_ = false;
+-  uint8_t report_id_ = 0;
+-  bool has_buffer_ = false;
+-  std::vector<uint8_t> buffer_;
++  mojo::Receiver<mojom::HidConnectionClient> receiver_{this};
++  base::test::RepeatingTestFuture<uint8_t, std::vector<uint8_t>> future_;
+ };
+ 
+ }  // namespace
+@@ -158,8 +102,8 @@ class TestIoCallback {
+ class HidConnectionImplTest : public DeviceServiceTestBase {
+  public:
+   HidConnectionImplTest() = default;
+-  HidConnectionImplTest(HidConnectionImplTest&) = delete;
+-  HidConnectionImplTest& operator=(HidConnectionImplTest&) = delete;
++  HidConnectionImplTest(const HidConnectionImplTest&) = delete;
++  HidConnectionImplTest& operator=(const HidConnectionImplTest&) = delete;
+ 
+  protected:
+   void SetUp() override {
+@@ -167,18 +111,28 @@ class HidConnectionImplTest : public DeviceServiceTestBase {
+     base::RunLoop().RunUntilIdle();
+   }
+ 
+-  void CreateHidConnection(bool with_connection_client) {
++  void TearDown() override {
++    // HidConnectionImpl is self-owned and will self-destruct when its mojo pipe
++    // is disconnected. Allow disconnect handlers to run so HidConnectionImpl
++    // can self-destruct before the end of the test.
++    base::RunLoop().RunUntilIdle();
++  }
++
++  mojo::Remote<mojom::HidConnection> CreateHidConnection(
++      bool with_connection_client) {
+     mojo::PendingRemote<mojom::HidConnectionClient> hid_connection_client;
+     if (with_connection_client) {
+       connection_client_ = std::make_unique<TestHidConnectionClient>();
+       connection_client_->Bind(
+           hid_connection_client.InitWithNewPipeAndPassReceiver());
+     }
+-    fake_connection_ = new FakeHidConnection(CreateTestDevice());
+-    hid_connection_impl_ = new HidConnectionImpl(
+-        fake_connection_, hid_connection_.InitWithNewPipeAndPassReceiver(),
+-        std::move(hid_connection_client),
+-        /*watcher=*/mojo::NullRemote());
++    mock_connection_ = new MockHidConnection(CreateTestDevice());
++    mojo::Remote<mojom::HidConnection> hid_connection;
++    HidConnectionImpl::Create(mock_connection_,
++                              hid_connection.BindNewPipeAndPassReceiver(),
++                              std::move(hid_connection_client),
++                              /*watcher=*/mojo::NullRemote());
++    return hid_connection;
+   }
+ 
+   scoped_refptr<HidDeviceInfo> CreateTestDevice() {
+@@ -190,7 +144,7 @@ class HidConnectionImplTest : public DeviceServiceTestBase {
+         /*vendor_id=*/0x1234, /*product_id=*/0xabcd, "product name",
+         "serial number", mojom::HidBusType::kHIDBusTypeUSB,
+         std::move(collection), kMaxReportSizeBytes, kMaxReportSizeBytes,
+-        /*max_feature_report_size=*/0);
++        kMaxReportSizeBytes);
+   }
+ 
+   std::vector<uint8_t> CreateTestReportBuffer(uint8_t report_id, size_t size) {
+@@ -201,37 +155,42 @@ class HidConnectionImplTest : public DeviceServiceTestBase {
+     return buffer;
+   }
+ 
+-  mojo::PendingRemote<mojom::HidConnection> hid_connection_;
+-  raw_ptr<HidConnectionImpl>
+-      hid_connection_impl_;  // Owned by |hid_connection_|.
+-  scoped_refptr<FakeHidConnection> fake_connection_;
++  MockHidConnection& mock_connection() { return *mock_connection_.get(); }
++  TestHidConnectionClient& connection_client() { return *connection_client_; }
++
++ private:
++  scoped_refptr<MockHidConnection> mock_connection_;
+   std::unique_ptr<TestHidConnectionClient> connection_client_;
+ };
+ 
+ TEST_F(HidConnectionImplTest, ReadWrite) {
+-  CreateHidConnection(/*with_connection_client=*/false);
++  auto hid_connection = CreateHidConnection(/*with_connection_client=*/false);
+   const size_t kTestBufferSize = kMaxReportSizeBytes;
+   std::vector<uint8_t> buffer_vec =
+       CreateTestReportBuffer(kTestReportId, kTestBufferSize);
+ 
+   // Simulate an output report (host to device).
+-  TestIoCallback write_callback;
+-  hid_connection_impl_->Write(kTestReportId, buffer_vec,
+-                              write_callback.GetWriteCallback());
+-  ASSERT_TRUE(write_callback.WaitForResult());
++  EXPECT_CALL(mock_connection(), PlatformWrite)
++      .WillOnce([](scoped_refptr<base::RefCountedBytes> buffer,
++                   HidConnectionImpl::WriteCallback callback) {
++        std::move(callback).Run(/*success=*/true);
++      });
++  WriteFuture write_future;
++  hid_connection->Write(kTestReportId, buffer_vec, write_future.GetCallback());
++  EXPECT_TRUE(write_future.Get());
+ 
+   // Simulate an input report (device to host).
+   auto buffer = base::MakeRefCounted<base::RefCountedBytes>(buffer_vec);
+   ASSERT_EQ(buffer->size(), kTestBufferSize);
+-  fake_connection_->SimulateInputReport(buffer);
++  mock_connection().SimulateInputReport(buffer);
+ 
+   // Simulate reading the input report.
+-  TestIoCallback read_callback;
+-  hid_connection_impl_->Read(read_callback.GetReadCallback());
+-  ASSERT_TRUE(read_callback.WaitForResult());
+-  EXPECT_EQ(read_callback.report_id(), kTestReportId);
+-  ASSERT_TRUE(read_callback.has_buffer());
+-  const auto& read_buffer = read_callback.buffer();
++  ReadFuture read_future;
++  hid_connection->Read(read_future.GetCallback());
++  EXPECT_TRUE(read_future.Get<0>());
++  EXPECT_EQ(read_future.Get<1>(), kTestReportId);
++  ASSERT_TRUE(read_future.Get<2>().has_value());
++  const auto& read_buffer = read_future.Get<2>().value();
+   ASSERT_EQ(read_buffer.size(), kTestBufferSize - 1);
+   for (size_t i = 1; i < kTestBufferSize; ++i) {
+     EXPECT_EQ(read_buffer[i - 1], buffer_vec[i])
+@@ -240,26 +199,29 @@ TEST_F(HidConnectionImplTest, ReadWrite) {
+ }
+ 
+ TEST_F(HidConnectionImplTest, ReadWriteWithConnectionClient) {
+-  CreateHidConnection(/*with_connection_client=*/true);
++  auto hid_connection = CreateHidConnection(/*with_connection_client=*/true);
+   const size_t kTestBufferSize = kMaxReportSizeBytes;
+   std::vector<uint8_t> buffer_vec =
+       CreateTestReportBuffer(kTestReportId, kTestBufferSize);
+ 
+   // Simulate an output report (host to device).
+-  TestIoCallback write_callback;
+-  hid_connection_impl_->Write(kTestReportId, buffer_vec,
+-                              write_callback.GetWriteCallback());
+-  ASSERT_TRUE(write_callback.WaitForResult());
++  EXPECT_CALL(mock_connection(), PlatformWrite)
++      .WillOnce([](scoped_refptr<base::RefCountedBytes> buffer,
++                   HidConnectionImpl::WriteCallback callback) {
++        std::move(callback).Run(/*success=*/true);
++      });
++  WriteFuture write_future;
++  hid_connection->Write(kTestReportId, buffer_vec, write_future.GetCallback());
++  EXPECT_TRUE(write_future.Get());
+ 
+   // Simulate an input report (device to host).
+   auto buffer = base::MakeRefCounted<base::RefCountedBytes>(buffer_vec);
+   ASSERT_EQ(buffer->size(), kTestBufferSize);
+-  fake_connection_->SimulateInputReport(buffer);
+-  connection_client_->WaitForInputReport();
++  mock_connection().SimulateInputReport(buffer);
++  auto [report_id, in_buffer] = connection_client().GetNextInputReport();
+ 
+   // The connection client should have been notified.
+-  EXPECT_EQ(connection_client_->report_id(), kTestReportId);
+-  const std::vector<uint8_t>& in_buffer = connection_client_->buffer();
++  EXPECT_EQ(report_id, kTestReportId);
+   ASSERT_EQ(in_buffer.size(), kTestBufferSize - 1);
+   for (size_t i = 1; i < kTestBufferSize; ++i) {
+     EXPECT_EQ(in_buffer[i - 1], buffer_vec[i])
+@@ -268,7 +230,7 @@ TEST_F(HidConnectionImplTest, ReadWriteWithConnectionClient) {
+ }
+ 
+ TEST_F(HidConnectionImplTest, DestroyWithPendingInputReport) {
+-  CreateHidConnection(/*with_connection_client=*/false);
++  auto hid_connection = CreateHidConnection(/*with_connection_client=*/false);
+   const size_t kTestBufferSize = kMaxReportSizeBytes;
+   std::vector<uint8_t> buffer_vec =
+       CreateTestReportBuffer(kTestReportId, kTestBufferSize);
+@@ -276,21 +238,20 @@ TEST_F(HidConnectionImplTest, DestroyWithPendingInputReport) {
+   // Simulate an input report (device to host).
+   auto buffer = base::MakeRefCounted<base::RefCountedBytes>(buffer_vec);
+   ASSERT_EQ(buffer->size(), kTestBufferSize);
+-  fake_connection_->SimulateInputReport(buffer);
++  mock_connection().SimulateInputReport(buffer);
+ 
+   // Destroy the connection without reading the report.
+-  hid_connection_.reset();
++  hid_connection.reset();
+ }
+ 
+ TEST_F(HidConnectionImplTest, DestroyWithPendingRead) {
+-  CreateHidConnection(/*with_connection_client=*/false);
++  auto hid_connection = CreateHidConnection(/*with_connection_client=*/false);
+ 
+   // Simulate reading an input report.
+-  TestIoCallback read_callback;
+-  hid_connection_impl_->Read(read_callback.GetReadCallback());
++  hid_connection->Read(base::DoNothing());
+ 
+   // Destroy the connection without receiving an input report.
+-  hid_connection_.reset();
++  hid_connection.reset();
+ }
+ 
+ TEST_F(HidConnectionImplTest, WatcherClosedWhenHidConnectionClosed) {
+@@ -301,7 +262,7 @@ TEST_F(HidConnectionImplTest, WatcherClosedWhenHidConnectionClosed) {
+ 
+   mojo::Remote<mojom::HidConnection> hid_connection;
+   HidConnectionImpl::Create(
+-      base::MakeRefCounted<FakeHidConnection>(CreateTestDevice()),
++      base::MakeRefCounted<MockHidConnection>(CreateTestDevice()),
+       hid_connection.BindNewPipeAndPassReceiver(),
+       /*connection_client=*/mojo::NullRemote(), std::move(watcher));
+ 
+@@ -326,7 +287,7 @@ TEST_F(HidConnectionImplTest, HidConnectionClosedWhenWatcherClosed) {
+ 
+   mojo::Remote<mojom::HidConnection> hid_connection;
+   HidConnectionImpl::Create(
+-      base::MakeRefCounted<FakeHidConnection>(CreateTestDevice()),
++      base::MakeRefCounted<MockHidConnection>(CreateTestDevice()),
+       hid_connection.BindNewPipeAndPassReceiver(),
+       /*connection_client=*/mojo::NullRemote(), std::move(watcher));
+ 
+@@ -344,4 +305,74 @@ TEST_F(HidConnectionImplTest, HidConnectionClosedWhenWatcherClosed) {
+   EXPECT_FALSE(hid_connection.is_connected());
+ }
+ 
++TEST_F(HidConnectionImplTest, ReadZeroLengthInputReport) {
++  auto hid_connection = CreateHidConnection(/*with_connection_client=*/false);
++  mock_connection().SimulateInputReport(
++      base::MakeRefCounted<base::RefCountedBytes>(
++          CreateTestReportBuffer(kTestReportId, /*size=*/1u)));
++  ReadFuture read_future;
++  hid_connection->Read(read_future.GetCallback());
++  EXPECT_TRUE(read_future.Get<0>());
++  EXPECT_EQ(read_future.Get<1>(), kTestReportId);
++  ASSERT_TRUE(read_future.Get<2>().has_value());
++  EXPECT_EQ(read_future.Get<2>().value().size(), 0u);
++}
++
++TEST_F(HidConnectionImplTest, ReadZeroLengthInputReportWithClient) {
++  auto hid_connection = CreateHidConnection(/*with_connection_client=*/true);
++  mock_connection().SimulateInputReport(
++      base::MakeRefCounted<base::RefCountedBytes>(
++          CreateTestReportBuffer(kTestReportId, /*size=*/1u)));
++  auto [report_id, in_buffer] = connection_client().GetNextInputReport();
++  EXPECT_EQ(report_id, kTestReportId);
++  EXPECT_EQ(in_buffer.size(), 0u);
++}
++
++TEST_F(HidConnectionImplTest, WriteZeroLengthOutputReport) {
++  auto hid_connection = CreateHidConnection(/*with_connection_client=*/false);
++  EXPECT_CALL(mock_connection(), PlatformWrite)
++      .WillOnce([](scoped_refptr<base::RefCountedBytes> buffer,
++                   HidConnectionImpl::WriteCallback callback) {
++        std::move(callback).Run(/*success=*/true);
++      });
++  WriteFuture write_future;
++  hid_connection->Write(kTestReportId, /*buffer=*/{},
++                        write_future.GetCallback());
++  EXPECT_TRUE(write_future.Get());
++}
++
++TEST_F(HidConnectionImplTest, ReadZeroLengthFeatureReport) {
++  auto hid_connection = CreateHidConnection(/*with_connection_client=*/false);
++  EXPECT_CALL(mock_connection(), PlatformGetFeatureReport)
++      .WillOnce([](uint8_t report_id, HidConnection::ReadCallback callback) {
++        std::move(callback).Run(/*success=*/true,
++                                base::MakeRefCounted<base::RefCountedBytes>(
++                                    std::vector<uint8_t>{report_id}),
++                                /*size=*/1u);
++      });
++  GetFeatureFuture get_feature_future;
++  hid_connection->GetFeatureReport(kTestReportId,
++                                   get_feature_future.GetCallback());
++  EXPECT_TRUE(get_feature_future.Get<0>());
++  ASSERT_TRUE(get_feature_future.Get<1>().has_value());
++  EXPECT_EQ(get_feature_future.Get<1>().value().size(), 1u);
++}
++
++TEST_F(HidConnectionImplTest, WriteZeroLengthFeatureReport) {
++  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;
++  hid_connection->SendFeatureReport(kTestReportId, /*buffer=*/{},
++                                    write_future.GetCallback());
++  EXPECT_TRUE(write_future.Get());
++  ASSERT_TRUE(feature_buffer);
++  EXPECT_THAT(feature_buffer->data(), ElementsAre(kTestReportId));
++}
++
+ }  // namespace device