All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH] libselinux: apply requested level to failsafe contexts
@ 2026-11-06 18:10 Stephen Smalley
  2026-11-09 16:26 ` Stephen Smalley
  2026-11-09 16:29 ` Filippo Bonazzi
  0 siblings, 2 replies; 3+ messages in thread
From: Stephen Smalley @ 2026-11-06 18:10 UTC (permalink / raw)
  To: selinux
  Cc: plautrba, omosnacek, jason, jwcart2, cahu, jsegitz, tweek,
	Stephen Smalley, Filippo Bonazzi

The level-aware context lookup APIs replace the range in the starting
context, which get_context_user() then copies into matching
candidates. When no candidate survives, get_failsafe_context() instead
returns the configured failsafe range unchanged. A NULL starting
context also hits this path when the current role and type have no
matching entry.

Carry the explicitly requested level through an internal list lookup
and apply it to the failsafe context before
security_check_context(). Keep the configured range for ordinary
lookups and preserve the existing ENOENT validation exception. Route
role-and-level lookup through the same level-aware list implementation
so it receives the fix too.

Add 309 regression cases covering all three APIs, explicit and current
starting contexts, per-user/global/missing configuration, rejected
candidates, validation of the final range, and failure/retry at the
new allocation steps. The test uses private configuration files and
wraps getcon and kernel validation; no host policy is changed.

Reported-by: Filippo Bonazzi <fbonazzi@suse.de>
Fixes: https://github.com/SELinuxProject/selinux/issues/508
Signed-off-by: Stephen Smalley <stephen.smalley.work@gmail.com>
---
 libselinux/src/get_context_list.c     |  84 +++++-----
 libselinux/tests/.gitignore           |   1 +
 libselinux/tests/Makefile             |   7 +
 libselinux/tests/test_context_level.c | 221 ++++++++++++++++++++++++++
 4 files changed, 273 insertions(+), 40 deletions(-)
 create mode 100644 libselinux/tests/test_context_level.c

diff --git a/libselinux/src/get_context_list.c b/libselinux/src/get_context_list.c
index 289ef89e..06cec418 100644
--- a/libselinux/src/get_context_list.c
+++ b/libselinux/src/get_context_list.c
@@ -13,8 +13,15 @@
 #include "context_internal.h"
 #include "get_context_list_internal.h"
 
-int get_default_context_with_role(const char *user, const char *role,
-				  const char *fromcon, char **newcon)
+static int get_ordered_context_list_internal(const char *user,
+					     const char *fromcon,
+					     const char *level, char ***list);
+
+static int get_default_context_with_role_internal(const char *user,
+						  const char *role,
+						  const char *level,
+						  const char *fromcon,
+						  char **newcon)
 {
 	char **conary;
 	char **ptr;
@@ -22,7 +29,7 @@ int get_default_context_with_role(const char *user, const char *role,
 	const char *role2;
 	int rc;
 
-	rc = get_ordered_context_list(user, fromcon, &conary);
+	rc = get_ordered_context_list_with_level(user, level, fromcon, &conary);
 	if (rc <= 0)
 		return -1;
 
@@ -52,44 +59,19 @@ out:
 	return rc;
 }
 
+int get_default_context_with_role(const char *user, const char *role,
+				  const char *fromcon, char **newcon)
+{
+	return get_default_context_with_role_internal(user, role, NULL, fromcon,
+						      newcon);
+}
+
 int get_default_context_with_rolelevel(const char *user, const char *role,
 				       const char *level, const char *fromcon,
 				       char **newcon)
 {
-	int rc;
-	char *backup_fromcon = NULL;
-	context_t con;
-	const char *newfromcon;
-
-	if (!level)
-		return get_default_context_with_role(user, role, fromcon,
-						     newcon);
-
-	if (!fromcon) {
-		rc = getcon(&backup_fromcon);
-		if (rc < 0)
-			return rc;
-		fromcon = backup_fromcon;
-	}
-
-	rc = -1;
-	con = context_new(fromcon);
-	if (!con)
-		goto out;
-
-	if (context_range_set(con, level))
-		goto out;
-
-	newfromcon = context_str(con);
-	if (!newfromcon)
-		goto out;
-
-	rc = get_default_context_with_role(user, role, newfromcon, newcon);
-
-out:
-	context_free(con);
-	freecon(backup_fromcon);
-	return rc;
+	return get_default_context_with_role_internal(user, role, level,
+						      fromcon, newcon);
 }
 
 int get_default_context(const char *user, const char *fromcon, char **newcon)
@@ -290,7 +272,8 @@ out:
 	return rc;
 }
 
