Skip to content

Commit 2d59977

Browse files
committed
Merge bitcoin/bitcoin#32604: log: Mitigate disk filling attacks by rate limiting LogPrintf, LogInfo, LogWarning, LogError
4c772cb doc: add release notes for new rate limiting logging behavior (Eugene Siegel) d541409 log: Add rate limiting to LogPrintf, LogInfo, LogWarning, LogError, LogPrintLevel (Eugene Siegel) a6a35cc log: use std::source_location in place of __func__, __FILE__, __LINE__ (Eugene Siegel) afb9e39 log: introduce LogRateLimiter, LogLimitStats, Status (Eugene Siegel) df7972a test: Mark ~DebugLogHelper as noexcept(false) (Eugene Siegel) Pull request description: This revives the work done by dergoegge in bitcoin/bitcoin#21603. The approach is similar — this PR uses `std::source_location` under the hood now that we can use c++20 features. It also resets the rate limiting statistics via the `CScheduler`. The logging functions have also changed slightly since that PR was opened, so work has been done to preserve the intent of the original rate limiting change. I have tried to give commit attribution where possible. **Approach:** Each source code location is given an hourly logging quota of 1MiB of logging per hour. Logging is only dropped from source locations that exceed the quota. - Only logging to disk is rate limited. Logging to console is not rate limited. - Printing with the category argument is not rate limited. - `UpdateTip: new best=[…]` is logged without rate limiting. High log volume is expected for that source location during IBD. - When logging is restarted a tally of how many bytes were dropped is printed. - All logs will be prefixed with [*] if there is at least one source location that is currently being suppressed. I've repurposed the old logging rpc mentioned in #21603 in another branch for testing [here](https://github.com/Crypt-iQ/bitcoin/tree/log_ratelimiting_05192025_rpc). This can be used to log from source locations and test out the new changes in logging behavior. Note that the `setmocktime` RPC needs to be used to set the mock time past the current clock time to reset the logging messages. Example usage: ``` bitcoin-cli -regtest excessivelog 1 1048500 # log just under 1MiB bitcoin-cli -regtest excessivelog 1 100 # this should get the total amount logged above 1MiB # and the rate limiting logic should kick in bitcoin-cli -regtest excessivelog 2 1048500 bitcoin-cli -regtest excessivelog 2 100 # trigger rate limiting from another location bitcoin-cli -regtest mockscheduler 3600 # fast-forward the scheduler bitcoin-cli -regtest excessivelog 1 100 # this should trigger logging to resume and will log the source locations that were reset ``` Example output: ``` 2025-07-02T22:03:56Z [warning] Excessive logging detected from rpc/node.cpp:142 (RPCHelpMan excessivelog()): >1048576 bytes logged during the last time window of 3600s. Suppressing logging to disk from this source location until time window resets. Console logging unaffected. Last log entry. [*] 2025-07-02T22:03:56Z aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 2025-07-02T22:04:58Z (mocktime: 2262-04-11T23:47:15Z) Restarting logging from rpc/node.cpp:142 (RPCHelpMan excessivelog()): 121 bytes were dropped during the last 3600s. 2025-07-02T22:04:58Z (mocktime: 2262-04-11T23:47:15Z) Restarting logging from rpc/node.cpp:139 (RPCHelpMan excessivelog()): 121 bytes were dropped during the last 3600s. ``` ACKs for top commit: maflcko: re-ACK 4c772cb 🕚 glozow: reACK 4c772cb stickies-v: re-ACK 4c772cb, no changes except release notes update Tree-SHA512: d07087cd0f2b188100b51c9b8c3da376fa24ec3612a2a284bd83f650bba0ea409f9fa0acd5f3b10f45e664ef4fdf3abc97ed3da08098d2beb599cc83e3fc4504
2 parents b7e9dc8 + 4c772cb commit 2d59977

File tree

8 files changed

+466
-62
lines changed

8 files changed

+466
-62
lines changed

doc/release-notes-32604.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Logging
2+
-------
3+
Unconditional logging to disk is now rate limited by giving each source location
4+
a quota of 1MiB per hour. Unconditional logging is any logging with a log level
5+
higher than debug, that is `info`, `warning`, and `error`. All logs will be
6+
prefixed with `[*]` if there is at least one source location that is currently
7+
being suppressed. (#32604)
8+
9+
When `-logsourcelocations` is enabled, the log output now contains the entire
10+
function signature instead of just the function name. (#32604)

src/init.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1378,6 +1378,11 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
13781378
}
13791379
}, std::chrono::minutes{5});
13801380

1381+
LogInstance().SetRateLimiting(std::make_unique<BCLog::LogRateLimiter>(
1382+
[&scheduler](auto func, auto window) { scheduler.scheduleEvery(std::move(func), window); },
1383+
BCLog::RATELIMIT_MAX_BYTES,
1384+
1h));
1385+
13811386
assert(!node.validation_signals);
13821387
node.validation_signals = std::make_unique<ValidationSignals>(std::make_unique<SerialTaskRunner>(scheduler));
13831388
auto& validation_signals = *node.validation_signals;

