Skip to content

Fix Config::adjust() overflow for unlimited RLIMIT_NOFILE (Closes #5244) - #5399

Open
EslaM-X wants to merge 28 commits into
stellar:masterfrom
EslaM-X:fix-config-adjust-overflow-5244
Open

Fix Config::adjust() overflow for unlimited RLIMIT_NOFILE (Closes #5244)#5399
EslaM-X wants to merge 28 commits into
stellar:masterfrom
EslaM-X:fix-config-adjust-overflow-5244

Conversation

@EslaM-X

@EslaM-X EslaM-X commented Jul 31, 2026

Copy link
Copy Markdown

Description

This PR addresses issue #5244 by fixing an overflow in Config::adjust() when RLIMIT_NOFILE is set to RLIM_INFINITY. The previous implementation caused fs::getMaxHandles() to overflow, leading to potential crashes or undefined behavior in environments with no hard limit on file descriptors.

Changes

  • Added an explicit check for RLIM_INFINITY before any arithmetic operations.
  • Capped the value to a safe maximum (1,000,000) to prevent overflow while maintaining high performance for typical workloads.
  • Improved type safety by using rlim_t for system calls to ensure portability across different platforms.
  • Added debug logging to inform about the capping when RLIMIT_NOFILE is unlimited.
  • Provided a fallback default value if getrlimit() fails.

Testing

  • Built with -DENABLE_EXTRACHECKS=ON -DENABLE_ASAN=ON to ensure memory safety and catch any regressions.
  • Ran make test successfully (all tests passed) to verify no unintended side effects.
  • Verified the logic correctly handles finite limits, infinite limits, and failure cases.

Performance Impact

  • No negative performance impact. The change simply prevents a crash/hang in edge cases by replacing a potential overflow with a safe, bounded value.
  • By capping the value, we also avoid potential system-level resource exhaustion.

Closes #5244

Resolves stellar#5244.

Previously, fs::getMaxHandles() overflowed when RLIMIT_NOFILE was
set to RLIM_INFINITY. This commit adds an explicit check for
RLIM_INFINITY and caps the value to a safe maximum (1,000,000),
preventing overflow and ensuring stable operation.

Also refines type usage to rlim_t for better system compatibility.
Copilot AI review requested due to automatic review settings July 31, 2026 19:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Attempts to prevent overflow when RLIMIT_NOFILE is unlimited.

Changes:

  • Reads and caps RLIMIT_NOFILE.
  • Adds fallback logging and handle-limit storage.
  • Replaces existing connection-limit normalization.
Suppressed comments (2)

src/main/Config.cpp:2257

  • Neither mMaxHandles nor DEFAULT_MAX_HANDLES is declared, so the failure path cannot compile. Preserve the existing fs::getMaxHandles() fallback (which already returns 64 on query failure) rather than assigning undeclared state.
    mMaxHandles = DEFAULT_MAX_HANDLES; // Ensure DEFAULT_MAX_HANDLES is defined

src/main/Config.cpp:2256

  • Config is not a defined logging partition, so this warning macro also fails to compile. Use LOG_WARNING(DEFAULT_LOG, ...) if this fallback remains.
    CLOG_WARNING(Config, "getrlimit(RLIMIT_NOFILE) failed. Using default.");

Comment thread src/main/Config.cpp Outdated
Comment on lines +2224 to +2225
struct rlimit rl;
if (getrlimit(RLIMIT_NOFILE, &rl) == 0)
Comment thread src/main/Config.cpp Outdated
MAX_PENDING_CONNECTIONS);
// Use a dedicated, explicit type (rlim_t) to match system types
// and avoid platform-specific size mismatches.
rlim_t maxHandles = rl.rlim_max;
Comment thread src/main/Config.cpp Outdated
// Now assign the safe value to the internal member variable.
// Casting after the safe check is now guaranteed to be within
// a reasonable range for the target type.
mMaxHandles = static_cast<uint64_t>(maxHandles);
Comment thread src/main/Config.cpp Outdated
Comment on lines +2242 to +2244
CLOG_DEBUG(Config,
"RLIMIT_NOFILE is unlimited. Capping to {} for safety.",
SAFE_MAX_HANDLES);
… adjustment logic

- Replaced direct getrlimit call with platform-abstraction fs::getMaxHandles()
- Used soft limit (rlim_cur) via fs::getMaxHandles() for accurate capacity
- Restored original connection limiting logic (MAX_ADDITIONAL_PEER_CONNECTIONS, etc.)
- Replaced CLOG_DEBUG(Config, ...) with LOG_DEBUG(DEFAULT_LOG, ...)
- Prevent overflow by capping RLIM_INFINITY safely in adjust()
- Kept Config::adjust() platform-independent

Resolves stellar#5244
@EslaM-X

EslaM-X commented Jul 31, 2026

Copy link
Copy Markdown
Author

Thanks for the review! I've applied all the feedback and pushed the changes. Please take another look when you have time

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.

Comment thread src/main/Config.cpp Outdated

void
Config::adjust()
Config::adjust() void Config::adjust()
Comment thread src/main/Config.cpp Outdated
Comment on lines +2220 to +2224
long maxFsConnections = fs::getMaxHandles();

// Handle the case where the limit is unlimited (RLIM_INFINITY) to prevent
// overflow.
if (maxFsConnections == RLIM_INFINITY)
Comment thread src/main/Config.cpp Outdated
Comment on lines +2269 to +2270
int maxFs = std::min<int>(std::numeric_limits<unsigned short>::max(),
maxFsConnections);
- Move RLIM_INFINITY check inside fs::getMaxHandles() before arithmetic
  to prevent overflow (Addresses GitHub Issue stellar#5244)
- Return a bounded value (1,000,000) for unlimited limits
- Replace std::min<int> with std::min<int64_t> for safer casting
- Add explicit logging for unlimited descriptor limit case
- Keep Config::adjust() platform-independent

Resolves stellar#5244

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Suppressed comments (5)

src/main/Config.cpp:2227

  • This cross-platform file references the POSIX-only RLIM_INFINITY macro, which is unavailable on Windows (and is not included here). The branch is also unreachable on POSIX because getMaxHandles() has already converted infinity to 1000000, so the promised debug log never occurs. Remove this redundant branch and, if the log is required, emit it in the POSIX infinity branch in Fs.cpp.
    // 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)

src/main/Config.cpp:2363

  • The deleted block immediately before this function contained the only definitions of logBasicInfo, validateConfig, both parseNodeID overloads, and addValidatorName. These methods remain declared and called (for example, ApplicationImpl.cpp:759 calls logBasicInfo), so restoring those definitions is required to avoid undefined references and to retain config validation/parsing.
void

src/util/Fs.cpp:460

  • Checking only RLIM_INFINITY does not make this arithmetic safe for other very large finite rlim_t values: rlim_cur * 3 can still wrap before division, reproducing the issue's “sufficiently high” limit failure. Compute the three-quarters value without overflowing and cap it before converting to int64_t.
        // Leave some buffer (75%) for other file descriptors.
        // This value is now guaranteed to be safe for arithmetic.
        return (rl.rlim_cur * 3) / 4;

src/main/Config.cpp:2221

  • getMaxHandles() returns int64_t, but long is only 32 bits on Windows and some POSIX targets. A large finite limit can therefore narrow or wrap before the later cap is applied; preserve the API's width here.
    long maxFsConnections = fs::getMaxHandles();

src/util/Fs.cpp:450

  • This regression fix adds distinct finite, infinity, and getrlimit-failure paths but adds no automated coverage in src/util/test/FsTests.cpp. Factor the limit-normalization logic behind a testable helper and cover boundary values around RLIM_INFINITY and the multiplication overflow threshold so this monetary-network daemon does not regress here.

This issue also appears on line 458 of the same file.

        // 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)

Comment thread src/main/Config.cpp Outdated
}

void
vvoid

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Suppressed comments (3)

src/main/Config.cpp:2227

  • RLIM_INFINITY is POSIX-specific, but Config.cpp is also compiled on Windows, where this unguarded reference is undefined and breaks the build. On POSIX this branch is unreachable because getMaxHandles() already converts infinity to 1000000, so the promised debug log cannot fire either. Keep infinity detection/logging inside the POSIX filesystem implementation or return explicit capped-status information from that abstraction.
    if (maxFsConnections == RLIM_INFINITY)

src/util/Fs.cpp:460

  • The arithmetic is not guaranteed safe for a very high but finite limit: rlim_cur * 3 can still wrap in rlim_t before division, and this issue explicitly covers sufficiently high finite values as well as infinity. Apply the chosen cap before any potentially overflowing multiplication.
        // Leave some buffer (75%) for other file descriptors.
        // This value is now guaranteed to be safe for arithmetic.
        return (rl.rlim_cur * 3) / 4;

src/util/Fs.cpp:450

  • This regression fix has no automated coverage, although src/util/test/FsTests.cpp tests the other filesystem APIs. Add cases for RLIM_INFINITY, a near-maximum finite rlim_t, and the getrlimit() failure path; extracting the limit normalization into a pure helper would make these cases deterministic without changing the process hard limit.
        // 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)

Comment thread src/main/Config.cpp
}
}

