git: e82a62943529 - main - jail: add ".include" directive to jail.conf

From: Jamie Gritton <jamie_at_FreeBSD.org>
Date: Wed, 07 Jun 2023 00:21:53 UTC
The branch main has been updated by jamie:

URL: https://cgit.FreeBSD.org/src/commit/?id=e82a62943529d1a7c1fcec39aec13eba69c671d6

commit e82a62943529d1a7c1fcec39aec13eba69c671d6
Author:     Jamie Gritton <jamie@FreeBSD.org>
AuthorDate: 2023-06-07 00:19:12 +0000
Commit:     Jamie Gritton <jamie@FreeBSD.org>
CommitDate: 2023-06-07 00:19:12 +0000

    jail: add ".include" directive to jail.conf
    
    Jail config files can now include literal filenames and file globs.
    They can not (yet) include files based on variables/parameters.
---
 usr.sbin/jail/config.c    | 86 ++++++++++++++++++++++++++++++++++++-----------
 usr.sbin/jail/jail.conf.5 | 17 +++++++++-
 usr.sbin/jail/jailp.h     | 27 ++++++++++-----
 usr.sbin/jail/jailparse.y | 47 ++++++++++++++++++++------
 4 files changed, 137 insertions(+), 40 deletions(-)

diff --git a/usr.sbin/jail/config.c b/usr.sbin/jail/config.c
index 52e1cbf05c28..35f40c123042 100644
--- a/usr.sbin/jail/config.c
+++ b/usr.sbin/jail/config.c
@@ -38,6 +38,7 @@ __FBSDID("$FreeBSD$");
 #include <netinet/in.h>
 
 #include <err.h>
+#include <glob.h>
 #include <netdb.h>
 #include <stdio.h>
 #include <stdlib.h>
@@ -46,6 +47,8 @@ __FBSDID("$FreeBSD$");
 
 #include "jailp.h"
 
+#define MAX_INCLUDE_DEPTH 32
+
 struct ipspec {
 	const char	*name;
 	unsigned	flags;
@@ -58,8 +61,8 @@ extern int yyset_in(FILE *fp, void *scanner);
 
 struct cfjails cfjails = TAILQ_HEAD_INITIALIZER(cfjails);
 
+static void parse_config(const char *fname, int is_stdin);
 static void free_param(struct cfparams *pp, struct cfparam *p);
-static void free_param_strings(struct cfparam *p);
 
 static const struct ipspec intparams[] = {
     [IP_ALLOW_DYING] =		{"allow.dying",		PF_INTERNAL | PF_BOOL},
@@ -135,26 +138,10 @@ load_config(const char *cfname)
 	struct cfparam *p, *vp, *tp;
 	struct cfstring *s, *vs, *ns;
 	struct cfvar *v, *vv;
-	struct cflex cflex;
 	char *ep;
-	void *scanner;
 	int did_self, jseq, pgen;
 
-	cflex.cfname = cfname;
-	cflex.error = 0;
-	yylex_init_extra(&cflex, &scanner);
-	if (!strcmp(cfname, "-")) {
-		cflex.cfname = "STDIN";
-		yyset_in(stdin, scanner);
-	} else {
-		FILE *yfp = fopen(cfname, "r");
-		if (!yfp)
-			err(1, "%s", cfname);
-		yyset_in(yfp, scanner);
-	}
-	if (yyparse(scanner) || cflex.error)
-		exit(1);
-	yylex_destroy(scanner);
+	parse_config(cfname, !strcmp(cfname, "-"));
 
 	/* Separate the wildcard jails out from the actual jails. */
 	jseq = 0;
@@ -281,6 +268,67 @@ load_config(const char *cfname)
 	}
 }
 
