Skip to content

Commit 578b512

Browse files
committed
Merge bitcoin/bitcoin#33011: log: rate limiting followups
5c74a0b config: add DEBUG_ONLY -logratelimit (Eugene Siegel) 9f3b017 test: logging_filesize_rate_limit improvements (stickies-v) 350193e test: don't leak log category mask across tests (stickies-v) 05d7c22 test: add ReadDebugLogLines helper function (stickies-v) 3d630c2 log: make m_limiter a shared_ptr (stickies-v) e8f9c37 log: clean up LogPrintStr_ and Reset, prefix all logs with "[*]" when there are suppressions (Eugene Siegel) 3c7cae4 log: change LogLimitStats to struct LogRateLimiter::Stats (Eugene Siegel) 8319a13 log: clarify RATELIMIT_MAX_BYTES comment, use RATELIMIT_WINDOW (Eugene Siegel) 5f70bc8 log: remove const qualifier from arguments in LogPrintFormatInternal (Eugene Siegel) b8e92fb log: avoid double hashing in SourceLocationHasher (Eugene Siegel) 616bc22 test: remove noexcept(false) comment in ~DebugLogHelper (Eugene Siegel) Pull request description: Followups to #32604. There are two behavior changes: - prefixing with `[*]` is done to all logs (regardless of `should_ratelimit`) per [this comment](bitcoin/bitcoin#32604 (comment)). - a DEBUG_ONLY `-disableratelimitlogging` flag is added by default to functional tests so they don't encounter rate limiting. ACKs for top commit: stickies-v: re-ACK 5c74a0b achow101: ACK 5c74a0b l0rinc: Code review ACK 5c74a0b Tree-SHA512: d32db5fcc28bb9b2a850f0048c8062200a3725b88f1cd9a0e137da065c0cf9a5d22e5d03cb16fe75ea7494801313ab34ffec7cf3e8577cd7527e636af53591c4
2 parents 8405fdb + 5c74a0b commit 578b512

File tree

7 files changed

+224
-206
lines changed

7 files changed

+224
-206
lines changed

src/init.cpp

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1400,10 +1400,14 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
14001400
}
14011401
}, std::chrono::minutes{5});
14021402

1403-
LogInstance().SetRateLimiting(std::make_unique<BCLog::LogRateLimiter>(
1404-
[&scheduler](auto func, auto window) { scheduler.scheduleEvery(std::move(func), window); },
1405-
BCLog::RATELIMIT_MAX_BYTES,
1406-
1h));
1403+
if (args.GetBoolArg("-logratelimit", BCLog::DEFAULT_LOGRATELIMIT)) {
1404+
LogInstance().SetRateLimiting(BCLog::LogRateLimiter::Create(
1405+
[&scheduler](auto func, auto window) { scheduler.scheduleEvery(std::move(func), window); },
1406+
BCLog::RATELIMIT_MAX_BYTES,
1407+
BCLog::RATELIMIT_WINDOW));
1408+
} else {
1409+
LogInfo("Log rate limiting disabled");
1410+
}
14071411

14081412
assert(!node.validation_signals);
14091413
node.validation_signals = std::make_unique<ValidationSignals>(std::make_unique<SerialTaskRunner>(scheduler));

src/init/common.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ void AddLoggingArgs(ArgsManager& argsman)
3838
argsman.AddArg("-logsourcelocations", strprintf("Prepend debug output with name of the originating source location (source file, line number and function name) (default: %u)", DEFAULT_LOGSOURCELOCATIONS), ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST);
3939
argsman.AddArg("-logtimemicros", strprintf("Add microsecond precision to debug timestamps (default: %u)", DEFAULT_LOGTIMEMICROS), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
4040
argsman.AddArg("-loglevelalways", strprintf("Always prepend a category and level (default: %u)", DEFAULT_LOGLEVELALWAYS), ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST);
41+
argsman.AddArg("-logratelimit", strprintf("Apply rate limiting to unconditional logging to mitigate disk-filling attacks (default: %u)", BCLog::DEFAULT_LOGRATELIMIT), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
4142
argsman.AddArg("-printtoconsole", "Send trace/debug info to console (default: 1 when no -daemon. To disable logging to file, set -nodebuglogfile)", ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST);
4243
argsman.AddArg("-shrinkdebugfile", "Shrink debug.log file on client startup (default: 1 when no -debug)", ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST);
4344
}

src/logging.cpp

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -371,23 +371,30 @@ static size_t MemUsage(const BCLog::Logger::BufferedLog& buflog)
371371
memusage::MallocUsage(sizeof(memusage::list_node<BCLog::Logger::BufferedLog>));
372372
}
373373

