chromium/ui/accessibility/ax_node_position_fuzzer.cc

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

#include "base/at_exit.h"
#include "base/i18n/icu_util.h"
#include "base/memory/raw_ptr.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/accessibility/ax_node_position.h"
#include "ui/accessibility/ax_position.h"
#include "ui/accessibility/ax_range.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/accessibility/ax_tree.h"
#include "ui/accessibility/ax_tree_data.h"
#include "ui/accessibility/ax_tree_fuzzer_util.h"
#include "ui/accessibility/ax_tree_id.h"
#include "ui/accessibility/ax_tree_update.h"

// Max amount of fuzz data needed to create the next position
const size_t kNextNodePositionMaxDataSize = 4;

// Min/Max node size for generated tree.
const size_t kMinNodeCount = 10;
const size_t kMaxNodeCount = kMinNodeCount + 50;

// Min fuzz data needed for fuzzer to function.
// Tree of minimum size with text for each node + 2 positions.
const size_t kMinFuzzDataSize =
    kMinNodeCount * AXTreeFuzzerGenerator::kMinimumNewNodeFuzzDataSize +
    kMinNodeCount * AXTreeFuzzerGenerator::kMinTextFuzzDataSize +
    2 * kNextNodePositionMaxDataSize;
// Cap fuzz data to avoid slowness.
const size_t kMaxFuzzDataSize = 3500;

using TestPositionType =
    std::unique_ptr<ui::AXPosition<ui::AXNodePosition, ui::AXNode>>;
using TestPositionRange =
    ui::AXRange<ui::AXPosition<ui::AXNodePosition, ui::AXNode>>;

// Helper to create positions in the given tree.
class AXNodePositionFuzzerGenerator {
 public:
  AXNodePositionFuzzerGenerator(ui::AXTree* tree,
                                ui::AXNodeID max_id,
                                FuzzerData& fuzzer_data);

  TestPositionType CreateNewPosition();
  TestPositionType GenerateNextPosition(TestPositionType& current_position,
                                        TestPositionType& previous_position);

  static void CallPositionAPIs(TestPositionType& position,
                               TestPositionType& other_position);

 private:
  static ax::mojom::MoveDirection GenerateMoveDirection(unsigned char byte);
  static ax::mojom::TextAffinity GenerateTextAffinity(unsigned char byte);
  static ui::AXPositionKind GeneratePositionKind(unsigned char byte);
  static ui::AXPositionAdjustmentBehavior GenerateAdjustmentBehavior(
      unsigned char byte);
  static ui::AXMovementOptions GenerateMovementOptions(
      unsigned char behavior_byte,
      unsigned char detection_byte);

  TestPositionType CreateNewPosition(ui::AXNodeID anchor_id,
                                     int child_index_or_text_offset,
                                     ui::AXPositionKind position_kind,
                                     ax::mojom::TextAffinity affinity);

  raw_ptr<ui::AXTree> tree_;
  const ui::AXNodeID max_id_;
  const raw_ref<FuzzerData> fuzzer_data_;
};

AXNodePositionFuzzerGenerator::AXNodePositionFuzzerGenerator(
    ui::AXTree* tree,
    ui::AXNodeID max_id,
    FuzzerData& fuzzer_data)
    : tree_(tree), max_id_(max_id), fuzzer_data_(fuzzer_data) {}

TestPositionType AXNodePositionFuzzerGenerator::CreateNewPosition() {
  return CreateNewPosition(fuzzer_data_->NextByte(), fuzzer_data_->NextByte(),
                           GeneratePositionKind(fuzzer_data_->NextByte()),
                           GenerateTextAffinity(fuzzer_data_->NextByte()));
}

TestPositionType AXNodePositionFuzzerGenerator::CreateNewPosition(
    ui::AXNodeID anchor_id,
    int child_index_or_text_offset,
    ui::AXPositionKind position_kind,
    ax::mojom::TextAffinity affinity) {
  // To ensure that anchor_id is between |ui::kInvalidAXNodeID| and the max ID
  // of the tree (non-inclusive), get a number [0, max_id - 1) and then shift by
  // 1 to get [1, max_id)
  anchor_id = (anchor_id % (max_id_ - 1)) + 1;
  ui::AXNode* anchor = tree_->GetFromId(anchor_id);
  DCHECK(anchor);

  switch (position_kind) {
    case ui::AXPositionKind::TREE_POSITION:
      // Avoid division by zero in the case where the node has no children.
      child_index_or_text_offset =
          anchor->GetChildCount()
              ? child_index_or_text_offset % anchor->GetChildCount()
              : 0;
      return ui::AXNodePosition::CreateTreePosition(*anchor,
                                                    child_index_or_text_offset);
    case ui::AXPositionKind::TEXT_POSITION: {
      // Avoid division by zero in the case where the node has no text.
      child_index_or_text_offset =
          anchor->GetTextContentLengthUTF16()
              ? child_index_or_text_offset % anchor->GetTextContentLengthUTF16()
              : 0;
      return ui::AXNodePosition::CreateTextPosition(
          *anchor, child_index_or_text_offset, affinity);
      case ui::AXPositionKind::NULL_POSITION:
        NOTREACHED_IN_MIGRATION();
        return ui::AXNodePosition::CreateNullPosition();
    }
  }
}

