Как реализовать модуль поставщика pKVM

На этой странице рассказывается, как реализовать модуль поставщика защищенной виртуальной машины на основе ядра (pKVM).

Если вы используете android16-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 – это структура, инкапсулирующая обратные вызовы гипервизора pKVM. Эта структура соответствует тем же правилам ABI, что и интерфейсы GKI.

  2. Создайте hyp/Makefile, чтобы сгенерировать код гипервизора:

    hyp-obj-y := el2.o
    include $(srctree)/arch/arm64/kvm/hyp/nvhe/Makefile.module
    
  3. Добавьте код ядра EL1 (el1.c). В разделе инициализации этого кода должен быть вызов 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 и более поздних следуйте инструкциям в разделе Как создать модуль 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 (модуля ядра)

Вызов гипервизора (HVC) – это инструкция, позволяющая ядру вызывать гипервизор. С появлением модулей поставщика 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. У пользователя root есть доступ к интерфейсу, который находится в /sys/kernel/tracing/hypervisor/:

  • tracing_on: включает или отключает трассировку.
  • trace: запись в этот файл сбрасывает трассировку.
  • trace_pipe: при чтении этого файла выводятся события гипервизора.
  • buffer_size_kb – размер буфера для каждого процессора, в котором хранятся события. Увеличьте это значение, если события теряются.

По умолчанию события отключены. Чтобы включить их, используйте соответствующий файл /sys/kernel/tracing/hypervisor/events/my_event/enable в Tracefs. Вы также можете включить любое событие гипервизора во время загрузки с помощью командной строки ядра 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.