Skip to content

Crash when calling std::exit while server running or client requests in flight #2097

@Y--

Description

@Y--

Hi!

Thanks for the great library!

I think there is a race condition when the process is terminated explicitly (for example with std::exit) while a server is processing queries and/or a client is executing some.

I crafted a small reproduction below (note that in the actual code we're not "sleeping" in the atexit but have the same issue nonetheless):

#include "httplib.h"

struct ServerWrapper {
  httplib::Server server;
  std::unique_ptr<std::thread> s_thread;
};

static std::unique_ptr<ServerWrapper> s_wrapper;

void mainServer() {
  s_wrapper->server.Get("/hi", [](const httplib::Request &req, httplib::Response &res) {
    res.set_content("Quack", "text/plain");
  });

  s_wrapper->server.listen("localhost", 8080);
}

void exitThread() {
  std::cout << "Will exit soon..." << std::endl;
  std::this_thread::sleep_for(std::chrono::milliseconds(500));
  std::cout << "Calling std::exit now." << std::endl;
  std::exit(0);
}

void runClient() {
  httplib::Client cli("localhost", 8080);
  while (true) {
    auto res = cli.Get("/hi");
    std::this_thread::sleep_for(std::chrono::milliseconds(1));
  }
}

void atex() {
  std::cout << "Entering std::atexit callback, will wait a bit and stop server" << std::endl;
  // Wait a bit before stopping server to simulate delayed exit
  std::this_thread::sleep_for(std::chrono::milliseconds(500));
  std::cout << "[std::atexit] Waited, will stop server now." << std::endl;
  s_wrapper->server.stop();
  std::cout << "[std::atexit] Server stopped." << std::endl;
  s_wrapper->s_thread->join();
  std::cout << "[std::atexit] Server thread completed." << std::endl;
}

int main() {
  std::atexit(atex);
  s_wrapper = std::make_unique<ServerWrapper>();
  s_wrapper->s_thread = std::make_unique<std::thread>(&mainServer);

  std::thread cliThread(&runClient);

  std::thread(&exitThread).join();
  return 0;
}

Can be compiled with:

$ g++ -g -O0 -std=c++20 test.cc

Then execution looks like:

$  ./a.out
Will exit soon...
Calling std::exit now.
Entering std::atexit callback, will wait a bit and stop server
[1]    37193 segmentation fault  ./a.out

(MacOS 15.3.1; g++: Apple clang version 16.0.0 (clang-1600.0.26.6))

The issue (if I understand it correctly) is that the static destructors are executed before the server is actually stopped. So it is possible for example that a server requests arrives after the static methods object is destroyed, but still attempts to use it.

An easy fix would be to remove all static initializations (for example removing both this one and that one "fixes" the example above, but of course we could craft a different one to make it crash with another static resource).
But that is probably not acceptable for performance reasons.

The C++ standard guarantees that functions registered with std::atexit are called in the reverse order of their registration, except that a function is called after any previously registered functions that had already been called at the time it was registered.
As a consumer of the library, this makes it quite cumbersome to make sure that we define the atexit handler after each static initialization (to make sure it is called first, and has a chance to destroy the server before other static destructors)

Another way would be to hoist all static definition at the top of the file, so that caller of std::atexit would be guaranteed to have their handler run first.

Of course there are more involved way to fix the problem.

I am happy to contribute to a fix when we settle on the right approach :-)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions