Skip to content

Thread safety analysis: scoped lockable members and temporaries #98004

Open
@leonid-s-usov

Description

@leonid-s-usov

Hi!

I am trying to create a movable scoped lockable. It kind of works, but there are some cases that generate false positives. It seems that there are two interconnected problems:

  1. The lack of support for scoped_lockable members
    • This has a side effect with lambda captures which are lambda class members under the hood
  2. Some issue with temporaries, presumably around copy elision via temporary binding
    • Might also affect lambda capture initialization

Here is a compiler explorer example with all of the observed issues: https://godbolt.org/z/vfcb78jqc

In case the above compiler explorer snippet is not available, please see the full source code under the cut:
#include <memory>
#include <iostream>
#include <cassert>

struct __attribute__((lockable)) Mutex {
    void lock() __attribute__((exclusive_lock_function())) {
        assert(!locked);
        locked = true;
    }
    void unlock()  __attribute__((unlock_function())) {
        assert(locked);
        locked = false;
    }
    bool locked = false;
};

struct  __attribute__((scoped_lockable)) MutexLocker {
    MutexLocker(Mutex* pmutex) __attribute__((exclusive_lock_function(*pmutex)))
        : pmutex(pmutex)
    {
        assert(pmutex);
        pmutex->lock();
        std::cout << "constructor lock" << std::endl;
    }

    ~MutexLocker() __attribute__((unlock_function(*pmutex))) {
        if (pmutex) {
            std::cout << "destructor unlock" << std::endl;
            pmutex->unlock();
        }
    }

    // we mark this as exclusive lock because
    // although we're taking the mutex locked already
    // the static analysis doesn't know that other.pmutex
    // and this->pmutex are referencing the same object
    // With the current accounting we should pretend that
    // we took our new lock, and the other will unlock
    // in the destructor - not in the runtime, but static analysis-wise.
    MutexLocker(MutexLocker &&other) __attribute__((exclusive_lock_function(*pmutex))) {
        pmutex = other.pmutex;
        other.pmutex = nullptr;

        std::cout << "constructor move" << std::endl;
    }

    MutexLocker(const MutexLocker& other) = delete;
    MutexLocker& operator=(const MutexLocker& other) = delete;
    MutexLocker& operator=(MutexLocker&& other) = delete;

    Mutex *pmutex;
};


int main() {
    Mutex m;

    std::cout << "=== no warnings in most cases" << std::endl;
    {
        MutexLocker l(&m);

        MutexLocker l2 = std::move(l);

        auto val = [&]() {
            auto l3 = std::move(l2);
            std::cout << "in lambda 1" << std::endl;
            return 10;
        }();
    }

    std::cout << std::endl << "=== warnings 1.1: struct members" << std::endl;
    {
        MutexLocker l(&m);

        struct Wrapper {
            MutexLocker wrapped_locker;
        };

        auto wl = Wrapper { std::move(l) };

        assert(m.locked);
    }

    std::cout << std::endl << "=== warnings 1.2: lambda capture initialization" << std::endl;
    {
        MutexLocker l(&m);

        auto val = [l2 = MutexLocker(std::move(l))]() mutable {
            auto l3 = std::move(l2);
            std::cout << "in lambda 2" << std::endl;
            return 20;
        }();

        assert(!m.locked);
    }

    std::cout << std::endl << "=== warnings 2: binding a temporary" << std::endl;
    {
        MutexLocker locker(&m);

        auto && locker2 = [&]() -> MutexLocker {
            return std::move(locker);
        }();
                
        assert(m.locked);
    }

    assert(!m.locked);
    return 0;
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions