Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/appengine/handlers/fuzzers.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ def apply_fuzzer_changes(self, fuzzer, upload_info):
differential = request.get('differential', False)
environment_string = request.get('additional_environment_string')
data_bundle_name = request.get('data_bundle_name')
primary_owner = request.get('primary_owner')

# Save the fuzzer file metadata.
if upload_info:
Expand All @@ -193,6 +194,7 @@ def apply_fuzzer_changes(self, fuzzer, upload_info):
fuzzer.sample_testcase = None
fuzzer.console_output = None
fuzzer.external_contribution = bool(external_contribution)
fuzzer.primary_owner = primary_owner
fuzzer.differential = bool(differential)
fuzzer.additional_environment_string = environment_string
fuzzer.timestamp = datetime.datetime.now(tz=datetime.timezone.utc).replace(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@
title="Name of fuzzer. Allowed characters include letters, numbers, dashes and underscores.">
</paper-input>
</div>
<div class="inline narrow">
<paper-input
label="Primary owner (email)"
value="{{fuzzer.primary_owner}}"
title="Primary owner for VRP reasons.">
</paper-input>
</div>
<div class="inline narrow">
<paper-menu-button
class="job-select dropdown-filter"
Expand Down Expand Up @@ -252,6 +259,7 @@
'differential',
'executable_path',
'external_contribution',
'primary_owner',
'jobs',
'launcher_script',
'max_testcases',
Expand Down
3 changes: 3 additions & 0 deletions src/clusterfuzz/_internal/datastore/data_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,9 @@ class Fuzzer(Model):
# reward flags.
external_contribution = ndb.BooleanProperty(default=False)

# Primary owner to be reported for bugs filed by CF
primary_owner = ndb.StringProperty()

# Max testcases to generate for this fuzzer.
max_testcases = ndb.IntegerProperty()

Expand Down
6 changes: 5 additions & 1 deletion src/clusterfuzz/_internal/issue_management/issue_filer.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ def file_issue(testcase,
logs.info(f'Filing new issue for testcase: {testcase.key.id()}.')

policy = issue_tracker_policy.get(issue_tracker.project)
fuzzer = None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this need to be a DataStore look up?

is_crash = not utils.sub_string_exists_in(NON_CRASH_TYPES,
testcase.crash_type)
properties = policy.get_new_issue_properties(
Expand Down Expand Up @@ -482,7 +483,10 @@ def file_issue(testcase,
testcase.one_time_crasher_flag and policy.unreproducible_component):
issue.components.add(policy.unreproducible_component)

issue.reporter = user_email
if fuzzer and fuzzer.external_contribution and fuzzer.primary_owner:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good, but I'm wondering whether the external_contribution is correctly set on our existing fuzzers. I'm not confident that it is :(

Before we enable this we should verify.

issue.reporter = fuzzer.primary_owner
else:
issue.reporter = user_email

if issue_tracker.project in ('chromium', 'chromium-testing'):
logs.info(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,77 @@ def test_filed_issues_oss_fuzz_disable_disclose(self):
self.assertIn(FIX_NOTE, issue_tracker._itm.last_issue.body)
self.assertIn(QUESTIONS_NOTE, issue_tracker._itm.last_issue.body)

def test_filed_issues_external_fuzzer_author(self):
"""Tests issue filing for external fuzzer author."""
self.mock.get.return_value = CHROMIUM_POLICY

data_types.Fuzzer(
name='fuzzer',
external_contribution=True,
primary_owner='owner@example.com').put()

issue_tracker = google_issue_tracker.IssueTracker('chromium', mock.Mock(), {
'default_component_id': '123',
'url': 'mock'
})

# Intercept new_issue so we can capture the saved issue object.
original_new_issue = issue_tracker.new_issue

def mock_new_issue():
issue = original_new_issue()

def mock_save(*args, **kwargs):
issue_tracker.last_issue = issue
issue._data['issueId'] = 12345

issue.save = mock_save
return issue

issue_tracker.new_issue = mock_new_issue

self.testcase1.security_flag = True
self.testcase1.put()

issue_filer.file_issue(
self.testcase1, issue_tracker, user_email='reporter@example.com')

self.assertEqual('owner@example.com', issue_tracker.last_issue.reporter)

def test_filed_issues_external_fuzzer_no_author(self):
"""Tests issue filing for external fuzzer without author."""
self.mock.get.return_value = CHROMIUM_POLICY

data_types.Fuzzer(name='fuzzer', external_contribution=True).put()

issue_tracker = google_issue_tracker.IssueTracker('chromium', mock.Mock(), {
'default_component_id': '123',
'url': 'mock'
})

# Intercept new_issue so we can capture the saved issue object.
original_new_issue = issue_tracker.new_issue

def mock_new_issue():
issue = original_new_issue()

def mock_save(*args, **kwargs):
issue_tracker.last_issue = issue
issue._data['issueId'] = 12345

issue.save = mock_save
return issue

issue_tracker.new_issue = mock_new_issue

self.testcase1.security_flag = True
self.testcase1.put()

issue_filer.file_issue(
self.testcase1, issue_tracker, user_email='reporter@example.com')

self.assertEqual('reporter@example.com', issue_tracker.last_issue.reporter)

def test_testcase_metadata_labels_and_components(self):
"""Tests issue filing with additional labels and components."""
self.mock.get.return_value = CHROMIUM_POLICY
Expand Down
63 changes: 63 additions & 0 deletions src/local/butler/scripts/migrate_fuzzer_owners.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# 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.
"""Script to migrate fuzzer owners."""

import argparse
import os
import sys

# Ensure the local project root is in the path
sys.path.append(os.path.join(os.path.dirname(__file__), '../src'))
sys.path.append(os.path.join(os.path.dirname(__file__), '../src/appengine'))

from google.cloud import ndb

from clusterfuzz._internal.datastore import data_types
from libs import helpers


def migrate(dry_run=True, default_owner='default-owner@example.com'):
"""Migrates fuzzer owners."""
query = data_types.Fuzzer.query(
ndb.OR(
data_types.Fuzzer.primary_owner == None, # pylint: disable=singleton-comparison
data_types.Fuzzer.primary_owner == ''))

count = 0
for fuzzer in query.fetch():
print(f"Processing fuzzer: {fuzzer.name}")
if not dry_run:
fuzzer.primary_owner = default_owner
fuzzer.put()
helpers.log(
f"Backfilled primary_owner for {fuzzer.name} to {default_owner}",
helpers.MODIFY_OPERATION)
count += 1

mode = "DRY RUN" if dry_run else "LIVE"
print(f"{mode}: Processed {count} fuzzers.")


if __name__ == '__main__':
parser = argparse.ArgumentParser(
description='Backfill primary_owner for Fuzzers.')
parser.add_argument(
'--live', action='store_true', help='Perform live updates.')
parser.add_argument(
'--owner',
default='default-owner@example.com',
help='Default owner email.')
args = parser.parse_args()

migrate(dry_run=not args.live, default_owner=args.owner)
Loading