guest@6admin.io:~$

vim-plugin-manager : injection d'arguments planquée dans un .gitmodules

vim-plugin-manager (dépôt ) est sorti en v1.0.0 en avril 2025 avec un design dont j’étais fier: git submodules, packages natifs, le vimrc comme source de vérité. Les seize mois depuis (v1.0.0 à v2.2.10) sont la partie dont je n’étais pas encore fier et c’est la partie intéressante: le CHANGELOG se lit comme un cours sur la façon dont un outil qui fait confiance à sa config se fait humilier. Trois histoires de la saga.

L’injection d’argument qui vivait dans un fichier de config

La trouvaille la plus effrayante de la série (#9, classée HIGH, exécution de commande arbitraire prouvée): submodule.<path>.branch du .gitmodules passait tout droit dans git pull origin <branch>. Un nom de branche commençant par un tiret est parsé par git comme une option et git a une option qui exécute une commande: --upload-pack=<cmd>. Démontré en local avec git 2.43, quoting du shell notwithstanding.

Réfléchissez à qui écrit le .gitmodules: parfois vous, mais aussi les plugins eux-mêmes quand vous les ajoutez et chaque repo tiers que vous avez un jour cloné dans la config. Un repo de plugin hostile pouvait nommer la branche de son submodule --upload-pack=... et attendre. Le fix est du genre ennuyeux: sanitize_branch, premier caractère qui ne peut pas être -, charset ^[A-Za-z0-9._/-]+$, appliqué à la lecture du .gitmodules et au git submodule add -b, valeurs refusées qui avertissent et retombent sur la résolution de branche par défaut. Le même sprint a attrapé les frères et sœurs: noms de remote interpolés sans échappement dans git remote add (opérateurs shell = exécution de commande arbitraire), noms de modules qui cassaient le quoting d’une commande de commit composée et une règle générale a été extraite: validez chaque valeur non-trustée avant qu’elle n’atteigne git, le système de fichiers ou les logs (#10).

Un plugin manager est un gestionnaire de paquets. Les gestionnaires de paquets sont des frontières de confiance. Il a fallu une exécution de commande arbitraire prouvée pour que cette phrase cesse d’être théorique.

Le budget subprocess, piné pour toujours

Le travail de performance a une forme que je veux désormais partout: collect_status_local est passé de 9 subprocess git par module à 7 (6 quand une branche est déclarée dans .gitmodules), en réutilisant le parse de modules caché par mtime et en réduisant une cascade séquentielle upstream/show-ref à un seul git for-each-ref. Les 2 subprocess économisés sont une anecdote; la garde de régression ne l’est pas: tests/perf.vader pine le budget sous forme de test, pour que personne ne réintroduise en douceur le neuvième appel. Le health check est aussi passé d’une fonction séquentielle de 200 lignes à une table de Funcrefs, pinée par un test de caractérisation qui affirme que la sortie du rapport est inchangée.

Un travail de performance livré sans budget est un travail de performance que vous referez le trimestre prochain. Le budget est la fonctionnalité.

Les détails qui mentaient

Deux bugs de l’étagère “ça marche, à peu près”:

  • Les plugins détachés se rapportaient à jour. Un plugin en HEAD détaché ne peut pas être pullé, mais le status le rendait comme à jour. Maintenant :PluginManager status rapporte Detached avec un hint de re-pin actionnable: le plus proche tag atteignable, calculé offline. Un affichage d’état qui cache la décision que l’utilisateur doit prendre est une décoration.
  • Des miroirs de tags périmés empoisonnaient chaque update. Diagnostiqué sur un vrai setup: 12 miroirs de tags v1.x/v2.1.x périmés faisaient échouer chaque update du module concerné avec “Fetch failed”. Les tags locaux d’un submodule de plugin sont des miroirs de l’upstream, jamais des créations locales, donc le fix aligne le niveau de confiance: git fetch --tags --force origin, miroirs rafraîchis, erreur honnête seulement quand elle est réelle.

La leçon de test, répétée jusqu’à ce qu’elle tienne

Le vim -es headless ne tourne jamais la event loop, ce qui le rend parfait pour tester du code qui n’en a pas besoin et inutile pour tester les jobs async. La CI fait tourner la suite async réelle sous script -qec (un pty) et la matrice de distros (AlmaLinux 9/10, Debian Bookworm/Trixie, Ubuntu 24.04/26.04, Arch, Gentoo non-bloquant) existe parce que “Vim 8.2+” veut dire différents niveaux de patch avec différents bugs. Environ 4 300 lignes de tests Vader contre 5 600 de code, plus une purge de dead code dans le CHANGELOG qui se lit comme un grand ménage: champs d’état jamais lus, keywords de highlight qu’aucun code n’émet, une branche inatteignable de l’updater de la sidebar.

La boucle dogfood a tenu pendant tout ça: MyVim , toute ma config d’éditeur, tourne sur ce manager comme un plugin et c’est le premier dépôt contre lequel chacun de ces fixes sécurité a été lancé.

Quand l’outil se retourne

  • Les fichiers de config venant de tiers sont de l’input non-trusté. Le .gitmodules est livré par chaque plugin que vous installez; parsez-le comme un payload réseau.
  • Un budget de perf appartient à la suite de tests ou l’optimisation est une histoire que vous racontez, pas une propriété que vous possédez.
  • Les affichages d’état doivent faire remonter des décisions, pas du confort. “À jour” pour un plugin détaché est un mensonge avec une coche verte.
  • Les tags locaux sont des miroirs, donnez-leur les permissions du miroir. La moitié des échecs git bizarres de cette saga étaient des mismatches de niveau de confiance.

[edit: 2026] L’implémentation Ansible de tout ceci: les rôles neovim et vim .