Fix startup failure with RLIM_INFINITY fd limits #34937

pull Sjors wants to merge 3 commits into bitcoin:master from Sjors:2026/03/file-descriptor-limit changing 5 files +79 −11
  1. Sjors commented at 12:49 pm on March 27, 2026: member

    When setting the fd limit to unlimited, the node fails to start:

    0ulimit -n unlimited
    1build/bin/bitcoind
    2Error: Not enough file descriptors available. -1 available, 160 required.
    

    This was caused by RaiseFileDescriptorLimit() (introduced in #2568) casting limitFD.rlim_cur to int, which for RLIM_INFINITY overflows to -1. Fix it by returning std::numeric_limits<int>::max() instead.

    Some platforms implement RLIM_INFINITY as the maximum uint64, others as int64 (-1). So simply changing the return type to uint64_t wouldn’t work.

    Similarly, though unlikely to actually happen:

    0ulimit -n 214748364
    1build/bin/bitcoind
    2Error: Not enough file descriptors available. -2147483648 available, 160 required.
    

    The second commit expands the fix by clamping all values above std::numeric_limits<int>::max() instead of letting them overflow.

    This PR also expands test/functional/feature_init.py to cover these, using resource.setrlimit. The check is skipped on environments with a hard limit below infinity (or that don’t have the Python Resource module).

    macOS by default has a hard limit of infinity, but on e.g. Ubuntu the default hard limit is 524288.

    The third commit applies a similar fix to PosixLockedPageAllocator::GetLimit() for 32-bit systems, but without a test.

  2. DrahtBot commented at 12:50 pm on March 27, 2026: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Reviews

    See the guideline for information on the review process. A summary of reviews will appear here.

    LLM Linter (✨ experimental)

    Possible typos and grammar issues:

    • Node started successfully with RLIM_INFINITY limit (soft={limit}) -> Node started successfully with fd limit (soft={limit}) and ensure it’s an f-string [the {limit} placeholder won’t be substituted (not an f"..." string), and the message hard-codes RLIM_INFINITY even when the function is called with other limits, making the intended meaning unclear]

    2026-03-27 19:01:29

  3. Sjors force-pushed on Mar 27, 2026
  4. DrahtBot added the label CI failed on Mar 27, 2026
  5. Sjors commented at 1:00 pm on March 27, 2026: member

    In case you wonder how I ran into this…

    While playing with the OpenCode and Claude terminal applications (on macOS), I noticed they started setting ulimit -n ... to get the functional tests running.

    It turns out these both use the Bun JavaScript runtime which for some reason sets RLIM_INFINITY.

    To see for yourself, create ulimit-demo.ts with:

    0const proc = Bun.spawn(["sh", "-c", "ulimit -n"], {
    1    stdout: "pipe",
    2    stderr: "pipe",
    3});
    4
    5const output = await new Response(proc.stdout).text();
    6await proc.exited;
    7
    8console.log("ulimit -n:", output.trim());
    
    0% ulimit -n
    1256
    2$ % bun ulimit-demo.ts
    3ulimit -n: unlimited
    

    Codex terminal is written in Rust and just keeps the system values.

    Codex UI on the other hand returns 1048575, just like VSCode, which is Electron behavior.

  6. in src/init.cpp:1047 in 49a2ce1dcb
    1041@@ -1041,7 +1042,9 @@ bool AppInitParameterInteraction(const ArgsManager& args)
    1042     int min_required_fds = MIN_CORE_FDS + MAX_ADDNODE_CONNECTIONS + nBind;
    1043 
    1044     // Try raising the FD limit to what we need (available_fds may be smaller than the requested amount if this fails)
    1045-    available_fds = RaiseFileDescriptorLimit(user_max_connection + max_private + min_required_fds);
    1046+    available_fds = std::min<uint64_t>(
    1047+        RaiseFileDescriptorLimit(user_max_connection + max_private + min_required_fds),
    1048+        std::numeric_limits<int>::max());
    


    Sjors commented at 1:23 pm on March 27, 2026:

    A lot of downstream code uses int. Changing all that would be a massive diff.

    An alternative approach is to keep RaiseFileDescriptorLimit use int and then have it special case RLIM_INFINITY.

    Not sure where the right boundary is.

    Note that PosixLockedPageAllocator::GetLimit() uses size_t.

  7. DrahtBot removed the label CI failed on Mar 27, 2026
  8. in src/util/fs_helpers.cpp:161 in 49a2ce1dcb outdated
    163 #else
    164     struct rlimit limitFD;
    165     if (getrlimit(RLIMIT_NOFILE, &limitFD) != -1) {
    166-        if (limitFD.rlim_cur < (rlim_t)nMinFD) {
    167-            limitFD.rlim_cur = nMinFD;
    168+        if (limitFD.rlim_cur < static_cast<rlim_t>(min_fd)) {
    


    luke-jr commented at 2:34 pm on March 27, 2026:
    Won’t this still attempt to “raise” RLIM_INFINITY?

    Sjors commented at 2:49 pm on March 27, 2026:

    I don’t think it would:

    0rlim_cur < static_cast<rlim_t>(min_fd))
    1  ^                               ^----- number we requested
    2  | 
    3  | RLIM_INFINITY
    

    So if(...) is false.


    luke-jr commented at 3:41 pm on March 27, 2026:
    RLIM_INFINITY is typically (but not guaranteed to be) -1, which is less than any requested limit…

    Sjors commented at 3:46 pm on March 27, 2026:

    On macOS it’s:

    0#define RLIM_INFINITY   (((__uint64_t)1 << 63) - 1)     /* no limit */
    

    But if other platforms define it as -1 (signed integer) then that’s indeed a problem.


    Sjors commented at 4:01 pm on March 27, 2026:
    In any case, we shouldn’t make assumptions about how rlim_t is implemented. I’ll try a different approach.
  9. Sjors marked this as a draft on Mar 27, 2026
  10. Sjors force-pushed on Mar 27, 2026
  11. Sjors marked this as ready for review on Mar 27, 2026
  12. Sjors commented at 4:33 pm on March 27, 2026: member
    I switched the return type back to int and instead added an early return std::numeric_limits<int>::max() for RLIM_INFINITY. See #34937 (review)
  13. 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<int>::max() instead.
    
    This commit also adds a functional test, which is skipped on environments
    with a hard limit below infinity.
    3cd0cbdc9e
  14. Sjors force-pushed on Mar 27, 2026
  15. Sjors commented at 5:30 pm on March 27, 2026: member
    Added a second commit for the (very unlikely in practice) int overflow case.
  16. Sjors commented at 6:32 pm on March 27, 2026: member
    Applied a similar fix to PosixLockedPageAllocator::GetLimit(), in this case because _FILE_OFFSET_BITS=64 makes rlim_t 64-bit whereas size_t might only be 32-bit. I did not bother with a test for this one, nor do I have recipe to hit the condition.
  17. 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<int>::max().
    af56e801fc
  18. 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 (see bits/resource.h in glibc). Since size_t
    could be 32-bit, clamp RLIMIT_MEMLOCK to std::numeric_limits<size_t>::max()
    in PosixLockedPageAllocator::GetLimit().
    e48f6b7a8a
  19. in src/util/fs_helpers.cpp:162 in 91b7198573 outdated
    164     struct rlimit limitFD;
    165     if (getrlimit(RLIMIT_NOFILE, &limitFD) != -1) {
    166-        if (limitFD.rlim_cur < (rlim_t)nMinFD) {
    167-            limitFD.rlim_cur = nMinFD;
    168+        if (limitFD.rlim_cur == RLIM_INFINITY ||
    169+            limitFD.rlim_cur >= static_cast<rlim_t>(std::numeric_limits<int>::max())) {
    


    luke-jr commented at 6:38 pm on March 27, 2026:
    The cast could mess things up if sizeof(int) > sizeof(rlim_t)

    Sjors commented at 6:52 pm on March 27, 2026:

    Is that possible? IIUC sizeof(int) is at least 32 bit on 32-bit systems and at least 64 bit on 64-bit systems.

    Whereas rlim_t is either 32 or 64 bit: https://sourceware.org/git/?p=glibc.git;a=blob;f=bits/resource.h;h=c2d25794d67a4eb574776c2ec77e3a92342851c0;hb=refs/heads/release/2.31/master#l94 and it’s 64 bit for us because of _FILE_OFFSET_BITS=64.

    I could added a static_assert to check.


    Sjors commented at 7:18 pm on March 27, 2026:
    There’s also some constraints enforced by compat/assumptions.h, but not this specific check.

    luke-jr commented at 8:00 pm on March 27, 2026:

    sizeof(int) is only guaranteed to be 15-bit, but typically 31-bit.

    When sizeof(rlim_t) is 64-bit (also normal), we already hit a casting issue…


    luke-jr commented at 8:16 pm on March 27, 2026:
    std::cmp_greater_equal feels like the right tool for this job
  20. Sjors force-pushed on Mar 27, 2026
  21. DrahtBot added the label CI failed on Mar 27, 2026
  22. Sjors commented at 7:02 pm on March 27, 2026: member
    Added static_assert(std::numeric_limits<rlim_t>::digits >= std::numeric_limits<int>::digits); to see if that assumption at least holds for all our CI platforms, see #34937 (review)
  23. DrahtBot removed the label CI failed on Mar 27, 2026
  24. luke-jr commented at 9:15 pm on March 27, 2026: member

    Dotting all the ‘i’s, I ended up with this:

     0diff --git a/src/support/lockedpool.cpp b/src/support/lockedpool.cpp
     1index 0841802e753..4b683e35e0b 100644
     2--- a/src/support/lockedpool.cpp
     3+++ b/src/support/lockedpool.cpp
     4@@ -261,14 +261,12 @@ void PosixLockedPageAllocator::FreeLocked(void* addr, size_t len)
     5 size_t PosixLockedPageAllocator::GetLimit()
     6 {
     7 #ifdef RLIMIT_MEMLOCK
     8-    static_assert(std::numeric_limits<rlim_t>::digits >= std::numeric_limits<size_t>::digits);
     9     struct rlimit rlim;
    10     if (getrlimit(RLIMIT_MEMLOCK, &rlim) == 0) {
    11-        if (rlim.rlim_cur == RLIM_INFINITY ||
    12-            rlim.rlim_cur >= static_cast<rlim_t>(std::numeric_limits<size_t>::max())) {
    13-            return std::numeric_limits<size_t>::max();
    14+        if (rlim.rlim_cur != RLIM_INFINITY &&
    15+            std::cmp_less_equal(rlim.rlim_cur, static_cast<rlim_t>(std::numeric_limits<size_t>::max()))) {
    16+            return rlim.rlim_cur;
    17         }
    18-        return rlim.rlim_cur;
    19     }
    20 #endif
    21     return std::numeric_limits<size_t>::max();
    22diff --git a/src/util/fs_helpers.cpp b/src/util/fs_helpers.cpp
    23index 1cf85e6ad13..10c7cc61266 100644
    24--- a/src/util/fs_helpers.cpp
    25+++ b/src/util/fs_helpers.cpp
    26@@ -164,25 +164,24 @@ int RaiseFileDescriptorLimit(int min_fd)
    27 #if defined(WIN32)
    28     return 2048;
    29 #else
    30-    static_assert(std::numeric_limits<rlim_t>::digits >= std::numeric_limits<int>::digits);
    31     struct rlimit limitFD;
    32     if (getrlimit(RLIMIT_NOFILE, &limitFD) != -1) {
    33+        if (limitFD.rlim_cur != RLIM_INFINITY && std::cmp_less(limitFD.rlim_cur, min_fd)) {
    34+            const auto current_limit{limitFD.rlim_cur};
    35+            limitFD.rlim_cur = std::in_range<rlim_t>(min_fd) ? static_cast<rlim_t>(min_fd) : RLIM_INFINITY;
    36+            if (limitFD.rlim_max != RLIM_INFINITY && (limitFD.rlim_cur == RLIM_INFINITY || limitFD.rlim_cur > limitFD.rlim_max)) {
    37+                limitFD.rlim_cur = limitFD.rlim_max;
    38+            }
    39+            if (current_limit != limitFD.rlim_cur) {
    40+                setrlimit(RLIMIT_NOFILE, &limitFD);
    41+                getrlimit(RLIMIT_NOFILE, &limitFD);
    42+            }
    43+        }
    44         if (limitFD.rlim_cur == RLIM_INFINITY ||
    45-            limitFD.rlim_cur >= static_cast<rlim_t>(std::numeric_limits<int>::max())) {
    46-            // Some platforms implement RLIM_INFINITY as the maximum uint64,
    47-            // others as int64 (-1). Avoid casting even if the return type
    48-            // is changed to uint64_t. We also cap unlikely but possible values
    49-            // that would overflow int.
    50+            std::cmp_greater_equal(limitFD.rlim_cur, std::numeric_limits<int>::max())) {
    51             return std::numeric_limits<int>::max();
    52         }
    53-        if (limitFD.rlim_cur < static_cast<rlim_t>(min_fd)) {
    54-            limitFD.rlim_cur = static_cast<rlim_t>(min_fd);
    55-            if (limitFD.rlim_cur > limitFD.rlim_max)
    56-                limitFD.rlim_cur = limitFD.rlim_max;
    57-            setrlimit(RLIMIT_NOFILE, &limitFD);
    58-            getrlimit(RLIMIT_NOFILE, &limitFD);
    59-        }
    60-        return limitFD.rlim_cur;
    61+        return static_cast<int>(limitFD.rlim_cur);
    62     }
    63     return min_fd; // getrlimit failed, assume it's fine
    64 #endif
    
  25. winterrdog commented at 9:35 am on March 28, 2026: none

    Really solid work with @luke-jr ’s patch.

    I was looking at the RaiseFileDescriptorLimit logic from the outside and had one small nit to toss in for review. Specifically on this line in the patch:

    limitFD.rlim_cur = std::in_range<rlim_t>(min_fd) ? static_cast<rlim_t>(min_fd) : RLIM_INFINITY;

    I noticed that if min_fd is out of range for the platform’s rlim_t, the local state is momentarily set to RLIM_INFINITY before the subsequent if block pulls it back down to rlim_max.

    While i realize this state isn’t committed via setrlimit until later, it might be cleaner to fallback directly to limitFD.rlim_max inside the ternary. it keeps the local rlim_cur in a consistently valid state throughout the function and makes the logic more linear for the reader: we go from ‘Current’ to ‘Target/Max’ in one step rather than ‘Current’ -> ‘Infinity’ -> ‘Max’.

     0int RaiseFileDescriptorLimit(int min_fd) {
     1#if defined(WIN32)
     2     return 2048;
     3#else
     4     struct rlimit limitFD;
     5     if (getrlimit(RLIMIT_NOFILE, &limitFD) != -1) {
     6          if (limitFD.rlim_cur != RLIM_INFINITY && std::cmp_less(limitFD.rlim_cur, min_fd)) {
     7               const auto current_limit{limitFD.rlim_cur};
     8               
     9-              limitFD.rlim_cur = std::in_range<rlim_t>(min_fd) ? static_cast<rlim_t>(min_fd) : RLIM_INFINITY;
    10+              limitFD.rlim_cur = std::in_range<rlim_t>(min_fd) ? static_cast<rlim_t>(min_fd) : limitFD.rlim_max;
    11-              if (limitFD.rlim_max != RLIM_INFINITY && (limitFD.rlim_cur == RLIM_INFINITY || limitFD.rlim_cur > limitFD.rlim_max)) {
    12+              if (limitFD.rlim_max != RLIM_INFINITY && limitFD.rlim_cur > limitFD.rlim_max) {
    13                    limitFD.rlim_cur = limitFD.rlim_max;
    14               }
    15               if (current_limit != limitFD.rlim_cur) {
    16                    setrlimit(RLIMIT_NOFILE, &limitFD);
    17                    getrlimit(RLIMIT_NOFILE, &limitFD);
    18               }
    19          }
    20          if (limitFD.rlim_cur == RLIM_INFINITY ||
    21               std::cmp_greater_equal(limitFD.rlim_cur, std::numeric_limits<int>::max())) {
    22               return std::numeric_limits<int>::max();
    23          }
    24          return static_cast<int>(limitFD.rlim_cur);
    25     }
    26     return min_fd; // getrlimit failed, assume it's fine
    27}
    

    what do you think about flattening that logic?

  26. Sjors commented at 7:49 am on March 30, 2026: member
    @luke-jr thanks for that patch, I’ll look into incorporating that and then I’ll also respond to @winterrdog.
  27. Sjors marked this as a draft on Mar 30, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-03-31 12:13 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me