← Retour à tous les articles
Publié le · par Renaud Deraison

L'espace libre n'était pas vide

Le 24 septembre, Cloudflare et Oren Yomtov d'Accomplish ont publié une faille inter-locataires dans Cloudflare Containers et Sandboxes. Écrivez 4 Kio dans un sandbox neuf, lisez le disque brut, et les 60 Kio restants du bloc reviennent chargés des fichiers .env, des fichiers d'identifiants et des profils Chromium du locataire précédent. Rien ne s'est échappé de rien : toutes les frontières actives ont tenu, et la fuite était dans ce qu'il advenait d'un bloc une fois que son propriétaire en avait fini avec lui. Cloudflare vend Sandboxes pour exécuter du code généré par des modèles de langage. Bromure Agentic Coding exécute ce code depuis un fichier image sur votre propre Mac, et les blocs libres dans lesquels il puise sont les vôtres.

Un sandbox a deux missions. Contenir ce qui tourne dedans, et oublier ce qui y tournait avant. La seconde est une option de configuration.

Vous envoyez la suite de tests de l'agent dans un sandbox cloud, parce que faire tourner du code non relu sur votre portable a cessé de paraître malin il y a un an environ. Le sandbox démarre en une seconde avec un système de fichiers Linux complet. L'agent écrit un petit fichier. En dessous, la plateforme lui remet un bloc de disque de 64 kilo-octets, et 60 de ces kilo-octets contiennent encore le .env de quelqu'un d'autre.

Vous n'avez eu à vous échapper de rien pour arriver là.

Écrivez quatre kilo-octets, lisez-en soixante

Le 24 septembre, Cloudflare a publié un compte rendu conjoint avec les chercheurs sur une faille inter-locataires dans Cloudflare Containers. Oren Yomtov d'Accomplish l'a signalée le 4 septembre via HackerOne et a publié sa propre version la même semaine. The Hacker News l'a couverte le lendemain.

Containers donne à chaque client un disque tiré d'un pool partagé, via le provisionnement fin de device-mapper sous Linux, avec une taille de bloc de 64 Kio. Le pool portait l'option skip_block_zeroing, qui fait ce qu'elle annonce : dm-thin n'effaçait pas un bloc fraîchement alloué avant de le remettre à un conteneur. Quand un conteneur était supprimé, ses blocs retournaient au pool, contenu intact, et le pool s'étendait à cheval sur les comptes clients.

À partir de là, la technique tient en quatre étapes et aucun exploit :

  1. Trouver les régions alignées sur 64 Kio que le système de fichiers ext4 du conteneur lui-même croit libres.
  2. Écrire un unique bloc de 4 Kio dans chacune. Cela suffit à faire allouer par dm-thin un bloc physique de 64 Kio pris dans le pool partagé.
  3. Lire le périphérique brut plutôt que le système de fichiers.
  4. Garder les 60 Kio restants.

La phrase de Cloudflare pour décrire la primitive est le bug tout entier : « une écriture couvrant tout le bloc remplaçait son contenu précédent, mais une écriture plus petite ne modifiait que la portion écrite. »

Les blocs récupérés contenaient des structures exploitables. Cloudflare rapporte des « structures de répertoires, des pages de bases de données et des bases SQLite structurellement complètes », et compte 2 700 inodes de répertoires étrangers distincts sur six placements de production, avec du matériel résiduel sur 18 des 24 placements. The Hacker News rapporte des données récupérées sur 20 des 22 machines sous-jacentes, réparties sur quatre continents. Yomtov énumère ce qu'il a lu dans ces blocs : « des listings de répertoires, des bases SQLite, des profils Chromium, des fichiers .env et des fichiers d'identifiants. »

Cloudflare énonce deux limites qui bornent l'incident. Un attaquant « ne pouvait pas choisir une victime particulière ni accéder à un disque activement attaché » : vous tombiez donc sur qui avait été sur cette machine, pas sur une cible choisie. Et Cloudflare a construit des signatures de détection à partir de la preuve de concept des chercheurs, les a passées sur sa télémétrie historique d'activité disque, et n'a trouvé « aucune preuve que ce vecteur d'attaque précis ait été exploité par quelqu'un d'autre. »

