U-Boot and memory permissions

Posted on Thu 13 March 2025 in UEFI • 3 min read

Why not RWX

Explaining why is a complex topic and not within the scope of this blog, but generally speaking mapping executables with specific permissions (RO, RX, and RW^X) instead of just RWX is crucial for security. It greatly reduces the attack surface and makes vulnerabilities harder to exploit. The Arm architecture has complex ways of configuring the MMU and it depends on the overall system configuration.

For U-Boot we only care about "Stage 1 VMSAv8-64 Block and Page descriptor fields" found in D8-6492 of the ARM ARM version L.a

  • XN: eXecute Never -- Prevents execution
  • PXN: Privileged eXecute Never -- Prevents execution from privileged mode
  • UXN: Unprivileged eXecute Never -- Prevents execution from unprivileged mode
  • RO: Read-only -- Prevents memory writes

Which ones you have to set depends mainly on the translation regime. Running U-Boot in QEMU without virtualization ends up running U-Boot in EL1&0 translation regime and both PXN/UXN need to be set.

Current status

In U-Boot, memory is either normal memory which is mapped as RWX, or device memory which is mapped as RW. When U-Boot relocates it self to the top of DRAM, it doesn't change any of the memory permissions.

The output below is only available in the -next branch which includes this patchset by enabling CMD_MEMINFO && CMD_CMD_MEMINFO_MAP in the qemu_arm64_lwip_defconfig.

Booting up QEMU gives us this

qemu-system-aarch64 -m 8192 -smp 2 -nographic -cpu cortex-a57 \
    -bios u-boot.bin -machine virt,secure=off \
    -device virtio-rng-pci

U-Boot 2025.04-rc4-00404-g0e1fc465fea6 (Mar 16 2025 - 23:18:56 +0200)

DRAM:  8 GiB
Core:  52 devices, 15 uclasses, devicetree: board
[...]

=> meminfo
DRAM:  8 GiB
Walking pagetable at 000000023ffe0000, va_bits: 40. Using 4 levels
[0x0000023ffe1000]                          |  Table |            |               |
  [0x0000023ffe2000]                        |  Table |            |               |
    [0x00000000000000 - 0x00000008000000]   |  Block | RWX        | Normal        | Inner-shareable
    [0x00000008000000 - 0x00000040000000]   |  Block | PXN UXN    | Device-nGnRnE | Non-shareable
  [0x00000040000000 - 0x00004000000000]     |  Block | RWX        | Normal        | Inner-shareable
  [0x0000023ffe3000]                        |  Table |            |               |
    [0x00004010000000 - 0x00004020000000]   |  Block | PXN UXN    | Device-nGnRnE | Non-shareable
[0x0000023ffe4000]                          |  Table |            |               |
  [0x00008000000000 - 0x00010000000000]     |  Block | PXN UXN    | Device-nGnRnE | Non-shareable
  • PXN UXN: Read-Write memory
  • RWX: Read-Write-Execute memory

As you can see, only device mapped memory is not allowed to execute.

Enable memory permissions

The patchset above adds another interesting flag CONFIG_MMU_PGPROT which only applies to arm64. Unfortunately we can't yet enable it by default for all arm64 boards, because bugs like this and this will now lead to a crash.

With the above fixes we can enable the flag in QEMU and look into the new mappings

=> meminfo
DRAM:  8 GiB
Walking pagetable at 000000023ffe0000, va_bits: 40. Using 4 levels
[0x0000023ffe1000]                          |  Table |            |               |
  [0x0000023ffe2000]                        |  Table |            |               |
    [0x00000000000000 - 0x00000008000000]   |  Block | RWX        | Normal        | Inner-shareable
    [0x00000008000000 - 0x00000040000000]   |  Block | PXN UXN    | Device-nGnRnE | Non-shareable
  [0x00000040000000 - 0x00000200000000]     |  Block | RWX        | Normal        | Inner-shareable
  [0x0000023ffea000]                        |  Table |            |               |
    [0x00000200000000 - 0x0000023f600000]   |  Block | RWX        | Normal        | Inner-shareable
    [0x0000023ffeb000]                      |  Table |            |               |
      [0x0000023f600000 - 0x0000023f6b9000] |  Pages | RWX        | Normal        | Inner-shareable
      [0x0000023f6b9000 - 0x0000023f77d000] |  Pages | RO         | Normal        | Inner-shareable
      [0x0000023f77d000 - 0x0000023f77e000] |  Pages | RWX        | Normal        | Inner-shareable
      [0x0000023f77e000 - 0x0000023f7c8000] |  Pages | PXN UXN RO | Normal        | Inner-shareable
      [0x0000023f7c8000 - 0x0000023f7e0000] |  Pages | PXN UXN    | Normal        | Inner-shareable
      [0x0000023f7e0000 - 0x0000023f800000] |  Pages | RWX        | Normal        | Inner-shareable
    [0x0000023f800000 - 0x00000240000000]   |  Block | RWX        | Normal        | Inner-shareable
  [0x00000240000000 - 0x00004000000000]     |  Block | RWX        | Normal        | Inner-shareable
  [0x0000023ffe3000]                        |  Table |            |               |
    [0x00004010000000 - 0x00004020000000]   |  Block | PXN UXN    | Device-nGnRnE | Non-shareable
[0x0000023ffe4000]                          |  Table |            |               |
  [0x00008000000000 - 0x00010000000000]     |  Block | PXN UXN    | Device-nGnRnE | Non-shareable
  • PXN UXN RO: Read-only memory
  • PXN UXN: Read-Write memory
  • RO: Read-Execute memory

U-Boot has relocate to [0x0000023f6b9000 - 0x0000023f77d000] [0x0000023f77e000 - 0x0000023f7c8000] and [0x0000023f7c8000 - 0x0000023f7e0000] which now have proper memory permissions!

But we still have RWX memory

There are several reasons that the rest of the memory is left as RWX. One of them is EFI runtime services. U-Boot doesn't separate between EFI RO, RW and RX sections, instead it bundles all of the in the .efi_runtime and places them right before .text. We end up mapping 64kb for runtime services, which includes all the EFI related sections and .text. The linker script will need a bigger rewrite to map EFI services properly.

Another reason is the SetVirtualAddressMap which allows the OS to remap EFI runtime services in a VA of its choice. This is rarely called for the arm64 architecture (only when VA_BITS < 39) in Linux, but when it's needed we need to switch any RX (which will now hold the runtime services) to RWX to allow relocations.

So for now certain pages are left as RWX.

Future work

  • Rework the linker scripts and decouple EFI memory from .text
  • Apply proper mapping to EFI runtime services as well