Fix startup failure with RLIM_INFINITY fd limits
What changed, and why it matters
This commit fixes a bug where Bitcoin Core would refuse to start if the user's system was configured to allow an unlimited number of open files ('ulimit -n unlimited'). The program incorrectly treated 'unlimited' as -1 available file descriptors, then reported 'Not enough file descriptors available' and exited. The fix makes the program recognize 'unlimited' as the maximum integer value instead, so startup succeeds. This is a reliability/availability fix, not a security vulnerability that an attacker can exploit.
Apply the patch to restore correct startup behavior on systems with unlimited file-descriptor limits. No urgent security response is needed; treat as a normal bug fix.
Security signals we found
Integer conversion/casting issue: RLIM_INFINITY cast to int yields -1
Denial-of-service-like symptom: local node fails to start under a legitimate system configuration
No attacker-controlled input or privilege boundary crossed
Fix is localized to resource-limit handling with added bounds/Asserts
Evidence from the diff
RaiseFileDescriptorLimit() in src/util/fs_helpers.cpp previously returned limitFD.rlim_cur as an int. When the soft RLIMIT_NOFILE was RLIM_INFINITY, the cast to int produced -1 on platforms where RLIM_INFINITY is defined as -1 (e.g., typical Linux glibc). The caller interpreted this as fewer than the required 160 descriptors and aborted startup. The patch adds an explicit RLIM_INFINITY check and returns std::numeric_limits
Changed components
src/util/fs_helpers.cpp: RaiseFileDescriptorLimit()src/util/fs_helpers.h: RaiseFileDescriptorLimit() declaration/documentationtest/functional/feature_init.py: new init_rlimit_test() and helpertest/functional/test_framework/test_framework.py: resource module availability helpersInspect captured patch +74 / −12
diff --git a/src/util/fs_helpers.cpp b/src/util/fs_helpers.cpp
index a41bf65a..3efc9fda 100644
--- a/src/util/fs_helpers.cpp
+++ b/src/util/fs_helpers.cpp
@@ -9,12 +9,14 @@
#include <sync.h>
#include <util/byte_units.h> // IWYU pragma: keep
+#include <util/check.h>
#include <util/fs.h>
#include <util/log.h>
#include <util/syserror.h>
#include <cerrno>
#include <fstream>
+#include <limits>
#include <map>
#include <memory>
#include <optional>
@@ -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<rlim_t>(std::numeric_limits<int>::max()));
+ limitFD.rlim_cur = static_cast<rlim_t>(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<int>::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 face17fd..b84fddef 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<int>::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 259e07b1..ee28a287 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 067388c6..fdb80904 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):
Why this scored 29/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.