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:
@@ -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
|
||||
|
||||
Executable
+154
@@ -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
|
||||
@@ -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" ]
|
||||
}
|
||||
Reference in New Issue
Block a user