374-
BCLog::LogRateLimiter::LogRateLimiter(
375-
SchedulerFunction scheduler_func,
376-
uint64_t max_bytes,
377-
std::chrono::seconds reset_window) : m_max_bytes{max_bytes}, m_reset_window{reset_window}
374+
BCLog::LogRateLimiter::LogRateLimiter(uint64_t max_bytes, std::chrono::seconds reset_window)
375+
: m_max_bytes{max_bytes}, m_reset_window{reset_window} {}
376+
377+
std::shared_ptr<BCLog::LogRateLimiter> BCLog::LogRateLimiter::Create(
378+
SchedulerFunction&& scheduler_func, uint64_t max_bytes, std::chrono::seconds reset_window)
378379
{
379-
scheduler_func([this] { Reset(); }, reset_window);
380+
auto limiter{std::shared_ptr<LogRateLimiter>(new LogRateLimiter(max_bytes, reset_window))};
381+
std::weak_ptr<LogRateLimiter> weak_limiter{limiter};
382+
auto reset = [weak_limiter] {
383+
if (auto shared_limiter{weak_limiter.lock()}) shared_limiter->Reset();
384+
};
385+
scheduler_func(reset, limiter->m_reset_window);
386+
return limiter;
380387
}
381388

382389
BCLog::LogRateLimiter::Status BCLog::LogRateLimiter::Consume(
383390
const std::source_location& source_loc,
384391
const std::string& str)
385392
{
386393
StdLockGuard scoped_lock(m_mutex);
387-
auto& counter{m_source_locations.try_emplace(source_loc, m_max_bytes).first->second};
388-
Status status{counter.GetDroppedBytes() > 0 ? Status::STILL_SUPPRESSED : Status::UNSUPPRESSED};
394+
auto& stats{m_source_locations.try_emplace(source_loc, m_max_bytes).first->second};
395+
Status status{stats.m_dropped_bytes > 0 ? Status::STILL_SUPPRESSED : Status::UNSUPPRESSED};
389396

390-
if (!counter.Consume(str.size()) && status == Status::UNSUPPRESSED) {
397+
if (!stats.Consume(str.size()) && status == Status::UNSUPPRESSED) {
391398
status = Status::NEWLY_SUPPRESSED;
392399
m_suppression_active = true;
393400
}
@@ -455,24 +462,26 @@ void BCLog::Logger::LogPrintStr_(std::string_view str, std::source_location&& so
455462
bool ratelimit{false};
456463
if (should_ratelimit && m_limiter) {
457464
auto status{m_limiter->Consume(source_loc, str_prefixed)};
458-
if (status == BCLog::LogRateLimiter::Status::NEWLY_SUPPRESSED) {
465+
if (status == LogRateLimiter::Status::NEWLY_SUPPRESSED) {
459466
// NOLINTNEXTLINE(misc-no-recursion)
460467
LogPrintStr_(strprintf(
461468
"Excessive logging detected from %s:%d (%s): >%d bytes logged during "
462469
"the last time window of %is. Suppressing logging to disk from this "
463470
"source location until time window resets. Console logging "
464-
"unaffected. Last log entry.\n",
471+
"unaffected. Last log entry.",
465472
source_loc.file_name(), source_loc.line(), source_loc.function_name(),
466473
m_limiter->m_max_bytes,
467474
Ticks<std::chrono::seconds>(m_limiter->m_reset_window)),
468475
std::source_location::current(), LogFlags::ALL, Level::Warning, /*should_ratelimit=*/false); // with should_ratelimit=false, this cannot lead to infinite recursion
476+
} else if (status == LogRateLimiter::Status::STILL_SUPPRESSED) {
477+
ratelimit = true;
469478
}
470-
ratelimit = status == BCLog::LogRateLimiter::Status::STILL_SUPPRESSED;
471-
// To avoid confusion caused by dropped log messages when debugging an issue,
472-
// we prefix log lines with "[*]" when there are any suppressed source locations.
473-
if (m_limiter->SuppressionsActive()) {
474-
str_prefixed.insert(0, "[*] ");
475-
}
479+
}
480+
481+
// To avoid confusion caused by dropped log messages when debugging an issue,
482+
// we prefix log lines with "[*]" when there are any suppressed source locations.
483+
if (m_limiter && m_limiter->SuppressionsActive()) {
484+
str_prefixed.insert(0, "[*] ");
476485
}
477486

478487
if (m_print_to_console) {
@@ -549,18 +558,17 @@ void BCLog::LogRateLimiter::Reset()
549558
source_locations.swap(m_source_locations);
550559
m_suppression_active = false;
551560
}
552-
for (const auto& [source_loc, counter] : source_locations) {
553-
uint64_t dropped_bytes{counter.GetDroppedBytes()};
554-
if (dropped_bytes == 0) continue;
561+
for (const auto& [source_loc, stats] : source_locations) {
562+
if (stats.m_dropped_bytes == 0) continue;
555563
LogPrintLevel_(
556-
LogFlags::ALL, Level::Info, /*should_ratelimit=*/false,
557-
"Restarting logging from %s:%d (%s): %d bytes were dropped during the last %ss.\n",
564+
LogFlags::ALL, Level::Warning, /*should_ratelimit=*/false,
565+
"Restarting logging from %s:%d (%s): %d bytes were dropped during the last %ss.",
558566
source_loc.file_name(), source_loc.line(), source_loc.function_name(),
559-
dropped_bytes, Ticks<std::chrono::seconds>(m_reset_window));
567+
stats.m_dropped_bytes, Ticks<std::chrono::seconds>(m_reset_window));
560568
}
561569
}
562570

563-
bool BCLog::LogLimitStats::Consume(uint64_t bytes)
571+
bool BCLog::LogRateLimiter::Stats::Consume(uint64_t bytes)
564572
{
565573
if (bytes > m_available_bytes) {
566574
m_dropped_bytes += bytes;

src/logging.h

Lines changed: 33 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <cstring>
1919
#include <functional>
2020
#include <list>
21+
#include <memory>
2122
#include <mutex>
2223
#include <source_location>
2324
#include <string>
@@ -46,10 +47,10 @@ struct SourceLocationHasher {
4647
size_t operator()(const std::source_location& s) const noexcept
4748
{
4849
// Use CSipHasher(0, 0) as a simple way to get uniform distribution.
49-
return static_cast<size_t>(CSipHasher(0, 0)
50-
.Write(std::hash<std::string_view>{}(s.file_name()))
51-
.Write(s.line())
52-
.Finalize());
50+
return size_t(CSipHasher(0, 0)
51+
.Write(s.line())
52+
.Write(MakeUCharSpan(std::string_view{s.file_name()}))
53+
.Finalize());
5354
}
5455
};
5556

@@ -104,47 +105,34 @@ namespace BCLog {
104105
};
105106
constexpr auto DEFAULT_LOG_LEVEL{Level::Debug};
106107
constexpr size_t DEFAULT_MAX_LOG_BUFFER{1'000'000}; // buffer up to 1MB of log data prior to StartLogging
107-
constexpr uint64_t RATELIMIT_MAX_BYTES{1024 * 1024}; // maximum number of bytes that can be logged within one window
108+
constexpr uint64_t RATELIMIT_MAX_BYTES{1024 * 1024}; // maximum number of bytes per source location that can be logged within the RATELIMIT_WINDOW
109+
constexpr auto RATELIMIT_WINDOW{1h}; // time window after which log ratelimit stats are reset
110+
constexpr bool DEFAULT_LOGRATELIMIT{true};
108111

109-
//! Keeps track of an individual source location and how many available bytes are left for logging from it.
110-
class LogLimitStats
112+
//! Fixed window rate limiter for logging.
113+
class LogRateLimiter
111114
{
112-
private:
113-
//! Remaining bytes in the current window interval.
114-
uint64_t m_available_bytes;
115-
//! Number of bytes that were not consumed within the current window.
116-
uint64_t m_dropped_bytes{0};
117-
118115
public:
119-
LogLimitStats(uint64_t max_bytes) : m_available_bytes{max_bytes} {}
120-
//! Consume bytes from the window if enough bytes are available.
121-
//!
122-
//! Returns whether enough bytes were available.
123-
bool Consume(uint64_t bytes);
124-
125-
uint64_t GetAvailableBytes() const
126-
{
127-
return m_available_bytes;
128-
}
129-
130-
uint64_t GetDroppedBytes() const
131-
{
132-
return m_dropped_bytes;
133-
}
134-
};
116+
//! Keeps track of an individual source location and how many available bytes are left for logging from it.
117+
struct Stats {
118+
//! Remaining bytes
119+
uint64_t m_available_bytes;
120+
//! Number of bytes that were consumed but didn't fit in the available bytes.
121+
uint64_t m_dropped_bytes{0};
122+
123+
Stats(uint64_t max_bytes) : m_available_bytes{max_bytes} {}
124+
//! Updates internal accounting and returns true if enough available_bytes were remaining
125+
bool Consume(uint64_t bytes);
126+
};
135127

136-
/**
137-
* Fixed window rate limiter for logging.
138-
*/
139-
class LogRateLimiter
140-
{
141128
private:
142129
mutable StdMutex m_mutex;
143130

144-
//! Counters for each source location that has attempted to log something.
145-
std::unordered_map<std::source_location, LogLimitStats, SourceLocationHasher, SourceLocationEqual> m_source_locations GUARDED_BY(m_mutex);
146-
//! True if at least one log location is suppressed. Cached view on m_source_locations for performance reasons.
131+
//! Stats for each source location that has attempted to log something.
132+
std::unordered_map<std::source_location, Stats, SourceLocationHasher, SourceLocationEqual> m_source_locations GUARDED_BY(m_mutex);
133+
//! Whether any log locations are suppressed. Cached view on m_source_locations for performance reasons.
147134
std::atomic<bool> m_suppression_active{false};
135+
LogRateLimiter(uint64_t max_bytes, std::chrono::seconds reset_window);
148136

149137
public:
150138
using SchedulerFunction = std::function<void(std::function<void()>, std::chrono::milliseconds)>;
@@ -154,9 +142,12 @@ namespace BCLog {
154142
* reset_window interval.
155143
* @param max_bytes Maximum number of bytes that can be logged for each source
156144
* location.
157-
* @param reset_window Time window after which the byte counters are reset.
145+
* @param reset_window Time window after which the stats are reset.
158146
*/
159-
LogRateLimiter(SchedulerFunction scheduler_func, uint64_t max_bytes, std::chrono::seconds reset_window);
147+
static std::shared_ptr<LogRateLimiter> Create(
148+
SchedulerFunction&& scheduler_func,
149+
uint64_t max_bytes,
150+
std::chrono::seconds reset_window);
160151
//! Maximum number of bytes logged per location per window.
161152
const uint64_t m_max_bytes;
162153
//! Interval after which the window is reset.
@@ -201,7 +192,7 @@ namespace BCLog {
201192
size_t m_buffer_lines_discarded GUARDED_BY(m_cs){0};
202193

203194
//! Manages the rate limiting of each log location.
204-
std::unique_ptr<LogRateLimiter> m_limiter GUARDED_BY(m_cs);
195+
std::shared_ptr<LogRateLimiter> m_limiter GUARDED_BY(m_cs);
205196

206197
//! Category-specific log level. Overrides `m_log_level`.
207198
std::unordered_map<LogFlags, Level> m_category_log_levels GUARDED_BY(m_cs);
@@ -270,7 +261,7 @@ namespace BCLog {
270261
/** Only for testing */
271262
void DisconnectTestLogger() EXCLUSIVE_LOCKS_REQUIRED(!m_cs);
272263

273-
void SetRateLimiting(std::unique_ptr<LogRateLimiter>&& limiter) EXCLUSIVE_LOCKS_REQUIRED(!m_cs)
264+
void SetRateLimiting(std::shared_ptr<LogRateLimiter> limiter) EXCLUSIVE_LOCKS_REQUIRED(!m_cs)
274265
{
275266
StdLockGuard scoped_lock(m_cs);
276267
m_limiter = std::move(limiter);
@@ -343,7 +334,7 @@ static inline bool LogAcceptCategory(BCLog::LogFlags category, BCLog::Level leve
343334
bool GetLogCategory(BCLog::LogFlags& flag, std::string_view str);
344335

345336
template <typename... Args>
346-
inline void LogPrintFormatInternal(std::source_location&& source_loc, const BCLog::LogFlags flag, const BCLog::Level level, const bool should_ratelimit, util::ConstevalFormatString<sizeof...(Args)> fmt, const Args&... args)
337+
inline void LogPrintFormatInternal(std::source_location&& source_loc, BCLog::LogFlags flag, BCLog::Level level, bool should_ratelimit, util::ConstevalFormatString<sizeof...(Args)> fmt, const Args&... args)
347338
{
348339
if (LogInstance().Enabled()) {
349340
std::string log_msg;

0 commit comments

Comments
 (0)