Aller au contenu
Note de recherche · RN 001

Self-Healing Lua Virtual Machines Through Runtime Bytecode Regeneration

Et si une machine virtuelle ne se contentait pas de signaler son bytecode dégradé, mais pouvait reconstruire localement l’intention perdue, tester la réparation et reprendre sa trajectoire sans redémarrage ?

Position de cette note

Asterion VM, PulseMap, ForgeLoop et Epoch Ledger sont des constructions originales développées pour ce carnet. Il s’agit d’une architecture exploratoire et d’un modèle exécutable, pas d’une annonce de production ni d’une synthèse de travaux externes.

Le problème : une instruction peut rester valide tout en devenant fausse#

Une machine virtuelle classique traite le bytecode comme un texte définitif. Elle le charge, le valide une fois, puis suppose que chaque instruction conserve son sens jusqu’à la fin du processus. Cette hypothèse fonctionne tant que la mémoire, le générateur de code et les structures auxiliaires restent cohérents.

Je m’intéresse à une panne plus subtile : la dérive sémantique locale. Le bloc continue d’être décodable, mais il n’exécute plus l’intention qui lui avait été assignée. Un saut de +3 devient +4, l’index d’une constante change, ou un opcode compatible remplace un autre opcode compatible. Le runtime ne plante pas nécessairement. Il produit simplement une trajectoire plausible et erronée.

Dans cette note, le terme bytecode Lua désigne le jeu d’instructions privé d’une VM écrite en Lua, et non le format interne d’un interprète existant. Ce choix rend l’expérience portable et me permet de contrôler chaque invariant.

Détecter

Observer une divergence sans arrêter chaque instruction pour une vérification complète.

Régénérer

Retrouver un bloc sain sans prétendre recréer une intention qui n’existe plus nulle part.

Basculer

Installer la réparation de manière atomique et conserver un chemin de retour.

La contrainte centrale est volontairement sévère : le fil d’exécution ne doit jamais voir un bloc à moitié reconstruit. Une génération est soit active, soit candidate, soit abandonnée.

Asterion VM : séparer l’exécution de son intention#

Mon prototype conceptuel s’appelle Asterion VM. Il maintient deux représentations différentes d’un même programme :

  • le ruban actif, optimisé pour être parcouru par l’interpréteur ;
  • la recette sémantique, compacte, immuable et suffisamment abstraite pour reconstruire un bloc.

La recette n’est pas une copie du bytecode. Elle décrit les opérations par rôle : charger une entrée, vérifier un type, additionner, bifurquer vers une étiquette. Le régénérateur choisit ensuite les opcodes, les registres et les déplacements compatibles avec la version courante de la VM.

Cette redondance est le prix de l’auto-réparation. Une VM ne peut pas déduire magiquement un programme perdu. Elle peut seulement le reconstruire si une forme indépendante de son intention a survécu.

Format minimal d’une génération#

Lua
local generation = {
  epoch = 42,
  block = "checkout.total",
  code = {
    { "LOADK",  1, 7 },
    { "GUARDN", 0 },
    { "ADD",    2, 0, 1 },
    { "RETURN", 2 }
  },
  seal = "checkout.total:42:4:9f",
  parent = 41
}

Le sceau n’a pas vocation à être cryptographique dans ce modèle. Il lie rapidement l’identité du bloc, son époque, sa longueur et son empreinte de contrôle. Une implémentation hostile devrait remplacer ce sceau par une primitive d’intégrité adaptée à son modèle de menace.

PulseMap : détecter une dérive sans tout recomptabiliser#

PulseMap découpe le ruban actif en blocs et attribue à chacun un pouls attendu. Le pouls agrège quatre signaux peu coûteux :

  1. la famille et l’arité des opcodes ;
  2. la forme du graphe de sorties ;
  3. les classes de constantes consultées ;
  4. un échantillon des transitions de pile virtuelle.

