הטמעת מודול של ספק pKVM

בדף הזה מוסבר איך מטמיעים מודול ספק של מכונה וירטואלית מוגנת מבוססת-ליבה (pKVM).

ב-Android 16-6.12 ואילך, אחרי שמסיימים את השלבים האלה, אמורה להיות לכם היררכיית ספריות שדומה לזו:

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

דוגמה מלאה זמינה במאמר איך בונים מודול pKVM באמצעות DDK.

בגרסה android15-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 הוא מבנה שמכיל קריאות חוזרות (callbacks) ל-hypervisor של 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. יוצרים את כללי הבנייה.

    ב-android16-6.12 ואילך, אפשר לעיין במאמר Build a pKVM module with DDK (יצירת מודול 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 (hypervisor) מ-EL1 (מודול ליבה)

קריאה ל-hypervisor‏ (HVC) היא הוראה שמאפשרת לליבה לקרוא ל-hypervisor. עם ההשקה של מודולי ספקים של pKVM, אפשר להשתמש ב-HVC כדי לקרוא לפונקציה שתפעל ב-EL2 (במודול ההיפר-ויזור) מ-EL1 (מודול הליבה):

  1. בקוד EL2 ‏ (el2.c), מכריזים על ה-handler של 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), רושמים את ה-handler של 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 תומך ב-hypervisor של 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() מאפשרת למתקשר להעביר עד ארבעה ארגומנטים למחרוזת פורמט ללא הצהרת אירוע:

  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 מציע שני אירועים func ו-func_ret.

אפשר לבחור את הפונקציות שרוצים לעקוב אחריהן באמצעות שורת הפקודה של ליבת hyp_ftrace_filter= או באמצעות אחד מהקבצים של tracefs:

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

המסננים משתמשים בהתאמה גלובלית בסגנון מעטפת.

המסנן הבא עוקב אחרי הפונקציות שמתחילות ב-pkvm_hyp_driver:

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

אירועים מסוג func ו-func_ret זמינים רק ב-CONFIG_PKVM_FTRACE=y. האפשרות הזו מושבתת כברירת מחדל ב-GKI.