drogonframework / drogonframework/drogon

SIGSEGV when exception thrown during coroutine promise aggregate initialization (Clang 21 / nlohmann::json)

Open
#2,579 4 comments 0 reactions 0 assignees View on GitHub
Dominant language
C++
Stars
14.3k
Forks
1.4k
Avg merge
1d 13h
Merged PRs (30d)
14

Description

**Describe the bug**

A `drogon::Task` coroutine that takes a parameter implicitly convertible to `T` crashes with **SIGSEGV at the call site** — before a single line of the coroutine body executes — when that conversion throws. The process dies inside `_Unwind_Resume` while unwinding from the partially constructed coroutine frame;
the exception is never catchable.

The most common real-world trigger is `nlohmann::json`: its implicit `operator ValueType()` calls `get()`, which throws `nlohmann::detail::type_error.302` for mismatched types. We first hit this in production: a bot process terminated silently right after invoking a coroutine tool handler whose
argument is a JSON object (`Task` taking `const json&`). Nothing was logged — the process simply vanished.

**Root cause**

1. `drogon::Task::promise_type` (`lib/inc/drogon/utils/coroutine.h`) is an **aggregate** — no user-declared constructors — and its **first data member is `std::optional value`**.
2. Clang 21 aggregate-initializes the promise from the coroutine arguments, positionally: the first argument initializes `value`, so any implicit conversion to `T` runs during promise construction — before the body and before `initial_suspend`.
3. If that conversion throws, unwinding from the partially constructed coroutine frame faults in `_Unwind_Resume`.

Crash report excerpt from the minimal repro below:

```text
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
0 libunwind.dylib _Unwind_Resume +228
1 repro_min byRef(FailConvert const&) +1060 <- coroutine ramp (promise init); body never entered
2 repro_min main +136
```

**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.

```cpp
// repro.cpp
#include
#include
#include
#include

// Minimal stand-in for nlohmann::json: implicitly convertible to std::string,
// and the conversion throws at runtime.
struct FailConvert
{
operator std::string() const
{
printf(" converting argument -> std::string (during promise init!)\n");
std::fflush(stdout);
throw std::runtime_error("conversion failed");
}
};

drogon::Task byRef(const FailConvert v)
{
co_return "ok"; // never reached on Clang 21
}

drogon::Task byPtr(const FailConvert *v)
{
co_return "ok";
}

int main(int argc, char **argv)
{
FailConvert v;
try
{
if (argc > 1)
{
auto t = byPtr(&v); // pointer: no conversion, promise default-constructed
printf("pointer param : ok\n");
}
else
{
auto t = byRef(v); // reference: Clang 21 aggregate-inits promise.value from it
printf("reference param : ok\n");
}
}
catch (const std::exception &e)
{
printf("caught: %s\n", e.what());
}
return 0;
}
```

```bash
clang++ -std=c++20 repro.cpp -I/opt/homebrew/include -o repro # adjust include path to your drogon

./repro
# converting argument -> std::string (during promise init!)
# Segmentation fault: 11 (exit code 139; the catch in main is never reached)

./repro ptr
# pointer param : ok
```

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):**
- OS: macOS 26.5.2 (arm64, Build 25F84)
- Compiler: Apple clang 21.0.0 (clang-2100.1.1.101), `/usr/bin/c++`
- Drogon Version: 1.9.13 (Homebrew); still present on master (`lib/inc/drogon/utils/coroutine.h` has no user-declared promise constructor)
- nlohmann/json: 3.12.0 (real-world trigger, used in the production project where this was found)

**Additional context**

Minimal library-side fix: add a user-declared default constructor to `promise_type` so it is no longer an aggregate, and Clang 21+ falls back to default construction (same semantics as pre-Clang-21 compilers):

```diff
--- a/lib/inc/drogon/utils/coroutine.h
+++ b/lib/inc/drogon/utils/coroutine.h
@@
struct promise_type
{
+ // A user-declared constructor makes promise_type a non-aggregate, so
+ // Clang 21+ will not aggregate-initialize `value` from the first
+ // coroutine argument (see #2579).
+ promise_type() = default;
+
Task get_return_object()
{
return Task{handle_type::from_promise(*this)};
}
```

Verified locally: with these lines added to `Task::promise_type` and `Task::promise_type` (the latter's first member is `std::exception_ptr` — same latent hazard), the repro prints `reference param : ok` on Clang 21. `AsyncTask::promise_type` has no data members and is not affected. The same helper type
makes 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 of `const 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.

Contributor guide

Open the contributing guide

Research direction

Start in lib/inc/drogon/utils/coroutine.h by reading Task::promise_type and Task::promise_type, then reproduce the failure with the supplied FailConvert example on Clang 21. Add a regression test around invoking byRef(v) and verify that the process no longer segfaults and the coroutine behaves normally.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.