Skip to content

Commit a8afc07

Browse files
fix(profiling): placement new for all containers post fork
1 parent b659bd0 commit a8afc07

3 files changed

Lines changed: 32 additions & 16 deletions

File tree

ddtrace/internal/datadog/profiling/stack/echion/echion/echion_sampler.h

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,23 @@ class EchionSampler
122122
// took its snapshot. Traversing a corrupted list to free nodes would crash.
123123
frame_cache_.postfork_child();
124124

125-
// Clear stale entries from parent process.
126-
// No lock needed: only one thread exists in child immediately after fork.
127-
task_link_map_.clear();
128-
weak_task_link_map_.clear();
129-
greenlet_info_map_.clear();
130-
greenlet_parent_map_.clear();
131-
greenlet_thread_map_.clear();
125+
// Use placement new for all containers the sampling thread writes to.
126+
// The sampling thread may have been mid-operation (insert, erase, rehash)
127+
// on any of these when fork() was called. Calling .clear() would traverse
128+
// potentially corrupted internal state and crash via _int_free_merge_chunk.
129+
// Placement new reconstructs an empty container without reading the old
130+
// data at all (intentional one-time memory leak per fork).
131+
new (&thread_info_map_) std::unordered_map<uintptr_t, ThreadInfo::Ptr>();
132+
new (&task_link_map_) std::unordered_map<PyObject*, PyObject*>();
133+
new (&weak_task_link_map_) std::unordered_map<PyObject*, PyObject*>();
134+
new (&greenlet_info_map_) std::unordered_map<GreenletInfo::ID, GreenletInfo::Ptr>();
135+
new (&greenlet_parent_map_) std::unordered_map<GreenletInfo::ID, GreenletInfo::ID>();
136+
new (&greenlet_thread_map_) std::unordered_map<uintptr_t, GreenletInfo::ID>();
137+
new (&previous_task_objects_) std::unordered_set<PyObject*>();
138+
139+
asyncio_frame_cache_key_.reset();
140+
uvloop_frame_cache_key_.reset();
141+
asyncio_task_count_ = 0;
132142

133143
// Reset the scratch set via placement new instead of clear() for the
134144
// same fork-safety reason as frame_cache_: the Sampling Thread may have

ddtrace/internal/datadog/profiling/stack/src/sampler.cpp

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -497,12 +497,14 @@ Sampler::postfork_child()
497497
auto current_thread_id = reinterpret_cast<uintptr_t>(pthread_self());
498498
#endif
499499

500-
// Extract the current ThreadInfo name if possible. All the other information needs to be updated.
501-
auto it = thread_info_map.find(current_thread_id);
502-
std::string name = it != thread_info_map.end() ? it->second->name : "MainThread";
503500

504-
// Clear all entries, we have extracted everything we care about.
505-
thread_info_map.clear();
501+
// thread_info_map was already reset via placement new in echion->postfork_child()
502+
// above, so it is empty here. We cannot safely read the old ThreadInfo name
503+
// because the sampling thread may have been mid-operation on the map or on a
504+
// ThreadInfo's FrameStack when fork() was called, leaving heap state corrupted.
505+
// Default to "MainThread"; the name is cosmetic and will be updated on the next
506+
// thread registration cycle.
507+
const std::string name = "MainThread";
506508

507509
// After fork, the current thread is the main (and only) thread,
508510
// so native_id == pid.

ddtrace/internal/datadog/profiling/stack/src/stack_renderer.cpp

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,12 @@ Datadog::StackRenderer::StackRenderer()
253253
void
254254
Datadog::StackRenderer::postfork_child()
255255
{
256-
// Clear the caches to avoid using stale interned string/function IDs
257-
// from the parent process's dictionary
258-
string_id_cache.clear();
259-
function_id_cache.clear();
256+
// Use placement new instead of .clear() because the sampling thread may
257+
// have been mid-rehash on either cache when fork() was called. Traversing
258+
// corrupted bucket/node pointers would crash; placement new abandons the
259+
// old data safely (intentional one-time memory leak per fork).
260+
new (&string_id_cache) std::unordered_map<StringTable::Key, string_id>();
261+
new (&function_id_cache)
262+
std::unordered_map<internal::PtrPair, function_id, internal::PtrPairHash, internal::PtrPairEq>();
263+
sample = nullptr;
260264
}

0 commit comments

Comments
 (0)