Implémenter un module de fournisseur pKVM

Cette page explique comment implémenter un module fournisseur de machine virtuelle protégée basée sur le noyau (pKVM).

Pour android16-6.12 et versions ultérieures, une fois ces étapes effectuées, vous devriez obtenir une arborescence de répertoires semblable à celle-ci :

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

Pour obtenir un exemple complet, consultez Créer un module pKVM avec DDK.

Pour android15-6.6 et versions antérieures :

Makefile
el1.c
hyp/
    Makefile
    el2.c
  1. Ajoutez le code de l'hyperviseur EL2 (el2.c). Au minimum, ce code doit déclarer une fonction d'initialisation acceptant une référence à la structure 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 du module fournisseur pKVM est une structure encapsulant les rappels de l'hyperviseur pKVM. Cette structure suit les mêmes règles ABI que les interfaces GKI.

  2. Créez le hyp/Makefile pour compiler le code de l'hyperviseur :

    hyp-obj-y := el2.o
    include $(srctree)/arch/arm64/kvm/hyp/nvhe/Makefile.module
    
  3. Ajoutez le code du noyau EL1 (el1.c). La section d'initialisation de ce code doit contenir un appel à pkvm_load_el2 module pour charger le code de l'hyperviseur EL2 de l'étape 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. Créez les règles de compilation.

    Pour android16-6.12 et versions ultérieures, consultez Créer un module pKVM avec DDK pour créer ddk_library() pour EL2 et ddk_module() pour EL1.

    Pour android15-6.6 et les versions antérieures, créez le fichier makefile racine pour relier le code EL1 et 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
    

Charger un module pKVM

Comme pour les modules fournisseur GKI, les modules fournisseur pKVM peuvent être chargés à l'aide de modprobe. Toutefois, pour des raisons de sécurité, le chargement doit avoir lieu avant la suppression des privilèges. Pour charger un module pKVM, vous devez vous assurer que vos modules sont inclus dans le système de fichiers racine (initramfs) et vous devez ajouter les éléments suivants à votre ligne de commande du noyau :

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

Les modules du fournisseur pKVM stockés dans initramfs héritent de la signature et de la protection de initramfs.

Si l'un des modules du fournisseur pKVM ne parvient pas à se charger, le système est considéré comme non sécurisé et il ne sera pas possible de démarrer une machine virtuelle protégée.

Appeler une fonction EL2 (hyperviseur) depuis EL1 (module du noyau)

Un appel d'hyperviseur (HVC) est une instruction qui permet au noyau d'appeler l'hyperviseur. Avec l'introduction des modules de fournisseur pKVM, un HVC peut être utilisé pour appeler une fonction à exécuter au niveau EL2 (dans le module d'hyperviseur) à partir du niveau EL1 (module du noyau) :

  1. Dans le code EL2 (el2.c), déclarez le gestionnaire EL2 :

Android 14

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

     cpu_reg(ctx, 1) = 0;
   }

Android 15 ou version ultérieure

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

     regs->regs[0] = SMCCC_RET_SUCCESS;
     regs->regs[1] = 0;
   }
  1. Dans votre code EL1 (el1.c), enregistrez le gestionnaire EL2 dans votre module fournisseur 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. Dans votre code EL1 (el1.c), appelez le HVC :

    pkvm_el2_mod_call(hvc_number);
    

Déboguer et profiler le code EL2

Cette section contient plusieurs options permettant de déboguer le code EL2 du module pKVM.

Émettre et lire les événements de trace de l'hyperviseur

Tracefs est compatible avec l'hyperviseur pKVM. L'utilisateur racine a accès à l'interface, qui se trouve dans /sys/kernel/tracing/hypervisor/ :

  • tracing_on : active ou désactive le traçage.
  • trace : l'écriture dans ce fichier réinitialise la trace.
  • trace_pipe : la lecture de ce fichier affiche les événements de l'hyperviseur.
  • buffer_size_kb : taille de la mémoire tampon par processeur contenant les événements. Augmentez cette valeur si des événements sont perdus.

Par défaut, les événements sont désactivés. Pour les activer, utilisez le fichier /sys/kernel/tracing/hypervisor/events/my_event/enable correspondant dans Tracefs. Vous pouvez également activer n'importe quel événement d'hyperviseur au moment du démarrage avec la ligne de commande du noyau hyp_event=event1,event2.

Avant de déclarer un événement, le code EL2 du module doit déclarer le boilerplate suivant, où pkvm_ops est le struct pkvm_module_ops * transmis à la fonction init du module :

  #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

Déclarer des événements

Déclarez les événements dans leur propre fichier .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

Émettre des événements

Vous pouvez consigner des événements dans le code EL2 en appelant la fonction C générée :

  trace_pkvm_driver_event(id);

Ajouter un enregistrement (Android 15 ou version antérieure)

Pour Android 15 et les versions antérieures, incluez un enregistrement supplémentaire lors de l'initialisation du module. Cette autorisation n'est pas requise sous Android 16 et versions ultérieures.

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

Émettre des événements sans déclaration préalable (Android 16 et versions ultérieures)

Déclarer des événements peut être fastidieux pour un débogage rapide. trace_hyp_printk() permet à l'appelant de transmettre jusqu'à quatre arguments à une chaîne de format sans aucune déclaration d'événement :

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

Un boilerplate dans le code EL2 est également requis. trace_hyp_printk() est une macro qui appelle la fonction 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

Activez l'événement __hyp_printk dans /sys/kernel/tracing/hypervisor/events/ ou au moment du démarrage avec la ligne de commande du noyau hyp_event=__hyp_printk.

Rediriger les événements vers dmesg

Le paramètre de ligne de commande du noyau hyp_trace_printk=1 permet à l'interface de traçage de l'hyperviseur de transférer chaque événement enregistré vers dmesg du noyau. Cela est utile pour lire les événements lorsque trace_pipe est inaccessible.

Vider les événements lors d'une panique du noyau (Android 16 et versions ultérieures)

Les événements de l'hyperviseur sont interrogés. Il existe donc une fenêtre entre la dernière interrogation et une panique du noyau, au cours de laquelle des événements ont été émis, mais n'ont pas été vidés dans la console. L'option de configuration du noyau CONFIG_PKVM_DUMP_TRACE_ON_PANIC tente de vider les événements les plus récents dans la console si hyp_trace_printk a été activé.

Cette option est désactivée par défaut pour GKI.

Utiliser Ftrace pour suivre les appels et les retours de fonction (Android 16 et versions ultérieures)

Ftrace est une fonctionnalité du noyau qui vous permet de suivre chaque appel et retour de fonction. De même, l'hyperviseur pKVM propose deux événements : func et func_ret.

Vous pouvez sélectionner les fonctions tracées avec la ligne de commande du noyau hyp_ftrace_filter= ou avec l'un des fichiers tracefs :

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

Les filtres utilisent la correspondance glob de style shell.

Le filtre suivant trace les fonctions commençant par pkvm_hyp_driver :

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

Les événements func et func_ret ne sont disponibles qu'avec CONFIG_PKVM_FTRACE=y. Cette option est désactivée par défaut pour GKI.