+void
+include_config(void *scanner, const char *cfname)
+{
+	static unsigned int depth;
+	glob_t g = {0};
+	const char *slash;
+	char *fullpath = NULL;
+
+	/* Simple sanity check for include loops. */
+	if (++depth > MAX_INCLUDE_DEPTH)
+		errx(1, "maximum include depth exceeded");
+	/* Base relative pathnames on the current config file. */
+	if (yyget_in(scanner) != stdin && cfname[0] != '/') {
+		const char *outer_cfname = yyget_extra(scanner)->cfname;
+		if ((slash = strrchr(outer_cfname, '/')) != NULL) {
+			size_t dirlen = (slash - outer_cfname) + 1;
+
+			fullpath = emalloc(dirlen + strlen(cfname) + 1);
+			strncpy(fullpath, outer_cfname, dirlen);
+			strcpy(fullpath + dirlen, cfname);
+			cfname = fullpath;
+		}
+	}
+	/*
+	 * Check if the include statement had a filename glob.
+	 * Globbing doesn't need to catch any files, but a non-glob
+	 * file needs to exist (enforced by parse_config).
+	 */
+	if (glob(cfname, GLOB_NOCHECK, NULL, &g) != 0)
+		errx(1, "%s: filename glob failed", cfname);
+	if (g.gl_flags & GLOB_MAGCHAR) {
+		for (size_t gi = 0; gi < g.gl_matchc; gi++)
+			parse_config(g.gl_pathv[gi], 0);
+	} else
+		parse_config(cfname, 0);
+	if (fullpath)
+		free(fullpath);
+	--depth;
+}
+
+static void
+parse_config(const char *cfname, int is_stdin)
+{
+	struct cflex cflex = {.cfname = cfname, .error = 0};
+	void *scanner;
+
+	yylex_init_extra(&cflex, &scanner);
+	if (is_stdin) {
+		cflex.cfname = "STDIN";
+		yyset_in(stdin, scanner);
+	} else {
+		FILE *yfp = fopen(cfname, "r");
+		if (!yfp)
+			err(1, "%s", cfname);
+		yyset_in(yfp, scanner);
+	}
+	if (yyparse(scanner) || cflex.error)
+		exit(1);
+	yylex_destroy(scanner);
+}
+
 /*
  * Create a new jail record.
  */
@@ -843,7 +891,7 @@ free_param(struct cfparams *pp, struct cfparam *p)
 	free(p);
 }
 
