ติดตั้งใช้งานโมดูลของผู้ให้บริการ pKVM

หน้านี้อธิบายวิธีติดตั้งใช้งานโมดูลของผู้ให้บริการเครื่องเสมือนที่ใช้เคอร์เนลที่ได้รับการปกป้อง (pKVM)

สำหรับ Android 6.12 ขึ้นไป เมื่อทำตามขั้นตอนเหล่านี้เสร็จแล้ว คุณควรมีโครงสร้างไดเรกทอรีที่คล้ายกับตัวอย่างต่อไปนี้

BUILD.bazel
el1.c
hyp/
    BUILD.bazel
    el2.c

ดูตัวอย่างที่สมบูรณ์ได้ที่ สร้างโมดูล pKVM ด้วย DDK

สำหรับ Android 5.6.6 และเวอร์ชันก่อนหน้า ให้ทำดังนี้

Makefile
el1.c
hyp/
    Makefile
    el2.c
  1. เพิ่มโค้ดไฮเปอร์ไวเซอร์ EL2 (el2.c) โค้ดนี้ต้องประกาศฟังก์ชัน init ที่ยอมรับการอ้างอิงไปยังโครงสร้าง pkvm_module_ops อย่างน้อย

    #include <asm/kvm_pkvm_module.h>
    
    int pkvm_driver_hyp_init(const struct pkvm_module_ops *ops)
    {
      /* Init the EL2 code */
    
      return 0;
    }
    

    API ของโมดูลผู้ให้บริการ pKVM เป็นโครงสร้างที่ห่อหุ้มการเรียกกลับไปยัง ไฮเปอร์ไวเซอร์ pKVM โครงสร้างนี้เป็นไปตามกฎ ABI เดียวกันกับอินเทอร์เฟซ GKI

  2. สร้าง hyp/Makefile เพื่อสร้างโค้ดไฮเปอร์ไวเซอร์

    hyp-obj-y := el2.o
    include $(srctree)/arch/arm64/kvm/hyp/nvhe/Makefile.module
    
  3. เพิ่มโค้ดเคอร์เนล EL1 (el1.c) ส่วน init ของโค้ดนี้ต้องมีการเรียกใช้ pkvm_load_el2 module เพื่อโหลดโค้ดไฮเปอร์ไวเซอร์ EL2 จากขั้นตอนที่ 1

    #include <linux/init.h>
    #include <linux/module.h>
    #include <linux/kernel.h>
    #include <asm/kvm_pkvm_module.h>
    
    int __kvm_nvhe_pkvm_driver_hyp_init(const struct pkvm_module_ops *ops);
    
    static int __init pkvm_driver_init(void)
    {
        unsigned long token;
    
        return pkvm_load_el2_module(__kvm_nvhe_pkvm_driver_hyp_init, &token);
    }
    module_init(pkvm_driver_init);
    
  4. สร้างกฎการสร้าง

    สำหรับ Android 16-6.12 ขึ้นไป โปรดดู สร้างโมดูล pKVM ด้วย DDK เพื่อสร้าง ddk_library() สำหรับ EL2 และ ddk_module() สำหรับ EL1

    สำหรับ android15-6.6 และเวอร์ชันก่อนหน้า ให้สร้างไฟล์ Makefile ระดับรูทเพื่อเชื่อมโยงโค้ด EL1 และ EL2 เข้าด้วยกัน

    ifneq ($(KERNELRELEASE),)
    clean-files := hyp/hyp.lds hyp/hyp-reloc.S
    
    obj-m := pkvm_module.o
    pkvm_module-y := el1.o hyp/kvm_nvhe.o
    
    $(PWD)/hyp/kvm_nvhe.o: FORCE
             $(Q)$(MAKE) $(build)=$(obj)/hyp $(obj)/hyp/kvm_nvhe.o
    else
    all:
            make -C $(KDIR) M=$(PWD) modules
    clean:
            make -C $(KDIR) M=$(PWD) clean
    endif
    

โหลดโมดูล pKVM

