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