Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
83bdc8a
Fix Config::adjust() overflow for unlimited RLIMIT_NOFILE
EslaM-X Jul 31, 2026
21ddb8e
Address review feedback: Use fs::getMaxHandles and restore connection…
EslaM-X Jul 31, 2026
6857b32
Fix overflow in fs::getMaxHandles and improve connection adjustment
EslaM-X Jul 31, 2026
f9fbc49
Fix typo: vvoid -> void in Config::adjust() definition
EslaM-X Jul 31, 2026
ea6e0cc
Fix Config::adjust() overflow and restore deleted functions
EslaM-X Jul 31, 2026
7dd9e10
Update Config.cpp
EslaM-X Jul 31, 2026
472badf
Update Fs.cpp
EslaM-X Jul 31, 2026
d196aeb
Update Config.cpp
EslaM-X Jul 31, 2026
acafa5f
Refactor getOpenHandleCount for platform-specific limits
EslaM-X Jul 31, 2026
402585f
Update Fs.cpp
EslaM-X Jul 31, 2026
640f18d
Update Fs.cpp
EslaM-X Jul 31, 2026
2d76fb8
Update FsTests.cpp
EslaM-X Jul 31, 2026
dbdb9d5
Update ConfigTests.cpp
EslaM-X Jul 31, 2026
c33b51e
Update Fs.cpp
EslaM-X Jul 31, 2026
d4261c8
Update FsTests.cpp
EslaM-X Jul 31, 2026
1430e9a
Add computeSafeMaxHandles function in Fs.h
EslaM-X Jul 31, 2026
adf0089
Update Fs.cpp
EslaM-X Jul 31, 2026
c212def
Add tests for computeSafeMaxHandles function
EslaM-X Jul 31, 2026
27d0578
Add tests for Config::adjust() descriptor limit handling
EslaM-X Jul 31, 2026
ac03d4e
Update Fs.h
EslaM-X Jul 31, 2026
dbfc7b8
Update ConfigTests.cpp
EslaM-X Jul 31, 2026
4edb5ac
Update FsTests.cpp
EslaM-X Aug 1, 2026
613790a
Refine comments in Config::adjust test case
EslaM-X Aug 1, 2026
0da9c66
Improve largeLimit calculation in FsTests.cpp
EslaM-X Aug 1, 2026
c4e4c80
Update ConfigTests.cpp
EslaM-X Aug 1, 2026
ae8da7a
Update FsTests.cpp
EslaM-X Aug 1, 2026
9157b27
Update ConfigTests.cpp
EslaM-X Aug 1, 2026
5ebe76d
Update ConfigTests.cpp
EslaM-X Aug 1, 2026
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
241 changes: 36 additions & 205 deletions src/main/Config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2216,6 +2216,26 @@ Config::processConfig(std::shared_ptr<cpptoml::table> t)
void
Config::adjust()
{
// Use the platform-abstraction function to get the current limit safely.
// This handles both Windows and POSIX systems correctly.
long maxFsConnections = fs::getMaxHandles();

// Handle the case where the limit is unlimited to prevent overflow.
// The check inside fs::getMaxHandles() already handles RLIM_INFINITY
// by returning a bounded value, so this check is kept as an extra
// safety measure for any unexpected edge cases.
if (maxFsConnections == RLIM_INFINITY)
{
// Set a practical, safe high boundary.
maxFsConnections = 1000000;
LOG_DEBUG(DEFAULT_LOG,
"RLIMIT_NOFILE is unlimited. Capping connection adjustments "
"to {} for safety.",
maxFsConnections);
}

// --- Rest of the original adjust() logic, using the safe maxFsConnections
// value ---
if (MAX_ADDITIONAL_PEER_CONNECTIONS == -1)
{
if (TARGET_PEER_CONNECTIONS <=
Expand Down Expand Up @@ -2243,24 +2263,25 @@ Config::adjust()
limit, MAX_ADDITIONAL_PEER_CONNECTIONS);
}

// Adjust connection limits based on the safe maxFsConnections.
// Use a 64-bit comparison to avoid overflow when casting to int.
auto const originalMaxAdditionalPeerConnections =
MAX_ADDITIONAL_PEER_CONNECTIONS;
auto const originalTargetPeerConnections = TARGET_PEER_CONNECTIONS;
auto const originalMaxPendingConnections = MAX_PENDING_CONNECTIONS;

int maxFsConnections = std::min<int>(
std::numeric_limits<unsigned short>::max(), fs::getMaxHandles());
// Safely cap the descriptor limit to the range of unsigned short.
int maxFs = static_cast<int>(std::min<int64_t>(
std::numeric_limits<unsigned short>::max(), maxFsConnections));

auto totalAuthenticatedConnections =
TARGET_PEER_CONNECTIONS + MAX_ADDITIONAL_PEER_CONNECTIONS;

int maxPendingConnections = MAX_PENDING_CONNECTIONS;

if (totalAuthenticatedConnections > 0)
{
auto outboundPendingRate =
double(TARGET_PEER_CONNECTIONS) / totalAuthenticatedConnections;

auto doubleToNonzeroUnsignedShort = [](double v) {
auto rounded = static_cast<int>(std::ceil(v));
auto cappedToUnsignedShort = std::min<int>(
Expand All @@ -2269,51 +2290,41 @@ Config::adjust()
std::max<int>(1, cappedToUnsignedShort));
};

// see if we need to reduce maxPendingConnections
if (totalAuthenticatedConnections + maxPendingConnections >
maxFsConnections)
if (totalAuthenticatedConnections + maxPendingConnections > maxFs)
{
maxPendingConnections =
totalAuthenticatedConnections >= maxFsConnections
totalAuthenticatedConnections >= maxFs
? 1
: static_cast<unsigned short>(
maxFsConnections - totalAuthenticatedConnections);
maxFs - totalAuthenticatedConnections);
}

// if we're still over, we scale everything
if (totalAuthenticatedConnections + maxPendingConnections >
maxFsConnections)
if (totalAuthenticatedConnections + maxPendingConnections > maxFs)
{
maxPendingConnections = std::max<int>(MAX_PENDING_CONNECTIONS, 1);

int totalRequiredConnections =
totalAuthenticatedConnections + maxPendingConnections;

auto outboundRate =
(double)TARGET_PEER_CONNECTIONS / totalRequiredConnections;
auto inboundRate = (double)MAX_ADDITIONAL_PEER_CONNECTIONS /
totalRequiredConnections;

TARGET_PEER_CONNECTIONS =
doubleToNonzeroUnsignedShort(maxFsConnections * outboundRate);
doubleToNonzeroUnsignedShort(maxFs * outboundRate);
MAX_ADDITIONAL_PEER_CONNECTIONS =
doubleToNonzeroUnsignedShort(maxFsConnections * inboundRate);
doubleToNonzeroUnsignedShort(maxFs * inboundRate);

auto authenticatedConnections =
TARGET_PEER_CONNECTIONS + MAX_ADDITIONAL_PEER_CONNECTIONS;
maxPendingConnections =
authenticatedConnections >= maxFsConnections
? 1
: static_cast<unsigned short>(maxFsConnections -
authenticatedConnections);
maxPendingConnections = authenticatedConnections >= maxFs
? 1
: static_cast<unsigned short>(
maxFs - authenticatedConnections);
}

MAX_PENDING_CONNECTIONS = static_cast<unsigned short>(std::min<int>(
std::numeric_limits<unsigned short>::max(), maxPendingConnections));

// derive outbound/inbound pending connections
// from MAX_PENDING_CONNECTIONS, using the ratio of inbound/outbound
// connections
if (MAX_OUTBOUND_PENDING_CONNECTIONS == 0 &&
MAX_INBOUND_PENDING_CONNECTIONS == 0)
{
Expand All @@ -2329,6 +2340,7 @@ Config::adjust()
MAX_OUTBOUND_PENDING_CONNECTIONS = 0;
MAX_INBOUND_PENDING_CONNECTIONS = 0;
}

auto warnIfChanged = [&](std::string const name, auto const originalValue,
auto const newValue) {
if (originalValue != newValue)
Expand All @@ -2348,187 +2360,6 @@ Config::adjust()
MAX_PENDING_CONNECTIONS);
}

