Repository navigation
SIGSEGV when exception thrown during coroutine promise aggregate initialization (Clang 21 / nlohmann::json) #2579
Description
Activity
A note on why Clang 21 triggers this
I'd like to add some analysis on why Clang 21 behaves this way, from a semantic perspective.
Conceptually, a coroutine frame should contain two distinct logical areas:
- Parameter storage — copies of the arguments passed from the caller, for use inside the coroutine body.
- Promise result slot — the storage where the co_return value ends up (i.e., std::optional value in Task::promise_type), for the caller to retrieve via the Task object.
These two are logically decoupled. Parameters are inputs; the result is an output. Before the coroutine body executes — specifically, before co_return — the result slot should be either uninitialized or default-constructed. It should not be touched by input arguments.
What Clang 21 does, however, is to treat promise_type as an aggregate and aggregate-initialize the entire promise from the coroutine arguments positionally — meaning the first argument goes straight into the first member, which happens to be value. This effectively forces:
promise.value = arg; // at coroutine startup, before the body runsThis conflates two independent lifecycle events — parameter copy and result initialization — into a single atomic operation (aggregate initialization). As a result, an implicit conversion (FailConvert → std::string) that should logically happen at co_return (when the result is actually produced) gets triggered at frame allocation time, before the exception handling machinery for the coroutine body is fully ready, leading to the unwind crash.
In short: Clang 21 is treating an input parameter as if it were an initializer for the output storage. This is not a logical error in Drogon's Task design — it's an implementation strategy in Clang that violates the intuitive separation between "arguments" and "return value storage."
The library-side fix (adding a user-declared default constructor to promise_type so it's no longer an aggregate) is a clean workaround.
@DreamDonghao Thanks for debugging can finding this. As drogon's maintainer I am keen to merge in the patch (since it has no harm to perf on GCC). My major complain is this is a clang bug and clang needs to fix it before other projects run into it. Have you reported the issue to LLVM?
Thank you for the review and feedback.
Regarding your question about reporting this to LLVM — I'm currently preparing a detailed issue and plan to submit it to the LLVM GitHub repository within the next couple of days. While researching existing discussions, I came across a related issue from 2024 (#84519), which deals with lambda coroutines and promise_type constructor argument passing. Although the exact trigger is different from our case (aggregate initialization + exception during conversion causing SIGSEGV), both issues revolve around the construction behavior of promise_type in coroutines, so they likely stem from the same underlying area.
I'll include the minimal standalone reproducer (without any dependency on Drogon or nlohmann/json) in the new issue to help LLVM developers pinpoint the problem quickly.
I'll post the link here once the issue is filed.
@marty1885
Regarding the LLVM report, I have filed an issue; the link is below:While preparing the standalone reproducer, I also clarified the actual trigger chain in Drogon:
Drogon's Task::promise_type is an aggregate type, and its first data member is std::optional value.
→ Under Clang 21's current non‑conforming behavior, coroutine parameters are used to aggregate‑initialize value.
→ Taking nlohmann::json as an example: its implicit conversion to T throws type_error::302 when the type mismatches, and this conversion happens during promise construction.
Drogon's final_awaiter::await_suspend() returns a std::coroutine_handle (symmetric‑transfer form).
→ When the exception thrown during promise construction is unwound, Clang's generated cleanup code accesses an invalid coroutine‑frame state, causing a SIGSEGV inside _Unwind_Resume.
→ The process dies silently, and the exception cannot be caught by any try‑catch.
This analysis is fully described in the LLVM issue. The library‑side fix (adding a user‑declared default constructor to make promise_type non‑aggregate) completely avoids this trigger chain and is consistent with standard behavior.- added a commit that references this issue
on Oct 4, 2026
Describe the bug
A
drogon::Task<T>coroutine that takes a parameter implicitly convertible toTcrashes with SIGSEGV at the call site — before a single line of the coroutine body executes — when that conversion throws. The process dies inside_Unwind_Resumewhile unwinding from the partially constructed coroutine frame;the exception is never catchable.
The most common real-world trigger is
nlohmann::json: its implicitoperator ValueType()callsget<ValueType>(), which throwsnlohmann::detail::type_error.302for mismatched types. We first hit this in production: a bot process terminated silently right after invoking a coroutine tool handler whoseargument is a JSON object (
Task<std::string>takingconst json&). Nothing was logged — the process simply vanished.Root cause
drogon::Task<T>::promise_type(lib/inc/drogon/utils/coroutine.h) is an aggregate — no user-declared constructors — and its first data member isstd::optional<T> value.value, so any implicit conversion toTruns during promise construction — before the body and beforeinitial_suspend._Unwind_Resume.Crash report excerpt from the minimal repro below:
To Reproduce
Environment: macOS 26.5.2 (arm64), Apple clang 21.0.0 (clang-2100.1.1.101), drogon 1.9.13 (Homebrew). C++20 suffices; no third-party library needed — the repro uses a 5-line stand-in type.
Per the standard C++20 rules, when no promise constructor takes the coroutine parameters the promise is default-constructed — that is what the reference variant should do (earlier compilers behave this way; only Clang 21 is available in this environment, so we did not re-verify older compilers ourselves).
Expected behavior
A conversion that throws during promise initialization must not crash the process with SIGSEGV. Either the exception should propagate cleanly to the caller after the coroutine frame is properly destroyed, or — as prescribed when no promise constructor takes the parameters — the promise should be
default-constructed and the coroutine body should run.
Desktop (please complete the following information):
/usr/bin/c++lib/inc/drogon/utils/coroutine.hhas no user-declared promise constructor)Additional context
Minimal library-side fix: add a user-declared default constructor to
promise_typeso it is no longer an aggregate, and Clang 21+ falls back to default construction (same semantics as pre-Clang-21 compilers):Verified locally: with these lines added to
Task<T>::promise_typeandTask<void>::promise_type(the latter's first member isstd::exception_ptr— same latent hazard), the repro printsreference param : okon Clang 21.AsyncTask::promise_typehas no data members and is not affected. The same helper typemakes for a simple regression test: invoking
byRef(v)from a test body is enough — with the bug the process segfaults before the next line; with the fix the coroutine behaves normally.In our project we worked around it by taking
const json*instead ofconst json&in all such coroutine signatures; we would rather revert that once a fixed version ships.There may also be a Clang-side issue in unwinding from a partially constructed coroutine frame (the SIGSEGV is inside
_Unwind_Resume), but the library-side fix above sidesteps the situation entirely. I'm happy to submit a PR with this change.