Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## X.X.X
- ! Minor breaking change ! Added SDK internal limits enforcement (max key length, value size, segmentation values, breadcrumb count, stack-trace lines per thread, stack-trace line length) across events, views, crashes, and user properties. Limits use config defaults overridable by server SDK Behavior Settings, and can be set via `setMaxKeyLength`, `setMaxValueSize`, `setMaxSegmentationValues`, `setMaxBreadcrumbCount`, `setMaxStackTraceLinesPerThread`, `setMaxStackTraceLineLength` during init.

## 26.1.1
- Updated CMake minimum required version to use the range format with upper the end of `3.31`.
- Hardened mutex handling against exceptions.
Expand Down
4 changes: 3 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ if(COUNTLY_BUILD_TESTS)
${CMAKE_CURRENT_SOURCE_DIR}/tests/config.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/immediate_stop.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/mutex_exception_safety.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/sbs.cpp)
${CMAKE_CURRENT_SOURCE_DIR}/tests/sbs.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/internal_limits.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/internal_limits_integration.cpp)

target_compile_options(countly-tests PRIVATE -g)
target_compile_definitions(countly-tests PRIVATE COUNTLY_BUILD_TESTS)
Expand Down
7 changes: 7 additions & 0 deletions include/countly.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,13 @@ class Countly : public cly::CountlyDelegates {
*/
void setEventsToRQThreshold(int value);

void setMaxKeyLength(unsigned int value);
void setMaxValueSize(unsigned int value);
void setMaxSegmentationValues(unsigned int value);
void setMaxBreadcrumbCount(unsigned int value);
void setMaxStackTraceLinesPerThread(unsigned int value);
void setMaxStackTraceLineLength(unsigned int value);

void flushEvents(std::chrono::seconds timeout = std::chrono::seconds(30));

bool beginSession();
Expand Down
1 change: 1 addition & 0 deletions include/countly/configuration_module.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class ConfigurationModule : public ConfigurationProvider {
bool isCrashReportingEnabled() const override;

unsigned int getRequestQueueSizeLimit() const override;
SDKLimits getLimits() const override;
unsigned int getEventQueueSizeLimit();
unsigned int getSessionUpdateInterval();

Expand Down
10 changes: 10 additions & 0 deletions include/countly/configuration_provider.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
#define CONFIGURATION_PROVIDER_HPP_
namespace cly {

struct SDKLimits {
unsigned int maxKeyLength;
unsigned int maxValueSize;
unsigned int maxSegmentationValues;
unsigned int maxBreadcrumbCount;
unsigned int maxStackTraceLinesPerThread;
unsigned int maxStackTraceLineLength;
};

class ConfigurationProvider {
public:
virtual ~ConfigurationProvider() = default;
Expand All @@ -11,6 +20,7 @@ class ConfigurationProvider {
virtual bool isCrashReportingEnabled() const = 0;
virtual bool isViewTrackingEnabled() const = 0;
virtual unsigned int getRequestQueueSizeLimit() const = 0;
virtual SDKLimits getLimits() const = 0;
};
} // namespace cly
#endif
7 changes: 7 additions & 0 deletions include/countly/constants.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
#define COUNTLY_POST_THRESHOLD 2000
#define COUNTLY_KEEPALIVE_INTERVAL 3000
#define COUNTLY_MAX_EVENTS_DEFAULT 200
#define COUNTLY_MAX_KEY_LENGTH_DEFAULT 128
#define COUNTLY_MAX_VALUE_SIZE_DEFAULT 256
#define COUNTLY_MAX_SEGMENTATION_VALUES_DEFAULT 100
#define COUNTLY_MAX_BREADCRUMB_COUNT_DEFAULT 100
#define COUNTLY_MAX_STACK_TRACE_LINES_PER_THREAD_DEFAULT 30
#define COUNTLY_MAX_STACK_TRACE_LINE_LENGTH_DEFAULT 200
#define COUNTLY_MAX_VALUE_SIZE_PICTURE 4096

namespace cly {
struct HTTPResponse {
Expand Down
9 changes: 9 additions & 0 deletions include/countly/countly_configuration.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ struct CountlyConfiguration {
*/
unsigned int breadcrumbsThreshold = 100;

/**
* SDK internal limits (defaults; overridable at runtime by SDK Behavior Settings).
*/
unsigned int maxKeyLength = COUNTLY_MAX_KEY_LENGTH_DEFAULT;
unsigned int maxValueSize = COUNTLY_MAX_VALUE_SIZE_DEFAULT;
unsigned int maxSegmentationValues = COUNTLY_MAX_SEGMENTATION_VALUES_DEFAULT;
unsigned int maxStackTraceLinesPerThread = COUNTLY_MAX_STACK_TRACE_LINES_PER_THREAD_DEFAULT;
unsigned int maxStackTraceLineLength = COUNTLY_MAX_STACK_TRACE_LINE_LENGTH_DEFAULT;

/**
* Set to send all requests made to the Countly server using HTTP POST.
*/
Expand Down
3 changes: 3 additions & 0 deletions include/countly/event.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ class Event {

void clearSegmentation();

// Enforce SDK internal limits on this event's key and developer segmentation.
void applyLimits(unsigned int maxKeyLength, unsigned int maxValueSize, unsigned int maxSegmentationValues);

private:
nlohmann::json object;
bool timer_running;
Expand Down
66 changes: 66 additions & 0 deletions include/countly/internal_limits.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#ifndef COUNTLY_INTERNAL_LIMITS_HPP_
#define COUNTLY_INTERNAL_LIMITS_HPP_

#include "countly/configuration_provider.hpp" // for cly::SDKLimits

#include <map>
#include <sstream>
#include <string>

namespace cly {
namespace limits {

// Truncate a string to at most maxLen bytes without splitting a multibyte
// UTF-8 sequence at the cut point.
inline std::string truncateString(const std::string &s, unsigned int maxLen) {
if (s.size() <= static_cast<size_t>(maxLen)) {
return s;
}
size_t cut = maxLen;
// UTF-8 continuation bytes are 0b10xxxxxx. If the cut lands on one, we are
// inside a character; back off until it sits on a code-point boundary.
while (cut > 0 && (static_cast<unsigned char>(s[cut]) & 0xC0) == 0x80) {
--cut;
}
return s.substr(0, cut);
}

// Truncate keys/values of a developer-supplied segmentation map and cap the
// entry count. std::map iterates in sorted key order, so the retained subset
// is deterministic.
inline std::map<std::string, std::string> applySegmentationLimits(const std::map<std::string, std::string> &in, const SDKLimits &lim) {
std::map<std::string, std::string> out;
unsigned int kept = 0;
for (const auto &kv : in) {
if (kept >= lim.maxSegmentationValues) {
break;
}
out[truncateString(kv.first, lim.maxKeyLength)] = truncateString(kv.second, lim.maxValueSize);
++kept;
}
return out;
}

// Cap a stack-trace string to maxLines lines and each line to maxLineLength
// bytes. Lines are split on '\n' and rejoined with '\n'.
inline std::string truncateStackTrace(const std::string &trace, unsigned int maxLines, unsigned int maxLineLength) {
std::istringstream stream(trace);
std::string line;
std::string result;
unsigned int count = 0;
bool first = true;
while (count < maxLines && std::getline(stream, line)) {
if (!first) {
result += "\n";
}
result += truncateString(line, maxLineLength);
first = false;
++count;
}
return result;
}

} // namespace limits
} // namespace cly

#endif
36 changes: 36 additions & 0 deletions src/configuration_module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ class ConfigurationModule::ConfigurationModuleImpl {
std::atomic<unsigned int> sessionUpdateInterval{0};
std::atomic<unsigned int> serverConfigUpdateInterval{4};

std::atomic<unsigned int> maxKeyLength{COUNTLY_MAX_KEY_LENGTH_DEFAULT};
std::atomic<unsigned int> maxValueSize{COUNTLY_MAX_VALUE_SIZE_DEFAULT};
std::atomic<unsigned int> maxSegmentationValues{COUNTLY_MAX_SEGMENTATION_VALUES_DEFAULT};
std::atomic<unsigned int> maxBreadcrumbCount{COUNTLY_MAX_BREADCRUMB_COUNT_DEFAULT};
std::atomic<unsigned int> maxStackTraceLinesPerThread{COUNTLY_MAX_STACK_TRACE_LINES_PER_THREAD_DEFAULT};
std::atomic<unsigned int> maxStackTraceLineLength{COUNTLY_MAX_STACK_TRACE_LINE_LENGTH_DEFAULT};

mutable std::mutex sbsMutex;
std::thread configFetchThread;

Expand Down Expand Up @@ -184,6 +191,13 @@ class ConfigurationModule::ConfigurationModuleImpl {
void _initializeConfigParameters() {
requestQueueSizeLimit.store(_configuration->requestQueueThreshold, std::memory_order_release);
sessionUpdateInterval.store(_configuration->sessionDuration, std::memory_order_release);

maxKeyLength.store(_configuration->maxKeyLength, std::memory_order_release);
maxValueSize.store(_configuration->maxValueSize, std::memory_order_release);
maxSegmentationValues.store(_configuration->maxSegmentationValues, std::memory_order_release);
maxBreadcrumbCount.store(_configuration->breadcrumbsThreshold, std::memory_order_release);
maxStackTraceLinesPerThread.store(_configuration->maxStackTraceLinesPerThread, std::memory_order_release);
maxStackTraceLineLength.store(_configuration->maxStackTraceLineLength, std::memory_order_release);
}

nlohmann::json _processSDKBehaviorSettings(const std::string &settings) {
Expand Down Expand Up @@ -267,6 +281,17 @@ class ConfigurationModule::ConfigurationModuleImpl {
populateListFilter(eventSegmentationFilter, KEY_EVENT_SEGMENTATION_BLACKLIST, KEY_EVENT_SEGMENTATION_WHITELIST, parseMap);
}

auto readLimit = [this](const char *key, unsigned int def) {
unsigned int v = getUInt(key, def);
return v < 1 ? def : v;
};
maxKeyLength.store(readLimit(KEY_LIMIT_KEY_LENGTH, _configuration->maxKeyLength), std::memory_order_release);
maxValueSize.store(readLimit(KEY_LIMIT_VALUE_SIZE, _configuration->maxValueSize), std::memory_order_release);
maxSegmentationValues.store(readLimit(KEY_LIMIT_SEG_VALUES, _configuration->maxSegmentationValues), std::memory_order_release);
maxBreadcrumbCount.store(readLimit(KEY_LIMIT_BREADCRUMB, _configuration->breadcrumbsThreshold), std::memory_order_release);
maxStackTraceLinesPerThread.store(readLimit(KEY_LIMIT_TRACE_LINE, _configuration->maxStackTraceLinesPerThread), std::memory_order_release);
maxStackTraceLineLength.store(readLimit(KEY_LIMIT_TRACE_LENGTH, _configuration->maxStackTraceLineLength), std::memory_order_release);

nlohmann::json changedSettings;
if (locationTrackingCurrent != locationTrackingEnabledVal) {
changedSettings[KEY_LOCATION_TRACKING] = locationTrackingCurrent;
Expand Down Expand Up @@ -475,6 +500,17 @@ bool ConfigurationModule::isCrashReportingEnabled() const { return impl->crashRe

unsigned int ConfigurationModule::getRequestQueueSizeLimit() const { return impl->requestQueueSizeLimit.load(std::memory_order_acquire); }

SDKLimits ConfigurationModule::getLimits() const {
SDKLimits lim;
lim.maxKeyLength = impl->maxKeyLength.load(std::memory_order_acquire);
lim.maxValueSize = impl->maxValueSize.load(std::memory_order_acquire);
lim.maxSegmentationValues = impl->maxSegmentationValues.load(std::memory_order_acquire);
lim.maxBreadcrumbCount = impl->maxBreadcrumbCount.load(std::memory_order_acquire);
lim.maxStackTraceLinesPerThread = impl->maxStackTraceLinesPerThread.load(std::memory_order_acquire);
lim.maxStackTraceLineLength = impl->maxStackTraceLineLength.load(std::memory_order_acquire);
return lim;
}

unsigned int ConfigurationModule::getEventQueueSizeLimit() {
// this is because we permit EQ size to change after initialization
unsigned int value = impl->eventQueueThreshold.load(std::memory_order_acquire);
Expand Down
95 changes: 86 additions & 9 deletions src/countly.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include "countly/internal_limits.hpp"
#include "countly/storage_module_db.hpp"
#include "countly/storage_module_memory.hpp"
#include <chrono>
Expand Down Expand Up @@ -163,6 +164,60 @@ void Countly::enableImmediateRequestOnStop() {
configuration->immediateRequestOnStop = true;
}

void Countly::setMaxKeyLength(unsigned int value) {
if (is_sdk_initialized) {
log(LogLevel::WARNING, "[Countly] setMaxKeyLength, This method can't be called after SDK initialization. Returning.");
return;
}
std::lock_guard<std::mutex> lk(*mutex);
configuration->maxKeyLength = value;
}

void Countly::setMaxValueSize(unsigned int value) {
if (is_sdk_initialized) {
log(LogLevel::WARNING, "[Countly] setMaxValueSize, This method can't be called after SDK initialization. Returning.");
return;
}
std::lock_guard<std::mutex> lk(*mutex);
configuration->maxValueSize = value;
}

void Countly::setMaxSegmentationValues(unsigned int value) {
if (is_sdk_initialized) {
log(LogLevel::WARNING, "[Countly] setMaxSegmentationValues, This method can't be called after SDK initialization. Returning.");
return;
}
std::lock_guard<std::mutex> lk(*mutex);
configuration->maxSegmentationValues = value;
}

void Countly::setMaxBreadcrumbCount(unsigned int value) {
if (is_sdk_initialized) {
log(LogLevel::WARNING, "[Countly] setMaxBreadcrumbCount, This method can't be called after SDK initialization. Returning.");
return;
}
std::lock_guard<std::mutex> lk(*mutex);
configuration->breadcrumbsThreshold = value;
}

void Countly::setMaxStackTraceLinesPerThread(unsigned int value) {
if (is_sdk_initialized) {
log(LogLevel::WARNING, "[Countly] setMaxStackTraceLinesPerThread, This method can't be called after SDK initialization. Returning.");
return;
}
std::lock_guard<std::mutex> lk(*mutex);
configuration->maxStackTraceLinesPerThread = value;
}

void Countly::setMaxStackTraceLineLength(unsigned int value) {
if (is_sdk_initialized) {
log(LogLevel::WARNING, "[Countly] setMaxStackTraceLineLength, This method can't be called after SDK initialization. Returning.");
return;
}
std::lock_guard<std::mutex> lk(*mutex);
configuration->maxStackTraceLineLength = value;
}

void Countly::setMetrics(const std::string &os, const std::string &os_version, const std::string &device, const std::string &resolution, const std::string &carrier, const std::string &app_version) {
if (is_sdk_initialized) {
log(LogLevel::WARNING, "[Countly] setMetrics, This method can't be called after SDK initialization. Returning.");
Expand Down Expand Up @@ -197,7 +252,14 @@ void Countly::setUserDetails(const std::map<std::string, std::string> &value) {
// unique_lock so the mutex is released on scope exit, including on a throwing
// json/map/addRequestToQueue op; unlock/relock around the self-locking flushEvents().
std::unique_lock<std::mutex> lk(*mutex);
session_params["user_details"] = value;
SDKLimits lim = configurationModule ? configurationModule->getLimits()
: SDKLimits{COUNTLY_MAX_KEY_LENGTH_DEFAULT, COUNTLY_MAX_VALUE_SIZE_DEFAULT, COUNTLY_MAX_SEGMENTATION_VALUES_DEFAULT, COUNTLY_MAX_BREADCRUMB_COUNT_DEFAULT, COUNTLY_MAX_STACK_TRACE_LINES_PER_THREAD_DEFAULT, COUNTLY_MAX_STACK_TRACE_LINE_LENGTH_DEFAULT};
std::map<std::string, std::string> limitedDetails;
for (const auto &kv : value) {
unsigned int cap = (kv.first == "picture") ? COUNTLY_MAX_VALUE_SIZE_PICTURE : lim.maxValueSize;
limitedDetails[kv.first] = cly::limits::truncateString(kv.second, cap);
}
session_params["user_details"] = limitedDetails;

if (!is_sdk_initialized) {
log(LogLevel::ERROR, "[Countly] setUserDetails, This method can't be called before SDK initialization. Returning.");
Expand All @@ -220,11 +282,13 @@ void Countly::setCustomUserDetails(const std::map<std::string, std::string> &val
// json/map/addRequestToQueue op; unlock/re-acquire around the self-locking flushEvents().
std::unique_lock<std::mutex> lk(*mutex);

// Apply user property filter
// Determine the post-filter custom property map first, then apply limits.
std::map<std::string, std::string> customValue;
SDKLimits lim{COUNTLY_MAX_KEY_LENGTH_DEFAULT, COUNTLY_MAX_VALUE_SIZE_DEFAULT, COUNTLY_MAX_SEGMENTATION_VALUES_DEFAULT, COUNTLY_MAX_BREADCRUMB_COUNT_DEFAULT, COUNTLY_MAX_STACK_TRACE_LINES_PER_THREAD_DEFAULT, COUNTLY_MAX_STACK_TRACE_LINE_LENGTH_DEFAULT};
if (configurationModule) {
lim = configurationModule->getLimits();
auto upFilter = configurationModule->getUserPropertyFilterList();
if (!upFilter.filterList.empty()) {
std::map<std::string, std::string> filteredValue;
for (const auto &kv : value) {
bool allowed;
if (upFilter.isWhitelist) {
Expand All @@ -233,22 +297,28 @@ void Countly::setCustomUserDetails(const std::map<std::string, std::string> &val
allowed = (upFilter.filterList.find(kv.first) == upFilter.filterList.end());
}
if (allowed) {
filteredValue[kv.first] = kv.second;
customValue[kv.first] = kv.second;
}
}

if (filteredValue.empty()) {
if (customValue.empty()) {
log(LogLevel::DEBUG, "[Countly] setCustomUserDetails, All user properties were filtered out by SBS user property filter.");
return;
}
session_params["user_details"]["custom"] = filteredValue;
} else {
session_params["user_details"]["custom"] = value;
customValue = value;
}
} else {
session_params["user_details"]["custom"] = value;
customValue = value;
}

// Apply SDK internal limits: truncate keys/values. No count cap for user
// properties (that would be the out-of-scope 'upcl').
std::map<std::string, std::string> limitedCustom;
for (const auto &kv : customValue) {
limitedCustom[cly::limits::truncateString(kv.first, lim.maxKeyLength)] = cly::limits::truncateString(kv.second, lim.maxValueSize);
}
session_params["user_details"]["custom"] = limitedCustom;

if (!is_sdk_initialized) {
log(LogLevel::ERROR, "[Countly] setCustomUserDetails, This method can't be called before SDK initialization. Returning.");
return;
Expand Down Expand Up @@ -666,6 +736,13 @@ void Countly::addEvent(const cly::Event &event) {
}
}

// Apply SDK internal limits to developer-supplied events only. Internal
// [CLY]_* events are limited at their own module boundary (e.g. views).
if (!isInternalEvent) {
SDKLimits limits = configurationModule->getLimits();
filteredEvent.applyLimits(limits.maxKeyLength, limits.maxValueSize, limits.maxSegmentationValues);
}

std::unique_lock<std::mutex> lk(*mutex);
#ifndef COUNTLY_USE_SQLITE
event_queue.push_back(filteredEvent.serialize());
Expand Down
Loading
Loading