ax::mojom::MoveDirection AXNodePositionFuzzerGenerator::GenerateMoveDirection(
    unsigned char byte) {
  constexpr unsigned char max_value =
      static_cast<unsigned char>(ax::mojom::MoveDirection::kMaxValue);
  return static_cast<ax::mojom::MoveDirection>(byte % max_value);
}

ax::mojom::TextAffinity AXNodePositionFuzzerGenerator::GenerateTextAffinity(
    unsigned char byte) {
  constexpr unsigned char max_value =
      static_cast<unsigned char>(ax::mojom::TextAffinity::kMaxValue);
  return static_cast<ax::mojom::TextAffinity>(byte % max_value);
}

ui::AXPositionKind AXNodePositionFuzzerGenerator::GeneratePositionKind(
    unsigned char byte) {
  return byte % 2 ? ui::AXPositionKind::TREE_POSITION
                  : ui::AXPositionKind::TEXT_POSITION;
}

ui::AXPositionAdjustmentBehavior
AXNodePositionFuzzerGenerator::GenerateAdjustmentBehavior(unsigned char byte) {
  return byte % 2 ? ui::AXPositionAdjustmentBehavior::kMoveBackward
                  : ui::AXPositionAdjustmentBehavior::kMoveForward;
}

ui::AXMovementOptions AXNodePositionFuzzerGenerator::GenerateMovementOptions(
    unsigned char behavior_byte,
    unsigned char detection_byte) {
  return ui::AXMovementOptions(
      static_cast<ui::AXBoundaryBehavior>(behavior_byte % 3),
      static_cast<ui::AXBoundaryDetection>(detection_byte % 3));
}

TestPositionType AXNodePositionFuzzerGenerator::GenerateNextPosition(
    TestPositionType& current_position,
    TestPositionType& previous_position) {
  switch (fuzzer_data_->NextByte() % 55) {
    case 0:
    default:
      return CreateNewPosition();
    case 1:
      return current_position->AsValidPosition();
    case 2:
      return current_position->AsTreePosition();
    case 3:
      return current_position->AsLeafTreePosition();
    case 4:
      return current_position->AsTextPosition();
    case 5:
      return current_position->AsLeafTextPosition();
    case 6:
      return current_position->AsDomSelectionPosition();
    case 7:
      return current_position->AsUnignoredPosition(
          GenerateAdjustmentBehavior(fuzzer_data_->NextByte()));
    case 8:
      return current_position->CreateAncestorPosition(
          previous_position->GetAnchor(),
          GenerateMoveDirection(fuzzer_data_->NextByte()));
    case 9:
      return current_position->CreatePositionAtStartOfAnchor();
    case 10:
      return current_position->CreatePositionAtEndOfAnchor();
    case 11:
      return current_position->CreatePositionAtStartOfAXTree();
    case 12:
      return current_position->CreatePositionAtEndOfAXTree();
    case 13:
      return current_position->CreatePositionAtStartOfContent();
    case 14:
      return current_position->CreatePositionAtEndOfContent();
    case 15:
      return current_position->CreateChildPositionAt(fuzzer_data_->NextByte() %
                                                     10);
    case 16:
      return current_position->CreateParentPosition(
          GenerateMoveDirection(fuzzer_data_->NextByte()));
    case 17:
      return current_position->CreateNextLeafTreePosition();
    case 18:
      return current_position->CreatePreviousLeafTreePosition();
    case 19:
      return current_position->CreateNextLeafTextPosition();
    case 20:
      return current_position->CreatePreviousLeafTextPosition();
    case 21:
      return current_position->AsLeafTextPositionBeforeCharacter();
    case 22:
      return current_position->AsLeafTextPositionAfterCharacter();
    case 23:
      return current_position->CreatePreviousCharacterPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 24:
      return current_position->CreateNextWordStartPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 25:
      return current_position->CreatePreviousWordStartPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 26:
      return current_position->CreateNextWordEndPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 27:
      return current_position->CreatePreviousWordEndPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 28:
      return current_position->CreateNextLineStartPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 29:
      return current_position->CreatePreviousLineStartPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 30:
      return current_position->CreateNextLineEndPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 31:
      return current_position->CreatePreviousLineEndPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 32:
      return current_position->CreateNextFormatStartPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 33:
      return current_position->CreatePreviousFormatStartPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 34:
      return current_position->CreateNextFormatEndPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 35:
      return current_position->CreatePreviousFormatEndPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 36:
      return current_position->CreateNextSentenceStartPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 37:
      return current_position->CreatePreviousSentenceStartPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 38:
      return current_position->CreateNextSentenceEndPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 39:
      return current_position->CreatePreviousSentenceEndPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 40:
      return current_position->CreateNextParagraphStartPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 41:
      return current_position
          ->CreateNextParagraphStartPositionSkippingEmptyParagraphs(
              GenerateMovementOptions(fuzzer_data_->NextByte(),
                                      fuzzer_data_->NextByte()));
    case 42:
      return current_position->CreatePreviousParagraphStartPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 43:
      return current_position
          ->CreatePreviousParagraphStartPositionSkippingEmptyParagraphs(
              GenerateMovementOptions(fuzzer_data_->NextByte(),
                                      fuzzer_data_->NextByte()));
    case 44:
      return current_position->CreateNextParagraphEndPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 45:
      return current_position->CreatePreviousParagraphEndPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 46:
      return current_position->CreateNextPageStartPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 47:
      return current_position->CreatePreviousPageStartPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 48:
      return current_position->CreateNextPageEndPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 49:
      return current_position->CreatePreviousPageEndPosition(
          GenerateMovementOptions(fuzzer_data_->NextByte(),
                                  fuzzer_data_->NextByte()));
    case 52:
      return current_position->CreateNextAnchorPosition();
    case 53:
      return current_position->CreatePreviousAnchorPosition();
    case 54:
      return current_position->LowestCommonAncestorPosition(
          *previous_position, GenerateMoveDirection(fuzzer_data_->NextByte()));
  }
}

