Skip to content

test(fuzz): couvrir les entrées que le moteur ne produit pas (0.1.64) - #156

Merged
stephrobert merged 1 commit into
mainfrom
test/fuzz-entrees-non-fiables
Aug 23, 2026
Merged

test(fuzz): couvrir les entrées que le moteur ne produit pas (0.1.64)#156
stephrobert merged 1 commit into
mainfrom
test/fuzz-entrees-non-fiables

Conversation

@stephrobert

Copy link
Copy Markdown
Owner

Le dépôt fuzzait les deux fichiers du contrat déclaratif, ce qui est la bonne
intuition mais pas la couverture complète : le contexte local et les outputs de
Terraform sont lus avec la même confiance, et n'étaient couverts par aucun
harnais.

Le harnais a trouvé un vrai défaut

Trente-quatre octets, et personne ne les avait écrits à la main :

$ uv run --group fuzz python fuzz/fuzz_terraform_outputs.py … -atheris_runs=30000
=== Uncaught Python exception: ===
AttributeError: 'str' object has no attribute 'items'
  File "src/dsoxlab/infra/inventory.py", line 89, in build_inventory
    k: str(v) for k, v in (raw.get("value", raw)).items()

{"hosts": {"value": "10.99.0.11"}}

Un AttributeError au moment de jouer un lab, sans jamais dire que la cause
est un state Terraform périmé, un provider qui a renommé sa sortie, ou un state
édité à la main. Corrigé, et figé par un test : le fuzzing découvre, un test
empêche le retour. Après correction, 200 000 exécutions ne rendent plus rien.

Chaque entrée est non fiable pour une raison différente

Et cette raison décide du contrat que son harnais assère. C'est le point de
conception de la PR, pas un détail :

Entrée Pourquoi elle n'est pas fiable Contrat assuré
lab.yaml, meta.yml écrits par un dépôt fournisseur KeyError, ValueError, YAMLError sont la façon de refuser
.dsoxlab-context.json vit sur le disque de l'apprenant : édité à la main, tronqué par un portable refermé au mauvais moment, laissé par une version ancienne aucune exception, jamais
outputs Terraform produits par un binaire externe dont la version, les providers et le schéma bougent rejet par exception prévue, pas par traceback

Le harnais du contexte n'a aucune exception de contrat, et c'est tout son
propos.
read_context promet de rendre un contexte vide plutôt que de lever,
parce que perdre le contexte coûte un dsoxlab use là où une exception coûte la
CLI entière, sans même nommer le fichier à supprimer.

Le harnais Terraform vise la consommation, pas le décodage.
read_terraform_outputs fait de l'E/S et un json.loads déjà protégé : le
fuzzer n'y mesurerait que la bibliothèque standard. Ce qui atteint l'utilisateur
en traceback, c'est ce que build_inventory fait du document décodé.

Le harnais m'a appris son propre contrat

Première exécution, sur la graine {} : InfraNotProvisioned. Ce n'est pas un
défaut, c'est l'exception dédiée au cas normal du premier lancement ou de
l'après-destroy, que la CLI rend en une phrase. Un harnais qui l'aurait comptée
comme un crash aurait réclamé de défaire exactement le patron que ce dépôt
applique. Elle rejoint donc les exceptions de contrat, avec la raison écrite.

Le corpus

Dix graines pour le contexte, neuf pour Terraform, chacune visant une forme
réelle
et non une bizarrerie inventée : la forme encapsulée de
terraform output -json et la forme aplatie, un bastion Outscale, un output
d'une autre version, un contexte complet tel que use l'écrit, une racine JSON
qui n'est pas un objet, une écriture interrompue.

Couverture mesurée : 66 arêtes pour le harnais Terraform, contre 73 pour le
harnais meta.yml existant. Il exerce donc bien du code, il ne se contente pas
d'échouer au décodage.