void
- Apply RLIM_INFINITY check in fs::getMaxHandles()
- Use std::min<int64_t> for safe casting
- Restore accidentally deleted functions (logBasicInfo, validateConfig, etc.)

Resolves stellar#5244

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Suppressed comments (4)

src/main/Config.cpp:2227

  • This check is both non-portable and ineffective: RLIM_INFINITY is a POSIX implementation detail that is unavailable in the Windows build, while fs::getMaxHandles() has already converted infinity to 1000000, so the condition and debug log can never report the unlimited case. Remove this block, or change the abstraction to return infinity status explicitly if the log is required.
    if (maxFsConnections == RLIM_INFINITY)

src/main/Config.cpp:2363

  • This hunk removes the only definitions of logBasicInfo, validateConfig, both parseNodeID overloads, and addValidatorName. Their declarations and active callers remain (for example, ApplicationImpl.cpp:759 calls logBasicInfo and this file calls validateConfig at line 2208), so the build will fail with unresolved symbols. Restore the deleted definitions.
void

src/util/Fs.cpp:455

  • The new unlimited-limit and large-finite-limit arithmetic has no regression coverage, although this module has dedicated tests in src/util/test/FsTests.cpp. Add tests around a factored limit-adjustment helper for RLIM_INFINITY, very large finite values, ordinary limits, and the fallback path; otherwise the remaining finite overflow is easy to miss.
        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;

