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