Type of change

  • Bug fix (l'AttributeError sur un output mal formé)
  • Tests / CI

Checklist

Always

  • uv run ruff check src/dsoxlab tests tests_e2e fuzz scripts — All checks passed!
  • uv run mypy src/dsoxlab — no issues in 60 source files
  • uv run pytest629 passed (620 avant, +9)
  • Domain-agnostic ; aucun chemin personnel
  • Les deux harnais joués localement à 200 000 exécutions, sans reproducteur

When behavior changes

  • CHANGELOG EN + FR ; version 0.1.64 ; uv.lock régénéré

When a command or option is added, removed or changed — N/A

When the declarative contract changes — N/A

When .github/workflows/ is touched

  • actionlint : 0 problème ; zizmor --persona=regular : No findings
  • Les deux étapes neuves au même budget que les existantes
    (-atheris_runs=20000 -max_len=4096), dans le job existant, donc sans
    allonger la porte de contribution
  • L'en-tête du job énumère les entrées couvertes et dit, pour chacune,
    pourquoi elle est considérée non fiable

Une exclusion de hook, et pourquoi elle n'est pas une facilité

Deux hooks d'hygiène ont refusé le corpus : check-json sur une graine tronquée,
end-of-file-fixer sur son absence de fin de ligne. Ils avaient raison, et
c'est le corpus qui est hors de leur périmètre : ce sont des graines
délibérément malformées, faites pour être données à un fuzzer. Un JSON coupé au
milieu d'une chaîne reproduit un portable refermé pendant l'écriture ; le
« réparer » retire au corpus le cas qu'il porte.

fuzz/corpus/ est donc exclu de check-json, check-yaml et
end-of-file-fixer, avec cette raison écrite dans la configuration. Aucun hook
de sécurité n'est touché : detect-private-key et trufflehog continuent de
voir ces fichiers.

Related issues

Closes #71

🤖 Generated with Claude Code

Le dépôt fuzzait les deux fichiers du contrat déclaratif, ce qui est la bonne
intuition mais pas la couverture complète : le contexte local et les outputs de
Terraform sont lus avec la même confiance, et n'étaient couverts par aucun
harnais.

Chaque entrée est non fiable pour une raison différente, et cette raison décide
du contrat que son harnais assère.

.dsoxlab-context.json vit sur le disque de l'apprenant : édité à la main,
tronqué par un portable refermé au mauvais moment, laissé par une version
ancienne. Son harnais n'a aucune exception de contrat, et c'est tout son propos :
read_context promet de rendre un contexte vide plutôt que de lever, parce que
perdre le contexte coûte un dsoxlab use là où une exception coûte la CLI entière.

Les outputs Terraform viennent d'un binaire externe dont la version, les
providers et le schéma de sortie bougent sans que dsoxlab le sache. Le harnais
vise ce que build_inventory fait du document, et non le json.loads qui le
précède : celui-là est déjà protégé, et le fuzzer n'y mesurerait que la
bibliothèque standard.

Il a trouvé un vrai défaut à sa troisième minute : {"hosts": {"value": "10.0.0.1"}}
faisait lever un AttributeError au moment de jouer un lab, sans jamais dire que
la cause était un state Terraform périmé. Corrigé, et figé par un test : le
fuzzing découvre, un test empêche le retour.

Le harnais m'a aussi appris son propre contrat : InfraNotProvisioned est une
exception attendue, celle du premier lancement ou de l'après-destroy, et un
harnais qui l'aurait comptée comme un crash aurait réclamé de la défaire.

Closes #71
@stephrobert stephrobert added this to the 0.2.0 — Productization milestone Aug 23, 2026
@stephrobert
stephrobert merged commit 6ed23ce into main Aug 23, 2026
18 checks passed
@stephrobert
stephrobert deleted the test/fuzz-entrees-non-fiables branch August 23, 2026 23:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P0] Étendre le fuzzing aux entrées non couvertes : contexte local et outputs Terraform

1 participant