Describe the bug
PubSubService<MessageType> stores topic names in a default-hashed std::unordered_map<std::string, ...>. Applications that accept client-chosen topics can expose collision-driven overhead: on builds with predictable string hashing and bucket selection, many distinct colliding names can make lookups approach O(n) and cumulative subscription work approach O(n²).
Drogon's bundled WebSocket chat example passes the client's room_name query value directly to subscribe. Creating a new topic involves lookup and insertion under an exclusive lock, so prolonged operations can delay other users of the same PubSubService instance.
The request parameter map uses SafeStringMap, but the parameter's value becomes a key in the separate default-hashed topic map. The parameter map's protection does not extend to topic-name lookups.
To Reproduce
- Build and run the bundled
websocket_server example. It listens on 127.0.0.1:8848 and exposes /chat.
- Connect to
ws://127.0.0.1:8848/chat?room_name=room_a with a WebSocket client. The handler uses room_a as a topic name. This ordinary name illustrates the input path; it is not a collision sample.
- For a collision workload, prepare distinct topic names that concentrate in one bucket under the target build's string hash and actual table growth. Open one connection per name, URL-encoding the query value as needed, and keep the connections open. Collision selection must use the decoded names.
- Compare subscription-processing time with an equal number of ordinary names of the same lengths, using the same connection schedule and a fresh server instance for each run. Repeat at increasing topic counts to measure collision-specific overhead separately from connection setup and ordinary topic allocation.
Each connection in this example holds one subscription. Closing it unsubscribes, and the last unsubscribe removes the topic. Repeated connections to the same room do not grow topicMap_; distinct live rooms are required.
Expected behavior
Applications using client-selected topics should be protected against hash flooding through a keyed hash designed for adversarial input and appropriate live-topic/subscription limits. Chosen topic names should not produce disproportionate processing costs that delay unrelated topic operations on the same service instance.
Additional context
Relevant source locations:
examples/websocket_server/WebSocketServer.cc:59–60: reads room_name and subscribes; 46–50: unsubscribes on disconnect.
lib/inc/drogon/PubSubService.h:275–276: declares the default-hashed topic map; 285–300: looks up and creates topics; 198–218: removes an empty topic after unsubscribe.
lib/src/HttpRequestImpl.h:198–210: request parameters use SafeStringMap; getParameter returns the parameter value.
Relevant statements, with unrelated code omitted:
std::unordered_map<std::string, std::shared_ptr<Topic<MessageType>>>
topicMap_;
// subscribeToTopic: insert an unknown topic under the exclusive lock.
topicMap_[topicName] = std::move(topicPtr);
// examples/websocket_server/WebSocketServer.cc:59-60
s.chatRoomName_ = req->getParameter("room_name");
s.id_ = chatRooms_.subscribe(s.chatRoomName_, /* callback */);
PubSubService imposes no topic-count limit itself. The example inherits a configured global connection limit of 100,000, with no per-IP limit by default (lib/src/HttpConnectionLimit.h:50,53). Its loopback listener requires deployment changes for external access.
The topic table is shared within one service instance. Subscription checks an unknown topic under an exclusive lock before inserting (PubSubService.h:292–300). Publication looks up the topic under a shared lock and releases that lock before invoking callbacks (148–159).
Suggested fix: use a hash-flooding-resistant keyed string hash for topicMap_, and add or document live-topic limits per service and subscription limits at the application layer, where connection/user identity is available.
Describe the bug
PubSubService<MessageType>stores topic names in a default-hashedstd::unordered_map<std::string, ...>. Applications that accept client-chosen topics can expose collision-driven overhead: on builds with predictable string hashing and bucket selection, many distinct colliding names can make lookups approach O(n) and cumulative subscription work approach O(n²).Drogon's bundled WebSocket chat example passes the client's
room_namequery value directly tosubscribe. Creating a new topic involves lookup and insertion under an exclusive lock, so prolonged operations can delay other users of the samePubSubServiceinstance.The request parameter map uses
SafeStringMap, but the parameter's value becomes a key in the separate default-hashed topic map. The parameter map's protection does not extend to topic-name lookups.To Reproduce
websocket_serverexample. It listens on127.0.0.1:8848and exposes/chat.ws://127.0.0.1:8848/chat?room_name=room_awith a WebSocket client. The handler usesroom_aas a topic name. This ordinary name illustrates the input path; it is not a collision sample.Each connection in this example holds one subscription. Closing it unsubscribes, and the last unsubscribe removes the topic. Repeated connections to the same room do not grow
topicMap_; distinct live rooms are required.Expected behavior
Applications using client-selected topics should be protected against hash flooding through a keyed hash designed for adversarial input and appropriate live-topic/subscription limits. Chosen topic names should not produce disproportionate processing costs that delay unrelated topic operations on the same service instance.
Additional context
Relevant source locations:
examples/websocket_server/WebSocketServer.cc:59–60: readsroom_nameand subscribes;46–50: unsubscribes on disconnect.lib/inc/drogon/PubSubService.h:275–276: declares the default-hashed topic map;285–300: looks up and creates topics;198–218: removes an empty topic after unsubscribe.lib/src/HttpRequestImpl.h:198–210: request parameters useSafeStringMap;getParameterreturns the parameter value.Relevant statements, with unrelated code omitted:
std::unordered_map<std::string, std::shared_ptr<Topic<MessageType>>> topicMap_; // subscribeToTopic: insert an unknown topic under the exclusive lock. topicMap_[topicName] = std::move(topicPtr); // examples/websocket_server/WebSocketServer.cc:59-60 s.chatRoomName_ = req->getParameter("room_name"); s.id_ = chatRooms_.subscribe(s.chatRoomName_, /* callback */);PubSubServiceimposes no topic-count limit itself. The example inherits a configured global connection limit of 100,000, with no per-IP limit by default (lib/src/HttpConnectionLimit.h:50,53). Its loopback listener requires deployment changes for external access.The topic table is shared within one service instance. Subscription checks an unknown topic under an exclusive lock before inserting (
PubSubService.h:292–300). Publication looks up the topic under a shared lock and releases that lock before invoking callbacks (148–159).Suggested fix: use a hash-flooding-resistant keyed string hash for
topicMap_, and add or document live-topic limits per service and subscription limits at the application layer, where connection/user identity is available.