Skip to main content

linuxutils_system/
prlimit.rs

1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use cols::{OutputMode, Table, WidthHint, print_table};
7use rustix::process::{Pid, Resource, Rlimit, getrlimit, prlimit, setrlimit};
8use std::{fs, os::unix::process::CommandExt, process, process::ExitCode};
9
10#[derive(Parser)]
11#[command(
12    name = "prlimit",
13    about = "Get and set process resource limits",
14    override_usage = "prlimit [options] [--<resource>=<limit>] [-p PID]\n       \
15                      prlimit [options] [--<resource>=<limit>] COMMAND [args...]"
16)]
17pub struct Args {
18    /// Process ID
19    #[arg(short, long, value_name = "pid")]
20    pid: Option<u32>,
21
22    /// Define which output columns to use
23    #[arg(short = 'o', long, value_delimiter = ',')]
24    output: Option<Vec<String>>,
25
26    /// Don't print headings
27    #[arg(long)]
28    noheadings: bool,
29
30    /// Use the raw output format
31    #[arg(long)]
32    raw: bool,
33
34    // Resource limits — absent = not selected, bare flag = show only,
35    // flag=limit = set and show.
36    /// Max size of core files (bytes)
37    #[arg(short = 'c', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
38    core: Option<String>,
39
40    /// Max size of a process's data segment (bytes)
41    #[arg(short = 'd', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
42    data: Option<String>,
43
44    /// Max nice priority allowed to raise
45    #[arg(short = 'e', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
46    nice: Option<String>,
47
48    /// Max size of files written by the process (bytes)
49    #[arg(short = 'f', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
50    fsize: Option<String>,
51
52    /// Max number of pending signals
53    #[arg(short = 'i', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
54    sigpending: Option<String>,
55
56    /// Max locked-in-memory address space (bytes)
57    #[arg(short = 'l', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
58    memlock: Option<String>,
59
60    /// Max resident set size (bytes)
61    #[arg(short = 'm', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
62    rss: Option<String>,
63
64    /// Max number of open files
65    #[arg(short = 'n', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
66    nofile: Option<String>,
67
68    /// Max bytes in POSIX message queues (bytes)
69    #[arg(short = 'q', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
70    msgqueue: Option<String>,
71
72    /// Max real-time scheduling priority
73    #[arg(short = 'r', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
74    rtprio: Option<String>,
75
76    /// Max stack size (bytes)
77    #[arg(short = 's', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
78    stack: Option<String>,
79
80    /// Max CPU time (seconds)
81    #[arg(short = 't', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
82    cpu: Option<String>,
83
84    /// Max number of user processes
85    #[arg(short = 'u', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
86    nproc: Option<String>,
87
88    /// Max virtual memory size (bytes)
89    #[arg(short = 'v', long = "as", value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
90    addr_space: Option<String>,
91
92    /// Max number of file locks held
93    #[arg(short = 'x', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
94    locks: Option<String>,
95
96    /// CPU time for real-time tasks (microseconds)
97    #[arg(short = 'y', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
98    rttime: Option<String>,
99
100    /// Command and arguments to run with new limits applied
101    #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
102    command: Vec<String>,
103}
104
105#[derive(Debug, Clone, Copy)]
106struct ResourceInfo {
107    resource: Resource,
108    name: &'static str,
109    description: &'static str,
110    units: &'static str,
111}
112
113const ALL_RESOURCES: &[ResourceInfo] = &[
114    ResourceInfo {
115        resource: Resource::As,
116        name: "AS",
117        description: "address space limit",
118        units: "bytes",
119    },
120    ResourceInfo {
121        resource: Resource::Core,
122        name: "CORE",
123        description: "max core file size",
124        units: "bytes",
125    },
126    ResourceInfo {
127        resource: Resource::Cpu,
128        name: "CPU",
129        description: "CPU time",
130        units: "seconds",
131    },
132    ResourceInfo {
133        resource: Resource::Data,
134        name: "DATA",
135        description: "max data size",
136        units: "bytes",
137    },
138    ResourceInfo {
139        resource: Resource::Fsize,
140        name: "FSIZE",
141        description: "max file size",
142        units: "bytes",
143    },
144    ResourceInfo {
145        resource: Resource::Locks,
146        name: "LOCKS",
147        description: "max number of file locks held",
148        units: "locks",
149    },
150    ResourceInfo {
151        resource: Resource::Memlock,
152        name: "MEMLOCK",
153        description: "max locked-in-memory address space",
154        units: "bytes",
155    },
156    ResourceInfo {
157        resource: Resource::Msgqueue,
158        name: "MSGQUEUE",
159        description: "max bytes in POSIX mqueues",
160        units: "bytes",
161    },
162    ResourceInfo {
163        resource: Resource::Nice,
164        name: "NICE",
165        description: "max nice prio allowed to raise",
166        units: "",
167    },
168    ResourceInfo {
169        resource: Resource::Nofile,
170        name: "NOFILE",
171        description: "max number of open files",
172        units: "files",
173    },
174    ResourceInfo {
175        resource: Resource::Nproc,
176        name: "NPROC",
177        description: "max number of processes",
178        units: "processes",
179    },
180    ResourceInfo {
181        resource: Resource::Rss,
182        name: "RSS",
183        description: "max resident set size",
184        units: "bytes",
185    },
186    ResourceInfo {
187        resource: Resource::Rtprio,
188        name: "RTPRIO",
189        description: "max real-time priority",
190        units: "",
191    },
192    ResourceInfo {
193        resource: Resource::Rttime,
194        name: "RTTIME",
195        description: "timeout for real-time tasks",
196        units: "microsecs",
197    },
198    ResourceInfo {
199        resource: Resource::Sigpending,
200        name: "SIGPENDING",
201        description: "max number of pending signals",
202        units: "signals",
203    },
204    ResourceInfo {
205        resource: Resource::Stack,
206        name: "STACK",
207        description: "max stack size",
208        units: "bytes",
209    },
210];
211
212fn resource_spec(args: &Args) -> Vec<(ResourceInfo, Option<&str>)> {
213    // Build a list of (resource, optional_limit_str) for selected resources.
214    // None limit = query only; Some(s) = set then query.
215    macro_rules! entry {
216        ($field:expr, $name:expr) => {
217            if let Some(ref val) = $field {
218                let ri = ALL_RESOURCES
219                    .iter()
220                    .find(|r| r.name == $name)
221                    .copied()
222                    .unwrap();
223                let limit = if val.is_empty() {
224                    None
225                } else {
226                    Some(val.as_str())
227                };
228                Some((ri, limit))
229            } else {
230                None
231            }
232        };
233    }
234
235    let selected: Vec<Option<(ResourceInfo, Option<&str>)>> = vec![
236        entry!(args.addr_space, "AS"),
237        entry!(args.core, "CORE"),
238        entry!(args.cpu, "CPU"),
239        entry!(args.data, "DATA"),
240        entry!(args.fsize, "FSIZE"),
241        entry!(args.locks, "LOCKS"),
242        entry!(args.memlock, "MEMLOCK"),
243        entry!(args.msgqueue, "MSGQUEUE"),
244        entry!(args.nice, "NICE"),
245        entry!(args.nofile, "NOFILE"),
246        entry!(args.nproc, "NPROC"),
247        entry!(args.rss, "RSS"),
248        entry!(args.rtprio, "RTPRIO"),
249        entry!(args.rttime, "RTTIME"),
250        entry!(args.sigpending, "SIGPENDING"),
251        entry!(args.stack, "STACK"),
252    ];
253
254    selected.into_iter().flatten().collect()
255}
256
257fn parse_limit(s: &str) -> Result<(Option<u64>, Option<u64>), String> {
258    // Formats: "soft:hard", "soft:", ":hard", "value"
259    if let Some(colon) = s.find(':') {
260        let soft_str = &s[..colon];
261        let hard_str = &s[colon + 1..];
262        let soft = parse_one_limit(soft_str)?;
263        let hard = parse_one_limit(hard_str)?;
264        Ok((soft, hard))
265    } else {
266        let v = parse_single_limit(s)?;
267        Ok((Some(v), Some(v)))
268    }
269}
270
271// Returns None for empty (= don't change this half)
272fn parse_one_limit(s: &str) -> Result<Option<u64>, String> {
273    if s.is_empty() {
274        Ok(None)
275    } else {
276        parse_single_limit(s).map(Some)
277    }
278}
279
280fn parse_single_limit(s: &str) -> Result<u64, String> {
281    match s {
282        "unlimited" | "infinity" => Ok(u64::MAX),
283        _ => s.parse::<u64>().map_err(|_| format!("invalid limit: {s}")),
284    }
285}
286
287fn limit_str(val: Option<u64>) -> String {
288    match val {
289        None | Some(u64::MAX) => "unlimited".to_string(),
290        Some(v) => v.to_string(),
291    }
292}
293
294#[derive(Debug, Clone, Copy, PartialEq, Eq)]
295enum Col {
296    Resource,
297    Description,
298    Soft,
299    Hard,
300    Units,
301}
302
303impl Col {
304    fn name(self) -> &'static str {
305        match self {
306            Col::Resource => "RESOURCE",
307            Col::Description => "DESCRIPTION",
308            Col::Soft => "SOFT",
309            Col::Hard => "HARD",
310            Col::Units => "UNITS",
311        }
312    }
313
314    fn whint(self) -> WidthHint {
315        match self {
316            Col::Resource => WidthHint::Fixed(10),
317            Col::Description => WidthHint::Fixed(36),
318            Col::Soft => WidthHint::Fixed(9),
319            Col::Hard => WidthHint::Fixed(9),
320            Col::Units => WidthHint::Fixed(9),
321        }
322    }
323
324    fn is_right(self) -> bool {
325        matches!(self, Col::Soft | Col::Hard)
326    }
327
328    fn from_name(s: &str) -> Option<Self> {
329        match s.to_uppercase().as_str() {
330            "RESOURCE" => Some(Col::Resource),
331            "DESCRIPTION" => Some(Col::Description),
332            "SOFT" => Some(Col::Soft),
333            "HARD" => Some(Col::Hard),
334            "UNITS" => Some(Col::Units),
335            _ => None,
336        }
337    }
338}
339
340const DEFAULT_COLUMNS: &[Col] = &[
341    Col::Resource,
342    Col::Description,
343    Col::Soft,
344    Col::Hard,
345    Col::Units,
346];
347
348// Read a resource limit for the given PID (or self if None).
349// Uses getrlimit for self, /proc/<pid>/limits for other PIDs.
350fn read_limit(pid: Option<u32>, resource: Resource) -> Option<Rlimit> {
351    if let Some(pid) = pid {
352        read_limit_from_proc(pid, resource)
353    } else {
354        Some(getrlimit(resource))
355    }
356}
357
358// Parse /proc/<pid>/limits to get the limit for a specific resource.
359fn read_limit_from_proc(pid: u32, resource: Resource) -> Option<Rlimit> {
360    let content = fs::read_to_string(format!("/proc/{pid}/limits")).ok()?;
361    // Map resource to the proc "Limit" prefix string.
362    let proc_name = match resource {
363        Resource::Cpu => "Max cpu time",
364        Resource::Fsize => "Max file size",
365        Resource::Data => "Max data size",
366        Resource::Stack => "Max stack size",
367        Resource::Core => "Max core file size",
368        Resource::Rss => "Max resident set",
369        Resource::Nproc => "Max processes",
370        Resource::Nofile => "Max open files",
371        Resource::Memlock => "Max locked memory",
372        Resource::As => "Max address space",
373        Resource::Locks => "Max file locks",
374        Resource::Sigpending => "Max pending signals",
375        Resource::Msgqueue => "Max msgqueue size",
376        Resource::Nice => "Max nice priority",
377        Resource::Rtprio => "Max realtime priority",
378        Resource::Rttime => "Max realtime timeout",
379        _ => return None,
380    };
381    for line in content.lines().skip(1) {
382        if line.starts_with(proc_name) {
383            let rest = line.strip_prefix(proc_name).unwrap().trim();
384            let parts: Vec<&str> = rest.split_whitespace().collect();
385            if parts.len() >= 2 {
386                let current = parse_proc_limit(parts[0]);
387                let maximum = parse_proc_limit(parts[1]);
388                return Some(Rlimit { current, maximum });
389            }
390        }
391    }
392    None
393}
394
395fn parse_proc_limit(s: &str) -> Option<u64> {
396    if s == "unlimited" {
397        None
398    } else {
399        s.parse().ok()
400    }
401}
402
403fn u64_to_rlimit_half(v: u64) -> Option<u64> {
404    if v == u64::MAX { None } else { Some(v) }
405}
406
407pub fn run(args: Args) -> ExitCode {
408    let selected = resource_spec(&args);
409
410    // Determine which resources to display.
411    let display: Vec<ResourceInfo> = if selected.is_empty() {
412        ALL_RESOURCES.to_vec()
413    } else {
414        selected.iter().map(|(ri, _)| *ri).collect()
415    };
416
417    let target_pid = args.pid;
418
419    // Apply any limit changes.
420    for (ri, limit_str_opt) in &selected {
421        let Some(limit_str_val) = limit_str_opt else {
422            continue;
423        };
424        let (new_soft, new_hard) = match parse_limit(limit_str_val) {
425            Ok(v) => v,
426            Err(e) => {
427                eprintln!("prlimit: {e}");
428                return ExitCode::FAILURE;
429            }
430        };
431
432        // Read current limits to fill in unchanged halves.
433        let current = match read_limit(target_pid, ri.resource) {
434            Some(r) => r,
435            None => {
436                eprintln!("prlimit: {}: failed to read current limit", ri.name);
437                return ExitCode::FAILURE;
438            }
439        };
440
441        let merged = Rlimit {
442            current: new_soft
443                .map(u64_to_rlimit_half)
444                .unwrap_or(current.current),
445            maximum: new_hard
446                .map(u64_to_rlimit_half)
447                .unwrap_or(current.maximum),
448        };
449
450        let result = if let Some(pid) = target_pid {
451            let raw_pid = unsafe { Pid::from_raw_unchecked(pid as i32) };
452            prlimit(Some(raw_pid), ri.resource, merged).map(|_| ())
453        } else {
454            setrlimit(ri.resource, merged)
455        };
456
457        if let Err(e) = result {
458            eprintln!("prlimit: failed to set {}: {e}", ri.name);
459            return ExitCode::FAILURE;
460        }
461    }
462
463    // If a command was given, exec it now (limits already applied above).
464    if !args.command.is_empty() {
465        let (prog, prog_args) = args.command.split_first().unwrap();
466        let err = process::Command::new(prog).args(prog_args).exec();
467        eprintln!("prlimit: {prog}: {err}");
468        return ExitCode::FAILURE;
469    }
470
471    // Display limits.
472    let columns: Vec<Col> = if let Some(ref names) = args.output {
473        let mut cols = Vec::new();
474        for name in names {
475            match Col::from_name(name.trim()) {
476                Some(c) => cols.push(c),
477                None => {
478                    eprintln!("prlimit: unknown column: {name}");
479                    return ExitCode::FAILURE;
480                }
481            }
482        }
483        cols
484    } else {
485        DEFAULT_COLUMNS.to_vec()
486    };
487
488    let mut table = Table::new();
489    if args.raw {
490        table.output_mode_set(OutputMode::Raw);
491    }
492    if args.noheadings {
493        table.headings_set(false);
494    }
495    for col in &columns {
496        let idx = table.new_column(col.name());
497        table.column_mut(idx).unwrap().width_hint_set(col.whint());
498        if col.is_right() {
499            table.column_mut(idx).unwrap().right_set(true);
500        }
501    }
502
503    for ri in &display {
504        let lim = match read_limit(target_pid, ri.resource) {
505            Some(r) => r,
506            None => {
507                eprintln!("prlimit: {}: failed to read limit", ri.name);
508                return ExitCode::FAILURE;
509            }
510        };
511
512        let line_id = table.new_line(None);
513        let line = table.line_mut(line_id);
514
515        for (ci, col) in columns.iter().enumerate() {
516            let val = match col {
517                Col::Resource => ri.name.to_string(),
518                Col::Description => ri.description.to_string(),
519                Col::Soft => limit_str(lim.current),
520                Col::Hard => limit_str(lim.maximum),
521                Col::Units => ri.units.to_string(),
522            };
523            line.data_set(ci, &val);
524        }
525    }
526
527    let stdout = std::io::stdout();
528    let mut out = stdout.lock();
529    if let Err(e) = print_table(&table, &mut out) {
530        eprintln!("prlimit: {e}");
531        return ExitCode::FAILURE;
532    }
533
534    ExitCode::SUCCESS
535}