From b08662060db7b13bb98b46d90f1b023be7324530 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Fri, 10 Jul 2026 11:49:46 -0400 Subject: [PATCH] init: account for maximum file descriptors needed by HTTP --- doc/REST-interface.md | 12 -------- doc/release-notes-35182.md | 4 ++- src/init.cpp | 49 ++++++++++++++++++++++++++++++--- test/functional/feature_init.py | 47 +++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 17 deletions(-) diff --git a/doc/REST-interface.md b/doc/REST-interface.md index 9b8f11d241c..91fa03995ae 100644 --- a/doc/REST-interface.md +++ b/doc/REST-interface.md @@ -31,18 +31,6 @@ If you front `bitcoind` with a reverse proxy or CDN such as Caddy or nginx with the headers-more module, you can override these defaults there. Keep overrides scoped to responses you know are safe to cache more aggressively. -Limitations ------------ - -There is a known issue in the REST interface that can cause a node to crash if -too many http connections are being opened at the same time because the system runs -out of available file descriptors. To prevent this from happening you might -want to increase the number of maximum allowed file descriptors in your system -and try to prevent opening too many connections to your rest interface at the -same time if this is under your control. It is hard to give general advice -since this depends on your system but if you make several hundred requests at -once you are definitely at risk of encountering this issue. - Supported API ------------- diff --git a/doc/release-notes-35182.md b/doc/release-notes-35182.md index c32a922cb2b..d095d2fb0c6 100644 --- a/doc/release-notes-35182.md +++ b/doc/release-notes-35182.md @@ -16,4 +16,6 @@ Certain HTTP edge cases will observe different behavior to be more RFC-compliant - Multiple "Content-Length" headers with different values are rejected A new configuration option `-rpcmaxconnections` (default `16`) limits the -number of simultaneously connected HTTP clients to the server. +number of simultaneously connected HTTP clients to the server. The application +will now attempt to reserve file descriptors for the HTTP server sockets. If your +system has limited resources, consider using a lower setting. diff --git a/src/init.cpp b/src/init.cpp index 5f9e0b92daf..e4cd2f8ae0c 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -114,6 +114,7 @@ #include #include #include +#include #include #include #include @@ -1075,18 +1076,58 @@ bool AppInitParameterInteraction(const ArgsManager& args) const size_t max_private{args.GetBoolArg("-privatebroadcast", DEFAULT_PRIVATE_BROADCAST) ? MAX_PRIVATE_BROADCAST_CONNECTIONS : 0}; - // Reserve enough FDs to account for the bare minimum, plus any manual connections, plus the bound interfaces - int min_required_fds = MIN_CORE_FDS + MAX_ADDNODE_CONNECTIONS + num_p2p_bind; - // Try raising the FD limit to what we need (available_fds may be smaller than the requested amount if this fails) - available_fds = RaiseFileDescriptorLimit(user_p2p_max_connections + max_private + min_required_fds); + // HTTP server listen sockets: by default two (IPv4 and IPv6 loopback), or one per -rpcbind entry + int num_rpc_bind = std::max(args.GetArgs("-rpcbind").size(), size_t(2)); + // HTTP server connected client sockets + int user_rpc_max_connections = args.GetArg("-rpcmaxconnections", DEFAULT_MAX_HTTP_CONNECTIONS); + if (user_rpc_max_connections < 1) { + return InitError(Untranslated("-rpcmaxconnections must be greater than zero. Use -server=0 to disable HTTP.")); + } + + // Reserve enough FDs to account for the bare minimum, plus any manual connections, plus the bound interfaces. + // Every element is an int >= 0 so summing in int64_t cannot overflow. + // RaiseFileDescriptorLimit() accepts an int so we check that limit before casting. + const int64_t total_fds = int64_t{MIN_CORE_FDS} + + MAX_ADDNODE_CONNECTIONS + + num_p2p_bind + + num_rpc_bind + + user_rpc_max_connections + + user_p2p_max_connections + + static_cast(max_private); + if (total_fds > std::numeric_limits::max()) { + return InitError(Untranslated("Too many file descriptors requested. Try lower values for -rpcmaxconnections " + "or -maxconnections, or fewer settings of " + "-rpcbind, -bind and -whitebind")); + } + + // Subset of total_fds must also be a safe int + int min_required_fds = MIN_CORE_FDS + + MAX_ADDNODE_CONNECTIONS + + num_p2p_bind + + num_rpc_bind + + user_rpc_max_connections; + + // Try raising the FD limit to what the user wants (available_fds may be smaller than the requested amount if this fails) + available_fds = RaiseFileDescriptorLimit(static_cast(total_fds)); // If we are using select instead of poll, our actual limit may be even smaller #ifndef USE_POLL available_fds = std::min(FD_SETSIZE, available_fds); #endif + // The system can't support our bare minimum if (available_fds < min_required_fds) return InitError(strprintf(_("Not enough file descriptors available. %d available, %d required."), available_fds, min_required_fds)); + // The system can support our minimum but not the full amount the user requested. + if (available_fds < total_fds) { + // If the user is requesting extra HTTP connections, abort. They need to change that. + if (user_rpc_max_connections > DEFAULT_MAX_HTTP_CONNECTIONS) { + return InitError(strprintf(_("Not enough file descriptors available. " + "Try reducing -rpcmaxconnections or using the default value of %d"), + DEFAULT_MAX_HTTP_CONNECTIONS)); + } + } + // Trim requested connection counts, to fit into system limitations num_p2p_max_connections = std::min(available_fds - min_required_fds, user_p2p_max_connections); diff --git a/test/functional/feature_init.py b/test/functional/feature_init.py index 18faba33b3b..9277bc4fed8 100755 --- a/test/functional/feature_init.py +++ b/test/functional/feature_init.py @@ -7,6 +7,7 @@ from concurrent.futures import ThreadPoolExecutor from pathlib import Path import os import platform +import re import shutil import signal import subprocess @@ -362,6 +363,51 @@ class InitTest(BitcoinTestFramework): self.log.info("Testing node startup with fd limit above INT_MAX") self.restart_node_with_fd_limit(1 << 31) + def init_fd_overflow_test(self): + node = self.nodes[1] + if node.running: + self.stop_node(1) + + # A value larger than any possible int saturates to INT_MAX during arg parsing. + # Adding in other file descriptor requirements is guaranteed to overflow, + # so expect an InitError before RaiseFileDescriptorLimit() is called. + self.log.info("Checking -rpcmaxconnections setting that would overflow int is rejected") + node.assert_start_raises_init_error( + extra_args=[f"-rpcmaxconnections={2**64}"], + expected_msg="Error: Too many file descriptors requested.", + match=ErrorMatch.PARTIAL_REGEX + ) + + if self.RLIM_INFINITY is not None: + # Get the platform's file descriptor limit, if possible + import resource + soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) + + # Lower the hard limit so RaiseFileDescriptorLimit() has a ceiling. + # The hard limit can not be raised again without root privilges, + # so this test should always be left for last in the process. + try: + resource.setrlimit(resource.RLIMIT_NOFILE, (soft, soft)) + except (ValueError, OSError): + self.log.info(f"Skipping rlimit test: cannot reduce hard limit (soft={soft}, hard={hard})") + return + + self.log.info("Checking that large -maxconnections setting gets adjusted for available file descriptors") + # Note this prints a message to the log and stderr but does not abort the process + with node.assert_debug_log(expected_msgs=[f"Reducing -maxconnections from {soft} "]): + self.restart_node(1, extra_args=[f"-maxconnections={soft}"]) + self.stop_node(1, expected_stderr=re.compile(fr"Reducing -maxconnections from {soft} ")) + + # From httpserver.h + DEFAULT_MAX_HTTP_CONNECTIONS = 16 + + self.log.info("Checking -rpcmaxconnections gets blamed if available file descriptors are insufficient") + node.assert_start_raises_init_error( + extra_args=[f"-rpcmaxconnections={DEFAULT_MAX_HTTP_CONNECTIONS + 1}", f"-maxconnections={soft}"], + expected_msg="Not enough file descriptors available. Try reducing -rpcmaxconnections", + match=ErrorMatch.PARTIAL_REGEX + ) + def run_test(self): self.init_pid_test() self.init_stress_test_interrupt() @@ -370,6 +416,7 @@ class InitTest(BitcoinTestFramework): self.init_empty_test() self.init_rlimit_test() self.init_rlimit_large_test() + self.init_fd_overflow_test() if __name__ == '__main__':