Add checkfc mode to validate file_contexts against test data
A new mode for checkfc is introduced (-t) which takes a file_contexts
and a test data file. Each line in the test data file contains a path
and the expected type. checkfc loads the file_contexts and repeatedly
calls selabel_lookup(3) to verify that the computed type is as expected.
This mode can be used to confirm that any modification to file_contexts
or its build process is benign.
A test data file (plat_file_contexts_test) is added. This file was
manually created based on private/file_contexts. Each static path was
copied as-is. Each regular expression was expanded into a couple of
entries. For instance, /dev/adf[0-9]* generated /dev/adf, /dev/adf0 and
/dev/adf123.
libselinux keeps track of which specification is being hit when using
selabel_lookup. When calling selabel_stats(3), the file backend will
output a warning if a specification has not been used. This can be
leveraged to ensure that each rule is at least hit once. This property
will be leveraged in a follow-up change (by running the test as part of
the build process), to ensure that the plat_file_contexts_test file
remains up-to-date (that is, when an entry is added to
private/file_contexts, the build will fail unless a test is also added
to plat_file_contexts_test to exercice the specification/regular
expression).
Test: m checkfc && checkfc -t ./private/file_contexts ./tests/plat_file_contexts_test
Bug: 299839280
Change-Id: Ibf56859a16bd17e1f878ce7b0570b2aead79c7e0
diff --git a/tools/checkfc.c b/tools/checkfc.c
index 83c631e..05826f9 100644
--- a/tools/checkfc.c
+++ b/tools/checkfc.c
@@ -7,6 +7,7 @@
#include <sepol/module.h>
#include <sepol/policydb/policydb.h>
#include <sepol/sepol.h>
+#include <selinux/context.h>
#include <selinux/selinux.h>
#include <selinux/label.h>
#include <sys/stat.h>
@@ -209,8 +210,14 @@
"If -e is specified, then the context_file is allowed to be empty.\n\n"
"usage2: %s -c file_contexts1 file_contexts2\n\n"
- "Compares two file contexts files and reports one of subset, equal, superset, or incomparable.\n\n",
- name, name);
+ "Compares two file contexts files and reports one of \n"
+ "subset, equal, superset, or incomparable.\n\n"
+
+ "usage3: %s -t file_contexts test_data\n\n"
+ "Validates a file contexts file against test_data.\n"
+ "test_data is a text file where each line has the format:\n"
+ " path expected_type\n\n\n",
+ name, name, name);
exit(1);
}
@@ -264,6 +271,67 @@
printf("%s\n", result_str[result]);
}
+static void do_test_data_and_die_on_error(struct selinux_opt opts[], unsigned int backend,
+ char *paths[])
+{
+ opts[0].value = NULL; /* not validating against a policy */
+ opts[1].value = paths[0];
+ global_state.sepolicy.sehnd[0] = selabel_open(backend, opts, 2);
+ if (!global_state.sepolicy.sehnd[0]) {
+ fprintf(stderr, "Error: could not load context file from %s: %s\n",
+ paths[0], strerror(errno));
+ exit(1);
+ }
+
+ FILE* test_data = fopen(paths[1], "r");
+ if (test_data == NULL) {
+ fprintf(stderr, "Error: could not load test file from %s : %s\n",
+ paths[1], strerror(errno));
+ exit(1);
+ }
+
+ char line[1024];
+ while (fgets(line, sizeof(line), test_data)) {
+ char *path;
+ char *expected_type;
+
+ if (!strcmp(line, "\n") || line[0] == '#') {
+ continue;
+ }
+
+ int ret = sscanf(line, "%ms %ms", &path, &expected_type);
+ if (ret != 2) {
+ fprintf(stderr, "Error: unable to parse the line %s\n", line);
+ exit(1);
+ }
+
+ char *found_context;
+ ret = selabel_lookup(global_state.sepolicy.sehnd[0], &found_context, path, 0);
+ if (ret != 0) {
+ fprintf(stderr, "Error: unable to lookup the path for %s\n", line);
+ exit(1);
+ }
+
+ context_t found = context_new(found_context);
+ const char *found_type = context_type_get(found);
+
+ if (strcmp(found_type, expected_type)) {
+ fprintf(stderr, "Incorrect type for %s: resolved to %s, expected %s\n",
+ path, found_type, expected_type);
+ }
+
+ free(found_context);
+ context_free(found);
+ free(path);
+ free(expected_type);
+ }
+ fclose(test_data);
+
+ // Prints the coverage of file_contexts on the test data. It includes
+ // warnings for rules that have not been hit by any test example.
+ selabel_stats(global_state.sepolicy.sehnd[0]);
+}
+
static void do_fc_check_and_die_on_error(struct selinux_opt opts[], unsigned int backend, filemode mode,
const char *sepolicy_file, const char *context_file, bool allow_empty)
{
@@ -345,11 +413,12 @@
bool allow_empty = false;
bool compare = false;
+ bool test_data = false;
char c;
filemode mode = filemode_file_contexts;
- while ((c = getopt(argc, argv, "clpsve")) != -1) {
+ while ((c = getopt(argc, argv, "clpsvet")) != -1) {
switch (c) {
case 'c':
compare = true;
@@ -373,6 +442,9 @@
mode = filemode_vendor_service_contexts;
backend = SELABEL_CTX_ANDROID_SERVICE;
break;
+ case 't':
+ test_data = true;
+ break;
case 'h':
default:
usage(argv[0]);
@@ -385,7 +457,7 @@
usage(argv[0]);
}
- if (compare && backend != SELABEL_CTX_FILE) {
+ if ((compare || test_data) && backend != SELABEL_CTX_FILE) {
usage(argv[0]);
}
@@ -393,6 +465,8 @@
if (compare) {
do_compare_and_die_on_error(opts, backend, &(argv[index]));
+ } else if (test_data) {
+ do_test_data_and_die_on_error(opts, backend, &(argv[index]));
} else {
/* remaining args are sepolicy file and context file */
char *sepolicy_file = argv[index];