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
13 changes: 13 additions & 0 deletions BedrockServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1299,6 +1299,19 @@ BedrockServer::BedrockServer(const SData& args_)
SSyslogFunc = &SSyslogSocketDirect;
}

// Log destination: rsyslog, fluentd, or both. Default is rsyslog.
// Fluentd defaults: 127.0.0.1:24224, tag=bedrock. Override with -fluentdHost, -fluentdPort, -fluentdTag
string logDestination = args.isSet("-logDestination") ? args["-logDestination"] : "rsyslog";
if (logDestination == "fluentd" || logDestination == "both") {
string host = args.isSet("-fluentdHost") ? args["-fluentdHost"] : "127.0.0.1";
int port = args.isSet("-fluentdPort") ? SToInt(args["-fluentdPort"]) : 24224;
string tag = args.isSet("-fluentdTag") ? args["-fluentdTag"] : "bedrock";
SFluentdInitialize(host, port, tag);
}
if (logDestination == "fluentd") {
SSyslogFunc = &SSyslogNoop;
}

// Check for commands that will be forced to use QUORUM write consistency.
if (args.isSet("-synchronousCommands")) {
list<string> syncCommands;
Expand Down
81 changes: 81 additions & 0 deletions libstuff/libstuff.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,87 @@ void SSyslogSocketDirect(int priority, const char* format, ...)
}
}

void SSyslogNoop(int priority, const char* format, ...)
{
}

static int SFluentdSocketFD = -1;
static mutex SFluentdSocketMutex;
static string SFluentdHost;
static int SFluentdPort = 0;
static string SFluentdTag;
static atomic<bool> SFluentdConfigured{false};

// Lock parameter enforces mutex is held before calling this function.
static bool SFluentdConnect(const lock_guard<mutex>&)
{
SFluentdSocketFD = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (SFluentdSocketFD == -1) {
return false;
}

struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(SFluentdPort);
inet_pton(AF_INET, SFluentdHost.c_str(), &addr.sin_addr);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve non-literal fluentdHost values before connect

Connection setup uses inet_pton(AF_INET, SFluentdHost.c_str(), ...) and ignores its return value, so hostnames like fluentd.service are parsed as invalid and leave the destination as 0.0.0.0; every connect attempt then fails silently. Since -fluentdHost is exposed as a host option, this breaks Fluentd logging whenever operators provide a DNS/service name instead of a raw IPv4 literal.

Useful? React with 👍 / 👎.


if (connect(SFluentdSocketFD, (struct sockaddr*) &addr, sizeof(addr)) == -1) {
close(SFluentdSocketFD);
SFluentdSocketFD = -1;
return false;
}

return true;
}

void SFluentdInitialize(const string& host, int port, const string& tag)
{
lock_guard<mutex> lock(SFluentdSocketMutex);
SFluentdHost = host;
SFluentdPort = port;
SFluentdTag = tag;
SFluentdConnect(lock);
SFluentdConfigured.store(true);
}
Comment on lines +317 to +325
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SFluentdInitialize / SFluentdConnect will overwrite SFluentdSocketFD with a new socket() result without closing any previously-open Fluentd socket. If initialization can happen more than once in a process (e.g., tests, reconfiguration), this will leak file descriptors. Close the existing FD (if != -1) before creating a new socket, or make initialization explicitly one-shot and enforce it.

Copilot uses AI. Check for mistakes.