Chaque passage ne vérifie qu’un sous-ensemble tournant. Au bout de plusieurs passages, toute la surface a été observée sans imposer le coût maximal à chaque exécution. Le score de suspicion est calculé ainsi :

Texte
suspicion = 0,40 × dérive_structurelle
           + 0,25 × dérive_des_sorties
           + 0,20 × dérive_des_constantes
           + 0,15 × surprise_d_exécution

Ces coefficients ne sont pas présentés comme optimaux. Ils définissent simplement une politique lisible pour le prototype. Une suspicion supérieure à 0,72 ouvre une demande de régénération ; entre 0,48 et 0,72, le bloc entre en observation renforcée.

Pourquoi ne pas comparer le bloc entier ?

Une comparaison complète repère efficacement une altération, mais elle transforme chaque passage chaud en audit. PulseMap échange une détection instantanée contre un coût réparti et une fenêtre de doute explicite.

ForgeLoop : reconstruire, reloger, sceller#

Lorsque PulseMap ouvre une alerte, ForgeLoop ne modifie jamais le ruban actif. Il travaille dans une zone candidate et suit quatre étapes.

1. Relire l’intention#

La recette sémantique est récupérée par identifiant de bloc. ForgeLoop vérifie sa version, ses entrées attendues et les capacités dont elle dépend. Une recette absente ou incohérente rend la réparation impossible : le bloc est isolé au lieu d’être deviné.

2. Allouer les registres#

Le régénérateur construit les durées de vie des valeurs temporaires. Il réutilise un registre uniquement si sa dernière lecture précède la prochaine écriture. L’objectif n’est pas de produire le meilleur code possible, mais un code déterministe que le validateur pourra expliquer.

3. Résoudre les sauts#

Les étiquettes symboliques deviennent des déplacements relatifs après la disposition finale. Chaque cible doit appartenir au bloc ou à une sortie déclarée. Un saut vers le milieu d’une instruction est impossible dans cette ISA, car toutes les instructions occupent une cellule Lua.

4. Sceller la génération#

Le candidat reçoit une époque, l’identité de son parent et son empreinte. Il reste invisible pour le dispatch principal jusqu’à ce qu’Epoch Ledger lui accorde un essai canari.

Lua
local function forger(recipe, abi, parent)
  local ir = normaliser(recipe)
  local allocation = allouer_registres(ir, abi.register_limit)
  local code = emettre(ir, allocation, abi.opcodes)
  resoudre_etiquettes(code)

  local candidate = {
    epoch = parent.epoch + 1,
    block = recipe.id,
    code = code,
    parent = parent.epoch
  }
  candidate.seal = sceller(candidate)
  return verifier(candidate, abi) and candidate or nil
end

Le code est intentionnellement petit : la propriété importante n’est pas la sophistication du compilateur, mais l’impossibilité d’écraser la génération courante avant validation.

Epoch Ledger : une réparation est une transaction#

Epoch Ledger tient l’historique local de chaque bloc. Le dispatch lit un seul pointeur de génération. Le basculement consiste donc à remplacer cette référence entre deux exécutions du bloc, jamais pendant l’une d’elles.

Cycle transactionnel d’une réparation Asterion

Le canari reçoit une copie des entrées récentes, mais ses effets sortants sont retenus dans un journal. Trois passages cohérents autorisent le commit. Une exception, une sortie structurellement différente ou un budget dépassé entraîne l’abandon du candidat.

La comparaison ne peut pas toujours exiger des valeurs identiques. Une horloge, un identifiant ou une source aléatoire rendent cette égalité absurde. La recette déclare donc une enveloppe d’effets : types de sorties, domaines permis et capacités appelables. Le canari doit rester à l’intérieur de cette enveloppe.

Banc de diagnostic interactif#

Le panneau suivant est un modèle de décision, pas une mesure de performance. Il permet d’explorer la politique d’Asterion : intensité de dérive, confiance dans la recette et coût de régénération. Les seuils correspondent aux règles décrites plus haut.

