2
0
mirror of https://github.com/xcat2/xcat-dep.git synced 2026-10-15 08:12:03 +00:00

fix(xcat-dep): the CI arm64 grub modules come from an unverified archive

The workflow step that puts the arm64-efi GRUB modules on the runner read a
directory listing over plain HTTP, took the last name in lexical order, and
unpacked the whole deb into / as root. Nothing authenticated the artifact, and
the name chosen was neither tied to the runner's suite nor to the installed
grub-mkimage.

install-grub-arm64-modules.sh now reads grub-efi-arm64-bin through apt in a
private state tree: apt verifies the InRelease signature of the suite that
/etc/os-release names, and the deb hash against that signed index. The host dpkg
architecture and the host sources are not changed. The script stops when the
deb and the host grub-mkimage report different GRUB upstream versions, unpacks
under TMPDIR, and copies only the files of usr/lib/grub/arm64-efi. The
documentation and the prebuilt images under monolithic/ stay out.

t/install-grub-arm64-modules.bats drives the script with apt-get, dpkg-deb and
the image builder as shell functions. Four removals were each caught by the
assertion that names them: the version gate, the file-only copy, the apt
authentication options, and the missing-module-directory check.

Measured in an ubuntu:24.04 container with the workflow's own steps:
t/aarch64_target.t reports 4 skips before the step and 0 after it, with all 38
checks passing, and t/aarch64_cross_build.t passes 27 with 0 skips.

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
This commit is contained in:
Daniel Hilst
2026-10-08 12:21:58 -03:00
parent 7b450d7c08
commit 380fccd625
3 changed files with 322 additions and 17 deletions
+6 -17
View File
@@ -24,25 +24,13 @@ jobs:
apt-utils bats createrepo-c dpkg-dev gnupg \
libfile-slurper-perl \
libparallel-forkmanager-perl libperl-critic-perl rpm rpm2cpio cpio \
grub-common curl ca-certificates
grub-common ca-certificates
- name: Install the arm64-efi grub modules
# t/aarch64_target.t runs the spec's own grub2-mkimage command, and it skips when the
# arm64-efi modules are absent -- which is a skip exactly where the suite runs, so it
# measures nothing. grub-efi-arm64-bin is Architecture: arm64 and cannot be installed on
# the amd64 runner, but the modules inside it are target code, not host code, so
# extracting the deb puts them in place without enabling a foreign architecture.
run: |
set -euo pipefail
base=http://ports.ubuntu.com/ubuntu-ports/pool/main/g/grub2-unsigned
deb=$(curl -sS "$base/" | grep -oE 'grub-efi-arm64-bin_[^"]*_arm64\.deb' | sort -u | tail -1)
[ -n "$deb" ] || { echo "no grub-efi-arm64-bin deb found under $base" >&2; exit 1; }
curl -sSO "$base/$deb"
sudo dpkg -x "$deb" /
# The modules are what the test needs; say so on the branch that earned it.
n=$(ls /usr/lib/grub/arm64-efi 2>/dev/null | wc -l)
[ "$n" -gt 0 ] || { echo "the deb extracted no arm64-efi modules" >&2; exit 1; }
echo "installed $n arm64-efi grub modules from $deb"
# t/aarch64_target.t runs the spec's own grub2-mkimage command, and it skips without
# the arm64-efi modules -- a skip exactly where the suite runs. The script reads them
# from the runner's own suite through apt's signed metadata.
run: sudo ./install-grub-arm64-modules.sh
- name: Run static checks
run: |
@@ -104,6 +92,7 @@ jobs:
prove -v -Ilib t/openeuler_native_trust_dbpath.t
prove -v -Ilib t/openeuler_native_cpio_scan.t
bats t/verify-xcatsn-install.bats
bats t/install-grub-arm64-modules.bats
# Ubuntu's AppArmor policy blocks these unprivileged test namespaces.
- name: Run native packaging tests
+154
View File
@@ -0,0 +1,154 @@
#!/bin/bash
# Install the arm64-efi GRUB modules, so a host grub-mkimage can build the AArch64
# network loader of grub2-xcat.spec. Ubuntu ships them only in grub-efi-arm64-bin,
# which is Architecture: arm64 and does not install on an amd64 host. The deb is read
# through apt's signed metadata, unpacked in a temporary directory, and only the
# module files are copied out.
#
# grub-mkimage embeds kernel.img and resolves moddep.lst from the module directory,
# so the modules must come from the GRUB version of the host tool.
usage() {
cat <<'EOF'
Usage: install-grub-arm64-modules.sh [-d DESTDIR] [-s SUITE] [-m MIRROR]
-d DESTDIR where the modules go (default /usr/lib/grub/arm64-efi)
-s SUITE suite to read (default: VERSION_CODENAME of /etc/os-release)
-m MIRROR apt mirror (default https://ports.ubuntu.com/ubuntu-ports)
apt verifies the suite InRelease signature with the host keyring, and the deb hash
against that signed index. The deb is unpacked under ${TMPDIR:-/tmp} and removed on
exit. Nothing else from it is installed.
EOF
}
PACKAGE=grub-efi-arm64-bin
MODULE_PATH=usr/lib/grub/arm64-efi
MIRROR_DEFAULT=https://ports.ubuntu.com/ubuntu-ports
# Prints the GRUB upstream version of the host image builder, e.g. 2.12.
# EL names the tool grub2-mkimage and Debian names it grub-mkimage.
host_grub_version() {
local tool out
for tool in grub2-mkimage grub-mkimage; do
command -v "$tool" >/dev/null 2>&1 || continue
out=$("$tool" --version 2>/dev/null) || continue
# "grub-mkimage (GRUB) 2.12-1ubuntu7.3"
out=${out##* }
echo "${out%%-*}"
return 0
done
echo "no grub2-mkimage and no grub-mkimage on PATH" >&2
return 1
}
# The upstream version of a deb version: no epoch, no distribution revision.
upstream_version() {
local v=${1#*:}
echo "${v%%-*}"
}
# apt-get against a private state tree, so the host dpkg architecture and the host
# sources stay as they are. An unsigned or stale index fails here.
apt_get() {
local root=$1
shift
apt-get \
-o Dir::State="$root/state" \
-o Dir::State::status="$root/status" \
-o Dir::Cache="$root/cache" \
-o Dir::Etc::SourceList="$root/sources.list" \
-o Dir::Etc::SourceParts="$root/none" \
-o Dir::Etc::Preferences="$root/none/preferences" \
-o Dir::Etc::PreferencesParts="$root/none" \
-o APT::Architecture=arm64 \
-o APT::Architectures=arm64 \
-o APT::Sandbox::User=root \
-o Acquire::AllowInsecureRepositories=false \
-o APT::Get::AllowUnauthenticated=false \
"$@"
}
main() {
local dest=/usr/lib/grub/arm64-efi suite='' mirror=$MIRROR_DEFAULT opt
OPTIND=1
while getopts 'd:s:m:h' opt; do
case $opt in
d) dest=$OPTARG ;;
s) suite=$OPTARG ;;
m) mirror=$OPTARG ;;
h) usage; return 0 ;;
*) usage >&2; return 2 ;;
esac
done
shift $((OPTIND - 1))
if (( $# != 0 )); then
usage >&2
return 2
fi
if [[ -z $suite ]]; then
suite=$(. /etc/os-release 2>/dev/null && echo "$VERSION_CODENAME")
fi
if [[ -z $suite ]]; then
echo "no suite: /etc/os-release names no VERSION_CODENAME, give -s" >&2
return 2
fi
local want
want=$(host_grub_version) || return 1
local root
root=$(mktemp -d "${TMPDIR:-/tmp}/grub-arm64.XXXXXX") || return 1
# shellcheck disable=SC2064
trap "rm -rf '$root'" EXIT
mkdir -p "$root/state/lists/partial" "$root/cache/archives/partial" "$root/none" "$root/deb" || return 1
: > "$root/status"
printf 'deb [arch=arm64] %s %s main\n' "$mirror" "$suite" > "$root/sources.list" || return 1
if ! apt_get "$root" update; then
echo "FAIL: apt-get update of $mirror $suite failed" >&2
return 1
fi
if ! (cd "$root/deb" && apt_get "$root" download "$PACKAGE"); then
echo "FAIL: apt-get download $PACKAGE from $suite failed" >&2
return 1
fi
local deb
deb=$(find "$root/deb" -maxdepth 1 -name "${PACKAGE}_*_arm64.deb" | sort | head -1)
if [[ -z $deb ]]; then
echo "FAIL: apt downloaded no $PACKAGE deb" >&2
return 1
fi
local got
got=$(dpkg-deb -f "$deb" Version) || return 1
got=$(upstream_version "$got")
if [[ $got != "$want" ]]; then
echo "FAIL: $PACKAGE is GRUB $got and the host image builder is GRUB $want" >&2
return 1
fi
dpkg-deb -x "$deb" "$root/x" || return 1
if [[ ! -d $root/x/$MODULE_PATH ]]; then
echo "FAIL: $PACKAGE carries no $MODULE_PATH" >&2
return 1
fi
# The modules, moddep.lst and kernel.img, and none of the documentation or the
# prebuilt images under monolithic/ that the deb also carries.
install -d "$dest" || return 1
find "$root/x/$MODULE_PATH" -maxdepth 1 -type f -exec install -m 0644 -t "$dest" {} + || return 1
local count
count=$(find "$dest" -maxdepth 1 -name '*.mod' | wc -l)
if (( count == 0 )) || [[ ! -f $dest/kernel.img || ! -f $dest/moddep.lst ]]; then
echo "FAIL: $dest has no modules, no kernel.img or no moddep.lst" >&2
return 1
fi
echo "PASS: $count arm64-efi modules of GRUB $got in $dest, from ${deb##*/}"
}
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
main "$@"
fi
+162
View File
@@ -0,0 +1,162 @@
#!/usr/bin/env bats
# Unit tests for install-grub-arm64-modules.sh. apt-get, dpkg-deb and the grub
# image builder are shell functions here, so no archive is read and the modules
# land only under BATS_TEST_TMPDIR.
setup() {
source "$BATS_TEST_DIRNAME/../install-grub-arm64-modules.sh"
export TMPDIR=$BATS_TEST_TMPDIR
CALLS=$BATS_TEST_TMPDIR/calls
DEST=$BATS_TEST_TMPDIR/dest
DEB_VERSION=2.12-1ubuntu7
TOOL_VERSION=2.12-1ubuntu7.3
DEB_HAS_MODULES=1
}
# Both names, because the loop takes whichever the host has and a developer host
# may carry the real EL one.
grub2-mkimage() {
echo "grub2-mkimage (GRUB) $TOOL_VERSION"
}
grub-mkimage() {
echo "grub-mkimage (GRUB) $TOOL_VERSION"
}
apt-get() {
echo "apt-get $*" >>"$CALLS"
local list
list=$(printf '%s\n' "$@" | sed -nE 's/^Dir::Etc::SourceList=//p')
[ -z "$list" ] || cp "$list" "$BATS_TEST_TMPDIR/sources.list.seen"
case " $* " in
*" download "*)
# apt-get download writes the deb into the working directory.
: >"grub-efi-arm64-bin_${DEB_VERSION}_arm64.deb"
;;
esac
}
# A deb tree with the module directory, the documentation and the prebuilt images
# the real grub-efi-arm64-bin carries.
dpkg-deb() {
echo "dpkg-deb $*" >>"$CALLS"
case $1 in
-f) echo "$DEB_VERSION" ;;
-x)
local out=$3 mod=$3/usr/lib/grub/arm64-efi
if [[ $out != "$BATS_TEST_TMPDIR"/* ]]; then
echo "dpkg-deb -x outside the temporary tree: $out" >&2
return 1
fi
mkdir -p "$out/usr/share/doc/grub-efi-arm64-bin"
: >"$out/usr/share/doc/grub-efi-arm64-bin/copyright"
(( DEB_HAS_MODULES )) || return 0
mkdir -p "$mod/monolithic"
local m
for m in normal linux tftp efinet; do : >"$mod/$m.mod"; done
: >"$mod/kernel.img"
: >"$mod/moddep.lst"
: >"$mod/monolithic/grubnetaa64.efi"
;;
esac
}
leftover_dirs() {
find "$BATS_TEST_TMPDIR" -maxdepth 1 -name 'grub-arm64.*' | wc -l
}
@test "an operand is a usage error" {
run main noble
[ "$status" -eq 2 ]
[[ $output == Usage:* ]]
}
@test "an unknown option is a usage error" {
run main -z
[ "$status" -eq 2 ]
[[ $output == *Usage:* ]]
}
@test "the upstream version of a deb version drops the epoch and the revision" {
[ "$(upstream_version 1:2.12-1ubuntu7.3)" = 2.12 ]
[ "$(upstream_version 2.06-104)" = 2.06 ]
}
@test "the host GRUB version comes from the image builder on PATH" {
[ "$(host_grub_version)" = 2.12 ]
}
@test "no image builder on PATH stops the run before any download" {
host_grub_version() { echo "no grub2-mkimage and no grub-mkimage on PATH" >&2; return 1; }
run main -d "$DEST" -s noble
[ "$status" -eq 1 ]
[ ! -e "$CALLS" ]
}
@test "apt reads the given suite and mirror, with authentication left on" {
run main -d "$DEST" -s jammy -m https://mirror.example/ubuntu-ports
[ "$status" -eq 0 ]
grep -q -- '-o APT::Get::AllowUnauthenticated=false' "$CALLS"
grep -q -- '-o Acquire::AllowInsecureRepositories=false' "$CALLS"
grep -q -- '-o APT::Architectures=arm64' "$CALLS"
grep -q 'apt-get .* update' "$CALLS"
grep -q 'download grub-efi-arm64-bin' "$CALLS"
# The suite and the mirror reach apt through its own sources.list, and the host
# sources and dpkg architectures are never read or changed.
local list
list=$(sed -nE 's/.*-o Dir::Etc::SourceList=([^ ]+).*/\1/p' "$CALLS" | head -1)
[ "$(cat "$BATS_TEST_TMPDIR/sources.list.seen")" = "deb [arch=arm64] https://mirror.example/ubuntu-ports jammy main" ]
[[ $list == "$BATS_TEST_TMPDIR"/grub-arm64.*/sources.list ]]
}
@test "only the module files are installed, and the temporary tree is removed" {
run main -d "$DEST" -s noble
[ "$status" -eq 0 ]
[[ $output == "PASS: 4 arm64-efi modules of GRUB 2.12 in $DEST, from grub-efi-arm64-bin_2.12-1ubuntu7_arm64.deb" ]]
[ -f "$DEST/normal.mod" ]
[ -f "$DEST/kernel.img" ]
[ -f "$DEST/moddep.lst" ]
# Not the documentation, and not the prebuilt images of monolithic/.
[ ! -e "$DEST/usr" ]
[ ! -e "$DEST/monolithic" ]
[ ! -e "$DEST/copyright" ]
[ "$(find "$DEST" -mindepth 1 | wc -l)" -eq 6 ]
[ "$(leftover_dirs)" -eq 0 ]
}
@test "a deb of another GRUB version than the host tool installs nothing" {
DEB_VERSION=2.06-1ubuntu1
run main -d "$DEST" -s noble
[ "$status" -eq 1 ]
[[ $output == *"grub-efi-arm64-bin is GRUB 2.06 and the host image builder is GRUB 2.12"* ]]
[ ! -e "$DEST" ]
[ "$(leftover_dirs)" -eq 0 ]
}
@test "a deb without the module directory installs nothing" {
DEB_HAS_MODULES=0
run main -d "$DEST" -s noble
[ "$status" -eq 1 ]
[[ $output == *"carries no usr/lib/grub/arm64-efi"* ]]
[ ! -e "$DEST" ]
}
@test "a failed apt-get update stops the run before the download" {
apt-get() {
echo "apt-get $*" >>"$CALLS"
case " $* " in *" update "*) return 100 ;; esac
}
run main -d "$DEST" -s noble
[ "$status" -eq 1 ]
[[ $output == *"apt-get update of https://ports.ubuntu.com/ubuntu-ports noble failed"* ]]
! grep -q 'download' "$CALLS"
[ ! -e "$DEST" ]
}
@test "a download that yields no deb is not reported as a success" {
apt-get() { echo "apt-get $*" >>"$CALLS"; }
run main -d "$DEST" -s noble
[ "$status" -eq 1 ]
[[ $output == *"apt downloaded no grub-efi-arm64-bin deb"* ]]
[ ! -e "$DEST" ]
}