blob: fb88e7469ef0c599d4091299f1030bbf2e822480 [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <array>
#include <cstdint>
#include <optional>
#include <queue>
#include <string>
#include <vector>
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/escape.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chrome/test/fuzzing/geolocation_element_css_fuzzer_grammar.h"
#include "chrome/test/fuzzing/geolocation_element_css_fuzzer_grammar.pb.h"
#include "chrome/test/fuzzing/in_process_fuzzer.h"
#include "components/viz/common/frame_sinks/copy_output_result.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test_utils.h"
#include "testing/libfuzzer/proto/lpm_interface.h"
#include "testing/libfuzzer/research/domatolpm/domatolpm.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/gfx/geometry/rect.h"
namespace {
// The amount of a pixel's color is allowed to diverge from the target color
// (calculated as Euclidean distance in the RGB space). This value was arrived
// at experimentally.
constexpr int kColorTolerance = 100;
// The amount of expected text/background islands that the element screenshot is
// expected to have. Since the text and its font is hard-set, this values should
// not vary.
// There are 14 text islands, 11 for every letter in "Use location", 1 for the
// "i" dot, and 2 for the element's location pin icon.
constexpr int kExpectedTextIslands = 14;
// There are 6 background islands, 1 for the large island around the text, 1 for
// the location pin icon, and 4 for every letter in "Use location" with a hole.
constexpr int kExpectedBgIslands = 6;
// Helper function to parse rgb(r, g, b) or rgba(r, g, b, a) strings
std::optional<SkColor> ParseRgbString(const std::string& color_str) {
if (color_str.empty()) {
return std::nullopt;
}
size_t start = color_str.find('(');
size_t end = color_str.find(')');
if (start == std::string::npos || end == std::string::npos || end <= start) {
return std::nullopt;
}
std::string values_str = color_str.substr(start + 1, end - start - 1);
std::vector<std::string> parts = base::SplitString(
values_str, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
if (parts.size() < 3) {
return std::nullopt;
}
std::vector<int> components;
for (int i = 0; i < 3; ++i) {
int component;
if (!base::StringToInt(parts[i], &component) || component < 0 ||
component > 255) {
return std::nullopt;
}
components.push_back(component);
}
return SkColorSetRGB(components[0], components[1], components[2]);
}
// Helper function to check if colors are similar within a tolerance
bool AreColorsSimilar(SkColor c1, SkColor c2, int tolerance) {
int r_diff =
static_cast<int>(SkColorGetR(c1)) - static_cast<int>(SkColorGetR(c2));
int g_diff =
static_cast<int>(SkColorGetG(c1)) - static_cast<int>(SkColorGetG(c2));
int b_diff =
static_cast<int>(SkColorGetB(c1)) - static_cast<int>(SkColorGetB(c2));
return (r_diff * r_diff + g_diff * g_diff + b_diff * b_diff) <=
(tolerance * tolerance);
}
// Helper function to count color islands
int CountColorIslands(const SkBitmap& bitmap,
SkColor target_color,
int tolerance) {
// Direction vectors for visiting neighbors.
std::array<int, 8> di = {-1, -1, -1, 0, 0, 1, 1, 1};
std::array<int, 8> dj = {-1, 0, 1, -1, 1, -1, 0, 1};
int width = bitmap.width();
int height = bitmap.height();
std::vector<std::vector<bool>> visited(height,
std::vector<bool>(width, false));
int island_count = 0;
for (int i = 0; i < height; ++i) {
for (int j = 0; j < width; ++j) {
if (visited[i][j]) {
continue;
}
if (!AreColorsSimilar(bitmap.getColor(j, i), target_color, tolerance)) {
visited[i][j] = true;
continue;
}
std::queue<std::pair<int, int>> q;
q.emplace(i, j);
visited[i][j] = true;
island_count++;
while (!q.empty()) {
std::pair<int, int> curr = q.front();
q.pop();
for (int k = 0; k < 8; ++k) {
int next_i = curr.first + di[k];
int next_j = curr.second + dj[k];
if (next_i < 0 || next_i >= height || next_j < 0 || next_j >= width) {
continue;
}
if (visited[next_i][next_j]) {
continue;
}
visited[next_i][next_j] = true;
if (!AreColorsSimilar(bitmap.getColor(next_j, next_i), target_color,
tolerance)) {
continue;
}
q.emplace(next_i, next_j);
}
}
}
}
return island_count;
}
} // namespace
// This fuzzer uses DomatoLPM to generate CSS to style a geolocation element.
class GeolocationElementCssFuzzer : public InProcessFuzzer {
public:
using FuzzCase =
domatolpm::generated::geolocation_element_css_fuzzer_grammar::fuzzcase;
GeolocationElementCssFuzzer() = default;
int Fuzz(const uint8_t* data, size_t size) override;
private:
void OnScreenshotCaptured(base::OnceClosure quit_closure,
SkColor text_color,
SkColor bg_color,
const content::CopyFromSurfaceResult& result);
};
DEFINE_CUSTOM_PROTO_MUTATOR_IMPL(true, GeolocationElementCssFuzzer::FuzzCase)
DEFINE_CUSTOM_PROTO_CROSSOVER_IMPL(true, GeolocationElementCssFuzzer::FuzzCase)
DEFINE_POST_PROCESS_PROTO_MUTATION_IMPL(GeolocationElementCssFuzzer::FuzzCase)
REGISTER_IN_PROCESS_FUZZER(GeolocationElementCssFuzzer)
int GeolocationElementCssFuzzer::Fuzz(const uint8_t* data, size_t size) {
FuzzCase fuzz_case;
if (!protobuf_mutator::libfuzzer::LoadProtoInput(false, data, size,
&fuzz_case)) {
LOG(ERROR) << "Protobuf mutator failed to load proto input";
return 0;
}
domatolpm::Context ctx;
CHECK(domatolpm::geolocation_element_css_fuzzer_grammar::handle_fuzzer(
&ctx, fuzz_case));
std::string html_content(ctx.GetBuilder()->view());
// See
// docs/security/url_display_guidelines/url_display_guidelines.md#url-length
const size_t kMaxUrlLength = 2 * 1024 * 1024;
std::string url_string = "data:text/html;charset=utf-8,";
const bool kUsePlus = false;
url_string.append(base::EscapeQueryParamValue(html_content, kUsePlus));
if (url_string.length() > kMaxUrlLength) {
return 0;
}
if (!ui_test_utils::NavigateToURL(browser(), GURL(url_string))) {
NOTREACHED() << "Failed to load page";
}
auto* web_contents = browser()->tab_strip_model()->GetActiveWebContents();
if (!web_contents) {
NOTREACHED() << "WebContents is null after navigation";
}
if (!content::WaitForLoadStop(web_contents)) {
NOTREACHED() << "Failed to wait for page load stop";
}
auto* rfh = web_contents->GetPrimaryMainFrame();
if (!rfh || !rfh->IsRenderFrameLive()) {
NOTREACHED() << "RenderFrameHost is not live after navigation";
}
// Get element bounds and styles, after the element is valid.
std::string script = R"(waitForElementAndGetData();)";
content::EvalJsResult result = content::EvalJs(rfh, script);
if (!result.is_ok()) {
NOTREACHED() << "Failed to get element data: " << result.ExtractError();
}
if (!result.is_dict()) {
// Gracefully exit this fuzz case, this is an expected possibility.
LOG(INFO) << "Element did not become valid";
return -1;
}
const base::DictValue& dict = result.ExtractDict();
const std::string* error_string = dict.FindString("error");
if (error_string) {
NOTREACHED() << "Javascript returned an error: " << *error_string;
}
const base::DictValue* element_dict = dict.FindDict("element");
if (!element_dict) {
NOTREACHED() << "Failed to find 'element' key in result: " << dict;
}
std::optional<int> x = element_dict->FindInt("x");
std::optional<int> y = element_dict->FindInt("y");
std::optional<int> width = element_dict->FindInt("width");
std::optional<int> height = element_dict->FindInt("height");
if (!x || !y || !width || !height || *width <= 0 || *height <= 0) {
NOTREACHED() << "Invalid element bounds: " << *element_dict;
}
gfx::Rect element_bounds(*x, *y, *width, *height);
const base::DictValue* style_dict = dict.FindDict("style");
if (!style_dict) {
NOTREACHED() << "Style dictionary is not present in response";
}
const std::string* text_color_str = style_dict->FindString("color");
const std::string* bg_color_str = style_dict->FindString("backgroundColor");
if (!text_color_str || !bg_color_str) {
NOTREACHED() << "Colors not available in response";
}
std::optional<SkColor> text_color = ParseRgbString(*text_color_str);
std::optional<SkColor> bg_color = ParseRgbString(*bg_color_str);
if (!text_color || !bg_color) {
NOTREACHED() << "Failed to parse colors: " << text_color_str << ", "
<< bg_color_str;
}
// Take screenshot of the element to run heuristic on.
base::RunLoop run_loop;
web_contents->GetPrimaryMainFrame()->GetView()->CopyFromSurface(
element_bounds, gfx::Size(), base::TimeDelta(),
base::BindOnce(&GeolocationElementCssFuzzer::OnScreenshotCaptured,
base::Unretained(this), run_loop.QuitClosure(),
*text_color, *bg_color));
run_loop.Run();
return 0;
}
void GeolocationElementCssFuzzer::OnScreenshotCaptured(
base::OnceClosure quit_closure,
SkColor text_color,
SkColor bg_color,
const content::CopyFromSurfaceResult& result) {
base::ScopedClosureRunner closer(std::move(quit_closure));
if (!result.has_value()) {
NOTREACHED() << "Screenshot failed";
}
const SkBitmap& bitmap = result->bitmap;
if (bitmap.height() == 0 || bitmap.width() == 0) {
NOTREACHED() << "Bitmap is empty";
}
// This is the main goal of the test. We use a simple island counting
// algorithm to count the number of islands matching background/text color
// respectively and check and it matches the expected amounts if the text was
// fully visible. This relies on the geolocation element text and font being
// fixed.
int text_islands = CountColorIslands(bitmap, text_color, kColorTolerance);
int bg_islands = CountColorIslands(bitmap, bg_color, kColorTolerance);
CHECK_EQ(text_islands, kExpectedTextIslands);
CHECK_EQ(bg_islands, kExpectedBgIslands);
}