-static int get_failsafe_context(const char *user, char **newcon)
+static int get_failsafe_context(const char *user, const char *level,
+				char **newcon)
 {
 	FILE *fp;
 	char buf[255], *ptr;
@@ -321,6 +304,20 @@ static int get_failsafe_context(const char *user, char **newcon)
 		return -1;
 	}
 
+	/* Validate the context we will return, including any requested range. */
+	if (level) {
+		context_t con = context_new(*newcon);
+		char *leveled = NULL;
+
+		if (con && context_range_set(con, level) == 0)
+			leveled = context_to_str(con);
+		context_free(con);
+		free(*newcon);
+		*newcon = leveled;
+		if (!leveled)
+			return -1;
+	}
+
 	/* If possible, check the context to catch
 	   errors early rather than waiting until the
 	   caller tries to use setexeccon on the context.
@@ -365,7 +362,7 @@ int get_ordered_context_list_with_level(const char *user, const char *level,
 	if (!newfromcon)
 		goto out;
 
-	rc = get_ordered_context_list(user, newfromcon, list);
+	rc = get_ordered_context_list_internal(user, newfromcon, level, list);
 
 out:
 	context_free(con);
@@ -392,6 +389,13 @@ int get_default_context_with_level(const char *user, const char *level,
 
 int get_ordered_context_list(const char *user, const char *fromcon,
 			     char ***list)
+{
+	return get_ordered_context_list_internal(user, fromcon, NULL, list);
+}
+
+static int get_ordered_context_list_internal(const char *user,
+					     const char *fromcon,
+					     const char *level, char ***list)
 {
 	char **reachable = NULL;
 	int rc = 0;
@@ -476,7 +480,7 @@ failsafe:
 		rc = -1;
 		goto out;
 	}
-	rc = get_failsafe_context(user, &reachable[0]);
+	rc = get_failsafe_context(user, level, &reachable[0]);
 	if (rc < 0) {
 		freeconary(reachable);
 		reachable = NULL;
diff --git a/libselinux/tests/.gitignore b/libselinux/tests/.gitignore
index 6cc9309b..ce2c26fc 100644
--- a/libselinux/tests/.gitignore
+++ b/libselinux/tests/.gitignore
@@ -19,3 +19,4 @@ test-restorecon-digest
 /test-avc-lock-failure
 /test-descendant-oom
 /test-regex-context-lifetime
+/test-context-level
diff --git a/libselinux/tests/Makefile b/libselinux/tests/Makefile
index c229e540..531c46df 100644
--- a/libselinux/tests/Makefile
+++ b/libselinux/tests/Makefile
@@ -21,6 +21,11 @@ test-restorecon-errno: test_restorecon_errno.c ../src/libselinux.a
 	$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(addprefix -Wl$(COMMA)--wrap=,$(ERRNO_WRAPS)) $(PCRE_LDLIBS) -ldl -pthread
 
 .PHONY: test clean
+test: test-context-level
+
+test-context-level: test_context_level.c ../src/libselinux.a
+	$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ $^ -Wl,--wrap=getcon,--wrap=security_check_context,--wrap=context_new,--wrap=context_range_set,--wrap=context_to_str $(PCRE_LDLIBS) -ldl -pthread
+
 test: test-restorecon-digest test-restorecon-errors test-restorecon-errno test-restorecon-exclude test-restorecon-xdev test-label-init-oom test-label-merge test-substitution-hash test-substitution-restorecon test-readable-fallback test-digest-inputs test-load-policy-version test-auxiliary-duplicates test-android-specfiles test-regex-tls test-policy-root-boundary test-match-limit test-avc-lock-failure test-descendant-oom test-regex-context-lifetime
 	./test-restorecon-digest
 	./test-restorecon-errors
@@ -43,8 +48,10 @@ test: test-restorecon-digest test-restorecon-errors test-restorecon-errno test-r
 	./test-avc-lock-failure
 	./test-descendant-oom
 	./test-regex-context-lifetime ../src/libselinux.so.1
+	./test-context-level
 
 clean:
+	rm -f test-context-level
 	rm -f test-restorecon-digest test-restorecon-errors test-restorecon-errno test-restorecon-exclude test-restorecon-xdev test-label-init-oom test-label-merge test-substitution-hash test-substitution-restorecon test-readable-fallback test-digest-inputs
 	rm -f test-load-policy-version
 	rm -f test-auxiliary-duplicates
diff --git a/libselinux/tests/test_context_level.c b/libselinux/tests/test_context_level.c
new file mode 100644
index 00000000..19e6b436
--- /dev/null
+++ b/libselinux/tests/test_context_level.c
@@ -0,0 +1,221 @@
+#define _GNU_SOURCE
+#include <assert.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <selinux/context.h>
+#include <selinux/get_context_list.h>
+#include <selinux/selinux.h>
+
+int __wrap_getcon(char **con);
+int __wrap_security_check_context(const char *con);
+context_t __wrap_context_new(const char *str);
+context_t __real_context_new(const char *str);
+int __wrap_context_range_set(context_t con, const char *range);
+int __real_context_range_set(context_t con, const char *range);
+char *__wrap_context_to_str(context_t con);
+char *__real_context_to_str(context_t con);
+
+static const char *current_context;
+static const char *expected_range;
+static unsigned int getcon_calls, failsafe_checks, cases;
+static int config_mode, validation_mode, fault, fault_hit;
+
+int __wrap_getcon(char **con)
+{
+	getcon_calls++;
+	*con = strdup(current_context);
+	return *con ? 0 : -1;
+}
+
+int __wrap_security_check_context(const char *str)
+{
+	context_t con = __real_context_new(str);
+	int failsafe;
+
+	assert(con);
+	failsafe = !strcmp(context_type_get(con), "failsafe_t");
+	if (failsafe) {
+		failsafe_checks++;
+		assert(!strcmp(context_range_get(con), expected_range));
+	}
+	context_free(con);
+	if (failsafe && validation_mode) {
+		errno = validation_mode == 1 ? EINVAL : ENOENT;
+		return -1;
+	}
+	if (!failsafe && config_mode == 3) {
+		errno = EINVAL;
+		return -1;
+	}
+	return 0;
+}
+
+context_t __wrap_context_new(const char *str)
+{
+	if (fault == 1 && strstr(str, ":failsafe_t:")) {
+		fault_hit++;
+		errno = ENOMEM;
+		return NULL;
+	}
+	return __real_context_new(str);
+}
+
+int __wrap_context_range_set(context_t con, const char *range)
+{
+	if (fault == 2 && !strcmp(context_type_get(con), "failsafe_t")) {
+		fault_hit++;
+		errno = ENOMEM;
+		return -1;
+	}
+	return __real_context_range_set(con, range);
+}
+
+char *__wrap_context_to_str(context_t con)
+{
+	if (fault == 3 && !strcmp(context_type_get(con), "failsafe_t")) {
+		fault_hit++;
+		errno = ENOMEM;
+		return NULL;
+	}
+	return __real_context_to_str(con);
+}
+
+static void put_file(const char *path, const char *text)
+{
+	FILE *fp = fopen(path, "w");
+
+	assert(fp);
+	assert(fputs(text, fp) >= 0);
+	assert(fclose(fp) == 0);
+}
+
+static void check_result(const char *str, int fallback)
+{
+	context_t con = __real_context_new(str);
+
+	assert(con);
+	assert(!strcmp(context_user_get(con), "test_u"));
+	assert(!strcmp(context_role_get(con), "session_r"));
+	assert(!strcmp(context_range_get(con), expected_range));
+	assert((strcmp(context_type_get(con), "failsafe_t") == 0) == fallback);
+	context_free(con);
+}
+
+static void run_case(int api, const char *level, const char *from, int fallback)
+{
+	char *sentinel = (char *)&cases;
+	char *one = sentinel;
+	char **list = (char **)&cases;
+	int rc, failure = fallback && (validation_mode == 1 || fault);
+
+	getcon_calls = failsafe_checks = fault_hit = 0;
+	if (api == 0)
+		rc = get_ordered_context_list_with_level("test_u", level, from,
+							 &list);
+	else if (api == 1)
+		rc = get_default_context_with_level("test_u", level, from,
+						    &one);
+	else
+		rc = get_default_context_with_rolelevel("test_u", "session_r",
+							level, from, &one);
+	assert(getcon_calls == (from == NULL));
+	assert(fault_hit == (fault != 0));
+	assert(failsafe_checks == (unsigned int)(fallback && !fault));
+	if (failure) {
+		assert(rc == -1);
+		assert(one == sentinel && list == (char **)&cases);
+	} else if (api == 0) {
+		assert(rc == (fallback ? 1 : 2));
+		for (int i = 0; i < rc; i++)
+			check_result(list[i], fallback);
+		assert(list[rc] == NULL);
+		freeconary(list);
+	} else {
+		assert(rc == 0);
+		check_result(one, fallback);
+		freecon(one);
+	}
+	cases++;
+}
+
+int main(void)
+{
+	char root[] = "/tmp/selinux-context-level-XXXXXX";
+	char contexts[256], users[256], user[256], defaults[256], failsafe[256];
+	const char *entries =
+		"source_r:source_t:s0 session_r:normal_t:s0 session_r:other_t:s0\n";
+
+	assert(mkdtemp(root));
+	assert(snprintf(contexts, sizeof(contexts), "%s/contexts", root) > 0);
+	assert(snprintf(users, sizeof(users), "%s/contexts/users", root) > 0);
+	assert(snprintf(user, sizeof(user), "%s/contexts/users/test_u", root) >
+	       0);
+	assert(snprintf(defaults, sizeof(defaults),
+			"%s/contexts/default_contexts", root) > 0);
+	assert(snprintf(failsafe, sizeof(failsafe),
+			"%s/contexts/failsafe_context", root) > 0);
+	assert(mkdir(contexts, 0700) == 0);
+	assert(mkdir(users, 0700) == 0);
+	assert(selinux_set_policy_root(root) == 0);
+	put_file(failsafe, "session_r:failsafe_t:s0\n");
+
+	for (config_mode = 0; config_mode < 4; config_mode++) {
+		put_file(user, config_mode == 0 ? entries : "");
+		put_file(defaults,
+			 config_mode == 1 || config_mode == 3 ? entries : "");
+		for (int api = 0; api < 3; api++)
+			for (int source = 0; source < 4; source++)
+				for (int leveled = 0; leveled < 2; leveled++)
+					for (validation_mode = 0;
+					     validation_mode < 3;
+					     validation_mode++) {
+						int fallback = source >= 2 ||
+							       config_mode >= 2;
+						current_context =
+							source < 2 ?
+								"old_u:source_r:source_t:s0:c2" :
+								"old_u:unmatched_r:unmatched_t:s0:c2";
+						expected_range =
+							leveled ?
+								"s0:c7" :
+								(fallback ?
+									 "s0" :
+									 "s0:c2");
+						run_case(
+							api,
+							leveled ? "s0:c7" :
+								  NULL,
+							source % 2 ?
+								NULL :
+								current_context,
+							fallback);
+					}
+	}
+
+	config_mode = 2;
+	validation_mode = 0;
+	current_context = "old_u:unmatched_r:unmatched_t:s0:c2";
+	expected_range = "s0:c7";
+	for (int api = 0; api < 3; api++)
+		for (int failure = 1; failure <= 3; failure++) {
+			fault = failure;
+			run_case(api, "s0:c7", NULL, 1);
+			fault = 0;
+			run_case(api, "s0:c7", NULL, 1);
+		}
+
+	assert(unlink(user) == 0);
+	assert(unlink(defaults) == 0);
+	for (int api = 0; api < 3; api++)
+		run_case(api, "s0:c7", NULL, 1);
+	assert(unlink(failsafe) == 0);
+	assert(rmdir(users) == 0);
+	assert(rmdir(contexts) == 0);
+	assert(rmdir(root) == 0);
+	printf("PASS %u context-level cases\n", cases);
+	return 0;
+}
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-11-09 16:30 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-06 18:10 [PATCH] libselinux: apply requested level to failsafe contexts Stephen Smalley
2026-11-09 16:26 ` Stephen Smalley
2026-11-09 16:29 ` Filippo Bonazzi

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.