Cloudflare a agi vite. Signalement à 15h26 UTC le 4 septembre ; mauvaise configuration de production confirmée à 18h45 ; correctif du runtime fusionné à 21h27 ; configuration du pool fusionnée à 22h03 ; déploiement lancé à 23h15. Tout cela en une seule journée. Le déploiement s'est achevé le 7 septembre, les chercheurs ont confirmé que leur preuve de concept était morte le 14 septembre, et Cloudflare a effacé les derniers instantanés disque antérieurs à la mitigation le 19 septembre. Les clients n'avaient aucune action à mener, et aucun moyen de vérifier depuis l'intérieur d'un sandbox si leurs propres blocs étaient arrivés propres.

Un pool de blocs de 64 Kio, deux clients, aucun effacementConteneur, client A.envidentifiantspages sqlite, profil chromiumsupprimé — blocs rendus au poolSandbox, client Bécrire 4 Kio dans l'espace librelire /dev/vdc, pas le fsgarde les 60 Kio restantsun compte Workers Paid, rien de plusPool dm-thin partagé — blocs de 64 Kio, à cheval sur les comptes clientsskip_block_zeroing — un bloc libéré est réalloué avec son contenu intactLe bloc remis au client B4 Kio écrits60 Kio de qui était là avantle fs appelle ça de l'espace libre ; le périphérique brut n'est pas d'accord
Une écriture de 4 Kio, un bloc de 64 Kio. Cloudflare Containers alloue le disque depuis un pool dm-thin partagé avec une taille de bloc de 64 Kio, et le pool était configuré avec skip_block_zeroing : un bloc libéré par un client retournait donc au pool avec son contenu intact. Écrire 4 Kio dans une région que le système de fichiers ext4 du conteneur croyait libre déclenchait l'allocation d'un de ces blocs ; lire le périphérique brut au lieu du système de fichiers renvoyait les 60 Kio restants. Les chercheurs ont compté 2 700 inodes de répertoires étrangers distincts sur six placements de production, et ce qu'ils y ont lu comprenait des fichiers .env, des fichiers d'identifiants, des bases SQLite et des profils Chromium.

Toutes les frontières actives ont tenu

Le titre du compte rendu de Yomtov, « Escaping the Cloudflare Sandbox », sous-vend à quel point ce bug est silencieux.

Il n'a trouvé aucun bug de sûreté mémoire, aucune élévation de privilèges et aucune évasion de conteneur. Toutes les frontières autour d'un conteneur en cours d'exécution ont tenu comme prévu pendant toute la durée de l'incident. Le client B ne pouvait pas lire le conteneur vivant du client A, et Cloudflare note qu'un sandbox ne pouvait pas non plus atteindre un disque activement attaché.

La fuite était dans le passage de relais. Cloudflare libérait un bloc d'un locataire et le remettait au suivant, et ce qui séparait ces deux clients était un booléen dans une configuration de pool.

Les chercheurs consacrent leur attention à ce que le code à l'intérieur d'un sandbox peut atteindre à l'instant présent, et bien moins à ce que le dernier occupant a laissé derrière lui. Cette seconde question a une réponse ennuyeuse la plupart du temps : la plateforme remet le bloc à zéro. La réponse tient jusqu'au jour où un ingénieur désactive l'effacement pour gagner en débit.

Accomplish compte celle-ci comme sa sixième évasion de sandbox publiée depuis juillet 2026. Les cinq précédentes : SharedRoot dans Claude Cowork, Beltdown dans Claude Code, Beltdown2 dans le CLI de Cursor, une dans le VMM de Docker, et une dans le sandbox Codex d'OpenAI. Cinq de ces six exécutent du code écrit par un agent. La conclusion de Yomtov tient en une ligne : « l'isolation peut échouer de bien des façons. »

C'est justement là que le code des agents devait aller

Cloudflare Sandboxes tourne sur Containers, et a donc hérité du bug. Sandboxes est aussi le produit que Cloudflare vend pour la chose dont ce blog n'arrête pas de parler. D'après la documentation de Cloudflare elle-même, le SDK existe pour « exécuter du code non fiable en toute sécurité dans des environnements isolés », il sert à « exécuter du code généré par de grands modèles de langage de façon sûre et fiable », et il convient aux « agents IA, assistants de code et systèmes autonomes qui ont besoin d'exécuter du code non fiable. » Chaque sandbox tourne « dans son propre conteneur isolé avec un environnement Linux complet, offrant de solides frontières de sécurité tout en maintenant les performances. »

Chaque proposition de cette phrase est une chose raisonnable à vouloir, et elle décrit à peu près aussi bien la VM de workspace Bromure qu'un Cloudflare Sandbox. La différence, c'est qui d'autre se trouve sur la machine.

