blob: 92b4bdceae1db16fff86a345e30c9841bad4affb [file] [edit]
//===-- copyprof_state_test.cpp -------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "copyprof/copyprof_state.h"
#include "copyprof/copyprof_interface_internal.h"
#include "copyprof/copyprof_shadow.h"
#include "gtest/gtest.h"
namespace __copyprof {
namespace {
// The runtime keeps its per-thread state in a single object that the callbacks
// mutate in place, so without a reset each test would inherit whatever nesting
// levels, mode and flags the previous one left behind -- and a test that
// aborts mid-scenario would cascade into the rest of the suite.
//
// This does NOT reset shadow memory: it is process-global and far too large to
// clear between tests, and the buffers under test are stack addresses that get
// reused across cases. Tests that depend on the shadow state of a buffer must
// establish it explicitly with `MarkApplicationMemory`.
class CopyProfStateTest : public testing::Test {
protected:
void SetUp() override {
__copyprof_init_once();
__copyprof_state = PerThreadState();
}
};
TEST_F(CopyProfStateTest, NestingCounters) {
EXPECT_EQ(__copyprof_state.construct_nesting_level, 0u);
EXPECT_EQ(__copyprof_state.copy_nesting_level, 0u);
EXPECT_EQ(__copyprof_state.destruct_nesting_level, 0u);
EXPECT_EQ(__copyprof_state.smf_context, SmfContext::NONE);
// Enter constructor callback.
__copyprof_ctor_enter_callback(/*this_ptr=*/nullptr, /*obj_size=*/16);
EXPECT_EQ(__copyprof_state.construct_nesting_level, 1u);
EXPECT_EQ(__copyprof_state.smf_context, SmfContext::CTOR);
// Exit constructor callback.
__copyprof_ctor_exit_callback(/*this_ptr=*/nullptr, /*obj_size=*/16);
EXPECT_EQ(__copyprof_state.construct_nesting_level, 0u);
EXPECT_EQ(__copyprof_state.smf_context, SmfContext::NONE);
}
TEST_F(CopyProfStateTest, SimulatedUnnecessaryCopy) {
char obj[32] = {0};
char other[32] = {0};
// Simulate copy constructor execution.
__copyprof_copy_ctor_enter_callback(obj, other, sizeof(obj));
EXPECT_EQ(__copyprof_state.copy_nesting_level, 1u);
EXPECT_EQ(__copyprof_state.smf_context, SmfContext::COPY);
__copyprof_copy_ctor_exit_callback(obj, other, sizeof(obj));
EXPECT_TRUE(IsMarkedAsCopy(obj, sizeof(obj)));
EXPECT_EQ(__copyprof_state.smf_context, SmfContext::NONE);
// Simulate destructor without any intervening store callbacks.
__copyprof_dtor_enter_callback(obj, sizeof(obj));
EXPECT_EQ(__copyprof_state.smf_context, SmfContext::DTOR);
EXPECT_TRUE(__copyprof_state.is_transitive_copy);
__copyprof_dtor_exit_callback(obj, sizeof(obj));
}
TEST_F(CopyProfStateTest, ModifiedCopyIsNotMakedAsCopyAnymore) {
char obj[32] = {0};
char other[32] = {0};
// Create copy `obj` based on `other`.
__copyprof_copy_assign_op_enter_callback(obj, other, sizeof(obj));
__copyprof_copy_assign_op_exit_callback(obj, other, sizeof(obj));
EXPECT_TRUE(IsMarkedAsCopy(obj, sizeof(obj)));
// Modifying `obj` must mark it as non-copy.
__copyprof_store_callback(obj + 4, /*size=*/4);
EXPECT_FALSE(IsMarkedAsCopy(obj, sizeof(obj)));
// After destroying the modified copy, `is_transitive_copy` must be `false`.
__copyprof_dtor_enter_callback(obj, sizeof(obj));
__copyprof_dtor_exit_callback(obj, sizeof(obj));
EXPECT_FALSE(__copyprof_state.is_transitive_copy);
}
TEST_F(CopyProfStateTest, CheckModeIgnoresStores) {
char obj[32] = {0};
char other[32] = {0};
__copyprof_copy_ctor_enter_callback(obj, other, sizeof(obj));
__copyprof_copy_ctor_exit_callback(obj, other, sizeof(obj));
EXPECT_TRUE(IsMarkedAsCopy(obj, sizeof(obj)));
__copyprof_dtor_enter_callback(obj, sizeof(obj));
// Simulate an internal store during destruction (e.g. vtable rewrite or
// member cleanup).
__copyprof_store_callback(obj, sizeof(obj));
// Prove that stores in CHECK mode do not clear the copy shadow bits.
EXPECT_TRUE(IsMarkedAsCopy(obj, sizeof(obj)));
__copyprof_dtor_exit_callback(obj, sizeof(obj));
}
// A temporary object created inside a d'tor is marked as copy, so that
// destroying it does not suppress the report (see
// TemporaryObjectInDtorDoesNotSuppressReport).
TEST_F(CopyProfStateTest, ConstructionInDtorMarksAsCopy) {
char obj[32] = {0};
char other[32] = {0};
char scratch[32] = {0};
// Mark `scratch` as non-copy up front so the shadow write is observable.
MarkApplicationMemory(scratch, sizeof(scratch), /*is_copy=*/false);
__copyprof_copy_ctor_enter_callback(obj, other, sizeof(obj));
__copyprof_copy_ctor_exit_callback(obj, other, sizeof(obj));
// Enter the d'tor, then construct an object over `scratch`.
__copyprof_dtor_enter_callback(obj, sizeof(obj));
__copyprof_ctor_enter_callback(scratch, sizeof(scratch));
__copyprof_ctor_exit_callback(scratch, sizeof(scratch));
EXPECT_TRUE(IsMarkedAsCopy(scratch, sizeof(scratch)));
__copyprof_dtor_exit_callback(obj, sizeof(obj));
}
// Unlike a c'tor, a copy assignment inside a d'tor overwrites an existing
// object, so it must leave shadow memory alone, just like a store.
TEST_F(CopyProfStateTest, CheckModeIgnoresCopyAssignment) {
char obj[32] = {0};
char other[32] = {0};
char target[32] = {0};
// Mark `target` as non-copy up front so a stray shadow write is observable.
MarkApplicationMemory(target, sizeof(target), /*is_copy=*/false);
__copyprof_copy_ctor_enter_callback(obj, other, sizeof(obj));
__copyprof_copy_ctor_exit_callback(obj, other, sizeof(obj));
// Enter the d'tor, then copy-assign to `target`.
__copyprof_dtor_enter_callback(obj, sizeof(obj));
__copyprof_copy_assign_op_enter_callback(target, other, sizeof(target));
__copyprof_copy_assign_op_exit_callback(target, other, sizeof(target));
EXPECT_FALSE(IsMarkedAsCopy(target, sizeof(target)));
__copyprof_dtor_exit_callback(obj, sizeof(obj));
}
// A d'tor whose body constructs and destroys an object that the destroyed
// object does not own (a string built for a log message, a lock guard, an
// iterator) must not suppress the report. Objects constructed inside a d'tor
// are marked as copy, so destroying them leaves `is_transitive_copy` unchanged.
// Contrast with SimulatedUnnecessaryCopy, which is the same scenario with an
// empty d'tor body.
TEST_F(CopyProfStateTest, TemporaryObjectInDtorDoesNotSuppressReport) {
char obj[32] = {0};
char source_obj[32] = {0};
char local[32] = {0};
// `obj` is an unnecessary copy: copy-constructed and never modified.
__copyprof_copy_ctor_enter_callback(obj, source_obj, sizeof(obj));
__copyprof_copy_ctor_exit_callback(obj, source_obj, sizeof(obj));
ASSERT_TRUE(IsMarkedAsCopy(obj, sizeof(obj)));
// Memory occupied by `local` is not a copy.
MarkApplicationMemory(local, sizeof(local), /*is_copy=*/false);
__copyprof_dtor_enter_callback(obj, sizeof(obj));
// Create and destroy `local` while `obj`'s d'tor is running.
__copyprof_ctor_enter_callback(local, sizeof(local));
__copyprof_ctor_exit_callback(local, sizeof(local));
__copyprof_dtor_enter_callback(local, sizeof(local));
__copyprof_dtor_exit_callback(local, sizeof(local));
// `obj` itself was never modified, so it must still be marked as copy.
__copyprof_dtor_exit_callback(obj, sizeof(obj));
EXPECT_TRUE(IsMarkedAsCopy(obj, sizeof(obj)));
EXPECT_TRUE(__copyprof_state.is_transitive_copy);
}
// Likewise for a temporary that the d'tor copy-constructs, e.g. from a member.
TEST_F(CopyProfStateTest, CopiedTemporaryInDtorDoesNotSuppressReport) {
char obj[32] = {0};
char source_obj[32] = {0};
char temp[32] = {0};
__copyprof_copy_ctor_enter_callback(obj, source_obj, sizeof(obj));
__copyprof_copy_ctor_exit_callback(obj, source_obj, sizeof(obj));
// Memory occupied by `temp` is not a copy.
MarkApplicationMemory(temp, sizeof(temp), /*is_copy=*/false);
__copyprof_dtor_enter_callback(obj, sizeof(obj));
__copyprof_copy_ctor_enter_callback(temp, obj, sizeof(temp));
__copyprof_copy_ctor_exit_callback(temp, obj, sizeof(temp));
__copyprof_dtor_enter_callback(temp, sizeof(temp));
__copyprof_dtor_exit_callback(temp, sizeof(temp));
__copyprof_dtor_exit_callback(obj, sizeof(obj));
EXPECT_TRUE(__copyprof_state.is_transitive_copy);
}
// A temporary with a trivial c'tor gets no c'tor callback, so it is not marked
// as copy when a d'tor creates it. If its d'tor is instrumented, destroying it
// folds whatever shadow its memory had before into `is_transitive_copy`, which
// may suppress the report.
//
// DISABLED: known limitation. Possible fixes are to also instrument trivial
// construction, or to skip the fold for objects in the stack frames below the
// top-level d'tor.
TEST_F(CopyProfStateTest,
DISABLED_TriviallyConstructedTemporaryInDtorDoesNotSuppressReport) {
char obj[32] = {0};
char source_obj[32] = {0};
char temp[32] = {0};
__copyprof_copy_ctor_enter_callback(obj, source_obj, sizeof(obj));
__copyprof_copy_ctor_exit_callback(obj, source_obj, sizeof(obj));
// Memory occupied by `temp` is not a copy.
MarkApplicationMemory(temp, sizeof(temp), /*is_copy=*/false);
__copyprof_dtor_enter_callback(obj, sizeof(obj));
// `temp` is created without a c'tor callback, then destroyed.
__copyprof_dtor_enter_callback(temp, sizeof(temp));
__copyprof_dtor_exit_callback(temp, sizeof(temp));
__copyprof_dtor_exit_callback(obj, sizeof(obj));
EXPECT_TRUE(__copyprof_state.is_transitive_copy);
}
} // namespace
} // namespace __copyprof