// 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.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "ui/accessibility/ax_tree_fuzzer_util.h"
#include <vector>
#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_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_id.h"
#include "ui/accessibility/ax_tree_update.h"
FuzzerData::FuzzerData(const unsigned char* data, size_t size)
: data_(data), data_size_(size), data_index_(0) {}
size_t FuzzerData::RemainingBytes() {
return data_size_ - data_index_;
}
unsigned char FuzzerData::NextByte() {
CHECK(RemainingBytes());
return data_[data_index_++];
}
const unsigned char* FuzzerData::NextBytes(size_t amount) {
CHECK(RemainingBytes() >= amount);
const unsigned char* current_position = &data_[data_index_];
data_index_ += amount;
return current_position;
}
ui::AXTree* AXTreeFuzzerGenerator::GetTree() {
return tree_manager_.GetTree();
}
void AXTreeFuzzerGenerator::GenerateInitialUpdate(FuzzerData& fuzz_data,
int node_count) {
max_assigned_node_id_ = 1;
ui::AXTreeUpdate initial_state;
initial_state.root_id = max_assigned_node_id_++;
initial_state.has_tree_data = true;
initial_state.tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID();
ui::AXNodeData root;
root.id = initial_state.root_id;
root.role = ax::mojom::Role::kRootWebArea;
std::stack<size_t> parent_index_stack;
parent_index_stack.push(initial_state.nodes.size());
initial_state.nodes.push_back(root);
// As we give out ids sequentially, starting at 1, the
// ...max_assigned_node_id_... is equivalent to the node count.
while (fuzz_data.RemainingBytes() >= kMinimumNewNodeFuzzDataSize &&
max_assigned_node_id_ < node_count) {
size_t extra_data_size =
fuzz_data.RemainingBytes() - kMinimumNewNodeFuzzDataSize;
ui::AXNodeData& parent = initial_state.nodes[parent_index_stack.top()];
// Create a node.
ui::AXNodeData node = CreateChildNodeData(parent, max_assigned_node_id_++);
// Determine role.
node.role = GetInterestingRole(fuzz_data.NextByte(), parent.role);
// Add role-specific properties.
AddRoleSpecificProperties(
fuzz_data, node,
parent.GetStringAttribute(ax::mojom::StringAttribute::kName),
extra_data_size);
// Determine the relationship of the next node from fuzz data. See
// implementation of `DetermineNextNodeRelationship` for details.
size_t ancestor_pop_count;
switch (DetermineNextNodeRelationship(node.role, fuzz_data.NextByte())) {
case kChild:
CHECK(CanHaveChildren(node.role));
parent_index_stack.push(initial_state.nodes.size());
break;
case kSibling:
initial_state.nodes.push_back(node);
break;
case kSiblingToAncestor:
ancestor_pop_count = 1 + fuzz_data.NextByte() % kMaxAncestorPopCount;
for (size_t i = 0;
i < ancestor_pop_count && parent_index_stack.size() > 1; ++i) {
parent_index_stack.pop();
}
break;
}
initial_state.nodes.push_back(node);
}
// Run with --v=1 to aid in debugging a specific crash.
VLOG(1) << "Input accessibility tree:\n" << initial_state.ToString();
tree_manager_.SetTree(std::make_unique<ui::AXTree>(initial_state));
}
// Pre-order depth first walk of tree. Skip over deleted subtrees.
void AXTreeFuzzerGenerator::RecursiveGenerateUpdate(
const ui::AXNode* node,
ui::AXTreeUpdate& tree_update,
FuzzerData& fuzz_data,
std::set<ui::AXNodeID>& updated_nodes) {
// Stop traversing if we run out of fuzz data.
if (fuzz_data.RemainingBytes() <= kMinimumNewNodeFuzzDataSize)
return;
size_t extra_data_size =
fuzz_data.RemainingBytes() - kMinimumNewNodeFuzzDataSize;
AXTreeFuzzerGenerator::TreeUpdateOperation operation = kNoOperation;
if (!updated_nodes.count(node->id()))
operation = DetermineTreeUpdateOperation(node, fuzz_data.NextByte());
switch (operation) {
case kAddChild: {
// Determine where to insert the node.
// Create node and attach to parent.
ui::AXNodeData parent = node->data();
ui::AXNodeData child =
CreateChildNodeData(parent, max_assigned_node_id_++);
// Determine role.
child.role = GetInterestingRole(fuzz_data.NextByte(), node->GetRole());
// Add role-specific properties.
AddRoleSpecificProperties(
fuzz_data, child,
node->GetStringAttribute(ax::mojom::StringAttribute::kName),
extra_data_size);
// Also add inline text child if we can.
ui::AXNodeData inline_text_data;
if (ui::CanHaveInlineTextBoxChildren(child.role)) {
inline_text_data = CreateChildNodeData(child, max_assigned_node_id_++);
inline_text_data.role = ax::mojom::Role::kInlineTextBox;
inline_text_data.SetName(
child.GetStringAttribute(ax::mojom::StringAttribute::kName));
}
// Add both the current node (parent) and the child to the tree update.
tree_update.nodes.push_back(parent);
tree_update.nodes.push_back(child);
updated_nodes.emplace(parent.id);
updated_nodes.emplace(child.id);
if (inline_text_data.id != ui::kInvalidAXNodeID) {
tree_update.nodes.push_back(inline_text_data);
updated_nodes.emplace(inline_text_data.id);
}
break;
}
case kRemoveNode: {
const ui::AXNode* parent = node->GetParent();
if (updated_nodes.count(parent->id()))
break;
// Determine what node to delete.
// To delete a node, just find the parent and update the child list to
// no longer include this node.
ui::AXNodeData parent_update = parent->data();
std::erase(parent_update.child_ids, node->id());
tree_update.nodes.push_back(parent_update);
updated_nodes.emplace(parent_update.id);
// This node was deleted, don't traverse to the subtree.
return;
}
case kTextChange: {
// Modify the text.
const ui::AXNode* child_inline_text = node->GetFirstChild();
if (!child_inline_text ||
child_inline_text->GetRole() != ax::mojom::Role::kInlineTextBox) {
break;
}
ui::AXNodeData static_text_data = node->data();
ui::AXNodeData inline_text_data = child_inline_text->data();
size_t text_size =
kMinTextFuzzDataSize + fuzz_data.NextByte() % kMaxTextFuzzDataSize;
if (text_size > extra_data_size)
text_size = extra_data_size;
extra_data_size -= text_size;
inline_text_data.SetName(
GenerateInterestingText(fuzz_data.NextBytes(text_size), text_size));
static_text_data.SetName(inline_text_data.GetStringAttribute(
ax::mojom::StringAttribute::kName));
tree_update.nodes.push_back(static_text_data);
tree_update.nodes.push_back(inline_text_data);
updated_nodes.emplace(static_text_data.id);
updated_nodes.emplace(inline_text_data.id);
break;
}
case kNoOperation:
break;
}
// Visit subtree.
for (auto iter = node->AllChildrenBegin(); iter != node->AllChildrenEnd();
++iter) {
RecursiveGenerateUpdate(iter.get(), tree_update, fuzz_data, updated_nodes);
}
}
// When building a tree update, we must take care to not create an
// unserializable tree. If the tree does not serialize, things like
// TestAXTreeObserver will not be able to handle the incorrectly serialized
// tree. This will require us to abort the fuzz run.
bool AXTreeFuzzerGenerator::GenerateTreeUpdate(FuzzerData& fuzz_data,
size_t node_count) {
ui::AXTreeUpdate tree_update;
std::set<ui::AXNodeID> updated_nodes;
RecursiveGenerateUpdate(tree_manager_.GetRoot(), tree_update, fuzz_data,
updated_nodes);
return GetTree()->Unserialize(tree_update);
}
ui::AXNodeID AXTreeFuzzerGenerator::GetMaxAssignedID() const {
return max_assigned_node_id_;
}
ui::AXNodeData AXTreeFuzzerGenerator::CreateChildNodeData(
ui::AXNodeData& parent,
ui::AXNodeID new_node_id) {
ui::AXNodeData node;
node.id = new_node_id;
// Connect parent to this node.
parent.child_ids.push_back(node.id);
return node;
}
// Determine the relationship of the next node from fuzz data.
AXTreeFuzzerGenerator::NextNodeRelationship
AXTreeFuzzerGenerator::DetermineNextNodeRelationship(ax::mojom::Role role,
unsigned char byte) {
// Force this to have a inline text child if it can.
if (ui::CanHaveInlineTextBoxChildren(role))
return NextNodeRelationship::kChild;
// Don't allow inline text boxes to have children or siblings.
if (role == ax::mojom::Role::kInlineTextBox)
return NextNodeRelationship::kSiblingToAncestor;
// Determine next node using fuzz data.
NextNodeRelationship relationship =
static_cast<NextNodeRelationship>(byte % 3);
// Check to ensure we can have children.
if (relationship == NextNodeRelationship::kChild && !CanHaveChildren(role)) {
return NextNodeRelationship::kSibling;
}
return relationship;
}
AXTreeFuzzerGenerator::TreeUpdateOperation
AXTreeFuzzerGenerator::DetermineTreeUpdateOperation(const ui::AXNode* node,
unsigned char byte) {
switch (byte % 4) {
case 0:
// Don't delete the following nodes:
// 1) The root. TODO(janewman): implement root changes in an update.
// 2) Inline text. We don't want to leave Static text nodes without inline
// text children.
if (ax::mojom::Role::kRootWebArea != node->GetRole())
return kRemoveNode;
ABSL_FALLTHROUGH_INTENDED;
case 1:
// Check to ensure this node can have children. Also consider that we
// shouldn't add children to static text, as these nodes only expect to
// have a inline text single child.
if (CanHaveChildren(node->GetRole()) && !ui::IsText(node->GetRole()))
return kAddChild;
ABSL_FALLTHROUGH_INTENDED;
case 2:
if (ax::mojom::Role::kStaticText == node->GetRole())
return kTextChange;
ABSL_FALLTHROUGH_INTENDED;
default:
return kNoOperation;
}
}
void AXTreeFuzzerGenerator::AddRoleSpecificProperties(
FuzzerData& fuzz_data,
ui::AXNodeData& node,
const std::string& parentName,
size_t extra_data_size) {
// TODO(janewman): Add ignored state.
// Add role-specific properties.
if (node.role == ax::mojom::Role::kInlineTextBox) {
node.SetName(parentName);
} else if (node.role == ax::mojom::Role::kLineBreak) {
node.SetName("\n");
} else if (ui::IsText(node.role)) {
size_t text_size =
kMinTextFuzzDataSize + fuzz_data.NextByte() % kMaxTextFuzzDataSize;
if (text_size > extra_data_size)
text_size = extra_data_size;
extra_data_size -= text_size;
node.SetName(
GenerateInterestingText(fuzz_data.NextBytes(text_size), text_size));
}
}
ax::mojom::Role AXTreeFuzzerGenerator::GetInterestingRole(
unsigned char byte,
ax::mojom::Role parent_role) {
if (ui::CanHaveInlineTextBoxChildren(parent_role))
return ax::mojom::Role::kInlineTextBox;
// Bias towards creating text nodes so we end up with more text in the tree.
switch (byte % 7) {
default:
case 0:
case 1:
case 2:
return ax::mojom::Role::kStaticText;
case 3:
return ax::mojom::Role::kLineBreak;
case 4:
return ax::mojom::Role::kParagraph;
case 5:
return ax::mojom::Role::kGenericContainer;
case 6:
return ax::mojom::Role::kGroup;
}
}
bool AXTreeFuzzerGenerator::CanHaveChildren(ax::mojom::Role role) {
switch (role) {
case ax::mojom::Role::kInlineTextBox:
return false;
default:
return true;
}
}
std::u16string AXTreeFuzzerGenerator::GenerateInterestingText(
const unsigned char* data,
size_t size) {
std::u16string wide_str;
for (size_t i = 0; i + 1 < size; i += 2) {
char16_t char_16 = data[i] << 8;
char_16 |= data[i + 1];
// Don't insert a null character.
if (char_16)
wide_str.push_back(char_16);
}
return wide_str;
}