Implementare un modulo del fornitore di pKVM

Questa pagina spiega come implementare un modulo fornitore di macchine virtuali basate sul kernel protetto (pKVM).

Per android16-6.12 e versioni successive, al termine di questi passaggi, dovresti avere una struttura di directory simile a:

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

Per un esempio completo, vedi Compilare un modulo pKVM con DDK .

Per android15-6.6 e versioni precedenti:

Makefile
el1.c
hyp/
    Makefile
    el2.c
  1. Aggiungi il codice dell'hypervisor EL2 (el2.c). Come minimo, questo codice deve dichiarare una funzione di inizializzazione che accetti un riferimento alla struct 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;
    }
    

    L'API del modulo fornitore pKVM è una struttura che incapsula i callback all'hypervisor pKVM. Questa struttura segue le stesse regole ABI delle interfacce GKI.

  2. Crea hyp/Makefile per creare il codice dell'hypervisor:

    hyp-obj-y := el2.o
    include $(srctree)/arch/arm64/kvm/hyp/nvhe/Makefile.module
    
  3. Aggiungi il codice del kernel EL1 (el1.c). La sezione init di questo codice deve contenere una chiamata a pkvm_load_el2 module per caricare il codice dell'hypervisor EL2 dal passaggio 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. Crea le regole di compilazione.

    Per android16-6.12 e versioni successive, consulta Crea un modulo pKVM con DDK per creare ddk_library() per EL2 e ddk_module() per EL1.

    Per android15-6.6 e versioni precedenti, crea il makefile principale per collegare il codice EL1 e 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
    

Caricare un modulo pKVM

Come per i moduli del fornitore GKI, i moduli del fornitore pKVM possono essere caricati utilizzando modprobe. Tuttavia, per motivi di sicurezza, il caricamento deve avvenire prima della rimozione dei privilegi. Per caricare un modulo pKVM, devi assicurarti che i moduli siano inclusi nel file system root (initramfs) e devi aggiungere quanto segue alla riga di comando del kernel:

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

I moduli del fornitore pKVM archiviati in initramfs ereditano la firma e la protezione di initramfs.

Se uno dei moduli del fornitore pKVM non viene caricato, il sistema viene considerato non sicuro e non sarà possibile avviare una macchina virtuale protetta.

Chiamare una funzione EL2 (hypervisor) da EL1 (modulo del kernel)

Una chiamata hypervisor (HVC) è un'istruzione che consente al kernel di chiamare l'hypervisor. Con l'introduzione dei moduli del fornitore pKVM, è possibile utilizzare un HVC per chiamare una funzione da eseguire a EL2 (nel modulo hypervisor) da EL1 (il modulo kernel):

  1. Nel codice EL2 (el2.c), dichiara il gestore EL2:

Android 14

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

     cpu_reg(ctx, 1) = 0;
   }

Android 15 o versioni successive

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

     regs->regs[0] = SMCCC_RET_SUCCESS;
     regs->regs[1] = 0;
   }
  1. Nel codice EL1 (el1.c), registra il gestore EL2 nel modulo fornitore 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. Nel codice EL1 (el1.c), chiama l'HVC:

    pkvm_el2_mod_call(hvc_number);
    

Esegui il debug e la profilazione del codice EL2

Questa sezione contiene diverse opzioni per eseguire il debug del codice EL2 del modulo pKVM.

Emettere e leggere gli eventi di traccia dell'hypervisor

Tracefs supporta l'hypervisor pKVM. L'utente root ha accesso all'interfaccia, che si trova in /sys/kernel/tracing/hypervisor/:

  • tracing_on: attiva o disattiva la tracciabilità.
  • trace: la scrittura in questo file reimposta la traccia.
  • trace_pipe: La lettura di questo file stampa gli eventi dell'hypervisor.
  • buffer_size_kb: la dimensione del buffer per CPU che contiene gli eventi. Aumenta questo valore se gli eventi vengono persi.

Per impostazione predefinita, gli eventi sono disattivati. Per attivarli, utilizza il file /sys/kernel/tracing/hypervisor/events/my_event/enable corrispondente in Tracefs. Puoi anche abilitare qualsiasi evento hypervisor all'avvio con la riga di comando del kernel di hyp_event=event1,event2.

Prima di dichiarare un evento, il codice EL2 del modulo deve dichiarare il seguente boilerplate, dove pkvm_ops è struct pkvm_module_ops * passato alla funzione init del modulo:

  #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

Dichiarare gli eventi

Dichiarare gli eventi nel proprio file .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

Emettere eventi

Puoi registrare gli eventi nel codice EL2 chiamando la funzione C generata:

  trace_pkvm_driver_event(id);

Aggiungere una registrazione aggiuntiva (Android 15 o versioni precedenti)

Per Android 15 e versioni precedenti, includi una registrazione aggiuntiva durante l'inizializzazione del modulo. Non è necessario in Android 16 e versioni successive.

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

Emettere eventi senza dichiarazione precedente (Android 16 e versioni successive)

La dichiarazione degli eventi può essere complessa per il debug rapido. trace_hyp_printk() consente al chiamante di passare fino a quattro argomenti a una stringa di formato senza alcuna dichiarazione di evento:

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

È necessario anche un boilerplate nel codice EL2. trace_hyp_printk() è una macro che chiama la funzione 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

Attiva l'evento __hyp_printk in /sys/kernel/tracing/hypervisor/events/ o al momento dell'avvio con la riga di comando del kernel hyp_event=__hyp_printk.

Reindirizzare gli eventi a dmesg

Il parametro della riga di comando del kernel hyp_trace_printk=1 fa sì che l'interfaccia di tracciamento dell'hypervisor inoltri ogni evento registrato a dmesg del kernel. È utile per leggere gli eventi quando trace_pipe non è accessibile.

Dump degli eventi durante un kernel panic (Android 16 e versioni successive)

Gli eventi dell'hypervisor vengono sottoposti a polling. Esiste quindi un intervallo tra l'ultimo polling e un kernel panic in cui sono stati emessi eventi, ma non sono stati scaricati nella console. L'opzione di configurazione del kernel CONFIG_PKVM_DUMP_TRACE_ON_PANIC tenta di eseguire il dump degli eventi più recenti nella console se hyp_trace_printk è stato attivato.

Questa opzione è disattivata per impostazione predefinita per GKI.

Utilizzare Ftrace per tracciare la chiamata e il ritorno della funzione (Android 16 e versioni successive)

Ftrace è una funzionalità del kernel che consente di tracciare ogni chiamata di funzione e il relativo valore restituito. In modo simile, l'hypervisor pKVM offre due eventi func e func_ret.

Puoi selezionare le funzioni tracciate con la riga di comando del kernel hyp_ftrace_filter= o con uno dei file tracefs:

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

I filtri utilizzano la corrispondenza con caratteri jolly in stile shell.

Il seguente filtro traccia le funzioni che iniziano con pkvm_hyp_driver:

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

Gli eventi func e func_ret sono disponibili solo con CONFIG_PKVM_FTRACE=y. Questa opzione è disattivata per impostazione predefinita per GKI.