• for Bonita...

    From Chris M. Thomasson@3:633/10 to All on Fri Oct 9 11:26:28 2026
    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)
  • From Bonita Montero@3:633/10 to All on Fri Oct 9 20:42:39 2026
    I won't read that.
    You don't understand my idea, although it's simple.

    --- PyGate Linux v1.5.19
    * Origin: Dragon's Lair, PyGate NNTP<>Fido Gate (3:633/10)
  • From Chris M. Thomasson@3:633/10 to All on Sat Oct 10 14:10:57 2026
    On 10/9/2026 11:42 AM, Bonita Montero wrote:
    I won't read that.

    Why not?

    You don't understand my idea, although it's simple.

    Huh?


    --- PyGate Linux v1.5.19
    * Origin: Dragon's Lair, PyGate NNTP<>Fido Gate (3:633/10)
  • From Chris M. Thomasson@3:633/10 to All on Mon Oct 12 21:17:22 2026
    On 10/9/2026 11:42 AM, Bonita Montero wrote:
    I won't read that.
    You don't understand my idea, although it's simple.

    You should port your thing over to Relacy, get it to pass all the
    various tests, then talk about it. Until then, okay, good luck.

    --- PyGate Linux v1.5.19
    * Origin: Dragon's Lair, PyGate NNTP<>Fido Gate (3:633/10)