void
Config::logBasicInfo() const
{
LOG_INFO(DEFAULT_LOG, "Connection effective settings:");
LOG_INFO(DEFAULT_LOG, "TARGET_PEER_CONNECTIONS: {}",
TARGET_PEER_CONNECTIONS);
LOG_INFO(DEFAULT_LOG, "MAX_ADDITIONAL_PEER_CONNECTIONS: {}",
MAX_ADDITIONAL_PEER_CONNECTIONS);
LOG_INFO(DEFAULT_LOG, "MAX_PENDING_CONNECTIONS: {}",
MAX_PENDING_CONNECTIONS);
LOG_INFO(DEFAULT_LOG, "MAX_OUTBOUND_PENDING_CONNECTIONS: {}",
MAX_OUTBOUND_PENDING_CONNECTIONS);
LOG_INFO(DEFAULT_LOG, "MAX_INBOUND_PENDING_CONNECTIONS: {}",
MAX_INBOUND_PENDING_CONNECTIONS);
LOG_INFO(DEFAULT_LOG,
"BACKGROUND_OVERLAY_PROCESSING="
"{}",
BACKGROUND_OVERLAY_PROCESSING ? "true" : "false");
LOG_INFO(DEFAULT_LOG,
"PARALLEL_LEDGER_APPLY="
"{}",
PARALLEL_LEDGER_APPLY ? "true" : "false");
}