เช่นเดียวกับโมดูลของผู้ให้บริการ GKI คุณสามารถโหลดโมดูลของผู้ให้บริการ pKVM ได้โดยใช้ modprobe อย่างไรก็ตาม เพื่อความปลอดภัย การโหลดจะต้องเกิดขึ้นก่อนการลดสิทธิ์ หากต้องการโหลดโมดูล pKVM คุณต้องตรวจสอบว่าโมดูลของคุณรวมอยู่ใน ระบบไฟล์รูท (initramfs) และคุณต้องเพิ่มรายการต่อไปนี้ลงใน บรรทัดคำสั่งของเคอร์เนล

kvm-arm.protected_modules=mod1,mod2,mod3,...

โมดูลของผู้ให้บริการ pKVM ที่จัดเก็บไว้ใน initramfs จะรับช่วงลายเซ็นและการป้องกันของ initramfs

หากโหลดโมดูลของผู้ให้บริการ pKVM รายการใดรายการหนึ่งไม่สำเร็จ ระบบจะถือว่าไม่ปลอดภัยและจะเริ่มเครื่องเสมือนที่ได้รับการปกป้องไม่ได้

เรียกใช้ฟังก์ชัน EL2 (ไฮเปอร์ไวเซอร์) จาก EL1 (โมดูลเคอร์เนล)

การเรียก Hypervisor (HVC) คือคำสั่งที่อนุญาตให้เคอร์เนลเรียก Hypervisor เมื่อเปิดตัวโมดูลของผู้ให้บริการ pKVM คุณจะใช้ HVC เพื่อ เรียกใช้ฟังก์ชันที่ EL2 (ในโมดูลไฮเปอร์ไวเซอร์) จาก EL1 (โมดูลเคอร์เนล) ได้

  1. ในโค้ด EL2 (el2.c) ให้ประกาศตัวแฮนเดิล EL2 ดังนี้

Android 14

   void pkvm_driver_hyp_hvc(struct kvm_cpu_context *ctx)
   {
     /* Handle the call */

     cpu_reg(ctx, 1) = 0;
   }

Android 15 ขึ้นไป

   void pkvm_driver_hyp_hvc(struct user_pt_regs *regs)
   {
     /* Handle the call */

     regs->regs[0] = SMCCC_RET_SUCCESS;
     regs->regs[1] = 0;
   }
  1. ในโค้ด EL1 (el1.c) ให้ลงทะเบียนแฮนเดิล EL2 ในโมดูลของผู้ให้บริการ pKVM ดังนี้

    int __kvm_nvhe_pkvm_driver_hyp_init(const struct pkvm_module_ops *ops);
    void __kvm_nvhe_pkvm_driver_hyp_hvc(struct kvm_cpu_context *ctx); // Android14
    void __kvm_nvhe_pkvm_driver_hyp_hvc(struct user_pt_regs *regs);   // Android15
    
    static int hvc_number;
    
    static int __init pkvm_driver_init(void)
    {
      long token;
      int ret;
    
      ret = pkvm_load_el2_module(__kvm_nvhe_pkvm_driver_hyp_init,token);
      if (ret)
        return ret;
    
      ret = pkvm_register_el2_mod_call(__kvm_nvhe_pkvm_driver_hyp_hvc, token)
      if (ret < 0)
        return ret;
    
      hvc_number = ret;
    
      return 0;
    }
    module_init(pkvm_driver_init);
    
  2. ในโค้ด EL1 (el1.c) ให้เรียกใช้ HVC ดังนี้

    pkvm_el2_mod_call(hvc_number);
    

แก้ไขข้อบกพร่องและสร้างโปรไฟล์โค้ด EL2

ส่วนนี้มีตัวเลือกหลายอย่างในการแก้ไขข้อบกพร่องของโค้ด EL2 ของโมดูล pKVM

ส่งและอ่านเหตุการณ์การติดตามไฮเปอร์ไวเซอร์

