Skip to content

Commit d415cd3

Browse files
fredroyhugtalbot
andauthored
[Simulation.Core] WorkerThread: fix memory leak/resource retention (#6125)
* illustrate the delete leak bug * fix the leak - task->~Task() is now called before operator delete. Because ~Task() is virtual (Task.h:56), this dispatches to the actual derived destructor ~CallableTask, ~SharedPtrHoldingTask, etc. - Captured sizeof(*task) into a local taskSize before calling the destructor, then passed it to operator delete. Reading sizeof(*task) after destruction would technically be reading a destroyed object's vtable, which is UB. With the local capture we sidestep that. (sizeof is a compile-time expression on the static type of *task, so the value is the same but using a local makes the intent explicit and avoids reasoning-trap.) --------- Co-authored-by: Hugo <hugo.talbot@sofa-framework.org>
1 parent 834d641 commit d415cd3

3 files changed

Lines changed: 137 additions & 4 deletions

File tree

Sofa/framework/Simulation/Core/src/sofa/simulation/task/WorkerThread.cpp

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,14 @@ void WorkerThread::runTask(Task *task)
155155
{
156156
if (task->run() & Task::MemoryAlloc::Dynamic)
157157
{
158-
// pooled memory: call destructor and free
159-
//task->~Task();
160-
task->operator delete(task, sizeof(*task));
161-
//delete task;
158+
// Run the (virtual) destructor before freeing the storage.
159+
// Skipping this leaks any non-trivially-destructible task
160+
// members: std::function in CallableTask (the lambda overload
161+
// of addTask) leaks its internal __func<>; std::shared_ptr,
162+
// std::vector, etc., never release their owned resources.
163+
const std::size_t taskSize = sizeof(*task);
164+
task->~Task();
165+
task->operator delete(task, taskSize);
162166
}
163167
}
164168

Sofa/framework/Simulation/Core/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ set(SOURCE_FILES
99
ParallelForEach_test.cpp
1010
PushTaskRace_test.cpp
1111
RequiredPlugin_test.cpp
12+
TaskDestructorLeak_test.cpp
1213
SceneCheckRegistry_test.cpp
1314
Simulation_test.cpp
1415
ParallelSparseMatrixProduct_test.cpp
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/******************************************************************************
2+
* SOFA, Simulation Open-Framework Architecture *
3+
* (c) 2006 INRIA, USTL, UJF, CNRS, MGH *
4+
* *
5+
* This program is free software; you can redistribute it and/or modify it *
6+
* under the terms of the GNU Lesser General Public License as published by *
7+
* the Free Software Foundation; either version 2.1 of the License, or (at *
8+
* your option) any later version. *
9+
* *
10+
* This program is distributed in the hope that it will be useful, but WITHOUT *
11+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
12+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License *
13+
* for more details. *
14+
* *
15+
* You should have received a copy of the GNU Lesser General Public License *
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
17+
*******************************************************************************
18+
* Authors: The SOFA Team and external contributors (see Authors.txt) *
19+
* *
20+
* Contact information: contact@sofa-framework.org *
21+
******************************************************************************/
22+
#include <gtest/gtest.h>
23+
#include <sofa/simulation/task/CpuTask.h>
24+
#include <sofa/simulation/task/CpuTaskStatus.h>
25+
#include <sofa/simulation/task/DefaultTaskScheduler.h>
26+
#include <sofa/simulation/task/MainTaskSchedulerFactory.h>
27+
28+
#include <atomic>
29+
#include <memory>
30+
31+
namespace sofa
32+
{
33+
34+
// Reproduction for a leak in WorkerThread::runTask. After running a task that
35+
// returns MemoryAlloc::Dynamic, the framework calls Task::operator delete to
36+
// release the memory but does NOT call the task's destructor. From
37+
// WorkerThread::runTask:
38+
//
39+
// if (task->run() & Task::MemoryAlloc::Dynamic) {
40+
// // pooled memory: call destructor and free
41+
// //task->~Task(); <-- commented out
42+
// task->operator delete(task, sizeof(*task));
43+
// }
44+
//
45+
// As a result, any task with non-trivially-destructible members (std::function,
46+
// std::shared_ptr, std::vector, ...) leaks those members' resources every time
47+
// it runs. The std::function-based addTask(Status&, lambda) overload always
48+
// triggers this because CallableTask wraps the lambda in a std::function.
49+
//
50+
// We demonstrate the leak with a custom CpuTask holding a std::shared_ptr.
51+
// If the destructor ran, every task copy of the shared_ptr would release a
52+
// reference and the original's use_count would return to 1 after the burst.
53+
// On the buggy code, use_count stays at 1 + (number of dispatched tasks).
54+
55+
namespace
56+
{
57+
58+
class SharedPtrHoldingTask : public simulation::CpuTask
59+
{
60+
public:
61+
SharedPtrHoldingTask(simulation::CpuTask::Status* status,
62+
std::shared_ptr<int> resource,
63+
std::atomic<int>* counter)
64+
: simulation::CpuTask(status)
65+
, m_resource(std::move(resource))
66+
, m_counter(counter)
67+
{}
68+
69+
sofa::simulation::Task::MemoryAlloc run() final
70+
{
71+
// Touch the resource so the compiler can't elide the member.
72+
if (m_resource)
73+
{
74+
m_counter->fetch_add(*m_resource, std::memory_order_relaxed);
75+
}
76+
return sofa::simulation::Task::MemoryAlloc::Dynamic;
77+
}
78+
79+
private:
80+
std::shared_ptr<int> m_resource;
81+
std::atomic<int>* m_counter;
82+
};
83+
84+
} // namespace
85+
86+
// Dispatch many tasks, each holding a copy of the same shared_ptr. After
87+
// workUntilDone, all task instances must have been destroyed; the only
88+
// remaining holder is the test's local `resource`, so use_count must be 1.
89+
//
90+
// On the buggy code, the destructors are skipped and use_count equals
91+
// 1 + kNumTasks.
92+
TEST(TaskDestructorLeak, SharedPtrTasksReleaseTheirReference)
93+
{
94+
constexpr int kNumTasks = 64;
95+
96+
auto* scheduler = simulation::MainTaskSchedulerFactory::createInRegistry(
97+
simulation::DefaultTaskScheduler::name());
98+
ASSERT_NE(scheduler, nullptr);
99+
100+
scheduler->init(0);
101+
if (scheduler->getThreadCount() < 2)
102+
{
103+
GTEST_SKIP() << "scheduler has fewer than 2 threads; skipping";
104+
}
105+
106+
auto resource = std::make_shared<int>(1);
107+
std::atomic<int> counter { 0 };
108+
109+
{
110+
simulation::CpuTaskStatus status;
111+
for (int i = 0; i < kNumTasks; ++i)
112+
{
113+
// Each task holds its own shared_ptr copy.
114+
scheduler->addTask(new SharedPtrHoldingTask(&status, resource, &counter));
115+
}
116+
scheduler->workUntilDone(&status);
117+
}
118+
119+
EXPECT_EQ(counter.load(std::memory_order_relaxed), kNumTasks);
120+
// After all tasks have been disposed of, only `resource` should hold the
121+
// shared_ptr. If the framework skipped the destructor, every task's copy
122+
// is leaked and use_count == 1 + kNumTasks.
123+
EXPECT_EQ(resource.use_count(), 1);
124+
125+
scheduler->stop();
126+
}
127+
128+
} // namespace sofa

0 commit comments

Comments
 (0)