یک ماژول فروشنده pKVM را پیاده سازی کنید

این صفحه نحوه پیاده‌سازی یک ماژول فروشنده ماشین مجازی مبتنی بر هسته محافظت‌شده (pKVM) را توضیح می‌دهد.

برای اندروید ۱۶-۶.۱۲ و بالاتر، وقتی این مراحل را انجام دادید، باید یک درخت دایرکتوری مشابه زیر داشته باشید:

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

برای مثال کامل، به ساخت ماژول pKVM با DDK مراجعه کنید.

برای اندروید ۱۵-۶.۶ و قبل از آن:

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 باشد تا کد hypervisor 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. قوانین ساخت را ایجاد کنید.

    برای اندروید ۱۶-۶.۱۲ و بالاتر، برای ایجاد ddk_library() برای EL2 و ddk_module() برای EL1 به بخش «ساخت ماژول pKVM با DDK» مراجعه کنید.

    برای اندروید ۱۵-۶.۶ و قبل از آن، فایل 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 (ماژول هسته)

فراخوانی هایپروایزر (HVC) دستورالعملی است که به هسته اجازه می‌دهد هایپروایزر را فراخوانی کند. با معرفی ماژول‌های فروشنده pKVM، می‌توان از HVC برای فراخوانی یک تابع برای اجرا در EL2 (در ماژول هایپروایزر) از EL1 (ماژول هسته) استفاده کرد:

  1. در کد EL2 ( el2.c )، کنترل‌کننده EL2 را تعریف کنید:

اندروید ۱۴

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

     cpu_reg(ctx, 1) = 0;
   }

اندروید ۱۵ یا بالاتر

   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 از hypervisor pKVM پشتیبانی می‌کند. کاربر root به رابط کاربری که در /sys/kernel/tracing/hypervisor/ قرار دارد، دسترسی دارد:

  • tracing_on : ردیابی را روشن یا خاموش می‌کند.
  • trace : نوشتن در این فایل، trace را ریست می‌کند.
  • trace_pipe : خواندن این فایل، رویدادهای hypervisor را چاپ می‌کند.
  • 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);

افزودن ثبت‌نام اضافی (اندروید ۱۵ یا پایین‌تر)

برای اندروید ۱۵ و پایین‌تر، در طول مقداردهی اولیه ماژول، یک ثبت اضافی را نیز لحاظ کنید. این کار در اندروید ۱۶ و بالاتر الزامی نیست.

  #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;
  }

انتشار رویدادها بدون اعلان قبلی (اندروید ۱۶ و بالاتر)

تعریف رویدادها برای اشکال‌زدایی سریع می‌تواند دشوار باشد. 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 باعث می‌شود رابط ردیابی hypervisor هر رویداد ثبت شده را به dmesg هسته ارسال کند. این برای خواندن رویدادها زمانی که trace_pipe غیرقابل دسترسی است مفید است.

حذف رویدادها در طول یک کرنل پنیک (اندروید ۱۶ و بالاتر)

رویدادهای هایپروایزر بررسی می‌شوند. بنابراین، پنجره‌ای بین آخرین بررسی و یک رویداد هسته وجود دارد که در آن رویدادها منتشر شده‌اند اما در کنسول ذخیره نشده‌اند. گزینه پیکربندی هسته CONFIG_PKVM_DUMP_TRACE_ON_PANIC در صورت فعال بودن hyp_trace_printk ، سعی می‌کند جدیدترین رویدادها را در کنسول ذخیره کند.

این گزینه به طور پیش‌فرض برای GKI غیرفعال است.

استفاده از Ftrace برای ردیابی فراخوانی تابع و برگرداندن آن (اندروید ۱۶ و بالاتر)

Ftrace یک ویژگی هسته است که به شما امکان می‌دهد هر فراخوانی تابع و بازگشت آن را ردیابی کنید. به طور مشابه، هایپروایزر pKVM دو رویداد func و func_ret را ارائه می‌دهد.

شما می‌توانید توابع ردیابی شده را با استفاده از دستور hyp_ftrace_filter= در خط فرمان هسته یا با استفاده از یکی از فایل‌های tracefs انتخاب کنید:

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

فیلترها از تطبیق glob به سبک shell استفاده می‌کنند.

فیلتر زیر توابعی را که با pkvm_hyp_driver شروع می‌شوند، ردیابی می‌کند:

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

رویدادهای func و func_ret فقط با CONFIG_PKVM_FTRACE=y در دسترس هستند. این گزینه به طور پیش‌فرض برای GKI غیرفعال است.