Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion sgcl/detail/array_metadata.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace sgcl::detail {
struct ArrayMetadata {
template<class T>
ArrayMetadata(T*) noexcept
: child_pointers(TypeInfo<T>::child_pointers)
: child_pointers(TypeInfo<T>::child_pointers())
, destroy(ArrayBase::get_destroy_function<T>())
, type_info(typeid(T[]))
, object_size(TypeInfo<T>::ObjectSize)
Expand Down
18 changes: 9 additions & 9 deletions sgcl/detail/maker.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ namespace sgcl::detail {
if constexpr(Info::MayContainTracked) {
auto& thread = current_thread();
auto range_guard = thread.use_alloc_range({(uintptr_t)(p), sizeof(T)});
if (!Info::child_pointers.final.load(std::memory_order_acquire)) {
if (!Info::child_pointers().final.load(std::memory_order_acquire)) {
auto count = sizeof(Type) / sizeof(RawPointer);
auto mem = (RawPointer*)p;
for (int i = 0; i < count; ++i) {
mem[i].store((void*)size_t(1), std::memory_order_relaxed);
}
auto range_guard = thread.use_child_pointers({(uintptr_t)p, &Info::child_pointers.map});
auto range_guard = thread.use_child_pointers({(uintptr_t)p, &Info::child_pointers().map});
_construct<Type>(p, std::forward<A>(a)...);
Info::child_pointers.final.store(true, std::memory_order_release);
Info::child_pointers().final.store(true, std::memory_order_release);
} else {
_construct<Type>(p, std::forward<A>(a)...);
}
Expand Down Expand Up @@ -90,11 +90,11 @@ namespace sgcl::detail {
auto mem = allocator.alloc();
if constexpr(Info::MayContainTracked) {
auto range_guard = thread.use_alloc_range({(uintptr_t)(mem), sizeof(T)});
if (!Info::child_pointers.final.load(std::memory_order_acquire)) {
if (!Info::child_pointers().final.load(std::memory_order_acquire)) {
std::fill_n((size_t*)mem, sizeof(T) / sizeof(size_t), size_t(1));
auto child_guard = thread.use_child_pointers({(uintptr_t)mem, &Info::child_pointers.map});
auto child_guard = thread.use_child_pointers({(uintptr_t)mem, &Info::child_pointers().map});
_construct_and_register<Type>(mem, std::forward<A>(a)...);
Info::child_pointers.final.store(true, std::memory_order_release);
Info::child_pointers().final.store(true, std::memory_order_release);
} else {
std::memset(mem, 0, sizeof(T));
_construct_and_register<Type>(mem, std::forward<A>(a)...);
Expand Down Expand Up @@ -245,13 +245,13 @@ namespace sgcl::detail {
int offset;
auto& thread = current_thread();
auto range_guard = thread.use_alloc_range({(uintptr_t)(array.data), sizeof(Type) * array.capacity});
if (array.size && !Info::child_pointers.final.load(std::memory_order_acquire)) {
if (array.size && !Info::child_pointers().final.load(std::memory_order_acquire)) {
std::fill_n((size_t*)array.data, sizeof(Type) / sizeof(size_t), size_t(1));
std::memset((void*)((Type*)array.data + 1), 0, sizeof(Type) * (array.capacity - 1));
array.metadata.store(&Info::array_metadata(), std::memory_order_release);
auto child_guard = thread.use_child_pointers({(uintptr_t)array.data, &Info::child_pointers.map});
auto child_guard = thread.use_child_pointers({(uintptr_t)array.data, &Info::child_pointers().map});
_init((Type*)array.data, 0, 1, std::forward<A>(a)...);
Info::child_pointers.final.store(true, std::memory_order_release);
Info::child_pointers().final.store(true, std::memory_order_release);
offset = 1;
} else {
std::memset(array.data, 0, sizeof(Type) * array.capacity);
Expand Down
2 changes: 1 addition & 1 deletion sgcl/detail/metadata.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace sgcl::detail {
struct Metadata {
template<class T>
Metadata(T*) noexcept
: child_pointers(TypeInfo<T>::child_pointers)
: child_pointers(TypeInfo<T>::child_pointers())
, destroy(TypeInfo<T>::get_destroy_function())
, free(TypeInfo<T>::Allocator::free)
, object_size(TypeInfo<T>::ObjectSize)
Expand Down
19 changes: 18 additions & 1 deletion sgcl/detail/page_info.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,24 @@ namespace sgcl::detail {
return *metadata;
}

inline static ChildPointers child_pointers {!MayContainTracked<Type>::value, ObjectSize};
// Lazily constructed (mirrors private_metadata()/array_metadata()
// above), not a plain eagerly-initialized static data member: this
// member's constructor does real dynamic (heap-allocating)
// initialization via ChildPointers' std::vector `map`, so an eager
// `inline static` here would have unspecified initialization order
// relative to every other translation unit's own static-duration
// objects. A caller whose own static/dynamic initializer is the
// first thing to construct a tracked object of this type -- e.g. a
// module-level variable initializer that runs during C++ static
// initialization, before main() -- could otherwise observe this
// member not yet constructed (an under-sized/absent `map`), corrupting
// the child-pointer bitmap offset check in Pointer's constructor.
// A function-local static is guaranteed to initialize exactly once,
// on first call, regardless of static-initialization order.
inline static ChildPointers& child_pointers() {
static ChildPointers cp{!MayContainTracked<Type>::value, ObjectSize};
return cp;
}

private:
static void _destroy(void* p) noexcept {
Expand Down
36 changes: 32 additions & 4 deletions sgcl/tracked_ptr.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,25 +108,53 @@ namespace sgcl {
}

tracked_ptr& operator=(std::nullptr_t) noexcept {
_ptr()->store(nullptr);
if (auto ptr = _ptr()) {
ptr->store(nullptr);
} else {
auto new_ptr = make_tracked<detail::Pointer>();
auto ref = new_ptr.release();
_raw_ptr_ref = _set_flag(ref, ExternalHeapFlag);
ref->store(nullptr);
}
return *this;
}

tracked_ptr& operator=(const tracked_ptr& p) noexcept {
_ptr()->store(p.get());
if (auto self_ptr = _ptr()) {
self_ptr->store(p.get());
} else {
auto new_ptr = make_tracked<detail::Pointer>();
auto ref = new_ptr.release();
_raw_ptr_ref = _set_flag(ref, ExternalHeapFlag);
ref->store(p.get());
}
return *this;
}

template<class U, std::enable_if_t<std::is_convertible_v<typename tracked_ptr<U>::element_type*, element_type*>, int> = 0>
tracked_ptr& operator=(const tracked_ptr<U>& p) noexcept {
_ptr()->store(static_cast<element_type*>(p.get()));
if (auto self_ptr = _ptr()) {
self_ptr->store(static_cast<element_type*>(p.get()));
} else {
auto new_ptr = make_tracked<detail::Pointer>();
auto ref = new_ptr.release();
_raw_ptr_ref = _set_flag(ref, ExternalHeapFlag);
ref->store(static_cast<element_type*>(p.get()));
}
return *this;
}

template<class U, std::enable_if_t<std::is_convertible_v<typename unique_ptr<U>::element_type*, element_type*>, int> = 0>
tracked_ptr& operator=(unique_ptr<U>&& u) noexcept {
auto p = u.release();
_ptr()->store(static_cast<element_type*>(p));
if (auto self_ptr = _ptr()) {
self_ptr->store(static_cast<element_type*>(p));
} else {
auto new_ptr = make_tracked<detail::Pointer>();
auto ref = new_ptr.release();
_raw_ptr_ref = _set_flag(ref, ExternalHeapFlag);
ref->store(static_cast<element_type*>(p));
}
return *this;
}

Expand Down
22 changes: 22 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,29 @@ project(SGCL_Tests LANGUAGES CXX)
enable_testing()

file(GLOB TEST_SOURCES "*.cpp")

# static_init.cpp/static_init_factory.cpp reproduce a tracked object being
# constructed during C++ static/dynamic initialization (before main()). This
# exercises whole-program/process startup behavior and deliberately keeps a
# tracked object alive for the process's entire lifetime, so it is built as
# its own standalone binary rather than folded into the shared `tests`
# executable below, which asserts exact collector::get_live_object_count()
# baselines throughout and must not share collector/thread state with it.
list(REMOVE_ITEM TEST_SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/static_init.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/static_init_factory.cpp"
)

add_executable(tests ${TEST_SOURCES} types.h)
target_link_libraries(tests gtest gtest_main sgcl)
target_include_directories(tests PRIVATE ${CMAKE_SOURCE_DIR})
add_test(NAME tests COMMAND tests)

add_executable(static_init_test
static_init.cpp
static_init_factory.cpp
static_init.h
)
target_link_libraries(static_init_test gtest gtest_main sgcl)
target_include_directories(static_init_test PRIVATE ${CMAKE_SOURCE_DIR})
add_test(NAME static_init_test COMMAND static_init_test)
40 changes: 40 additions & 0 deletions tests/static_init.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//------------------------------------------------------------------------------
// SGCL: Smart Garbage Collection Library
// Copyright (c) 2022-2025 Sebastian Nibisz
// SPDX-License-Identifier: Apache-2.0
//------------------------------------------------------------------------------
#include "static_init.h"

// Constructed during C++ static/dynamic initialization, before main() (and
// therefore before any TEST body) runs. This is the very first construction
// of StaticInitOuter (and, nested inside its constructor, the very first
// construction of StaticInitInner) anywhere in the program. This reproduces
// the scenario that used to crash: PageInfo<T>::child_pointers was a plain
// eagerly-initialized `inline static` data member (see sgcl/detail/page_info.h)
// whose own dynamic initialization order relative to *this* variable's
// initializer, across translation units, was unspecified by the standard. If
// this initializer ran before the other translation unit's own static
// initialization got around to constructing `child_pointers`, the nested
// make_tracked<StaticInitInner>() call inside StaticInitOuter's constructor
// would hit `Pointer`'s assertion:
// Assertion failed: (offset / 8 < pointers.map->size()), function Pointer, ...
// The fix converts `child_pointers` into a lazily-constructed function-local
// static (see page_info.h), which the C++ standard guarantees is initialized
// on first use regardless of static-initialization order.
//
// This is built as its own standalone test binary (see tests/CMakeLists.txt)
// rather than folded into the shared `tests` executable: constructing a
// tracked object during static initialization -- before any SGCL machinery
// has necessarily run, and before the collector has otherwise been touched
// by the process -- is a whole-program concern, and this reproduction should
// not share collector/thread state with the rest of the (much larger) test
// suite, which asserts exact collector::get_live_object_count() baselines
// throughout.
tracked_ptr<StaticInitOuter> g_static_init_outer = make_static_init_outer();

TEST(StaticInit_Tests, GlobalConstructedDuringStaticInitialization) {
ASSERT_NE(g_static_init_outer, nullptr);
EXPECT_EQ(g_static_init_outer->value, 7);
ASSERT_NE(g_static_init_outer->ptr, nullptr);
EXPECT_EQ(g_static_init_outer->ptr->value, 42);
}
43 changes: 43 additions & 0 deletions tests/static_init.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//------------------------------------------------------------------------------
// SGCL: Smart Garbage Collection Library
// Copyright (c) 2022-2025 Sebastian Nibisz
// SPDX-License-Identifier: Apache-2.0
//------------------------------------------------------------------------------
#pragma once

#include "sgcl/sgcl.h"

#include <gtest/gtest.h>

using namespace sgcl;

// Types used to reproduce the "first construction of a tracked type happens
// during static/dynamic initialization of a global object, before main()"
// scenario. StaticInitOuter is itself constructed for the first time inside
// another translation unit's global initializer, and its own constructor, in
// turn, triggers the first-ever construction of the nested tracked type
// StaticInitInner via a tracked_ptr field. This mirrors the real-world crash:
// a module-level global variable's initializer calling a factory function
// defined in a different translation unit, whose constructor nested-
// constructs yet another tracked type, all before main() runs.
struct StaticInitInner {
int value = 42;
};

struct StaticInitOuter {
int value;
tracked_ptr<StaticInitInner> ptr;

StaticInitOuter() {
value = 7;
ptr = make_tracked<StaticInitInner>();
}
};

// Defined in a separate translation unit (static_init_factory.cpp) so that
// the first instantiation/use of PageInfo<StaticInitOuter> and
// PageInfo<StaticInitInner> happens in that other TU, while the call into it
// happens from this TU's own global variable initializer -- reproducing the
// cross translation-unit static-initialization-order dependency that the
// original bug exposed.
tracked_ptr<StaticInitOuter> make_static_init_outer();
14 changes: 14 additions & 0 deletions tests/static_init_factory.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//------------------------------------------------------------------------------
// SGCL: Smart Garbage Collection Library
// Copyright (c) 2022-2025 Sebastian Nibisz
// SPDX-License-Identifier: Apache-2.0
//------------------------------------------------------------------------------
#include "static_init.h"

// Defined in its own translation unit deliberately: this is where
// PageInfo<StaticInitOuter>/PageInfo<StaticInitInner> get instantiated, kept
// separate from the translation unit that calls this function from a global
// variable's own dynamic initializer (see static_init.cpp).
tracked_ptr<StaticInitOuter> make_static_init_outer() {
return make_tracked<StaticInitOuter>();
}
49 changes: 49 additions & 0 deletions tests/tracked_ptr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -406,3 +406,52 @@ TEST(TrackedPtr_Tests, Casts) {
auto pbar = const_pointer_cast<Bar>(cbar);
EXPECT_EQ(pbar->get_value(), 5);
}

// Regression test: tracked_ptrs on the C++ heap (external-heap mode) can be
// left in a "moved-from" state where _ptr() returns nullptr after a move
// construction where both source and destination are external-heap tracked_ptrs.
// All assignment operators must handle this case gracefully by re-allocating
// a fresh Pointer rather than calling _ptr()->store(...) unconditionally.
TEST(TrackedPtr_Tests, AssignmentToMovedFromExternalHeap) {
// Create an external-heap tracked_ptr with a value
auto src = std::make_unique<tracked_ptr<Bar>>(make_tracked<Foo>(42));
ASSERT_NE(*src, nullptr);
EXPECT_EQ((*src)->get_value(), 42);
EXPECT_TRUE(src->allocated_on_external_heap());

// Move-construct another external-heap tracked_ptr from src.
// Both being on the C++ heap (external-heap mode) triggers the move
// constructor's Pointer-steal path, leaving *src moved-from with null _ptr().
auto dest = std::make_unique<tracked_ptr<Bar>>(std::move(*src));
ASSERT_NE(*dest, nullptr);
EXPECT_EQ((*dest)->get_value(), 42);
EXPECT_TRUE(dest->allocated_on_external_heap());
// src is now moved-from; do not dereference it before reassigning.

// At this point src is moved-from: _ptr() returns nullptr.
// Verify that copy assignment recovers gracefully.
*src = *dest;
ASSERT_NE(*src, nullptr);
EXPECT_EQ((*src)->get_value(), 42);
EXPECT_TRUE(src->allocated_on_external_heap());

// Move-assign to the recovered src
auto dest2 = std::make_unique<tracked_ptr<Bar>>(make_tracked<Foo>(99));
*src = std::move(*dest2);
ASSERT_NE(*src, nullptr);
EXPECT_EQ((*src)->get_value(), 99);

// Nullptr assignment to a moved-from tracked_ptr
auto src2 = std::make_unique<tracked_ptr<int>>(make_tracked<int>(7));
auto dest3 = std::make_unique<tracked_ptr<int>>(std::move(*src2));
*src2 = nullptr;
EXPECT_EQ(*src2, nullptr);
EXPECT_TRUE(src2->allocated_on_external_heap());

// Unique_ptr assignment to a moved-from tracked_ptr
*src2 = make_tracked<int>(21);
ASSERT_NE(*src2, nullptr);
EXPECT_EQ(**src2, 21);

// Cleanup (destructors run automatically)
}