Re: [PATCH v3] semodule-utils: add bad-data tests for semodule_package
Stephen Smalley <[email protected]> Thu, 23 Jul 2026 11:46:36 -0400
| Newsgroups | org.kernel.vger.selinux |
|---|---|
| Message-ID | <CAEjxPJ77sv_bX0B9vhKnZKay9xnu-TOvA0LZW3f04avLdTK3uA@mail.gmail.com> |
On Thu, Jul 23, 2026 at 9:16 AM 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. Semantic validation of bad file contexts is > deferred to sefcontext_compile > > Signed-off-by: Akhil Kohli <[email protected]> Looks good to me but would appreciate review by those more familiar with the GitHub CI. Acked-by: Stephen Smalley <[email protected]> > --- > .github/workflows/run_tests.yml | 13 + > 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 | 355 ++++++++++++++++++ > 7 files changed, 380 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..4322ddff 100644 > --- a/.github/workflows/run_tests.yml > +++ b/.github/workflows/run_tests.yml > @@ -82,6 +82,19 @@ 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 > + # World-readable checkout; run via relative path (inherited CWD). Do not > + # cd to $PWD — bad-data-test cannot traverse /home/runner/work/... . > + chmod -R a+rX . > + sudo runuser -u bad-data-test -- env PATH="$PATH" LD_LIBRARY_PATH="$LD_LIBRARY_PATH" \ > + ./semodule-utils/tests/bad-data/run.sh > + 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..7c9f7c7d > --- /dev/null > +++ b/semodule-utils/tests/bad-data/run.sh > @@ -0,0 +1,355 @@ > +#!/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. > +# > + > +set -u > + > +# Prefer an absolute script dir, but keep a relative dirname when cd fails. > +# Non-root CI can inherit the repo as CWD without being able to traverse > +# /home/runner/work/...; relative paths from that CWD still work. > +BASEDIR=$(dirname -- "$0") > +ABS_BASEDIR=$(CDPATH= cd -- "${BASEDIR}" 2>/dev/null && pwd) || ABS_BASEDIR= > +if [ -n "${ABS_BASEDIR}" ]; then > + BASEDIR="${ABS_BASEDIR}" > +fi > +FIXTURES="${BASEDIR}/fixtures" > +if [ ! -d "${FIXTURES}" ]; then > + echo "FAIL: cannot resolve fixtures directory (\$0=$0 BASEDIR=${BASEDIR})" >&2 > + exit 1 > +fi > +OUTDIR=$(mktemp -d "${TMPDIR:-/tmp}/semodule-package-bad-data.XXXXXX") > +PASS=0 > +FAIL=0 > + > +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} > + > +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 > >