Tracefs รองรับไฮเปอร์ไวเซอร์ pKVM ผู้ใช้รูทมีสิทธิ์เข้าถึงอินเทอร์เฟซ ซึ่งอยู่ใน /sys/kernel/tracing/hypervisor/

  • tracing_on: เปิดหรือปิดการติดตาม
  • trace: การเขียนลงในไฟล์นี้จะรีเซ็ตการติดตาม
  • trace_pipe: การอ่านไฟล์นี้จะพิมพ์เหตุการณ์ของไฮเปอร์ไวเซอร์
  • buffer_size_kb: ขนาดของบัฟเฟอร์ต่อ CPU ที่เก็บเหตุการณ์ เพิ่ม ค่านี้หากเหตุการณ์สูญหาย

โดยค่าเริ่มต้น ระบบจะปิดใช้เหตุการณ์ หากต้องการเปิดใช้เหตุการณ์ ให้ใช้ไฟล์ /sys/kernel/tracing/hypervisor/events/my_event/enable ที่เกี่ยวข้องใน Tracefs คุณยังเปิดใช้เหตุการณ์ Hypervisor ใดก็ได้ในเวลาบูตด้วยบรรทัดคำสั่งเคอร์เนลของ hyp_event=event1,event2

ก่อนประกาศเหตุการณ์ โค้ด EL2 ของโมดูลต้องประกาศ บอยเลอร์เพลตต่อไปนี้ โดยที่ pkvm_ops คือ struct pkvm_module_ops * ที่ส่งไปยังฟังก์ชัน init ของโมดูล

  #include "events.h"
  #define HYP_EVENT_FILE ../../../../relative/path/to/hyp/events.h
  #include <nvhe/define_events.h>

  #ifdef CONFIG_TRACING
  void *tracing_reserve_entry(unsigned long length)
  {
      return pkvm_ops->tracing_reserve_entry(length);
  }

  void tracing_commit_entry(void)
  {
      pkvm_ops->tracing_commit_entry();
  }
  #endif

ประกาศเหตุการณ์

ประกาศเหตุการณ์ในไฟล์ .h ของตัวเอง

  $ cat hyp/events.h
  #if !defined(__PKVM_DRIVER_HYPEVENTS_H_) || defined(HYP_EVENT_MULTI_READ)
  #define __PKVM_DRIVER_HYPEVENTS_H_

  #ifdef __KVM_NVHE_HYPERVISOR__
  #include <nvhe/trace.h>
  #endif

  HYP_EVENT(pkvm_driver_event,
          HE_PROTO(u64 id),
          HE_STRUCT(
                  he_field(u64, id)
          ),
          HE_ASSIGN(
                  __entry->id = id;
          ),
          HE_PRINTK("id=0x%08llx", __entry->id)
  );
  #endif

ปล่อยเหตุการณ์

คุณบันทึกเหตุการณ์ในโค้ด EL2 ได้โดยเรียกใช้ฟังก์ชัน C ที่สร้างขึ้น ดังนี้

  trace_pkvm_driver_event(id);

เพิ่มการลงทะเบียนเพิ่มเติม (Android 15 หรือต่ำกว่า)

สำหรับ Android 15 และต่ำกว่า ให้รวมการลงทะเบียนเพิ่มเติมในระหว่างการเริ่มต้นโมดูล ไม่จำเป็นต้องดำเนินการนี้ใน Android 16 ขึ้นไป

  #ifdef CONFIG_TRACING
  extern char __hyp_event_ids_start[];
  extern char __hyp_event_ids_end[];
  #endif

  int pkvm_driver_hyp_init(const struct pkvm_module_ops *ops)
  {
  #ifdef CONFIG_TRACING
      ops->register_hyp_event_ids((unsigned long)__hyp_event_ids_start,
                                        (unsigned long)__hyp_event_ids_end);
  #endif

      /* init module ... */

      return 0;
  }

ปล่อยเหตุการณ์โดยไม่ต้องประกาศก่อน (Android 16 ขึ้นไป)