Déplacer l'exécution des agents hors de votre portable relocalise une décision de confiance, elle ne la supprime pas. Vous faites désormais confiance à un allocateur de pool que vous ne voyez pas, dans un placement que vous n'avez pas choisi, à côté de locataires que personne ne vous nommera. Ce peut être le bon compromis. Faites-le exprès, et posez d'abord la question du résidu : d'où viennent mes blocs libres, et qu'y avait-il dedans.

Le disque est un fichier sur votre Mac

Bromure Agentic Coding répond à cette question en n'ayant pas de pool.

Chaque workspace correspond à exactement une VM. Deux workspaces ne partagent jamais une VM, et un workspace n'en a jamais deux. La VM démarre depuis deux disques virtuels qui sont des fichiers ordinaires dans le répertoire propre à ce workspace, sur votre Mac. disk.img est le disque système, créé au premier lancement comme un clone APFS en copie sur écriture de l'image de base Ubuntu locale : instantané, et gratuit jusqu'à ce que l'invité écrive. home.img est une image ext4 creuse et privée qui porte /home/ubuntu, attachée comme second périphérique virtio-blk.

La technique en quatre étapes n'a nulle part où atterrir ici. Un agent dans l'invité peut lire le périphérique bloc brut au lieu du système de fichiers, puisqu'il y est root, et c'est très bien. Il atteint un fichier image appartenant à un workspace. Les blocs dans lesquels ce fichier grandit viennent du volume APFS de votre propre Mac, et l'occupant précédent d'un bloc libre sur votre Mac, c'était vous.

Un seul objet est partagé entre les workspaces, et « rien n'est partagé » serait une affirmation fausse, alors nommons-le : chaque disque de workspace est cloné depuis base.img. Ubuntu 24.04, 24 Go logiques et 6 à 8 Go physiques, récupéré contre un catalogue d'images signé portant une signature ed25519 faite avec la même clé qui signe les mises à jour de l'app, couvrant l'identité de l'image, son SHA-256 et chaque commande de postinstall, avec protection contre le retour arrière et refus des catalogues non signés. Il est identique au bit près sur chaque installation et ne contient rien de vous. C'est le bon genre de chose à partager : un artefact public dont le métier est d'être identique partout.

Un placement multi-locatairesclient Aclient B…et tous les autresun pool de blocs de 64 Kioun bloc libéré est réattribué au suivantpropre à l'arrivée : un réglage, pas une garantiece qu'il y avait dans les blocs.env, fichiers d'identifiantsbases sqlite, profils chromium2 700 inodes de répertoires étrangers sur six placements ;résidu sur 18 sur 24. Aucune action client possible.Bromure Agentic Coding, sur votre Macbase.imgUbuntu 24.04, catalogue signé, identique partout, rien de vous dedansworkspace undisk.img — clone CoWhome.img — ext4 privéworkspace deuxdisk.img — clone CoWhome.img — ext4 privéce qu'il y a dans ces blocsghp_… et sk-ant-api03-brm-… — bouche-trousLes blocs libres viennent de votre volume. Les vrais secretssont dans le proxy hôte et sont injectés à la sortie.
Deux réponses à la même question. Sur un placement multi-locataires, le disque d'un sandbox est un bail sur des blocs pris dans un pool partagé avec des clients dont on ne vous parle pas, et le fait que ces blocs arrivent propres est une propriété de la configuration de la plateforme. Dans Bromure Agentic Coding, le disque système du workspace est un clone APFS en copie sur écriture d'une image de base publique et signée, identique au bit près, son home est une image ext4 creuse et privée, les deux sont des fichiers dans le répertoire propre à ce workspace, et les blocs libres dans lesquels ils grandissent viennent de votre propre volume. La seconde différence est ce qui s'écrit dans ces blocs : les artefacts récupérés dans le cas Cloudflare étaient des fichiers .env et d'identifiants, et dans un invité Bromure ceux-là contiennent des faux qui préservent la structure, tandis que les vraies valeurs restent dans le proxy hôte, sur le Mac.

Et le résidu ne vaudrait rien

La disposition des blocs est la moitié de la réponse. L'autre moitié est ce qu'un agent de code écrit sur le disque, et c'est ce qui rendait ce bug digne d'être signalé : des fichiers .env, des fichiers d'identifiants, des profils Chromium.

