From bc26fff7c25ee043d9b575b4010d2d5bbbf8fe93 Mon Sep 17 00:00:00 2001 From: Jacob Carlborg Date: Thu, 30 Jul 2026 13:01:29 +0200 Subject: [PATCH] Stop bundling the Linaro UEFI firmware for ARM64 The Linaro release server that `linaro_uefi.fd` was downloaded from has been retired. It now redirects to a landing page that answers with HTTP 200 and an HTML body, so 1.1.0 shipped a 49 KB HTML page where a 2 MiB firmware belonged and broke every OpenBSD ARM64 job. Rather than replace the download, drop the firmware. OpenBSD ARM64 was its only consumer and it now boots on the shared edk2 `uefi.fd` with the machine type set to `virt,acpi=off`, so there is nothing left to bundle. `download_file` now refuses to write anything but the requested payload: it raises on a non-success HTTP status and on an HTML body, detected both by `Content-Type` and by sniffing the leading bytes. The riscv64 U-Boot download still uses it. `test.rb` gained a `FirmwareValidator` that reads every bundled firmware out of the tarball and rejects HTML documents and implausibly small files, so a corrupt bundle fails the build instead of shipping. Co-Authored-By: Claude Opus 5 (1M context) --- changelog.md | 15 +++++++ ci.rb | 41 +++++++++++-------- test.rb | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 151 insertions(+), 18 deletions(-) diff --git a/changelog.md b/changelog.md index ca8ea39..af80d9f 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Removed + +* Stop bundling the Linaro UEFI firmware for ARM64. `linaro_uefi.fd` is no + longer part of the `qemu-system-aarch64` tarball. Its only consumer, OpenBSD + ARM64, now boots on the shared edk2 `uefi.fd` with ACPI disabled + ([cross-platform-actions/action#160](https://github.com/cross-platform-actions/action/pull/160)). + The Linaro release server it was downloaded from has been retired and + redirects to a landing page, so 1.1.0 shipped an HTML page in its place + +### Fixed + +* Fail the build when a download returns an unsuccessful HTTP status or an HTML + document instead of the requested file +* Verify the contents of the bundled firmware files, not only their presence + ## [1.1.0] - 2026-07-26 ### Added diff --git a/ci.rb b/ci.rb index 504d07d..4680e7e 100755 --- a/ci.rb +++ b/ci.rb @@ -120,8 +120,6 @@ def bundle_uefi File.open(uefi_target_path, File::RDWR) do |file| file.truncate(file.read.bytes.rindex { _1 != 0 }) end - - bundle_linaro_uefi end private @@ -141,19 +139,6 @@ def unpack_uefi FileUtils.rm_f uefi_source_path unpack_bzip2 archive end - - def bundle_linaro_uefi - download_file(linaro_uefi_url, linaro_uefi_target_path) - end - - def linaro_uefi_url - "https://releases.linaro.org/components/kernel/uefi-linaro/latest/release/qemu64/QEMU_EFI.fd" - end - - def linaro_uefi_target_path - @linaro_uefi_target_path ||= - File.join(firmware_target_directory, "linaro_uefi.fd") - end end class Riscv64 < Architecture @@ -583,12 +568,34 @@ def execute(*args, env: {}) Kernel.system env, *args, exception: true end +HTML_CONTENT_TYPES = %w[text/html application/xhtml+xml].freeze + +HTML_SIGNATURE = /\A\s*<(?:!doctype\s+html|html[\s>])/i + +# Downloads a file, refusing to write anything but the requested payload. A +# retired host that redirects to a landing page answers with a successful +# status and an HTML body, which would otherwise silently be bundled as if it +# were the expected file. def download_file(url, destination) - URI.open(url) do |uri| - File.open(destination, 'w') { _1.write(uri.read) } + URI.open(url) do |io| + content = io.read + validate_download(url, io, content) + File.binwrite(destination, content) end end +def validate_download(url, io, content) + raise "Failed to download #{url}: HTTP #{Array(io.status).join(' ')}" unless successful_response?(io) + raise "Failed to download #{url}: expected a file, got an HTML document" if html?(io, content) +end + +def successful_response?(io) = Array(io.status).first.to_s.start_with?("2") + +def html?(io, content) + HTML_CONTENT_TYPES.include?(io.content_type) || + HTML_SIGNATURE.match?(content.byteslice(0, 1024).b) +end + def unpack_bzip2(archive) execute "bzip2", "-d", archive end diff --git a/test.rb b/test.rb index 7654564..d3c74e3 100755 --- a/test.rb +++ b/test.rb @@ -22,6 +22,11 @@ def assert_qemu_system(architecture, firmwares:) assert validator.valid?, validator.message end +def assert_usable_firmwares(architecture) + validator = FirmwareValidator.new(architecture) + assert validator.valid?, validator.message +end + def assert_only_system_dependencies(architecture) return unless QemuSystemValidator.host_os == "macos" @@ -62,6 +67,10 @@ def assert_statically_linked(architecture) ] end + it "contains usable firmware files for x86_64" do + assert_usable_firmwares "x86_64" + end + it "is only linked with system dependencies" do assert_only_system_dependencies "x86_64" end @@ -77,10 +86,13 @@ def assert_statically_linked(architecture) efi-e1000.rom efi-virtio.rom uefi.fd - linaro_uefi.fd ] end + it "contains usable firmware files for arm64" do + assert_usable_firmwares "aarch64" + end + it "is only linked with system dependencies" do assert_only_system_dependencies "aarch64" end @@ -99,6 +111,10 @@ def assert_statically_linked(architecture) ] end + it "contains usable firmware files for riscv64" do + assert_usable_firmwares "riscv64" + end + it "is only linked with system dependencies" do assert_only_system_dependencies "riscv64" end @@ -200,6 +216,24 @@ def firmware_paths @firmware_paths ||= paths.filter { _1.start_with?(firmware_directory) } end + # The contents have to be read while the archive is being iterated, an + # entry cannot be read after the reader has moved past it. + def firmware_files + @firmware_files ||= File.open(filename) do |io| + firmware_files = [] + + Gem::Package::TarReader.new(io) do |tar| + tar.each do |entry| + next unless firmware?(entry) + + firmware_files << Firmware.new(basename(entry), entry.read.to_s) + end + end + + firmware_files + end + end + def qemu_binary @qemu_binary ||= paths.filter { _1.start_with?("bin/qemu") } end @@ -211,6 +245,16 @@ def firmwares def firmware_directory "share/qemu/" end + + private + + def firmware?(entry) + entry.file? && full_name(entry).start_with?(firmware_directory) + end + + def basename(entry) = full_name(entry).delete_prefix(firmware_directory) + + def full_name(entry) = entry.full_name.delete_prefix("./") end class MessageFormatter @@ -266,3 +310,70 @@ def to_full_path(array) end end end + +# Verifies that the bundled firmware files actually contain firmware. A failed +# download can produce an HTML error or landing page, which is otherwise +# indistinguishable from a correct bundle since the file is still present. +class FirmwareValidator + def initialize(architecture) + @architecture = architecture + end + + def valid? = unusable_firmwares.empty? + + def message + ["Unusable firmware files in '#{tar_file.filename}':"] + .concat(unusable_firmwares.map(&:message)) + .join("\n") + end + + private + + attr_reader :architecture + + def unusable_firmwares + @unusable_firmwares ||= tar_file.firmware_files.reject(&:usable?) + end + + def tar_file + @tar_file ||= QemuSystemValidator::TarFile.for( + architecture: architecture, + host_os: QemuSystemValidator.host_os + ) + end +end + +# A single firmware file extracted from a bundle. +class Firmware + # The smallest firmware bundled, kvmvapic.bin, is a few kilobytes. + MINIMUM_SIZE = 4096 + + private_constant :MINIMUM_SIZE + + HTML_SIGNATURE = /\A\s*<(?:!doctype\s+html|html[\s>])/i + + private_constant :HTML_SIGNATURE + + attr_reader :name + + def initialize(name, content) + @name = name + @content = content + end + + def usable? = !html? && large_enough? + + def message + return "'#{name}' is an HTML document, not firmware" if html? + + "'#{name}' is #{content.bytesize} bytes, expected at least #{MINIMUM_SIZE}" + end + + private + + attr_reader :content + + def html? = HTML_SIGNATURE.match?(content.byteslice(0, 1024).b) + + def large_enough? = content.bytesize >= MINIMUM_SIZE +end