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
56static int
57parse_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
84static int
85parse_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 */
115static char *
116expand_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
180static char *
181expand_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
252static char *
253humanize_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
295static int
296str2int64(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
310static char *
311humanize_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 */
362static void
363print_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
378static void
379enosys(void)
380{
381	int error, racct_enable;
382	size_t racct_enable_len;
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 == 0)
396		errx(1, "RACCT/RCTL present, but disabled; enable using kern.racct.enable=1 tunable");
397}
398
399static int
400add_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
414static int
415show_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
447static int
448remove_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
462static char *
463humanize_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 */
498static int
499show_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 */
544static int
545show_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
581static void
582usage(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
590int
591main(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