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; } } diff --git a/src/util/fs_helpers.cpp b/src/util/fs_helpers.cpp index 097fbef9af3..bc0179b55ac 100644 --- a/src/util/fs_helpers.cpp +++ b/src/util/fs_helpers.cpp @@ -10,12 +10,14 @@ #include #include #include // IWYU pragma: keep +#include #include #include #include #include #include +#include #include #include #include @@ -154,27 +156,40 @@ 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); + } } - return limitFD.rlim_cur; + // 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. 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 static_cast(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 f4d406f75f0..a2399d4e97f 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 526fe63a2bc..ad20e39cda8 100755 --- a/test/functional/feature_init.py +++ b/test/functional/feature_init.py @@ -323,12 +323,48 @@ 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 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() self.init_stress_test_removals() self.break_wait_test() self.init_empty_test() + self.init_rlimit_test() + self.init_rlimit_large_test() if __name__ == '__main__': diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index 8f5fb2397ba..64dcbfd7ec5 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 @@ -1082,6 +1083,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):