-static void
+void
 free_param_strings(struct cfparam *p)
 {
 	struct cfstring *s;
diff --git a/usr.sbin/jail/jail.conf.5 b/usr.sbin/jail/jail.conf.5
index 67277e6d78ce..a6990c081a3b 100644
--- a/usr.sbin/jail/jail.conf.5
+++ b/usr.sbin/jail/jail.conf.5
@@ -24,7 +24,7 @@
 .\"
 .\" $FreeBSD$
 .\"
-.Dd July 8, 2022
+.Dd Jun 3, 2023
 .Dt JAIL.CONF 5
 .Os
 .Sh NAME
@@ -163,6 +163,14 @@ would apply to jails with names like
 .Dq foo.bar
 and
 .Dq foo.bar.baz .
+.Ss Includes
+A line of the form
+.Bd -literal -offset ident
+.include "filename";
+.Ed
+.Pp
+will include another file in the configuration.  The filename must be
+a literal string, and cannot contain variable expansions.
 .Ss Comments
 The configuration file may contain comments in the common C, C++, and
 shell formats:
@@ -212,6 +220,13 @@ bar {
 	mount.nodevfs;
 	persist;	// Required because there are no processes
 }
+
+# Include configurations from standard locations.
+\[char46]include "/etc/jail.conf.d/*.conf";
+\[char46]include "/etc/jail.*.conf";
+\[char46]include "/usr/local/etc/jail[.]conf";
+\[char46]include "/usr/local/etc/jail.conf.d/*.conf";
+\[char46]include "/usr/local/etc/jail.*.conf";
 .Ed
 .Sh SEE ALSO
 .Xr jail_set 2 ,
diff --git a/usr.sbin/jail/jailp.h b/usr.sbin/jail/jailp.h
index 6ea4e3ec09a3..cd97063507c8 100644
--- a/usr.sbin/jail/jailp.h
+++ b/usr.sbin/jail/jailp.h
@@ -35,6 +35,7 @@
 #include <sys/time.h>
 
 #include <jail.h>
+#include <stdio.h>
 
 #define CONF_FILE	"/etc/jail.conf"
 
@@ -45,15 +46,16 @@
 #define DF_LIGHT	0x02	/* Implied dependency on jail existence only */
 #define DF_NOFAIL	0x04	/* Don't propagate failed jails */
 
-#define PF_VAR		0x01	/* This is a variable, not a true parameter */
-#define PF_APPEND	0x02	/* Append to existing parameter list */
-#define PF_BAD		0x04	/* Unable to resolve parameter value */
-#define PF_INTERNAL	0x08	/* Internal parameter, not passed to kernel */
-#define PF_BOOL		0x10	/* Boolean parameter */
-#define PF_INT		0x20	/* Integer parameter */
-#define PF_CONV		0x40	/* Parameter duplicated in converted form */
-#define PF_REV		0x80	/* Run commands in reverse order on stopping */
-#define	PF_IMMUTABLE	0x100	/* Immutable parameter */
+#define PF_VAR		0x0001	/* This is a variable, not a true parameter */
+#define PF_APPEND	0x0002	/* Append to existing parameter list */
+#define PF_BAD		0x0004	/* Unable to resolve parameter value */
+#define PF_INTERNAL	0x0008	/* Internal parameter, not passed to kernel */
+#define PF_BOOL		0x0010	/* Boolean parameter */
+#define PF_INT		0x0020	/* Integer parameter */
+#define PF_CONV		0x0040	/* Parameter duplicated in converted form */
+#define PF_REV		0x0080	/* Run commands in reverse order on stopping */
+#define	PF_IMMUTABLE	0x0100	/* Immutable parameter */
+#define	PF_NAMEVAL	0x0200	/* Parameter is in "name value" form */
 
 #define JF_START	0x0001	/* -c */
 #define JF_SET		0x0002	/* -m */
@@ -215,6 +217,7 @@ extern int finish_command(struct cfjail *j);
 extern struct cfjail *next_proc(int nonblock);
 
 extern void load_config(const char *cfname);
+extern void include_config(void *scanner, const char *cfname);
 extern struct cfjail *add_jail(void);
 extern void add_param(struct cfjail *j, const struct cfparam *p,
     enum intparam ipnum, const char *value);
@@ -226,6 +229,7 @@ extern int import_params(struct cfjail *j);
 extern int equalopts(const char *opt1, const char *opt2);
 extern int wild_jail_name(const char *wname);
 extern int wild_jail_match(const char *jname, const char *wname);
+extern void free_param_strings(struct cfparam *p);
 
 extern void dep_setup(int docf);
 extern int dep_check(struct cfjail *j);
@@ -237,6 +241,11 @@ extern int start_state(const char *target, int docf, unsigned state,
 extern void requeue(struct cfjail *j, struct cfjails *queue);
 extern void requeue_head(struct cfjail *j, struct cfjails *queue);
 
+extern struct cflex *yyget_extra(void *scanner);
+extern FILE *yyget_in(void *scanner);
+extern int yyget_lineno(void *scanner);
+extern char *yyget_text(void *scanner);
+
 extern struct cfjails cfjails;
 extern struct cfjails ready;
 extern struct cfjails depend;
diff --git a/usr.sbin/jail/jailparse.y b/usr.sbin/jail/jailparse.y
index ccc311a76223..e4f2310c7fb4 100644
--- a/usr.sbin/jail/jailparse.y
+++ b/usr.sbin/jail/jailparse.y
@@ -73,16 +73,18 @@ conf	:
 	| conf jail
 	| conf param ';'
 	{
-		struct cfjail *j = current_jail;
+		if (!special_param($2, scanner)) {
+			struct cfjail *j = current_jail;
 
-		if (j == NULL) {
-			if (global_jail == NULL) {
-				global_jail = add_jail();
-				global_jail->name = estrdup("*");
+			if (j == NULL) {
+				if (global_jail == NULL) {
+					global_jail = add_jail();
+					global_jail->name = estrdup("*");
+				}
+				j = global_jail;
 			}
-			j = global_jail;
+			TAILQ_INSERT_TAIL(&j->params, $2, tq);
 		}
-		TAILQ_INSERT_TAIL(&j->params, $2, tq);
 	}
 	| conf ';'
 	;
@@ -141,6 +143,7 @@ param	: name
 	{
 		$$ = $1;
 		TAILQ_CONCAT(&$$->val, $2, tq);
+		$$->flags |= PF_NAMEVAL;
 		free($2);
 	}
 	| error
@@ -230,10 +233,6 @@ string	: STR
 
 extern int YYLEX_DECL();
 
-extern struct cflex *yyget_extra(void *scanner);
-extern int yyget_lineno(void *scanner);
-extern char *yyget_text(void *scanner);
-
 static void
 YYERROR_DECL()
 {
@@ -248,3 +247,29 @@ YYERROR_DECL()
 		    yyget_extra(scanner)->cfname, yyget_lineno(scanner),
 		    yyget_text(scanner), s);
 }
+
+/* Handle special parameters (i.e. the include directive).
+ * Return true if the parameter was specially handled.
+ */
+static int
+special_param(struct cfparam *p, void *scanner)
+{
+	if ((p->flags & (PF_VAR | PF_APPEND | PF_NAMEVAL)) != PF_NAMEVAL
+	    || strcmp(p->name, ".include"))
+		return 0;
+	struct cfstring *s;
+	TAILQ_FOREACH(s, &p->val, tq) {
+		if (STAILQ_EMPTY(&s->vars))
+			include_config(scanner, s->s);
+		else {
+			warnx("%s line %d: "
+			    "variables not permitted in '.include' filename",
+			    yyget_extra(scanner)->cfname,
+			    yyget_lineno(scanner));
+			yyget_extra(scanner)->error = 1;
+		}
+	}
+	free_param_strings(p);
+	free(p);
+	return 1;
+}