CVE-2026-11141
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
AudioParametersBuilderservices/audio/snooper_node_unittest.cc |
modified | |
SnooperNodeTestservices/audio/snooper_node_unittest.cc |
modified | |
forservices/audio/snooper_node_unittest.cc |
modified | |
switchservices/audio/snooper_node_unittest.cc |
modified |
Files Changed
services/audio/snooper_node.ccservices/audio/snooper_node_unittest.cc
Patch
From 257d15cb760fbb2e48949af8cd79a4c85ef808ee Mon Sep 17 00:00:00 2001
From: Syed AbuTalib <lowkey@google.com>
Date: Mon, 27 Apr 2026 14:17:10 -0700
Subject: [PATCH] Enable Channel Mixing for DISCRETE with differing counts
It is still worth using ChannelMixStrategy for `DISCRETE` cases as it is
not a guarantee that the input/output params have the same channel
count.
Also, the unittest file was optimized for testing out a max of stereo
(2) layout, updated the unittests to handle higher channel counts and
mixing between the higher channel counts.
Bug: 501667839
Change-Id: I7e3cacddebb85f7cbe346087f8a58048947b4293
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7759235
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Commit-Queue: Syed AbuTalib <lowkey@google.com>
Reviewed-by: Thomas Guilbert <tguilbert@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1621314}
---
diff --git a/services/audio/snooper_node.cc b/services/audio/snooper_node.cc
index 06709f97..f4af944 100644
--- a/services/audio/snooper_node.cc
+++ b/services/audio/snooper_node.cc
@@ -73,7 +73,8 @@
base::BindRepeating(&SnooperNode::ReadFromDelayBuffer,
base::Unretained(this))),
channel_mix_strategy_(
- (input_params_.channel_layout() == output_params_.channel_layout())
+ (input_params_.channel_layout_config() ==
+ output_params_.channel_layout_config())
? ChannelMixStrategy::kNone
: ((output_params_.channels() < input_params_.channels())
? ChannelMixStrategy::kBefore
diff --git a/services/audio/snooper_node_unittest.cc b/services/audio/snooper_node_unittest.cc
index 9ad44924..945fe20 100644
--- a/services/audio/snooper_node_unittest.cc
+++ b/services/audio/snooper_node_unittest.cc
@@ -32,9 +32,10 @@
// something finite.
constexpr float kInvalidAudioSample = std::numeric_limits<float>::infinity();
-// The tones the source should generate into the left and right channels.
-constexpr double kLeftChannelFrequency = 500.0;
-constexpr double kRightChannelFrequency = 1200.0;
+// The tones the source should generate into the channels, and the alternate
+// frequency to use when testing swapped/discontinuous input.
+constexpr double kDefaultFrequency = 500.0;
+constexpr double kSwappedFrequency = 1200.0;
constexpr double kSourceVolume = 0.5;
// The duration of the audio that flows through the SnooperNode for each test.
@@ -50,29 +51,59 @@
constexpr std::string_view kDumpAsWavSwitch = "dump-as-wav";
// Test parameters.
-struct InputAndOutputParams {
- media::AudioParameters input;
- media::AudioParameters output;
-};
+class AudioParametersBuilder {
+ public:
+ AudioParametersBuilder() = default;
-// Helper so that gtest can produce useful logging of the test parameters.
-std::ostream& operator<<(std::ostream& out,
- const InputAndOutputParams& test_params) {
- return out << "{input=" << test_params.input.AsHumanReadableString()
- << ", output=" << test_params.output.AsHumanReadableString()
- << "}";
-}
+ AudioParametersBuilder& WithLayout(media::ChannelLayout layout,
+ int channels) {
+ layout_ = layout;
+ channels_ = channels;
+ return *this;
+ }
+ AudioParametersBuilder& WithRate(int rate) {
+ rate_ = rate;
+ return *this;
+ }
+ AudioParametersBuilder& WithFrames(int frames) {
+ frames_ = frames;
+ return *this;
+ }
+
+ media::AudioParameters Build() const {
+ return media::AudioParameters(
+ media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
+ media::ChannelLayoutConfig(layout_, channels_), rate_, frames_);
+ }
+
+ private:
+ media::ChannelLayout layout_ = media::CHANNEL_LAYOUT_STEREO;
+ int channels_ = 2;
+ int rate_ = 48000;
+ int frames_ = 480;
+};
+struct InputAndOutputParams {
+ AudioParametersBuilder input;
+ AudioParametersBuilder output;
+ std::string name;
+};
class SnooperNodeTest : public testing::TestWithParam<InputAndOutputParams> {
public:
SnooperNodeTest() = default;
~SnooperNodeTest() override = default;
- const media::AudioParameters& input_params() const {
- return GetParam().input;
+ media::AudioParameters input_params() {
+ if (!input_params_.IsValid()) {
+ input_params_ = GetParam().input.Build();
+ }
+ return input_params_;
}
- const media::AudioParameters& output_params() const {
- return GetParam().output;
+ media::AudioParameters output_params() {
+ if (!output_params_.IsValid()) {
+ output_params_ = GetParam().output.Build();
+ }
+ return output_params_;
}
base::TimeDelta output_delay() const { return output_delay_; }
double max_relative_error() const { return max_relative_error_; }
@@ -80,6 +111,40 @@
base::TestMockTimeTaskRunner* task_runner() const {
return task_runner_.get();
}
+ // Builds a gain matrix so tests can compute the expected output without
+ // hardcoding the values.
+ void PrecomputeExpectedMixMatrix() {
+ const int in_channels = input_params().channels();
+ const int out_channels = output_params().channels();
+ expected_mix_matrix_.assign(in_channels, std::vector<double>(out_channels));
+
+ auto input_bus = media::AudioBus::Create(in_channels, /*frames=*/1);
+ auto output_bus = media::AudioBus::Create(out_channels, /*frames=*/1);
+ media::ChannelMixer mixer(input_params(), output_params());
+
+ for (int in_ch = 0; in_ch < in_channels; ++in_ch) {
+ input_bus->Zero();
+ input_bus->channel(in_ch)[0] = 1.0f;
+ mixer.Transform(input_bus.get(), output_bus.get());
+ for (int out_ch = 0; out_ch < out_channels; ++out_ch) {
+ expected_mix_matrix_[in_ch][out_ch] =
+ static_cast<double>(output_bus->channel(out_ch)[0]);
+ }
+ }
+ }
+
+ double GetExpectedGain(int input_ch, int output_ch) const {
+ return expected_mix_matrix_[input_ch][output_ch];
+ }
+
+ bool IsOutputChannelSilent(int out_ch) {
+ for (int in_ch = 0; in_ch < input_params().channels(); ++in_ch) {
+ if (GetExpectedGain(in_ch, out_ch) > 0.0) {
+ return false;
+ }
+ }
+ return true;
+ }
FakeLoopbackGroupMember* group_member() { return &*group_member_; }
SnooperNode* node() { return &*node_; }
FakeConsumer* consumer() { return &*consumer_; }
@@ -131,6 +196,8 @@
// "huge" to ensure time calculations are being tested for overflow cases.
task_runner_ = base::MakeRefCounted<base::TestMockTimeTaskRunner>(
base::Time(), base::TimeTicks() + base::Microseconds(INT64_C(1) << 62));
+
+ PrecomputeExpectedMixMatrix();
}
void TearDown() override {
@@ -148,54 +215,10 @@
}
}
- // Selects which frequency to return for each channel based on the input and
- // output channel layout.
- enum WhichFlow : int8_t {
- FOR_INPUT,
- FOR_SWAPPED_INPUT,
- FOR_OUTPUT,
- FOR_SWAPPED_OUTPUT,
- };
-
- double GetLeftChannelFrequency(WhichFlow which) const {
- switch (which) {
- case FOR_INPUT:
- case FOR_OUTPUT:
- return kLeftChannelFrequency;
- case FOR_SWAPPED_INPUT:
- case FOR_SWAPPED_OUTPUT:
- return kRightChannelFrequency;
Regression Test / PoC
diff --git a/services/audio/snooper_node_unittest.cc b/services/audio/snooper_node_unittest.cc
index 9ad44924..945fe20 100644
--- a/services/audio/snooper_node_unittest.cc
+++ b/services/audio/snooper_node_unittest.cc
@@ -32,9 +32,10 @@
// something finite.
constexpr float kInvalidAudioSample = std::numeric_limits<float>::infinity();
-// The tones the source should generate into the left and right channels.
-constexpr double kLeftChannelFrequency = 500.0;
-constexpr double kRightChannelFrequency = 1200.0;
+// The tones the source should generate into the channels, and the alternate
+// frequency to use when testing swapped/discontinuous input.
+constexpr double kDefaultFrequency = 500.0;
+constexpr double kSwappedFrequency = 1200.0;
constexpr double kSourceVolume = 0.5;
// The duration of the audio that flows through the SnooperNode for each test.
@@ -50,29 +51,59 @@
constexpr std::string_view kDumpAsWavSwitch = "dump-as-wav";
// Test parameters.
-struct InputAndOutputParams {
- media::AudioParameters input;
- media::AudioParameters output;
-};
+class AudioParametersBuilder {
+ public:
+ AudioParametersBuilder() = default;
-// Helper so that gtest can produce useful logging of the test parameters.
-std::ostream& operator<<(std::ostream& out,
- const InputAndOutputParams& test_params) {
- return out << "{input=" << test_params.input.AsHumanReadableString()
- << ", output=" << test_params.output.AsHumanReadableString()
- << "}";
-}
+ AudioParametersBuilder& WithLayout(media::ChannelLayout layout,
+ int channels) {
+ layout_ = layout;
+ channels_ = channels;
+ return *this;
+ }
+ AudioParametersBuilder& WithRate(int rate) {
+ rate_ = rate;
+ return *this;
+ }
+ AudioParametersBuilder& WithFrames(int frames) {
+ frames_ = frames;
+ return *this;
+ }
+
+ media::AudioParameters Build() const {
+ return media::AudioParameters(
+ media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
+ media::ChannelLayoutConfig(layout_, channels_), rate_, frames_);
+ }
+
+ private:
+ media::ChannelLayout layout_ = media::CHANNEL_LAYOUT_STEREO;
+ int channels_ = 2;
+ int rate_ = 48000;
+ int frames_ = 480;
+};
+struct InputAndOutputParams {
+ AudioParametersBuilder input;
+ AudioParametersBuilder output;
+ std::string name;
+};
class SnooperNodeTest : public testing::TestWithParam<InputAndOutputParams> {
public:
SnooperNodeTest() = default;
~SnooperNodeTest() override = default;
- const media::AudioParameters& input_params() const {
- return GetParam().input;
+ media::AudioParameters input_params() {
+ if (!input_params_.IsValid()) {
+ input_params_ = GetParam().input.Build();
+ }
+ return input_params_;
}
- const media::AudioParameters& output_params() const {
- return GetParam().output;
+ media::AudioParameters output_params() {
+ if (!output_params_.IsValid()) {
+ output_params_ = GetParam().output.Build();
+ }
+ return output_params_;
}
base::TimeDelta output_delay() const { return output_delay_; }
double max_relative_error() const { return max_relative_error_; }
@@ -80,6 +111,40 @@
base::TestMockTimeTaskRunner* task_runner() const {
return task_runner_.get();
}
+ // Builds a gain matrix so tests can compute the expected output without
+ // hardcoding the values.
+ void PrecomputeExpectedMixMatrix() {
+ const int in_channels = input_params().channels();
+ const int out_channels = output_params().channels();
+ expected_mix_matrix_.assign(in_channels, std::vector<double>(out_channels));
+
+ auto input_bus = media::AudioBus::Create(in_channels, /*frames=*/1);
+ auto output_bus = media::AudioBus::Create(out_channels, /*frames=*/1);
+ media::ChannelMixer mixer(input_params(), output_params());
+
+ for (int in_ch = 0; in_ch < in_channels; ++in_ch) {
+ input_bus->Zero();
+ input_bus->channel(in_ch)[0] = 1.0f;
+ mixer.Transform(input_bus.get(), output_bus.get());
+ for (int out_ch = 0; out_ch < out_channels; ++out_ch) {
+ expected_mix_matrix_[in_ch][out_ch] =
+ static_cast<double>(output_bus->channel(out_ch)[0]);
+ }
+ }
+ }
+
+ double GetExpectedGain(int input_ch, int output_ch) const {
+ return expected_mix_matrix_[input_ch][output_ch];
+ }
+
+ bool IsOutputChannelSilent(int out_ch) {
+ for (int in_ch = 0; in_ch < input_params().channels(); ++in_ch) {
+ if (GetExpectedGain(in_ch, out_ch) > 0.0) {
+ return false;
+ }
+ }
+ return true;
+ }
FakeLoopbackGroupMember* group_member() { return &*group_member_; }
SnooperNode* node() { return &*node_; }
FakeConsumer* consumer() { return &*consumer_; }
@@ -131,6 +196,8 @@
// "huge" to ensure time calculations are being tested for overflow cases.
task_runner_ = base::MakeRefCounted<base::TestMockTimeTaskRunner>(
base::Time(), base::TimeTicks() + base::Microseconds(INT64_C(1) << 62));
+
+ PrecomputeExpectedMixMatrix();
}
void TearDown() override {
@@ -148,54 +215,10 @@
}
}
- // Selects which frequency to return for each channel based on the input and
- // output channel layout.
- enum WhichFlow : int8_t {
- FOR_INPUT,
- FOR_SWAPPED_INPUT,
- FOR_OUTPUT,
- FOR_SWAPPED_OUTPUT,
- };
-
- double GetLeftChannelFrequency(WhichFlow which) const {
- switch (which) {
- case FOR_INPUT:
- case FOR_OUTPUT:
- return kLeftChannelFrequency;
- case FOR_SWAPPED_INPUT:
- case FOR_SWAPPED_OUTPUT:
- return kRightChannelFrequency;
- }
- }
-
- double GetRightChannelFrequency(WhichFlow which) const {
- switch (which) {
- // If the test parameters call for stereo→mono channel down-mixing, use
- // the left channel frequency again for the right channel input. Down-
- // mixing is tested elsewhere.
- case FOR_INPUT:
- return (output_params().channels() == 1 ? kLeftChannelFrequency
- : kRightChannelFrequency);
- case FOR_SWAPPED_INPUT:
- return (output_params().channels() == 1 ? kRightChannelFrequency
- : kLeftChannelFrequency);
-
- // If the input was monaural, the output's right channel should contain
- // the input's "left" channel frequency.
- case FOR_OUTPUT:
- return (input_params().channels() == 1 ? kLeftChannelFrequency
- : kRightChannelFrequency);
- case FOR_SWAPPED_OUTPUT:
- return (input_params().channels() == 1 ? kRightChannelFrequency
- : kLeftChannelFrequency);
- }
- }
-
void CreateNewPipeline() {
group_member_.emplace(input_params());
- group_member_->SetChannelTone(0, GetLeftChannelFrequency(FOR_INPUT));
- if (input_params().channels() > 1) {
- group_member_->SetChannelTone(1, GetRightChannelFrequency(FOR_INPUT));
+ for (int ch = 0; ch < input_params().channels(); ++ch) {
+ group_member_->SetChannelTone(ch, kDefaultFrequency);
}
group_member_->SetVolume(kSourceVolume);
@@ -239,6 +262,32 @@
consumer_->Consume(*bus);
}
+ // Used to check each output channel is producing the correct tones at
+ // `position`.
+ void VerifyTonesAt(int position, double freq) {
+ for (int out_ch = 0; out_ch < output_params().channels(); ++out_ch) {
+ double expected_vol = 0.0;
+ for (int in_ch = 0; in_ch < input_params().channels(); ++in_ch) {
+ expected_vol += GetExpectedGain(in_ch, out_ch) * kSourceVolume;
+ }
+ if (expected_vol == 0.0) {
+ continue;
+ }
+ EXPECT_NEAR(expected_vol,
+ consumer()->ComputeAmplitudeAt(out_ch, freq, position),
+ expected_vol * max_relative_error())
+ << "Failure for output ch " << out_ch;
+ }
+ }
+
+ void VerifySilenceInRange(int start_frame, int end_frame) {
+ for (int ch = 0; ch < output_params().channels(); ++ch) {
+ EXPECT_TRUE(consumer()->IsSilentInRange(ch, start_frame, end_frame))
+ << "Channel " << ch << " expected to be silent between "
+ << start_frame << " and " << end_frame;
+ }
+ }
+
// Post delayed tasks to schedule normal, uninterrupted input with the default
// kInputAdvanceTime delay.
void ScheduleDefaultInputTasks(double skew = 1.0) {
@@ -286,6 +335,11 @@
private:
scoped_refptr<base::TestMockTimeTaskRunner> task_runner_;
+ media::AudioParameters input_params_;
+ media::AudioParameters output_params_;
+
+ std::vector<std::vector<double>> expected_mix_matrix_;
+
// A suitable output delay to use for rendering audio from the pipeline. See
// comments in SetUp() for further details.
base::TimeDelta output_delay_;
@@ -333,17 +387,7 @@
((input_skew * kInputAdvanceTime.InSecondsF()) +
(output_skew * output_delay().InSecondsF())) *
output_params().sample_rate();
- const double frames_in_one_millisecond =
- output_params().sample_rate() /
- double{base::Time::kMillisecondsPerSecond};
- EXPECT_NEAR(expected_end_of_silence_position,
- consumer()->FindEndOfSilence(0, 0),
- frames_in_one_millisecond);
- if (output_params().channels() > 1) {
- EXPECT_NEAR(expected_end_of_silence_position,
- consumer()->FindEndOfSilence(1, 0),
- frames_in_one_millisecond);
- }
+ VerifySilenceInRange(0, expected_end_of_silence_position);
// Analyze the recording in several places for the expected tones.
constexpr int kNumToneChecks = 16;
@@ -351,16 +395,7 @@
const int end_frame =
consumer()->GetRecordedFrameCount() * i / kNumToneChecks;
SCOPED_TRACE(testing::Message() << "end_frame=" << end_frame);
- EXPECT_NEAR(kSourceVolume,
- consumer()->ComputeAmplitudeAt(
- 0, GetLeftChannelFrequency(FOR_OUTPUT), end_frame),
- kSourceVolume * max_relative_error());
- if (output_params().channels() > 1) {
- EXPECT_NEAR(kSourceVolume,
- consumer()->ComputeAmplitudeAt(
- 1, GetRightChannelFrequency(FOR_OUTPUT), end_frame),
- kSourceVolume * max_relative_error());
- }
+ VerifyTonesAt(end_frame, kDefaultFrequency);
}
if (HasFailure()) {
@@ -425,18 +460,7 @@
// Just before the drop, there should be a tone.
const int position_a_little_before_silence_begins =
output_silence_position - output_frames_in_20_milliseconds;
- EXPECT_NEAR(
- kSourceVolume,
- consumer()->ComputeAmplitudeAt(0, GetLeftChannelFrequency(FOR_OUTPUT),
- position_a_little_before_silence_begins),
- kSourceVolume * max_relative_error());
- if (output_params().channels() > 1) {
- EXPECT_NEAR(kSourceVolume,
- consumer()->ComputeAmplitudeAt(
- 1, GetRightChannelFrequency(FOR_OUTPUT),
- position_a_little_before_silence_begins),
- kSourceVolume * max_relative_error());
- }
+ VerifyTonesAt(position_a_little_before_silence_begins, kDefaultFrequency);
... (truncated)
Original Bug Report
Uninitialized heap leak in SnooperNode due to mismatched DISCRETE channel counts
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 compromised renderer can potentially leak uninitialized heap memory from the Audio Service by requesting a loopback stream with a higher discrete channel count than the source stream. Due to a flaw in channel mixing strategy selection, the audio resampler fails to overwrite all channels in the uninitialized output buffer. This leaves uninitialized heap data in the remaining channels, which is subsequently transferred to the renderer via shared memory.
Affected files:
services/audio/snooper_node.ccmedia/base/multi_channel_resampler.ccservices/audio/loopback_stream.ccservices/audio/loopback_signal_provider.ccservices/audio/input_sync_writer.ccmedia/base/audio_bus.cc
Estimated timestamp from git blame: 2025-12-12
Summary
There is a potential information leak in the Chrome audio service’s loopback mechanism. When a renderer requests a loopback stream using CHANNEL_LAYOUT_DISCRETE with a higher channel count than the source stream, SnooperNode incorrectly assumes that no channel mixing is required. Consequently, the audio resampler only populates a subset of the output buffer’s channels. The remaining channels retain uninitialized heap memory, which is then copied into a shared memory region accessible by the renderer.
Vulnerability Details
- Flawed Strategy Selection: In
services/audio/snooper_node.cc, theSnooperNodeconstructor selects aChannelMixStrategyby comparing thechannel_layout()of the input and output parameters. If both areCHANNEL_LAYOUT_DISCRETE, they compare as equal even if their channel counts differ. As a result,ChannelMixStrategy::kNoneis incorrectly selected for mismatched channel counts. - Uninitialized Memory Allocation: In
services/audio/loopback_stream.cc, theLoopbackSignalForwarderconstructor allocates amix_bus_usingmedia::AudioBus::Create. Under the hood (media/base/audio_bus.cc), this usesbase::AlignedUninit<float>, meaning the allocated memory is not zeroed and contains whatever heap data previously occupied the space. - Bypassed Zeroing: During
SnooperNode::Render(), theoutput_bus(which is the uninitializedmix_bus_) is only explicitly zeroed if recording has not yet started (estimated_output_position == kNullPosition). If the source stream has already sent data (SnooperNode::OnDatahas been called), this zeroing is bypassed. - Partial Resampling: Because
ChannelMixStrategy::kNonewas selected,Render()passes the output bus directly toMultiChannelResampler::Resample. However, the resampler is initialized with the minimum of the input and output channel counts. If the input has 1 channel and the output has 32, the resampler only processes and overwrites the first channel. Channels 1 through 31 remain entirely untouched and uninitialized. - Data Transfer: Finally,
InputSyncWriter::WriteDataToCurrentSegmentusesmedia::AudioBus::CopyToto copy the entire 32-channel bus into anUnsafeSharedMemoryRegion. The uninitialized heap data is sent to the renderer process.
Impact
A compromised renderer with a valid media capture session (e.g., getDisplayMedia()) could read sensitive audio service heap data.
- On ChromeOS and Android, the audio service runs within the browser process.
- On Linux, it runs as an unsandboxed utility process.
The leak could be up to ~95MB per packet (32 channels * 768,000 frames). This uninitialized memory could contain pointers, tokens, or previously freed cross-origin audio data, providing a powerful primitive for bypassing ASLR and aiding in a sandbox escape.
Suggested Reproduction Steps
Note: Our tooling agent does not have the ability to run code, so these are potential steps to trigger the issue based on code analysis.
- From a compromised renderer with a
getDisplayMediasession, create an active audio output stream withformat=AUDIO_FAKE,channel_layout=CHANNEL_LAYOUT_DISCRETE, andchannels=1. - Request a loopback stream via
RendererAudioInputStreamFactory.CreateStreamusingchannel_layout=CHANNEL_LAYOUT_DISCRETEandchannels=32. - Call
AudioInputStream.Record()to start the loopback stream. - Because the source stream is already active,
SnooperNode::OnDatawill trigger beforeSnooperNode::Render, bypassing the zero-fill safety check. - Read the resulting audio data from the
UnsafeSharedMemoryRegion. Channel 0 will contain the resampled audio, while channels 1-31 will contain leaked heap memory from the audio service process.
Suggested Fix
In services/audio/snooper_node.cc, update the condition for selecting ChannelMixStrategy::kNone to explicitly require that both the channel layouts and the channel counts match:
channel_mix_strategy_(
(input_params_.channel_layout() == output_params_.channel_layout() &&
input_params_.channels() == output_params_.channels())
? ChannelMixStrategy::kNone
: ((output_params_.channels() < input_params_.channels())
? ChannelMixStrategy::kBefore
: ChannelMixStrategy::kAfter)),
Alternatively, ensure that mix_bus_ is explicitly zeroed (e.g., using mix_bus_->Zero()) upon creation in LoopbackSignalForwarder, or that SnooperNode::Render zeros any channels that the resampler will not overwrite.
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.