Wdrażanie modułu dostawcy pKVM

Na tej stronie dowiesz się, jak wdrożyć moduł dostawcy chronionej maszyny wirtualnej opartej na jądrze (pKVM).

W przypadku wersji android16-6.12 i nowszych po wykonaniu tych czynności powinna być widoczna struktura katalogów podobna do tej:

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

Pełny przykład znajdziesz w artykule Tworzenie modułu pKVM za pomocą DDK.

W przypadku Androida 15–6.6 i starszych:

Makefile
el1.c
hyp/
    Makefile
    el2.c
  1. Dodaj kod hiperwizora EL2 (el2.c). Musi on zawierać co najmniej deklarację funkcji init, która akceptuje odwołanie do struktury 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;
    }
    

    Interfejs API modułu dostawcy pKVM to struktura zawierająca wywołania zwrotne do hiperwizora pKVM. Ta struktura jest zgodna z tymi samymi regułami interfejsu ABI co interfejsy GKI.

  2. Utwórz hyp/Makefile, aby skompilować kod hiperwizora:

    hyp-obj-y := el2.o
    include $(srctree)/arch/arm64/kvm/hyp/nvhe/Makefile.module
    
  3. Dodaj kod jądra EL1 (el1.c). Sekcja inicjowania tego kodu musi zawierać wywołanie funkcji pkvm_load_el2 module, aby wczytać kod hiperwizora EL2 z kroku 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. Utwórz reguły kompilacji.

    W przypadku Androida 16–6.12 i nowszych zapoznaj się z artykułem Tworzenie modułu pKVM za pomocą DDK, aby utworzyć ddk_library() dla EL2 i ddk_module() dla EL1.

    W przypadku Androida 15-6.6 i starszych wersji utwórz główny plik makefile, aby połączyć kod EL1 i 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
    

Wczytywanie modułu pKVM

Podobnie jak w przypadku modułów dostawcy GKI, moduły dostawcy pKVM można wczytywać za pomocą polecenia modprobe. Ze względów bezpieczeństwa wczytywanie musi jednak nastąpić przed ograniczeniem uprawnień. Aby załadować moduł pKVM, musisz się upewnić, że moduły są uwzględnione w głównym systemie plików (initramfs), i dodać do wiersza poleceń jądra te elementy:

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

Moduły dostawcy pKVM przechowywane w initramfs dziedziczą podpis i ochronę initramfs.

Jeśli nie uda się wczytać jednego z modułów dostawcy pKVM, system zostanie uznany za niezabezpieczony i nie będzie można uruchomić chronionej maszyny wirtualnej.

Wywoływanie funkcji EL2 (hiperwizora) z EL1 (modułu jądra)

Wywołanie hypervisora (HVC) to instrukcja, która umożliwia jądru wywołanie hypervisora. Wraz z wprowadzeniem modułów dostawcy pKVM wywołanie HVC może służyć do wywoływania funkcji do uruchomienia na poziomie EL2 (w module hipernadzorcy) z poziomu EL1 (w module jądra):

  1. W kodzie EL2 (el2.c) zadeklaruj moduł obsługi EL2:

Android 14

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

     cpu_reg(ctx, 1) = 0;
   }

Android 15 lub nowszy

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

     regs->regs[0] = SMCCC_RET_SUCCESS;
     regs->regs[1] = 0;
   }
  1. W kodzie EL1 (el1.c) zarejestruj moduł obsługi EL2 w module dostawcy 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. W kodzie EL1 (el1.c) wywołaj HVC:

    pkvm_el2_mod_call(hvc_number);
    

Debugowanie i profilowanie kodu EL2

Ta sekcja zawiera kilka opcji debugowania kodu EL2 modułu pKVM.

Wysyłanie i odczytywanie zdarzeń logu czasu hiperwizora

