Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 24 additions & 17 deletions ci.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
113 changes: 112 additions & 1 deletion test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Loading