4

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();
}
emacs drives me nuts
  • 2,785
  • 13
  • 23
  • **Comments have been [moved to chat](https://chat.stackoverflow.com/rooms/253334/discussion-on-question-by-emacs-drives-me-nuts-is-it-legitimate-to-delete-a-cons); please do not continue the discussion here.** Before posting a comment below this one, please review the [purposes of comments](/help/privileges/comment). Comments that do not request clarification or suggest improvements usually belong as an [answer](/help/how-to-answer), on [meta], or in [chat]. Comments continuing discussion may be removed. – Samuel Liew Apr 26 '23 at 12:50

2 Answers2

1

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.

emacs drives me nuts
  • 2,785
  • 13
  • 23
0

Seeing as this was reopened, I'll post my comments as an answer:

Using the simplified version from godbolt: https://godbolt.org/z/n1qzrWsdq

When delete token->lexer; is done, the destructor ~Lexer() deletes its token, which in this case is the token from the delete statement. At this point, you are deleting a pointer which is still in use, which would be undefined behaviour.

From the generated assembly, in a non-optimized build:

        mov     rax, QWORD PTR [rbp-16]
        mov     rax, QWORD PTR [rax]
        mov     rdi, rax
        call    Lexer::~Lexer() [complete object destructor]
        mov     rax, QWORD PTR [rbp-16]
        mov     rax, QWORD PTR [rax]
        mov     esi, 8
        mov     rdi, rax
        call    operator delete(void*, unsigned long)

(where [rbp-16] is the address for token), you can see that after ~Lexer() is called, token is reloaded.

ChrisMM
  • 8,448
  • 13
  • 29
  • 48
  • From my understanding, as `delete` is not a function call but an operator, the deletion is not sequenced against dereferencing `token`. This sounds a bit crazy because in order to delete anything, `token` must have been dereferenced already. But nothing keeps the compiler from dereferencing it as often as it likes. What's also strange is that it only happens with `Lexer* const lexer;` not without the const, and g++ and clangdo similar stuff. Can you see why the compiler needs to dereference `token` again after `~Lexer()` ? – emacs drives me nuts Apr 26 '23 at 15:35
  • There isn't a _need_ to, but like you said, the compiler can do it as often as it wants. In debug mode (or no optimizations), it's very common for compilers to generate code like that. When you turn on optimizations, it's already detected UB, so it can do whatever it wants. If you look at the code compiled with `-O1` (or higher), it only generates a single `new`, but two `deletes`. If you remove the `const`, g++ compiles the code to `mov eax, 0; ret` because it can / there's no side-effects. It's strange that it doesn't with `const` though. – ChrisMM Apr 26 '23 at 16:20
  • It's a GCC bug, see the other answer. – emacs drives me nuts May 04 '23 at 11:17