From be0e055deaacd3ea099f0a49233756c7129445da Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Mon, 20 Jul 2026 19:39:34 +0000 Subject: [PATCH 1/4] 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 720e09de026813d323bf306d1be735af6df9e4be Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Wed, 22 Jul 2026 18:18:48 +0000 Subject: [PATCH 2/4] 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 533ded1cac752f8c924a4c078b9fba4b39bdf1d0 Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Wed, 17 Jun 2026 18:22:01 +0000 Subject: [PATCH 3/4] Mark testcases as untrusted if generated by untrusted fuzzers --- .../core/bot/tasks/utasks/fuzz_task_test.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) 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 f67a2113d94..613bc44e114 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 @@ -2154,6 +2154,48 @@ def test_create_testcase_untrusted_fuzzer(self): kwargs = self.mock.store_testcase.call_args[1] self.assertFalse(kwargs.get('trusted')) + 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 1bea54b7e92f6871cde34bcad39aaa76953c8d05 Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Wed, 15 Jul 2026 19:22:19 +0000 Subject: [PATCH 4/4] Harden testcase safety check in long-lived bots --- .../_internal/bot/tasks/task_creation.py | 2 - .../bot/tasks/utasks/corpus_pruning_task.py | 4 +- .../_internal/bot/tasks/utasks/uworker_io.py | 15 ++++--- .../core/bot/tasks/utasks/fuzz_task_test.py | 42 ------------------- .../bot/tasks/utasks/minimize_task_test.py | 3 +- .../core/bot/tasks/utasks/uworker_io_test.py | 29 +++++++++++++ .../_internal/tests/test_libs/test_utils.py | 1 + .../tests/test_libs/utask_helpers.py | 2 +- 8 files changed, 44 insertions(+), 54 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/tasks/task_creation.py b/src/clusterfuzz/_internal/bot/tasks/task_creation.py index 0f1bdf26b09..a049855322e 100644 --- a/src/clusterfuzz/_internal/bot/tasks/task_creation.py +++ b/src/clusterfuzz/_internal/bot/tasks/task_creation.py @@ -149,8 +149,6 @@ def create_regression_task_if_needed(testcase): def create_variant_tasks_if_needed(testcase): """Creates a variant task if needed.""" - # TODO(https://b.corp.google.com/issues/328691756): Allow untrusted - # testcases to only run untrusted variants. if testcase.duplicate_of: # If another testcase exists with same params, no need to spend cycles on # calculating variants again. diff --git a/src/clusterfuzz/_internal/bot/tasks/utasks/corpus_pruning_task.py b/src/clusterfuzz/_internal/bot/tasks/utasks/corpus_pruning_task.py index e587b6d4e2e..f4dc0c31516 100644 --- a/src/clusterfuzz/_internal/bot/tasks/utasks/corpus_pruning_task.py +++ b/src/clusterfuzz/_internal/bot/tasks/utasks/corpus_pruning_task.py @@ -887,8 +887,8 @@ def _process_corpus_crashes(output: uworker_msg_pb2.Output): # pylint: disable= absolute_testcase_path = os.path.join( environment.get_value('FUZZ_INPUTS'), 'testcase') - # TODO(https://b.corp.google.com/issues/328691756): Set trusted based on - # the job when we start doing untrusted fuzzing. + # TODO(b/556173460): Set trusted based on + # the job when we start doing untrusted engine fuzzing testcase_id = data_handler.store_testcase( crash=crash, fuzzed_keys=key, diff --git a/src/clusterfuzz/_internal/bot/tasks/utasks/uworker_io.py b/src/clusterfuzz/_internal/bot/tasks/utasks/uworker_io.py index 96de5a8d0f4..4ec121b8cda 100644 --- a/src/clusterfuzz/_internal/bot/tasks/utasks/uworker_io.py +++ b/src/clusterfuzz/_internal/bot/tasks/utasks/uworker_io.py @@ -234,13 +234,16 @@ def entity_from_protobuf(entity_proto: any_pb2.Any, model_type: Type[T]) -> T: def check_handling_testcase_safe(testcase): """Exits when the current task execution model is trusted but the testcase is untrusted. This will allow uploading testcases to trusted jobs (e.g. Mac) more - safely.""" + safely. Returns True if safe to handle, False otherwise.""" 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.') + return True + if environment.is_uworker(): + return True + + logs.log_fatal_and_exit( + f'Security Violation: Cannot handle untrusted testcase ' + f'{testcase.key.id()} in long-lived bot.') + return False def check_running_fuzzer_safe(fuzzer): 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 613bc44e114..f67a2113d94 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 @@ -2154,48 +2154,6 @@ def test_create_testcase_untrusted_fuzzer(self): kwargs = self.mock.store_testcase.call_args[1] self.assertFalse(kwargs.get('trusted')) - 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.""" 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/core/bot/tasks/utasks/uworker_io_test.py b/src/clusterfuzz/_internal/tests/core/bot/tasks/utasks/uworker_io_test.py index 969f7e2277e..57ffab2aa49 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 @@ -416,3 +416,32 @@ def test_untrusted_fuzzer_not_uworker_raises(self): self.mock.is_uworker.return_value = False with self.assertRaises(SystemExit): uworker_io.check_running_fuzzer_safe(self.fuzzer) + + +class TestCheckRunningTestcaseSafe(unittest.TestCase): + """Tests check_handling_testcase_safe.""" + + def setUp(self): + helpers.patch(self, [ + 'clusterfuzz._internal.system.environment.is_uworker', + ]) + self.testcase = mock.MagicMock(spec=data_types.Testcase) + self.testcase.name = 'test_testcase' + + def test_trusted_testcase(self): + """Test that trusted testcase passes without checks.""" + self.testcase.trusted = True + self.assertTrue(uworker_io.check_handling_testcase_safe(self.testcase)) + + def test_untrusted_testcase_uworker(self): + """Test that untrusted testcase on uworker passes.""" + self.testcase.trusted = False + self.mock.is_uworker.return_value = True + self.assertTrue(uworker_io.check_handling_testcase_safe(self.testcase)) + + def test_untrusted_testcase_not_uworker_raises(self): + """Test that untrusted testcase not on uworker raises SystemExit.""" + self.testcase.trusted = False + self.mock.is_uworker.return_value = False + with self.assertRaises(SystemExit): + uworker_io.check_handling_testcase_safe(self.testcase) 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'))