diff --git a/usr.sbin/jail/config.c b/usr.sbin/jail/config.c --- a/usr.sbin/jail/config.c +++ b/usr.sbin/jail/config.c @@ -29,14 +29,19 @@ #include #include #include +#include #include +#include #include #include #include +#include #include +#include #include +#include #include #include #include @@ -306,10 +311,107 @@ --depth; } +extern char **environ; + +static FILE * +open_config(const char *cfname, pid_t pid[static 1]) +{ + + int read_fd, write_fd, exec_fd, pipes[2]; + FILE *file; + + /* Invalidate the child PID. */ + *pid = -1; + + /* The configuration must be readable. */ + read_fd = open(cfname, O_RDONLY|O_CLOEXEC); + if (read_fd < 0) + err(1, "open(): %s", cfname); + + /* Try to open the configuration for execution (to parse its standard output config instead). */ + exec_fd = openat(read_fd, "", O_EMPTY_PATH|O_EXEC); + if (exec_fd < 0) { + if (errno != EACCES) + err(1, "openat(): %s", cfname); + } else if (!xflag) { + errx(1, "invoke jail with -x to execute jail.conf"); + } else { + const size_t cfname_size = strlen(cfname) + 1; + char dir_buf[PATH_MAX], base_buf[PATH_MAX]; + if (cfname_size > PATH_MAX) { + errno = ENAMETOOLONG; + err(1, "open_config(): %s", cfname); + } + + /* Set the argument list: . */ + const struct cfjail *const current_jail = TAILQ_LAST(&cfjails, cfjails); + char *const jail_conf = __DECONST(char *, cfname); + char *const jail_dir = dirname(memcpy(dir_buf, cfname, cfname_size)); + char *const jail_base = basename(memcpy(base_buf, cfname, cfname_size)); + char *const jail_name = current_jail && current_jail->name ? current_jail->name : __DECONST(char *, ""); + char *argv[] = { jail_conf, jail_conf, jail_dir, jail_base, jail_name, NULL }; + + /* Read from the pipe instead of the configuration. */ + close(read_fd); + if (pipe(pipes)) + err(1, "pipe(): %s", cfname); + read_fd = pipes[0]; + write_fd = pipes[1]; + + /* Run the configuration as child process. */ + switch ((*pid = fork())) { + + /* Failed to fork(). */ + case -1: + err(1, "fork(): %s", cfname); + break; + + /* After successful fork() inside the child process. */ + case 0: + /* Export the arguments to the executable configuration. */ + if (setenv("JAIL_CONF", cfname, 1)) + err(1, "setenv(\"JAIL_CONF\", \"%s\"): %s", cfname, cfname); + if (setenv("JAIL_DIR", jail_dir, 1)) + err(1, "setenv(\"JAIL_DIR\", \"%s\"): %s", jail_dir, cfname); + if (setenv("JAIL_BASE", jail_base, 1)) + err(1, "setenv(\"JAIL_BASE\", \"%s\"): %s", jail_base, cfname); + if (setenv("JAIL_NAME", jail_name, 1)) + err(1, "setenv(\"JAIL_NAME\", \"%s\"): %s", jail_name, cfname); + + /* Redirect the child's standard output into the pipe. */ + if (write_fd != STDOUT_FILENO) { + if (dup2(write_fd, STDOUT_FILENO) != STDOUT_FILENO) + err(1, "dup2(): %s", cfname); + close(write_fd); + } + + /* Replace the forked child with the configuration command. */ + fexecve(exec_fd, argv, environ); + err(1, "fexecve(): %s", cfname); + break; + + /* After successful fork() inside the parent process. */ + default: + /* Close the write end of the pipe and the executable file descriptor in the parent. */ + close(write_fd); write_fd = -1; + close(exec_fd); exec_fd = -1; + break; + } + } + + /* Wrap a FILE handle around the read-only file descriptor. */ + file = fdopen(read_fd, "r"); + if (file == NULL) + err(1, "fdopen(): %s", cfname); + + return file; +} + static void parse_config(const char *cfname, int is_stdin) { struct cflex cflex = {.cfname = cfname, .error = 0}; + pid_t child = -1; void *scanner; yylex_init_extra(&cflex, &scanner); @@ -317,7 +419,7 @@ cflex.cfname = "STDIN"; yyset_in(stdin, scanner); } else { - FILE *yfp = fopen(cfname, "r"); + FILE *yfp = open_config(cfname, &child); if (!yfp) err(1, "%s", cfname); yyset_in(yfp, scanner); @@ -325,6 +427,16 @@ if (yyparse(scanner) || cflex.error) exit(1); yylex_destroy(scanner); + if (child > 0) { + pid_t exited; + int status; + do { + exited = waitpid(child, &status, 0); + } while (exited < 0 || errno == EINTR); + status = WEXITSTATUS(status); + if (status != 0) + errx(status, "Config child failed (exit code = %i): %s", status, cfname); + } } /* diff --git a/usr.sbin/jail/jail.8 b/usr.sbin/jail/jail.8 --- a/usr.sbin/jail/jail.8 +++ b/usr.sbin/jail/jail.8 @@ -33,7 +33,7 @@ .Ss From Configuration File .Nm .Op Fl cm -.Op Fl Cdqv +.Op Fl Cdqvx .Op Fl f Ar conf_file .Op Fl p Ar limit .Op Ar jail @@ -214,6 +214,13 @@ .It Fl v Print a message on every operation, such as running commands and mounting filesystems. +.It Fl x +Allow executable jail configuration file(s). +An executable jail configuration runs during configuration parsing. +It must write valid +.Xr jail.conf 5 +to standard output. +A non-zero exit status is treated like a syntax error. .It Fl d This is deprecated and is equivalent to the .Va allow.dying diff --git a/usr.sbin/jail/jail.c b/usr.sbin/jail/jail.c --- a/usr.sbin/jail/jail.c +++ b/usr.sbin/jail/jail.c @@ -54,6 +54,7 @@ }; int iflag; +int xflag; int note_remove; int verbose; const char *separator = "\t"; @@ -180,12 +181,12 @@ #endif op = 0; - dflag = eflag = Rflag = 0; + dflag = eflag = Rflag = xflag = 0; docf = 1; cfname = CONF_FILE; JidFile = NULL; - while ((ch = getopt(argc, argv, "cCde:f:hiJ:lmn:p:qrRs:u:U:v")) != -1) { + while ((ch = getopt(argc, argv, "cCdxe:f:hiJ:lmn:p:qrRs:u:U:v")) != -1) { switch (ch) { case 'c': op |= JF_START; @@ -196,6 +197,9 @@ case 'd': dflag = 1; break; + case 'x': + xflag = 1; + break; case 'e': eflag = 1; separator = optarg; diff --git a/usr.sbin/jail/jailp.h b/usr.sbin/jail/jailp.h --- a/usr.sbin/jail/jailp.h +++ b/usr.sbin/jail/jailp.h @@ -249,6 +249,7 @@ extern struct cfjails ready; extern struct cfjails depend; extern int iflag; +extern int xflag; extern int note_remove; extern int paralimit; extern int verbose;