c++g++delete-operator

Is it legitimate to delete a const pointer (not pointer-to-const)?


Edit1: Now the code follows the "rule of five". Problem persists.
Edit2: Now passing only void* to printf's %p. Problem persists.
Edit3: tl;dr: It's a GCC bug.

Tracking down a segmentation fault in some code, I noticed that when a line like

    Lexer* const lexer_;

for a property was present, the code crashes; whereas without the const it works smoothly.

It const allowed in the above place?

For reference, below is a C-Reduce'd C++ code from a much bigger program that exposes the problem. Unfortunately, C-Reduce starts obfuscating identifiers to single letters at some point, so I stopped reducing and tried to get the code as neat as possible. To compile, I use g++ v11.3 on linux x86_64 with

> g++ main.cpp -o main.x -fsanitize=address -Werror=all -Werror=extra

Running, it prints

0x602000000010 = new Lexer
0x602000000030 = new Token
0x7ffca90b51f0 = new Expression
0x7ffca90b51f0 = start delete Expression
0x602000000010 = start delete Lexer
0x602000000030 = delete Token
0x602000000010 = done delete Lexer
=================================================================
==1232849==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000030 at pc 0x556fc889953d bp 0x7ffca90b5190 sp 0x7ffca90b5180
READ of size 8 at 0x602000000030 thread T0
    #0 0x556fc889953c in ExpressionParser::Expression::~Expression() (.../main.x+0x153c)
    ...
0x602000000030 is located 0 bytes inside of 8-byte region [0x602000000030,0x602000000038)
freed by thread T0 here:
    #0 0x7f5258f6f22f in operator delete(void*, unsigned long) .../libsanitizer/asan/asan_new_delete.cpp:172
    #1 0x556fc889965f in ExpressionParser::Lexer::~Lexer() (.../main.x+0x165f)
    ...
previously allocated by thread T0 here:
    #0 0x7f5258f6e1c7 in operator new(unsigned long) .../libsanitizer/asan/asan_new_delete.cpp:99
    #1 0x556fc8899588 in ExpressionParser::Lexer::tokenize() (.../main.x+0x1588)
    ...
SUMMARY: AddressSanitizer: heap-use-after-free (/home/john/own/C/mp-gmp/const-problem/main-2.x+0x153c) in ExpressionParser::Expression::~Expression()
...

With -D CONST= so that lexer_ is non-const, the code runs fine and prints:

0x602000000010 = new Lexer
0x602000000030 = new Token
0x7ffff44937e0 = new Expression
0x7ffff44937e0 = start delete Expression
0x602000000010 = start delete Lexer
0x602000000030 = delete Token
0x602000000010 = done delete Lexer
0x7ffff44937e0 = end delete Expression

What also works is to virtual ~Lexer();; which should not be needed as Lexer has no virtual methods?

Source

#include <cstdio>

#ifndef CONST
#define CONST const
#endif

class ExpressionParser
{
public:
    class Token;
    class Lexer;
    class Expression
    {
        friend ExpressionParser;
        Expression (Token *token) : expression_(token)
        {
            printf ("%p = new Expression\n", (void*) this);
        }
        Expression (const Expression&) = delete;
        Expression (Expression&&) = delete;
        void operator= (const Expression&) = delete;
        void operator= (Expression&&) = delete;
        ~Expression();
        Token *expression_;
    };
    static void eval();
};

using EP = ExpressionParser;

class EP::Lexer
{
public:
    Token *tokens_ = nullptr;
    Lexer()
    {
        printf ("%p = new Lexer\n", (void*) this);
    }
    Lexer (const Lexer&) = delete;
    Lexer (Lexer&&) = delete;
    void operator= (const Lexer&) = delete;
    void operator= (Lexer&&) = delete;
    ~Lexer();
    void tokenize();
};

class EP::Token
{
    friend ExpressionParser;
    Lexer * CONST lexer_;
    Token (Lexer *lexer) : lexer_(lexer)
    {
        printf ("%p = new Token\n", (void*) this);
    }
    Token (const Token&) = delete;
    Token (Token&&) = delete;
    void operator= (const Token&) = delete;
    void operator= (Token&&) = delete;
    ~Token()
    {
        printf ("%p = delete Token\n", (void*) this);
    }
};

void EP::eval()
{
    Lexer *lexer = new Lexer();
    lexer->tokenize();
    (void) Expression (lexer->tokens_);
}

EP::Expression::~Expression()
{
    printf ("%p = start delete Expression\n", (void*) this);
    delete expression_->lexer_;
    printf ("%p = end delete Expression\n", (void*) this);
}

void EP::Lexer::tokenize()
{
    tokens_= new Token (this);
}

EP::Lexer::~Lexer()
{
    printf ("%p = start delete Lexer\n", (void*) this);
    delete tokens_;
    printf ("%p = done delete Lexer\n", (void*) this);
}

int main (void)
{
    ExpressionParser::eval();
}

Solution

  • According to GCC C++ maintainer (and as apple apple already pointed out in a comment), this is a GCC bug already known since 2012 / v4.6, namely PR52339. It is already present in v4.0, but also reproducible with current master (future v14) or v11.3. Reason is that the expression in the final delete is evaluated more than once, in conflict with [expr.delete]:

    4The cast-expression in a delete-expression shall be evaluated exactly once.

    Test case:

    struct Lexer;
    
    struct Token
    {
        Lexer* const lexer_;
        Token (Lexer *l) : lexer_(l) {}
        ~Token() = default;
    
        Token() = delete;
        Token (const Token&) = delete;
        Token (Token&&) = delete;
        void operator= (const Token&) = delete;
        void operator= (Token&&) = delete;
    };
    
    struct Lexer
    {
        Token *token_;
        Lexer() = default;
        ~Lexer() { delete token_; }
    
        Lexer (const Lexer&) = delete;
        Lexer (Lexer&&) = delete;
        void operator= (const Lexer&) = delete;
        void operator= (Lexer&&) = delete;
    };
    
    int main()
    {
        Lexer *lexer = new Lexer();
        Token *token = new Token (lexer);
        lexer->token_ = token;
        delete token->lexer_;
        // delete lexer; // is OK
    }
    

    Command line

    $ g++ main-3.cpp -O2 && ./a.out
    

    but also triggered with -O0 or -m32.