Implement @include functionality for seccomp policy files.

Policies can now use the @include directive to include other policy
files. Included files can have absolute paths or be relative to CWD.
Only a single level of includes is allowed.

Bug: 36007996
Test: New unit tests.

Change-Id: Ideb8ef5d48b6f737dc156e3c50170817e0ba91ec
diff --git a/syscall_filter.c b/syscall_filter.c
index 3318052..956fe21 100644
--- a/syscall_filter.c
+++ b/syscall_filter.c
@@ -164,6 +164,11 @@
 	return get_label_id(labels, lbl_str);
 }
 
+int is_implicit_relative_path(const char *filename)
+{
+	return filename[0] != '/' && (filename[0] != '.' || filename[1] != '/');
+}
+
 int compile_atom(struct filter_block *head, char *atom,
 		 struct bpf_labels *labels, int nr, int grp_idx)
 {
@@ -428,9 +433,57 @@
 	return head;
 }
 
+int parse_include_statement(char *policy_line, unsigned int include_level,
+			    const char **ret_filename)
+{
+	if (strncmp("@include", policy_line, strlen("@include")) != 0) {
+		warn("invalid statement '%s'", policy_line);
+		return -1;
+	}
+
+	if (policy_line[strlen("@include")] != ' ') {
+		warn("invalid include statement '%s'", policy_line);
+		return -1;
+	}
+
+	/*
+	 * Disallow nested includes: only the initial policy file can have
+	 * @include statements.
+	 * Nested includes are not currently necessary and make the policy
+	 * harder to understand.
+	 */
+	if (include_level > 0) {
+		warn("@include statement nested too deep");
+		return -1;
+	}
+
+	char *statement = policy_line;
+	/* Discard "@include" token. */
+	(void)strsep(&statement, " ");
+
+	/*
+	 * compile_filter() below receives a FILE*, so it's not trivial to open
+	 * included files relative to the initial policy filename.
+	 * To avoid mistakes, force the included file path to be absolute
+	 * (start with '/'), or to explicitly load the file relative to CWD by
+	 * using './'.
+	 */
+	const char *filename = statement;
+	if (is_implicit_relative_path(filename)) {
+		warn("compile_file: implicit relative path '%s' not supported, "
+		     "use './%s'",
+		     filename, filename);
+		return -1;
+	}
+
+	*ret_filename = filename;
+	return 0;
+}
+
 int compile_file(FILE *policy_file, struct filter_block *head,
 		 struct filter_block **arg_blocks, struct bpf_labels *labels,
-		 int use_ret_trap, int allow_logging)
+		 int use_ret_trap, int allow_logging,
+		 unsigned int include_level)
 {
 	/*
 	 * Loop through all the lines in the policy file.
@@ -442,27 +495,62 @@
 	 */
 	char *line = NULL;
 	size_t len = 0;
+	int ret = 0;
+
 	while (getline(&line, &len, policy_file) != -1) {
 		char *policy_line = line;
-		char *syscall_name = strsep(&policy_line, ":");
-		int nr = -1;
-
-		syscall_name = strip(syscall_name);
+		policy_line = strip(policy_line);
 
 		/* Allow comments and empty lines. */
-		if (*syscall_name == '#' || *syscall_name == '\0') {
+		if (*policy_line == '#' || *policy_line == '\0') {
 			/* Reuse |line| in the next getline() call. */
 			continue;
 		}
 
+		/* Allow @include statements. */
+		if (*policy_line == '@') {
+			const char *filename = NULL;
+			if (parse_include_statement(policy_line, include_level,
+						    &filename) != 0) {
+				warn("compile_file: failed to parse include "
+				     "statement");
+				ret = -1;
+				goto free_line;
+			}
+
+			FILE *included_file = fopen(filename, "re");
+			if (included_file == NULL) {
+				pwarn("compile_file: fopen('%s') failed",
+				      filename);
+				ret = -1;
+				goto free_line;
+			}
+			if (compile_file(included_file, head, arg_blocks,
+					 labels, use_ret_trap, allow_logging,
+					 ++include_level) == -1) {
+				warn("compile_file: '@include %s' failed",
+				     filename);
+				fclose(included_file);
+				ret = -1;
+				goto free_line;
+			}
+			fclose(included_file);
+			continue;
+		}
+
+		/*
+		 * If it's not a comment, or an empty line, or an @include
+		 * statement, treat |policy_line| as a regular policy line.
+		 */
+		char *syscall_name = strsep(&policy_line, ":");
 		policy_line = strip(policy_line);
 		if (*policy_line == '\0') {
 			warn("compile_file: empty policy line");
-			free(line);
-			return -1;
+			ret = -1;
+			goto free_line;
 		}
 
-		nr = lookup_syscall(syscall_name);
+		int nr = lookup_syscall(syscall_name);
 		if (nr < 0) {
 			warn("compile_file: nonexistent syscall '%s'",
 			     syscall_name);
@@ -481,8 +569,8 @@
 				/* Reuse |line| in the next getline() call. */
 				continue;
 			}
-			free(line);
-			return -1;
+			ret = -1;
+			goto free_line;
 		}
 
 		/*
@@ -511,8 +599,8 @@
 				if (*arg_blocks) {
 					free_block_list(*arg_blocks);
 				}
-				free(line);
-				return -1;
+				ret = -1;
+				goto free_line;
 			}
 
 			if (*arg_blocks) {
@@ -523,8 +611,10 @@
 		}
 		/* Reuse |line| in the next getline() call. */
 	}
+
+free_line:
 	free(line);
-	return 0;
+	return ret;
 }
 
 int compile_filter(FILE *initial_file, struct sock_fprog *prog,
@@ -556,7 +646,7 @@
 		allow_logging_syscalls(head);
 
 	if (compile_file(initial_file, head, &arg_blocks, &labels, use_ret_trap,
-			 allow_logging) != 0) {
+			 allow_logging, 0 /* include_level */) != 0) {
 		warn("compile_filter: compile_file() failed");
 		free_block_list(head);
 		free_block_list(arg_blocks);