From b3c0c32851d5c7a5bdbf228e165876ecafc523f4 Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Mon, 20 Jul 2026 19:39:34 +0000 Subject: [PATCH 01/10] Migration script for setting existing fuzzers as trusted --- .../butler/scripts/migrate_fuzzer_trusted.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/local/butler/scripts/migrate_fuzzer_trusted.py diff --git a/src/local/butler/scripts/migrate_fuzzer_trusted.py b/src/local/butler/scripts/migrate_fuzzer_trusted.py new file mode 100644 index 00000000000..350c07c55d1 --- /dev/null +++ b/src/local/butler/scripts/migrate_fuzzer_trusted.py @@ -0,0 +1,39 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Migration script to set trusted=True for existing Fuzzers.""" + +from clusterfuzz._internal.datastore import data_types + + +def execute(args): + """Migrate Fuzzer entities to set trusted=True. + + Existing fuzzers are assumed to be trusted. + """ + print('Starting Fuzzer.trusted migration.') + count = 0 + + # We query all fuzzers. Since 'trusted' is a new field with default=False, + # existing entities might not have it set, or it might be False. + # We want to set it to True for all of them to maintain existing behavior. + for fuzzer in data_types.Fuzzer.query(): + if not args.non_dry_run: + print(f'DRY RUN: Would set trusted=True for fuzzer: {fuzzer.name}') + else: + fuzzer.trusted = True + fuzzer.put() + print(f'Updated fuzzer: {fuzzer.name} to trusted=True') + count += 1 + + print(f'Migration complete. Processed {count} Fuzzer entities.') From 7113b65a459d0918de2117b0d314e835d7a86a96 Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Wed, 22 Jul 2026 18:18:48 +0000 Subject: [PATCH 02/10] Change default of Fuzzer.trusted to True and remove migration script --- .../butler/scripts/migrate_fuzzer_trusted.py | 39 ------------------- 1 file changed, 39 deletions(-) delete mode 100644 src/local/butler/scripts/migrate_fuzzer_trusted.py diff --git a/src/local/butler/scripts/migrate_fuzzer_trusted.py b/src/local/butler/scripts/migrate_fuzzer_trusted.py deleted file mode 100644 index 350c07c55d1..00000000000 --- a/src/local/butler/scripts/migrate_fuzzer_trusted.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Migration script to set trusted=True for existing Fuzzers.""" - -from clusterfuzz._internal.datastore import data_types - - -def execute(args): - """Migrate Fuzzer entities to set trusted=True. - - Existing fuzzers are assumed to be trusted. - """ - print('Starting Fuzzer.trusted migration.') - count = 0 - - # We query all fuzzers. Since 'trusted' is a new field with default=False, - # existing entities might not have it set, or it might be False. - # We want to set it to True for all of them to maintain existing behavior. - for fuzzer in data_types.Fuzzer.query(): - if not args.non_dry_run: - print(f'DRY RUN: Would set trusted=True for fuzzer: {fuzzer.name}') - else: - fuzzer.trusted = True - fuzzer.put() - print(f'Updated fuzzer: {fuzzer.name} to trusted=True') - count += 1 - - print(f'Migration complete. Processed {count} Fuzzer entities.') From bb1e742cb4ba2e717a0552d1ee980ba689dc2dc6 Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Fri, 19 Jun 2026 15:27:07 +0000 Subject: [PATCH 03/10] Filter out untrusted fuzzers from running on long-lived bots --- .../_internal/fuzzing/fuzzer_selection.py | 13 ++++++++ .../core/fuzzing/fuzzer_selection_test.py | 31 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/clusterfuzz/_internal/fuzzing/fuzzer_selection.py b/src/clusterfuzz/_internal/fuzzing/fuzzer_selection.py index 0d3edfec30b..079d95fae3a 100644 --- a/src/clusterfuzz/_internal/fuzzing/fuzzer_selection.py +++ b/src/clusterfuzz/_internal/fuzzing/fuzzer_selection.py @@ -170,6 +170,19 @@ def get_fuzz_task_payload(platform=None): jobs = get_job_list(jobs_selection) selected_mappings = [entity for entity in mappings if entity.job in jobs] + if not environment.is_uworker(): + untrusted_fuzzers = { + fuzzer.name for fuzzer in ndb_utils.get_all_from_query( + data_types.Fuzzer.query( + data_types.Fuzzer.untrusted_content == True)) # pylint: disable=singleton-comparison + } + + if untrusted_fuzzers: + selected_mappings = [ + mapping for mapping in selected_mappings + if mapping.fuzzer not in untrusted_fuzzers + ] + if not selected_mappings: return None, None diff --git a/src/clusterfuzz/_internal/tests/core/fuzzing/fuzzer_selection_test.py b/src/clusterfuzz/_internal/tests/core/fuzzing/fuzzer_selection_test.py index 4859eb12db6..b7e2c061642 100644 --- a/src/clusterfuzz/_internal/tests/core/fuzzing/fuzzer_selection_test.py +++ b/src/clusterfuzz/_internal/tests/core/fuzzing/fuzzer_selection_test.py @@ -325,6 +325,37 @@ def test_platform_restriction(self, local_development): argument, job = fuzzer_selection.get_fuzz_task_payload('ANDROID:PIXEL6') self.assertEqual(('pixel_fuzzer', 'job_pixel6'), (argument, job)) + @parameterized.parameterized.expand([('False',), ('True',)]) + def test_untrusted_fuzzer_exclusion(self, is_uworker): + """Ensure that untrusted fuzzers are filtered out on long-lived bots.""" + os.environ['UWORKER'] = is_uworker + + data_types.Fuzzer(name='untrusted_fuzzer', untrusted_content=True).put() + data_types.Fuzzer(name='trusted_fuzzer', untrusted_content=False).put() + + untrusted_mapping = data_types.FuzzerJob() + untrusted_mapping.fuzzer = 'untrusted_fuzzer' + untrusted_mapping.job = 'job_1' + untrusted_mapping.platform = 'linux' + untrusted_mapping.put() + + trusted_mapping = data_types.FuzzerJob() + trusted_mapping.fuzzer = 'trusted_fuzzer' + trusted_mapping.job = 'job_2' + trusted_mapping.platform = 'linux' + trusted_mapping.put() + + data_types.FuzzerJobs( + platform='linux', fuzzer_jobs=[untrusted_mapping, + trusted_mapping]).put() + + argument, job = fuzzer_selection.get_fuzz_task_payload('linux') + + if is_uworker == 'True': + self.assertEqual(('untrusted_fuzzer', 'job_1'), (argument, job)) + else: + self.assertEqual(('trusted_fuzzer', 'job_2'), (argument, job)) + @test_utils.with_cloud_emulators('datastore') class UpdatePlatformForJobTest(unittest.TestCase): From af9e6721e7769e8cc23a6b825e8d13f4034bafd9 Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Tue, 7 Jul 2026 17:52:53 +0000 Subject: [PATCH 04/10] add check when getting tasks with full utask model --- src/clusterfuzz/_internal/base/tasks/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/clusterfuzz/_internal/base/tasks/__init__.py b/src/clusterfuzz/_internal/base/tasks/__init__.py index acfb8f5de87..5177e7d4375 100644 --- a/src/clusterfuzz/_internal/base/tasks/__init__.py +++ b/src/clusterfuzz/_internal/base/tasks/__init__.py @@ -235,6 +235,13 @@ def get_regular_task(queue=None): task = get_task_from_message(messages[0], queue) if task: + if task.command == 'fuzz' and not environment.is_tworker(): + fuzzer = data_types.Fuzzer.query( + data_types.Fuzzer.name == task.argument).get() + if fuzzer and fuzzer.untrusted: + logs.warning( + f'Skipping untrusted fuzzer {task.argument} on long-lived bot.') + continue return task From 43e6573802f44f267ceaf14fe0c3645d83d9b95e Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Tue, 7 Jul 2026 18:14:22 +0000 Subject: [PATCH 05/10] Harden untrusted fuzzer execution safety with runtime assertions --- .../_internal/bot/tasks/utasks/fuzz_task.py | 1 + .../_internal/bot/tasks/utasks/uworker_io.py | 12 ++++++ .../core/bot/tasks/utasks/uworker_io_test.py | 37 +++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py b/src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py index 3b6f9ac4447..2bdecdfe47b 100644 --- a/src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py +++ b/src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py @@ -2125,6 +2125,7 @@ def run(self): time.sleep(failure_wait_interval) return uworker_msg_pb2.Output( # pylint: disable=no-member error_type=uworker_msg_pb2.ErrorType.FUZZ_NO_FUZZER) # pylint: disable=no-member + uworker_io.check_running_fuzzer_safe(self.fuzzer) # Update the session's test_timeout to use the fuzzer's timeout (if any). # When the fuzzer has a specified timeout, `update_fuzzer_and_data_bundles` diff --git a/src/clusterfuzz/_internal/bot/tasks/utasks/uworker_io.py b/src/clusterfuzz/_internal/bot/tasks/utasks/uworker_io.py index 7380a512890..db71ebf0b86 100644 --- a/src/clusterfuzz/_internal/bot/tasks/utasks/uworker_io.py +++ b/src/clusterfuzz/_internal/bot/tasks/utasks/uworker_io.py @@ -243,6 +243,18 @@ def check_handling_testcase_safe(testcase): logs.warning(f'Cannot handle {testcase.key.id()} in trusted task.') +def check_running_fuzzer_safe(fuzzer): + """Exits when the fuzzer is untrusted but the execution environment is + trusted.""" + if not fuzzer.untrusted: + return + if environment.is_uworker(): + return + logs.log_fatal_and_exit( + f'Security Violation: Cannot run untrusted fuzzer {fuzzer.name} ' + f'in trusted environment.') + + def timestamp_to_proto_timestamp(pydt) -> Timestamp: proto_timestamp = Timestamp() proto_timestamp.FromDatetime(pydt) diff --git a/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/uworker_io_test.py b/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/uworker_io_test.py index 527a821d272..7a6f36b1a28 100644 --- a/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/uworker_io_test.py +++ b/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/uworker_io_test.py @@ -387,3 +387,40 @@ def test_issue_metadata_field(self): wire_format = uworker_io.serialize_uworker_input(output) deserialized = uworker_io.deserialize_uworker_output(wire_format) self.assertEqual(json.loads(deserialized.issue_metadata), metadata) + + +class TestCheckRunningFuzzerSafe(unittest.TestCase): + """Tests check_running_fuzzer_safe.""" + + def setUp(self): + helpers.patch(self, [ + 'clusterfuzz._internal.system.environment.is_uworker', + 'clusterfuzz._internal.metrics.logs.log_fatal_and_exit', + ]) + self.fuzzer = mock.MagicMock(spec=data_types.Fuzzer) + self.fuzzer.name = 'test_fuzzer' + + def test_trusted_fuzzer(self): + """Test that trusted fuzzer passes without checks.""" + self.fuzzer.untrusted = False + uworker_io.check_running_fuzzer_safe(self.fuzzer) + self.mock.is_uworker.assert_not_called() + self.mock.log_fatal_and_exit.assert_not_called() + + def test_untrusted_fuzzer_uworker(self): + """Test that untrusted fuzzer on uworker passes.""" + self.fuzzer.untrusted = True + self.mock.is_uworker.return_value = True + uworker_io.check_running_fuzzer_safe(self.fuzzer) + self.mock.is_uworker.assert_called_once() + self.mock.log_fatal_and_exit.assert_not_called() + + def test_untrusted_fuzzer_not_uworker_raises(self): + """Test that untrusted fuzzer not on uworker raises SystemExit.""" + self.fuzzer.untrusted = True + self.mock.is_uworker.return_value = False + uworker_io.check_running_fuzzer_safe(self.fuzzer) + self.mock.is_uworker.assert_called_once() + self.mock.log_fatal_and_exit.assert_called_once_with( + 'Security Violation: Cannot run untrusted fuzzer test_fuzzer ' + 'in trusted environment.') From 54867a18b2b9dcf94452f7c35bfbeb853595943e Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Tue, 21 Jul 2026 15:58:44 +0000 Subject: [PATCH 06/10] Update custom safety logic to use inverted 'trusted' field --- src/clusterfuzz/_internal/base/tasks/__init__.py | 2 +- src/clusterfuzz/_internal/bot/tasks/utasks/uworker_io.py | 2 +- .../tests/core/bot/tasks/utasks/uworker_io_test.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/clusterfuzz/_internal/base/tasks/__init__.py b/src/clusterfuzz/_internal/base/tasks/__init__.py index 5177e7d4375..9772d117d70 100644 --- a/src/clusterfuzz/_internal/base/tasks/__init__.py +++ b/src/clusterfuzz/_internal/base/tasks/__init__.py @@ -238,7 +238,7 @@ def get_regular_task(queue=None): if task.command == 'fuzz' and not environment.is_tworker(): fuzzer = data_types.Fuzzer.query( data_types.Fuzzer.name == task.argument).get() - if fuzzer and fuzzer.untrusted: + if fuzzer and not fuzzer.trusted: logs.warning( f'Skipping untrusted fuzzer {task.argument} on long-lived bot.') continue diff --git a/src/clusterfuzz/_internal/bot/tasks/utasks/uworker_io.py b/src/clusterfuzz/_internal/bot/tasks/utasks/uworker_io.py index db71ebf0b86..88bf4311f16 100644 --- a/src/clusterfuzz/_internal/bot/tasks/utasks/uworker_io.py +++ b/src/clusterfuzz/_internal/bot/tasks/utasks/uworker_io.py @@ -246,7 +246,7 @@ def check_handling_testcase_safe(testcase): def check_running_fuzzer_safe(fuzzer): """Exits when the fuzzer is untrusted but the execution environment is trusted.""" - if not fuzzer.untrusted: + if fuzzer.trusted: return if environment.is_uworker(): return diff --git a/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/uworker_io_test.py b/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/uworker_io_test.py index 7a6f36b1a28..2446b0d8459 100644 --- a/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/uworker_io_test.py +++ b/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/uworker_io_test.py @@ -402,14 +402,14 @@ def setUp(self): def test_trusted_fuzzer(self): """Test that trusted fuzzer passes without checks.""" - self.fuzzer.untrusted = False + self.fuzzer.trusted = True uworker_io.check_running_fuzzer_safe(self.fuzzer) self.mock.is_uworker.assert_not_called() self.mock.log_fatal_and_exit.assert_not_called() def test_untrusted_fuzzer_uworker(self): """Test that untrusted fuzzer on uworker passes.""" - self.fuzzer.untrusted = True + self.fuzzer.trusted = False self.mock.is_uworker.return_value = True uworker_io.check_running_fuzzer_safe(self.fuzzer) self.mock.is_uworker.assert_called_once() @@ -417,7 +417,7 @@ def test_untrusted_fuzzer_uworker(self): def test_untrusted_fuzzer_not_uworker_raises(self): """Test that untrusted fuzzer not on uworker raises SystemExit.""" - self.fuzzer.untrusted = True + self.fuzzer.trusted = False self.mock.is_uworker.return_value = False uworker_io.check_running_fuzzer_safe(self.fuzzer) self.mock.is_uworker.assert_called_once() From 930958ba7cd5719dd7adbc45dcc0f45811452064 Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Tue, 21 Jul 2026 16:57:12 +0000 Subject: [PATCH 07/10] Fix fuzz_task_test by simulating uworker environment in utask tests --- .../_internal/tests/core/bot/tasks/utasks/fuzz_task_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/fuzz_task_test.py b/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/fuzz_task_test.py index f0df7d3572a..cbe1d2419c5 100644 --- a/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/fuzz_task_test.py +++ b/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/fuzz_task_test.py @@ -1245,6 +1245,7 @@ def _setup_utask_env(self): os.environ['DATA_BUNDLES_DIR'] = '/tmp/data-bundles' os.environ['FUZZ_DATA'] = '/tmp/fuzz-data' os.environ['BOT_NAME'] = 'test-bot' + os.environ['UWORKER'] = 'True' self.fs.create_dir('/tmp/fuzz-data') self.fs.create_dir('/tmp/data-bundles') From ef323f1448ee9eea91ac1e524057e254f1d286c8 Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Tue, 21 Jul 2026 19:24:26 +0000 Subject: [PATCH 08/10] Check by the new field and reduce nasting --- .../_internal/base/tasks/__init__.py | 26 ++++++++++++------- .../_internal/bot/tasks/utasks/uworker_io.py | 5 ++-- .../_internal/fuzzing/fuzzer_selection.py | 2 +- .../core/bot/tasks/utasks/uworker_io_test.py | 16 +++--------- .../core/fuzzing/fuzzer_selection_test.py | 17 ++++++------ 5 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/clusterfuzz/_internal/base/tasks/__init__.py b/src/clusterfuzz/_internal/base/tasks/__init__.py index 9772d117d70..98709dcbb21 100644 --- a/src/clusterfuzz/_internal/base/tasks/__init__.py +++ b/src/clusterfuzz/_internal/base/tasks/__init__.py @@ -234,15 +234,23 @@ def get_regular_task(queue=None): return None task = get_task_from_message(messages[0], queue) - if task: - if task.command == 'fuzz' and not environment.is_tworker(): - fuzzer = data_types.Fuzzer.query( - data_types.Fuzzer.name == task.argument).get() - if fuzzer and not fuzzer.trusted: - logs.warning( - f'Skipping untrusted fuzzer {task.argument} on long-lived bot.') - continue - return task + if not task: + continue + + if task.command == 'fuzz' and not environment.is_tworker(): + fuzzer = data_types.Fuzzer.query( + data_types.Fuzzer.name == task.argument).get() + if not fuzzer: + logs.error( + f'Fuzzer {task.argument} not found. Discarding invalid task.') + task.dont_retry() + continue + if not fuzzer.trusted: + logs.info( + f'Skipping untrusted fuzzer {task.argument} on long-lived bot.') + continue + + return task def get_machine_template_for_queue(queue_name): diff --git a/src/clusterfuzz/_internal/bot/tasks/utasks/uworker_io.py b/src/clusterfuzz/_internal/bot/tasks/utasks/uworker_io.py index 88bf4311f16..96de5a8d0f4 100644 --- a/src/clusterfuzz/_internal/bot/tasks/utasks/uworker_io.py +++ b/src/clusterfuzz/_internal/bot/tasks/utasks/uworker_io.py @@ -247,12 +247,13 @@ def check_running_fuzzer_safe(fuzzer): """Exits when the fuzzer is untrusted but the execution environment is trusted.""" if fuzzer.trusted: - return + return True if environment.is_uworker(): - return + return True logs.log_fatal_and_exit( f'Security Violation: Cannot run untrusted fuzzer {fuzzer.name} ' f'in trusted environment.') + return False def timestamp_to_proto_timestamp(pydt) -> Timestamp: diff --git a/src/clusterfuzz/_internal/fuzzing/fuzzer_selection.py b/src/clusterfuzz/_internal/fuzzing/fuzzer_selection.py index 079d95fae3a..09be079e2d5 100644 --- a/src/clusterfuzz/_internal/fuzzing/fuzzer_selection.py +++ b/src/clusterfuzz/_internal/fuzzing/fuzzer_selection.py @@ -174,7 +174,7 @@ def get_fuzz_task_payload(platform=None): untrusted_fuzzers = { fuzzer.name for fuzzer in ndb_utils.get_all_from_query( data_types.Fuzzer.query( - data_types.Fuzzer.untrusted_content == True)) # pylint: disable=singleton-comparison + ndb_utils.is_false(data_types.Fuzzer.trusted))) } if untrusted_fuzzers: diff --git a/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/uworker_io_test.py b/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/uworker_io_test.py index 2446b0d8459..969f7e2277e 100644 --- a/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/uworker_io_test.py +++ b/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/uworker_io_test.py @@ -395,7 +395,6 @@ class TestCheckRunningFuzzerSafe(unittest.TestCase): def setUp(self): helpers.patch(self, [ 'clusterfuzz._internal.system.environment.is_uworker', - 'clusterfuzz._internal.metrics.logs.log_fatal_and_exit', ]) self.fuzzer = mock.MagicMock(spec=data_types.Fuzzer) self.fuzzer.name = 'test_fuzzer' @@ -403,24 +402,17 @@ def setUp(self): def test_trusted_fuzzer(self): """Test that trusted fuzzer passes without checks.""" self.fuzzer.trusted = True - uworker_io.check_running_fuzzer_safe(self.fuzzer) - self.mock.is_uworker.assert_not_called() - self.mock.log_fatal_and_exit.assert_not_called() + self.assertTrue(uworker_io.check_running_fuzzer_safe(self.fuzzer)) def test_untrusted_fuzzer_uworker(self): """Test that untrusted fuzzer on uworker passes.""" self.fuzzer.trusted = False self.mock.is_uworker.return_value = True - uworker_io.check_running_fuzzer_safe(self.fuzzer) - self.mock.is_uworker.assert_called_once() - self.mock.log_fatal_and_exit.assert_not_called() + self.assertTrue(uworker_io.check_running_fuzzer_safe(self.fuzzer)) def test_untrusted_fuzzer_not_uworker_raises(self): """Test that untrusted fuzzer not on uworker raises SystemExit.""" self.fuzzer.trusted = False self.mock.is_uworker.return_value = False - uworker_io.check_running_fuzzer_safe(self.fuzzer) - self.mock.is_uworker.assert_called_once() - self.mock.log_fatal_and_exit.assert_called_once_with( - 'Security Violation: Cannot run untrusted fuzzer test_fuzzer ' - 'in trusted environment.') + with self.assertRaises(SystemExit): + uworker_io.check_running_fuzzer_safe(self.fuzzer) diff --git a/src/clusterfuzz/_internal/tests/core/fuzzing/fuzzer_selection_test.py b/src/clusterfuzz/_internal/tests/core/fuzzing/fuzzer_selection_test.py index b7e2c061642..5421cd9418d 100644 --- a/src/clusterfuzz/_internal/tests/core/fuzzing/fuzzer_selection_test.py +++ b/src/clusterfuzz/_internal/tests/core/fuzzing/fuzzer_selection_test.py @@ -325,13 +325,17 @@ def test_platform_restriction(self, local_development): argument, job = fuzzer_selection.get_fuzz_task_payload('ANDROID:PIXEL6') self.assertEqual(('pixel_fuzzer', 'job_pixel6'), (argument, job)) - @parameterized.parameterized.expand([('False',), ('True',)]) - def test_untrusted_fuzzer_exclusion(self, is_uworker): + @parameterized.parameterized.expand([ + ('False', 'trusted_fuzzer', 'job_2'), + ('True', 'untrusted_fuzzer', 'job_1'), + ]) + def test_untrusted_fuzzer_exclusion(self, is_uworker, expected_fuzzer, + expected_job): """Ensure that untrusted fuzzers are filtered out on long-lived bots.""" os.environ['UWORKER'] = is_uworker - data_types.Fuzzer(name='untrusted_fuzzer', untrusted_content=True).put() - data_types.Fuzzer(name='trusted_fuzzer', untrusted_content=False).put() + data_types.Fuzzer(name='untrusted_fuzzer', trusted=False).put() + data_types.Fuzzer(name='trusted_fuzzer', trusted=True).put() untrusted_mapping = data_types.FuzzerJob() untrusted_mapping.fuzzer = 'untrusted_fuzzer' @@ -351,10 +355,7 @@ def test_untrusted_fuzzer_exclusion(self, is_uworker): argument, job = fuzzer_selection.get_fuzz_task_payload('linux') - if is_uworker == 'True': - self.assertEqual(('untrusted_fuzzer', 'job_1'), (argument, job)) - else: - self.assertEqual(('trusted_fuzzer', 'job_2'), (argument, job)) + self.assertEqual((expected_fuzzer, expected_job), (argument, job)) @test_utils.with_cloud_emulators('datastore') From d37c2510d6a011f6b2880a8b2b35f904cfeae269 Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Wed, 17 Jun 2026 18:22:01 +0000 Subject: [PATCH 09/10] Mark testcases as untrusted if generated by untrusted fuzzers --- .../_internal/bot/tasks/utasks/fuzz_task.py | 9 +++- .../core/bot/tasks/utasks/fuzz_task_test.py | 42 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py b/src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py index 2bdecdfe47b..ea0a5cda1b7 100644 --- a/src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py +++ b/src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py @@ -1169,6 +1169,13 @@ def create_testcase(group: uworker_msg_pb2.FuzzTaskCrashGroup, comment = (f'Fuzzer {fully_qualified_fuzzer_name} generated testcase crashed ' f'in {crash.crash_time} seconds ' f'(r{fuzz_task_output.crash_revision})') + + is_trusted = True + if uworker_input.setup_input.HasField('fuzzer'): + fuzzer = uworker_io.entity_from_protobuf(uworker_input.setup_input.fuzzer, + data_types.Fuzzer) + is_trusted = fuzzer.trusted + testcase_id = data_handler.store_testcase( crash=crash, fuzzed_keys=crash.fuzzed_key or None, @@ -1194,7 +1201,7 @@ def create_testcase(group: uworker_msg_pb2.FuzzTaskCrashGroup, minimized_arguments=crash.arguments, # TODO(https://github.com/google/clusterfuzz/issues/4175): Before enabling # oss-fuzz-on-demand change this. - trusted=True) + trusted=is_trusted) testcase = data_handler.get_testcase_by_id(testcase_id) events.emit( events.TestcaseCreationEvent( diff --git a/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/fuzz_task_test.py b/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/fuzz_task_test.py index cbe1d2419c5..b1904c2dae5 100644 --- a/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/fuzz_task_test.py +++ b/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/fuzz_task_test.py @@ -2111,6 +2111,48 @@ def test_create_testcase_event_emit(self): creation_origin=events.TestcaseOrigin.FUZZ_TASK, uploader=None)) + def test_create_testcase_trusted_fuzzer(self): + """Test creating a testcase with a trusted fuzzer.""" + fuzzer = data_types.Fuzzer(name='engine', trusted=True) + fuzzer.put() + + self.uworker_input.setup_input.CopyFrom(uworker_msg_pb2.SetupInput()) + self.uworker_input.setup_input.fuzzer.CopyFrom( + uworker_io.entity_to_protobuf(fuzzer)) + + self.mock.store_testcase.side_effect = _store_generic_testcase + + fuzz_task.create_testcase( + group=self.group, + uworker_input=self.uworker_input, + uworker_output=self.uworker_output, + fully_qualified_fuzzer_name='engine') + + self.mock.store_testcase.assert_called_once() + kwargs = self.mock.store_testcase.call_args[1] + self.assertTrue(kwargs.get('trusted')) + + def test_create_testcase_untrusted_fuzzer(self): + """Test creating a testcase with an untrusted fuzzer.""" + fuzzer = data_types.Fuzzer(name='engine', trusted=False) + fuzzer.put() + + self.uworker_input.setup_input.CopyFrom(uworker_msg_pb2.SetupInput()) + self.uworker_input.setup_input.fuzzer.CopyFrom( + uworker_io.entity_to_protobuf(fuzzer)) + + self.mock.store_testcase.side_effect = _store_generic_testcase + + fuzz_task.create_testcase( + group=self.group, + uworker_input=self.uworker_input, + uworker_output=self.uworker_output, + fully_qualified_fuzzer_name='engine') + + self.mock.store_testcase.assert_called_once() + kwargs = self.mock.store_testcase.call_args[1] + self.assertFalse(kwargs.get('trusted')) + def _store_generic_testcase(*args, **kwargs): # pylint: disable=unused-argument """Store a generic testcase and return its id.""" From 3f3929cdd061cb0af04cac9ea33d693b0c4cb15a Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Wed, 15 Jul 2026 19:22:19 +0000 Subject: [PATCH 10/10] Harden testcase safety check in long-lived bots --- .../_internal/bot/tasks/utasks/uworker_io.py | 10 ++++++---- .../tests/core/bot/tasks/utasks/minimize_task_test.py | 3 ++- .../_internal/tests/test_libs/test_utils.py | 1 + .../_internal/tests/test_libs/utask_helpers.py | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/tasks/utasks/uworker_io.py b/src/clusterfuzz/_internal/bot/tasks/utasks/uworker_io.py index 96de5a8d0f4..72f9ad9ad30 100644 --- a/src/clusterfuzz/_internal/bot/tasks/utasks/uworker_io.py +++ b/src/clusterfuzz/_internal/bot/tasks/utasks/uworker_io.py @@ -237,10 +237,12 @@ def check_handling_testcase_safe(testcase): safely.""" if testcase.trusted: return - if not environment.get_value('UNTRUSTED_UTASK'): - # TODO(https://b.corp.google.com/issues/328691756): Change this to - # log_fatal_and_exit once we are handling untrusted tasks properly. - logs.warning(f'Cannot handle {testcase.key.id()} in trusted task.') + if environment.is_uworker(): + return + + logs.log_fatal_and_exit( + f'Security Violation: Cannot handle untrusted testcase ' + f'{testcase.key.id()} in long-lived bot.') def check_running_fuzzer_safe(fuzzer): diff --git a/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/minimize_task_test.py b/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/minimize_task_test.py index 75f22804990..069ba877abb 100644 --- a/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/minimize_task_test.py +++ b/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/minimize_task_test.py @@ -112,6 +112,7 @@ def setUp(self): """Set up.""" super().setUp() environment.set_value('JOB_NAME', 'libfuzzer_asan_job') + environment.set_value('UWORKER', True) patcher = mock.patch( 'clusterfuzz._internal.bot.fuzzers.libFuzzer.fuzzer.LibFuzzer.fuzzer_directory', @@ -384,7 +385,7 @@ def test_check_app_path_exit(self, setup_testcase, preprocess_setup_testcase, setup_testcase.return_value = ([], '/path', None) del setup_build del check_app_path - testcase = data_types.Testcase() + testcase = data_types.Testcase(trusted=True) testcase.put() environment.set_value('FAIL_WAIT', 10) uworker_input = uworker_msg_pb2.Input( diff --git a/src/clusterfuzz/_internal/tests/test_libs/test_utils.py b/src/clusterfuzz/_internal/tests/test_libs/test_utils.py index 5048122163f..6b0d8e88381 100644 --- a/src/clusterfuzz/_internal/tests/test_libs/test_utils.py +++ b/src/clusterfuzz/_internal/tests/test_libs/test_utils.py @@ -63,6 +63,7 @@ def create_generic_testcase(created_days_ago=28): testcase.timestamp = CURRENT_TIME - datetime.timedelta(days=created_days_ago) testcase.project_name = 'project' testcase.platform = 'linux' + testcase.trusted = True testcase.put() return testcase diff --git a/src/clusterfuzz/_internal/tests/test_libs/utask_helpers.py b/src/clusterfuzz/_internal/tests/test_libs/utask_helpers.py index 174ffc1d537..d6470620a53 100644 --- a/src/clusterfuzz/_internal/tests/test_libs/utask_helpers.py +++ b/src/clusterfuzz/_internal/tests/test_libs/utask_helpers.py @@ -64,7 +64,7 @@ def setUp(self): self.uworker_env = commands.update_environment_for_job(environment_string) job.put() self.fuzz_target = 'test_fuzzer' - self.testcase = data_types.Testcase(job_type=self.job_type) + self.testcase = data_types.Testcase(job_type=self.job_type, trusted=True) self.testcase.fuzzed_keys = blobs.write_blob( os.path.join(TEST_LIBS_DATA_DIR, 'crash-adc83b19e793491b1c6ea0fd8b46cd9f32e592fc'))