Well, keep in mind that an AI tried to port your code over to a Relacy
test unit... Examine all of its usage cases. Then try to fix the memory
leak.
______________
/*
Relacy test unit for shared_obj<T> / tshared_obj<T>.
The shared_obj/tshared_obj logic below is a port of the code as posted,
with only
the mechanical changes Relacy needs:
- std::atomic_ref on the refcount -> std::atomic<uint32_t> member
- alignas / CL_SIZE padding removed (irrelevant to the logic)
- ThreadMode fixed to "threaded" (template parameter dropped)
- unused converting constructors omitted
- assert() -> RL_ASSERT()
- payload is VAR_T so Relacy reports races / use-after-free on the
object contents
Optional switches (OFF = the code exactly as posted):
-DFIX_LEAK slowAssign releases the reference it already held
-DFIX_PUBLISH slowAssign's lock CAS uses acquire (pairs with the
writer's release store)
-DFIX_DECR decr() is release, with an acquire fence before the delete
-DFIX_EMPTY operator=(const shared_obj&) resets the pointer when
the source is empty
-DFIX_WAIT tshared_obj::operator=(shared_obj&&) tests the value
it waits on (lost wakeup otherwise)
Run: shared_obj_rl <test> [iterations] [random|bound N|full]
Tests: churn, churn_copy, leak, dangling
*/
#include <relacy/relacy_std.hpp>
#include <cstdio>
#include <cstdlib>
#include <cstdint>
#include <string>
#define mb_relaxed std::memory_order_relaxed
#define mb_acquire std::memory_order_acquire
#define mb_release std::memory_order_release
#define mb_acq_rel std::memory_order_acq_rel
template<typename T> struct shared_obj;
template<typename T> struct tshared_obj;
template<typename T, typename ... Args>
shared_obj<T> make_shared_obj( Args &&... args );
// ---------------------------------------------------------------------------- template<typename T>
struct shared_obj
{
struct ref_count
{
std::atomic<std::uint32_t> m_refCount;
ref_count( std::uint32_t n ) : m_refCount( n ) {}
void incr( std::memory_order order = mb_relaxed )
{
std::uint32_t before = m_refCount.fetch_add( 1, order, $ );
RL_ASSERT( before && before != (unsigned)-1 );
}
bool decr()
{
#ifdef FIX_DECR
if( m_refCount.fetch_sub( 1, mb_release, $ ) == 1 )
{
std::atomic_thread_fence( mb_acquire, $ );
return true;
}
return false;
#else
return m_refCount.fetch_sub( 1, mb_relaxed, $ ) == 1;
#endif
}
};
struct data : ref_count, T
{
template<typename ... Args>
data( Args &&... args ) :
ref_count( 1 ),
T( T( std::forward<Args>( args ) ... ) )
{
}
};
data *m_pData;
shared_obj() : m_pData( nullptr ) {}
shared_obj( data *p ) : m_pData( p ) {}
~shared_obj()
{
if( m_pData && m_pData->decr() )
delete m_pData;
}
// move assignment
shared_obj &operator =( shared_obj &&other )
{
if( m_pData == other.m_pData )
{
other.m_pData = nullptr;
if( m_pData )
m_pData->decr();
return *this;
}
if( m_pData && m_pData->decr() )
delete m_pData;
m_pData = other.m_pData;
other.m_pData = nullptr;
return *this;
}
// copy assignment
shared_obj &operator =( const shared_obj &other )
{
if( m_pData == other.m_pData )
return *this;
if( m_pData && m_pData->decr() )
{
delete m_pData;
m_pData = nullptr;
}
#ifdef FIX_EMPTY
m_pData = nullptr;
#endif
if( !other.m_pData )
return *this;
m_pData = other.m_pData;
m_pData->incr();
return *this;
}
// fast path: a relaxed load and a compare
shared_obj &operator =( const tshared_obj<T> &tso )
{
data *pDtaRef = tso.m_pAtmData.load( mb_relaxed, $ );
if( m_pData != pDtaRef )
slowAssign( tso, pDtaRef );
return *this;
}
void slowAssign( const tshared_obj<T> &tso, data *pDtaRef )
{
for( ; ; )
{
if( !pDtaRef )
{
data *pData = m_pData;
if( pData && pData->decr() )
delete pData;
m_pData = nullptr;
return;
}
if( pDtaRef == (data *)-1 )
{
tso.m_pAtmData.wait( pDtaRef,
std::memory_order_seq_cst, $ );
pDtaRef = tso.m_pAtmData.load( mb_relaxed, $ );
}
#ifdef FIX_PUBLISH
else if( tso.m_pAtmData.compare_exchange_strong( pDtaRef,
(data *)-1, mb_acquire, mb_relaxed, $ ) )
#else
else if( tso.m_pAtmData.compare_exchange_strong( pDtaRef,
(data *)-1, mb_relaxed, mb_relaxed, $ ) )
#endif
break;
if( m_pData == pDtaRef )
return;
}
#ifdef FIX_LEAK
data *pOld = m_pData;
#endif
m_pData = pDtaRef;
m_pData->incr( mb_acquire );
tso.m_pAtmData.store( pDtaRef, mb_relaxed, $ );
tso.m_pAtmData.notify_all( $ );
#ifdef FIX_LEAK
if( pOld && pOld->decr() )
delete pOld;
#endif
}
data *get_data() const { return m_pData; }
T *get() const { return static_cast<T *>( m_pData ); }
operator bool() const { return (bool)m_pData; }
T *operator ->() const { RL_ASSERT( m_pData ); return get(); }
T &operator *() const { RL_ASSERT( m_pData ); return *get(); }
std::uint32_t use_count() const
{
return m_pData ? m_pData->m_refCount.load( mb_relaxed, $ ) : 0;
}
};
template<typename T, typename ... Args>
shared_obj<T> make_shared_obj( Args &&... args )
{
using data = typename shared_obj<T>::data;
return shared_obj<T>( new data( std::forward<Args>( args ) ... ) );
}
// ---------------------------------------------------------------------------- template<typename T>
struct tshared_obj
{
using data = typename shared_obj<T>::data;
mutable std::atomic<data *> m_pAtmData;
tshared_obj() : m_pAtmData( nullptr ) {}
tshared_obj( shared_obj<T> &&so ) : m_pAtmData( so.m_pData )
{
so.m_pData = nullptr;
}
tshared_obj( const shared_obj<T> &so ) : m_pAtmData( so.m_pData )
{
if( so.m_pData )
so.m_pData->incr( mb_release );
}
~tshared_obj()
{
data *p = m_pAtmData.load( mb_relaxed, $ );
if( p && p->decr() )
delete p;
}
tshared_obj &operator =( const shared_obj<T> &so )
{
data *pDtaRef = m_pAtmData.load( mb_relaxed, $ );
for( ; ; )
if( pDtaRef == so.m_pData )
return *this;
else if( pDtaRef == (data *)-1 )
{
m_pAtmData.wait( pDtaRef, std::memory_order_seq_cst, $ );
pDtaRef = m_pAtmData.load( mb_relaxed, $ );
}
else if( m_pAtmData.compare_exchange_strong( pDtaRef, (data
*)-1, mb_relaxed, mb_relaxed, $ ) )
break;
if( pDtaRef && pDtaRef->decr() )
delete pDtaRef;
so.m_pData->incr(); // as posted: no null check
m_pAtmData.store( so.m_pData, mb_release, $ );
m_pAtmData.notify_all( $ );
return *this;
}
tshared_obj &operator =( shared_obj<T> &&so )
{
data *pDtaRef = m_pAtmData.load( mb_relaxed, $ );
for( ; ; )
if( pDtaRef == so.m_pData )
{
if( !so.m_pData )
return *this;
so.m_pData->decr();
so.m_pData = nullptr;
return *this;
}
#ifdef FIX_WAIT
else if( pDtaRef == (data *)-1 ) // test the value
we wait on
#else
else if( m_pAtmData.load( mb_relaxed, $ ) == (data *)-1 )
// as posted: tests a fresh load, waits on the old value
#endif
{
m_pAtmData.wait( pDtaRef, std::memory_order_seq_cst, $ );
pDtaRef = m_pAtmData.load( mb_relaxed, $ );
}
#ifdef BREAK_PUBLISH
else if( m_pAtmData.compare_exchange_strong( pDtaRef,
so.m_pData, mb_relaxed, mb_relaxed, $ ) ) // sanity: deliberately too weak #else
else if( m_pAtmData.compare_exchange_strong( pDtaRef,
so.m_pData, mb_release, mb_relaxed, $ ) )
#endif
break;
if( pDtaRef && pDtaRef->decr() )
delete pDtaRef;
so.m_pData = nullptr;
return *this;
}
void exchange( shared_obj<T> &so )
{
data *pDtaRef = m_pAtmData.load( mb_relaxed, $ );
for( ; ; )
if( pDtaRef == so.m_pData )
return;
else if( pDtaRef == (data *)-1 )
{
m_pAtmData.wait( pDtaRef, std::memory_order_seq_cst, $ );
pDtaRef = m_pAtmData.load( mb_relaxed, $ );
}
else if( m_pAtmData.compare_exchange_strong( pDtaRef,
so.m_pData, mb_acq_rel, mb_relaxed, $ ) )
break;
so.m_pData = pDtaRef;
}
void reset()
{
data *pDtaRef = m_pAtmData.load( mb_relaxed, $ );
for( ; ; )
if( !pDtaRef )
return;
else if( pDtaRef == (data *)-1 )
{
m_pAtmData.wait( pDtaRef, std::memory_order_seq_cst, $ );
pDtaRef = m_pAtmData.load( mb_relaxed, $ );
}
else if( m_pAtmData.compare_exchange_strong( pDtaRef,
nullptr, mb_relaxed, mb_relaxed, $ ) )
break;
if( pDtaRef->decr() )
delete pDtaRef;
}
};
// ----------------------------------------------------------------------------
// Payload: two words that must always be equal. VAR_T lets Relacy
report a data
// race if a reader sees the object before it is published, or a use-after-free.
struct cfg
{
VAR_T(unsigned) m_a;
VAR_T(unsigned) m_b;
explicit cfg( unsigned k )
{
VAR(m_a) = k;
VAR(m_b) = k;
}
cfg( cfg &&o )
{
VAR(m_a) = VAR(o.m_a);
VAR(m_b) = VAR(o.m_b);
}
};
typedef shared_obj<cfg> so_type;
typedef tshared_obj<cfg> tso_type;
#ifndef ITERS
#define ITERS 3
#endif
#ifndef WRITERS
#define WRITERS 1
#endif
#ifndef READERS
#define READERS 2
#endif
// ---- churn: readers poll a pointer that writers republish (rvalue
publish) ----
struct churn_test
: rl::test_suite<churn_test, WRITERS + READERS>
{
tso_type g_tso;
unsigned g_writers; // for test termination only
void before()
{
g_writers = WRITERS;
g_tso = make_shared_obj<cfg>( 1u );
}
void thread( unsigned tidx )
{
rl::backoff b;
if( tidx < READERS )
{
so_type local;
while( g_writers )
{
local = g_tso;
if( local )
{
unsigned a = VAR(local->m_a);
unsigned c = VAR(local->m_b);
RL_ASSERT( a == c );
}
b.yield( $ );
}
}
else
{
for( unsigned i = 0; i < ITERS; ++i )
{
g_tso = make_shared_obj<cfg>( i + 2 );
b.yield( $ );
}
--g_writers;
}
}
};
// ---- churn_copy: writers publish with the const& overload ----
struct churn_copy_test
: rl::test_suite<churn_copy_test, WRITERS + READERS>
{
tso_type g_tso;
unsigned g_writers;
void before()
{
g_writers = WRITERS;
g_tso = make_shared_obj<cfg>( 1u );
}
void thread( unsigned tidx )
{
rl::backoff b;
if( tidx < READERS )
{
so_type local;
while( g_writers )
{
local = g_tso;
if( local )
{
unsigned a = VAR(local->m_a);
unsigned c = VAR(local->m_b);
RL_ASSERT( a == c );
}
b.yield( $ );
}
}
else
{
for( unsigned i = 0; i < ITERS; ++i )
{
so_type fresh = make_shared_obj<cfg>( i + 2 );
g_tso = fresh;
b.yield( $ );
}
--g_writers;
}
}
};
// ---- leak: the smallest reproduction, one thread, no races needed ---- struct leak_test
: rl::test_suite<leak_test, 1>
{
void thread( unsigned )
{
tso_type tso( make_shared_obj<cfg>( 1u ) );
so_type local;
local = tso; // reader holds v1
tso = make_shared_obj<cfg>( 2u ); // writer publishes v2
local = tso; // reader moves to v2;
should release v1
}
};
// ---- dangling: assigning an empty shared_obj while another owner
exists ----
struct dangling_test
: rl::test_suite<dangling_test, 1>
{
void thread( unsigned )
{
so_type empty;
so_type c;
{
so_type a = make_shared_obj<cfg>( 7u );
c = a; // two owners
a = empty; // should leave a empty,
count 1
} // ~a: must not drop c's reference
// c is still a valid owner here
unsigned v = VAR(c->m_a);
RL_ASSERT( v == 7u );
}
};
// ---------------------------------------------------------------------------- template<typename Test>
static void run( unsigned iters, int argc, char **argv )
{
rl::test_params p;
p.iteration_count = iters;
p.execution_depth_limit = 20000;
if( argc > 3 )
{
std::string m = argv[3];
if( m == "full" )
p.search_type = rl::sched_full;
else if( m == "bound" )
{
p.search_type = rl::sched_bound;
p.context_bound = argc > 4 ? atoi( argv[4] ) : 2;
}
}
rl::simulate<Test>( p );
}
int main( int argc, char **argv )
{
std::string t = argc > 1 ? argv[1] : "churn";
unsigned iters = argc > 2 ? (unsigned)atoi( argv[2] ) : 100000;
if( t == "churn" ) run<churn_test>( iters, argc, argv );
else if( t == "churn_copy" ) run<churn_copy_test>( iters, argc, argv );
else if( t == "leak" ) run<leak_test>( iters, argc, argv );
else if( t == "dangling" ) run<dangling_test>( iters, argc, argv );
else { std::printf( "unknown test '%s'\n", t.c_str() ); return 2; }
return 0;
}
______________
Well, unfortunate:
______________
leak (1 thread) MEMORY LEAK, iteration 1
dangling (1 thread) ACCESS TO FREED MEMORY, iteration 1
churn (readers plus a writer) MEMORY LEAK, iteration 1
churn_copy MEMORY LEAK, iteration 1
______________
So you need to fix it? Download Relacy, get it running, and then test?
https://github.com/dvyukov/relacy
--- PyGate Linux v1.5.19
* Origin: Dragon's Lair, PyGate NNTP<>Fido Gate (3:633/10)