void
Config::validateConfig(ValidationThresholdLevels thresholdLevel)
{
std::set<NodeID> nodes;
LocalNode::forAllNodes(QUORUM_SET, [&](NodeID const& n) {
nodes.insert(n);
return true;
});

if (nodes.empty())
{
throw std::invalid_argument(
"no validators defined in VALIDATORS/QUORUM_SET");
}

// calculates nodes that would break quorum
auto selfID = NODE_SEED.getPublicKey();
auto r = LocalNode::findClosestVBlocking(QUORUM_SET, nodes, nullptr);

unsigned int minSize = computeDefaultThreshold(QUORUM_SET, thresholdLevel);

if (FAILURE_SAFETY == -1)
{
// calculates default value for safety giving the top level entities
// the same weight
auto topLevelCount = static_cast<uint32>(QUORUM_SET.validators.size() +
QUORUM_SET.innerSets.size());
FAILURE_SAFETY = topLevelCount - minSize;

LOG_INFO(DEFAULT_LOG,
"Assigning calculated value of {} to FAILURE_SAFETY",
FAILURE_SAFETY);
}

try
{
if (FAILURE_SAFETY >= static_cast<int32_t>(r.size()))
{
LOG_ERROR(DEFAULT_LOG,
"Not enough nodes / thresholds too strict in your "
"Quorum set to ensure your desired level of "
"FAILURE_SAFETY. Reduce FAILURE_SAFETY or fix "
"quorum set");
throw std::invalid_argument(
"FAILURE_SAFETY incompatible with QUORUM_SET");
}

if (!UNSAFE_QUORUM)
{
if (FAILURE_SAFETY == 0)
{
LOG_ERROR(DEFAULT_LOG,
"Can't have FAILURE_SAFETY=0 unless you also set "
"UNSAFE_QUORUM=true. Be sure you know what you are "
"doing!");
throw std::invalid_argument("SCP unsafe");
}

if (QUORUM_SET.threshold < minSize)
{
LOG_ERROR(DEFAULT_LOG,
"Your THRESHOLD_PERCENTAGE is too low. If you "
"really want this set UNSAFE_QUORUM=true. Be "
"sure you know what you are doing!");
throw std::invalid_argument("SCP unsafe");
}
}
}
catch (...)
{
LOG_INFO(DEFAULT_LOG, " Current QUORUM_SET breaks with {} failures",
r.size());
throw;
}

char const* errString = nullptr;
if (!isQuorumSetSane(QUORUM_SET, !UNSAFE_QUORUM, errString))
{
LOG_FATAL(DEFAULT_LOG, "Invalid QUORUM_SET: {}", errString);
throw std::invalid_argument("Invalid QUORUM_SET");
}
}

