. "$BST_LIB" # BST-TIMEOUT: 300 # TWO starts of the wasm32 CPython, not sixteen. The tool is # # driven once through data/khc-batch.py, which produces all # # fourteen captures in one process; the one start of the # # console script that remains is the version case, which # # exists to prove the shebang resolves at exec time. # # Sixteen starts of a 63 MB static interpreter was not merely # # slow: the guest does not return a process' memory on exit, # # so the suite's own shape read to the verify harness as a # # runaway host-RSS climb and the boot was ended mid-suite -- # # and the on-target runner replays a suite's output only when # # the suite ENDS, so every case that had already passed was # # lost with it. Measured on batch-1649: zero cases reported, # # twice, the second time on a fresh boot with nothing else # # running. # # kernel-hardening-checker 0.6.17.1 (meta-oe recipes-security, the a13xp0p0v git # at SRCREV afc376f2) -- a pure-Python tool that reads a kernel config, a kernel # cmdline and a sysctl dump and prints a verdict per hardening option. # # WHY THIS SUITE DRIVES FIXTURES AND NOT THE RUNNING KERNEL. The tool's default # mode is --autodetect, which reads /proc/config.gz, /proc/cmdline and the output # of `sysctl -a`. That would make every verdict a statement about whatever kernel # happens to be booted, so the same suite would report differently on two images # and a red would mean nothing. Everything asserted here is instead driven over # COMMITTED fixtures under $BST_DATA, so given the same inputs the tool produces # the same report and any difference is the port. # # HOW THE FIXTURES AND THE EXPECTATIONS WERE MADE, so they can be regenerated: # # khc-config `kernel-hardening-checker -g X86_64` at 0.6.17.1, plus a version # header, CONFIG_GCC_VERSION/CONFIG_CLANG_VERSION and the two # ARCH_MMAP_RND_*_MAX values the refine step needs -- then FIVE # deliberate mutations, one per FAIL shape the tool can print: # CONFIG_BUG unset -> FAIL: "is not set" # CONFIG_LSM_MMAP_MIN_ADDR wrong value -> FAIL: "32768" # CONFIG_DEBUG_WX deleted -> FAIL: is not found # CONFIG_WERROR deleted -> FAIL: is not found # CONFIG_DEVMEM set, wants off -> FAIL: "y" # plus CONFIG_BRINTOS_FIXTURE_UNKNOWN, a sentinel with no check, # which is what the -m verbose case below looks for. # khc-config.gz gzip -9 -n of the same bytes. It is not decoration: the .gz # path is the one that needs python3-compression, and a report # that differs between the two would mean the gzip module did. # khc-cmdline one line of kernel parameters, a mix of set-and-wanted, # set-and-unwanted (nokaslr) and absent. # khc-sysctl a `sysctl -a`-shaped dump, including kernel.printk and # kernel.cad_pid -- without those two the tool prints two # "try checking the output of sudo sysctl -a" warnings, so # leaving them out would have made the fixture's own shape part # of the expectation. # *.expected the tool's own output over those fixtures, produced by the # UPSTREAM SOURCE at the recipe's SRCREV under x86-64 CPython # 3.12, with the fixture directory rewritten to / exactly as # khc-batch.py rewrites it. So "identical to x86-64" is a real # cross-arm claim: the tool is pure Python and nothing in it may # depend on pointer width, byte order or the platform string. # # WHAT IS DELIBERATELY NOT ASSERTED: the verdict on this image's own kernel. It # gets one informational case at the end and is reported, never asserted -- the # hardening of the brintOS kernel is that kernel's business, not this port's. It # will also not resolve: SUPPORTED_ARCHS is ['X86_64','X86_32','ARM64','ARM', # 'RISCV'] and there is no wasm32 in it, so detect_arch_by_kconfig cannot succeed # on a config for this machine even when one is present. require kernel-hardening-checker kernel-hardening-checker/present KHC=/usr/bin/kernel-hardening-checker SITE=/usr/lib/python3.12/site-packages/kernel_hardening_checker CFG=$BST_DATA/khc-config CFGZ=$BST_DATA/khc-config.gz CMD=$BST_DATA/khc-cmdline SYS=$BST_DATA/khc-sysctl BAD=$BST_DATA/khc-config-bad SCRATCH=${TMPDIR:-/tmp}/bst-khc.$$ mkdir -p "$SCRATCH" 2>/dev/null # verdict : the check_result field of that option's row, # trimmed. Matching on the WHOLE first field and not with grep is what keeps # CONFIG_BUG from also matching CONFIG_BUG_ON_DATA_CORRUPTION. verdict() { awk -F'|' -v n="$1" 'NF > 3 { nm = $1; sub(/ +$/, "", nm) if (nm == n) { r = $NF; sub(/^ +/, "", r); sub(/ +$/, "", r); print r; exit } }' "$2" 2>/dev/null } # EVERY invocation below this line comes out of ONE interpreter start. # khc-batch.py sets sys.argv and calls main() once per argument vector, writing # each capture to $B/.out and each exit status to $B/.rc. It also # does what the shell used to: the tool echoes each input file's path in its # first lines and in two of its warnings, so a capture is path-dependent and the # committed expectation cannot be -- both sides carry / instead. Trailing # newlines are normalised the way the old `$( ... )` capture normalised them, so # the cmp cases compare the same bytes they always did. B=$SCRATCH/b mkdir -p "$B" 2>/dev/null python3 "$BST_DATA/khc-batch.py" "$BST_DATA" "$B" > "$SCRATCH/batch.log" 2>&1 batch_rc=$? # The driver is load-bearing for everything below, so its success is a case # rather than an assumption: without it every case reads an absent file and reds # for a reason that says nothing about the port. check_eq kernel-hardening-checker/batch-driver-rc "0" "$batch_rc" check kernel-hardening-checker/batch-driver-captures \ sh -c "test -s '$B/full.out' && test -s '$B/print.out'" rc_of() { cat "$B/$1.rc" 2>/dev/null; } # ---------------------------------------------------------- the deliverable --- check kernel-hardening-checker/binary test -x $KHC check kernel-hardening-checker/module-init test -f $SITE/__init__.py check kernel-hardening-checker/module-checks test -f $SITE/checks.py check kernel-hardening-checker/module-engine test -f $SITE/engine.py # The console script setuptools generates carries `#!/usr/bin/env python3`, so # the port depends on env(1) resolving python3 on PATH -- a dependency no # RDEPENDS line states, and one that fails at exec time rather than at rootfs # time. Both halves get a case. check_eq kernel-hardening-checker/shebang "#!/usr/bin/env python3" \ "$(head -n 1 $KHC 2>&1)" check kernel-hardening-checker/env-on-path sh -c 'command -v env >/dev/null' check kernel-hardening-checker/python3-on-path sh -c 'command -v python3 >/dev/null' # THE PACKAGE SPLIT, ASSERTED AS AN ABSENCE. 17 of the upstream package's 18 MB # are config_files/ (CI defconfig captures) and test_engine.py, neither reachable # from the entry point; the bbappend moves them to a -testdata package the image # does not install. Asserting only that the tool works would not notice the day # that split stops taking, and on a guest that holds its rootfs in memory 17 MB # is not a rounding error. check kernel-hardening-checker/no-testdata-configs sh -c "! test -d $SITE/config_files" check kernel-hardening-checker/no-testdata-engine sh -c "! test -f $SITE/test_engine.py" # ------------------------------------------------------ identity, one start --- # THE ONE REMAINING START OF THE CONSOLE SCRIPT, kept as a real exec: the # shebang case above is only a claim about the first line of a file, and this is # the case that proves `env python3` resolves it at exec time. _ver=$( "$KHC" --version 2>&1 ); rc=$? check_eq kernel-hardening-checker/version-rc "0" "$rc" check_eq kernel-hardening-checker/version "kernel-hardening-checker 0.6.17.1" \ "$_ver" # ------------------------------------- -c over the committed Kconfig fixture --- CFG_OUT=$B/cfg.out check_eq kernel-hardening-checker/config-rc "0" "$(rc_of cfg)" # The three lines the tool derives from the fixture rather than reads out of it. check_eq kernel-hardening-checker/config-detect-version \ "[+] Detected kernel version: (6, 12, 9)" \ "$(grep -F 'Detected kernel version' "$CFG_OUT" 2>&1)" check_eq kernel-hardening-checker/config-detect-arch \ "[+] Detected architecture: X86_64" \ "$(grep -F 'Detected architecture' "$CFG_OUT" 2>&1)" check_eq kernel-hardening-checker/config-detect-compiler \ "[+] Detected compiler: GCC 140200" \ "$(grep -F 'Detected compiler' "$CFG_OUT" 2>&1)" # THE ENUMERATED VERDICT LIST. One case per mutation in khc-config, each written # out in full here rather than compared against a file, so a red names the # verdict that changed and the fixture's design is readable beside it. check_eq kernel-hardening-checker/verdict-bug-unset \ 'FAIL: "is not set"' "$(verdict CONFIG_BUG "$CFG_OUT")" check_eq kernel-hardening-checker/verdict-lsm-mmap-min-addr-wrong \ 'FAIL: "32768"' "$(verdict CONFIG_LSM_MMAP_MIN_ADDR "$CFG_OUT")" check_eq kernel-hardening-checker/verdict-debug-wx-absent \ 'FAIL: is not found' "$(verdict CONFIG_DEBUG_WX "$CFG_OUT")" check_eq kernel-hardening-checker/verdict-werror-absent \ 'FAIL: is not found' "$(verdict CONFIG_WERROR "$CFG_OUT")" check_eq kernel-hardening-checker/verdict-devmem-set \ 'FAIL: "y"' "$(verdict CONFIG_DEVMEM "$CFG_OUT")" check_eq kernel-hardening-checker/verdict-slub-debug-ok \ 'OK' "$(verdict CONFIG_SLUB_DEBUG "$CFG_OUT")" # Not a mutation: a check whose FAIL is about an unmet DEPENDENCY rather than # about the option's own value. The fixture declares GCC, so the clang-only # checks report against CONFIG_CC_IS_CLANG. It is the fifth FAIL shape. check_eq kernel-hardening-checker/verdict-cfi-clang-dependency \ 'FAIL: CONFIG_CC_IS_CLANG is not "y"' "$(verdict CONFIG_CFI_CLANG "$CFG_OUT")" check_eq kernel-hardening-checker/config-summary \ "[+] Config check is finished: 'OK' - 237 / 'FAIL' - 13" \ "$(grep -F 'Config check is finished' "$CFG_OUT" 2>&1)" check_eq kernel-hardening-checker/config-fail-count "13" \ "$(grep -c '| FAIL' "$CFG_OUT" 2>&1)" # ------------------------------------------------ the same config, gzipped --- # gzip.open is the tool's only use of python3-compression, and it is reached only # by a .gz argument. The report must be byte-identical to the plain one once the # echoed filename is removed. GZ_OUT=$B/gz.out check_eq kernel-hardening-checker/gz-rc "0" "$(rc_of gz)" check_eq kernel-hardening-checker/gz-identical-to-plain \ "$(grep -v 'file to check:' "$CFG_OUT" 2>&1)" \ "$(grep -v 'file to check:' "$GZ_OUT" 2>&1)" # ------------------------------ -c -l -s together, against the x86-64 control --- # One run, then one case per section of the report. Grouping keeps a red pointing # at the section that changed instead of at a 300-line blob, and keeps the case # list stable when upstream adds a check. FULL_OUT=$B/full.out FULL_EXP=$BST_DATA/khc-report.expected check_eq kernel-hardening-checker/full-rc "0" "$(rc_of full)" sect() { # sect check_eq "kernel-hardening-checker/$1" \ "$(awk -F'|' -v t="$2" 'NF > 3 { ty=$2; sub(/ +$/,"",ty); sub(/^ +/,"",ty); if (ty==t) print }' "$FULL_EXP" 2>/dev/null)" \ "$(awk -F'|' -v t="$2" 'NF > 3 { ty=$2; sub(/ +$/,"",ty); sub(/^ +/,"",ty); if (ty==t) print }' "$FULL_OUT" 2>/dev/null)" } sect full-kconfig-rows kconfig sect full-cmdline-rows cmdline sect full-sysctl-rows sysctl # The header the tool derives, and the score. The score is its own case because a # run that died half way through the table would otherwise match every section it # had already printed. check_eq kernel-hardening-checker/full-header \ "$(grep '^\[[+!-]\]' "$FULL_EXP" 2>/dev/null)" \ "$(grep '^\[[+!-]\]' "$FULL_OUT" 2>/dev/null)" check kernel-hardening-checker/full-identical \ sh -c "cmp -s '$FULL_EXP' '$FULL_OUT'" # Three verdicts from the cmdline arm and three from the sysctl arm, written out # for the same reason the Kconfig ones are. check_eq kernel-hardening-checker/verdict-nokaslr-present \ 'FAIL: ""' "$(verdict nokaslr "$FULL_OUT")" check_eq kernel-hardening-checker/verdict-rodata-on \ 'OK' "$(verdict rodata "$FULL_OUT")" check_eq kernel-hardening-checker/verdict-mds-not-off \ 'OK: is not off, "full,nosmt"' "$(verdict mds "$FULL_OUT")" check_eq kernel-hardening-checker/verdict-modules-disabled \ 'FAIL: "0"' "$(verdict kernel.modules_disabled "$FULL_OUT")" check_eq kernel-hardening-checker/verdict-kptr-restrict \ 'OK' "$(verdict kernel.kptr_restrict "$FULL_OUT")" check_eq kernel-hardening-checker/verdict-default-accept-redirects \ 'FAIL: "1"' "$(verdict net.ipv4.conf.default.accept_redirects "$FULL_OUT")" # ---------------------------------------- -s alone: the no-Kconfig code path --- # perform_checking(mode, None, None, None, sysctl) is a different branch, and the # arch comes from `kernel.arch` rather than from a CONFIG_ line. SYS_OUT=$B/sys.out SYS_EXP=$BST_DATA/khc-sysctl-only.expected check_eq kernel-hardening-checker/sysctl-only-rc "0" "$(rc_of sys)" check_eq kernel-hardening-checker/sysctl-only-arch \ "[+] Detected architecture: X86_64 (x86_64)" \ "$(grep -F 'Detected architecture' "$SYS_OUT" 2>&1)" check kernel-hardening-checker/sysctl-only-identical \ sh -c "cmp -s '$SYS_EXP' '$SYS_OUT'" # ------------------------------------------------------------- -m json mode --- # json mode routes every diagnostic through mprint, which drops it, so the whole # of stdout is one JSON document and nothing in it carries a path. That is what # makes this capture comparable without normalisation. JSON_OUT=$B/json.out JSON_EXP=$BST_DATA/khc-json.expected check_eq kernel-hardening-checker/json-rc "0" "$(rc_of json)" check_eq kernel-hardening-checker/json-one-line "1" "$(wc -l < "$JSON_OUT" 2>&1)" check kernel-hardening-checker/json-starts-array \ sh -c "head -c 1 '$JSON_OUT' | grep -q '\['" check kernel-hardening-checker/json-no-banner \ sh -c "! grep -q '^\[+\]' '$JSON_OUT'" check kernel-hardening-checker/json-identical \ sh -c "cmp -s '$JSON_EXP' '$JSON_OUT'" # The document is parsed by the interpreter that produced it, so a well-formed # capture is asserted rather than assumed -- json.dumps and json.loads are # different halves of python3-json. check_eq kernel-hardening-checker/json-parses "0" "$(rc_of json-parses)" # ---------------------------------------------------------- -m verbose mode --- # verbose adds the mode banner, a separator line under every row, and one line # per Kconfig option the tool has no check for. CONFIG_BRINTOS_FIXTURE_UNKNOWN is # in khc-config for exactly this case. VERB_OUT=$B/verbose.out check_eq kernel-hardening-checker/verbose-rc "0" "$(rc_of verbose)" check_eq kernel-hardening-checker/verbose-banner "[+] Special report mode: verbose" \ "$(head -n 1 "$VERB_OUT" 2>&1)" check_eq kernel-hardening-checker/verbose-unknown-option \ "[?] No check for kconfig option CONFIG_BRINTOS_FIXTURE_UNKNOWN (y)" \ "$(grep -F 'CONFIG_BRINTOS_FIXTURE_UNKNOWN' "$VERB_OUT" 2>&1)" check_eq kernel-hardening-checker/verbose-same-score \ "$(grep -F 'Config check is finished' "$CFG_OUT" 2>&1)" \ "$(grep -F 'Config check is finished' "$VERB_OUT" 2>&1)" # -------------------------------------------------------- -m show_fail mode --- # show_fail prints the failing rows only and says so in the score line. The count # of rows it prints must equal the FAIL count of the full report -- that equality # is the whole of the mode. SF_OUT=$B/showfail.out check_eq kernel-hardening-checker/show-fail-rc "0" "$(rc_of showfail)" check_eq kernel-hardening-checker/show-fail-rows "13" \ "$(awk -F'|' 'NF > 3 && $1 !~ /option_name/' "$SF_OUT" 2>/dev/null | wc -l)" check kernel-hardening-checker/show-fail-suppresses-ok \ sh -c "! awk -F'|' 'NF > 3 && \$NF ~ /OK/' '$SF_OUT' | grep -q ." check_eq kernel-hardening-checker/show-fail-score \ "[+] Config check is finished: 'OK' - 237 (suppressed in output) / 'FAIL' - 13" \ "$(grep -F 'Config check is finished' "$SF_OUT" 2>&1)" # ----------------------------------------- -p and -g: no input file at all --- # Both build the checklist from checks.py and print it, so they exercise the # recommendation tables themselves rather than the parsers. P_OUT=$B/print.out check_eq kernel-hardening-checker/print-rc "0" "$(rc_of print)" check_eq kernel-hardening-checker/print-banner \ "[+] Printing kernel security hardening options for X86_64..." \ "$(grep -F 'Printing kernel security' "$P_OUT" 2>&1)" check_eq kernel-hardening-checker/print-rows "352" \ "$(awk -F'|' 'NF > 3 && $1 !~ /option_name/' "$P_OUT" 2>/dev/null | wc -l)" check kernel-hardening-checker/print-has-no-results \ sh -c "! grep -q 'check_result' '$P_OUT'" G_OUT=$B/generate.out G_EXP=$BST_DATA/khc-generate-x86_64.expected check_eq kernel-hardening-checker/generate-rc "0" "$(rc_of generate)" check_eq kernel-hardening-checker/generate-first-line "CONFIG_X86_64=y" \ "$(head -n 1 "$G_OUT" 2>&1)" check kernel-hardening-checker/generate-identical \ sh -c "cmp -s '$G_EXP' '$G_OUT'" # A generated fragment is a Kconfig file, so the tool must be able to read its own # output back. This is the one case that closes the loop between -g and -c. check_eq kernel-hardening-checker/generate-is-parseable "0" \ "$(rc_of generate-parse)" # ------------------------------------- the documented failures, and their rc --- # Each of these is a sys.exit() in the tool, which POSIX renders as # status 1 with the message on stderr -- distinct from status 2, which is # argparse refusing an argument before the tool ever runs. E1=$B/err-parse.out check_eq kernel-hardening-checker/err-unparseable-rc "1" "$(rc_of err-parse)" check_eq kernel-hardening-checker/err-unparseable-msg \ '[-] ERROR: unexpected line in Kconfig file: "this line is not a Kconfig statement"' \ "$(grep -F 'ERROR:' "$E1" 2>&1)" E2=$B/err-open.out check_eq kernel-hardening-checker/err-missing-file-rc "1" "$(rc_of err-open)" check_eq kernel-hardening-checker/err-missing-file-msg \ '[-] ERROR: unable to open /nonexistent-kconfig, are you sure it exists?' \ "$(grep -F 'ERROR:' "$E2" 2>&1)" E3=$B/err-order.out check_eq kernel-hardening-checker/err-cmdline-alone-rc "1" "$(rc_of err-order)" check_eq kernel-hardening-checker/err-cmdline-alone-msg \ '[-] ERROR: checking cmdline depends on checking Kconfig' \ "$(grep -F 'ERROR:' "$E3" 2>&1)" E4=$B/err-arch.out check_eq kernel-hardening-checker/err-unsupported-arch-rc "2" "$(rc_of err-arch)" check kernel-hardening-checker/err-unsupported-arch-msg \ sh -c "grep -q \"invalid choice: 'WASM32'\" '$E4'" # --------------------------------------------- informational: this image's own --- # REPORTED, NOT ASSERTED. Whether this kernel is hardened is the kernel's # business; what this case records is only what the tool says when pointed at it. # It is expected not to resolve at all -- SUPPORTED_ARCHS carries no wasm32, so # detect_arch_by_kconfig has nothing to match -- and that is a fact about the # tool's recommendation tables, not a defect in this port. IMG_KCONFIG= for c in /proc/config.gz /boot/config-"$(uname -r 2>/dev/null)"; do [ -f "$c" ] && { IMG_KCONFIG=$c; break; } done if [ -z "$IMG_KCONFIG" ]; then bst_skip kernel-hardening-checker/image-kconfig \ "no /proc/config.gz and no /boot/config-* on this image -- nothing to report" else _img=$( "$KHC" -c "$IMG_KCONFIG" 2>&1 ); _img_rc=$? bst_skip kernel-hardening-checker/image-kconfig \ "$IMG_KCONFIG rc=$_img_rc: $(_bst_1line "$(printf '%s' "$_img" | grep -E 'ERROR|Config check is finished|Detected' | tr '\n' ' ')")" fi rm -rf "$SCRATCH" 2>/dev/null exit 0