Package: edk2
Version: 2026.05-2
Severity: normal
Tags: patch

Hi,

Disclosure: this report was drafted with AI assistance, then reviewed against
every source it cites (upstream edk2, Debian's packaging, OpenBSD's 
loader, and
the shipped firmware image). I understand it and can defend any part of it. The
patch below has been checked with `patch -p1 --dry-run` against debian/rules as
of 2026.05-2. Happy to make it shorter if you would rather have the gist.

`qemu-efi-riscv64` cannot boot OpenBSD/riscv64, and the escape hatch Debian
already ships for x86 and arm64 is not reachable on riscv64. The fix is one
line in debian/rules.

Summary
-------

The RISC-V platform applies the DXE NX memory protection policy, which makes
`EfiLoaderData` non-executable. OpenBSD's loader allocates the kernel 
buffer as
`EfiLoaderData` and jumps to it, so the kernel faults on its first instruction.

`debian/rules` already defines the escape hatch,

    NO_STRICTNX_COMMON_FLAGS = --pcd PcdUninstallMemAttrProtocol=TRUE

applies it to x86 and arm64, and ships a *strict* counterpart for each so the
relaxed build is the default and strict NX is opt-in:

    OVMF_4M_SECBOOT_FLAGS           = $(OVMF_4M_COMMON_FLAGS) 
$(NO_STRICTNX_COMMON_FLAGS) -D...
    OVMF_4M_SECBOOT_STRICTNX_FLAGS  = $(OVMF_4M_COMMON_FLAGS)                   
   -D...
    AAVMF_SECBOOT_FLAGS             = $(AAVMF_COMMON_FLAGS)   
$(NO_STRICTNX_COMMON_FLAGS) -D...
    AAVMF_SECBOOT_STRICTNX_FLAGS    = $(AAVMF_COMMON_FLAGS)                     
   -D...

The two lines in each pair differ only by `$(NO_STRICTNX_COMMON_FLAGS)`, and
`install-qemu-efi-aarch64` builds and installs all three of its variants. RISC-V
has a single build with no opt-out and no variant:

    RISCV64_FLAGS     = $(COMMON_FLAGS)
    LOONGARCH64_FLAGS = $(COMMON_FLAGS)

So both other architectures already default to a relaxed image; riscv64 has no
such image at all. This is a request for parity, not a claim that the NX policy
or the value `0x7FD5` is wrong.

Patch
-----

diff -u against debian/rules as of 2026.05-2; applies with `patch -p1`:

    --- a/debian/rules
    +++ b/debian/rules
    @@ -60,7 +60,11 @@
     AAVMF_SECBOOT_FLAGS = $(AAVMF_COMMON_FLAGS) $(NO_STRICTNX_COMMON_FLAGS) 
-DBUILD_SHELL=FALSE -DSECURE_BOOT_ENABLE=TRUE
     AAVMF_SECBOOT_STRICTNX_FLAGS = $(AAVMF_COMMON_FLAGS) -DBUILD_SHELL=FALSE 
-DSECURE_BOOT_ENABLE=TRUE
     
    -RISCV64_FLAGS = $(COMMON_FLAGS)
    +# Unlike OVMF and AAVMF there is only one RISC-V image, so there is no
    +# no-secboot variant to relax selectively.  Make EfiLoaderData executable,
    +# which is the value upstream documents for loaders that copy a kernel into
    +# memory they allocated and then execute it.
    +RISCV64_FLAGS = $(COMMON_FLAGS) --pcd 
PcdDxeNxMemoryProtectionPolicy=0xC000000000007FD1
     LOONGARCH64_FLAGS = $(COMMON_FLAGS)
     
     # Clear variables used internally by the edk2 build system

`0x...FD1` differs from the current `0x...FD5` only in bit 2 (`EfiLoaderData`:
NX -> executable). `EfiLoaderCode` stays executable, so the sanity assertions
in `CoreInitializeMemoryProtection()` are still satisfied. The value is not my
invention -- it is the one upstream documents for exactly this situation, in
`ArmVirtPkg/ArmVirt.dsc.inc:437-439`:

    # By passing --pcd PcdDxeNxMemoryProtectionPolicy=0xC000000000007FD1 on the
    # build command line you can allow code execution in EfiLoaderData. This is
    # required when using some outdated GRUB versions.

Note this is *narrower* than what the other two architectures carry:
`PcdUninstallMemAttrProtocol` turns NX enforcement off wholesale, whereas this
relaxes one memory type.

If you would rather preserve the hardened image, the aarch64 pair is the model
to copy: `install-qemu-efi-aarch64-secboot-strictnx` ships
`AAVMF_CODE.secboot.strictnx.fd` inside the same binary package, alongside the
relaxed default. I have deliberately not written that part of the patch, because
it would also need a matching entry in
`debian/descriptors/60-edk2-riscv64.json` and I would rather not guess at that
file. The one-line change above is the smallest fix for the reported bug.

