Search code examples
c++stringc++20c++-coroutine

Invalid free when yielding a temporary with a string field from a coroutine


I'm using C++20 coroutines to implement procedures for my application. Procedures run until they have a message to report to the application, at which point they suspend until the application resumes them. Each message contains some text stored in a std::string.

Here's my procedure definition (procedure.hpp):

#ifndef PROCEDURE_HPP
#define PROCEDURE_HPP

#include <coroutine>
#include <string>

struct Message {

    std::string value;

};

struct Procedure {

    struct promise_type {

        Message message;

        Procedure get_return_object() {
            return {std::coroutine_handle<promise_type>::from_promise(*this)};
        }

        std::suspend_always initial_suspend() noexcept {
            message = {"Coroutine started"};
            return {};
        }

        std::suspend_always final_suspend() noexcept {
            message = {"Coroutine finished"};
            return {};
        }

        std::suspend_always yield_value(Message message) {
            this->message = message;
            return {};
        }

        void unhandled_exception() {}

        void return_void() {}

    };

    std::coroutine_handle<promise_type> handle;
    
};

#endif

And here's a working example of how to use them (main.cpp):

#include <iostream>

#include "procedure.hpp"

Procedure run() {
    // NOTE: message is an lvalue
    Message message{"message lifetime spans co_yield"};
    co_yield message;
}

int main() {
    auto procedure = run();
    while (true) {
        std::cout << "[Message] " << procedure.handle.promise().message.value << std::endl;
        if (procedure.handle.done()) {
            return 0;
        } else {
            procedure.handle.resume();
        }
    }
}

When I compile and run this, I get the following output (which is expected):

$ g++-12 main.cpp -fcoroutines
$ ./a.out
[Message] Procedure started
[Message] message lifetime spans co_yield
[Message] Procedure finished

My compiler is GCC 12 on macOS:

g++-12 (Homebrew GCC 12.2.0) 12.2.0

However, if I change the definition of run so the message yielded is a temporary (in main.cpp):

Procedure run() {
    // NOTE: message is a temporary
    co_yield {"message is a temporary"};
}

Then the program crashes with an invalid free:

$ g++-12 main.cpp -fcoroutines
$ ./a.out
[Message] Procedure started
[Message] message is a temporary
a.out(15267,0x115cd8e00) malloc: *** error for object 0x7fe306c05b50: pointer being freed was not allocated
a.out(15267,0x115cd8e00) malloc: *** set a breakpoint in malloc_error_break to debug
zsh: abort      ./a.out

As best as I can tell via debugging, the invalid free occurs during the destruction of the message's value field. This is very unexpected to me since I have not done any memory management myself beyond std::string does, and I copy the message during the yield.

The crash doesn't occur if the value field has some other type (e.g. an integer). It also doesn't occur if I yield the value directly (instead of wrapping it in the Message type).

What's going on here? How can I resolve this so I can safely yield a temporary?


Solution

  • Thanks to those who pointed out that it could be a compiler issue! After updating to the latest version of GCC (13), I no longer have an issue.