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
55static int
56parse_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
83static int
84parse_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 */
114static char *
115expand_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
179static char *
180expand_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
251static char *
252humanize_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
294static int
295str2int64(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
309static char *
310humanize_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 */
361static void
362print_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
377static void
378enosys(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
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