CVE-2026-13903
Overview
Files Changed
content/browser/bluetooth/web_bluetooth_service_impl.cccontent/browser/bluetooth/web_bluetooth_service_impl.hcontent/browser/bluetooth/web_bluetooth_service_impl_unittest.ccthird_party/blink/web_tests/external/wpt/bluetooth/resources/bluetooth-fake-devices.js
Patch
From a0b55d797bbc3d7dfd31113f4957df873d50d20b Mon Sep 17 00:00:00 2001 From: Rob Pitkin <[email protected]> Date: Fri, 15 May 2026 16:40:45 -0700 Subject: [PATCH] bluetooth: Fix missing blocklist check for characteristic in descriptors Web Bluetooth fails to check the GATT blocklist for a descriptor's parent characteristic during read and write operations. This allows websites to bypass security restrictions and interact with descriptors of protected characteristics. This CL adds checks in `RemoteDescriptorReadValue` and `RemoteDescriptorWriteValue` to ensure that the parent characteristic is also checked against the GATT blocklist before allowing read or write operations on its descriptors. Unit tests are added to verify that reads and writes are blocked when the parent characteristic is blocklisted. Bug: 503912196 Change-Id: If62e10d5ac66ed414aec1a3a4af346133a584374 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7837659 Commit-Queue: Rob Pitkin <[email protected]> Reviewed-by: Matt Reynolds <[email protected]> Cr-Commit-Position: refs/heads/main@{#1631633} --- diff --git a/content/browser/bluetooth/web_bluetooth_service_impl.cc b/content/browser/bluetooth/web_bluetooth_service_impl.cc index a075173..deea337 100644 --- a/content/browser/bluetooth/web_bluetooth_service_impl.cc +++ b/content/browser/bluetooth/web_bluetooth_service_impl.cc @@ -1383,7 +1383,9 @@ } if (BluetoothBlocklist::Get().IsExcludedFromReads( - query_result.descriptor->GetUUID())) { + query_result.descriptor->GetUUID()) || + BluetoothBlocklist::Get().IsExcludedFromReads( + query_result.characteristic->GetUUID())) { std::move(callback).Run(blink::mojom::WebBluetoothResult::BLOCKLISTED_READ, /*value=*/{}); return; @@ -1423,7 +1425,9 @@ } if (BluetoothBlocklist::Get().IsExcludedFromWrites( - query_result.descriptor->GetUUID())) { + query_result.descriptor->GetUUID()) || + BluetoothBlocklist::Get().IsExcludedFromWrites( + query_result.characteristic->GetUUID())) { std::move(callback).Run( blink::mojom::WebBluetoothResult::BLOCKLISTED_WRITE); return; diff --git a/content/browser/bluetooth/web_bluetooth_service_impl.h b/content/browser/bluetooth/web_bluetooth_service_impl.h index c414992..73196dd4 100644 --- a/content/browser/bluetooth/web_bluetooth_service_impl.h +++ b/content/browser/bluetooth/web_bluetooth_service_impl.h @@ -178,6 +178,12 @@ EmulatedAdapterRemovalRestoresOriginalAdapter); FRIEND_TEST_ALL_PREFIXES(WebBluetoothServiceImplTest, ServiceDestroyedDuringAdapterAcquisition); + FRIEND_TEST_ALL_PREFIXES( + WebBluetoothServiceImplTest, + RemoteDescriptorReadValue_ParentCharacteristicBlocklisted); + FRIEND_TEST_ALL_PREFIXES( + WebBluetoothServiceImplTest, + RemoteDescriptorWriteValue_ParentCharacteristicBlocklisted); #if PAIR_BLUETOOTH_ON_DEMAND() FRIEND_TEST_ALL_PREFIXES(WebBluetoothServiceImplTest, diff --git a/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc b/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc index 42523def..4880d79 100644 --- a/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc +++ b/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc @@ -32,6 +32,7 @@ #include "device/bluetooth/test/mock_bluetooth_adapter.h" #include "device/bluetooth/test/mock_bluetooth_device.h" #include "device/bluetooth/test/mock_bluetooth_gatt_characteristic.h" +#include "device/bluetooth/test/mock_bluetooth_gatt_descriptor.h" #include "device/bluetooth/test/mock_bluetooth_gatt_notify_session.h" #include "device/bluetooth/test/mock_bluetooth_gatt_service.h" #include "mojo/public/cpp/bindings/associated_receiver.h" @@ -96,6 +97,8 @@ const device::BluetoothUUID kBatteryServiceUUID(kBatteryServiceUUIDString); const device::BluetoothUUID kBatteryLevelCharacteristicUUID( "00002a19-0000-1000-8000-00805f9b34fb"); +const device::BluetoothUUID kCharacteristicUserDescriptionDescriptorUUID( + "00002901-0000-1000-8000-00805f9b34fb"); const BluetoothDeviceBundleData battery_device_bundle_data = { kBatteryServiceId, kBatteryLevelCharacteristicId, kBatteryServiceUUID, kBatteryLevelCharacteristicUUID, kTestCharacteristicProperties}; @@ -559,6 +562,7 @@ heart_rate_device_bundle_.reset(); service_ptr_ = nullptr; SetBrowserClientForTesting(old_browser_client_); + BluetoothBlocklist::Get().ResetToDefaultValuesForTest(); RenderViewHostImplTestHarness::TearDown(); } @@ -1442,4 +1446,95 @@ EXPECT_CALL(*adapter_, StopScan).Times(1); } +TEST_F(WebBluetoothServiceImplTest, + RemoteDescriptorReadValue_ParentCharacteristicBlocklisted) { + RegisterTestCharacteristic(); + + FakeBluetoothCharacteristic& test_characteristic = + battery_device_bundle().characteristic(); + + auto mock_descriptor = + std::make_unique<testing::NiceMock<device::MockBluetoothGattDescriptor>>( + &test_characteristic, "test_descriptor_id", + kCharacteristicUserDescriptionDescriptorUUID, + device::BluetoothRemoteGattCharacteristic::PERMISSION_NONE); + + test_characteristic.AddMockDescriptor(std::move(mock_descriptor)); + + base::test::TestFuture< + WebBluetoothResult, + std::optional< + std::vector<blink::mojom::WebBluetoothRemoteGATTDescriptorPtr>>> + get_descriptors_future; + service_ptr_->RemoteCharacteristicGetDescriptors( + test_characteristic.GetIdentifier(), + WebBluetoothGATTQueryQuantity::SINGLE, std::nullopt, + get_descriptors_future.GetCallback()); + + EXPECT_EQ(get_descriptors_future.Get<0>(), WebBluetoothResult::SUCCESS); + + const auto& descriptors = get_descriptors_future.Get<1>(); + ASSERT_TRUE(descriptors.has_value()); + ASSERT_FALSE(descriptors->empty()); + std::string descriptor_instance_id = descriptors->at(0)->instance_id; + + BluetoothBlocklist::Get().Add(test_characteristic.GetUUID(), + BluetoothBlocklist::Value::EXCLUDE_READS); + + base::test::TestFuture<WebBluetoothResult, std::vector<uint8_t>> read_future; + service_ptr_->RemoteDescriptorReadValue( + descriptor_instance_id, + base::BindLambdaForTesting( + [&read_future](WebBluetoothResult result, + base::span<const uint8_t> value) { + read_future.SetValue( + result, std::vector<uint8_t>(value.begin(), value.end())); + })); + + EXPECT_EQ(read_future.Get<0>(), WebBluetoothResult::BLOCKLISTED_READ); +} + +TEST_F(WebBluetoothServiceImplTest, + RemoteDescriptorWriteValue_ParentCharacteristicBlocklisted) { + RegisterTestCharacteristic(); + + FakeBluetoothCharacteristic& test_characteristic = + battery_device_bundle().characteristic(); + + auto mock_descriptor = + std::make_unique<testing::NiceMock<device::MockBluetoothGattDescriptor>>( + &test_characteristic, "test_descriptor_id", + kCharacteristicUserDescriptionDescriptorUUID, + device::BluetoothRemoteGattCharacteristic::PERMISSION_NONE); + + test_characteristic.AddMockDescriptor(std::move(mock_descriptor)); + + base::test::TestFuture< + WebBluetoothResult, + std::optional< + std::vector<blink::mojom::WebBluetoothRemoteGATTDescriptorPtr>>> + get_descriptors_future; + service_ptr_->RemoteCharacteristicGetDescriptors( + test_characteristic.GetIdentifier(), + WebBluetoothGATTQueryQuantity::SINGLE, std::nullopt, + get_descriptors_future.GetCallback()); + + EXPECT_EQ(get_descriptors_future.Get<0>(), WebBluetoothResult::SUCCESS); + + const auto& descriptors = get_descriptors_future.Get<1>(); + ASSERT_TRUE(descriptors.has_value()); + ASSERT_FALSE(descriptors->empty()); + std::string descriptor_instance_id = descriptors->at(0)->instance_id; + + BluetoothBlocklist::Get().Add(test_characteristic.GetUUID(), + BluetoothBlocklist::Value::EXCLUDE_WRITES); + + std::vector<uint8_t> value = {1, 2, 3}; + base::test::TestFuture<WebBluetoothResult> write_future; + service_ptr_->RemoteDescriptorWriteValue(descriptor_instance_id, value, + write_future.GetCallback()); + + EXPECT_EQ(write_future.Get(), WebBluetoothResult::BLOCKLISTED_WRITE); +} + } // namespace content diff --git a/third_party/blink/web_tests/external/wpt/bluetooth/resources/bluetooth-fake-devices.js b/third_party/blink/web_tests/external/wpt/bluetooth/resources/bluetooth-fake-devices.js index c503bf7..37e126df 100644 --- a/third_party/blink/web_tests/external/wpt/bluetooth/resources/bluetooth-fake-devices.js +++ b/third_party/blink/web_tests/external/wpt/bluetooth/resources/bluetooth-fake-devices.js @@ -29,6 +29,7 @@ 'bad1c9a2-9a5b-4015-8b60-1579bbbf2135';
Regression Test / PoC
diff --git a/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc b/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
index 42523def..4880d79 100644
--- a/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
+++ b/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
@@ -32,6 +32,7 @@
#include "device/bluetooth/test/mock_bluetooth_adapter.h"
#include "device/bluetooth/test/mock_bluetooth_device.h"
#include "device/bluetooth/test/mock_bluetooth_gatt_characteristic.h"
+#include "device/bluetooth/test/mock_bluetooth_gatt_descriptor.h"
#include "device/bluetooth/test/mock_bluetooth_gatt_notify_session.h"
#include "device/bluetooth/test/mock_bluetooth_gatt_service.h"
#include "mojo/public/cpp/bindings/associated_receiver.h"
@@ -96,6 +97,8 @@
const device::BluetoothUUID kBatteryServiceUUID(kBatteryServiceUUIDString);
const device::BluetoothUUID kBatteryLevelCharacteristicUUID(
"00002a19-0000-1000-8000-00805f9b34fb");
+const device::BluetoothUUID kCharacteristicUserDescriptionDescriptorUUID(
+ "00002901-0000-1000-8000-00805f9b34fb");
const BluetoothDeviceBundleData battery_device_bundle_data = {
kBatteryServiceId, kBatteryLevelCharacteristicId, kBatteryServiceUUID,
kBatteryLevelCharacteristicUUID, kTestCharacteristicProperties};
@@ -559,6 +562,7 @@
heart_rate_device_bundle_.reset();
service_ptr_ = nullptr;
SetBrowserClientForTesting(old_browser_client_);
+ BluetoothBlocklist::Get().ResetToDefaultValuesForTest();
RenderViewHostImplTestHarness::TearDown();
}
@@ -1442,4 +1446,95 @@
EXPECT_CALL(*adapter_, StopScan).Times(1);
}
+TEST_F(WebBluetoothServiceImplTest,
+ RemoteDescriptorReadValue_ParentCharacteristicBlocklisted) {
+ RegisterTestCharacteristic();
+
+ FakeBluetoothCharacteristic& test_characteristic =
+ battery_device_bundle().characteristic();
+
+ auto mock_descriptor =
+ std::make_unique<testing::NiceMock<device::MockBluetoothGattDescriptor>>(
+ &test_characteristic, "test_descriptor_id",
+ kCharacteristicUserDescriptionDescriptorUUID,
+ device::BluetoothRemoteGattCharacteristic::PERMISSION_NONE);
+
+ test_characteristic.AddMockDescriptor(std::move(mock_descriptor));
+
+ base::test::TestFuture<
+ WebBluetoothResult,
+ std::optional<
+ std::vector<blink::mojom::WebBluetoothRemoteGATTDescriptorPtr>>>
+ get_descriptors_future;
+ service_ptr_->RemoteCharacteristicGetDescriptors(
+ test_characteristic.GetIdentifier(),
+ WebBluetoothGATTQueryQuantity::SINGLE, std::nullopt,
+ get_descriptors_future.GetCallback());
+
+ EXPECT_EQ(get_descriptors_future.Get<0>(), WebBluetoothResult::SUCCESS);
+
+ const auto& descriptors = get_descriptors_future.Get<1>();
+ ASSERT_TRUE(descriptors.has_value());
+ ASSERT_FALSE(descriptors->empty());
+ std::string descriptor_instance_id = descriptors->at(0)->instance_id;
+
+ BluetoothBlocklist::Get().Add(test_characteristic.GetUUID(),
+ BluetoothBlocklist::Value::EXCLUDE_READS);
+
+ base::test::TestFuture<WebBluetoothResult, std::vector<uint8_t>> read_future;
+ service_ptr_->RemoteDescriptorReadValue(
+ descriptor_instance_id,
+ base::BindLambdaForTesting(
+ [&read_future](WebBluetoothResult result,
+ base::span<const uint8_t> value) {
+ read_future.SetValue(
+ result, std::vector<uint8_t>(value.begin(), value.end()));
+ }));
+
+ EXPECT_EQ(read_future.Get<0>(), WebBluetoothResult::BLOCKLISTED_READ);
+}
+
+TEST_F(WebBluetoothServiceImplTest,
+ RemoteDescriptorWriteValue_ParentCharacteristicBlocklisted) {
+ RegisterTestCharacteristic();
+
+ FakeBluetoothCharacteristic& test_characteristic =
+ battery_device_bundle().characteristic();
+
+ auto mock_descriptor =
+ std::make_unique<testing::NiceMock<device::MockBluetoothGattDescriptor>>(
+ &test_characteristic, "test_descriptor_id",
+ kCharacteristicUserDescriptionDescriptorUUID,
+ device::BluetoothRemoteGattCharacteristic::PERMISSION_NONE);
+
+ test_characteristic.AddMockDescriptor(std::move(mock_descriptor));
+
+ base::test::TestFuture<
+ WebBluetoothResult,
+ std::optional<
+ std::vector<blink::mojom::WebBluetoothRemoteGATTDescriptorPtr>>>
+ get_descriptors_future;
+ service_ptr_->RemoteCharacteristicGetDescriptors(
+ test_characteristic.GetIdentifier(),
+ WebBluetoothGATTQueryQuantity::SINGLE, std::nullopt,
+ get_descriptors_future.GetCallback());
+
+ EXPECT_EQ(get_descriptors_future.Get<0>(), WebBluetoothResult::SUCCESS);
+
+ const auto& descriptors = get_descriptors_future.Get<1>();
+ ASSERT_TRUE(descriptors.has_value());
+ ASSERT_FALSE(descriptors->empty());
+ std::string descriptor_instance_id = descriptors->at(0)->instance_id;
+
+ BluetoothBlocklist::Get().Add(test_characteristic.GetUUID(),
+ BluetoothBlocklist::Value::EXCLUDE_WRITES);
+
+ std::vector<uint8_t> value = {1, 2, 3};
+ base::test::TestFuture<WebBluetoothResult> write_future;
+ service_ptr_->RemoteDescriptorWriteValue(descriptor_instance_id, value,
+ write_future.GetCallback());
+
+ EXPECT_EQ(write_future.Get(), WebBluetoothResult::BLOCKLISTED_WRITE);
+}
+
} // namespace content
diff --git a/third_party/blink/web_tests/external/wpt/bluetooth/resources/bluetooth-fake-devices.js b/third_party/blink/web_tests/external/wpt/bluetooth/resources/bluetooth-fake-devices.js
index c503bf7..37e126df 100644
--- a/third_party/blink/web_tests/external/wpt/bluetooth/resources/bluetooth-fake-devices.js
+++ b/third_party/blink/web_tests/external/wpt/bluetooth/resources/bluetooth-fake-devices.js
@@ -29,6 +29,7 @@
'bad1c9a2-9a5b-4015-8b60-1579bbbf2135';
var request_disconnection_characteristic_uuid =
'01d7d88a-7451-419f-aeb8-d65e7b9277af';
+var heart_rate_measurement_uuid = '00002a37-0000-1000-8000-00805f9b34fb';
/* Descriptor UUIDs */
var blocklist_test_descriptor_uuid = 'bad2ddcf-60db-45cd-bef9-fd72b153cf7c';
@@ -610,12 +611,25 @@
* objects.
*/
async function getBlocklistExcludeReadsDescriptor() {
- let result = await getBlocklistExcludeWritesCharacteristic();
- let descriptor = await result.characteristic.getDescriptor(
- blocklist_exclude_reads_descriptor_uuid);
+ let result = await getBlocklistTestService();
+
+ let fake_heart_rate_characteristic =
+ await result.fake_service.addFakeCharacteristic({
+ uuid: heart_rate_measurement_uuid,
+ properties: ['read', 'write'],
+ });
+
+ let fake_descriptor = await fake_heart_rate_characteristic.addFakeDescriptor(
+ {uuid: blocklist_exclude_reads_descriptor_uuid});
+
+ let characteristic = await result.service.getCharacteristic(heart_rate_measurement_uuid);
+ let descriptor = await characteristic.getDescriptor(blocklist_exclude_reads_descriptor_uuid);
+
return Object.assign(result, {
+ characteristic,
+ fake_characteristic: fake_heart_rate_characteristic,
descriptor,
- fake_descriptor: result.fake_blocklist_exclude_reads_descriptor
+ fake_descriptor
});
}
@@ -641,12 +655,25 @@
* objects.
*/
async function getBlocklistExcludeWritesDescriptor() {
- let result = await getBlocklistExcludeWritesCharacteristic();
- let descriptor = await result.characteristic.getDescriptor(
- 'gatt.client_characteristic_configuration');
+ let result = await getBlocklistTestService();
+
+ let fake_heart_rate_characteristic =
+ await result.fake_service.addFakeCharacteristic({
+ uuid: heart_rate_measurement_uuid,
+ properties: ['read', 'write'],
+ });
+
+ let fake_descriptor = await fake_heart_rate_characteristic.addFakeDescriptor(
+ {uuid: 'gatt.client_characteristic_configuration'});
+
+ let characteristic = await result.service.getCharacteristic(heart_rate_measurement_uuid);
+ let descriptor = await characteristic.getDescriptor('gatt.client_characteristic_configuration');
+
return Object.assign(result, {
- descriptor: descriptor,
- fake_descriptor: result.fake_blocklist_exclude_writes_descriptor,
+ characteristic,
+ fake_characteristic: fake_heart_rate_characteristic,
+ descriptor,
+ fake_descriptor
});
}
Original Bug Report
Missing GATT blocklist check for parent characteristic in Web Bluetooth descriptors
Flapjack, 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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: The Web Bluetooth implementation fails to check the GATT blocklist for a descriptor’s parent characteristic during read and write operations. This allows websites to bypass security restrictions and interact with descriptors of protected characteristics, violating the Web Bluetooth specification.
Affected files:
content/browser/bluetooth/web_bluetooth_service_impl.cc
Estimated timestamp from git blame: 2024-10-23
Summary
The GATT blocklist in Web Bluetooth is designed to restrict access to sensitive services, characteristics, and descriptors. However, the implementation of GATT descriptor read and write operations in WebBluetoothServiceImpl fails to verify the blocklist status of the descriptor’s parent characteristic. This omission allows a malicious site to read or write descriptors associated with a partially blocked characteristic, bypassing the security and privacy protections intended by the GATT blocklist.
Technical Details
In content/browser/bluetooth/web_bluetooth_service_impl.cc, the methods RemoteDescriptorReadValue and RemoteDescriptorWriteValue handle GATT descriptor interactions.
According to the Web Bluetooth specification:
- For
readValue()on a descriptor (§ 13.5): “If characteristic is in the Blocked Characteristic for Read UUIDs list, then return a promise rejected with a ‘SecurityError’ DOMException.” - For
writeValue()on a descriptor (§ 13.5): “If characteristic is in the Blocked Characteristic for Write UUIDs list, then return a promise rejected with a ‘SecurityError’ DOMException.”
The current implementation only checks whether the descriptor’s own UUID is in the blocklist, ignoring the status of the characteristic that owns it.
Vulnerable Code Snippets
In WebBluetoothServiceImpl::RemoteDescriptorReadValue:
1378: if (BluetoothBlocklist::Get().IsExcludedFromReads(
1379: query_result.descriptor->GetUUID())) {
1380: std::move(callback).Run(blink::mojom::WebBluetoothResult::BLOCKLISTED_READ,
1381: /*value=*/{});
1382: return;
1383: }
In WebBluetoothServiceImpl::RemoteDescriptorWriteValue:
1418: if (BluetoothBlocklist::Get().IsExcludedFromWrites(
1419: query_result.descriptor->GetUUID())) {
1420: std::move(callback).Run(
1421: blink::mojom::WebBluetoothResult::BLOCKLISTED_WRITE);
1422: return;
1423: }
Because characteristics entirely excluded from the blocklist (marked with Value::EXCLUDE) are filtered during discovery (WebBluetoothServiceImpl::RemoteServiceGetCharacteristics), they are unaffected. However, partially restricted characteristics (e.g., those marked with Value::EXCLUDE_READS or Value::EXCLUDE_WRITES) remain discoverable by the renderer, allowing an attacker to request their descriptors.
Potential Attack Steps
These are potential steps an attacker would follow to trigger the vulnerability:
- An attacker hosts a malicious website and tricks a user into pairing a Bluetooth device that exposes a partially blocklisted characteristic (e.g.,
2a02Peripheral Privacy Flag, which isEXCLUDE_WRITES). - The attacker’s JavaScript connects to the GATT server and requests the partially blocklisted characteristic (e.g.,
service.getCharacteristic('2a02')). This succeeds because it is not fully excluded. - The script retrieves the descriptors associated with this characteristic by calling
characteristic.getDescriptors(). - The script attempts to read or write to one of these descriptors (e.g.,
descriptor.writeValue(new Uint8Array([...]))). - The browser verifies the descriptor’s UUID against the blocklist, but fails to check the parent characteristic’s UUID.
- The browser allows the write operation, successfully bypassing the GATT blocklist restriction.
Suggested Fix
In WebBluetoothServiceImpl::RemoteDescriptorReadValue and WebBluetoothServiceImpl::RemoteDescriptorWriteValue, add a check for the parent characteristic’s blocklist status before allowing the operation.
For RemoteDescriptorReadValue:
if (BluetoothBlocklist::Get().IsExcludedFromReads(
query_result.descriptor->GetUUID()) ||
BluetoothBlocklist::Get().IsExcludedFromReads(
query_result.characteristic->GetUUID())) {
std::move(callback).Run(blink::mojom::WebBluetoothResult::BLOCKLISTED_READ,
/*value=*/{});
return;
}
For RemoteDescriptorWriteValue:
if (BluetoothBlocklist::Get().IsExcludedFromWrites(
query_result.descriptor->GetUUID()) ||
BluetoothBlocklist::Get().IsExcludedFromWrites(
query_result.characteristic->GetUUID())) {
std::move(callback).Run(
blink::mojom::WebBluetoothResult::BLOCKLISTED_WRITE);
return;
}
Evaluated with Chrome root at commit: c0eb5541aebfa4ea08806eaf6e94bcc69f87ab2f
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.