[PATCH testsuite v3 2/2] tests/file_contexts: add tests for multiple SELABEL_OPT_PATH

"ThiĆ©baud Weksteen" <[email protected]> Mon, 27 Jul 2026 12:52:14 +1000
Newsgroups org.kernel.vger.selinux
Message-ID <[email protected]>
Add unit tests in test_multiple.c to exercise opening the file contexts
backend with multiple SELABEL_OPT_PATH options under various validation
and path configuration scenarios.

The following test functions were added:
- Verifies opening multiple file contexts without validation.
- Verifies validation failure when contexts contain undefined types.
- Verifies extra files (.subs, .local, .homedirs) are processed only for
  the primary path.
- Verifies multiple files with the same definition, no error is raised.
- Verifies context lookups when providing three distinct
  SELABEL_OPT_PATH options.

These tests are skipped if libselinux does not support that feature.

Signed-off-by: ThiĆ©baud Weksteen <[email protected]>
---
Changes since v2:
- Add subcommand "check" to skip test_multiple when not supported by
  libselinux.

 tests/file_contexts/Makefile        |   2 +-
 tests/file_contexts/test            |  35 +++-
 tests/file_contexts/test_multiple.c | 275 ++++++++++++++++++++++++++++
 3 files changed, 301 insertions(+), 11 deletions(-)
 create mode 100644 tests/file_contexts/test_multiple.c

diff --git a/tests/file_contexts/Makefile b/tests/file_contexts/Makefile
index 592a65f..083ef25 100644
--- a/tests/file_contexts/Makefile
+++ b/tests/file_contexts/Makefile
@@ -1,4 +1,4 @@
-TARGETS=test_open test_lookup test_open_base test_validate
+TARGETS=test_open test_lookup test_open_base test_validate test_multiple
 CFLAGS += -O2 -Werror -Wall
 LDLIBS += -lselinux
 
diff --git a/tests/file_contexts/test b/tests/file_contexts/test
index 1534925..6d97597 100755
--- a/tests/file_contexts/test
+++ b/tests/file_contexts/test
@@ -3,23 +3,38 @@
 # This test validates the parsing of file_contexts.
 #
 
-use Test;
-
-BEGIN { plan tests => 4; }
-
-$basedir = $0;
-$basedir =~ s|(.*)/[^/]*|$1|;
+use Test::More;
+
+BEGIN {
+    $basedir = $0;
+    $basedir =~ s|(.*)/[^/]*|$1|;
+
+    $test_multiple = 0;
+    $result        = system "$basedir/test_multiple $basedir check 2>/dev/null";
+    if ( $result eq 0 ) {
+        $test_multiple = 1;
+        plan tests => 5;
+    }
+    else {
+        plan tests => 4;
+    }
+}
 
 $result = system "$basedir/test_open $basedir 2>&1";
-ok( $result, 0 );
+ok( $result eq 0 );
 
 $result = system "$basedir/test_lookup $basedir 2>&1";
-ok( $result, 0 );
+ok( $result eq 0 );
 
 $result = system "$basedir/test_open_base $basedir 2>&1";
-ok( $result, 0 );
+ok( $result eq 0 );
 
 $result = system "$basedir/test_validate $basedir 2>&1";
-ok( $result, 0 );
+ok( $result eq 0 );
+
+if ($test_multiple) {
+    $result = system "$basedir/test_multiple $basedir 2>&1";
+    ok( $result eq 0 );
+}
 
 exit;