void AXNodePositionFuzzerGenerator::CallPositionAPIs(
    TestPositionType& position,
    TestPositionType& other_position) {
  // Call APIs on the created position. We don't care about any of the results,
  // we just want to make sure none of these crash or hang.
  std::ignore = position->GetAnchor();
  std::ignore = position->GetAnchorSiblingCount();
  std::ignore = position->IsIgnored();
  std::ignore = position->IsLeaf();
  std::ignore = position->IsValid();
  std::ignore = position->AtStartOfWord();
  std::ignore = position->AtEndOfWord();
  std::ignore = position->AtStartOfLine();
  std::ignore = position->AtEndOfLine();
  std::ignore = position->GetFormatStartBoundaryType();
  std::ignore = position->GetFormatEndBoundaryType();
  std::ignore = position->AtStartOfSentence();
  std::ignore = position->AtEndOfSentence();
  std::ignore = position->AtStartOfParagraph();
  std::ignore = position->AtEndOfParagraph();
  std::ignore = position->AtStartOfInlineBlock();
  std::ignore = position->AtStartOfPage();
  std::ignore = position->AtEndOfPage();
  std::ignore = position->AtStartOfAXTree();
  std::ignore = position->AtEndOfAXTree();
  std::ignore = position->AtStartOfContent();
  std::ignore = position->AtEndOfContent();
  std::ignore = position->LowestCommonAnchor(*other_position);
  std::ignore = position->CompareTo(*other_position);
  std::ignore = position->GetText();
  std::ignore = position->IsPointingToLineBreak();
  std::ignore = position->IsInTextObject();
  std::ignore = position->IsInWhiteSpace();
  std::ignore = position->MaxTextOffset();
  std::ignore = position->GetRole();
}

struct Environment {
  Environment() { CHECK(base::i18n::InitializeICU()); }
  base::AtExitManager at_exit_manager;
};

// Entry point for LibFuzzer.
extern "C" int LLVMFuzzerTestOneInput(const unsigned char* data, size_t size) {
  if (size < kMinFuzzDataSize || size > kMaxFuzzDataSize)
    return 0;
  static Environment env;
  AXTreeFuzzerGenerator generator;
  FuzzerData fuzz_data(data, size);
  const size_t node_count =
      kMinNodeCount + fuzz_data.NextByte() % kMaxNodeCount;
  generator.GenerateInitialUpdate(fuzz_data, node_count);
  ui::AXNodeID max_id = generator.GetMaxAssignedID();

  ui::AXTree* tree = generator.GetTree();

  // Run with --v=1 to aid in debugging a specific crash.
  VLOG(1) << tree->ToString();

  // Check to ensure there is enough fuzz data to create two positions.
  if (fuzz_data.RemainingBytes() < kNextNodePositionMaxDataSize * 2)
    return 0;
  AXNodePositionFuzzerGenerator position_fuzzer(tree, max_id, fuzz_data);

  // Having two positions allows us to test "more interesting" APIs that do work
  // on multiple positions.
  TestPositionType previous_position = position_fuzzer.CreateNewPosition();
  TestPositionType position = position_fuzzer.CreateNewPosition();

  while (fuzz_data.RemainingBytes() > kNextNodePositionMaxDataSize) {
    // Run with --v=1 to aid in debugging a specific crash.
    VLOG(1) << position->ToString() << fuzz_data.RemainingBytes();

    position_fuzzer.CallPositionAPIs(position, previous_position);

    // Determine next position to test:
    TestPositionType next_position =
        position_fuzzer.GenerateNextPosition(position, previous_position);
    previous_position = std::move(position);
    position = std::move(next_position);
  }

  return 0;
}