xref: /freebsd/usr.bin/rctl/rctl.c (revision 8aac90f18aef7c9eea906c3ff9a001ca7b94f375)
1 /*-
2  * SPDX-License-Identifier: BSD-2-Clause
3  *
4  * Copyright (c) 2010 The FreeBSD Foundation
5  *
6  * This software was developed by Edward Tomasz Napierala under sponsorship
7  * from the FreeBSD Foundation.
8  *
9  * Redistribution and use in source and binary forms, with or without
10  * modification, are permitted provided that the following conditions
11  * are met:
12  * 1. Redistributions of source code must retain the above copyright
13  *    notice, this list of conditions and the following disclaimer.
14  * 2. Redistributions in binary form must reproduce the above copyright
15  *    notice, this list of conditions and the following disclaimer in the
16  *    documentation and/or other materials provided with the distribution.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
19  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21  * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
22  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
24  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
27  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
28  * SUCH DAMAGE.
29  */
30 
31 #include <sys/types.h>
32 #include <sys/rctl.h>
33 #include <sys/sysctl.h>
34 #include <assert.h>
35 #include <ctype.h>
36 #include <err.h>
37 #include <errno.h>
38 #include <getopt.h>
39 #include <grp.h>
40 #include <libutil.h>
41 #include <pwd.h>
42 #include <stdbool.h>
43 #include <stdint.h>
44 #include <stdio.h>
45 #include <stdlib.h>
46 #include <string.h>
47 
48 #define	RCTL_DEFAULT_BUFSIZE	128 * 1024
49 
50 static int
51 parse_user(const char *s, id_t *uidp, const char *unexpanded_rule)
52 {
53 	char *end;
54 	struct passwd *pwd;
55 
56 	pwd = getpwnam(s);
57 	if (pwd != NULL) {
58 		*uidp = pwd->pw_uid;
59 		return (0);
60 	}
61 
62 	if (!isnumber(s[0])) {
63 		warnx("malformed rule '%s': unknown user '%s'",
64 		    unexpanded_rule, s);
65 		return (1);
66 	}
67 
68 	*uidp = strtod(s, &end);
69 	if ((size_t)(end - s) != strlen(s)) {
70 		warnx("malformed rule '%s': trailing characters "
71 		    "after numerical id", unexpanded_rule);
72 		return (1);
73 	}
74 
75 	return (0);
76 }
77 
78 static int
79 parse_group(const char *s, id_t *gidp, const char *unexpanded_rule)
80 {
81 	char *end;
82 	struct group *grp;
83 
84 	grp = getgrnam(s);
85 	if (grp != NULL) {
86 		*gidp = grp->gr_gid;
87 		return (0);
88 	}
89 
90 	if (!isnumber(s[0])) {
91 		warnx("malformed rule '%s': unknown group '%s'",
92 		    unexpanded_rule, s);
93 		return (1);
94 	}
95 
96 	*gidp = strtod(s, &end);
97 	if ((size_t)(end - s) != strlen(s)) {
98 		warnx("malformed rule '%s': trailing characters "
99 		    "after numerical id", unexpanded_rule);
100 		return (1);
101 	}
102 
103 	return (0);
104 }
105 
106 /*
107  * Replace human-readable number with its expanded form.
108  */
109 static char *
110 expand_amount(const char *rule, const char *unexpanded_rule)
111 {
112 	uint64_t num;
113 	const char *subject, *subject_id, *resource, *action, *amount, *per;
114 	char *copy, *expanded, *tofree;
115 	int ret;
116 
117 	tofree = copy = strdup(rule);
118 	if (copy == NULL) {
119 		warn("strdup");
120 		return (NULL);
121 	}
122 
123 	subject = strsep(&copy, ":");
124 	subject_id = strsep(&copy, ":");
125 	resource = strsep(&copy, ":");
126 	action = strsep(&copy, "=/");
127 	amount = strsep(&copy, "/");
128 	per = copy;
129 
130 	if (amount == NULL || strlen(amount) == 0) {
131 		/*
132 		 * The "copy" has already been tinkered with by strsep().
133 		 */
134 		free(tofree);
135 		copy = strdup(rule);
136 		if (copy == NULL) {
137 			warn("strdup");
138 			return (NULL);
139 		}
140 		return (copy);
141 	}
142 
143 	assert(subject != NULL);
144 	assert(subject_id != NULL);
145 	assert(resource != NULL);
146 	assert(action != NULL);
147 
148 	if (expand_number(amount, &num)) {
149 		warnx("malformed rule '%s': invalid numeric value '%s'",
150 		    unexpanded_rule, amount);
151 		free(tofree);
152 		return (NULL);
153 	}
154 
155 	if (per == NULL) {
156 		ret = asprintf(&expanded, "%s:%s:%s:%s=%ju",
157 		    subject, subject_id, resource, action, (uintmax_t)num);
158 	} else {
159 		ret = asprintf(&expanded, "%s:%s:%s:%s=%ju/%s",
160 		    subject, subject_id, resource, action, (uintmax_t)num, per);
161 	}
162 
163 	if (ret <= 0) {
164 		warn("asprintf");
165 		free(tofree);
166 		return (NULL);
167 	}
168 
169 	free(tofree);
170 
171 	return (expanded);
172 }
173 
174 static char *
175 expand_rule(const char *rule, bool resolve_ids)
176 {
177 	id_t id;
178 	const char *subject, *textid, *rest;
179 	char *copy, *expanded, *resolved, *tofree;
180 	int error, ret;
181 
182 	tofree = copy = strdup(rule);
183 	if (copy == NULL) {
184 		warn("strdup");
185 		return (NULL);
186 	}
187 
188 	subject = strsep(&copy, ":");
189 	textid = strsep(&copy, ":");
190 	if (textid == NULL) {
191 		warnx("malformed rule '%s': missing subject", rule);
192 		return (NULL);
193 	}
194 	if (copy != NULL)
195 		rest = copy;
196 	else
197 		rest = "";
198 
199 	if (strcasecmp(subject, "u") == 0)
200 		subject = "user";
201 	else if (strcasecmp(subject, "g") == 0)
202 		subject = "group";
203 	else if (strcasecmp(subject, "p") == 0)
204 		subject = "process";
205 	else if (strcasecmp(subject, "l") == 0 ||
206 	    strcasecmp(subject, "c") == 0 ||
207 	    strcasecmp(subject, "class") == 0)
208 		subject = "loginclass";
209 	else if (strcasecmp(subject, "j") == 0)
210 		subject = "jail";
211 
212 	if (resolve_ids &&
213 	    strcasecmp(subject, "user") == 0 && strlen(textid) > 0) {
214 		error = parse_user(textid, &id, rule);
215 		if (error != 0) {
216 			free(tofree);
217 			return (NULL);
218 		}
219 		ret = asprintf(&resolved, "%s:%d:%s", subject, (int)id, rest);
220 	} else if (resolve_ids &&
221 	    strcasecmp(subject, "group") == 0 && strlen(textid) > 0) {
222 		error = parse_group(textid, &id, rule);
223 		if (error != 0) {
224 			free(tofree);
225 			return (NULL);
226 		}
227 		ret = asprintf(&resolved, "%s:%d:%s", subject, (int)id, rest);
228 	} else {
229 		ret = asprintf(&resolved, "%s:%s:%s", subject, textid, rest);
230 	}
231 
232 	if (ret <= 0) {
233 		warn("asprintf");
234 		free(tofree);
235 		return (NULL);
236 	}
237 
238 	free(tofree);
239 
240 	expanded = expand_amount(resolved, rule);
241 	free(resolved);
242 
243 	return (expanded);
244 }
245 
246 static char *
247 humanize_ids(char *rule)
248 {
249 	id_t id;
250 	struct passwd *pwd;
251 	struct group *grp;
252 	const char *subject, *textid, *rest;
253 	char *end, *humanized;
254 	int ret;
255 
256 	subject = strsep(&rule, ":");
257 	textid = strsep(&rule, ":");
258 	if (textid == NULL)
259 		errx(1, "rule passed from the kernel didn't contain subject");
260 	if (rule != NULL)
261 		rest = rule;
262 	else
263 		rest = "";
264 
265 	/* Replace numerical user and group ids with names. */
266 	if (strcasecmp(subject, "user") == 0) {
267 		id = strtod(textid, &end);
268 		if ((size_t)(end - textid) != strlen(textid))
269 			errx(1, "malformed uid '%s'", textid);
270 		pwd = getpwuid(id);
271 		if (pwd != NULL)
272 			textid = pwd->pw_name;
273 	} else if (strcasecmp(subject, "group") == 0) {
274 		id = strtod(textid, &end);
275 		if ((size_t)(end - textid) != strlen(textid))
276 			errx(1, "malformed gid '%s'", textid);
277 		grp = getgrgid(id);
278 		if (grp != NULL)
279 			textid = grp->gr_name;
280 	}
281 
282 	ret = asprintf(&humanized, "%s:%s:%s", subject, textid, rest);
283 	if (ret <= 0)
284 		err(1, "asprintf");
285 
286 	return (humanized);
287 }
288 
289 static int
290 str2int64(const char *str, int64_t *value)
291 {
292 	char *end;
293 
294 	if (str == NULL)
295 		return (EINVAL);
296 
297 	*value = strtoul(str, &end, 10);
298 	if ((size_t)(end - str) != strlen(str))
299 		return (EINVAL);
300 
301 	return (0);
302 }
303 
304 static char *
305 humanize_amount(char *rule)
306 {
307 	int64_t num;
308 	const char *subject, *subject_id, *resource, *action, *amount, *per;
309 	char *copy, *humanized, buf[6], *tofree;
310 	int ret;
311 
312 	tofree = copy = strdup(rule);
313 	if (copy == NULL)
314 		err(1, "strdup");
315 
316 	subject = strsep(&copy, ":");
317 	subject_id = strsep(&copy, ":");
318 	resource = strsep(&copy, ":");
319 	action = strsep(&copy, "=/");
320 	amount = strsep(&copy, "/");
321 	per = copy;
322 
323 	if (amount == NULL || strlen(amount) == 0 ||
324 	    str2int64(amount, &num) != 0) {
325 		free(tofree);
326 		return (rule);
327 	}
328 
329 	assert(subject != NULL);
330 	assert(subject_id != NULL);
331 	assert(resource != NULL);
332 	assert(action != NULL);
333 
334 	if (humanize_number(buf, sizeof(buf), num, "", HN_AUTOSCALE,
335 	    HN_DECIMAL | HN_NOSPACE) == -1)
336 		err(1, "humanize_number");
337 
338 	if (per == NULL) {
339 		ret = asprintf(&humanized, "%s:%s:%s:%s=%s",
340 		    subject, subject_id, resource, action, buf);
341 	} else {
342 		ret = asprintf(&humanized, "%s:%s:%s:%s=%s/%s",
343 		    subject, subject_id, resource, action, buf, per);
344 	}
345 
346 	if (ret <= 0)
347 		err(1, "asprintf");
348 
349 	free(tofree);
350 	return (humanized);
351 }
352 
353 /*
354  * Print rules, one per line.
355  */
356 static void
357 print_rules(char *rules, int hflag, int nflag)
358 {
359 	char *rule;
360 
361 	while ((rule = strsep(&rules, ",")) != NULL) {
362 		if (rule[0] == '\0')
363 			break; /* XXX */
364 		if (nflag == 0)
365 			rule = humanize_ids(rule);
366 		if (hflag)
367 			rule = humanize_amount(rule);
368 		printf("%s\n", rule);
369 	}
370 }
371 
372 static void
373 enosys(void)
374 {
375 	size_t racct_enable_len;
376 	int error;
377 	bool racct_enable;
378 
379 	racct_enable_len = sizeof(racct_enable);
380 	error = sysctlbyname("kern.racct.enable",
381 	    &racct_enable, &racct_enable_len, NULL, 0);
382 
383 	if (error != 0) {
384 		if (errno == ENOENT)
385 			errx(1, "RACCT/RCTL support not present in kernel; see rctl(8) for details");
386 
387 		err(1, "sysctlbyname");
388 	}
389 
390 	if (!racct_enable)
391 		errx(1, "RACCT/RCTL present, but disabled; enable using kern.racct.enable=1 tunable");
392 }
393 
394 static int
395 add_rule(const char *rule, const char *unexpanded_rule)
396 {
397 	int error;
398 
399 	error = rctl_add_rule(rule, strlen(rule) + 1, NULL, 0);
400 	if (error != 0) {
401 		if (errno == ENOSYS)
402 			enosys();
403 		warn("failed to add rule '%s'", unexpanded_rule);
404 	}
405 
406 	return (error);
407 }
408 
409 static int
410 show_limits(const char *filter, const char *unexpanded_rule,
411     int hflag, int nflag)
412 {
413 	int error;
414 	char *outbuf = NULL;
415 	size_t outbuflen = RCTL_DEFAULT_BUFSIZE / 4;
416 
417 	for (;;) {
418 		outbuflen *= 4;
419 		outbuf = realloc(outbuf, outbuflen);
420 		if (outbuf == NULL)
421 			err(1, "realloc");
422 		error = rctl_get_limits(filter, strlen(filter) + 1,
423 		    outbuf, outbuflen);
424 		if (error == 0)
425 			break;
426 		if (errno == ERANGE)
427 			continue;
428 		if (errno == ENOSYS)
429 			enosys();
430 		warn("failed to get limits for '%s'", unexpanded_rule);
431 		free(outbuf);
432 
433 		return (error);
434 	}
435 
436 	print_rules(outbuf, hflag, nflag);
437 	free(outbuf);
438 
439 	return (error);
440 }
441 
442 static int
443 remove_rule(const char *filter, const char *unexpanded_rule)
444 {
445 	int error;
446 
447 	error = rctl_remove_rule(filter, strlen(filter) + 1, NULL, 0);
448 	if (error != 0) {
449 		if (errno == ENOSYS)
450 			enosys();
451 		warn("failed to remove rule '%s'", unexpanded_rule);
452 	}
453 
454 	return (error);
455 }
456 
457 static char *
458 humanize_usage_amount(char *usage)
459 {
460 	int64_t num;
461 	const char *resource, *amount;
462 	char *copy, *humanized, buf[6], *tofree;
463 	int ret;
464 
465 	tofree = copy = strdup(usage);
466 	if (copy == NULL)
467 		err(1, "strdup");
468 
469 	resource = strsep(&copy, "=");
470 	amount = copy;
471 
472 	assert(resource != NULL);
473 	assert(amount != NULL);
474 
475 	if (str2int64(amount, &num) != 0 ||
476 	    humanize_number(buf, sizeof(buf), num, "", HN_AUTOSCALE,
477 	    HN_DECIMAL | HN_NOSPACE) == -1) {
478 		free(tofree);
479 		return (usage);
480 	}
481 
482 	ret = asprintf(&humanized, "%s=%s", resource, buf);
483 	if (ret <= 0)
484 		err(1, "asprintf");
485 
486 	free(tofree);
487 	return (humanized);
488 }
489 
490 /*
491  * Query the kernel about a resource usage and print it out.
492  */
493 static int
494 show_usage(const char *filter, const char *unexpanded_rule, int hflag)
495 {
496 	int error;
497 	char *copy, *outbuf = NULL, *tmp;
498 	size_t outbuflen = RCTL_DEFAULT_BUFSIZE / 4;
499 
500 	for (;;) {
501 		outbuflen *= 4;
502 		outbuf = realloc(outbuf, outbuflen);
503 		if (outbuf == NULL)
504 			err(1, "realloc");
505 		error = rctl_get_racct(filter, strlen(filter) + 1,
506 		    outbuf, outbuflen);
507 		if (error == 0)
508 			break;
509 		if (errno == ERANGE)
510 			continue;
511 		if (errno == ENOSYS)
512 			enosys();
513 		warn("failed to show resource consumption for '%s'",
514 		    unexpanded_rule);
515 		free(outbuf);
516 
517 		return (error);
518 	}
519 
520 	copy = outbuf;
521 	while ((tmp = strsep(&copy, ",")) != NULL) {
522 		if (tmp[0] == '\0')
523 			break; /* XXX */
524 
525 		if (hflag)
526 			tmp = humanize_usage_amount(tmp);
527 
528 		printf("%s\n", tmp);
529 	}
530 
531 	free(outbuf);
532 
533 	return (error);
534 }
535 
536 /*
537  * Query the kernel about resource limit rules and print them out.
538  */
539 static int
540 show_rules(const char *filter, const char *unexpanded_rule,
541     int hflag, int nflag)
542 {
543 	int error;
544 	char *outbuf = NULL;
545 	size_t filterlen, outbuflen = RCTL_DEFAULT_BUFSIZE / 4;
546 
547 	if (filter != NULL)
548 		filterlen = strlen(filter) + 1;
549 	else
550 		filterlen = 0;
551 
552 	for (;;) {
553 		outbuflen *= 4;
554 		outbuf = realloc(outbuf, outbuflen);
555 		if (outbuf == NULL)
556 			err(1, "realloc");
557 		error = rctl_get_rules(filter, filterlen, outbuf, outbuflen);
558 		if (error == 0)
559 			break;
560 		if (errno == ERANGE)
561 			continue;
562 		if (errno == ENOSYS)
563 			enosys();
564 		warn("failed to show rules for '%s'", unexpanded_rule);
565 		free(outbuf);
566 
567 		return (error);
568 	}
569 
570 	print_rules(outbuf, hflag, nflag);
571 	free(outbuf);
572 
573 	return (error);
574 }
575 
576 static void
577 usage(void)
578 {
579 
580 	fprintf(stderr, "usage: rctl [ -h ] [-a rule | -l filter | -r filter "
581 	    "| -u filter | filter]\n");
582 	exit(1);
583 }
584 
585 int
586 main(int argc, char **argv)
587 {
588 	int ch, aflag = 0, hflag = 0, nflag = 0, lflag = 0, rflag = 0,
589 	    uflag = 0;
590 	char *rule = NULL, *unexpanded_rule;
591 	int i, cumulated_error, error;
592 
593 	while ((ch = getopt(argc, argv, "ahlnru")) != -1) {
594 		switch (ch) {
595 		case 'a':
596 			aflag = 1;
597 			break;
598 		case 'h':
599 			hflag = 1;
600 			break;
601 		case 'l':
602 			lflag = 1;
603 			break;
604 		case 'n':
605 			nflag = 1;
606 			break;
607 		case 'r':
608 			rflag = 1;
609 			break;
610 		case 'u':
611 			uflag = 1;
612 			break;
613 
614 		case '?':
615 		default:
616 			usage();
617 		}
618 	}
619 
620 	argc -= optind;
621 	argv += optind;
622 
623 	if (aflag + lflag + rflag + uflag > 1)
624 		errx(1, "at most one of -a, -l, -r, or -u may be specified");
625 
626 	if (argc == 0) {
627 		if (aflag + lflag + rflag + uflag == 0) {
628 			rule = strdup("::");
629 			show_rules(rule, rule, hflag, nflag);
630 
631 			return (0);
632 		}
633 
634 		usage();
635 	}
636 
637 	cumulated_error = 0;
638 
639 	for (i = 0; i < argc; i++) {
640 		unexpanded_rule = argv[i];
641 
642 		/*
643 		 * Skip resolving if passed -n _and_ -a.  Ignore -n otherwise,
644 		 * so we can still do "rctl -n u:root" and see the rules without
645 		 * resolving the UID.
646 		 */
647 		if (aflag != 0 && nflag != 0)
648 			rule = expand_rule(unexpanded_rule, false);
649 		else
650 			rule = expand_rule(unexpanded_rule, true);
651 
652 		if (rule == NULL) {
653 			cumulated_error++;
654 			continue;
655 		}
656 
657 		/*
658 		 * The reason for passing the unexpanded_rule is to make
659 		 * it easier for the user to search for the problematic
660 		 * rule in the passed input.
661 		 */
662 		if (aflag) {
663 			error = add_rule(rule, unexpanded_rule);
664 		} else if (lflag) {
665 			error = show_limits(rule, unexpanded_rule,
666 			    hflag, nflag);
667 		} else if (rflag) {
668 			error = remove_rule(rule, unexpanded_rule);
669 		} else if (uflag) {
670 			error = show_usage(rule, unexpanded_rule, hflag);
671 		} else  {
672 			error = show_rules(rule, unexpanded_rule,
673 			    hflag, nflag);
674 		}
675 
676 		if (error != 0)
677 			cumulated_error++;
678 
679 		free(rule);
680 	}
681 
682 	return (cumulated_error);
683 }
684