Tracefs obsługuje hiperwizor pKVM. Użytkownik root ma dostęp do interfejsu, który znajduje się w /sys/kernel/tracing/hypervisor/:

  • tracing_on: włącza lub wyłącza śledzenie.
  • trace: zapisanie danych w tym pliku powoduje zresetowanie śladu.
  • trace_pipe: Odczytanie tego pliku powoduje wydrukowanie zdarzeń hiperwizora.
  • buffer_size_kb: rozmiar bufora na procesor przechowującego zdarzenia. Zwiększ tę wartość, jeśli zdarzenia są tracone.

Domyślnie zdarzenia są wyłączone. Aby je włączyć, użyj odpowiedniego pliku /sys/kernel/tracing/hypervisor/events/my_event/enable w Tracefs. Możesz też włączyć dowolne zdarzenie hipernadzorcy w momencie uruchamiania za pomocą wiersza poleceń jądra hyp_event=event1,event2.

Przed zadeklarowaniem zdarzenia kod EL2 modułu musi zadeklarować poniższy tekst standardowy, gdzie pkvm_ops to struct pkvm_module_ops * przekazane do funkcji modułu 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

Deklarowanie zdarzeń

Deklarowanie zdarzeń w osobnym pliku .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

Emitowanie zdarzeń

Zdarzenia możesz rejestrować w kodzie EL2, wywołując wygenerowaną funkcję C:

  trace_pkvm_driver_event(id);

Dodawanie dodatkowej rejestracji (Android 15 lub starszy)

W przypadku Androida 15 i starszych wersji dodaj dodatkową rejestrację podczas inicjowania modułu. Nie jest to wymagane w Androidzie 16 i nowszym.

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

Wysyłanie zdarzeń bez wcześniejszej deklaracji (Android 16 i nowszy)

Deklarowanie zdarzeń może być uciążliwe w przypadku szybkiego debugowania. trace_hyp_printk() umożliwia przekazywanie do ciągu formatującego maksymalnie 4 argumentów bez deklaracji zdarzenia:

  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);

W kodzie EL2 wymagany jest też tekst standardowy. trace_hyp_printk() to makro, które wywołuje funkcję 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

Włącz zdarzenie __hyp_printk/sys/kernel/tracing/hypervisor/events/ lub podczas uruchamiania za pomocą wiersza poleceń jądra hyp_event=__hyp_printk.

Przekierowywanie zdarzeń do dmesg

Parametr wiersza poleceń jądra hyp_trace_printk=1 sprawia, że interfejs śledzenia hiperwizora przekazuje każde zarejestrowane zdarzenie do dmesg jądra. Jest to przydatne do odczytywania zdarzeń, gdy trace_pipe jest niedostępny.

Zrzucanie zdarzeń podczas paniki jądra (Android 16 i nowszy)

Zdarzenia hiperwizora są odpytywane. Dlatego między ostatnim odpytywaniem a błędem krytycznym jądra istnieje okres, w którym zdarzenia zostały wyemitowane, ale nie zostały zrzucone do konsoli. Opcja konfiguracji jądra CONFIG_PKVM_DUMP_TRACE_ON_PANIC próbuje zrzucić najnowsze zdarzenia w konsoli, jeśli włączono hyp_trace_printk.

W przypadku GKI ta opcja jest domyślnie wyłączona.

Używanie Ftrace do śledzenia wywołań i powrotów funkcji (Android 16 i nowszy)

Ftrace to funkcja jądra, która umożliwia śledzenie każdego wywołania funkcji i każdego powrotu z niej. Podobnie hiperwizor pKVM oferuje 2 zdarzenia: funcfunc_ret.

Funkcje śledzone możesz wybrać za pomocą wiersza poleceń jądrahyp_ftrace_filter= lub jednego z plików tracefs:

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

Filtry używają dopasowywania wzorców glob w stylu powłoki.

Ten filtr śledzi funkcje zaczynające się od pkvm_hyp_driver:

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

Zdarzenia funcfunc_ret są dostępne tylko w przypadku CONFIG_PKVM_FTRACE=y. W przypadku GKI ta opcja jest domyślnie wyłączona.