From 4afbabdcef86c095b19b3b42b70a2483db8cab4a Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 31 Mar 2026 10:20:59 +0200 Subject: [PATCH 1/3] Fix startup failure with RLIM_INFINITY fd limits When setting the fd limit to unlimited, the node fails to start: ulimit -n unlimited build/bin/bitcoind Error: Not enough file descriptors available. -1 available, 160 required. This was caused by RaiseFileDescriptorLimit() casting limitFD.rlim_cur to int, which for RLIM_INFINITY overflows to -1. Fix it by returning std::numeric_limits::max() instead. This commit also adds a functional test, which is skipped on environments with a hard limit below infinity. Co-authored-by: Luke Dashjr Co-authored-by: winterrdog --- src/util/fs_helpers.cpp | 35 +++++++++++++------ src/util/fs_helpers.h | 13 ++++++- test/functional/feature_init.py | 26 ++++++++++++++ .../test_framework/test_framework.py | 12 +++++++ 4 files changed, 74 insertions(+), 12 deletions(-) diff --git a/src/util/fs_helpers.cpp b/src/util/fs_helpers.cpp index a41bf65ad86..3efc9fda4b3 100644 --- a/src/util/fs_helpers.cpp +++ b/src/util/fs_helpers.cpp @@ -9,12 +9,14 @@ #include #include // IWYU pragma: keep +#include #include #include #include #include #include +#include #include #include #include @@ -152,27 +154,38 @@ bool TruncateFile(FILE* file, unsigned int length) #endif } -/** - * this function tries to raise the file descriptor limit to the requested number. - * It returns the actual file descriptor limit (which may be more or less than nMinFD) - */ -int RaiseFileDescriptorLimit(int nMinFD) +int RaiseFileDescriptorLimit(int min_fd) { + Assert(min_fd >= 0); #if defined(WIN32) return 2048; #else struct rlimit limitFD; if (getrlimit(RLIMIT_NOFILE, &limitFD) != -1) { - if (limitFD.rlim_cur < (rlim_t)nMinFD) { - limitFD.rlim_cur = nMinFD; - if (limitFD.rlim_cur > limitFD.rlim_max) + // If the current soft limit is already higher, don't raise it + if (limitFD.rlim_cur != RLIM_INFINITY && std::cmp_less(limitFD.rlim_cur, min_fd)) { + const auto current_limit{limitFD.rlim_cur}; + static_assert(std::in_range(std::numeric_limits::max())); + limitFD.rlim_cur = static_cast(min_fd); + // Don't raise soft limit beyond hard limit + if ((limitFD.rlim_max != RLIM_INFINITY) && (limitFD.rlim_cur > limitFD.rlim_max)) { limitFD.rlim_cur = limitFD.rlim_max; - setrlimit(RLIMIT_NOFILE, &limitFD); - getrlimit(RLIMIT_NOFILE, &limitFD); + } + if (current_limit != limitFD.rlim_cur) { + setrlimit(RLIMIT_NOFILE, &limitFD); + getrlimit(RLIMIT_NOFILE, &limitFD); + } + } + // Check the (possibly raised) current soft limit against the special + // value of RLIM_INFINITY. Some platforms implement this as the maximum + // uint64, others as int64 (-1). Avoid casting even if the return type + // is changed to uint64_t. + if (limitFD.rlim_cur == RLIM_INFINITY) { + return std::numeric_limits::max(); } return limitFD.rlim_cur; } - return nMinFD; // getrlimit failed, assume it's fine + return min_fd; // getrlimit failed, assume it's fine #endif } diff --git a/src/util/fs_helpers.h b/src/util/fs_helpers.h index face17fd8b5..b84fddef4b8 100644 --- a/src/util/fs_helpers.h +++ b/src/util/fs_helpers.h @@ -45,7 +45,18 @@ bool FileCommit(FILE* file); void DirectoryCommit(const fs::path& dirname); bool TruncateFile(FILE* file, unsigned int length); -int RaiseFileDescriptorLimit(int nMinFD); + +/** + * Try to raise the file descriptor limit to the requested number. + * + * @param[in] min_fd The requested minimum number of file descriptors. + * @returns The actual file descriptor limit. It may be lower or + * higher than min_fd. Returns std::numeric_limits::max() + * if the OS imposes no limit (RLIM_INFINITY). + * + */ +int RaiseFileDescriptorLimit(int min_fd); + void AllocateFileRange(FILE* file, unsigned int offset, unsigned int length); /** diff --git a/test/functional/feature_init.py b/test/functional/feature_init.py index 259e07b1869..ee28a2871a5 100755 --- a/test/functional/feature_init.py +++ b/test/functional/feature_init.py @@ -323,12 +323,38 @@ class InitTest(BitcoinTestFramework): for option in options: self.restart_node(1, option) + def restart_node_with_fd_limit(self, limit): + """Restart node 1 with a given soft RLIMIT_NOFILE. Skips if the limit cannot be set.""" + import resource + soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) + try: + resource.setrlimit(resource.RLIMIT_NOFILE, (limit, hard)) + except (ValueError, OSError): + self.log.info(f"Skipping rlimit test: cannot set soft limit (hard={hard})") + return + try: + self.restart_node(1) + self.log.debug(f"Node started successfully with RLIM_INFINITY limit (soft={limit})") + finally: + resource.setrlimit(resource.RLIMIT_NOFILE, (soft, hard)) + self.log.debug(f"Restored previous RLIMIT_NOFILE limits (soft={soft}, hard={hard})") + + def init_rlimit_test(self): + """Test that bitcoind starts correctly when the soft RLIMIT_NOFILE limit is RLIM_INFINITY.""" + if self.RLIM_INFINITY is None: + self.log.info("Skipping: resource module not available") + return + + self.log.info("Testing node startup with RLIM_INFINITY fd limit") + self.restart_node_with_fd_limit(self.RLIM_INFINITY) + def run_test(self): self.init_pid_test() self.init_stress_test_interrupt() self.init_stress_test_removals() self.break_wait_test() self.init_empty_test() + self.init_rlimit_test() if __name__ == '__main__': diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index 067388c6f67..fdb8090433e 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -8,6 +8,7 @@ import configparser from enum import Enum import argparse from datetime import datetime, timezone +from importlib.util import find_spec import logging import os from pathlib import Path @@ -948,6 +949,17 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): if not self.has_previous_releases(): raise SkipTest("previous releases not available or disabled") + def has_resource_module(self): + """Checks whether the resource module is available.""" + return find_spec('resource') is not None + + @property + def RLIM_INFINITY(self): + if not self.has_resource_module(): + return None + import resource + return resource.RLIM_INFINITY + def has_previous_releases(self): """Checks whether previous releases are present and enabled.""" if not os.path.isdir(self.options.previous_releases_path): From 8ab4b9fc856433ebcaaefb31524cb71ef8ff8089 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 31 Mar 2026 10:21:18 +0200 Subject: [PATCH 2/3] init: clamp fd limits to int When setting the fd limit to 1 >> 31, the node fails to start: ulimit -n 214748364 build/bin/bitcoind Error: Not enough file descriptors available. -2147483648 available, 160 required. Similar to the previous commit, this is fixed by capping the limit to std::numeric_limits::max(). Co-authored-by: Luke Dashjr --- src/util/fs_helpers.cpp | 8 +++++--- test/functional/feature_init.py | 10 ++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/util/fs_helpers.cpp b/src/util/fs_helpers.cpp index 3efc9fda4b3..e2b4c899c99 100644 --- a/src/util/fs_helpers.cpp +++ b/src/util/fs_helpers.cpp @@ -179,11 +179,13 @@ int RaiseFileDescriptorLimit(int min_fd) // Check the (possibly raised) current soft limit against the special // value of RLIM_INFINITY. Some platforms implement this as the maximum // uint64, others as int64 (-1). Avoid casting even if the return type - // is changed to uint64_t. - if (limitFD.rlim_cur == RLIM_INFINITY) { + // is changed to uint64_t. We also cap unlikely but possible values + // that would overflow int. + if (limitFD.rlim_cur == RLIM_INFINITY || + std::cmp_greater_equal(limitFD.rlim_cur, std::numeric_limits::max())) { return std::numeric_limits::max(); } - return limitFD.rlim_cur; + return static_cast(limitFD.rlim_cur); } return min_fd; // getrlimit failed, assume it's fine #endif diff --git a/test/functional/feature_init.py b/test/functional/feature_init.py index ee28a2871a5..e7c226b2556 100755 --- a/test/functional/feature_init.py +++ b/test/functional/feature_init.py @@ -348,6 +348,15 @@ class InitTest(BitcoinTestFramework): self.log.info("Testing node startup with RLIM_INFINITY fd limit") self.restart_node_with_fd_limit(self.RLIM_INFINITY) + def init_rlimit_large_test(self): + """Test that bitcoind starts correctly when the soft RLIMIT_NOFILE limit is above INT_MAX.""" + if self.RLIM_INFINITY is None: + self.log.info("Skipping: resource module not available") + return + + self.log.info("Testing node startup with fd limit above INT_MAX") + self.restart_node_with_fd_limit(1 << 31) + def run_test(self): self.init_pid_test() self.init_stress_test_interrupt() @@ -355,6 +364,7 @@ class InitTest(BitcoinTestFramework): self.break_wait_test() self.init_empty_test() self.init_rlimit_test() + self.init_rlimit_large_test() if __name__ == '__main__': From 735b25519aad2d3c24345c2d6f69c30d1040f473 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 31 Mar 2026 10:21:31 +0200 Subject: [PATCH 3/3] support: clamp RLIMIT_MEMLOCK to size_t On 32-bit systems we build with _FILE_OFFSET_BITS=64 (see CMakeLists.txt), which makes rlim_t 64-bit when building against glibc (see bits/resource.h). Since size_t could be 32-bit, clamp RLIMIT_MEMLOCK to std::numeric_limits::max() in PosixLockedPageAllocator::GetLimit(). Co-authored-by: Luke Dashjr --- src/support/lockedpool.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/support/lockedpool.cpp b/src/support/lockedpool.cpp index ff3a9e69c58..97f0df409fb 100644 --- a/src/support/lockedpool.cpp +++ b/src/support/lockedpool.cpp @@ -262,7 +262,8 @@ size_t PosixLockedPageAllocator::GetLimit() #ifdef RLIMIT_MEMLOCK struct rlimit rlim; if (getrlimit(RLIMIT_MEMLOCK, &rlim) == 0) { - if (rlim.rlim_cur != RLIM_INFINITY) { + if (rlim.rlim_cur != RLIM_INFINITY && + std::cmp_less_equal(rlim.rlim_cur, static_cast(std::numeric_limits::max()))) { return rlim.rlim_cur; } }