[PATCH] semodule-utils: add bad-data tests for sefcontext_compile

Akhil Kohli <[email protected]> Tue, 28 Jul 2026 15:10:07 +0200
Newsgroups org.kernel.vger.selinux
Message-ID <[email protected]>
Extend the bad-data harness to validate file contexts after packaging:
wrong field count and NUL bytes are rejected by sefcontext_compile, invalid
contexts are rejected with -p, and unpackage'd .pp file contexts are
checked the same way.

Adds 7 sefcontext_compile test steps on top of the semodule_package
harness. Adds bad_mls.mod.fc fixture. Preserve errexit restoration
(set -e after rc=$?) in shared helpers.

Signed-off-by: Akhil Kohli <[email protected]>
---
 .../fixtures/file_contexts/bad_mls.mod.fc     |   1 +
 semodule-utils/tests/bad-data/run.sh          | 227 +++++++++++++++++-
 2 files changed, 223 insertions(+), 5 deletions(-)
 create mode 100644 semodule-utils/tests/bad-data/fixtures/file_contexts/bad_mls.mod.fc

diff --git a/semodule-utils/tests/bad-data/fixtures/file_contexts/bad_mls.mod.fc b/semodule-utils/tests/bad-data/fixtures/file_contexts/bad_mls.mod.fc
new file mode 100644
index 00000000..1913faaa
--- /dev/null
+++ b/semodule-utils/tests/bad-data/fixtures/file_contexts/bad_mls.mod.fc
@@ -0,0 +1 @@
+/usr/bin/bad_mls	--	system_u:object_r:test_good_exec_t:s0:c999
diff --git a/semodule-utils/tests/bad-data/run.sh b/semodule-utils/tests/bad-data/run.sh
index 7c9f7c7d..1c7d8f51 100755
--- a/semodule-utils/tests/bad-data/run.sh
+++ b/semodule-utils/tests/bad-data/run.sh
@@ -1,9 +1,10 @@
 #!/bin/sh
 #
-# Bad-data tests for semodule_package in the modular policy packaging pipeline.
-# Covers packaging path edge cases (-m/-f) and documents deferred rejection of bad
-# .mod.fc content (enforced later by sefcontext_compile).
-# Unreadable -m/-f cases skip when run as root; CI runs this script as non-root.
+# Bad-data tests for semodule_package and sefcontext_compile in the modular
+# policy packaging pipeline. Covers packaging path edge cases (-m/-f), documents
+# deferred rejection of bad .mod.fc content at packaging time, and validates
+# labeling through sefcontext_compile. Unreadable -m/-f cases skip when run as
+# root; CI runs this script as non-root.
 #
 
 set -u
@@ -29,6 +30,8 @@ CHECKMODULE=${CHECKMODULE:-checkmodule}
 # Modular TE for MCS/MLS builds (checkmodule -M -m).
 CHECKMODULE_MOD_FLAGS=${CHECKMODULE_MOD_FLAGS:--M -m}
 SEMODULE_PACKAGE=${SEMODULE_PACKAGE:-semodule_package}
+SEMODULE_UNPACKAGE=${SEMODULE_UNPACKAGE:-semodule_unpackage}
+SEFCONTEXT_COMPILE=${SEFCONTEXT_COMPILE:-sefcontext_compile}
 
 GOOD_TE="${FIXTURES}/modules/good.te"
 GOOD_MOD="${OUTDIR}/test_good.mod"
@@ -250,6 +253,161 @@ expect_package_fail_unreadable_fc() {
 	pass "${desc} (rejected as expected, rc=${rc})"
 }
 
