diff options
author | Jorge Lucangeli Obes <jorgelo@google.com> | 2017-03-27 20:04:04 +0000 |
---|---|---|
committer | android-build-merger <android-build-merger@google.com> | 2017-03-27 20:04:04 +0000 |
commit | 3772d3674bda9b5d0beecb4c689e347ea5ce174b (patch) | |
tree | 8dddb9922153a5686d555ba66c52466c4f92a717 | |
parent | 2ed4130a89b1a4e69781a92080d0d32c4b05be93 (diff) | |
parent | 379130180580668ab3cd36561e3c9fdd36a13036 (diff) | |
download | minijail-3772d3674bda9b5d0beecb4c689e347ea5ce174b.tar.gz |
Implement @include functionality for seccomp policy files. am: bce4ccb48c am: 85b8238493android-vts-8.0_r2android-vts-8.0_r1oreo-dev
am: 3791301805
Change-Id: Icc24c16e58eb7c578a01e8949a1c76ad2c9712a6
-rw-r--r-- | syscall_filter.c | 120 | ||||
-rw-r--r-- | syscall_filter.h | 3 | ||||
-rw-r--r-- | syscall_filter_unittest.cc | 245 | ||||
-rw-r--r-- | test/nested.policy | 1 |
4 files changed, 348 insertions, 21 deletions
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 @@ unsigned int success_lbl(struct bpf_labels *labels, int nr) 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 @@ struct filter_block *compile_policy_line(int nr, const char *policy_line, 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 @@ int compile_file(FILE *policy_file, struct filter_block *head, */ 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 @@ int compile_file(FILE *policy_file, struct filter_block *head, /* Reuse |line| in the next getline() call. */ continue; } - free(line); - return -1; + ret = -1; + goto free_line; } /* @@ -511,8 +599,8 @@ int compile_file(FILE *policy_file, struct filter_block *head, if (*arg_blocks) { free_block_list(*arg_blocks); } - free(line); - return -1; + ret = -1; + goto free_line; } if (*arg_blocks) { @@ -523,8 +611,10 @@ int compile_file(FILE *policy_file, struct filter_block *head, } /* 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 @@ int compile_filter(FILE *initial_file, struct sock_fprog *prog, 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); diff --git a/syscall_filter.h b/syscall_filter.h index 3bfbfee..d15e8a9 100644 --- a/syscall_filter.h +++ b/syscall_filter.h @@ -32,7 +32,8 @@ struct filter_block *compile_policy_line(int nr, const char *policy_line, int do_ret_trap); 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); int compile_filter(FILE *policy_file, struct sock_fprog *prog, int do_ret_trap, int add_logging_syscalls); diff --git a/syscall_filter_unittest.cc b/syscall_filter_unittest.cc index 4a80976..85c8a55 100644 --- a/syscall_filter_unittest.cc +++ b/syscall_filter_unittest.cc @@ -948,7 +948,6 @@ class FileTest : public ::testing::Test { }; TEST_F(FileTest, seccomp_mode1) { - // struct sock_fprog actual; const char *policy = "read: 1\n" "write: 1\n" @@ -958,7 +957,7 @@ TEST_F(FileTest, seccomp_mode1) { FILE *policy_file = write_policy_to_pipe(policy, strlen(policy)); ASSERT_NE(policy_file, nullptr); int res = compile_file( - policy_file, head_, &arg_blocks_, &labels_, USE_RET_KILL, NO_LOGGING); + policy_file, head_, &arg_blocks_, &labels_, USE_RET_KILL, NO_LOGGING, 0); fclose(policy_file); /* @@ -993,7 +992,7 @@ TEST_F(FileTest, seccomp_read) { FILE *policy_file = write_policy_to_pipe(policy, strlen(policy)); ASSERT_NE(policy_file, nullptr); int res = compile_file( - policy_file, head_, &arg_blocks_, &labels_, USE_RET_KILL, NO_LOGGING); + policy_file, head_, &arg_blocks_, &labels_, USE_RET_KILL, NO_LOGGING, 0); fclose(policy_file); /* @@ -1174,7 +1173,7 @@ TEST(FilterTest, missing_atom) { struct sock_fprog actual; const char* policy = "open:\n"; - FILE* policy_file = write_policy_to_pipe(policy, strlen(policy)); + FILE *policy_file = write_policy_to_pipe(policy, strlen(policy)); ASSERT_NE(policy_file, nullptr); int res = compile_filter(policy_file, &actual, USE_RET_KILL, NO_LOGGING); @@ -1186,7 +1185,7 @@ TEST(FilterTest, whitespace_atom) { struct sock_fprog actual; const char* policy = "open:\t \n"; - FILE* policy_file = write_policy_to_pipe(policy, strlen(policy)); + FILE *policy_file = write_policy_to_pipe(policy, strlen(policy)); ASSERT_NE(policy_file, nullptr); int res = compile_filter(policy_file, &actual, USE_RET_KILL, NO_LOGGING); @@ -1315,3 +1314,239 @@ TEST(FilterTest, allow_log_but_kill) { free(actual.filter); } + +TEST(FilterTest, include_invalid_token) { + struct sock_fprog actual; + const char *invalid_token = "@unclude ./test/seccomp.policy\n"; + + FILE *policy_file = + write_policy_to_pipe(invalid_token, strlen(invalid_token)); + ASSERT_NE(policy_file, nullptr); + int res = compile_filter(policy_file, &actual, USE_RET_KILL, NO_LOGGING); + fclose(policy_file); + EXPECT_NE(res, 0); +} + +TEST(FilterTest, include_no_space) { + struct sock_fprog actual; + const char *no_space = "@includetest/seccomp.policy\n"; + + FILE *policy_file = write_policy_to_pipe(no_space, strlen(no_space)); + ASSERT_NE(policy_file, nullptr); + int res = compile_filter(policy_file, &actual, USE_RET_KILL, NO_LOGGING); + fclose(policy_file); + EXPECT_NE(res, 0); +} + +TEST(FilterTest, include_double_token) { + struct sock_fprog actual; + const char *double_token = "@includeinclude ./test/seccomp.policy\n"; + + FILE *policy_file = write_policy_to_pipe(double_token, strlen(double_token)); + ASSERT_NE(policy_file, nullptr); + int res = compile_filter(policy_file, &actual, USE_RET_KILL, NO_LOGGING); + fclose(policy_file); + EXPECT_NE(res, 0); +} + +TEST(FilterTest, include_no_file) { + struct sock_fprog actual; + const char *no_file = "@include\n"; + + FILE *policy_file = write_policy_to_pipe(no_file, strlen(no_file)); + ASSERT_NE(policy_file, nullptr); + int res = compile_filter(policy_file, &actual, USE_RET_KILL, NO_LOGGING); + fclose(policy_file); + EXPECT_NE(res, 0); +} + +TEST(FilterTest, include_space_no_file) { + struct sock_fprog actual; + const char *space_no_file = "@include \n"; + + FILE *policy_file = + write_policy_to_pipe(space_no_file, strlen(space_no_file)); + ASSERT_NE(policy_file, nullptr); + int res = compile_filter(policy_file, &actual, USE_RET_KILL, NO_LOGGING); + fclose(policy_file); + EXPECT_NE(res, 0); +} + +TEST(FilterTest, include_implicit_relative_path) { + struct sock_fprog actual; + const char *implicit_relative_path = "@include test/seccomp.policy\n"; + + FILE *policy_file = write_policy_to_pipe(implicit_relative_path, + strlen(implicit_relative_path)); + ASSERT_NE(policy_file, nullptr); + int res = compile_filter(policy_file, &actual, USE_RET_KILL, NO_LOGGING); + fclose(policy_file); + EXPECT_NE(res, 0); +} + +TEST(FilterTest, include_extra_text) { + struct sock_fprog actual; + const char *extra_text = "@include /some/file: sneaky comment\n"; + + FILE *policy_file = + write_policy_to_pipe(extra_text, strlen(extra_text)); + ASSERT_NE(policy_file, nullptr); + int res = compile_filter(policy_file, &actual, USE_RET_KILL, NO_LOGGING); + fclose(policy_file); + EXPECT_NE(res, 0); +} + +TEST(FilterTest, include_split_filename) { + struct sock_fprog actual; + const char *split_filename = "@include /some/file:colon.policy\n"; + + FILE *policy_file = + write_policy_to_pipe(split_filename, strlen(split_filename)); + ASSERT_NE(policy_file, nullptr); + int res = compile_filter(policy_file, &actual, USE_RET_KILL, NO_LOGGING); + fclose(policy_file); + EXPECT_NE(res, 0); +} + +TEST(FilterTest, include_nonexistent_file) { + struct sock_fprog actual; + const char *include_policy = "@include ./nonexistent.policy\n"; + + FILE *policy_file = + write_policy_to_pipe(include_policy, strlen(include_policy)); + ASSERT_NE(policy_file, nullptr); + + int res = compile_filter(policy_file, &actual, USE_RET_KILL, NO_LOGGING); + fclose(policy_file); + + ASSERT_NE(res, 0); +} + +// TODO(jorgelo): Android unit tests don't currently support data files. +// Re-enable by creating a temporary policy file at runtime. +#if !defined(__ANDROID__) + +TEST(FilterTest, include) { + struct sock_fprog compiled_plain; + struct sock_fprog compiled_with_include; + + const char *policy_plain = + "read: 1\n" + "write: 1\n" + "rt_sigreturn: 1\n" + "exit: 1\n"; + + const char *policy_with_include = "@include ./test/seccomp.policy\n"; + + FILE *file_plain = write_policy_to_pipe(policy_plain, strlen(policy_plain)); + ASSERT_NE(file_plain, nullptr); + int res_plain = + compile_filter(file_plain, &compiled_plain, USE_RET_KILL, NO_LOGGING); + fclose(file_plain); + + FILE *file_with_include = + write_policy_to_pipe(policy_with_include, strlen(policy_with_include)); + ASSERT_NE(file_with_include, nullptr); + int res_with_include = compile_filter( + file_with_include, &compiled_with_include, USE_RET_KILL, NO_LOGGING); + fclose(file_with_include); + + /* + * Checks that filter length is the same for a plain policy and an equivalent + * policy with an @include statement. Also checks that the filter generated + * from the policy with an @include statement is exactly the same as one + * generated from a plain policy. + */ + ASSERT_EQ(res_plain, 0); + ASSERT_EQ(res_with_include, 0); + + EXPECT_EQ(compiled_plain.len, 13); + EXPECT_EQ(compiled_with_include.len, 13); + + EXPECT_ARCH_VALIDATION(compiled_with_include.filter); + EXPECT_EQ_STMT(compiled_with_include.filter + ARCH_VALIDATION_LEN, + BPF_LD + BPF_W + BPF_ABS, + syscall_nr); + EXPECT_ALLOW_SYSCALL(compiled_with_include.filter + ARCH_VALIDATION_LEN + 1, + __NR_read); + EXPECT_ALLOW_SYSCALL(compiled_with_include.filter + ARCH_VALIDATION_LEN + 3, + __NR_write); + EXPECT_ALLOW_SYSCALL(compiled_with_include.filter + ARCH_VALIDATION_LEN + 5, + __NR_rt_sigreturn); + EXPECT_ALLOW_SYSCALL(compiled_with_include.filter + ARCH_VALIDATION_LEN + 7, + __NR_exit); + EXPECT_EQ_STMT(compiled_with_include.filter + ARCH_VALIDATION_LEN + 9, + BPF_RET + BPF_K, + SECCOMP_RET_KILL); + + free(compiled_plain.filter); + free(compiled_with_include.filter); +} + +TEST(FilterTest, include_same_syscalls) { + struct sock_fprog actual; + const char *policy = + "read: 1\n" + "write: 1\n" + "rt_sigreturn: 1\n" + "exit: 1\n" + "@include ./test/seccomp.policy\n"; + + FILE *policy_file = + write_policy_to_pipe(policy, strlen(policy)); + ASSERT_NE(policy_file, nullptr); + + int res = compile_filter(policy_file, &actual, USE_RET_KILL, NO_LOGGING); + fclose(policy_file); + + ASSERT_EQ(res, 0); + EXPECT_EQ(actual.len, + ARCH_VALIDATION_LEN + 1 /* load syscall nr */ + + 2 * 8 /* check syscalls twice */ + 1 /* filter return */); + free(actual.filter); +} + +TEST(FilterTest, include_invalid_policy) { + struct sock_fprog actual; + const char *policy = + "read: 1\n" + "write: 1\n" + "rt_sigreturn: 1\n" + "exit: 1\n" + "@include ./test/invalid_syscall_name.policy\n"; + + FILE *policy_file = + write_policy_to_pipe(policy, strlen(policy)); + ASSERT_NE(policy_file, nullptr); + + /* Ensure the included (invalid) policy file exists. */ + FILE *included_file = fopen("./test/invalid_syscall_name.policy", "r"); + ASSERT_NE(included_file, nullptr); + fclose(included_file); + + int res = compile_filter(policy_file, &actual, USE_RET_KILL, NO_LOGGING); + fclose(policy_file); + + ASSERT_NE(res, 0); +} + +TEST(FilterTest, include_nested) { + struct sock_fprog actual; + const char *policy = "@include ./test/nested.policy\n"; + + FILE *policy_file = + write_policy_to_pipe(policy, strlen(policy)); + ASSERT_NE(policy_file, nullptr); + + /* Ensure the policy file exists. */ + FILE *included_file = fopen("./test/nested.policy", "r"); + ASSERT_NE(included_file, nullptr); + fclose(included_file); + + int res = compile_filter(policy_file, &actual, USE_RET_KILL, NO_LOGGING); + fclose(policy_file); + + ASSERT_NE(res, 0); +} + +#endif // !__ANDROID__ diff --git a/test/nested.policy b/test/nested.policy new file mode 100644 index 0000000..df4491b --- /dev/null +++ b/test/nested.policy @@ -0,0 +1 @@ +@include ./test/nested.policy |