diff --git a/deploy.sh b/deploy.sh index dca8cac..e712dfa 100644 --- a/deploy.sh +++ b/deploy.sh @@ -36,6 +36,21 @@ ENABLED_EXTENSION_MANIFESTS="$LOCAL_BOOTSTRAP_MANIFEST" # USE_AF_XDP=1 to make and rejects a cached AF_PACKET-only binary. ANYSCAN_USE_AF_XDP="${ANYSCAN_USE_AF_XDP:-0}" +# Opt-in kernel backport upgrade. Mirrors install-external-deps.sh — +# see PR 65 issuecomment-4336192354 / anygpt-42 / anygpt-44. Default 0 +# leaves the running kernel untouched (existing AMIs unchanged). 1 +# installs the Debian bookworm-backports kernel image so the host can +# run kernel 6.16+ with the in-flight ena_xdp_zc patches that AF_XDP +# zerocopy on ENA needs. Never auto-reboots; the operator schedules +# the reboot. After install probes /sys/module/ena/version + dmesg +# for ena_xdp_zc support. +ANYSCAN_INSTALL_KERNEL_BACKPORT="${ANYSCAN_INSTALL_KERNEL_BACKPORT:-0}" +ANYSCAN_KERNEL_BACKPORT_MIN_VERSION="${ANYSCAN_KERNEL_BACKPORT_MIN_VERSION:-6.16}" +ANYSCAN_KERNEL_BACKPORT_PACKAGE="${ANYSCAN_KERNEL_BACKPORT_PACKAGE:-linux-image-cloud-amd64}" +ANYSCAN_KERNEL_BACKPORT_SUITE="${ANYSCAN_KERNEL_BACKPORT_SUITE:-bookworm-backports}" +ANYSCAN_KERNEL_BACKPORT_SOURCES_LIST="${ANYSCAN_KERNEL_BACKPORT_SOURCES_LIST:-/etc/apt/sources.list.d/anyscan-bookworm-backports.list}" +ANYSCAN_KERNEL_BACKPORT_MIRROR="${ANYSCAN_KERNEL_BACKPORT_MIRROR:-http://deb.debian.org/debian}" + print_banner() { printf '═══════════════════════════════════════════════════════════\n' printf ' AnyScan Rust Deploy Script \n' @@ -105,6 +120,110 @@ binary_has_afxdp_linkage() { return 1 } +# Mirrors install-external-deps.sh::kernel_version_at_least. +kernel_version_at_least() { + local have="$1" need="$2" + local have_major have_minor need_major need_minor + have_major="${have%%.*}" + have_minor="${have#*.}" + have_minor="${have_minor%%.*}" + have_minor="${have_minor%%[!0-9]*}" + need_major="${need%%.*}" + need_minor="${need#*.}" + need_minor="${need_minor%%.*}" + need_minor="${need_minor%%[!0-9]*}" + have_major="${have_major:-0}" + have_minor="${have_minor:-0}" + need_major="${need_major:-0}" + need_minor="${need_minor:-0}" + if [ "$have_major" -gt "$need_major" ]; then + return 0 + fi + if [ "$have_major" -lt "$need_major" ]; then + return 1 + fi + if [ "$have_minor" -ge "$need_minor" ]; then + return 0 + fi + return 1 +} + +# Mirrors install-external-deps.sh::probe_ena_xdp_zc. +probe_ena_xdp_zc() { + if [ ! -e /sys/module/ena/version ]; then + printf '[!] ena driver not loaded on running kernel — cannot confirm AF_XDP zerocopy support. Reboot into the backport kernel and re-run this probe.\n' >&2 + return 1 + fi + local ena_ver + ena_ver="$(cat /sys/module/ena/version 2>/dev/null || true)" + printf '[*] ena driver version on running kernel: %s\n' "${ena_ver:-unknown}" + if command -v dmesg >/dev/null 2>&1 \ + && dmesg 2>/dev/null | grep -qiE 'ena_xdp_zc|ena.*xdp.*zerocopy|ena.*xdp_zc'; then + printf '[*] ena_xdp_zc indicator detected in dmesg — AF_XDP zerocopy should be available.\n' + return 0 + fi + printf '[!] ena_xdp_zc indicator NOT found in dmesg on running kernel %s. AF_XDP zerocopy may not be available; the scanner will fall back to drv+copy mode. Reboot into kernel %s+ and re-run if you just installed the backport image.\n' \ + "$(uname -r 2>/dev/null || echo unknown)" "$ANYSCAN_KERNEL_BACKPORT_MIN_VERSION" >&2 + return 1 +} + +# Mirrors install-external-deps.sh::install_kernel_backport_if_requested. +# deploy.sh runs as root (the script enforces this above), so the +# function takes a slightly simpler path than the install-external-deps.sh +# variant — no sudo branch is needed here. +install_kernel_backport_if_requested() { + if [ "${ANYSCAN_INSTALL_KERNEL_BACKPORT:-0}" != "1" ]; then + return 0 + fi + local current_kernel current_kernel_ver + current_kernel="$(uname -r 2>/dev/null || echo unknown)" + current_kernel_ver="${current_kernel%%-*}" + printf '[*] ANYSCAN_INSTALL_KERNEL_BACKPORT=1 — current kernel %s (need >= %s for ena_xdp_zc).\n' \ + "$current_kernel" "$ANYSCAN_KERNEL_BACKPORT_MIN_VERSION" + if kernel_version_at_least "$current_kernel_ver" "$ANYSCAN_KERNEL_BACKPORT_MIN_VERSION"; then + printf '[*] Running kernel already meets %s+; backport image install skipped.\n' \ + "$ANYSCAN_KERNEL_BACKPORT_MIN_VERSION" + probe_ena_xdp_zc || true + return 0 + fi + if ! command -v apt-get >/dev/null 2>&1; then + printf '[*] Skipping kernel backport: apt-get not on PATH (this knob targets Debian-family hosts).\n' + return 0 + fi + if [ ! -f "$ANYSCAN_KERNEL_BACKPORT_SOURCES_LIST" ]; then + printf '[*] Writing apt source for %s to %s...\n' \ + "$ANYSCAN_KERNEL_BACKPORT_SUITE" "$ANYSCAN_KERNEL_BACKPORT_SOURCES_LIST" + if ! printf 'deb %s %s main\n' \ + "$ANYSCAN_KERNEL_BACKPORT_MIRROR" \ + "$ANYSCAN_KERNEL_BACKPORT_SUITE" \ + >"$ANYSCAN_KERNEL_BACKPORT_SOURCES_LIST"; then + printf '[!] Failed to write %s; cannot install backport kernel image. Skipping.\n' \ + "$ANYSCAN_KERNEL_BACKPORT_SOURCES_LIST" >&2 + return 0 + fi + else + printf '[*] Reusing existing apt source list at %s.\n' "$ANYSCAN_KERNEL_BACKPORT_SOURCES_LIST" + fi + printf '[*] Refreshing apt indexes for %s...\n' "$ANYSCAN_KERNEL_BACKPORT_SUITE" + if ! apt-get update >/dev/null 2>&1; then + printf '[!] apt-get update failed; cannot install backport kernel image. Skipping.\n' >&2 + return 0 + fi + printf '[*] Installing %s from %s (no auto-reboot)...\n' \ + "$ANYSCAN_KERNEL_BACKPORT_PACKAGE" "$ANYSCAN_KERNEL_BACKPORT_SUITE" + if ! apt-get install -y --no-install-recommends \ + -t "$ANYSCAN_KERNEL_BACKPORT_SUITE" \ + "$ANYSCAN_KERNEL_BACKPORT_PACKAGE" >/dev/null 2>&1; then + printf '[!] Failed to install %s from %s; existing kernel unchanged.\n' \ + "$ANYSCAN_KERNEL_BACKPORT_PACKAGE" "$ANYSCAN_KERNEL_BACKPORT_SUITE" >&2 + return 0 + fi + printf '[*] REBOOT REQUIRED: backport kernel image %s staged on disk. This script does NOT auto-reboot — schedule a maintenance window and reboot to activate kernel %s+.\n' \ + "$ANYSCAN_KERNEL_BACKPORT_PACKAGE" "$ANYSCAN_KERNEL_BACKPORT_MIN_VERSION" + probe_ena_xdp_zc || true + return 0 +} + install_vulnscanner_binary() { local source_bin="" local make_args=() @@ -167,6 +286,8 @@ if [ "$EUID" -ne 0 ]; then exit 1 fi +install_kernel_backport_if_requested + if ! command -v cargo >/dev/null 2>&1; then printf '[!] cargo was not found in PATH. Install the Rust toolchain before deploying.\n' >&2 exit 1 diff --git a/install-external-deps.sh b/install-external-deps.sh index cd47d38..3fadb84 100755 --- a/install-external-deps.sh +++ b/install-external-deps.sh @@ -26,6 +26,28 @@ VULNSCANNER_INSTALLED_BIN="/opt/anyscan/bin/scanner" # to. See plans/2026-04-27-portscan-afxdp-plan-v1.md §3.6. ANYSCAN_USE_AF_XDP="${ANYSCAN_USE_AF_XDP:-0}" +# Opt-in kernel backport upgrade. Default 0 leaves the running kernel +# untouched (existing AMIs unchanged). Setting 1 installs the Debian +# bookworm-backports kernel image so the host can run kernel 6.16+ +# with the in-flight `ena_xdp_zc` ENA driver patches that AF_XDP +# zerocopy on ENA needs. The PR 65 §10 / anygpt-42 live bench showed +# ENA on kernel 6.12.74 caps the c6in.metal 8-NIC cap=4 throughput +# at ~22M pps in drv+copy mode, vs the 30-50M projection driver-mode +# zerocopy was supposed to deliver. See PR 65 issuecomment-4336192354 +# for the constraint trace. +# +# Never auto-reboots. The new kernel is staged on disk and the operator +# has to schedule the reboot themselves. After install the script +# probes `/sys/module/ena/version` + dmesg for `ena_xdp_zc` support +# and warns if absent so the operator knows whether the +# CURRENTLY-RUNNING kernel will deliver zerocopy. +ANYSCAN_INSTALL_KERNEL_BACKPORT="${ANYSCAN_INSTALL_KERNEL_BACKPORT:-0}" +ANYSCAN_KERNEL_BACKPORT_MIN_VERSION="${ANYSCAN_KERNEL_BACKPORT_MIN_VERSION:-6.16}" +ANYSCAN_KERNEL_BACKPORT_PACKAGE="${ANYSCAN_KERNEL_BACKPORT_PACKAGE:-linux-image-cloud-amd64}" +ANYSCAN_KERNEL_BACKPORT_SUITE="${ANYSCAN_KERNEL_BACKPORT_SUITE:-bookworm-backports}" +ANYSCAN_KERNEL_BACKPORT_SOURCES_LIST="${ANYSCAN_KERNEL_BACKPORT_SOURCES_LIST:-/etc/apt/sources.list.d/anyscan-bookworm-backports.list}" +ANYSCAN_KERNEL_BACKPORT_MIRROR="${ANYSCAN_KERNEL_BACKPORT_MIRROR:-http://deb.debian.org/debian}" + # True when the existing scanner binary was linked against libxdp at build # time. The AF_XDP build path (USE_AF_XDP=1 in the engine Makefile) adds # `-lxdp -lbpf -lelf -lz` so libxdp.so shows up as a dynamic dependency; @@ -57,6 +79,146 @@ vulnscanner_make_args() { fi } +# Lexicographic numeric compare of two `.` version strings. +# Returns 0 (true) when $1 >= $2. Tolerant of missing fields and +# non-numeric trailing tokens (`6.16.0-rc1` / `6.16+deb13` etc). +kernel_version_at_least() { + local have="$1" need="$2" + local have_major have_minor need_major need_minor + have_major="${have%%.*}" + have_minor="${have#*.}" + have_minor="${have_minor%%.*}" + have_minor="${have_minor%%[!0-9]*}" + need_major="${need%%.*}" + need_minor="${need#*.}" + need_minor="${need_minor%%.*}" + need_minor="${need_minor%%[!0-9]*}" + have_major="${have_major:-0}" + have_minor="${have_minor:-0}" + need_major="${need_major:-0}" + need_minor="${need_minor:-0}" + if [ "$have_major" -gt "$need_major" ]; then + return 0 + fi + if [ "$have_major" -lt "$need_major" ]; then + return 1 + fi + if [ "$have_minor" -ge "$need_minor" ]; then + return 0 + fi + return 1 +} + +# Probe the ena driver for AF_XDP zerocopy capability. ena_xdp_zc is +# the upstream patch series (in-flight against kernel 6.16+) that lets +# ENA advertise XDP_ZC; without it the scanner's AF_XDP path falls +# back to drv+copy and caps c6in.metal 8-NIC cap=4 throughput at +# ~22M pps (anygpt-42 live bench). Best-effort: a kernel with no +# `/sys/module/ena/version` (ena module not loaded) or no +# ena_xdp_zc indicator in dmesg just emits a warning so the operator +# knows zerocopy is unavailable on the CURRENTLY-RUNNING kernel +# (most useful right after a reboot into the backport kernel). +probe_ena_xdp_zc() { + if [ ! -e /sys/module/ena/version ]; then + printf '[!] ena driver not loaded on running kernel — cannot confirm AF_XDP zerocopy support. Reboot into the backport kernel and re-run this probe.\n' >&2 + return 1 + fi + local ena_ver + ena_ver="$(cat /sys/module/ena/version 2>/dev/null || true)" + printf '[*] ena driver version on running kernel: %s\n' "${ena_ver:-unknown}" + if command -v dmesg >/dev/null 2>&1 \ + && dmesg 2>/dev/null | grep -qiE 'ena_xdp_zc|ena.*xdp.*zerocopy|ena.*xdp_zc'; then + printf '[*] ena_xdp_zc indicator detected in dmesg — AF_XDP zerocopy should be available.\n' + return 0 + fi + printf '[!] ena_xdp_zc indicator NOT found in dmesg on running kernel %s. AF_XDP zerocopy may not be available; the scanner will fall back to drv+copy mode. Reboot into kernel %s+ and re-run if you just installed the backport image.\n' \ + "$(uname -r 2>/dev/null || echo unknown)" "$ANYSCAN_KERNEL_BACKPORT_MIN_VERSION" >&2 + return 1 +} + +# Opt-in path that installs a backport kernel image (default +# linux-image-cloud-amd64 from Debian bookworm-backports) on hosts +# whose stock kernel is older than 6.16. Default OFF — existing +# AMIs are unchanged. Never auto-reboots: installing a kernel image +# only stages it on disk, the operator has to schedule the reboot +# themselves. After the install (or if the kernel is already new +# enough) probes /sys/module/ena/version + dmesg for ena_xdp_zc +# support and warns if absent. +install_kernel_backport_if_requested() { + if [ "${ANYSCAN_INSTALL_KERNEL_BACKPORT:-0}" != "1" ]; then + return 0 + fi + local current_kernel current_kernel_ver + current_kernel="$(uname -r 2>/dev/null || echo unknown)" + current_kernel_ver="${current_kernel%%-*}" + printf '[*] ANYSCAN_INSTALL_KERNEL_BACKPORT=1 — current kernel %s (need >= %s for ena_xdp_zc).\n' \ + "$current_kernel" "$ANYSCAN_KERNEL_BACKPORT_MIN_VERSION" + if kernel_version_at_least "$current_kernel_ver" "$ANYSCAN_KERNEL_BACKPORT_MIN_VERSION"; then + printf '[*] Running kernel already meets %s+; backport image install skipped.\n' \ + "$ANYSCAN_KERNEL_BACKPORT_MIN_VERSION" + probe_ena_xdp_zc || true + return 0 + fi + if ! command -v apt-get >/dev/null 2>&1; then + printf '[*] Skipping kernel backport: apt-get not on PATH (this knob targets Debian-family hosts).\n' + return 0 + fi + local apt_cmd=() tee_cmd=() + if [ "$(id -u 2>/dev/null || echo 1)" = "0" ]; then + apt_cmd=(apt-get) + tee_cmd=(tee) + elif command -v sudo >/dev/null 2>&1; then + if ! sudo -n true >/dev/null 2>&1; then + printf '[*] Skipping kernel backport: sudo would prompt for a password.\n' + printf ' Install manually if you want kernel %s+:\n' "$ANYSCAN_KERNEL_BACKPORT_MIN_VERSION" + printf ' echo "deb %s %s main" | sudo tee %s\n' \ + "$ANYSCAN_KERNEL_BACKPORT_MIRROR" \ + "$ANYSCAN_KERNEL_BACKPORT_SUITE" \ + "$ANYSCAN_KERNEL_BACKPORT_SOURCES_LIST" + printf ' sudo apt-get update && sudo apt-get install -y -t %s %s\n' \ + "$ANYSCAN_KERNEL_BACKPORT_SUITE" "$ANYSCAN_KERNEL_BACKPORT_PACKAGE" + return 0 + fi + apt_cmd=(sudo -n apt-get) + tee_cmd=(sudo -n tee) + else + printf '[*] Skipping kernel backport: not root and sudo is not available.\n' + return 0 + fi + if [ ! -f "$ANYSCAN_KERNEL_BACKPORT_SOURCES_LIST" ]; then + printf '[*] Writing apt source for %s to %s...\n' \ + "$ANYSCAN_KERNEL_BACKPORT_SUITE" "$ANYSCAN_KERNEL_BACKPORT_SOURCES_LIST" + if ! printf 'deb %s %s main\n' \ + "$ANYSCAN_KERNEL_BACKPORT_MIRROR" \ + "$ANYSCAN_KERNEL_BACKPORT_SUITE" \ + | "${tee_cmd[@]}" "$ANYSCAN_KERNEL_BACKPORT_SOURCES_LIST" >/dev/null; then + printf '[!] Failed to write %s; cannot install backport kernel image. Skipping.\n' \ + "$ANYSCAN_KERNEL_BACKPORT_SOURCES_LIST" >&2 + return 0 + fi + else + printf '[*] Reusing existing apt source list at %s.\n' "$ANYSCAN_KERNEL_BACKPORT_SOURCES_LIST" + fi + printf '[*] Refreshing apt indexes for %s...\n' "$ANYSCAN_KERNEL_BACKPORT_SUITE" + if ! "${apt_cmd[@]}" update >/dev/null 2>&1; then + printf '[!] apt-get update failed; cannot install backport kernel image. Skipping.\n' >&2 + return 0 + fi + printf '[*] Installing %s from %s (no auto-reboot)...\n' \ + "$ANYSCAN_KERNEL_BACKPORT_PACKAGE" "$ANYSCAN_KERNEL_BACKPORT_SUITE" + if ! "${apt_cmd[@]}" install -y --no-install-recommends \ + -t "$ANYSCAN_KERNEL_BACKPORT_SUITE" \ + "$ANYSCAN_KERNEL_BACKPORT_PACKAGE" >/dev/null 2>&1; then + printf '[!] Failed to install %s from %s; existing kernel unchanged.\n' \ + "$ANYSCAN_KERNEL_BACKPORT_PACKAGE" "$ANYSCAN_KERNEL_BACKPORT_SUITE" >&2 + return 0 + fi + printf '[*] REBOOT REQUIRED: backport kernel image %s staged on disk. This script does NOT auto-reboot — schedule a maintenance window and reboot to activate kernel %s+.\n' \ + "$ANYSCAN_KERNEL_BACKPORT_PACKAGE" "$ANYSCAN_KERNEL_BACKPORT_MIN_VERSION" + probe_ena_xdp_zc || true + return 0 +} + EXTENSION_MANIFESTS="$SCRIPT_DIR/local-bootstrap-provisioner.json,$SCRIPT_DIR/vulnscanner-zmap-adapter.json" ANYGPT_API_ENV_FILE_DEFAULT="$REPO_ROOT/apps/api/.env" @@ -144,6 +306,7 @@ if ! command -v git >/dev/null 2>&1; then fi install_afxdp_build_deps +install_kernel_backport_if_requested if [ -d "$VULNSCANNER_REPO_DIR/.git" ]; then printf '[*] Updating external repository in %s...\n' "$VULNSCANNER_REPO_DIR" diff --git a/package-worker-bundle.sh b/package-worker-bundle.sh index 15aaecf..75687e2 100755 --- a/package-worker-bundle.sh +++ b/package-worker-bundle.sh @@ -38,6 +38,17 @@ ANYSCAN_USE_AF_XDP="${ANYSCAN_USE_AF_XDP:-0}" ANYSCAN_VULNSCANNER_REPO_DIR_DEFAULT="$SCRIPT_DIR/../../anyscan-engine-c" ANYSCAN_VULNSCANNER_REPO_DIR="${ANYSCAN_VULNSCANNER_REPO_DIR:-$ANYSCAN_VULNSCANNER_REPO_DIR_DEFAULT}" +# Opt-in kernel backport flag (mirrors install-external-deps.sh / +# deploy.sh). Bundles do not contain a kernel image — the actual +# install happens at deploy time via install-external-deps.sh or +# deploy.sh on the target host. The bundle README records the flag +# so a downstream operator knows the producer's intent (e.g. "this +# bundle was built expecting kernel 6.16+ on the target"). Default +# 0; existing AMIs unchanged. See PR 65 issuecomment-4336192354 for +# the ENA / ena_xdp_zc constraint trace and anygpt-44 for this +# wire-up. +ANYSCAN_INSTALL_KERNEL_BACKPORT="${ANYSCAN_INSTALL_KERNEL_BACKPORT:-0}" + print_banner() { printf '═══════════════════════════════════════════════════════════\n' printf ' Remote Agent Packager \n' @@ -744,6 +755,7 @@ Bundle control route: Bundle scanner build: scanner_included: ${include_scanner} use_af_xdp: ${ANYSCAN_USE_AF_XDP} + install_kernel_backport: ${ANYSCAN_INSTALL_KERNEL_BACKPORT} EOF bundle_path="$DIST_DIR/${bundle_name}.tar.gz" diff --git a/runtime.worker.env.template b/runtime.worker.env.template index 476664e..c969e81 100644 --- a/runtime.worker.env.template +++ b/runtime.worker.env.template @@ -162,6 +162,35 @@ POLL_INTERVAL_SECONDS=15 # plans/2026-04-27-portscan-afxdp-plan-v1.md §3.6. # ANYSCAN_USE_AF_XDP=0 +# Opt-in kernel backport upgrade (read by install-external-deps.sh, +# package-worker-bundle.sh, deploy.sh — NOT by the agentd runtime). +# Set 1 to install the Debian bookworm-backports kernel image +# (linux-image-cloud-amd64 by default) on a Debian-family host whose +# stock kernel is older than 6.16. Kernel 6.16+ carries the in-flight +# `ena_xdp_zc` ENA driver patches that AF_XDP zerocopy on ENA needs; +# without them the scanner's AF_XDP path falls back to drv+copy and +# caps c6in.metal 8-NIC cap=4 throughput at ~22M pps (anygpt-42 live +# bench, vs the 30-50M projection in +# plans/2026-04-27-portscan-afxdp-plan-v1.md §10). +# +# Default 0 — existing AMIs are unchanged. The scripts NEVER +# auto-reboot; the kernel image is staged on disk and the operator +# schedules the reboot. After install the scripts probe +# /sys/module/ena/version + dmesg for `ena_xdp_zc` support and warn +# if absent so the operator knows whether the CURRENTLY-RUNNING +# kernel will deliver zerocopy. Out of scope here: AMI rebuild, +# auto-reboot, the ena driver patches themselves. +# +# Override the package / suite / source list / mirror with the +# matching ANYSCAN_KERNEL_BACKPORT_* variables if you carry a +# different backport channel (e.g. an internal Debian mirror). +# ANYSCAN_INSTALL_KERNEL_BACKPORT=0 +# ANYSCAN_KERNEL_BACKPORT_MIN_VERSION=6.16 +# ANYSCAN_KERNEL_BACKPORT_PACKAGE=linux-image-cloud-amd64 +# ANYSCAN_KERNEL_BACKPORT_SUITE=bookworm-backports +# ANYSCAN_KERNEL_BACKPORT_SOURCES_LIST=/etc/apt/sources.list.d/anyscan-bookworm-backports.list +# ANYSCAN_KERNEL_BACKPORT_MIRROR=http://deb.debian.org/debian + # Installed bundle asset locations EXTENSION_MANIFEST_PATHS=/opt/agentd/extensions/bootstrap-provisioner.json,/opt/agentd/extensions/portscan-adapter.json ARTIFACT_DIR=/var/lib/agentd/artifacts diff --git a/tools/test-install-external-deps-kernel-backport.sh b/tools/test-install-external-deps-kernel-backport.sh new file mode 100755 index 0000000..5a649a0 --- /dev/null +++ b/tools/test-install-external-deps-kernel-backport.sh @@ -0,0 +1,360 @@ +#!/usr/bin/env bash +# Unit test for the ANYSCAN_INSTALL_KERNEL_BACKPORT wire-up in +# install-external-deps.sh (anygpt-44). +# +# Asserts the four paths the operator can hit: +# +# 1. ANYSCAN_INSTALL_KERNEL_BACKPORT unset (default 0) → no apt-get +# install fires for the backport package; no apt source list is +# written. +# 2. ANYSCAN_INSTALL_KERNEL_BACKPORT=1 + running kernel already 6.16+ → +# no apt-get install fires; "already meets" message printed; +# ena_xdp_zc probe still runs. +# 3. ANYSCAN_INSTALL_KERNEL_BACKPORT=1 + running kernel < 6.16 + +# apt-get on PATH → apt-get update + apt-get install -t +# bookworm-backports linux-image-cloud-amd64 fire; the apt source +# list is written; reboot-required message printed. +# 4. ANYSCAN_INSTALL_KERNEL_BACKPORT=1 + running kernel < 6.16 + +# apt-get NOT on PATH → graceful skip with note, no apt-get +# invocations recorded. +# +# Implementation notes: +# - Stubs `uname`, `apt-get`, `id`, `tee`, `make`, `git`, `ldd`, +# `readelf` on PATH and records every invocation to per-case logs. +# `make` writes a synthetic scanner binary so the rest of +# install-external-deps.sh succeeds end-to-end. +# - Disables the AF_XDP apt-deps block (ANYSCAN_INSTALL_AFXDP_DEPS=false) +# so this test only exercises the kernel-backport path. +# - Redirects the apt sources list path to a per-case temp file so we +# never touch /etc/apt on the test host. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TARGET_SCRIPT="${SCRIPT_DIR}/../install-external-deps.sh" + +if [ ! -x "$TARGET_SCRIPT" ]; then + printf '[!] %s is not executable\n' "$TARGET_SCRIPT" >&2 + exit 1 +fi + +PASS=0 +FAIL=0 + +note_pass() { + PASS=$(( PASS + 1 )) + printf ' [ok] %s\n' "$1" +} + +note_fail() { + FAIL=$(( FAIL + 1 )) + printf ' [FAIL] %s: %s\n' "$1" "$2" >&2 +} + +assert_contains_substring() { + local label="$1" needle="$2" file="$3" + if [ -f "$file" ] && grep -Fq "$needle" "$file"; then + note_pass "$label" + else + note_fail "$label" "expected substring $(printf '%q' "$needle") in $file" + if [ -f "$file" ]; then + sed 's/^/ /' "$file" >&2 + fi + fi +} + +assert_not_contains_substring() { + local label="$1" needle="$2" file="$3" + if [ -f "$file" ] && grep -Fq "$needle" "$file"; then + note_fail "$label" "did not expect substring $(printf '%q' "$needle") in $file" + sed 's/^/ /' "$file" >&2 + else + note_pass "$label" + fi +} + +assert_file_missing() { + local label="$1" file="$2" + if [ -e "$file" ]; then + note_fail "$label" "did not expect $file to exist" + else + note_pass "$label" + fi +} + +assert_file_present() { + local label="$1" file="$2" + if [ -e "$file" ]; then + note_pass "$label" + else + note_fail "$label" "expected $file to exist" + fi +} + +WORK_ROOT="$(mktemp -d)" +KEEP_WORK_ROOT="${KERNEL_BACKPORT_TEST_KEEP_TMP:-0}" +cleanup_work_root() { + if [ "$KEEP_WORK_ROOT" = "1" ]; then + printf '[*] WORK_ROOT preserved at %s\n' "$WORK_ROOT" >&2 + else + rm -rf "$WORK_ROOT" + fi +} +trap cleanup_work_root EXIT + +prepare_stubs() { + local stub_dir="$1" + local apt_log="$2" + local make_log="$3" + local git_log="$4" + local uname_release="$5" # what `uname -r` should print + local apt_present="$6" # "yes" → apt-get stub installed; "no" → omitted + + mkdir -p "$stub_dir" + + # Symlink the OS-side essentials install-external-deps.sh needs into + # stub_dir so the test can run with PATH=$stub_dir only — that lets + # us drop apt-get from PATH for case 4 without also losing bash / + # mktemp / install / python3 / etc. + local cmd resolved + for cmd in bash env mktemp install sed cat dirname basename \ + python3 printf chmod cp rm mkdir touch ln awk grep sort \ + head tail tr find readlink openssl true false; do + if resolved="$(command -v "$cmd" 2>/dev/null)"; then + ln -sf "$resolved" "$stub_dir/$cmd" + fi + done + + cat >"$stub_dir/uname" <"$stub_dir/id" <<'EOF' +#!/usr/bin/env bash +if [ "${1:-}" = "-u" ]; then + printf '0\n' +else + printf 'uid=0(root) gid=0(root) groups=0(root)\n' +fi +EOF + chmod +x "$stub_dir/id" + + if [ "$apt_present" = "yes" ]; then + cat >"$stub_dir/apt-get" <>"$apt_log" +exit 0 +EOF + chmod +x "$stub_dir/apt-get" + fi + + cat >"$stub_dir/tee" <<'EOF' +#!/usr/bin/env bash +target="${1:-/dev/null}" +cat >"$target" +EOF + chmod +x "$stub_dir/tee" + + cat >"$stub_dir/make" <>"$make_log" +target_dir="" +i=0 +while [ \$i -lt \$# ]; do + i=\$(( i + 1 )) + arg="\${!i}" + if [ "\$arg" = "-C" ]; then + i=\$(( i + 1 )) + target_dir="\${!i}" + fi +done +if [ -n "\$target_dir" ]; then + mkdir -p "\$target_dir" + printf '#!/bin/sh\necho stub-scanner\n' >"\$target_dir/scanner" + chmod +x "\$target_dir/scanner" +fi +exit 0 +EOF + chmod +x "$stub_dir/make" + + cat >"$stub_dir/git" <>"$git_log" +exit 0 +EOF + chmod +x "$stub_dir/git" + + # Pretend ldd/readelf exist but say nothing about libxdp; the + # AF_XDP path is gated off in this test (ANYSCAN_USE_AF_XDP unset). + cat >"$stub_dir/ldd" <<'EOF' +#!/usr/bin/env bash +exit 0 +EOF + chmod +x "$stub_dir/ldd" + + cat >"$stub_dir/readelf" <<'EOF' +#!/usr/bin/env bash +exit 0 +EOF + chmod +x "$stub_dir/readelf" +} + +run_install_script() { + local case_dir="$1" + local install_kernel_backport="$2" + local uname_release="$3" + local apt_present="$4" + + local repo_dir="$case_dir/engine" + local runtime_env="$case_dir/runtime.env" + local artifact_dir="$case_dir/artifacts" + local apt_log="$case_dir/apt-get.log" + local make_log="$case_dir/make.log" + local git_log="$case_dir/git.log" + local stub_dir="$case_dir/stubs" + local sources_list="$case_dir/anyscan-bookworm-backports.list" + + mkdir -p "$repo_dir" "$artifact_dir" + : >"$apt_log" + : >"$make_log" + : >"$git_log" + + printf 'all:\n\t@true\n' >"$repo_dir/Makefile" + mkdir -p "$repo_dir/.git" + + prepare_stubs "$stub_dir" "$apt_log" "$make_log" "$git_log" \ + "$uname_release" "$apt_present" + + ( + # PATH = stub_dir only. prepare_stubs symlinks the essentials + # install-external-deps.sh needs (bash, mktemp, install, sed, + # python3, ...) into stub_dir, so we get a curated PATH that + # contains exactly what we want. Whether apt-get is present is + # controlled by prepare_stubs based on `apt_present` — when "no" + # the apt-get stub is simply not written and `command -v + # apt-get` returns false for the install script. + export PATH="$stub_dir" + export ANYSCAN_INSTALL_KERNEL_BACKPORT="$install_kernel_backport" + export ANYSCAN_KERNEL_BACKPORT_SOURCES_LIST="$sources_list" + export ANYSCAN_VULNSCANNER_REPO_DIR="$repo_dir" + export ANYSCAN_INSTALL_AFXDP_DEPS=false + export ANYSCAN_USE_AF_XDP=0 + export ANYSCAN_RUNTIME_ENV_FILE="$runtime_env" + export ANYSCAN_LOCAL_BOOTSTRAP_ARTIFACT_DIR="$artifact_dir" + unset SUDO_USER + "$TARGET_SCRIPT" >"$case_dir/stdout.log" 2>"$case_dir/stderr.log" + ) +} + +# --------------------------------------------------------------------------- +# Case 1: knob unset → no apt-get install for backport, no source list. +# --------------------------------------------------------------------------- +case_dir="$WORK_ROOT/case-default-off" +mkdir -p "$case_dir" +if run_install_script "$case_dir" "0" "6.12.74" "yes"; then + note_pass "default (knob=0) build runs successfully" + assert_not_contains_substring \ + "default (knob=0): no apt-get install for backport package" \ + "linux-image-cloud-amd64" \ + "$case_dir/apt-get.log" + assert_not_contains_substring \ + "default (knob=0): no apt-get update for backports suite" \ + "bookworm-backports" \ + "$case_dir/apt-get.log" + assert_file_missing \ + "default (knob=0): apt source list NOT created" \ + "$case_dir/anyscan-bookworm-backports.list" +else + note_fail "default (knob=0) build" "install-external-deps.sh exited non-zero (see $case_dir/stderr.log)" +fi + +# --------------------------------------------------------------------------- +# Case 2: knob=1 + kernel 6.16 already → skip install but probe ena_xdp_zc. +# --------------------------------------------------------------------------- +case_dir="$WORK_ROOT/case-already-new-enough" +mkdir -p "$case_dir" +if run_install_script "$case_dir" "1" "6.16.0-cloud-amd64" "yes"; then + note_pass "knob=1 + kernel 6.16 build runs successfully" + assert_not_contains_substring \ + "knob=1 + kernel 6.16: no apt-get install for backport" \ + "linux-image-cloud-amd64" \ + "$case_dir/apt-get.log" + assert_contains_substring \ + "knob=1 + kernel 6.16: 'already meets' message printed" \ + "already meets" \ + "$case_dir/stdout.log" + assert_file_missing \ + "knob=1 + kernel 6.16: apt source list NOT created" \ + "$case_dir/anyscan-bookworm-backports.list" +else + note_fail "knob=1 + kernel 6.16 build" "install-external-deps.sh exited non-zero (see $case_dir/stderr.log)" +fi + +# --------------------------------------------------------------------------- +# Case 3: knob=1 + kernel 6.12 + apt-get available → install fires. +# --------------------------------------------------------------------------- +case_dir="$WORK_ROOT/case-install-backport" +mkdir -p "$case_dir" +if run_install_script "$case_dir" "1" "6.12.74-cloud-amd64" "yes"; then + note_pass "knob=1 + kernel 6.12 build runs successfully" + assert_contains_substring \ + "knob=1 + kernel 6.12: apt-get update fires" \ + "update" \ + "$case_dir/apt-get.log" + assert_contains_substring \ + "knob=1 + kernel 6.12: apt-get install -t bookworm-backports fires" \ + "install -y --no-install-recommends -t bookworm-backports linux-image-cloud-amd64" \ + "$case_dir/apt-get.log" + assert_contains_substring \ + "knob=1 + kernel 6.12: REBOOT REQUIRED notice printed" \ + "REBOOT REQUIRED" \ + "$case_dir/stdout.log" + assert_file_present \ + "knob=1 + kernel 6.12: apt source list created" \ + "$case_dir/anyscan-bookworm-backports.list" + assert_contains_substring \ + "knob=1 + kernel 6.12: apt source list points at bookworm-backports" \ + "bookworm-backports" \ + "$case_dir/anyscan-bookworm-backports.list" +else + note_fail "knob=1 + kernel 6.12 build" "install-external-deps.sh exited non-zero (see $case_dir/stderr.log)" +fi + +# --------------------------------------------------------------------------- +# Case 4: knob=1 + kernel 6.12 + apt-get NOT on PATH → graceful skip. +# --------------------------------------------------------------------------- +case_dir="$WORK_ROOT/case-no-apt" +mkdir -p "$case_dir" +if run_install_script "$case_dir" "1" "6.12.74-cloud-amd64" "no"; then + note_pass "knob=1 + no apt-get build runs successfully" + if [ -s "$case_dir/apt-get.log" ]; then + note_fail "knob=1 + no apt-get: no apt-get invocations recorded" \ + "expected empty apt-get.log but got: $(tr '\n' '|' <"$case_dir/apt-get.log")" + else + note_pass "knob=1 + no apt-get: no apt-get invocations recorded" + fi + assert_contains_substring \ + "knob=1 + no apt-get: 'apt-get not on PATH' note printed" \ + "apt-get not on PATH" \ + "$case_dir/stdout.log" + assert_file_missing \ + "knob=1 + no apt-get: apt source list NOT created" \ + "$case_dir/anyscan-bookworm-backports.list" +else + note_fail "knob=1 + no apt-get build" "install-external-deps.sh exited non-zero (see $case_dir/stderr.log)" +fi + +printf '\n' +printf 'PASS: %d\n' "$PASS" +printf 'FAIL: %d\n' "$FAIL" + +if [ "$FAIL" -gt 0 ]; then + exit 1 +fi