+find_policy_file() {
+	if [ -n "${POLICY_FILE:-}" ] && [ -f "${POLICY_FILE}" ]; then
+		echo "${POLICY_FILE}"
+		return 0
+	fi
+
+	# Prefer the active policy store (SELINUXTYPE); fall back to targeted, then any
+	# installed policy.* (POLICY_FILE overrides all of this).
+	selinuxtype=targeted
+	if [ -r /etc/selinux/config ]; then
+		selinuxtype=$(grep -E '^[[:space:]]*SELINUXTYPE=' /etc/selinux/config 2>/dev/null \
+			| tail -1 | cut -d= -f2- | tr -d ' "')
+		[ -n "${selinuxtype}" ] || selinuxtype=targeted
+	fi
+
+	# shellcheck disable=SC2086
+	set -- /etc/selinux/"${selinuxtype}"/policy/policy.*
+	if [ -f "$1" ]; then
+		echo "$1"
+		return 0
+	fi
+
+	for policy in /etc/selinux/*/policy/policy.*; do
+		[ -f "${policy}" ] || continue
+		echo "${policy}"
+		return 0
+	done
+
+	return 1
+}
+
+expect_sefcontext_pass() {
+	desc="$1"
+	outname="$2"
+	fc="$3"
+	policy="${4:-}"
+
+	outbin="${OUTDIR}/${outname}.bin"
+	stderr="${OUTDIR}/${outname}.err"
+
+	echo "==== POSITIVE (expect sefcontext_compile success): ${desc}"
+	rm -f "${outbin}"
+
+	set +e
+	if [ -n "${policy}" ]; then
+		"${SEFCONTEXT_COMPILE}" -p "${policy}" -o "${outbin}" "${fc}" \
+			2>"${stderr}"
+	else
+		"${SEFCONTEXT_COMPILE}" -o "${outbin}" "${fc}" 2>"${stderr}"
+	fi
+	rc=$?
+	set -e
+
+	if [ "${rc}" -ne 0 ]; then
+		cat "${stderr}" >&2
+		die "${desc}: expected exit 0, got rc=${rc}"
+		return 0
+	fi
+	if [ ! -s "${outbin}" ]; then
+		die "${desc}: expected non-empty ${outbin}"
+		return 0
+	fi
+
+	pass "${desc} (sefcontext_compile succeeded)"
+}
+
+expect_sefcontext_fail() {
+	desc="$1"
+	pattern="$2"
+	outname="$3"
+	fc="$4"
+	policy="${5:-}"
+
+	outbin="${OUTDIR}/${outname}.bin"
+	stderr="${OUTDIR}/${outname}.err"
+
+	echo "==== NEGATIVE (expect sefcontext_compile failure): ${desc}"
+	rm -f "${outbin}"
+
+	set +e
+	if [ -n "${policy}" ]; then
+		"${SEFCONTEXT_COMPILE}" -p "${policy}" -o "${outbin}" "${fc}" \
+			2>"${stderr}"
+	else
+		"${SEFCONTEXT_COMPILE}" -o "${outbin}" "${fc}" 2>"${stderr}"
+	fi
+	rc=$?
+	set -e
+
+	if [ "${rc}" -eq 0 ]; then
+		die "${desc}: expected non-zero exit, got rc=0"
+		return 0
+	fi
+	if [ -f "${outbin}" ] && [ -s "${outbin}" ]; then
+		die "${desc}: did not expect successful ${outbin}"
+		return 0
+	fi
+	if ! grep -Eq "${pattern}" "${stderr}"; then
+		echo "FAIL: stderr did not match /${pattern}/" >&2
+		cat "${stderr}" >&2
+		FAIL=$((FAIL + 1))
+		return 0
+	fi
+
+	pass "${desc} (rejected as expected, rc=${rc})"
+}
+
+expect_sefcontext_fail_or_skip_no_policy() {
+	desc="$1"
+	pattern="$2"
+	outname="$3"
+	fc="$4"
+
+	policy=$(find_policy_file) || policy=
+	if [ -z "${policy}" ]; then
+		echo "==== SKIP (no binary policy for -p validation): ${desc}"
+		echo "Set POLICY_FILE= to enable this check."
+		PASS=$((PASS + 1))
+		echo ""
+		return 0
+	fi
+
+	expect_sefcontext_fail "${desc}" "${pattern}" "${outname}" "${fc}" \
+		"${policy}"
+}
+
+expect_sefcontext_pass_deferred() {
+	desc="$1"
+	outname="$2"
+	fc="$3"
+
+	outbin="${OUTDIR}/${outname}.bin"
+	stderr="${OUTDIR}/${outname}.err"
+
+	echo "==== DOCUMENT (sefcontext_compile accepts input; complements packaging deferral): ${desc}"
+	rm -f "${outbin}"
+
+	set +e
+	"${SEFCONTEXT_COMPILE}" -o "${outbin}" "${fc}" 2>"${stderr}"
+	rc=$?
+	set -e
+
+	if [ "${rc}" -ne 0 ]; then
+		cat "${stderr}" >&2
+		die "${desc}: expected exit 0 at compile stage, got rc=${rc}"
+		return 0
+	fi
+	if [ ! -s "${outbin}" ]; then
+		die "${desc}: expected non-empty ${outbin} at compile stage"
+		return 0
+	fi
+
+	pass "${desc} (sefcontext_compile exit 0; complements packaging deferral)"
+}
+
 build_good_mod
 
 # Ephemeral path fixtures for packaging path tests.
@@ -258,7 +416,7 @@ ln -sf /nonexistent/test_good.mod.fc "${OUTDIR}/broken_symlink.mod.fc"
 printf '' > "${OUTDIR}/empty.mod.fc"
 
 # NUL byte inside path column (packaging accepts; sefcontext_compile rejects).
-printf '/bin/foo\x00bar\t--\tsystem_u:object_r:test_good_exec_t:s0\n' \
+printf '/bin/foo\000bar\t--\tsystem_u:object_r:test_good_exec_t:s0\n' \
 	> "${OUTDIR}/nul_bytes.mod.fc"
 
 GOOD_FC="${FIXTURES}/file_contexts/good.mod.fc"
@@ -347,6 +505,65 @@ expect_package_pass_deferred \
 	empty_fc \
 	-m "${GOOD_MOD}" -f "${OUTDIR}/empty.mod.fc"
 
+# --- sefcontext_compile / unpackage validation ---
+expect_sefcontext_pass \
+	"control good .mod.fc through sefcontext_compile" \
+	good_fc \
+	"${GOOD_FC}"
+
+expect_sefcontext_fail \
+	"wrong field count in .mod.fc (after packaging)" \
+	"missing fields|process_file failed" \
+	sefcontext_bad_fields \
+	"${FIXTURES}/file_contexts/bad_fields.mod.fc"
+
+expect_sefcontext_fail \
+	"NUL byte in .mod.fc path field (after packaging)" \
+	"missing fields|process_file failed" \
+	sefcontext_nul_bytes \
+	"${OUTDIR}/nul_bytes.mod.fc"
+
+expect_sefcontext_fail_or_skip_no_policy \
+	"invalid SELinux context requires policy validation (-p)" \
+	"invalid context|malformed context|process_file failed" \
+	sefcontext_bad_context \
+	"${FIXTURES}/file_contexts/bad_context.mod.fc"
+
+expect_sefcontext_pass_deferred \
+	"empty .mod.fc file through sefcontext_compile" \
+	sefcontext_empty_fc \
+	"${OUTDIR}/empty.mod.fc"
+
+expect_sefcontext_fail_or_skip_no_policy \
+	"invalid MLS context requires policy validation (-p)" \
+	"invalid context|malformed context|process_file failed" \
+	sefcontext_bad_mls \
+	"${FIXTURES}/file_contexts/bad_mls.mod.fc"
+
+BAD_PP="${OUTDIR}/bad_context.pp"
+EXTRACTED_MOD="${OUTDIR}/extracted.mod"
+EXTRACTED_FC="${OUTDIR}/extracted.fc"
+
+echo "==== Setup: extract file contexts from packaged module with invalid context"
+rm -f "${EXTRACTED_MOD}" "${EXTRACTED_FC}"
+set +e
+"${SEMODULE_UNPACKAGE}" "${BAD_PP}" "${EXTRACTED_MOD}" "${EXTRACTED_FC}" \
+	2>"${OUTDIR}/unpackage.err"
+rc=$?
+set -e
+if [ "${rc}" -ne 0 ] || [ ! -s "${EXTRACTED_FC}" ]; then
+	die "unpackage setup: could not extract .fc from ${BAD_PP}"
+	cat "${OUTDIR}/unpackage.err" >&2
+else
+	echo "==== unpackage setup succeeded"
+	echo ""
+	expect_sefcontext_fail_or_skip_no_policy \
+		"invalid context from unpackage'd .pp file contexts" \
+		"invalid context|malformed context|process_file failed" \
+		unpackaged_bad_context \
+		"${EXTRACTED_FC}"
+fi
+
 echo "========================================"
 echo "Results: ${PASS} passed, ${FAIL} failed"
 if [ "${FAIL}" -ne 0 ]; then
-- 
2.55.0