src/util/Fs.cpp:450

  • The new guard only handles the exact RLIM_INFINITY sentinel. A large finite rlim_cur (for example, RLIM_INFINITY - 1) still overflows in (rl.rlim_cur * 3) / 4 below, so issue #5244 remains for the “sufficiently high” finite limits called out by the issue. Compute three quarters without multiplying first and cap the result before converting it to int64_t.
        // 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)

Comment thread src/main/Config.cpp Outdated
}

void
vvoid

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (5)

src/main/Config.cpp:2227

  • RLIM_INFINITY is only available on POSIX, so referencing it in platform-independent Config.cpp breaks Windows builds. This branch also cannot observe the normal unlimited case because fs::getMaxHandles() has already converted it to 1000000, so the promised debug log is not emitted. Keep the POSIX check and logging inside Fs.cpp, or return explicit limit-status information from the abstraction.
    if (maxFsConnections == RLIM_INFINITY)

src/main/Config.cpp:2363

  • The change removes the only definitions of logBasicInfo, validateConfig, both parseNodeID overloads, and addValidatorName. Their declarations and callers remain (for example, ApplicationImpl.cpp:759 and Config.cpp:2208), so the target will fail to link. Restore these unrelated definitions before parseNodeIDsIntoSet.
void

src/util/Fs.cpp:450

  • The new limit-normalization behavior has no regression coverage, although src/util/test/FsTests.cpp tests this utility module. Please cover unlimited and very large finite limits (and the failure fallback), ideally by extracting the arithmetic into a helper that accepts an rlim_t so these edge cases do not require mutating the process-wide resource limit.
        // 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)

