Skip to content

Commit 71cb002

Browse files
TomTascheclaudeandiwand
authored
Attempt to fix Android app crashes with C++ library (#455)
* fix: prevent SIGSEGV crash in httplib HTTP server on Android This fixes a crash that was affecting ~950 users with the following root causes: 1. Chunked transfer encoding issues: Replace ContentProviderWithoutLength with buffered content using set_content(). The streaming content provider could crash when: - Client disconnects during transfer - Exceptions thrown during content generation - Server stopped while requests in-flight 2. Unhandled exceptions: Add try-catch blocks around request handling to gracefully handle exceptions instead of letting them propagate into httplib's internals where they caused SIGSEGV. 3. Race condition in stop(): Reorder operations to call m_server.stop() before clear() so that in-flight requests complete before resources are invalidated. The crash was reported as SIGSEGV in httplib::Server::write_response_core on various Android devices and versions (Android 10-16, Samsung, Motorola devices). * fix: prevent crash in httplib process_request during shutdown Additional fixes for Android crashes in httplib HTTP server: 1. Add destructor to Impl class: Ensures the server is properly stopped before the Impl object is destroyed. This prevents worker threads from accessing freed memory (use-after-free crash). 2. Add atomic stopping flag: Handlers check this flag and return 503 Service Unavailable during shutdown, preventing new work from starting while resources are being freed. 3. Set up httplib exception handler: Catches any internal httplib exceptions and returns HTTP 500 instead of crashing. 4. Change lambda captures from [&] to [this]: More explicit about what's captured, making the code's intent clearer. 5. Delete copy constructor/assignment: Prevents unsafe copying since lambdas capture 'this' pointer. The second crash was occurring in std::__tree::__assign_multi during httplib::Server::process_request, caused by accessing freed memory when the Impl object was destroyed while worker threads were still running. * fix: ensure thread pool is joined before resources are destroyed Root cause: C++ member destruction order was causing use-after-free. The m_content map was being destroyed BEFORE m_server, but m_server's destructor is what joins the thread pool threads. This meant worker threads could still be accessing m_content after it was destroyed. Crashes were occurring in httplib's internal map operations (__construct_node, __assign_multi) during process_request because threads were accessing freed memory. Fix: 1. Changed m_server to unique_ptr<httplib::Server> to enable explicit destruction timing control. 2. In destructor, explicitly destroy server via m_server.reset() BEFORE other members are destroyed. This ensures thread pool threads are fully joined first. 3. In stop(), also destroy the server to ensure threads are joined before clearing content. 4. Reordered member declarations: m_server is now declared LAST so if we miss any explicit destruction, the natural destruction order will still destroy it first (reverse of declaration order). This is a critical fix for the SIGSEGV crashes in httplib's internal map operations during request processing on Android. * format --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Andreas Stefl <stefl.andreas@gmail.com>
1 parent 53593cc commit 71cb002

1 file changed

Lines changed: 140 additions & 42 deletions

File tree

src/odr/http_server.cpp

Lines changed: 140 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,46 +8,111 @@
88

99
#include <httplib/httplib.h>
1010

11+
#include <atomic>
12+
#include <memory>
13+
#include <sstream>
14+
1115
namespace odr {
1216

1317
class HttpServer::Impl {
1418
public:
1519
Impl(Config config, std::shared_ptr<Logger> logger)
16-
: m_config{std::move(config)}, m_logger{std::move(logger)} {
17-
m_server.Get("/",
18-
[](const httplib::Request & /*req*/, httplib::Response &res) {
19-
res.set_content("Hello World!", "text/plain");
20-
});
21-
22-
m_server.Get("/file/" + std::string(prefix_pattern),
23-
[&](const httplib::Request &req, httplib::Response &res) {
24-
serve_file(req, res);
25-
});
26-
m_server.Get("/file/" + std::string(prefix_pattern) + "/(.*)",
27-
[&](const httplib::Request &req, httplib::Response &res) {
28-
serve_file(req, res);
29-
});
20+
: m_config{std::move(config)}, m_logger{std::move(logger)},
21+
m_server{std::make_unique<httplib::Server>()} {
22+
// Set up exception handler to catch any internal httplib exceptions.
23+
// This prevents crashes when exceptions occur during request processing.
24+
m_server->set_exception_handler([this](const httplib::Request & /*req*/,
25+
httplib::Response &res,
26+
std::exception_ptr ep) {
27+
try {
28+
if (ep) {
29+
std::rethrow_exception(ep);
30+
}
31+
} catch (const std::exception &e) {
32+
ODR_ERROR(*m_logger, "Exception in HTTP handler: " << e.what());
33+
} catch (...) {
34+
ODR_ERROR(*m_logger, "Unknown exception in HTTP handler");
35+
}
36+
res.status = 500;
37+
res.set_content("Internal Server Error", "text/plain");
38+
});
39+
40+
m_server->Get("/",
41+
[](const httplib::Request & /*req*/, httplib::Response &res) {
42+
res.set_content("Hello World!", "text/plain");
43+
});
44+
45+
m_server->Get("/file/" + std::string(prefix_pattern),
46+
[this](const httplib::Request &req, httplib::Response &res) {
47+
if (m_stopping.load(std::memory_order_acquire)) {
48+
res.status = 503;
49+
res.set_content("Service Unavailable", "text/plain");
50+
return;
51+
}
52+
serve_file(req, res);
53+
});
54+
m_server->Get("/file/" + std::string(prefix_pattern) + "/(.*)",
55+
[this](const httplib::Request &req, httplib::Response &res) {
56+
if (m_stopping.load(std::memory_order_acquire)) {
57+
res.status = 503;
58+
res.set_content("Service Unavailable", "text/plain");
59+
return;
60+
}
61+
serve_file(req, res);
62+
});
63+
}
64+
65+
~Impl() {
66+
// Ensure the server is properly stopped before destruction.
67+
// This prevents crashes from worker threads accessing freed memory.
68+
//
69+
// IMPORTANT: We must fully stop and destroy the server BEFORE
70+
// m_content is destroyed. httplib's thread pool threads may still
71+
// be running after stop() returns - they only fully stop when the
72+
// Server destructor joins them. By explicitly destroying the server
73+
// here (via unique_ptr::reset), we ensure all threads are joined
74+
// before any other members are destroyed.
75+
m_stopping.store(true, std::memory_order_release);
76+
if (m_server) {
77+
m_server->stop();
78+
m_server.reset(); // Destroy server, join all thread pool threads
79+
}
80+
// Now safe to let other members destruct - no threads are running
3081
}
3182

83+
// Prevent copying - the lambdas capture 'this' so copying would be unsafe
84+
Impl(const Impl &) = delete;
85+
Impl &operator=(const Impl &) = delete;
86+
3287
const Config &config() const { return m_config; }
3388

3489
const std::shared_ptr<Logger> &logger() const { return m_logger; }
3590

3691
void serve_file(const httplib::Request &req, httplib::Response &res) {
37-
std::string id = req.matches[1].str();
38-
std::string path = req.matches.size() > 1 ? req.matches[2].str() : "";
39-
40-
std::unique_lock lock{m_mutex};
41-
auto it = m_content.find(id);
42-
if (it == m_content.end()) {
43-
ODR_ERROR(*m_logger, "Content not found for ID: " << id);
44-
res.status = 404;
45-
return;
92+
try {
93+
std::string id = req.matches[1].str();
94+
std::string path = req.matches.size() > 1 ? req.matches[2].str() : "";
95+
96+
std::unique_lock lock{m_mutex};
97+
auto it = m_content.find(id);
98+
if (it == m_content.end()) {
99+
ODR_ERROR(*m_logger, "Content not found for ID: " << id);
100+
res.status = 404;
101+
return;
102+
}
103+
auto [_, service] = it->second;
104+
lock.unlock();
105+
106+
serve_file(res, service, path);
107+
} catch (const std::exception &e) {
108+
ODR_ERROR(*m_logger, "Error handling request: " << e.what());
109+
res.status = 500;
110+
res.set_content("Internal Server Error", "text/plain");
111+
} catch (...) {
112+
ODR_ERROR(*m_logger, "Unknown error handling request");
113+
res.status = 500;
114+
res.set_content("Internal Server Error", "text/plain");
46115
}
47-
auto [_, service] = it->second;
48-
lock.unlock();
49-
50-
serve_file(res, service, path);
51116
}
52117

53118
void serve_file(httplib::Response &res, const HtmlService &service,
@@ -60,17 +125,27 @@ class HttpServer::Impl {
60125

61126
ODR_VERBOSE(*m_logger, "Serving file: " << path);
62127

63-
httplib::ContentProviderWithoutLength content_provider =
64-
[service = service, path = path](const std::size_t offset,
65-
httplib::DataSink &sink) -> bool {
66-
if (offset != 0) {
67-
throw std::runtime_error("Invalid offset: " + std::to_string(offset) +
68-
". Must be 0.");
69-
}
70-
service.write(path, sink.os);
71-
return false;
72-
};
73-
res.set_content_provider(service.mimetype(path), content_provider);
128+
// Buffer content to avoid streaming issues on Android.
129+
// Using ContentProviderWithoutLength (chunked transfer encoding) can cause
130+
// SIGSEGV crashes in httplib::Server::write_response_core when:
131+
// 1. The client disconnects during transfer
132+
// 2. Exceptions are thrown during content generation
133+
// 3. The server is stopped while requests are in-flight
134+
// By buffering content first, we can handle errors gracefully and use
135+
// Content-Length based responses which are more reliable.
136+
try {
137+
std::ostringstream buffer;
138+
service.write(path, buffer);
139+
res.set_content(buffer.str(), service.mimetype(path));
140+
} catch (const std::exception &e) {
141+
ODR_ERROR(*m_logger, "Error serving file " << path << ": " << e.what());
142+
res.status = 500;
143+
res.set_content("Internal Server Error", "text/plain");
144+
} catch (...) {
145+
ODR_ERROR(*m_logger, "Unknown error serving file: " << path);
146+
res.status = 500;
147+
res.set_content("Internal Server Error", "text/plain");
148+
}
74149
}
75150

76151
void connect_service(HtmlService service, const std::string &prefix) {
@@ -88,7 +163,7 @@ class HttpServer::Impl {
88163
void listen(const std::string &host, const std::uint32_t port) {
89164
ODR_VERBOSE(*m_logger, "Listening on " << host << ":" << port);
90165

91-
m_server.listen(host, static_cast<int>(port));
166+
m_server->listen(host, static_cast<int>(port));
92167
}
93168

94169
void clear() {
@@ -107,17 +182,32 @@ class HttpServer::Impl {
107182
void stop() {
108183
ODR_VERBOSE(*m_logger, "Stopping HTTP server...");
109184

110-
clear();
185+
// Set stopping flag first to reject new requests immediately.
186+
// This prevents new requests from starting while we're shutting down.
187+
m_stopping.store(true, std::memory_order_release);
188+
189+
if (m_server) {
190+
// Stop the server to prevent new connections.
191+
// Note: httplib::Server::stop() signals shutdown but thread pool
192+
// threads may still be running. They only fully stop when the
193+
// Server is destroyed. For explicit stop() calls (not destructor),
194+
// we destroy the server here to ensure threads are joined.
195+
m_server->stop();
196+
m_server.reset(); // Destroy server, join all thread pool threads
197+
}
111198

112-
m_server.stop();
199+
// Clear content after server is fully destroyed to avoid use-after-free.
200+
clear();
113201
}
114202

115203
private:
116204
Config m_config;
117205

118206
std::shared_ptr<Logger> m_logger;
119207

120-
httplib::Server m_server;
208+
// Flag to indicate server is shutting down - checked by handlers
209+
// to reject new requests during shutdown.
210+
std::atomic<bool> m_stopping{false};
121211

122212
struct Content {
123213
std::string id;
@@ -126,6 +216,14 @@ class HttpServer::Impl {
126216

127217
std::mutex m_mutex;
128218
std::unordered_map<std::string, Content> m_content;
219+
220+
// IMPORTANT: m_server is declared LAST and as unique_ptr so we can
221+
// explicitly destroy it in the destructor BEFORE other members.
222+
// httplib's Server destructor joins thread pool threads, so we must
223+
// ensure threads are fully stopped before m_content is destroyed.
224+
// Using unique_ptr allows us to call reset() to trigger destruction
225+
// at a controlled point in the destructor.
226+
std::unique_ptr<httplib::Server> m_server;
129227
};
130228

131229
HttpServer::HttpServer(const Config &config, std::shared_ptr<Logger> logger)

0 commit comments

Comments
 (0)