tuwunel_core/utils/sys/limits.rs
1//! Process resource-limit utilities.
2//!
3//! The helpers query soft and hard limits and raise selected soft limits when
4//! supported. Platform-specific implementations provide neutral fallbacks when
5//! an interface is unavailable.
6
7#[cfg(target_os = "linux")]
8use std::{
9 fs::read_to_string,
10 path::{Path, PathBuf},
11};
12
13#[cfg(unix)]
14use nix::sys::resource::{Resource, getrlimit};
15#[cfg(unix)]
16use nix::unistd::{SysconfVar, sysconf};
17
18use crate::Result;
19#[cfg(target_os = "linux")]
20use crate::utils::result::FlatOk;
21#[cfg(unix)]
22use crate::{apply, debug, utils::math::ExpectInto};
23
24#[cfg(unix)]
25/// Raises the soft file descriptor limit to the current hard limit.
26///
27/// RocksDB and concurrent federation connections can exceed the common soft
28/// limit of 1,024 during startup. Systemd commonly provides a hard limit of
29/// 524,288.
30///
31/// * <https://www.freedesktop.org/software/systemd/man/systemd.exec.html#id-1.12.2.1.17.6>
32/// * <https://github.com/systemd/systemd/commit/0abf94923b4a95a7d89bc526efc84e7ca2b71741>
33pub fn maximize_fd_limit() -> Result {
34 use nix::sys::resource::setrlimit;
35
36 let (soft_limit, hard_limit) = max_file_descriptors()?;
37 if soft_limit < hard_limit {
38 let new_limit = hard_limit.try_into()?;
39 setrlimit(Resource::RLIMIT_NOFILE, new_limit, new_limit)?;
40 assert_eq!((hard_limit, hard_limit), max_file_descriptors()?, "getrlimit != setrlimit");
41 debug!(to = hard_limit, from = soft_limit, "Raised RLIMIT_NOFILE");
42 }
43
44 Ok(())
45}
46
47#[cfg(not(unix))]
48/// Performs no file-descriptor limit adjustment on unsupported platforms.
49pub fn maximize_fd_limit() -> Result { Ok(()) }
50
51#[cfg(all(unix, not(target_os = "macos")))]
52/// Raises the soft thread limit to the current hard limit.
53///
54/// Some distributions default to about 1,024 threads, which can constrain hosts
55/// with 32 or more cores. Thread limits are otherwise reached less often than
56/// file descriptor limits.
57pub fn maximize_thread_limit() -> Result {
58 use nix::sys::resource::setrlimit;
59
60 let (soft_limit, hard_limit) = max_threads()?;
61 if soft_limit < hard_limit {
62 let new_limit = hard_limit.try_into()?;
63 setrlimit(Resource::RLIMIT_NPROC, new_limit, new_limit)?;
64 assert_eq!((hard_limit, hard_limit), max_threads()?, "getrlimit != setrlimit");
65 debug!(to = hard_limit, from = soft_limit, "Raised RLIMIT_NPROC");
66 }
67
68 Ok(())
69}
70
71#[cfg(any(not(unix), target_os = "macos"))]
72/// Performs no thread limit adjustment on platforms where nix does not expose
73/// `RLIMIT_NPROC`, notably macOS.
74pub fn maximize_thread_limit() -> Result { Ok(()) }
75
76/// Returns the soft and hard file-descriptor limits.
77///
78/// The tuple is ordered as the current soft limit followed by the maximum hard
79/// limit. Values come from `RLIMIT_NOFILE`.
80///
81/// # Panics
82///
83/// Panics when either platform limit cannot be represented as `usize`.
84#[cfg(unix)]
85#[inline]
86pub fn max_file_descriptors() -> Result<(usize, usize)> {
87 getrlimit(Resource::RLIMIT_NOFILE)
88 .map(apply!(2, ExpectInto::expect_into))
89 .map_err(Into::into)
90}
91
92/// Returns sentinel file-descriptor limits on unsupported platforms.
93///
94/// Both tuple elements are `usize::MAX`, representing no known finite limit.
95/// No operating-system query is performed.
96#[cfg(not(unix))]
97#[inline]
98pub fn max_file_descriptors() -> Result<(usize, usize)> { Ok((usize::MAX, usize::MAX)) }
99
100/// Returns the soft and hard process stack-size limits.
101///
102/// The tuple is ordered as the current soft limit followed by the maximum hard
103/// limit. Values come from `RLIMIT_STACK`.
104///
105/// # Panics
106///
107/// Panics when either platform limit cannot be represented as `usize`.
108#[cfg(unix)]
109#[inline]
110pub fn max_stack_size() -> Result<(usize, usize)> {
111 getrlimit(Resource::RLIMIT_STACK)
112 .map(apply!(2, ExpectInto::expect_into))
113 .map_err(Into::into)
114}
115
116/// Returns sentinel stack-size limits on unsupported platforms.
117///
118/// Both tuple elements are `usize::MAX`, representing no known finite limit.
119/// No operating-system query is performed.
120#[cfg(not(unix))]
121#[inline]
122pub fn max_stack_size() -> Result<(usize, usize)> { Ok((usize::MAX, usize::MAX)) }
123
124/// Returns the soft and hard locked-memory limits.
125///
126/// The tuple is ordered as the current soft limit followed by the maximum hard
127/// limit. Values come from `RLIMIT_MEMLOCK`.
128///
129/// # Panics
130///
131/// Panics when either platform limit cannot be represented as `usize`.
132#[cfg(all(unix, not(target_os = "macos")))]
133#[inline]
134pub fn max_memory_locked() -> Result<(usize, usize)> {
135 getrlimit(Resource::RLIMIT_MEMLOCK)
136 .map(apply!(2, ExpectInto::expect_into))
137 .map_err(Into::into)
138}
139
140/// Returns sentinel locked-memory limits on unsupported platforms.
141///
142/// Both tuple elements are zero, the module's unsupported-platform sentinel.
143/// No operating-system query is performed.
144#[cfg(any(not(unix), target_os = "macos"))]
145#[inline]
146pub fn max_memory_locked() -> Result<(usize, usize)> { Ok((usize::MIN, usize::MIN)) }
147
148/// Returns the soft and hard per-user process-count limits.
149///
150/// The tuple is ordered as the current soft limit followed by the maximum hard
151/// limit. Values come from `RLIMIT_NPROC`; on Linux, it counts extant threads
152/// for the caller's real user ID.
153///
154/// # Panics
155///
156/// Panics when either platform limit cannot be represented as `usize`.
157#[cfg(all(unix, not(target_os = "macos")))]
158#[inline]
159pub fn max_threads() -> Result<(usize, usize)> {
160 getrlimit(Resource::RLIMIT_NPROC)
161 .map(apply!(2, ExpectInto::expect_into))
162 .map_err(Into::into)
163}
164
165/// Returns sentinel thread limits on unsupported platforms.
166///
167/// Both tuple elements are `usize::MAX`, representing no known finite limit.
168/// No operating-system query is performed.
169#[cfg(any(not(unix), target_os = "macos"))]
170#[inline]
171pub fn max_threads() -> Result<(usize, usize)> { Ok((usize::MAX, usize::MAX)) }
172
173/// Returns the cgroup task-count limit applying to this process.
174///
175/// Container runtimes impose a task ceiling through the cgroup v2 `pids`
176/// controller, which `RLIMIT_NPROC` does not reflect: Podman defaults to 2,048
177/// tasks while leaving the rlimit unbounded. The effective ceiling is the
178/// lowest limit set anywhere in the cgroup's ancestry, so every ancestor is
179/// read; `None` means none of them sets one, which also covers a cgroup v1
180/// hierarchy.
181#[cfg(target_os = "linux")]
182#[must_use]
183pub fn cgroup_max_tasks() -> Option<usize> {
184 let cgroup = read_to_string("/proc/self/cgroup").unwrap_or_default();
185 let leaf = cgroup
186 .lines()
187 .find_map(|line| line.strip_prefix("0::"))
188 .unwrap_or_default();
189
190 let mount = Path::new("/sys/fs/cgroup");
191 let limit = Path::new("pids.max");
192
193 Path::new(leaf.trim_start_matches('/'))
194 .ancestors()
195 .map(|dir| [mount, dir, limit].into_iter().collect())
196 .filter_map(pids_max)
197 .min()
198}
199
200/// Returns no cgroup task limit on systems without cgroups.
201///
202/// No operating-system query is performed.
203#[cfg(not(target_os = "linux"))]
204#[must_use]
205#[inline]
206pub fn cgroup_max_tasks() -> Option<usize> { None }
207
208/// Reads one cgroup `pids.max` file.
209///
210/// An unreadable file and the literal `max` both yield `None`, so a cgroup
211/// setting no limit of its own contributes nothing to the minimum.
212#[cfg(target_os = "linux")]
213fn pids_max(path: PathBuf) -> Option<usize> {
214 read_to_string(path)
215 .ok()
216 .as_deref()
217 .map(str::trim)
218 .map(str::parse)
219 .flat_ok()
220}
221
222#[cfg(unix)]
223/// Get the system's page size in bytes.
224#[inline]
225pub fn page_size() -> Result<usize> {
226 sysconf(SysconfVar::PAGE_SIZE)?
227 .unwrap_or(-1)
228 .try_into()
229 .map_err(Into::into)
230}
231
232#[cfg(not(unix))]
233/// Get the system's page size in bytes.
234#[inline]
235pub fn page_size() -> Result<usize> { Ok(4096) }