Re: [PATCH] semodule-utils: add bad-data tests for semodule_package
Stephen Smalley <[email protected]> Wed, 22 Jul 2026 08:21:29 -0400
| Newsgroups | org.kernel.vger.selinux |
|---|---|
| Message-ID | <CAEjxPJ6i97g7nq-Yx2UJ3wJLYUNfH-w4_dvXF2L=NKXxHn6B-Q@mail.gmail.com> |
On Tue, Jul 21, 2026 at 5:34 PM Akhil Kohli <[email protected]> wrote: > > Add tests/bad-data/run.sh and fixtures for semodule_package bad-data > protection: input path validation for -m and -f (missing paths, directories, > broken symlinks, unreadable files, empty arguments) and malformed .mod.fc > content at packaging time. Wire the driver into semodule-utils make test. > Semantic validation of bad file contexts is deferred to sefcontext_compile. > > Run the harness as a non-root user in GitHub Actions so unreadable -m/-f > inputs are exercised in CI. > > --- > v2 changes: > - Fix CI non-root bad-data run: cd to repo root and chmod checkout for runuser > - Fail fast if run.sh cannot resolve BASEDIR You didn't update the subject line to include "v2" (use -v2 to git-send-email to add it automatically), which confuses b4 when it fetches the patches and tries to decide which one(s) to apply. We can manually fetch and apply but note for future patches please. > > Signed-off-by: Akhil Kohli <[email protected]> > --- > .github/workflows/run_tests.yml | 11 + > semodule-utils/Makefile | 1 + > .../fixtures/file_contexts/bad_context.mod.fc | 1 + > .../fixtures/file_contexts/bad_fields.mod.fc | 1 + > .../fixtures/file_contexts/good.mod.fc | 1 + > .../tests/bad-data/fixtures/modules/good.te | 8 + > semodule-utils/tests/bad-data/run.sh | 347 ++++++++++++++++++ > 7 files changed, 370 insertions(+) > create mode 100644 semodule-utils/tests/bad-data/fixtures/file_contexts/bad_context.mod.fc > create mode 100644 semodule-utils/tests/bad-data/fixtures/file_contexts/bad_fields.mod.fc > create mode 100644 semodule-utils/tests/bad-data/fixtures/file_contexts/good.mod.fc > create mode 100644 semodule-utils/tests/bad-data/fixtures/modules/good.te > create mode 100755 semodule-utils/tests/bad-data/run.sh > > diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml > index f4aa1a4c..45778c25 100644 > --- a/.github/workflows/run_tests.yml > +++ b/.github/workflows/run_tests.yml > @@ -82,6 +82,17 @@ jobs: > eval make test $EXPLICIT_MAKE_VARS > echo "::endgroup::" > > + # semodule_package bad-data: unreadable -m/-f inputs need non-root (root reads mode 000). > + if [ "${{ matrix.python-ruby-version.other }}" != "sanitizers" ] ; then > + echo "::group::semodule-utils bad-data (non-root)" > + sudo useradd -m -s /usr/sbin/nologin bad-data-test 2>/dev/null || true > + chmod -R a+rX . > + sudo runuser -u bad-data-test -- env PATH="$PATH" LD_LIBRARY_PATH="$LD_LIBRARY_PATH" \ > + sh -c 'cd "$1" && exec ./semodule-utils/tests/bad-data/run.sh' sh "$PWD" > + sudo userdel -r bad-data-test 2>/dev/null || true > + echo "::endgroup::" > + fi > + > if [ "${{ matrix.python-ruby-version.other }}" != "sanitizers" ] ; then > # Test Python and Ruby wrappers > echo "::group::Test Python and Ruby wrappers" > diff --git a/semodule-utils/Makefile b/semodule-utils/Makefile > index 81c97af0..4a902146 100644 > --- a/semodule-utils/Makefile > +++ b/semodule-utils/Makefile > @@ -6,3 +6,4 @@ all install relabel clean: > done > > test: > + ./tests/bad-data/run.sh > diff --git a/semodule-utils/tests/bad-data/fixtures/file_contexts/bad_context.mod.fc b/semodule-utils/tests/bad-data/fixtures/file_contexts/bad_context.mod.fc > new file mode 100644 > index 00000000..06aec43e > --- /dev/null > +++ b/semodule-utils/tests/bad-data/fixtures/file_contexts/bad_context.mod.fc > @@ -0,0 +1 @@ > +/usr/bin/bad not_a_valid_selinux_context > diff --git a/semodule-utils/tests/bad-data/fixtures/file_contexts/bad_fields.mod.fc b/semodule-utils/tests/bad-data/fixtures/file_contexts/bad_fields.mod.fc > new file mode 100644 > index 00000000..13821d61 > --- /dev/null > +++ b/semodule-utils/tests/bad-data/fixtures/file_contexts/bad_fields.mod.fc > @@ -0,0 +1 @@ > +only_one_field_on_this_line > diff --git a/semodule-utils/tests/bad-data/fixtures/file_contexts/good.mod.fc b/semodule-utils/tests/bad-data/fixtures/file_contexts/good.mod.fc > new file mode 100644 > index 00000000..12b26cae > --- /dev/null > +++ b/semodule-utils/tests/bad-data/fixtures/file_contexts/good.mod.fc > @@ -0,0 +1 @@ > +/usr/bin/test_good -- system_u:object_r:test_good_exec_t:s0 > diff --git a/semodule-utils/tests/bad-data/fixtures/modules/good.te b/semodule-utils/tests/bad-data/fixtures/modules/good.te > new file mode 100644 > index 00000000..a079aed6 > --- /dev/null > +++ b/semodule-utils/tests/bad-data/fixtures/modules/good.te > @@ -0,0 +1,8 @@ > +module test_good 1.0; > + > +require { > + type test_good_t; > + class file { read }; > +} > + > +allow test_good_t self:file read; > diff --git a/semodule-utils/tests/bad-data/run.sh b/semodule-utils/tests/bad-data/run.sh > new file mode 100755 > index 00000000..0265032a > --- /dev/null > +++ b/semodule-utils/tests/bad-data/run.sh > @@ -0,0 +1,347 @@ > +#!/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 on the Bucket D branch). > +# Unreadable -m/-f cases skip when run as root; CI runs this script as non-root. > +# > + > +set -u > + > +BASEDIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) || { > + echo "FAIL: cannot resolve script directory (\$0=$0)" >&2 > + exit 1 > +} > +FIXTURES="${BASEDIR}/fixtures" > +OUTDIR=$(mktemp -d "${TMPDIR:-/tmp}/semodule-package-bad-data.XXXXXX") > +PASS=0 > +FAIL=0 > + > +CHECKMODULE=${CHECKMODULE:-checkmodule} > +# Modular TE for MCS/MLS builds (matches RHIVOS checkmodule -M -m). > +CHECKMODULE_MOD_FLAGS=${CHECKMODULE_MOD_FLAGS:--M -m} > +SEMODULE_PACKAGE=${SEMODULE_PACKAGE:-semodule_package} > + > +GOOD_TE="${FIXTURES}/modules/good.te" > +GOOD_MOD="${OUTDIR}/test_good.mod" > + > +cleanup() { > + rm -rf "${OUTDIR}" > +} > +trap cleanup EXIT > + > +die() { > + echo "FAIL: $*" >&2 > + FAIL=$((FAIL + 1)) > +} > + > +pass() { > + echo "==== $*" > + PASS=$((PASS + 1)) > + echo "" > +} > + > +build_good_mod() { > + echo "==== Setup: build control module ${GOOD_MOD} from good.te" > + rm -f "${GOOD_MOD}" > + > + set +e > + # shellcheck disable=SC2086 > + "${CHECKMODULE}" ${CHECKMODULE_MOD_FLAGS} -o "${GOOD_MOD}" "${GOOD_TE}" \ > + 2>"${OUTDIR}/build_good.err" > + rc=$? > + set -e > + > + if [ "${rc}" -ne 0 ]; then > + echo "FAIL: could not build control module from ${GOOD_TE}" >&2 > + cat "${OUTDIR}/build_good.err" >&2 > + exit 1 > + fi > + if [ ! -s "${GOOD_MOD}" ]; then > + echo "FAIL: ${GOOD_MOD} is empty" >&2 > + exit 1 > + fi > + echo "" > +} > + > +expect_package_pass() { > + desc="$1" > + outname="$2" > + shift 2 > + > + outpp="${OUTDIR}/${outname}.pp" > + stderr="${OUTDIR}/${outname}.err" > + > + echo "==== POSITIVE (expect semodule_package success): ${desc}" > + rm -f "${outpp}" > + > + set +e > + "${SEMODULE_PACKAGE}" -o "${outpp}" "$@" 2>"${stderr}" > + rc=$? > + set -e > + > + if [ "${rc}" -ne 0 ]; then > + echo "stderr:" >&2 > + cat "${stderr}" >&2 > + die "${desc}: expected exit 0, got rc=${rc}" > + return 0 > + fi > + if [ ! -s "${outpp}" ]; then > + die "${desc}: expected non-empty ${outpp}" > + return 0 > + fi > + > + pass "${desc} (exit 0, .pp created)" > +} > + > +expect_package_pass_deferred() { > + desc="$1" > + outname="$2" > + shift 2 > + > + outpp="${OUTDIR}/${outname}.pp" > + stderr="${OUTDIR}/${outname}.err" > + > + echo "==== DOCUMENT (semodule_package accepts input; validate in sefcontext_compile): ${desc}" > + rm -f "${outpp}" > + > + set +e > + "${SEMODULE_PACKAGE}" -o "${outpp}" "$@" 2>"${stderr}" > + rc=$? > + set -e > + > + if [ "${rc}" -ne 0 ]; then > + echo "stderr:" >&2 > + cat "${stderr}" >&2 > + die "${desc}: expected exit 0 at packaging stage, got rc=${rc}" > + return 0 > + fi > + if [ ! -s "${outpp}" ]; then > + die "${desc}: expected non-empty ${outpp} at packaging stage" > + return 0 > + fi > + > + pass "${desc} (packaging exit 0; labeling validation deferred to sefcontext_compile)" > +} > + > +expect_package_fail() { > + desc="$1" > + pattern="$2" > + outname="$3" > + shift 3 > + > + outpp="${OUTDIR}/${outname}.pp" > + stderr="${OUTDIR}/${outname}.err" > + > + echo "==== NEGATIVE (expect semodule_package failure): ${desc}" > + rm -f "${outpp}" > + > + set +e > + "${SEMODULE_PACKAGE}" -o "${outpp}" "$@" 2>"${stderr}" > + rc=$? > + set -e > + > + if [ "${rc}" -eq 0 ]; then > + die "${desc}: expected non-zero exit, got rc=0" > + return 0 > + fi > + if [ -f "${outpp}" ]; then > + die "${desc}: did not expect output ${outpp}" > + 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_package_fail_unreadable() { > + desc="unreadable -m file" > + mod="${OUTDIR}/unreadable.mod" > + outpp="${OUTDIR}/unreadable.pp" > + stderr="${OUTDIR}/unreadable.err" > + > + echo "==== NEGATIVE (expect semodule_package failure): ${desc}" > + if [ "$(id -u)" -eq 0 ]; then > + echo "SKIP: root can read mode 000 files; unreadable check is non-root only" > + PASS=$((PASS + 1)) > + echo "" > + return 0 > + fi > + > + rm -f "${outpp}" > + cp "${GOOD_MOD}" "${mod}" > + chmod 000 "${mod}" > + > + set +e > + "${SEMODULE_PACKAGE}" -o "${outpp}" -m "${mod}" 2>"${stderr}" > + rc=$? > + set -e > + > + if [ "${rc}" -eq 0 ]; then > + die "${desc}: expected non-zero exit, got rc=0" > + return 0 > + fi > + if [ -f "${outpp}" ]; then > + die "${desc}: did not expect output ${outpp}" > + return 0 > + fi > + if ! grep -Eq 'Permission denied|Could not open|Failed to open' "${stderr}"; then > + echo "FAIL: stderr did not mention permission or open failure" >&2 > + cat "${stderr}" >&2 > + FAIL=$((FAIL + 1)) > + return 0 > + fi > + > + pass "${desc} (rejected as expected, rc=${rc})" > +} > + > +expect_package_fail_unreadable_fc() { > + desc="unreadable -f file" > + fc="${OUTDIR}/unreadable.mod.fc" > + outpp="${OUTDIR}/unreadable_fc.pp" > + stderr="${OUTDIR}/unreadable_fc.err" > + > + echo "==== NEGATIVE (expect semodule_package failure): ${desc}" > + if [ "$(id -u)" -eq 0 ]; then > + echo "SKIP: root can read mode 000 files; unreadable check is non-root only" > + PASS=$((PASS + 1)) > + echo "" > + return 0 > + fi > + > + rm -f "${outpp}" > + cp "${GOOD_FC}" "${fc}" > + chmod 000 "${fc}" > + > + set +e > + "${SEMODULE_PACKAGE}" -o "${outpp}" -m "${GOOD_MOD}" -f "${fc}" \ > + 2>"${stderr}" > + rc=$? > + set -e > + > + if [ "${rc}" -eq 0 ]; then > + die "${desc}: expected non-zero exit, got rc=0" > + return 0 > + fi > + if [ -f "${outpp}" ]; then > + die "${desc}: did not expect output ${outpp}" > + return 0 > + fi > + if ! grep -Eq 'Permission denied|Could not open|Failed to open' "${stderr}"; then > + echo "FAIL: stderr did not mention permission or open failure" >&2 > + cat "${stderr}" >&2 > + FAIL=$((FAIL + 1)) > + return 0 > + fi > + > + pass "${desc} (rejected as expected, rc=${rc})" > +} > + > +build_good_mod > + > +# Ephemeral path fixtures for packaging path tests. > +ln -sf /nonexistent/test_good.mod "${OUTDIR}/broken_symlink.mod" > +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' \ > + > "${OUTDIR}/nul_bytes.mod.fc" > + > +GOOD_FC="${FIXTURES}/file_contexts/good.mod.fc" > + > +# --- semodule_package input paths --- > +expect_package_pass \ > + "control good .mod without file contexts" \ > + good_mod \ > + -m "${GOOD_MOD}" > + > +expect_package_pass \ > + "control good .mod with good .mod.fc" \ > + good_mod_fc \ > + -m "${GOOD_MOD}" -f "${GOOD_FC}" > + > +expect_package_fail \ > + "missing -m path" \ > + "Could not open|Failed to open" \ > + missing_mod \ > + -m "${OUTDIR}/does_not_exist.mod" > + > +expect_package_fail \ > + "missing -f path" \ > + "Failed to open|Could not open" \ > + missing_fc \ > + -m "${GOOD_MOD}" -f "${OUTDIR}/does_not_exist.mod.fc" > + > +expect_package_fail \ > + "directory instead of -m file" \ > + "Error while reading policy module|Could not open" \ > + directory_mod \ > + -m "${BASEDIR}" > + > +expect_package_fail \ > + "broken symlink for -m" \ > + "Could not open|Failed to open|No such file" \ > + symlink_mod \ > + -m "${OUTDIR}/broken_symlink.mod" > + > +expect_package_fail_unreadable > + > +expect_package_fail \ > + "empty -m path argument" \ > + "Could not open|Failed to open" \ > + empty_mod \ > + -m "" > + > +expect_package_fail \ > + "directory instead of -f file" \ > + "Permission denied|Failed to mmap|Failed to open|Could not open" \ > + directory_fc \ > + -m "${GOOD_MOD}" -f "${BASEDIR}" > + > +expect_package_fail \ > + "broken symlink for -f" \ > + "Could not open|Failed to open|No such file" \ > + symlink_fc \ > + -m "${GOOD_MOD}" -f "${OUTDIR}/broken_symlink.mod.fc" > + > +expect_package_fail_unreadable_fc > + > +expect_package_fail \ > + "empty -f path argument" \ > + "Could not open|Failed to open" \ > + empty_fc_arg \ > + -m "${GOOD_MOD}" -f "" > + > +# --- bad .mod.fc at packaging time --- > +expect_package_pass_deferred \ > + "invalid SELinux context in .mod.fc" \ > + bad_context \ > + -m "${GOOD_MOD}" -f "${FIXTURES}/file_contexts/bad_context.mod.fc" > + > +expect_package_pass_deferred \ > + "wrong field count in .mod.fc" \ > + bad_fields \ > + -m "${GOOD_MOD}" -f "${FIXTURES}/file_contexts/bad_fields.mod.fc" > + > +expect_package_pass_deferred \ > + "NUL byte in .mod.fc path field" \ > + nul_bytes \ > + -m "${GOOD_MOD}" -f "${OUTDIR}/nul_bytes.mod.fc" > + > +expect_package_pass_deferred \ > + "empty .mod.fc file" \ > + empty_fc \ > + -m "${GOOD_MOD}" -f "${OUTDIR}/empty.mod.fc" > + > +echo "========================================" > +echo "Results: ${PASS} passed, ${FAIL} failed" > +if [ "${FAIL}" -ne 0 ]; then > + exit 1 > +fi > +exit 0 > -- > 2.55.0 > >