chromium/components/browsing_topics/annotator_fuzzer.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <string>

#include <fuzzer/FuzzedDataProvider.h>

#include "base/files/file_util.h"
#include "base/path_service.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "components/browsing_topics/annotator_impl.h"
#include "components/optimization_guide/core/optimization_guide_features.h"
#include "components/optimization_guide/core/optimization_guide_model_provider.h"
#include "components/optimization_guide/core/test_model_info_builder.h"
#include "components/optimization_guide/core/test_optimization_guide_model_provider.h"
#include "components/optimization_guide/proto/models.pb.h"
#include "components/optimization_guide/proto/page_topics_model_metadata.pb.h"

// Sends a fully working model and configuration to the calling observer.
class ModelProvider
    : public optimization_guide::TestOptimizationGuideModelProvider {
 public:
  ModelProvider() = default;
  ~ModelProvider() override = default;

  // optimization_guide::TestOptimizationGuideModelProvider:
  void AddObserverForOptimizationTargetModel(
      optimization_guide::proto::OptimizationTarget optimization_target,
      const std::optional<optimization_guide::proto::Any>& model_metadata,
      optimization_guide::OptimizationTargetModelObserver* observer) override {
    optimization_guide::proto::Any any_metadata;
    any_metadata.set_type_url(
        "type.googleapis.com/com.foo.PageTopicsModelMetadata");
    optimization_guide::proto::PageTopicsModelMetadata
        page_topics_model_metadata;
    page_topics_model_metadata.set_version(123);
    page_topics_model_metadata.add_supported_output(
        optimization_guide::proto::PAGE_TOPICS_SUPPORTED_OUTPUT_CATEGORIES);
    auto* output_params =
        page_topics_model_metadata.mutable_output_postprocessing_params();
    auto* category_params = output_params->mutable_category_params();
    category_params->set_max_categories(5);
    category_params->set_min_none_weight(0.8);
    category_params->set_min_category_weight(0.1);
    category_params->set_min_normalized_weight_within_top_n(0.1);
    page_topics_model_metadata.SerializeToString(any_metadata.mutable_value());

    base::FilePath source_root_dir;
    base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &source_root_dir);
    base::FilePath model_file_path =
        source_root_dir.AppendASCII("components")
            .AppendASCII("test")
            .AppendASCII("data")
            .AppendASCII("browsing_topics")
            .AppendASCII("golden_data_model.tflite");
    std::unique_ptr<optimization_guide::ModelInfo> model_info =
        optimization_guide::TestModelInfoBuilder()
            .SetModelFilePath(model_file_path)
            .SetModelMetadata(any_metadata)
            .Build();

    observer->OnModelUpdated(
        optimization_guide::proto::OPTIMIZATION_TARGET_PAGE_TOPICS_V2,
        *model_info);
  }
};

// An AnnotatorImpl that never unloads the model, thus keeping the executions
// per second high.
class TestAnnotator : public browsing_topics::AnnotatorImpl {
 public:
  TestAnnotator(
      optimization_guide::OptimizationGuideModelProvider* model_provider,
      scoped_refptr<base::SequencedTaskRunner> background_task_runner,
      const std::optional<optimization_guide::proto::Any>& model_metadata)
      : browsing_topics::AnnotatorImpl(model_provider,
                                       std::move(background_task_runner),
                                       model_metadata) {}

 protected:
  // AnnotatorImpl:
  void UnloadModel() override {
    // Do nothing so that the model stays loaded.
  }
};

// Static test fixture to maintain the state needed to run repeated fuzz tests.
class AnnotatorFuzzerTest {
 public:
  explicit AnnotatorFuzzerTest()
      : annotator_(std::make_unique<TestAnnotator>(
            &model_provider_,
            task_environment_.GetMainThreadTaskRunner(),
            std::nullopt)) {
    scoped_feature_list_.InitAndDisableFeature(
        optimization_guide::features::kPreventLongRunningPredictionModels);
  }
  ~AnnotatorFuzzerTest() {
    annotator_.reset();
    // To prevent ASAN issues, ensure the ModelExecutor gets destroyed since
    // that is done using a DeleteSoon PostTask.
    task_environment_.RunUntilIdle();
  }

  browsing_topics::Annotator* annotator() { return annotator_.get(); }

  void RunUntilIdle() { task_environment_.RunUntilIdle(); }

 private:
  base::test::TaskEnvironment task_environment_;
  base::test::ScopedFeatureList scoped_feature_list_;
  ModelProvider model_provider_;
  std::unique_ptr<browsing_topics::AnnotatorImpl> annotator_;
};

extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
  static AnnotatorFuzzerTest test;
  static bool had_successful_run = false;

  std::string input(reinterpret_cast<const char*>(data), size);

  base::RunLoop run_loop;
  test.annotator()->BatchAnnotate(
      base::BindOnce(
          [](base::RunLoop* run_loop,
             const std::vector<browsing_topics::Annotation>& annotations) {
            if (!annotations[0].topics.empty() && !had_successful_run) {
              had_successful_run = true;
              // Print a single debug message so that its obvious things are
              // working (or able to at least once) when running locally.
              LOG(INFO) << "Congrats! Got a successful model execution. This "
                           "message will not be printed again.";
            }

            run_loop->Quit();
          },
          &run_loop),
      {input});
  run_loop.Run();

  // The model executor does some PostTask'ing to manage its state. While these
  // tasks are not important for fuzzing, we don't want to queue up a ton of
  // them.
  test.RunUntilIdle();

  return 0;
}