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