e6b8f35
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
e6b8f35
From: Laszlo Ersek <lersek@redhat.com>
e6b8f35
Date: Fri, 7 Apr 2023 16:21:54 +0200
e6b8f35
Subject: [PATCH] grub_dl_set_mem_attrs(): add self-check for the tramp/GOT
e6b8f35
 sizes
e6b8f35
e6b8f35
On aarch64 UEFI, we currently have a crasher:
e6b8f35
e6b8f35
  grub_dl_load_core()
e6b8f35
    grub_dl_load_core_noinit()
e6b8f35
e6b8f35
      /* independent allocation: must remain writable */
e6b8f35
      mod = grub_zalloc();
e6b8f35
e6b8f35
      /* allocates module image with incorrect tail alignment */
e6b8f35
      grub_dl_load_segments()
e6b8f35
e6b8f35
      /* write-protecting the module image makes "mod" read-only! */
e6b8f35
      grub_dl_set_mem_attrs()
e6b8f35
        grub_update_mem_attrs()
e6b8f35
e6b8f35
    grub_dl_init()
e6b8f35
      /* page fault, crash */
e6b8f35
      mod->next = ...;
e6b8f35
e6b8f35
- Commit 887f1d8fa976 ("modules: load module sections at page-aligned
e6b8f35
  addresses", 2023-02-08) forgot to page-align the allocation of the
e6b8f35
  trampolines and GOT areas of grub2 modules, in grub_dl_load_segments().
e6b8f35
e6b8f35
- Commit ad1b904d325b ("nx: set page permissions for loaded modules.",
e6b8f35
  2023-02-08) calculated a common bounding box for the trampolines and GOT
e6b8f35
  areas in grub_dl_set_mem_attrs(), rounded the box size up to a whole
e6b8f35
  multiple of EFI page size ("arch_addralign"), and write-protected the
e6b8f35
  resultant page range.
e6b8f35
e6b8f35
Consequently, grub_dl_load_segments() places the module image in memory
e6b8f35
such that its tail -- the end of the trampolines and GOT areas -- lands at
e6b8f35
the head of a page whose tail in turn contains independent memory
e6b8f35
allocations, such as "mod". grub_dl_set_mem_attrs() will then unwittingly
e6b8f35
write-protect these other allocations too.
e6b8f35
e6b8f35
But "mod" must remain writable: we assign "mod->next" in grub_dl_init()
e6b8f35
subsequently. Currently we crash there with a page fault / permission
e6b8f35
fault.
e6b8f35
e6b8f35
(The crash is not trivial to hit: the tramp/GOT areas are irrelevant on
e6b8f35
x86_64, plus the page protection depends on the UEFI platform firmware
e6b8f35
providing EFI_MEMORY_ATTRIBUTE_PROTOCOL. In practice, the crash is
e6b8f35
restricted to aarch64 edk2 (ArmVirtQemu) builds containing commit
e6b8f35
1c4dfadb4611, "ArmPkg/CpuDxe: Implement EFI memory attributes protocol",
e6b8f35
2023-03-16.)
e6b8f35
e6b8f35
Example log before the patch:
e6b8f35
e6b8f35
> kern/dl.c:736: updating attributes for GOT and trampolines ("video_fb")
e6b8f35
> kern/efi/mm.c:927: set +rx -w on 0x13b88b000-0x13b88bfff before:rwx after:r-x
e6b8f35
> kern/dl.c:744: done updating module memory attributes for "video_fb"
e6b8f35
> kern/dl.c:639: flushing 0xe4f0 bytes at 0x13b87d000
e6b8f35
> kern/arm64/cache.c:42: D$ line size: 64
e6b8f35
> kern/arm64/cache.c:43: I$ line size: 64
e6b8f35
> kern/dl.c:839: module name: video_fb
e6b8f35
> kern/dl.c:840: init function: 0x0
e6b8f35
> kern/dl.c:865: Initing module video_fb
e6b8f35
>
e6b8f35
> Synchronous Exception at 0x000000013B8A76EC
e6b8f35
> PC 0x00013B8A76EC
e6b8f35
>
e6b8f35
>   X0 0x000000013B88B960   X1 0x0000000000000000   X2 0x000000013F93587C   X3 0x0000000000000075
e6b8f35
>
e6b8f35
>   SP 0x00000000470745C0  ELR 0x000000013B8A76EC  SPSR 0x60000205  FPSR 0x00000000
e6b8f35
>  ESR 0x9600004F          FAR 0x000000013B88B9D0
e6b8f35
>
e6b8f35
>  ESR : EC 0x25  IL 0x1  ISS 0x0000004F
e6b8f35
>
e6b8f35
> Data abort: Permission fault, third level
e6b8f35
e6b8f35
Note the following:
e6b8f35
e6b8f35
- The whole 4K page at 0x1_3B88_B000 is write-protected.
e6b8f35
e6b8f35
- The "video_fb" module actually lives at [0x1_3B87_D000, 0x1_3B88_B4F0)
e6b8f35
  -- left-inclusive, right-exclusive --; that is, in the last page (at
e6b8f35
  0x1_3B88_B000), it only occupies the first 0x4F0 bytes.
e6b8f35
e6b8f35
- The instruction at 0x1_3B8A_76EC faults. Not shown here, but it is a
e6b8f35
  store instruction, which writes to the field at offset 0x70 of the
e6b8f35
  structure pointed-to by the X0 register. This is the "mod->next"
e6b8f35
  assignment from grub_dl_init().
e6b8f35
e6b8f35
- The faulting address is therefore (X0 + 0x70), i.e., 0x1_3B88_B9D0. This
e6b8f35
  is indeed the value held in the FAR register.
e6b8f35
e6b8f35
- The faulting address 0x1_3B88_B9D0 falls in the above-noted page (at
e6b8f35
  0x1_3B88_B000), namely at offset 0x9D0. This is *beyond* the first 0x4F0
e6b8f35
  bytes that the very tail of the "video_fb" module occupies at the front
e6b8f35
  of that page.
e6b8f35
e6b8f35
For now, add a self-check that reports this bug (and prevents the crash by
e6b8f35
skipping the write protection).
e6b8f35
e6b8f35
Example log after the patch:
e6b8f35
e6b8f35
> kern/dl.c:742:BUG: trying to protect pages outside of module allocation
e6b8f35
> ("video_fb"): module base 0x13b87d000, size 0xe4f0; tramp/GOT base
e6b8f35
> 0x13b88b000, size 0x1000
e6b8f35
e6b8f35
Signed-off-by: Laszlo Ersek <lersek@redhat.com>
e6b8f35
---
e6b8f35
 grub-core/kern/dl.c | 11 ++++++++++-
e6b8f35
 1 file changed, 10 insertions(+), 1 deletion(-)
e6b8f35
e6b8f35
diff --git a/grub-core/kern/dl.c b/grub-core/kern/dl.c
e6b8f35
index a97f4a8b13..3b66fa410e 100644
e6b8f35
--- a/grub-core/kern/dl.c
e6b8f35
+++ b/grub-core/kern/dl.c
e6b8f35
@@ -682,7 +682,7 @@ grub_dl_set_mem_attrs (grub_dl_t mod, void *ehdr)
e6b8f35
 #if !defined (__i386__) && !defined (__x86_64__) && !defined(__riscv)
e6b8f35
   grub_size_t arch_addralign = grub_arch_dl_min_alignment ();
e6b8f35
   grub_addr_t tgaddr;
e6b8f35
-  grub_uint64_t tgsz;
e6b8f35
+  grub_size_t tgsz;
e6b8f35
 #endif
e6b8f35
 
e6b8f35
   grub_dprintf ("modules", "updating memory attributes for \"%s\"\n",
e6b8f35
@@ -736,6 +736,15 @@ grub_dl_set_mem_attrs (grub_dl_t mod, void *ehdr)
e6b8f35
       grub_dprintf ("modules",
e6b8f35
 		    "updating attributes for GOT and trampolines (\"%s\")\n",
e6b8f35
 		    mod->name);
e6b8f35
+      if (tgaddr < (grub_addr_t)mod->base ||
e6b8f35
+          tgsz > (grub_addr_t)-1 - tgaddr ||
e6b8f35
+	  tgaddr + tgsz > (grub_addr_t)mod->base + mod->sz)
e6b8f35
+	return grub_error (GRUB_ERR_BUG,
e6b8f35
+			   "BUG: trying to protect pages outside of module "
e6b8f35
+			   "allocation (\"%s\"): module base %p, size 0x%"
e6b8f35
+			   PRIxGRUB_SIZE "; tramp/GOT base 0x%" PRIxGRUB_ADDR
e6b8f35
+			   ", size 0x%" PRIxGRUB_SIZE,
e6b8f35
+			   mod->name, mod->base, mod->sz, tgaddr, tgsz);
e6b8f35
       grub_update_mem_attrs (tgaddr, tgsz, GRUB_MEM_ATTR_R|GRUB_MEM_ATTR_X,
e6b8f35
 			     GRUB_MEM_ATTR_W);
e6b8f35
     }