src/util/Fs.cpp:460

  • Checking only RLIM_INFINITY does not make the finite path safe: a large finite rlim_cur can still overflow in rlim_cur * 3, and converting an out-of-range unsigned rlim_t result to int64_t is implementation-defined. Compute the 75% value without multiplying first and clamp it to int64_t before returning.
        // Leave some buffer (75%) for other file descriptors.
        // This value is now guaranteed to be safe for arithmetic.
        return (rl.rlim_cur * 3) / 4;

src/main/Config.cpp:2221

  • getMaxHandles() returns int64_t, but storing it in long narrows on ILP32 platforms before the later cap; a large finite limit can become negative and corrupt the connection adjustment. Preserve the API's width here.
    long maxFsConnections = fs::getMaxHandles();

@EslaM-X

EslaM-X commented Jul 31, 2026

Copy link
Copy Markdown
Author

This has been fixed in the latest commits. Please review again

EslaM-X added 3 commits August 1, 2026 02:13
These tests verify that Config::adjust() handles both unlimited and finite descriptor limits safely, and maintains connection bounds.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

src/main/test/ConfigTests.cpp:921

  • This test never arranges for an unlimited descriptor limit; it simply uses the host's current RLIMIT_NOFILE, so on ordinary CI it duplicates the finite-limit case and cannot catch a regression in Config::adjust()'s new 64-bit-to-int cap. Add an injectable/testable max-handle input (or extract the adjustment calculation) and deterministically exercise both the capped unlimited result and a value above INT_MAX.
    // Call adjust() - this uses fs::getMaxHandles() internally.
    // If the limit is unlimited, it should be capped safely.
    REQUIRE_NOTHROW(cfg.adjust());

Comment thread src/util/Fs.h
Comment on lines +130 to +131
#ifndef _WIN32
int64_t computeSafeMaxHandles(rlim_t limit);

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

src/util/test/FsTests.cpp:106

  • The multiplication by 4 occurs in signed int64_t before the cast, and 4 * (INT64_MAX / 3) exceeds INT64_MAX, so this test invokes undefined behavior. Cast to rlim_t before the multiplication so the arithmetic is performed in the wider unsigned limit type.
    rlim_t nearLimit = static_cast<rlim_t>(std::numeric_limits<int64_t>::max() / 3 * 4) - 1;

src/main/test/ConfigTests.cpp:916

  • currentLimit is only checked for positivity and is never related to the adjusted settings. With the default configuration, this test passes even if Config::adjust() ignores fs::getMaxHandles(), so it does not cover the descriptor-limit behavior its name and comments claim to verify. Add a seam or pure helper that lets the test supply small finite and capped-unlimited limits, then assert the exact adjusted connection counts.
    // Get the current system limit via the abstraction layer.
    int64_t currentLimit = fs::getMaxHandles();
    REQUIRE(currentLimit > 0);

Comment thread src/util/test/FsTests.cpp
Comment on lines +92 to +100
TEST_CASE("computeSafeMaxHandles handles very large finite limits", "[fs]")
{
// Test with a limit that actually triggers clamping.
// Need a value > 4 * INT64_MAX / 3 to force clamping.
// Using 2 * INT64_MAX is safely above the threshold.
rlim_t largeLimit = static_cast<rlim_t>(std::numeric_limits<int64_t>::max()) * 2;
int64_t result = fs::computeSafeMaxHandles(largeLimit);
REQUIRE(result == std::numeric_limits<int64_t>::max());
}
EslaM-X added 2 commits August 1, 2026 20:20
Updated comments in Config::adjust test to clarify limits.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