Why the existing workaround does not transfer
---------------------------------------------

This is the part I would most like a second opinion on, because it is why I am
not simply asking for the x86 patch to be copied onto riscv64.

`PcdUninstallMemAttrProtocol` removes `EFI_MEMORY_ATTRIBUTE_PROTOCOL`. On
riscv64 there is nothing to remove: `UefiCpuPkg/CpuDxeRiscV64/CpuDxe.c`
installs only the CPU arch protocol, and the string `MemoryAttributeProtocol`
occurs neither in `CpuDxe.c` nor in `CpuDxeRiscV64.inf`.

    Status = gBS->InstallMultipleProtocolInterfaces (
                    &mCpuHandle,
                    &gEfiCpuArchProtocolGuid,
                    &gCpu,
                    NULL);      /* UefiCpuPkg/CpuDxeRiscV64/CpuDxe.c:374 */

Both other architectures do publish it, which is what gives the uninstall
something to act on:

    UefiCpuPkg/CpuDxe/CpuDxe.c:1034       InstallEfiMemoryAttributeProtocol 
(mCpuHandle);
    ArmPkg/Drivers/CpuDxe/CpuDxe.c:432    &gEfiMemoryAttributeProtocolGuid,

And on riscv64 the type-level policy does not go through that protocol anyway.
It is applied by `InitializeDxeNxMemoryProtectionPolicy()` in
`MdeModulePkg/Core/Dxe/Misc/MemoryProtection.c`, gated only on the PCD, driving
`gCpu->SetMemoryAttributes()`:

    MemoryProtection.c:907   if (PcdGet64 (PcdDxeNxMemoryProtectionPolicy) != 
0) {
    MemoryProtection.c:908     InitializeDxeNxMemoryProtectionPolicy ();
    MemoryProtection.c:861   gCpu->SetMemoryAttributes (gCpu, 
Entry->BaseAddress, ..., Attributes);

`gMemoryAttributeProtocol` appears in that file once, on the image-protection
path (`LocateProtocol`, line 1056). So unloading it cannot lift the type-level
policy. That is also true on arm64, which sets the same `0x7FD5` -- which is why
the fix above targets the policy rather than the protocol.

Consistent with all of this, `strings` on the shipped image finds only two
`opt/org.tianocore/*` keys and no `UninstallMemAttrProtocol`, so passing
`-fw_cfg opt/org.tianocore/UninstallMemAttrProtocol,string=y` cannot have any
effect either way:

    $ strings RISCV_VIRT_CODE.fd | grep 'opt/org'
    opt/org.tianocore/IPv4PXESupport
    opt/org.tianocore/IPv6PXESupport

(Against the decompressed image; the string lives in the compressed DXE volume
in the shipped file.)

The loader's side
-----------------

This is not inference from the policy value. OpenBSD's riscv64 EFI loader
allocates exclusively with `EfiLoaderData`:

    sys/arch/riscv64/stand/efiboot/efiboot.c:229   AllocateAnyPages, 
EfiLoaderData   /* heap */
    sys/arch/riscv64/stand/efiboot/efiboot.c:525   AllocateAnyPages, 
EfiLoaderData   /* FDT */
    sys/arch/riscv64/stand/efiboot/efiboot.c:970   AllocateAddress, 
EfiLoaderData    /* kernel */

`EfiLoaderCode` occurs 0 times in the whole efiboot directory, and so does
`EfiBootServicesData`. The kernel buffer is the 64 MB block reserved at boot:

    efiboot.c:638-644
        /* ... * on a 2MB boundary.  We allocate a block of 64MB of memory ... 
*/
        if (efi_memprobe_find(EFI_SIZE_TO_PAGES(64 * 1024 * 1024),
            0x200000, &addr) != EFI_SUCCESS)
                printf("Can't allocate memory\n");
        efi_loadaddr = addr;

That 2 MB alignment matches the fault: `0x84200000` is 2 MB aligned, and the PTE
that lost its X bit (below) is a 2 MiB leaf.

Observed
--------

The loader runs and reads the kernel off the disk -- `booting sd0a:/bsd`
completes -- then the first instruction of the kernel faults:

    !!!! RISCV64 Exception Type - 
000000000000000C(EXCEPT_RISCV_INST_ACCESS_PAGE_FAULT) !!!!
       sepc = 0x00000000084200000    sstatus = 0x08000000200006100
      stval = 0x00000000084200000
    Recursive exception occurred while dumping the CPU state

Reproducer
----------

    qemu-system-riscv64 \
      -M virt,acpi=off -m 512 -nographic \
      -drive 
if=pflash,format=raw,unit=0,file=/usr/share/qemu-efi-riscv64/RISCV_VIRT_CODE.fd,readonly=on
 \
      -drive if=pflash,format=raw,unit=1,file=VARScopy.fd \
      -drive file=openbsd-riscv64.img,format=raw,if=none,id=hd0 \
      -device virtio-blk-device,drive=hd0

Any ESP containing OpenBSD's `/EFI/BOOT/BOOTRISCV64.EFI` reproduces it. The
image can be built from `install78.img` or `miniroot78.img` from
`ftp.openbsd.org:/pub/OpenBSD/7.8/riscv64/`.

Diagnosis
---------

The mapping is correct. The leaf PTE for `0x84200000` is a valid 2 MiB leaf
mapping the address to itself, with every bit set except execute:

    FAULT cause=12 vaddr=0x84200000 satp=0x900000000009f862 mode=9 (Sv48)
      L3: vpn=0   pte=0x27e18421
      L2: vpn=2   pte=0x27e15c21
      L1: vpn=33  pte=0x210800e7      <- leaf: V R W G A D, no X

`mstatus.MXR` is 0, so the fault is architecturally correct: an instruction
access requires X, and MXR only affects loads.

Sampling that leaf across the whole boot shows it is built executable and
stripped a third of a million instructions later:

    @76857792   0x210800ef  R W X     <- as built, matches BaseRiscVMmuLib
    @77230336   0x210800e7  R W -     <- X cleared

`BaseRiscVMmuLib` maps `EfiGcdMemoryTypeSystemMemory` as `RISCV_PG_R | W | X`,
so the removal comes from the DXE policy, not the initial map. The value in the
shipped image is `0x7FD5` -- every type except the three Code types -- and it
comes from the platform, not from an override, so it is upstream&#39;s choice 
rather
than something Debian introduced:

    OvmfPkg/RiscVVirt/RiscVVirt.dsc.inc:310
      
gEfiMdeModulePkgTokenSpaceGuid.PcdDxeNxMemoryProtectionPolicy|0xC000000000007FD5

A related inconsistency in that file
------------------------------------

While confirming the source of the value, the comment directly above it says
something the value does not do:

    OvmfPkg/RiscVVirt/RiscVVirt.dsc.inc:305-310
      # Enable NX memory protection for all non-code regions, including OEM and 
OS
      # reserved ones, with the exception of LoaderData regions, of which OS 
loaders
      # (i.e., GRUB) may assume that its contents are executable.
      
gEfiMdeModulePkgTokenSpaceGuid.PcdDxeNxMemoryProtectionPolicy|0xC000000000007FD5

Bit 2 is set in `0x7FD5`, so `EfiLoaderData` is NX -- the comment promises the
opposite. `ArmVirt.dsc.inc` carries the same value but correctly attributes the
LoaderData exception to the build-time override instead of claiming it is the
default. Either the comment or the value is wrong; as written the file
contradicts itself. I cannot tell from the history which was intended, and I am
not asking you to change it -- only noting it, since it is what led me to the
value above.

Verified on two independent implementations
-------------------------------------------

To rule out an emulator bug, the same firmware was run on QEMU and on an
independent RISC-V emulator, with identical results both ways:

    firmware          QEMU 11.0.92            independent emulator
    -------------     --------------------    ----------------------------
    as shipped        fault at 0x84200000     identical fault: same sepc and
                                              stval, same register dump,
                                              same recursive-handler failure
    paging disabled   boots to shell prompt   boots to the same point

The second row also confirms the cause: with paging off, `satp` stays zero for
the entire boot, the hand-off is a physical jump that consults no page tables,
and the NX policy never applies.

A note on upstream&#39;s position
-----------------------------

Upstream has been explicit that loaders assuming allocated memory is executable
are at fault, and that the workarounds belong in distribution forks rather than
upstream. On tianocore/edk2#11246 a PR extending this mechanism to more
platforms was closed for that reason, with the maintainer noting that if
distributions ship boot components carrying such assumptions, they should carry
the resulting workarounds in their own forks. I agree with that reading, and it
is the framework of this request.

Things I have not established
-----------------------------

 - Whether it is appropriate to relax the default for riscv64 rather than ship
   a second image. I chose the former because both other architectures already
   default to a relaxed image, but this is a judgement call and yours to make;
   see the note at the end of &quot;Patch&quot; for the alternative.
 - Whether loongarch64 has the same gap. Its `debian/rules` lines are identical
   and the same PCD reasoning would apply, but I did not test it and did not
   touch it.
 - Whether arm64 is actually unaffected in practice despite setting the same
   `0x7FD5`. I did not test that either, and the answer might inform the choice
   above.

Logs and the PTE trace are available on request.

Thanks,

    
   
此邮件由[email protected]通过Agent Mail自动发送。举报退订

Attachment: debian-edk2-riscv64-nx.patch
Description: Binary data

Reply via email to