src/logging.cpp

Lines changed: 99 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
#include <util/time.h>
1313

1414
#include <array>
15+
#include <cstring>
1516
#include <map>
1617
#include <optional>
18+
#include <utility>
1719

1820
using util::Join;
1921
using util::RemovePrefixView;
@@ -73,12 +75,12 @@ bool BCLog::Logger::StartLogging()
7375
// dump buffered messages from before we opened the log
7476
m_buffering = false;
7577
if (m_buffer_lines_discarded > 0) {
76-
LogPrintStr_(strprintf("Early logging buffer overflowed, %d log lines discarded.\n", m_buffer_lines_discarded), __func__, __FILE__, __LINE__, BCLog::ALL, Level::Info);
78+
LogPrintStr_(strprintf("Early logging buffer overflowed, %d log lines discarded.\n", m_buffer_lines_discarded), std::source_location::current(), BCLog::ALL, Level::Info, /*should_ratelimit=*/false);
7779
}
7880
while (!m_msgs_before_open.empty()) {
7981
const auto& buflog = m_msgs_before_open.front();
8082
std::string s{buflog.str};
81-
FormatLogStrInPlace(s, buflog.category, buflog.level, buflog.source_file, buflog.source_line, buflog.logging_function, buflog.threadname, buflog.now, buflog.mocktime);
83+
FormatLogStrInPlace(s, buflog.category, buflog.level, buflog.source_loc, buflog.threadname, buflog.now, buflog.mocktime);
8284
m_msgs_before_open.pop_front();
8385

8486
if (m_print_to_file) FileWriteStr(s, m_fileout);
@@ -364,17 +366,43 @@ std::string BCLog::Logger::GetLogPrefix(BCLog::LogFlags category, BCLog::Level l
364366

365367
static size_t MemUsage(const BCLog::Logger::BufferedLog& buflog)
366368
{
367-
return buflog.str.size() + buflog.logging_function.size() + buflog.source_file.size() + buflog.threadname.size() + memusage::MallocUsage(sizeof(memusage::list_node<BCLog::Logger::BufferedLog>));
369+
return memusage::DynamicUsage(buflog.str) +
370+
memusage::DynamicUsage(buflog.threadname) +
371+
memusage::MallocUsage(sizeof(memusage::list_node<BCLog::Logger::BufferedLog>));
368372
}
369373

370-
void BCLog::Logger::FormatLogStrInPlace(std::string& str, BCLog::LogFlags category, BCLog::Level level, std::string_view source_file, int source_line, std::string_view logging_function, std::string_view threadname, SystemClock::time_point now, std::chrono::seconds mocktime) const
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}
378+
{
379+
scheduler_func([this] { Reset(); }, reset_window);
380+
}
381+
382+
BCLog::LogRateLimiter::Status BCLog::LogRateLimiter::Consume(
383+
const std::source_location& source_loc,
384+
const std::string& str)
385+
{
386+
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};
389+
390+
if (!counter.Consume(str.size()) && status == Status::UNSUPPRESSED) {
391+
status = Status::NEWLY_SUPPRESSED;
392+
m_suppression_active = true;
393+
}
394+
395+
return status;
396+
}
397+
398+
void BCLog::Logger::FormatLogStrInPlace(std::string& str, BCLog::LogFlags category, BCLog::Level level, const std::source_location& source_loc, std::string_view threadname, SystemClock::time_point now, std::chrono::seconds mocktime) const
371399
{
372400
if (!str.ends_with('\n')) str.push_back('\n');
373401

374402
str.insert(0, GetLogPrefix(category, level));
375403

376404
if (m_log_sourcelocations) {
377-
str.insert(0, strprintf("[%s:%d] [%s] ", RemovePrefixView(source_file, "./"), source_line, logging_function));
405+
str.insert(0, strprintf("[%s:%d] [%s] ", RemovePrefixView(source_loc.file_name(), "./"), source_loc.line(), source_loc.function_name()));
378406
}
379407

380408
if (m_log_threadnames) {
@@ -384,28 +412,27 @@ void BCLog::Logger::FormatLogStrInPlace(std::string& str, BCLog::LogFlags catego
384412
str.insert(0, LogTimestampStr(now, mocktime));
385413
}
386414

387-
void BCLog::Logger::LogPrintStr(std::string_view str, std::string_view logging_function, std::string_view source_file, int source_line, BCLog::LogFlags category, BCLog::Level level)
415+
void BCLog::Logger::LogPrintStr(std::string_view str, std::source_location&& source_loc, BCLog::LogFlags category, BCLog::Level level, bool should_ratelimit)
388416
{
389417
StdLockGuard scoped_lock(m_cs);
390-
return LogPrintStr_(str, logging_function, source_file, source_line, category, level);
418+
return LogPrintStr_(str, std::move(source_loc), category, level, should_ratelimit);
391419
}
392420

393-
void BCLog::Logger::LogPrintStr_(std::string_view str, std::string_view logging_function, std::string_view source_file, int source_line, BCLog::LogFlags category, BCLog::Level level)
421+
// NOLINTNEXTLINE(misc-no-recursion)
422+
void BCLog::Logger::LogPrintStr_(std::string_view str, std::source_location&& source_loc, BCLog::LogFlags category, BCLog::Level level, bool should_ratelimit)
394423
{
395424
std::string str_prefixed = LogEscapeMessage(str);
396425

397426
if (m_buffering) {
398427
{
399428
BufferedLog buf{
400-
.now=SystemClock::now(),
401-
.mocktime=GetMockTime(),
402-
.str=str_prefixed,
403-
.logging_function=std::string(logging_function),
404-
.source_file=std::string(source_file),
405-
.threadname=util::ThreadGetInternalName(),
406-
.source_line=source_line,
407-
.category=category,
408-
.level=level,
429+
.now = SystemClock::now(),
430+
.mocktime = GetMockTime(),
431+
.str = str_prefixed,
432+
.threadname = util::ThreadGetInternalName(),
433+
.source_loc = std::move(source_loc),
434+
.category = category,
435+
.level = level,
409436
};
410437
m_cur_buffer_memusage += MemUsage(buf);
411438
m_msgs_before_open.push_back(std::move(buf));
@@ -424,7 +451,29 @@ void BCLog::Logger::LogPrintStr_(std::string_view str, std::string_view logging_
424451
return;
425452
}
426453

427-
FormatLogStrInPlace(str_prefixed, category, level, source_file, source_line, logging_function, util::ThreadGetInternalName(), SystemClock::now(), GetMockTime());
454+
FormatLogStrInPlace(str_prefixed, category, level, source_loc, util::ThreadGetInternalName(), SystemClock::now(), GetMockTime());
455+
bool ratelimit{false};
456+
if (should_ratelimit && m_limiter) {
457+
auto status{m_limiter->Consume(source_loc, str_prefixed)};
458+
if (status == BCLog::LogRateLimiter::Status::NEWLY_SUPPRESSED) {
459+
// NOLINTNEXTLINE(misc-no-recursion)
460+
LogPrintStr_(strprintf(
461+
"Excessive logging detected from %s:%d (%s): >%d bytes logged during "
462+
"the last time window of %is. Suppressing logging to disk from this "
463+
"source location until time window resets. Console logging "
464+
"unaffected. Last log entry.\n",
465+
source_loc.file_name(), source_loc.line(), source_loc.function_name(),
466+
m_limiter->m_max_bytes,
467+
Ticks<std::chrono::seconds>(m_limiter->m_reset_window)),
468+
std::source_location::current(), LogFlags::ALL, Level::Warning, /*should_ratelimit=*/false); // with should_ratelimit=false, this cannot lead to infinite recursion
469+
}
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+
}
476+
}
428477

429478
if (m_print_to_console) {
430479
// print to console
@@ -434,7 +483,7 @@ void BCLog::Logger::LogPrintStr_(std::string_view str, std::string_view logging_
434483
for (const auto& cb : m_print_callbacks) {
435484
cb(str_prefixed);
436485
}
437-
if (m_print_to_file) {
486+
if (m_print_to_file && !ratelimit) {
438487
assert(m_fileout != nullptr);
439488

440489
// reopen the log file, if requested
@@ -492,6 +541,37 @@ void BCLog::Logger::ShrinkDebugFile()
492541
fclose(file);
493542
}
494543

544+
void BCLog::LogRateLimiter::Reset()
545+
{
546+
decltype(m_source_locations) source_locations;
547+
{
548+
StdLockGuard scoped_lock(m_mutex);
549+
source_locations.swap(m_source_locations);
550+
m_suppression_active = false;
551+
}
552+
for (const auto& [source_loc, counter] : source_locations) {
553+
uint64_t dropped_bytes{counter.GetDroppedBytes()};
554+
if (dropped_bytes == 0) continue;
555+
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",
558+
source_loc.file_name(), source_loc.line(), source_loc.function_name(),
559+
dropped_bytes, Ticks<std::chrono::seconds>(m_reset_window));
560+
}
561+
}
562+
563+
bool BCLog::LogLimitStats::Consume(uint64_t bytes)
564+
{
565+
if (bytes > m_available_bytes) {
566+
m_dropped_bytes += bytes;
567+
m_available_bytes = 0;
568+
return false;
569+
}
570+
571+
m_available_bytes -= bytes;
572+
return true;
573+
}
574+
495575
bool BCLog::Logger::SetLogLevel(std::string_view level_str)
496576
{
497577
const auto level = GetLogLevel(level_str);

0 commit comments

Comments
 (0)