Skip to content

Probe UEFI compatibility without assuming a file object - #720

Open
zardus wants to merge 3 commits into
masterfrom
feature/fix-cle-uefi-fileno
Open

Probe UEFI compatibility without assuming a file object#720
zardus wants to merge 3 commits into
masterfrom
feature/fix-cle-uefi-fileno

Conversation

@zardus

@zardus zardus commented Aug 9, 2026

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Loading a BSD-flavored static archive fails with AttributeError: 'ArchiveFileData' object has no attribute 'fileno'. Detection offers every stream to every default backend, and the loader promises a backend only read and seek, but UefiFirmware._to_bytes calls fileno() and catches only the io.UnsupportedOperation an io.IOBase would raise.

The probe has to answer for such a stream, so it now reads it instead, taking the mmap shortcut only when the stream spans the file behind the descriptor. It also returns bytes rather than a memoryview for a BytesIO — firmware unpacked by the CARTFile backend arrives that way, and uefi_firmware joins a compressed section's preamble to its body before decompressing it.

The regression test loads a real firmware volume and a real BSD archive, both fixtures added by angr/binaries#176, so the checks here stay red until that merges. Such an archive still does not load afterwards, because its symbol index is handed to the loader as if it were an object file; and #716 registers STM32Backend, whose is_compatible calls tell() and will need the same treatment.

Validation: #720 (comment)

@zardus

zardus commented Aug 9, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head 200cf5d9f6bbc111e9116ccf272a3e39855dd8ba against baseline b58ea02a446106647cdaae32bdf91b7062404cc1. Commands run from the cle checkout root on CPython 3.12.13, with uefi-firmware 1.16 and arpy 1.1.1, and with a binaries checkout on the branch of angr/binaries#176. The Test jobs on this PR stay red until that merges, because CI checks out binaries master.

  • Focused: pytest tests/test_uefi_firmware.py — 27 passed
  • Regression: the same command with only cle/backends/uefi_firmware.py reverted to the baseline — 7 failed, 20 passed
  • Baseline failures, all from the fileno() call in _to_bytes unless noted: AttributeError: 'ArchiveFileData' object has no attribute 'fileno' from is_compatible on an archive member and again from cle.Loader on the archive itself; AttributeError: 'MinimalStream' object has no attribute 'fileno' for a bare read/seek stream, both from is_compatible and from loading the volume that way; TypeError: unsupported operand type(s) for +: 'memoryview' and 'memoryview' at uefi_firmware/uefi.py:644 when the volume is given as a BytesIO; ValueError: cannot mmap an empty file from mmap.mmap for the empty file; and, for a stream over one archive member, the whole container returned rather than the member's bytes
  • Pre-commit: all configured hooks, --all-files — passed, no file rewritten
  • Merge-base comparison, the one the hosted Lint and Typecheck jobs make over changed files: pylint cle/backends/uefi_firmware.py 10.00 -> 10.00, tests/test_uefi_firmware.py 10.00 as a new file; pyright badness cle/backends/uefi_firmware.py 0.050 -> 0.048, tests/test_uefi_firmware.py 0.0 -> 0.0
  • is_compatible was also offered 401 inputs derived from the volume — truncations at one percent steps, 200 single-bit flips of its first 128 KiB, and random and zero buffers — and returned without raising for every one
  • At the previous head 2369d83, which carried this same production change under a test that assembled its own inputs: full suite pytest tests/ — 229 passed, 9 skipped; pylint cle/backends/uefi_firmware.py 10.00 -> 10.00; pyright badness 0.050 -> 0.048

Head 200cf5d repairs the Typecheck job, which head e030880 failed for a reason unrelated to the missing fixtures: the test read user_interface_name straight off child_objects, which is typed list[Backend], while that attribute lives on UefiModuleMixin. Every child a volume builds is a UefiPE or a UefiTE, so the test now asserts that and reads the name through the narrowed type, which also makes it say what a child of a firmware volume is.

Inputs, both added by angr/binaries#176: tests/aarch64/edk2_armvirtqemu.fd, Debian's edk2 build of the ArmVirtQemu platform, a single firmware volume whose 93 AArch64 PE drivers sit behind an LZMA-compressed section, so reaching them by name is what says the volume was parsed rather than only recognized; and tests/aarch64/bsd_symdef_archive.a, a BSD-flavored static archive whose first member is a __.SYMDEF SORTED symbol index and whose second is a Mach-O object, which gives _to_bytes a stream sharing a descriptor with a larger file. The failure was originally found in a corpus sweep, on real static archives produced by a BSD or Apple ar.

Caveats: the two assertions on _to_bytes call the private helper directly, because no public path can distinguish a stream that shares a descriptor with a larger file; everything else goes through cle.Loader or is_compatible. The mmap shortcut and the fallback are both covered on every platform CI runs, since the same volume is loaded as a path, a file object, a BytesIO and a bare stream.

@zardus
zardus force-pushed the feature/fix-cle-uefi-fileno branch from 01afdc9 to ee7855f Compare August 9, 2026 20:03
@angr-bot

angr-bot commented Aug 9, 2026

Copy link
Copy Markdown
Member

Corpus decompilation diffs can be found at angr/dec-snapshots@master...angr/cle_720

The loader promises a backend only that a stream supports read and seek, but
UefiFirmware._to_bytes called fileno() on it and caught only the
io.UnsupportedOperation an io.IOBase would raise. Backend detection offers
every stream to every default backend, so a stream that is not a file object
aborted detection with an AttributeError instead of the probe returning False.
The stream arpy hands out for a static archive member is one: a BSD-flavored
archive puts its __.SYMDEF symbol index in the member list, no backend claims
it, and the load of the whole archive failed there.

Catch that too, and take the mmap shortcut only when the stream spans the file
behind the descriptor. mmap maps that whole file from offset 0, which is not
what an archive member sharing its container's descriptor holds. An empty file
skips the shortcut as well, where mmap used to raise ValueError.

Return bytes rather than a memoryview for a BytesIO, which is what the CARTFile
backend re-enters the loader with. uefi_firmware joins a compressed section's
preamble to its body before decompressing it, and a memoryview cannot be joined
to anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
zardus and others added 2 commits August 10, 2026 20:57
The test assembled its own firmware volume and BSD-flavored archive with
struct.pack, which pins the loader to a shape no toolchain emits and passes
while the real formats still fail. Load Debian's edk2 build of ArmVirtQemu and
a BSD static archive whose first member is a __.SYMDEF symbol index, both from
angr/binaries.

The volume holds its 93 AArch64 PE drivers behind an LZMA-compressed section,
so asserting that the loader reports them as child objects says the volume was
parsed rather than only recognized, which is what _to_bytes returning a
concatenable object buys. The archive's symbol index is the member arpy hands
out that has no fileno, and its second member gives _to_bytes a stream that
shares a descriptor with a larger file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The name a firmware volume gives a driver lives on UefiModuleMixin, not on
Backend, so reading it straight off child_objects fails the Typecheck job.
Every child a volume produces is built as a UefiPE or a UefiTE, so assert
that and read the name through the narrowed type.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants