https://gcc.gnu.org/bugzilla/show_bug.cgi?id=126194

            Bug ID: 126194
           Summary: [15/17 Regression?] LTO/DSE removes temporary
                    initialization observed by placement delete cleanup
           Product: gcc
           Version: 17.0
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: middle-end
          Assignee: unassigned at gcc dot gnu.org
          Reporter: dimitar.yordanov at sap dot com
  Target Milestone: ---

With -O2 -flto, GCC appears to eliminate initialization of a temporary object
bound to the placement operator new argument. If the constructed object's
constructor throws, the compiler-generated new-expression cleanup calls the
matching placement operator delete. That delete function reads the temporary's
fields again. In the bad code, the cleanup path passes the address of
uninitialized stack storage as the placement argument, so placement delete
reads
garbage and writes through it, causing SIGSEGV.


-----
cat > handle.hpp <<'EOF'
#pragma once

using Size = decltype(sizeof(0));

struct A
{
    void (*const f_)(void**) noexcept;
    void** const p_;
    Size const a_;

    A(void** pp, void (*f)(void**) noexcept, Size a) noexcept
        : f_(f), p_(pp), a_(a) {}

    void reset() const noexcept
    {
        if (*p_) {
            (*f_)(p_);
        }
    }

    A& operator=(const A&) = delete;

    void* create(Size size, const void* caller, struct R& r) const;
    void destroy(void* p, const void* caller, struct R& r) const noexcept;
};

struct R
{
    virtual ~R() = default;
    virtual void* allocate(Size size, const void* caller) = 0;
    virtual void deallocate(void* p, const void* caller) noexcept = 0;
};

R& get_r() noexcept;
void touch(const void* p) noexcept;

struct B
{
    void* q = nullptr;

    static void cb(void** pp) noexcept
    {
        B* self = reinterpret_cast<B*>(pp);
        if (self->q) {
            extern void release_q(void*);
            release_q(self->q);
        }
        self->q = nullptr;
    }

    ~B() noexcept
    {
        touch(this);
        if (q) {
            extern void release_q(void*);
            release_q(q);
        }
    }

    B() noexcept = default;
    B(const B&) = delete;
    B& operator=(const B&) = delete;
    B(B&& other) noexcept : q(other.q)
    {
        other.q = nullptr;
    }
    B& operator=(B&& other) noexcept
    {
        if (this != &other) {
            if (q) {
                extern void release_q(void*);
                release_q(q);
            }
            q = other.q;
            other.q = nullptr;
        }
        return *this;
    }

    operator A() noexcept
    {
        return A(&q, &cb, alignof(void*));
    }
};

void* operator new(Size size, A const& a, R& r) noexcept(false);
void  operator delete(void* p, A const& a, R& r) noexcept;
EOF

cat > handle.cpp <<'EOF'
#include "handle.hpp"

extern "C" void* malloc(Size size) noexcept;
extern "C" void free(void* p) noexcept;

namespace {
class M final : public R
{
public:
    void* allocate(Size size, const void* /*caller*/) override
    {
        return malloc(size);
    }

    void deallocate(void* p, const void* /*caller*/) noexcept override
    {
        free(p);
    }
};

M g_r;
}

R& get_r() noexcept
{
    return g_r;
}

void touch(const void* p) noexcept
{
    static const volatile void* sink = nullptr;
    sink = p;
}

void release_q(void* p)
{
    free(p);
}

int g_should_throw = 0;

void* A::create(Size size, const void* caller, R& r) const
{
    reset();
    if (size < a_) size = a_;
    void* p = r.allocate(size, caller);
    if (!p) throw 2;
    *p_ = p;
    return p;
}

__attribute__((noinline))
void A::destroy(void* p, const void* caller, R& r) const noexcept
{
    *p_ = nullptr;
    r.deallocate(p, caller);
}

void* operator new(Size size, A const& a, R& r) noexcept(false)
{
    return a.create(size, __builtin_return_address(0), r);
}

__attribute__((noinline))
void operator delete(void* p, A const& a, R& r) noexcept
{
    a.destroy(p, __builtin_return_address(0), r);
}
EOF

cat > main.cpp <<'EOF'
#include "handle.hpp"

extern int g_should_throw;

struct T
{
    int a;
    __attribute__((noinline)) T() : a(0)
    {
        if (g_should_throw) {
            throw 1;
        }
    }
};

__attribute__((noinline))
int make_widget(bool go, void** out_ptr)
{
    B b;
    R& r = get_r();
    if (go) {
        T* t = new(b, r) T();
        (void)t;
    }
    *out_ptr = b.q;
    b.q = nullptr;
    return 42;
}

extern "C" __attribute__((visibility("default")))
int run_repro(int argc)
{
    g_should_throw = (argc > 0);
    void* p = nullptr;
    try {
        (void)make_widget(argc > 0, &p);
        return 1;
    } catch (...) {
        return 0;
    }
}
EOF

cat > driver.cpp <<'EOF'
extern "C" int run_repro(int argc);

int main(int argc, char**)
{
    return run_repro(argc);
}
EOF

g++ -std=c++20 -O2 -flto -fPIC -fvisibility=hidden -c handle.cpp -o handle.o
g++ -std=c++20 -O2 -flto -fPIC -fvisibility=hidden -c main.cpp -o main.o
g++ -std=c++20 -O2 -flto -fPIC -fvisibility=hidden -shared handle.o main.o -o
librepro.so
g++ -std=c++20 -O2 driver.cpp -L. -lrepro -o repro

./repro

The temporary bound to the placement operator new reference parameter lives
until
completion of the full-expression, including if evaluation ends by throwing.
The placement delete call is part of new-expression cleanup, so it occurs while
the placement argument temporary is still alive. Eliminating the temporary's
field initialization is therefore not valid if placement delete can reread
those
fields.

Reply via email to