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