Banc de diagnostic Asterionmodèle local · aucune donnée envoyée
exécution
Décision proposée Forger un canari

La dérive est forte et la recette reste suffisamment fiable.

confiance de décision82%

Scénario complet : réparer un saut décalé#

Imaginons un bloc cart.discount dont la dernière condition doit éviter la remise si le panier est vide. Le ruban actif contient un saut +4, toujours valide, alors que la recette décrit l’étiquette without_discount située à +3.

  1. PulseMap remarque que la forme des sorties ne correspond plus au pouls attendu.
  2. Deux passages renforcés confirment la divergence ; le bloc demeure exécutable mais passe en mode surveillé.
  3. ForgeLoop relit la recette, réalloue les temporaires et produit le saut +3 dans l’époque suivante.
  4. Epoch Ledger rejoue trois entrées récentes dans un journal d’effets isolé.
  5. Les sorties rentrent dans l’enveloppe déclarée ; la génération candidate devient active.
  6. L’ancienne génération est conservée jusqu’à expiration de la fenêtre de retour.

Le système s’est réparé sans prétendre que le bytecode seul contenait la réponse. La réponse venait de la recette indépendante ; le runtime a seulement établi que le ruban et l’intention avaient divergé.

Invariants que je refuse de négocier#

01

Pas d’écriture en place. Une réparation construit toujours une génération distincte.

02

Pas d’intention inventée. Une recette absente produit un arrêt contrôlé, jamais une approximation.

03

Pas d’effet canari direct. Les sorties sont journalisées avant d’être acceptées ou jetées.

04

Pas d’histoire effacée. Chaque génération connaît son parent et la raison du basculement.

05

Pas de guérison silencieuse. Une réparation réussie reste un incident observable.

Ce que l’architecture ne résout pas#

L’auto-réparation déplace les pannes ; elle ne les abolit pas. Si la recette et le ruban sont corrompus de la même manière, Asterion perd son point de comparaison. Si le runtime Lua hôte est compromis, le journal d’effets n’offre aucune frontière forte. Si un bloc dépend d’une entrée non reproductible, le canari ne peut établir qu’une conformité partielle.

Le coût mémoire augmente également : générations, recettes et journaux coexistent. La latence de forge peut devenir incompatible avec une boucle temps réel. Enfin, une réparation répétée du même bloc indique probablement un défaut systémique dans le générateur ou dans le stockage ; le runtime doit alors cesser de « guérir » et ouvrir un incident durable.

Frontière de sécurité

Asterion est une couche logique dans un même processus. Elle n’isole ni la mémoire native ni l’ordonnanceur du système. Son objectif est la continuité contrôlée face à une dérive locale, pas l’exécution de code hostile.

Conclusion provisoire#

Une VM auto-réparatrice crédible ne doit pas être imaginée comme un organisme qui « comprend » son code. Je la vois plutôt comme un système transactionnel muni de deux descriptions indépendantes : une forme rapide pour agir et une forme stable pour se rappeler ce qu’il devait faire.

PulseMap réduit la détection à des signaux tournants. ForgeLoop reconstruit un candidat sans toucher au présent. Epoch Ledger transforme le changement de génération en transaction réversible. Ensemble, ces mécanismes dessinent une propriété simple : continuer n’est autorisé que lorsque la réparation est plus explicable que la panne.

La prochaine version de cette note devra confronter le modèle à un exécuteur Lua minimal, mesurer le coût de PulseMap et déterminer à quel moment la conservation des générations devient plus dangereuse que le redémarrage. Jusque-là, Asterion reste ce qu’il doit être : une constellation de conception suffisamment nette pour guider une implémentation, mais pas encore une promesse opérationnelle.

Fin de la note RN 001Recherche personnelle · version 0.2 · 21 juillet 2026

Toutes les recherches
Index local, aucune donnée envoyée↑↓ parcourir · ↵ ouvrir