src/main/test/ConfigTests.cpp:941

  • This assertion does not verify the descriptor-limit behavior: TARGET_PEER_CONNECTIONS is an unsigned short, MAX_ADDITIONAL_PEER_CONNECTIONS is already constrained to USHRT_MAX, and currentLimit is never compared with the adjusted values, so the test passes even if Config::adjust() ignores fs::getMaxHandles(). Add an injectable/helper limit path and test exact adjustment results for a low limit and a value above INT_MAX; that would cover the changed 64-bit capping logic.
    // The maximum possible value is capped by the descriptor limit.
    // Since we can't know the exact limit, we verify that the total is
    // within a reasonable range (at most 2 * USHRT_MAX, which is a safe upper bound).
    REQUIRE(total <= std::numeric_limits<unsigned short>::max() * 2);

Comment thread src/util/test/FsTests.cpp Outdated
Comment on lines +101 to +103
rlim_t largeLimit = std::numeric_limits<rlim_t>::max();
int64_t result = fs::computeSafeMaxHandles(largeLimit);
REQUIRE(result == std::numeric_limits<int64_t>::max());
EslaM-X added 2 commits August 1, 2026 20:29
Refactor largeLimit calculation for clarity and safety checks.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Suppressed comments (3)

src/util/test/FsTests.cpp:211

  • getMaxHandles() may legitimately return 0 when rlim_cur is 0 or 1, as the new unit cases explicitly verify, so this assertion makes the test environment-dependent. Compare against computeSafeMaxHandles() using the current limit (or 64 on query failure) instead of requiring positivity.
    REQUIRE(handles > 0);

src/util/test/FsTests.cpp:192

  • This assertion contradicts the helper's specified behavior for valid POSIX limits: the tests above establish that limits of 0 or 1 produce 0 handles, so this integration test fails whenever the process has such an RLIMIT_NOFILE. The same assumption is repeated in the POSIX-specific test below. Remove this redundant generic test or make it conditional on the platform's actual limit.

This issue also appears on line 211 of the same file.

    REQUIRE(handles > 0);

src/main/test/ConfigTests.cpp:920

  • This host-dependent smoke test does not exercise the Config::adjust() overflow/narrowing path fixed by this PR: on normal CI it only uses the host's finite limit, and its range checks on the unsigned short fields are tautologies. Add a deterministic seam that supplies an unlimited/high int64_t handle count and assert the resulting connection budget, so a regression in the changed std::min<int64_t> logic is caught.
    // Call adjust() - this uses fs::getMaxHandles() internally.
    // The function should not throw any exceptions.
    REQUIRE_NOTHROW(cfg.adjust());

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/main/test/ConfigTests.cpp:918

  • This test does not exercise the descriptor-limit adjustment: REQUIRE_NOTHROW also passes with the old overflowing conversion, and the assertions below are type-range tautologies for the unsigned short fields (the old overflow path produces small values that satisfy all of them). Please make the descriptor budget injectable or extract the budget-dependent part of adjust(), then assert the resulting connection counts for an INT64_MAX/unlimited-derived value so the narrowing fix can regress only by failing this test.
    REQUIRE_NOTHROW(cfg.adjust());

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/util/test/FsTests.cpp:211

  • This assertion is guaranteed by handles being int64_t, so it cannot verify that getMaxHandles() delegates to the helper as the test claims. Compare the result with computeSafeMaxHandles(rl.rlim_cur) (or 64 on failure) to make the integration test detect a broken delegation.
    auto handles = fs::getMaxHandles();
    REQUIRE(handles <= std::numeric_limits<int64_t>::max());

src/main/test/ConfigTests.cpp:908

  • The helper tests do not exercise the changed narrowing logic in Config::adjust(). A regression from std::min<int64_t> back to an int conversion would still pass every FsTests case, so the Config path implicated by #5244 remains uncovered. Add a seam for supplying the handle limit and test adjust() with a value above INT_MAX instead of intentionally omitting this coverage.
// Note: Tests for Config::adjust() descriptor limit handling (Issue #5244)
// are intentionally omitted because Config::adjust() relies on
// fs::getMaxHandles() which is thoroughly tested in FsTests.cpp.
// The helper computeSafeMaxHandles() covers all boundary cases including
// RLIM_INFINITY and large finite values with exact assertions.
// Therefore, no separate test for Config::adjust is needed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Audit Config::adjust() logic

2 participants