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