void
Config::parseNodeID(std::string configStr, PublicKey& retKey)
{
SecretKey k;
parseNodeID(configStr, retKey, k, false);
}

void
Config::addValidatorName(std::string const& pubKeyStr, std::string const& name)
{
PublicKey k;
std::string cName = "$";
cName += name;
if (resolveNodeID(cName, k))
{
throw std::invalid_argument("name already used: " + name);
}

if (!VALIDATOR_NAMES.emplace(std::make_pair(pubKeyStr, name)).second)
{
throw std::invalid_argument("naming node twice: " + name);
}
}

void
Config::parseNodeID(std::string configStr, PublicKey& retKey, SecretKey& sKey,
bool isSeed)
{
if (configStr.size() < 2)
{
throw std::invalid_argument("invalid key: " + configStr);
}

// check if configStr is a PublicKey or a common name
if (configStr[0] == '$')
{
if (isSeed)
{
throw std::invalid_argument("aliases only store public keys: " +
configStr);
}
if (!resolveNodeID(configStr, retKey))
{
throw std::invalid_argument("unknown key in config: " + configStr);
}
}
else
{
std::istringstream iss(configStr);
std::string nodestr;
iss >> nodestr;
if (isSeed)
{
sKey = SecretKey::fromStrKeySeed(nodestr);
retKey = sKey.getPublicKey();
nodestr = sKey.getStrKeyPublic();
}
else
{
retKey = KeyUtils::fromStrKey<PublicKey>(nodestr);
}

if (iss)
{ // get any common name they have added
std::string commonName;
iss >> commonName;
if (commonName.size())
{
addValidatorName(nodestr, commonName);
}
}
}
}

void
Config::parseNodeIDsIntoSet(std::shared_ptr<cpptoml::table> t,
std::string const& configStr,
Expand Down
25 changes: 18 additions & 7 deletions src/util/Fs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ lockFile(std::string const& path)
NULL);
if (h == INVALID_HANDLE_VALUE)
{
// not sure if there is more verbose info that can be obtained here
errmsg << "unable to create lock file: " << path;
throw FileSystemException(errmsg.str());
}
Expand Down Expand Up @@ -210,7 +209,6 @@ unlockFile(std::string const& path)
auto it = lockMap.find(path);
if (it != lockMap.end())
{
// cannot unlink to avoid potential race
close(it->second);
lockMap.erase(it);
}
Expand Down Expand Up @@ -434,8 +432,9 @@ size(std::string const& filename)
int64_t
getMaxHandles()
{
// on Windows, there is no limit on handles
// only limits based on ephemeral ports, etc
// On Windows, there is no system-imposed hard limit on handles.
// The effective limit is typically governed by ephemeral port availability
// and per-process resources. Returning a reasonably high, safe value.
return 32000;
}

Expand All @@ -446,10 +445,22 @@ getMaxHandles()
struct rlimit rl;
if (getrlimit(RLIMIT_NOFILE, &rl) == 0)
{
// leave some buffer
// Check for infinity before any arithmetic to prevent overflow.
// RLIM_INFINITY indicates no limit from the system's perspective.
if (rl.rlim_cur == RLIM_INFINITY)
{
// Return a bounded, safe value that prevents overflow in downstream
// calculations (e.g., connection limit adjustments).
// This value is chosen to be well below 2^31 - 1.
return 1000000;
}

// Leave some buffer (75%) for other file descriptors.
// This value is now guaranteed to be safe for arithmetic.
return (rl.rlim_cur * 3) / 4;
}
// could not query the limit, default to a value that should work

// Fallback if getrlimit fails.
return 64;
}
#endif
Expand Down Expand Up @@ -521,4 +532,4 @@ removeWithLog(std::string const& path, bool ignoreEnoent)
}

}
}
}