การประกาศเหตุการณ์อาจเป็นเรื่องยุ่งยากสำหรับการแก้ไขข้อบกพร่องอย่างรวดเร็ว trace_hyp_printk() ช่วยให้ผู้โทรส่งอาร์กิวเมนต์ได้สูงสุด 4 รายการไปยังสตริงรูปแบบโดยไม่ต้องประกาศเหตุการณ์

  trace_hyp_printk("This is my debug");
  trace_hyp_printk("This is my variable: %d", (int)foo);
  trace_hyp_printk("This is my address: 0x%llx", phys);

นอกจากนี้ ยังต้องมีข้อความมาตรฐานในโค้ด EL2 ด้วย trace_hyp_printk() คือมาโคร ที่เรียกฟังก์ชัน trace___hyp_printk()

  #include <nvhe/trace.h>

  #ifdef CONFIG_TRACING
  void trace___hyp_printk(u8 fmt_id, u64 a, u64 b, u64 c, u64 d)
  {
          pkvm_ops->tracing_mod_hyp_printk(fmt_id, a, b, c, d);
  }
  #endif

เปิดใช้เหตุการณ์ __hyp_printk ใน /sys/kernel/tracing/hypervisor/events/ หรือ ในเวลาบูตด้วยบรรทัดคำสั่งของเคอร์เนล hyp_event=__hyp_printk

เปลี่ยนเส้นทางเหตุการณ์ไปยัง dmesg

พารามิเตอร์บรรทัดคำสั่งของเคอร์เนล hyp_trace_printk=1 ทำให้อินเทอร์เฟซการติดตามไฮเปอร์ไวเซอร์ ส่งต่อเหตุการณ์ที่บันทึกแต่ละรายการไปยัง dmesg ของเคอร์เนล ซึ่งมีประโยชน์ในการอ่านเหตุการณ์เมื่อเข้าถึง trace_pipe ไม่ได้

ที่เหลือ

ทิ้งเหตุการณ์ระหว่างเคอร์เนลแพนิก (Android 16 ขึ้นไป)

ระบบจะสำรวจเหตุการณ์ของไฮเปอร์ไวเซอร์ ดังนั้นจึงมีช่วงเวลาระหว่างการสำรวจครั้งสุดท้าย กับเคอร์เนลแพนิกที่เหตุการณ์ได้รับการปล่อยออกมาแต่ไม่ได้ส่งไปยังคอนโซล ตัวเลือกการกำหนดค่าเคอร์เนล CONFIG_PKVM_DUMP_TRACE_ON_PANIC พยายามทิ้งเหตุการณ์ล่าสุดในคอนโซลหากเปิดใช้ hyp_trace_printk

ตัวเลือกนี้จะปิดไว้โดยค่าเริ่มต้นสำหรับ GKI

ใช้ Ftrace เพื่อติดตามการเรียกฟังก์ชันและการคืนค่า (Android 16 ขึ้นไป)

Ftrace เป็นฟีเจอร์ของเคอร์เนลที่ช่วยให้คุณติดตามการเรียกใช้ฟังก์ชันแต่ละรายการและการคืนค่าได้ ในลักษณะที่คล้ายกัน ไฮเปอร์ไวเซอร์ pKVM มีเหตุการณ์ 2 รายการ func และ func_ret

คุณเลือกฟังก์ชันที่ติดตามได้ด้วยบรรทัดคำสั่งเคอร์เนล hyp_ftrace_filter= หรือด้วยไฟล์ tracefs อย่างใดอย่างหนึ่งต่อไปนี้

  • /sys/kernel/tracing/hypervisor/set_ftrace_filter
  • /sys/kernel/tracing/hypervisor/set_ftrace_notrace

ตัวกรองใช้การจับคู่ Glob รูปแบบเชลล์

ตัวกรองต่อไปนี้จะติดตามฟังก์ชันที่ขึ้นต้นด้วย pkvm_hyp_driver

  echo "__kvm_nvhe_pkvm_hyp_driver*" > /sys/kernel/tracing/hypervisor/set_ftrace_filter

กิจกรรม func และ func_ret จะใช้ได้กับ CONFIG_PKVM_FTRACE=y เท่านั้น ตัวเลือกนี้จะปิดไว้โดยค่าเริ่มต้นสำหรับ GKI