À l'intérieur d'un workspace Bromure, ces fichiers existent, et les outils qui les lisent fonctionnent comme d'habitude, mais les valeurs qu'ils contiennent sont fausses. Aucun fichier, aucune variable d'environnement, aucun processus dans la VM ne détient jamais une vraie clé d'API, un vrai jeton OAuth, un vrai secret AWS ou une vraie clé privée SSH. L'invité reçoit à la place des bouche-trous qui préservent la structure : sk-ant-api03-brm-…, un jeton GitHub à la forme ghp_, brm_…, déterministes par installation, pour que les validateurs côté client soient satisfaits et qu'aucun outil ne croie jamais qu'une clé a tourné entre deux sessions. Les vraies valeurs vivent dans la table de substitution en mémoire du proxy hôte, sur le Mac, chacune associée à l'hôte de destination auquel elle appartient, et sont injectées à la sortie avec une portée exacte-ou-sous-domaine, jamais par sous-chaîne. Les requêtes AWS sont re-signées côté hôte : l'invité signe avec un faux secret, l'hôte retire cette signature et re-signe. SSH passe par un pont vsock en signature seule, et les octets des clés privées ne franchissent jamais la frontière vers l'invité.

Bromure traite le profil Chromium séparément. Le volet navigateur que l'agent pilote tourne dans sa propre VM Alpine éphémère, un clone en copie sur écriture dans le répertoire temporaire que l'app supprime quand vous fermez le volet. Conserver les sessions est en opt-in, et cela place le profil sur un disque chiffré propre au workspace.

Si une faille future exposait les blocs d'un invité Bromure, le lecteur en repartirait avec une arborescence de répertoires, du code source que vous avez déjà, et un fichier d'identifiants qui ne peut être dépensé nulle part.

La jetabilité comme verbe

Les workspaces sont persistants à dessein. Le code a besoin d'état durable : dépôts clonés, caches de paquets, chaînes d'outils, historique du shell. Bromure contrôle donc ce que la VM a le droit de contenir plutôt que de la détruire chaque nuit, et effacer un disque est quelque chose que vous demandez. L'app est explicite sur chaque façon de le demander :

Réinitialiser à la base

Supprime disk.img pour que le lancement suivant re-clone une copie fraîche de l'image de base. La boîte de dialogue nomme ce qui disparaît : tout ce que vous avez installé avec apt install, et vos modifications de /etc, /var, /usr, /opt. Le home survit.

Effacer le home, supprimer le workspace

Effacer le home fait tomber /home/ubuntu et la paire de clés générée. Supprimer le workspace retire tout le répertoire de profil, les deux images, le fichier de secrets chiffré, chaque point de contrôle et le matériel SSH côté hôte, puis libère son adresse MAC réservée.

Jetables et points de contrôle

bromure-cli vm run --rm vous donne un workspace à la manière de docker, supprimé à l'arrêt de la VM. Chaque démarrage réussi laisse un point de contrôle en copie sur écriture du disque et du home, pris seulement après un démarrage ayant produit un terminal fonctionnel, donc chacun est un démarrage prouvé.

L'effacement forcé

Si le proxy attrape un bouche-trou d'identifiant partant vers un endroit où il n'a pas sa place, Bromure marque le workspace comme compromis, et il ne redémarrera pas avant que vous n'effaciez son disque et son home. Vous gardez vos réglages, vos jetons et vos clés SSH, et l'effacement ne touche jamais aux dossiers hôtes partagés.

Cet ensemble de verbes, c'est la différence entre un effacement que vous avez demandé et un effacement que vous avez supposé.

Demandez ce que le locataire précédent a laissé

Cloudflare a trouvé la mauvaise configuration, l'a corrigée en neuf heures, a purgé les pools et les instantanés, a construit des signatures de détection pour vérifier le passé, et a publié la taille de bloc et le nom du drapeau. Lisez cela comme un éditeur qui gère bien une mauvaise journée.

« Sandbox » nomme un espoir à propos d'une frontière, et cette frontière s'est avérée être un cycle de vie de ressource plutôt qu'un mur. La réponse de l'industrie aux agents qui exécutent du code non relu a été de déplacer l'exécution ailleurs et de faire confiance à cet ailleurs pour arriver propre. Il arrive propre, la plupart du temps. Vous ne pouvez pas le vérifier depuis l'intérieur du sandbox, ni cette semaine ni une autre.

Faites tourner le code sur une infrastructure que vous pouvez voir et vous pourrez répondre vous-même à la question du résidu : un fichier image par workspace, des blocs libres pris sur votre propre volume, un seul objet partagé et c'est une image de base publique signée, et rien sur ce disque qui vaille la peine d'être lu s'il fuyait un jour. L'effacer est une entrée de menu.