void SFluentdLog(int priority, const string& message, const STable& params)
{
if (!SFluentdConfigured.load()) {
return;
}

// Build JSON before acquiring lock to avoid doing heavy stuff in the critical section
STable record;
record["timestamp"] = to_string(time(nullptr));
record["priority"] = to_string(priority);
record["thread_name"] = SThreadLogName;
record["thread_prefix"] = SThreadLogPrefix;
record["process"] = SProcessName;
record["message"] = message;

for (const auto& [key, value] : params) {
record[key] = value;
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When merging params into record, user-supplied keys can overwrite reserved metadata fields like timestamp, priority, process, or even message. To keep the structured log schema stable, consider preventing overwrites (only insert when absent) or namespacing user params under a dedicated sub-object key (e.g., params).

Suggested change
record[key] = value;
// Prevent user-supplied parameters from overwriting reserved metadata fields.
if (!record.count(key)) {
record[key] = value;
}

Copilot uses AI. Check for mistakes.
Comment on lines +342 to +343

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Redact params before writing Fluentd records

SFluentdLog currently copies every entry from params directly into the JSON record, which bypasses the existing redaction path used by addLogParams (libstuff/SLog.cpp lines 81-98). When -logDestination is fluentd or both, any sensitive fields passed in logging params (that are intentionally redacted in rsyslog output) will be emitted in cleartext to Fluentd, creating a data-leak regression.

Useful? React with 👍 / 👎.

}

string json = "[\"" + SFluentdTag + "\"," + to_string(time(nullptr)) + "," + SComposeJSONObject(record) + "]\n";
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Fluentd tag is injected into the JSON payload via string concatenation (["" + SFluentdTag + "", ...]) without JSON escaping. If -fluentdTag contains quotes/backslashes/control characters, the emitted JSON becomes invalid and can be abused for log injection. Build the outer JSON using the existing JSON helpers (e.g., SToJSON(SFluentdTag, /*forceString=*/true)) so the tag is correctly escaped.

Suggested change
string json = "[\"" + SFluentdTag + "\"," + to_string(time(nullptr)) + "," + SComposeJSONObject(record) + "]\n";
string json = "[" + SToJSON(SFluentdTag, /*forceString=*/true) + "," + to_string(time(nullptr)) + "," + SComposeJSONObject(record) + "]\n";

Copilot uses AI. Check for mistakes.
Comment on lines +335 to +346
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SFluentdLog calls time(nullptr) twice (once for record["timestamp"] and again for the Fluentd event time). If a second boundary is crossed between calls, the two timestamps can disagree. Capture the timestamp once and reuse it for both fields to keep the record consistent.

Copilot uses AI. Check for mistakes.
Comment on lines +333 to +346
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New behavior builds a specific Fluentd JSON frame ([tag, time, record]\n) and reconnection logic, but there are existing libstuff unit tests (e.g., JSON helpers) and nothing here validates the emitted frame or escaping. Consider adding a unit test that exercises the JSON payload composition (including escaping and reserved-field behavior) without requiring a real Fluentd instance (e.g., by extracting payload formatting into a helper function).

Copilot uses AI. Check for mistakes.

lock_guard<mutex> lock(SFluentdSocketMutex);

// Reconnect if needed
if (SFluentdSocketFD == -1) {
if (!SFluentdConnect(lock)) {
return;
}
}

// Try to send the log over TCP. Close the socket on failure. It'll try to reconnect on next attempt
if (send(SFluentdSocketFD, json.c_str(), json.size(), MSG_NOSIGNAL) == -1) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle partial TCP writes when sending Fluentd JSON

The send path treats any return value other than -1 as success, but send() on a TCP socket may return a short byte count under backpressure; in that case the remaining bytes are dropped and the emitted Fluentd frame is truncated. This can corrupt JSON log records intermittently for larger messages or busy sockets, even though no reconnect/error path is triggered.

Useful? React with 👍 / 👎.

close(SFluentdSocketFD);
SFluentdSocketFD = -1;
Comment on lines +358 to +360
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

send() on a TCP socket can return a short write; the current code treats any non--1 return as success, which can truncate the JSON frame and break Fluentd parsing. Track the number of bytes sent and loop until the full buffer is written, or treat partial writes as an error and reconnect.

Suggested change
if (send(SFluentdSocketFD, json.c_str(), json.size(), MSG_NOSIGNAL) == -1) {
close(SFluentdSocketFD);
SFluentdSocketFD = -1;
size_t totalSent = 0;
const size_t totalSize = json.size();
while (totalSent < totalSize) {
ssize_t bytesSent = send(SFluentdSocketFD, json.c_str() + totalSent, totalSize - totalSent, MSG_NOSIGNAL);
if (bytesSent <= 0) {
// Error or connection closed; close the socket so it will reconnect on the next attempt
close(SFluentdSocketFD);
SFluentdSocketFD = -1;
break;
}
totalSent += static_cast<size_t>(bytesSent);

Copilot uses AI. Check for mistakes.
}
}

/////////////////////////////////////////////////////////////////////////////
// Math stuff
/////////////////////////////////////////////////////////////////////////////
Expand Down
20 changes: 18 additions & 2 deletions libstuff/libstuff.h
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,23 @@ void SWhitelistLogParams(const set<string>& params);
// This is a drop-in replacement for syslog that directly logs to `/run/systemd/journal/syslog` bypassing journald.
void SSyslogSocketDirect(int priority, const char* format, ...);

// Atomic pointer to the syslog function that we'll actually use. Easy to change to `syslog` or `SSyslogSocketDirect`.
// No-op function to disable rsyslog logging.
void SSyslogNoop(int priority, const char* format, ...);

// Atomic pointer to the syslog function that we'll actually use.
// Can be set to `syslog`, `SSyslogSocketDirect`, or `SSyslogNoop`.
extern atomic<void (*)(int priority, const char* format, ...)> SSyslogFunc;

// --------------------------------------------------------------------------
// Fluentd JSON logging stuff
// --------------------------------------------------------------------------
// Initialize Fluentd TCP socket connection. Call once at startup.
void SFluentdInitialize(const string& host, int port, const string& tag);
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment for SFluentdInitialize says it “Returns true on success”, but the function is declared/defined as void and there’s no success signal. Either change the signature to return bool (and actually reflect connection success), or update the comment to match the current behavior (e.g., best-effort configure + lazy reconnect).

Suggested change
void SFluentdInitialize(const string& host, int port, const string& tag);
// Performs best-effort initialization. If initialization fails, SFluentdLog will be a no-op.

Copilot uses AI. Check for mistakes.

// Log a message to Fluentd in JSON format. Thread-safe.
// No-op if Fluentd is not initialized. Handles connection failures gracefully.
void SFluentdLog(int priority, const string& message, const STable& params = {});

string addLogParams(string&& message, const STable& params = {});

// **NOTE: rsyslog default max line size is 8k bytes. We split on 7k byte boundaries in order to fit the syslog line prefix and the expanded \r\n to #015#012
Expand All @@ -267,11 +281,13 @@ string addLogParams(string&& message, const STable& params = {});
if (_g_SLogMask & (1 << (_PRI_))) { \
ostringstream __out; \
__out << _MSG_; \
const string s = addLogParams(__out.str(), ## __VA_ARGS__); \
const string __rawMsg = __out.str(); \
const string s = addLogParams(string(__rawMsg), ## __VA_ARGS__); \
const string prefix = SWHEREAMI; \
for (size_t i = 0; i < s.size(); i += 7168) { \
(*SSyslogFunc)(_PRI_, "%s", (prefix + s.substr(i, 7168)).c_str()); \
} \
SFluentdLog(_PRI_, prefix + __rawMsg, ## __VA_ARGS__); \
} \
} while (false)
Comment on lines +285 to 292
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When -logDestination is set to fluentd, SSyslogFunc becomes SSyslogNoop, but this macro still builds s, appends params, and loops over 7k chunks calling the no-op function. This adds avoidable per-log CPU overhead in fluentd-only mode; consider gating the rsyslog formatting/chunking path behind a separate enabled flag (or skip the chunk loop when rsyslog is disabled).

Copilot uses AI. Check for mistakes.

Expand Down