1#[cfg(unix)]
6use libc::{
7 getrlimit, rlimit, setrlimit, RLIMIT_AS, RLIMIT_CORE, RLIMIT_CPU, RLIMIT_DATA, RLIMIT_FSIZE,
8 RLIMIT_NOFILE, RLIMIT_STACK, RLIM_INFINITY,
9};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum LimitType {
14 Memory,
15 Number,
16 Time,
17 Microseconds,
18 Unknown,
19}
20
21#[derive(Debug, Clone)]
23pub struct ResInfo {
24 pub res: i32,
25 pub name: &'static str,
26 pub limit_type: LimitType,
27 pub unit: u64,
28 pub opt: char,
29 pub descr: &'static str,
30}
31
32#[cfg(unix)]
34pub static KNOWN_RESOURCES: &[ResInfo] = &[
35 ResInfo {
36 res: RLIMIT_CPU as i32,
37 name: "cputime",
38 limit_type: LimitType::Time,
39 unit: 1,
40 opt: 't',
41 descr: "cpu time (seconds)",
42 },
43 ResInfo {
44 res: RLIMIT_FSIZE as i32,
45 name: "filesize",
46 limit_type: LimitType::Memory,
47 unit: 512,
48 opt: 'f',
49 descr: "file size (blocks)",
50 },
51 ResInfo {
52 res: RLIMIT_DATA as i32,
53 name: "datasize",
54 limit_type: LimitType::Memory,
55 unit: 1024,
56 opt: 'd',
57 descr: "data seg size (kbytes)",
58 },
59 ResInfo {
60 res: RLIMIT_STACK as i32,
61 name: "stacksize",
62 limit_type: LimitType::Memory,
63 unit: 1024,
64 opt: 's',
65 descr: "stack size (kbytes)",
66 },
67 ResInfo {
68 res: RLIMIT_CORE as i32,
69 name: "coredumpsize",
70 limit_type: LimitType::Memory,
71 unit: 512,
72 opt: 'c',
73 descr: "core file size (blocks)",
74 },
75 ResInfo {
76 res: RLIMIT_NOFILE as i32,
77 name: "descriptors",
78 limit_type: LimitType::Number,
79 unit: 1,
80 opt: 'n',
81 descr: "file descriptors",
82 },
83 ResInfo {
84 res: RLIMIT_AS as i32,
85 name: "addressspace",
86 limit_type: LimitType::Memory,
87 unit: 1024,
88 opt: 'v',
89 descr: "address space (kbytes)",
90 },
91];
92
93#[cfg(not(unix))]
94pub static KNOWN_RESOURCES: &[ResInfo] = &[];
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum LimitValue {
99 Unlimited,
100 Value(u64),
101}
102
103impl LimitValue {
104 #[cfg(unix)]
105 pub fn from_rlim(val: u64) -> Self {
106 if val == RLIM_INFINITY as u64 {
107 LimitValue::Unlimited
108 } else {
109 LimitValue::Value(val)
110 }
111 }
112
113 #[cfg(unix)]
114 pub fn to_rlim(&self) -> u64 {
115 match self {
116 LimitValue::Unlimited => RLIM_INFINITY as u64,
117 LimitValue::Value(v) => *v,
118 }
119 }
120
121 pub fn format(&self, info: Option<&ResInfo>) -> String {
122 match self {
123 LimitValue::Unlimited => "unlimited".to_string(),
124 LimitValue::Value(val) => {
125 if let Some(info) = info {
126 match info.limit_type {
127 LimitType::Time => {
128 let hours = val / 3600;
129 let mins = (val / 60) % 60;
130 let secs = val % 60;
131 format!("{}:{:02}:{:02}", hours, mins, secs)
132 }
133 LimitType::Microseconds => format!("{}us", val),
134 LimitType::Memory => {
135 if *val >= 1024 * 1024 {
136 format!("{}MB", val / (1024 * 1024))
137 } else {
138 format!("{}kB", val / 1024)
139 }
140 }
141 _ => format!("{}", val),
142 }
143 } else {
144 format!("{}", val)
145 }
146 }
147 }
148 }
149}
150
151impl std::fmt::Display for LimitValue {
152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153 match self {
154 LimitValue::Unlimited => write!(f, "unlimited"),
155 LimitValue::Value(v) => write!(f, "{}", v),
156 }
157 }
158}
159
160#[derive(Debug, Default)]
162pub struct ResourceLimits {
163 #[cfg(unix)]
164 cached: std::collections::HashMap<i32, (LimitValue, LimitValue)>,
165}
166
167impl ResourceLimits {
168 pub fn new() -> Self {
169 Self {
170 #[cfg(unix)]
171 cached: std::collections::HashMap::new(),
172 }
173 }
174
175 pub fn find_by_name(&self, name: &str) -> Option<&'static ResInfo> {
177 let mut found: Option<&'static ResInfo> = None;
178 let mut ambiguous = false;
179
180 for info in KNOWN_RESOURCES {
181 if info.name.starts_with(name) {
182 if found.is_some() {
183 ambiguous = true;
184 break;
185 }
186 found = Some(info);
187 }
188 }
189
190 if ambiguous {
191 None
192 } else {
193 found
194 }
195 }
196
197 pub fn find_by_opt(&self, opt: char) -> Option<&'static ResInfo> {
199 KNOWN_RESOURCES.iter().find(|info| info.opt == opt)
200 }
201
202 pub fn find_by_res(&self, res: i32) -> Option<&'static ResInfo> {
204 KNOWN_RESOURCES.iter().find(|info| info.res == res)
205 }
206
207 #[cfg(unix)]
209 pub fn get(&self, res: i32) -> Result<(LimitValue, LimitValue), String> {
210 let mut rlim = rlimit {
211 rlim_cur: 0,
212 rlim_max: 0,
213 };
214
215 if unsafe { getrlimit(res as _, &mut rlim) } < 0 {
216 return Err(format!(
217 "can't read limit: {}",
218 std::io::Error::last_os_error()
219 ));
220 }
221
222 Ok((
223 LimitValue::from_rlim(rlim.rlim_cur),
224 LimitValue::from_rlim(rlim.rlim_max),
225 ))
226 }
227
228 #[cfg(not(unix))]
229 pub fn get(&self, _res: i32) -> Result<(LimitValue, LimitValue), String> {
230 Err("resource limits not supported on this platform".to_string())
231 }
232
233 #[cfg(unix)]
235 pub fn set(
236 &mut self,
237 res: i32,
238 soft: Option<LimitValue>,
239 hard: Option<LimitValue>,
240 ) -> Result<(), String> {
241 let (cur_soft, cur_hard) = self.get(res)?;
242
243 let new_soft = soft.unwrap_or(cur_soft);
244 let new_hard = hard.unwrap_or(cur_hard);
245
246 if let LimitValue::Value(s) = new_soft {
247 if let LimitValue::Value(h) = new_hard {
248 if s > h {
249 return Err("soft limit exceeds hard limit".to_string());
250 }
251 }
252 }
253
254 let euid = unsafe { libc::geteuid() };
255 if euid != 0 {
256 if let (LimitValue::Value(new_h), LimitValue::Value(cur_h)) = (new_hard, cur_hard) {
257 if new_h > cur_h {
258 return Err("can't raise hard limits".to_string());
259 }
260 }
261 }
262
263 let rlim = rlimit {
264 rlim_cur: new_soft.to_rlim(),
265 rlim_max: new_hard.to_rlim(),
266 };
267
268 if unsafe { setrlimit(res as _, &rlim) } < 0 {
269 return Err(format!(
270 "setrlimit failed: {}",
271 std::io::Error::last_os_error()
272 ));
273 }
274
275 self.cached.insert(res, (new_soft, new_hard));
276 Ok(())
277 }
278
279 #[cfg(not(unix))]
280 pub fn set(
281 &mut self,
282 _res: i32,
283 _soft: Option<LimitValue>,
284 _hard: Option<LimitValue>,
285 ) -> Result<(), String> {
286 Err("resource limits not supported on this platform".to_string())
287 }
288
289 pub fn unlimit(&mut self, res: i32, hard: bool) -> Result<(), String> {
291 if hard {
292 self.set(
293 res,
294 Some(LimitValue::Unlimited),
295 Some(LimitValue::Unlimited),
296 )
297 } else {
298 let (_, cur_hard) = self.get(res)?;
299 self.set(res, Some(cur_hard), None)
300 }
301 }
302
303 pub fn list_all(&self, hard: bool) -> Vec<(String, LimitValue)> {
305 let mut result = Vec::new();
306
307 for info in KNOWN_RESOURCES {
308 if let Ok((soft, hard_val)) = self.get(info.res) {
309 let val = if hard { hard_val } else { soft };
310 result.push((info.name.to_string(), val));
311 }
312 }
313
314 result
315 }
316}
317
318pub fn parse_limit_value(s: &str, info: Option<&ResInfo>) -> Result<LimitValue, String> {
320 if s == "unlimited" {
321 return Ok(LimitValue::Unlimited);
322 }
323
324 let info = info.ok_or("unknown resource type")?;
325
326 match info.limit_type {
327 LimitType::Time => {
328 if let Some(colon_pos) = s.find(':') {
329 let hours: u64 = s[..colon_pos].parse().map_err(|_| "invalid number")?;
330 let rest = &s[colon_pos + 1..];
331
332 let (mins, secs) = if let Some(colon2) = rest.find(':') {
333 let m: u64 = rest[..colon2].parse().map_err(|_| "invalid number")?;
334 let s: u64 = rest[colon2 + 1..].parse().map_err(|_| "invalid number")?;
335 (m, s)
336 } else {
337 let m: u64 = rest.parse().map_err(|_| "invalid number")?;
338 (m, 0)
339 };
340
341 Ok(LimitValue::Value(hours * 3600 + mins * 60 + secs))
342 } else {
343 let s_lower = s.to_lowercase();
344 let (num_str, multiplier) = if s_lower.ends_with('h') {
345 (&s[..s.len() - 1], 3600)
346 } else if s_lower.ends_with('m') {
347 (&s[..s.len() - 1], 60)
348 } else {
349 (s, 1)
350 };
351
352 let val: u64 = num_str.parse().map_err(|_| "invalid number")?;
353 Ok(LimitValue::Value(val * multiplier))
354 }
355 }
356 LimitType::Memory => {
357 let s_lower = s.to_lowercase();
358 let (num_str, multiplier) = if s_lower.ends_with('g') {
359 (&s[..s.len() - 1], 1024 * 1024 * 1024)
360 } else if s_lower.ends_with('m') {
361 (&s[..s.len() - 1], 1024 * 1024)
362 } else if s_lower.ends_with('k') {
363 (&s[..s.len() - 1], 1024)
364 } else {
365 (s, 1024)
366 };
367
368 let val: u64 = num_str.parse().map_err(|_| "invalid number")?;
369 Ok(LimitValue::Value(val * multiplier))
370 }
371 _ => {
372 let val: u64 = s.parse().map_err(|_| "limit must be a number")?;
373 Ok(LimitValue::Value(val))
374 }
375 }
376}
377
378pub fn format_limit_display(name: &str, val: LimitValue, info: Option<&ResInfo>) -> String {
380 format!("{:<16}{}", name, val.format(info))
381}
382
383pub fn format_ulimit_display(info: &ResInfo, val: LimitValue, show_header: bool) -> String {
385 let mut result = String::new();
386
387 if show_header {
388 result.push_str(&format!("-{}: {:<32}", info.opt, info.descr));
389 }
390
391 match val {
392 LimitValue::Unlimited => result.push_str("unlimited"),
393 LimitValue::Value(v) => {
394 let display_val = v / info.unit;
395 result.push_str(&format!("{}", display_val));
396 }
397 }
398
399 result
400}
401
402pub fn builtin_limit(
404 args: &[&str],
405 limits: &mut ResourceLimits,
406 hard: bool,
407 set: bool,
408) -> (i32, String) {
409 let mut output = String::new();
410
411 if args.is_empty() {
412 for (name, val) in limits.list_all(hard) {
413 let info = limits.find_by_name(&name);
414 output.push_str(&format_limit_display(&name, val, info));
415 output.push('\n');
416 }
417 return (0, output);
418 }
419
420 let mut i = 0;
421 while i < args.len() {
422 let name = args[i];
423
424 if name.chars().all(|c| c.is_ascii_digit()) {
425 let res: i32 = match name.parse() {
426 Ok(n) => n,
427 Err(_) => return (1, "limit: invalid resource number\n".to_string()),
428 };
429
430 if i + 1 >= args.len() {
431 match limits.get(res) {
432 Ok((soft, hard_val)) => {
433 let val = if hard { hard_val } else { soft };
434 output.push_str(&format!("{:<16}{}\n", res, val));
435 }
436 Err(e) => return (1, format!("limit: {}\n", e)),
437 }
438 i += 1;
439 continue;
440 }
441
442 let val_str = args[i + 1];
443 let val = match parse_limit_value(val_str, None) {
444 Ok(v) => v,
445 Err(e) => return (1, format!("limit: {}\n", e)),
446 };
447
448 if set {
449 let (soft, hard_opt) = if hard {
450 (None, Some(val))
451 } else {
452 (Some(val), None)
453 };
454
455 if let Err(e) = limits.set(res, soft, hard_opt) {
456 return (1, format!("limit: {}\n", e));
457 }
458 }
459
460 i += 2;
461 continue;
462 }
463
464 let info = match limits.find_by_name(name) {
465 Some(info) => info,
466 None => return (1, format!("limit: no such resource: {}\n", name)),
467 };
468
469 if i + 1 >= args.len() {
470 match limits.get(info.res) {
471 Ok((soft, hard_val)) => {
472 let val = if hard { hard_val } else { soft };
473 output.push_str(&format_limit_display(info.name, val, Some(info)));
474 output.push('\n');
475 }
476 Err(e) => return (1, format!("limit: {}\n", e)),
477 }
478 i += 1;
479 continue;
480 }
481
482 let val_str = args[i + 1];
483 let val = match parse_limit_value(val_str, Some(info)) {
484 Ok(v) => v,
485 Err(e) => return (1, format!("limit: {}\n", e)),
486 };
487
488 if set {
489 let (soft, hard_opt) = if hard {
490 (None, Some(val))
491 } else {
492 (Some(val), None)
493 };
494
495 if let Err(e) = limits.set(info.res, soft, hard_opt) {
496 return (1, format!("limit: {}\n", e));
497 }
498 }
499
500 i += 2;
501 }
502
503 (0, output)
504}
505
506pub fn builtin_ulimit(
508 args: &[&str],
509 limits: &mut ResourceLimits,
510 hard: bool,
511 soft: bool,
512) -> (i32, String) {
513 let mut output = String::new();
514 let show_all = args.iter().any(|a| *a == "-a");
515
516 if show_all || args.is_empty() {
517 let use_hard = hard && !soft;
518
519 for info in KNOWN_RESOURCES {
520 if let Ok((s, h)) = limits.get(info.res) {
521 let val = if use_hard { h } else { s };
522 output.push_str(&format_ulimit_display(info, val, true));
523 output.push('\n');
524 }
525 }
526 return (0, output);
527 }
528
529 let mut i = 0;
530 let mut res = RLIMIT_FSIZE as i32;
531 let mut use_hard = hard && !soft;
532
533 while i < args.len() {
534 let arg = args[i];
535
536 if arg.starts_with('-') {
537 for c in arg[1..].chars() {
538 match c {
539 'H' => use_hard = true,
540 'S' => use_hard = false,
541 'a' => {}
542 _ => {
543 if let Some(info) = limits.find_by_opt(c) {
544 res = info.res;
545 } else {
546 return (1, format!("ulimit: bad option: -{}\n", c));
547 }
548 }
549 }
550 }
551 i += 1;
552 continue;
553 }
554
555 let info = limits.find_by_res(res);
556 let val = match parse_limit_value(arg, info) {
557 Ok(v) => v,
558 Err(e) => return (1, format!("ulimit: {}\n", e)),
559 };
560
561 let (soft_opt, hard_opt) = if use_hard {
562 (None, Some(val))
563 } else {
564 (Some(val), None)
565 };
566
567 if let Err(e) = limits.set(res, soft_opt, hard_opt) {
568 return (1, format!("ulimit: {}\n", e));
569 }
570
571 i += 1;
572 }
573
574 if let Some(info) = limits.find_by_res(res) {
575 if let Ok((s, h)) = limits.get(res) {
576 let val = if use_hard { h } else { s };
577 output.push_str(&format_ulimit_display(info, val, false));
578 output.push('\n');
579 }
580 }
581
582 (0, output)
583}
584
585pub fn builtin_unlimit(args: &[&str], limits: &mut ResourceLimits, hard: bool) -> (i32, String) {
587 if args.is_empty() {
588 for info in KNOWN_RESOURCES {
589 if let Err(e) = limits.unlimit(info.res, hard) {
590 if hard {
591 return (1, format!("unlimit: {}: {}\n", info.name, e));
592 }
593 }
594 }
595 return (0, String::new());
596 }
597
598 for name in args {
599 let info = match limits.find_by_name(name) {
600 Some(info) => info,
601 None => {
602 if name.chars().all(|c| c.is_ascii_digit()) {
603 let res: i32 = match name.parse() {
604 Ok(n) => n,
605 Err(_) => return (1, "unlimit: invalid resource number\n".to_string()),
606 };
607 if let Err(e) = limits.unlimit(res, hard) {
608 return (1, format!("unlimit: {}\n", e));
609 }
610 continue;
611 }
612 return (1, format!("unlimit: no such resource: {}\n", name));
613 }
614 };
615
616 if let Err(e) = limits.unlimit(info.res, hard) {
617 return (1, format!("unlimit: {}: {}\n", info.name, e));
618 }
619 }
620
621 (0, String::new())
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627
628 #[test]
629 fn test_limit_value_format() {
630 assert_eq!(LimitValue::Unlimited.format(None), "unlimited");
631 assert_eq!(LimitValue::Value(1234).format(None), "1234");
632 }
633
634 #[test]
635 fn test_parse_limit_unlimited() {
636 let info = &KNOWN_RESOURCES[0]; assert_eq!(
638 parse_limit_value("unlimited", Some(info)).unwrap(),
639 LimitValue::Unlimited
640 );
641 }
642
643 #[test]
644 #[cfg(unix)]
645 fn test_parse_limit_time() {
646 let info = KNOWN_RESOURCES
647 .iter()
648 .find(|i| i.limit_type == LimitType::Time)
649 .unwrap();
650
651 assert_eq!(
652 parse_limit_value("60", Some(info)).unwrap(),
653 LimitValue::Value(60)
654 );
655 assert_eq!(
656 parse_limit_value("1h", Some(info)).unwrap(),
657 LimitValue::Value(3600)
658 );
659 assert_eq!(
660 parse_limit_value("5m", Some(info)).unwrap(),
661 LimitValue::Value(300)
662 );
663 assert_eq!(
664 parse_limit_value("1:30", Some(info)).unwrap(),
665 LimitValue::Value(3600 + 30 * 60)
666 );
667 assert_eq!(
668 parse_limit_value("1:30:45", Some(info)).unwrap(),
669 LimitValue::Value(3600 + 30 * 60 + 45)
670 );
671 }
672
673 #[test]
674 #[cfg(unix)]
675 fn test_parse_limit_memory() {
676 let info = KNOWN_RESOURCES
677 .iter()
678 .find(|i| i.limit_type == LimitType::Memory)
679 .unwrap();
680
681 assert_eq!(
682 parse_limit_value("100", Some(info)).unwrap(),
683 LimitValue::Value(100 * 1024)
684 );
685 assert_eq!(
686 parse_limit_value("100k", Some(info)).unwrap(),
687 LimitValue::Value(100 * 1024)
688 );
689 assert_eq!(
690 parse_limit_value("10M", Some(info)).unwrap(),
691 LimitValue::Value(10 * 1024 * 1024)
692 );
693 assert_eq!(
694 parse_limit_value("1G", Some(info)).unwrap(),
695 LimitValue::Value(1024 * 1024 * 1024)
696 );
697 }
698
699 #[test]
700 #[cfg(unix)]
701 fn test_find_resource() {
702 let limits = ResourceLimits::new();
703
704 assert!(limits.find_by_name("cpu").is_some());
705 assert!(limits.find_by_name("cputime").is_some());
706 assert!(limits.find_by_name("file").is_some());
707 assert!(limits.find_by_name("nonexistent").is_none());
708
709 assert!(limits.find_by_opt('t').is_some());
710 assert!(limits.find_by_opt('f').is_some());
711 assert!(limits.find_by_opt('z').is_none());
712 }
713
714 #[test]
715 #[cfg(unix)]
716 fn test_get_limits() {
717 let limits = ResourceLimits::new();
718
719 let result = limits.get(RLIMIT_NOFILE as i32);
720 assert!(result.is_ok());
721
722 let (soft, hard) = result.unwrap();
723 match soft {
724 LimitValue::Unlimited => {}
725 LimitValue::Value(v) => assert!(v > 0),
726 }
727 match hard {
728 LimitValue::Unlimited => {}
729 LimitValue::Value(v) => assert!(v > 0),
730 }
731 }
732
733 #[test]
734 #[cfg(unix)]
735 fn test_list_all() {
736 let limits = ResourceLimits::new();
737 let all = limits.list_all(false);
738
739 assert!(!all.is_empty());
740 assert!(all.iter().any(|(name, _)| name == "cputime"));
741 assert!(all.iter().any(|(name, _)| name == "filesize"));
742 }
743}