diff --git a/tests/file_contexts/test_multiple.c b/tests/file_contexts/test_multiple.c
new file mode 100644
index 0000000..88b4cc8
--- /dev/null
+++ b/tests/file_contexts/test_multiple.c
@@ -0,0 +1,275 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <selinux/label.h>
+#include <selinux/selinux.h>
+
+#include "internal.h"
+
+void test_multiple_no_validation(const char *basedir)
+{
+	struct selabel_handle *hnd;
+
+	/* f1.fc and f2.fc file */
+	char *f1_path, *f2_path;
+	asprintf(&f1_path, "%s/f1.fc", basedir);
+	asprintf(&f2_path, "%s/f2.fc", basedir);
+	struct selinux_opt opts[] = {
+		{ .type = SELABEL_OPT_PATH, .value = f1_path },
+		{ .type = SELABEL_OPT_PATH, .value = f2_path }
+	};
+
+	hnd = selabel_open(SELABEL_CTX_FILE, opts, ARRAY_SIZE(opts));
+	free(f1_path);
+	free(f2_path);
+
+	if (!hnd) {
+		log_errno("Unable to open file backend");
+		exit(2);
+	}
+
+	struct test_t tests[] = {
+		{ .path = "/", .context = "system_u:object_r:rootfs:s0" },
+		{
+			.path = "/base",
+			.context = "system_u:object_r:test_base_t:s0"
+		},
+	};
+	assertContextsMatch(hnd, __func__, tests, ARRAY_SIZE(tests));
+
+	selabel_close(hnd);
+}
+
+void test_multiple_with_validation(const char *basedir)
+{
+	struct selabel_handle *hnd;
+
+	/* f1.fc and f2.fc file - f1 has undefined type rootfs in test policy */
+	char *f1_path, *f2_path;
+	asprintf(&f1_path, "%s/f1.fc", basedir);
+	asprintf(&f2_path, "%s/f2.fc", basedir);
+	struct selinux_opt opts[] = {
+		{ .type = SELABEL_OPT_PATH, .value = f1_path },
+		{ .type = SELABEL_OPT_PATH, .value = f2_path },
+		{ .type = SELABEL_OPT_VALIDATE, .value = "1" }
+	};
+
+	hnd = selabel_open(SELABEL_CTX_FILE, opts, ARRAY_SIZE(opts));
+	free(f1_path);
+	free(f2_path);
+
+	if (hnd) {
+		log_err("Validation of f1 and f2 should have failed");
+		selabel_close(hnd);
+		exit(2);
+	}
+}
+
+void test_multiple_with_extras(const char *basedir)
+{
+	struct selabel_handle *hnd;
+	char *f2_path, *f3_path;
+	asprintf(&f2_path, "%s/f2.fc", basedir);
+	asprintf(&f3_path, "%s/f3.fc", basedir);
+
+	/* 1. f3.fc is the first path: extras (.subs, .local, .homedirs) of f3 ARE processed */
+	struct selinux_opt opts_f3_first[] = {
+		{ .type = SELABEL_OPT_PATH, .value = f3_path },
+		{ .type = SELABEL_OPT_PATH, .value = f2_path }
+	};
+	hnd = selabel_open(SELABEL_CTX_FILE, opts_f3_first,
+			   ARRAY_SIZE(opts_f3_first));
+	if (!hnd) {
+		log_errno("Unable to open file backend");
+		exit(2);
+	}
+
+	struct test_t tests_f3_first[] = {
+		{ .path = "/", .context = "system_u:object_r:rootfs:s0" },
+		{
+			.path = "/sub",
+			.context = "system_u:object_r:test_subbed:s0"
+		},
+		{
+			.path = "/local",
+			.context = "system_u:object_r:test_local:s0"
+		},
+		{
+			.path = "/homedirs",
+			.context = "system_u:object_r:test_homedirs:s0"
+		},
+		{
+			.path = "/base",
+			.context = "system_u:object_r:test_base_t:s0"
+		},
+	};
+	assertContextsMatch(hnd, __func__, tests_f3_first,
+			    ARRAY_SIZE(tests_f3_first));
+	selabel_close(hnd);
+
+	/* 2. f2.fc is the first path, f3.fc is second: extras from f3 are NOT processed */
+	struct selinux_opt opts_f2_first[] = {
+		{ .type = SELABEL_OPT_PATH, .value = f2_path },
+		{ .type = SELABEL_OPT_PATH, .value = f3_path }
+	};
+	hnd = selabel_open(SELABEL_CTX_FILE, opts_f2_first,
+			   ARRAY_SIZE(opts_f2_first));
+	if (!hnd) {
+		log_errno("Unable to open file backend");
+		exit(2);
+	}
+
+	struct test_t tests_f2_first[] = {
+		{
+			.path = "/base",
+			.context = "system_u:object_r:test_base_t:s0"
+		},
+		{ .path = "/", .context = "system_u:object_r:rootfs:s0" },
+		{
+			.path = "/subbed",
+			.context = "system_u:object_r:test_subbed:s0"
+		},
+		/* /sub, /local, /homedirs should NOT match the f3 extra contexts */
+		{ .path = "/sub", .context = NULL },
+		{ .path = "/local", .context = NULL },
+		{ .path = "/homedirs", .context = NULL },
+	};
+	assertContextsMatch(hnd, __func__, tests_f2_first,
+			    ARRAY_SIZE(tests_f2_first));
+	selabel_close(hnd);
+
+	free(f2_path);
+	free(f3_path);
+}
+
+void test_multiple_three_paths(const char *basedir)
+{
+	struct selabel_handle *hnd;
+	char *f1_path, *f2_path, *f3_path;
+	asprintf(&f1_path, "%s/f1.fc", basedir);
+	asprintf(&f2_path, "%s/f2.fc", basedir);
+	asprintf(&f3_path, "%s/f3.fc", basedir);
+
+	struct selinux_opt opts[] = {
+		{ .type = SELABEL_OPT_PATH, .value = f1_path },
+		{ .type = SELABEL_OPT_PATH, .value = f2_path },
+		{ .type = SELABEL_OPT_PATH, .value = f3_path }
+	};
+
+	hnd = selabel_open(SELABEL_CTX_FILE, opts, ARRAY_SIZE(opts));
+	free(f1_path);
+	free(f2_path);
+	free(f3_path);
+
+	if (!hnd) {
+		log_errno("Unable to open file backend");
+		exit(2);
+	}
+
+	struct test_t tests[] = {
+		{ .path = "/", .context = "system_u:object_r:rootfs:s0" },
+		{
+			.path = "/base",
+			.context = "system_u:object_r:test_base_t:s0"
+		},
+		{
+			.path = "/subbed",
+			.context = "system_u:object_r:test_subbed:s0"
+		},
+	};
+	assertContextsMatch(hnd, __func__, tests, ARRAY_SIZE(tests));
+	selabel_close(hnd);
+}
+
+void test_multiple_duplicate_validation(const char *basedir)
+{
+	struct selabel_handle *hnd;
+
+	/* Two copies of f2.fc to provide duplicate specifications */
+	char *f2_path;
+	asprintf(&f2_path, "%s/f2.fc", basedir);
+	struct selinux_opt opts[] = {
+		{ .type = SELABEL_OPT_PATH, .value = f2_path },
+		{ .type = SELABEL_OPT_PATH, .value = f2_path },
+		{ .type = SELABEL_OPT_VALIDATE, .value = "1" }
+	};
+
+	hnd = selabel_open(SELABEL_CTX_FILE, opts, ARRAY_SIZE(opts));
+	free(f2_path);
+
+	if (!hnd) {
+		log_errno("Unable to open file backend");
+		exit(2);
+	}
+
+	struct test_t tests[] = {
+		{
+			.path = "/base",
+			.context = "system_u:object_r:test_base_t:s0"
+		},
+	};
+	assertContextsMatch(hnd, __func__, tests, ARRAY_SIZE(tests));
+
+	selabel_close(hnd);
+}
+
+/* Check if multiple paths are supported. Returns 0 if they are; 1 otherwise */
+static int check_multiple_path_support(const char *basedir)
+{
+	struct selabel_handle *hnd;
+	char *f1_path, *f2_path;
+	char *context1 = NULL, *context2 = NULL;
+	bool supported = false;
+
+	asprintf(&f1_path, "%s/f1.fc", basedir);
+	asprintf(&f2_path, "%s/f2.fc", basedir);
+
+	struct selinux_opt opts[] = {
+		{ .type = SELABEL_OPT_PATH, .value = f1_path },
+		{ .type = SELABEL_OPT_PATH, .value = f2_path }
+	};
+
+	hnd = selabel_open(SELABEL_CTX_FILE, opts, ARRAY_SIZE(opts));
+	free(f1_path);
+	free(f2_path);
+
+	if (!hnd) {
+		/* On older libselinux releases, the second file_context would
+		 * be ignored but selabel_open should not fail. */
+		log_err("Unable to open file backend");
+		exit(2);
+	}
+
+	if (selabel_lookup(hnd, &context1, "/", S_IFREG) == 0 &&
+	    selabel_lookup(hnd, &context2, "/base", S_IFREG) == 0) {
+		supported = true;
+	}
+
+	free(context1);
+	free(context2);
+	selabel_close(hnd);
+	return supported ? 0 : 1;
+}
+
+int main(int argc, char **argv)
+{
+	if (argc < 2 || argc > 3) {
+		log_err("usage: %s <basedir> [check]", argv[0]);
+		exit(1);
+	}
+
+	if (argc == 3 && strcmp(argv[2], "check") == 0) {
+		return check_multiple_path_support(argv[1]);
+	}
+
+	test_multiple_no_validation(argv[1]);
+	test_multiple_with_validation(argv[1]);
+	test_multiple_duplicate_validation(argv[1]);
+	test_multiple_with_extras(argv[1]);
+	test_multiple_three_paths(argv[1]);
+
+	return 0;
+}
-- 
2.55.0.229.g6434b31f56-goog