blob: 76368433323882e660eb291a689bf85c1195c928 [file] [log] [blame]
// Copyright 2014 Google Inc. All Rights Reserved.
//
// 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.
//
// Allocation filter transform instrumentation unit-tests.
#include "syzygy/instrument/transforms/allocation_filter_transform.h"
#include "base/values.h"
#include "base/json/json_writer.h"
#include "gtest/gtest.h"
#include "syzygy/block_graph/basic_block.h"
#include "syzygy/block_graph/basic_block_decomposer.h"
#include "syzygy/block_graph/basic_block_subgraph.h"
#include "syzygy/block_graph/block_graph.h"
#include "syzygy/block_graph/typed_block.h"
#include "syzygy/core/unittest_util.h"
#include "syzygy/instrument/transforms/unittest_util.h"
#include "mnemonics.h" // NOLINT
namespace instrument {
namespace transforms {
namespace {
using base::DictionaryValue;
using base::ListValue;
using base::Value;
using block_graph::BasicBlock;
using block_graph::BasicBlockDecomposer;
using block_graph::BasicBlockSubGraph;
using block_graph::BasicCodeBlock;
using block_graph::BlockGraph;
using block_graph::Instruction;
typedef AllocationFilterTransform::Offset Offset;
typedef AllocationFilterTransform::OffsetSet OffsetSet;
typedef AllocationFilterTransform::FunctionNameOffsetMap
FunctionNameOffsetMap;
static wchar_t kConfigBadPathDoesNotExist[] =
L"syzygy/instrument/test_data/"
L"allocation-filter-bad-path-does-not-exist.json";
static wchar_t kConfigBadInvalidkey[] =
L"syzygy/instrument/test_data/allocation-filter-bad-invalid-key.json";
static wchar_t kConfigBadInvalidOffsetList[] =
L"syzygy/instrument/test_data/"
L"allocation-filter-bad-invalid-offset-list.json";
static wchar_t kConfigBadList[] =
L"syzygy/instrument/test_data/allocation-filter-bad-list.json";
static wchar_t kConfigBadNegativeOffset[] =
L"syzygy/instrument/test_data/allocation-filter-bad-negative-offset.json";
static wchar_t kConfigBadString[] =
L"syzygy/instrument/test_data/allocation-filter-bad-string.json";
static wchar_t kConfigGoodEmpty[] =
L"syzygy/instrument/test_data/allocation-filter-good-empty.json";
static wchar_t kConfigGoodFull[] =
L"syzygy/instrument/test_data/allocation-filter-good-full.json";
static wchar_t kConfigGoodMinimal[] =
L"syzygy/instrument/test_data/allocation-filter-good-minimal.json";
class TestAllocationFilterTransform : public AllocationFilterTransform {
public:
using AllocationFilterTransform::pre_call_hook_ref_;
using AllocationFilterTransform::post_call_hook_ref_;
using AllocationFilterTransform::targets_;
using AllocationFilterTransform::instrumented_;
TestAllocationFilterTransform()
: AllocationFilterTransform(FunctionNameOffsetMap()) {
// Disabling reporting to make the tests faster, as reporting is very slow
// when there are many invalid targets. This only avoids logging;
// instrumented calls are still tracked.
set_enable_reporting(false);
set_debug_friendly(true);
}
};
class AllocationFilterTransformTest : public testing::TestDllTransformTest {
public:
// Collects all the valid call instructions of the target binary.
FunctionNameOffsetMap CollectCalls();
// Generates strictly invalid target addresses based on the target binary.
// e.g. pointing to non-call instructions or invalid instruction offsets.
// Non-existent function names are also included.
FunctionNameOffsetMap GenerateInvalidTargets();
// Ensures that the target binary is correctly instrumented.
// Also checks that function names in |do_not_hook_| were not instrumented.
void CheckInstrumentation();
// Ensures that the basic block does not contains any hooked calls.
void CheckBasicBlockIsClean(const BasicCodeBlock* bb_code_block);
// Ensures that all calls in the basic block are correctly instrumented.
void CheckBasicBlockIsInstrumented(const BasicCodeBlock* bb_code_block);
// Dumps target call addresses which are represented by a function name and
// an offset to a JSON string.
// The resulting |json| string will follow the following format:
// {
// "hooks": {
// "function_name1": [offset1_1, offset1_2, ...],
// "function_name2": [offset2_1, offset2_2, ...],
// "function_name3": [offset3_1, offset3_2, ...],
// ...
// }
// }
// All offsets are represented as integers.
// @param pretty_print If true the JSON output will be pretty-printed.
// @param targets Source map to be JSON-ified.
// @param json Output parameter, a JSON string containing the call
// addresses in |targets| following the format described above.
static void WriteToJSON(bool pretty_print,
const FunctionNameOffsetMap& targets,
std::string* json);
protected:
TestAllocationFilterTransform tx_;
// Function names that won't be hooked.
std::set<std::string> do_not_hook_;
};
// This function detects a hooked call, which consists of three contiguous
// instructions as follows:
// CALL pre_call_hook
// CALL some_address or [register] // The original call
// CALL post_call_hook
void AllocationFilterTransformTest::CheckBasicBlockIsClean(
const BasicCodeBlock* bb_code_block) {
BasicBlock::Instructions::const_iterator inst_iter =
bb_code_block->instructions().begin();
for (; inst_iter != bb_code_block->instructions().end(); ++inst_iter) {
if (inst_iter->IsCall() && !inst_iter->CallsNonReturningFunction()) {
BasicBlock::Instructions::const_iterator next_iter = inst_iter;
// A call that contains a single reference to pre_call_hoook_.
if (I_CALL != next_iter->representation().opcode)
continue;
if (1u != next_iter->references().size())
continue;
if (tx_.pre_call_hook_ref_.referenced() !=
inst_iter->references().begin()->second.block())
continue;
// Original call.
++next_iter;
if (next_iter == bb_code_block->instructions().end())
continue;
if (I_CALL != next_iter->representation().opcode)
continue;
// A call that contains a single reference to post_call_hoook_.
++next_iter;
if (next_iter == bb_code_block->instructions().end())
continue;
if (I_CALL != next_iter->representation().opcode)
continue;
if (1u != next_iter->references().size())
continue;
if (tx_.post_call_hook_ref_.referenced() !=
next_iter->references().begin()->second.block())
continue;
// If this point is reached it means that a hooked call was found.
FAIL();
}
}
}
void AllocationFilterTransformTest::CheckBasicBlockIsInstrumented(
const BasicCodeBlock* bb_code_block) {
BasicBlock::Instructions::const_iterator inst_iter =
bb_code_block->instructions().begin();
for (; inst_iter != bb_code_block->instructions().end(); ++inst_iter) {
if (inst_iter->IsCall() && !inst_iter->CallsNonReturningFunction()) {
// Call to the enter hook.
EXPECT_EQ(I_CALL, inst_iter->representation().opcode);
EXPECT_EQ(1u, inst_iter->references().size());
EXPECT_EQ(tx_.pre_call_hook_ref_.referenced(),
inst_iter->references().begin()->second.block());
// Original call.
++inst_iter;
EXPECT_NE(bb_code_block->instructions().end(), inst_iter);
EXPECT_EQ(I_CALL, inst_iter->representation().opcode);
// Call to the exit hook.
++inst_iter;
EXPECT_NE(bb_code_block->instructions().end(), inst_iter);
EXPECT_EQ(I_CALL, inst_iter->representation().opcode);
EXPECT_EQ(1u, inst_iter->references().size());
EXPECT_EQ(tx_.post_call_hook_ref_.referenced(),
inst_iter->references().begin()->second.block());
}
}
}
FunctionNameOffsetMap AllocationFilterTransformTest::CollectCalls() {
FunctionNameOffsetMap call_addresses;
// Let's examine each eligible block to collect 'call' instructions.
BlockGraph::BlockMap::const_iterator block_iter =
block_graph_.blocks().begin();
for (; block_iter != block_graph_.blocks().end(); ++block_iter) {
const BlockGraph::Block& block = block_iter->second;
// Skip non-code blocks.
if (block.type() != BlockGraph::CODE_BLOCK)
continue;
// Skip non-decomposable blocks.
if (!policy_->BlockIsSafeToBasicBlockDecompose(&block))
continue;
// Decompose the block to basic-blocks.
BasicBlockSubGraph subgraph;
BasicBlockDecomposer bb_decomposer(&block, &subgraph);
EXPECT_TRUE(bb_decomposer.Decompose());
// Retrieve the first basic block.
DCHECK_EQ(1U, subgraph.block_descriptions().size());
const std::string function_name = block_iter->second.name();
BasicBlockSubGraph::BBCollection::const_iterator bb_iter =
subgraph.basic_blocks().begin();
// For each valid block, collect the function name and offset for each call.
for (; bb_iter != subgraph.basic_blocks().end(); ++bb_iter) {
const BasicCodeBlock* bb = BasicCodeBlock::Cast(*bb_iter);
if (bb == NULL || bb->is_padding() || !bb->IsValid())
continue;
BasicBlock::Instructions::const_iterator inst_iter =
bb->instructions().begin();
BasicBlock::Instructions::iterator next_iter;
Instruction::Offset inst_offset = bb->offset();
for (; inst_iter != bb->instructions().end();
// Adjust the offset for next instruction.
inst_offset += inst_iter->size(),
++inst_iter) {
if (inst_iter->IsCall() && !inst_iter->CallsNonReturningFunction()) {
EXPECT_EQ(I_CALL, inst_iter->representation().opcode);
call_addresses[function_name].insert(inst_offset + inst_iter->size());
}
}
}
}
return call_addresses;
}
FunctionNameOffsetMap AllocationFilterTransformTest::GenerateInvalidTargets() {
FunctionNameOffsetMap invalid_targets;
// Let's examine each eligible block to generate some invalid targets. So,
// generated targets are not completely unrelated to the binary.
BlockGraph::BlockMap::const_iterator block_iter =
block_graph_.blocks().begin();
for (; block_iter != block_graph_.blocks().end(); ++block_iter) {
const BlockGraph::Block& block = block_iter->second;
// Skip non-code blocks.
if (block.type() != BlockGraph::CODE_BLOCK)
continue;
// Skip non-decomposable blocks.
if (!policy_->BlockIsSafeToBasicBlockDecompose(&block))
continue;
// Decompose the block to basic-blocks.
BasicBlockSubGraph subgraph;
BasicBlockDecomposer bb_decomposer(&block, &subgraph);
EXPECT_TRUE(bb_decomposer.Decompose());
BasicBlockSubGraph::BBCollection::const_iterator bb_iter =
subgraph.basic_blocks().begin();
const std::string function_name = block_iter->second.name();
// Add offsets for inexistent function names.
for (Instruction::Offset offset = 0; offset < 0xFF; offset += 7) {
const std::string unique_name = function_name + "180914_unique_suffix";
invalid_targets[unique_name].insert(offset);
}
// For each valid block, collect the function name and offset for each call.
for (; bb_iter != subgraph.basic_blocks().end(); ++bb_iter) {
const BasicCodeBlock* bb = BasicCodeBlock::Cast(*bb_iter);
if (bb == NULL || bb->is_padding() || !bb->IsValid())
continue;
BasicBlock::Instructions::const_iterator inst_iter =
bb->instructions().begin();
BasicBlock::Instructions::iterator next_iter;
Instruction::Offset inst_offset = bb->offset();
bool previous_inst_is_call = true;
for (; inst_iter != bb->instructions().end();
// Adjust the offset for next instruction.
inst_offset += inst_iter->size(),
++inst_iter) {
// Add non-call address.
if (!previous_inst_is_call)
invalid_targets[function_name].insert(inst_offset);
// Add an offset out of the instruction boundary.
if (inst_iter->size() > 1) {
Instruction::Offset out_of_inst_boundary =
inst_offset + inst_iter->size() - 1;
invalid_targets[function_name].insert(out_of_inst_boundary);
}
previous_inst_is_call = inst_iter->IsCall();
}
}
}
return invalid_targets;
}
void AllocationFilterTransformTest::CheckInstrumentation() {
// Let's examine each eligible block to verify that its basic blocks have been
// instrumented (or not).
BlockGraph::BlockMap::const_iterator block_iter =
block_graph_.blocks().begin();
for (; block_iter != block_graph_.blocks().end(); ++block_iter) {
const BlockGraph::Block& block = block_iter->second;
// Skip non-code blocks.
if (block.type() != BlockGraph::CODE_BLOCK)
continue;
// Skip non-decomposable blocks.
if (!policy_->BlockIsSafeToBasicBlockDecompose(&block))
continue;
// Decompose the block to basic-blocks.
BasicBlockSubGraph subgraph;
BasicBlockDecomposer bb_decomposer(&block, &subgraph);
ASSERT_TRUE(bb_decomposer.Decompose());
BasicBlockSubGraph::BBCollection::const_iterator bb_iter =
subgraph.basic_blocks().begin();
for (; bb_iter != subgraph.basic_blocks().end(); ++bb_iter) {
const BasicCodeBlock* bb = BasicCodeBlock::Cast(*bb_iter);
if (bb == NULL || bb->is_padding() || !bb->IsValid())
continue;
// Check that the functions in |do_not_hook_| are not instrumented.
// The remaining functions should be instrumented.
if (do_not_hook_.find(block_iter->second.name()) != do_not_hook_.end())
ASSERT_NO_FATAL_FAILURE(CheckBasicBlockIsClean(bb));
else
ASSERT_NO_FATAL_FAILURE(CheckBasicBlockIsInstrumented(bb));
}
}
}
void AllocationFilterTransformTest::WriteToJSON(
bool pretty_print,
const AllocationFilterTransform::FunctionNameOffsetMap& targets,
std::string* json) {
DCHECK_NE(static_cast<std::string*>(NULL), json);
DictionaryValue* hooks = new DictionaryValue();
FunctionNameOffsetMap::const_iterator it = targets.begin();
for (; it != targets.end(); ++it) {
std::string function_name = it->first;
// Build the list of offsets.
ListValue * offset_list = new ListValue();
OffsetSet::const_iterator offset_iter = it->second.begin();
for (; offset_iter != it->second.end(); ++offset_iter) {
int offset = static_cast<int>(*offset_iter);
// Offsets shouldn't be too large to overflow an int.
DCHECK_EQ(static_cast<Offset>(static_cast<int>(*offset_iter)),
*offset_iter);
offset_list->AppendInteger(offset);
}
// Store the function name associated with its offset list.
hooks->SetWithoutPathExpansion(function_name, offset_list);
}
DictionaryValue outer_dict;
outer_dict.SetWithoutPathExpansion("hooks", hooks);
if (pretty_print) {
base::JSONWriter::WriteWithOptions(
outer_dict, base::JSONWriter::OPTIONS_PRETTY_PRINT, json);
} else {
base::JSONWriter::WriteWithOptions(outer_dict, 0, json);
}
}
} // namespace
TEST_F(AllocationFilterTransformTest, InstrumentAllCalls) {
ASSERT_NO_FATAL_FAILURE(DecomposeTestDll());
// Collect all the instrumentable calls.
tx_.targets_ = CollectCalls();
// Skip none.
do_not_hook_.clear();
ASSERT_TRUE(block_graph::ApplyBlockGraphTransform(
&tx_, policy_, &block_graph_, header_block_));
ASSERT_TRUE(tx_.pre_call_hook_ref_.IsValid());
ASSERT_TRUE(tx_.post_call_hook_ref_.IsValid());
// Validate that all basic blocks have been instrumented.
ASSERT_NO_FATAL_FAILURE(CheckInstrumentation());
}
TEST_F(AllocationFilterTransformTest, InstrumentTargetedCallsOnly) {
ASSERT_NO_FATAL_FAILURE(DecomposeTestDll());
FunctionNameOffsetMap targets = CollectCalls();
size_t index = 0;
FunctionNameOffsetMap::const_iterator it = targets.begin();
for (; it != targets.end(); it++, index++) {
if (index % 2 == 0) {
tx_.targets_.insert(*it);
} else {
do_not_hook_.insert(it->first);
}
}
// Apply the allocation filter transform only to some specific functions.
ASSERT_TRUE(block_graph::ApplyBlockGraphTransform(
&tx_, policy_, &block_graph_, header_block_));
EXPECT_TRUE(tx_.pre_call_hook_ref_.IsValid());
EXPECT_TRUE(tx_.post_call_hook_ref_.IsValid());
// Checks the instrumented and non-instrumented basic blocks.
ASSERT_NO_FATAL_FAILURE(CheckInstrumentation());
}
TEST_F(AllocationFilterTransformTest, InvalidTargetsAreIgnored) {
ASSERT_NO_FATAL_FAILURE(DecomposeTestDll());
// Loads lots of strictly invalid target addresses, including non-call
// instructions, invalid offsets, and non-existent function names.
tx_.targets_ = GenerateInvalidTargets();
// Apply the allocation filter transform with invalid targets.
ASSERT_TRUE(block_graph::ApplyBlockGraphTransform(
&tx_, policy_, &block_graph_, header_block_));
EXPECT_TRUE(tx_.pre_call_hook_ref_.IsValid());
EXPECT_TRUE(tx_.post_call_hook_ref_.IsValid());
// Check that no invalid addresses were instrumented.
EXPECT_TRUE(tx_.instrumented_.empty());
}
TEST_F(AllocationFilterTransformTest, JSONReadWrite) {
ASSERT_NO_FATAL_FAILURE(DecomposeTestDll());
// This test JSON-ifies a valid target address map, then loads a new map
// from the produced JSON and ensures that both,
// (the original and de-serialized) are equal.
// Collect all call addresses.
FunctionNameOffsetMap original = CollectCalls();
std::string json;
// Dump all the addresses to JSON.
WriteToJSON(true, original, &json);
// Loads from JSON.
FunctionNameOffsetMap output;
EXPECT_TRUE(AllocationFilterTransform::ReadFromJSON(json, &output));
ASSERT_EQ(original, output);
}
TEST_F(AllocationFilterTransformTest, JSONFailsPathDoesNotExists) {
base::FilePath config_file = testing::GetSrcRelativePath(
kConfigBadPathDoesNotExist);
FunctionNameOffsetMap output;
EXPECT_FALSE(AllocationFilterTransform::ReadFromJSON(config_file, &output));
}
TEST_F(AllocationFilterTransformTest, JSONFailsInvalidkey) {
base::FilePath config_file = testing::GetSrcRelativePath(
kConfigBadInvalidkey);
FunctionNameOffsetMap output;
EXPECT_FALSE(AllocationFilterTransform::ReadFromJSON(config_file, &output));
}
TEST_F(AllocationFilterTransformTest, JSONFailsInvalidOffsetList) {
base::FilePath config_file = testing::GetSrcRelativePath(
kConfigBadInvalidOffsetList);
FunctionNameOffsetMap output;
EXPECT_FALSE(AllocationFilterTransform::ReadFromJSON(config_file, &output));
}
TEST_F(AllocationFilterTransformTest, JSONFailsList) {
base::FilePath config_file = testing::GetSrcRelativePath(
kConfigBadList);
FunctionNameOffsetMap output;
EXPECT_FALSE(AllocationFilterTransform::ReadFromJSON(config_file, &output));
}
TEST_F(AllocationFilterTransformTest, JSONFailsNegativeOffset) {
base::FilePath config_file = testing::GetSrcRelativePath(
kConfigBadNegativeOffset);
FunctionNameOffsetMap output;
EXPECT_FALSE(AllocationFilterTransform::ReadFromJSON(config_file, &output));
}
TEST_F(AllocationFilterTransformTest, JSONFailsString) {
base::FilePath config_file = testing::GetSrcRelativePath(
kConfigBadString);
FunctionNameOffsetMap output;
EXPECT_FALSE(AllocationFilterTransform::ReadFromJSON(config_file, &output));
}
TEST_F(AllocationFilterTransformTest, JSONLoadsEmpty) {
base::FilePath config_file = testing::GetSrcRelativePath(
kConfigGoodEmpty);
FunctionNameOffsetMap output;
EXPECT_TRUE(AllocationFilterTransform::ReadFromJSON(config_file, &output));
}
TEST_F(AllocationFilterTransformTest, JSONLoadsFull) {
base::FilePath config_file = testing::GetSrcRelativePath(
kConfigGoodFull);
FunctionNameOffsetMap output;
EXPECT_TRUE(AllocationFilterTransform::ReadFromJSON(config_file, &output));
}
TEST_F(AllocationFilterTransformTest, JSONLoadsMinimal) {
base::FilePath config_file = testing::GetSrcRelativePath(
kConfigGoodMinimal);
FunctionNameOffsetMap output;
EXPECT_TRUE(AllocationFilterTransform::ReadFromJSON(config_file, &output));
}
} // namespace transforms
} // namespace instrument