Hub Python Library documentation

Les sandboxes en coulisses

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Les sandboxes en coulisses

Ce guide explique le fonctionnement interne de Sandboxes, les raisons de leur conception ainsi que leurs limites. Si vous souhaitez simplement utiliser des sandboxes, le guide Sandboxes suffit ; poursuivez la lecture si vous voulez comprendre le mécanisme, évaluer le modèle de confiance ou déboguer un problème.

L'API Sandbox est expérimentale. Son API et son comportement peuvent changer sans préavis.

Il n’existe pas de « service sandbox »

La première chose à comprendre est qu’il n’existe pas de backend de sandbox dédié. Une sandbox n’est qu’un HF Job (une VM) exécutant un petit binaire statique unique sbx-server qui communique en HTTP. Le client dialogue avec ce serveur via le proxy des Jobs (l’URL *.hf.jobs exposée par le Job). Tout le reste — l’authentification, la découverte, le regroupement de nombreuses sandboxes dans un seul Job — est construit à partir des primitives existantes des Jobs : étiquettes, variables d’environnement et secrets.

Cette conception « sans nouvelle infrastructure » est la raison pour laquelle un sandbox fonctionne dans n’importe quelle image Docker et hérite gratuitement de la facturation, des types de matériel et des autorisations d’espace de noms des Jobs.

Initialisation du serveur

Au démarrage du job, la commande du Job est un petit script /bin/sh -c qui récupère le binaire sbx-server, le rend exécutable, puis l’exécute via exec. Le binaire est une compilation statique musl d’environ 640 Ko, sans aucune dépendance d’exécution, ce qui lui permet de s’exécuter dans n’importe quelle image Linux x86_64.

Quelques décisions à souligner :

  • Téléchargement, avec repli sur le montage. Le chemin rapide télécharge le binaire depuis le CDN HF avec wget ou curl (toute image de base courante en fournit un), ce qui est rapide et gratuit. En filet de sécurité, le dépôt Hub du serveur est également monté en volume sur chaque job : si l’image ne contient ni wget ni curl, le script copie le binaire depuis ce montage. Le montage est transparent — il ne coûte rien s’il n’est pas réellement lu — mais sa lecture ajoute environ 2 à 3 s au démarrage à froid, raison pour laquelle ce n’est pas le comportement par défaut. La seule exigence stricte sur l’image est /bin/sh.
  • Un serveur HTTP/1.1 fait maison, sans framework. Le streaming de la sortie en direct nécessite de vider le tampon (flush) de chaque fragment dès sa production. Les serveurs HTTP Rust minimaux courants (par ex. tiny_http) mettent en tampon les réponses par fragments jusqu'à la fin de la réponse, ce qui casse le streaming. Le serveur implémente donc HTTP/1.1 à la main : des flux d'événements NDJSON pour exec, des corps bruts pour les fichiers, et un flush explicite par fragment.
  • Port 49983. Le serveur écoute sur un port volontairement peu courant afin de laisser les ports de développement courants (3000, 8000, 8080, …) libres pour votre propre code.

Le serveur est open source sur github.com/huggingface/sandbox-server.

L'authentification est sans état

Deux couches indépendantes protègent un bac à sable :

  1. La passerelle du proxy. Le proxy des Jobs ne transmet que les requêtes portant un jeton HF avec un accès en lecture à l’espace de noms du job. Un membre aléatoire d’Internet ne peut pas accéder à l’URL.
  2. La porte d’entrée de l’application. sbx-server vérifie en outre l’en-tête X-Sandbox-Token sur chaque requête, à l’exception de /health. Il s’agit d’une défense en profondeur : un membre du namespace en lecture seule qui peut atteindre le proxy ne peut toujours pas exécuter de commandes.

Le jeton de bac à sable est dérivé, non stocké :

nonce  = random 128-bit hex                       # stored in the job label "hf-sandbox-nonce"
token  = HMAC-SHA256(key=your_hf_token, msg="hf-sandbox:" + nonce)

Le nonce est public et la clé HMAC est votre jeton HF, de sorte que n'importe quelle machine détenant ce jeton peut recalculer le jeton du bac à sable — c'est précisément ce qui rend la reconnexion sans état. Cela signifie également que le jeton est une capacité dérivée de votre identifiant, et non une vérification d'identité de l'appelant : le serveur vérifie que l'appelant connaît le jeton, et non qui il est.

Chaque tâche de sandbox porte également deux étiquettes stables pour la découverte — hf-sandbox=1 (sur toutes) et hf-sandbox-mode=dedicated ou hf-sandbox-mode=pool — ce qui permet de les lister ou de les filtrer côté serveur, par exemple avec hf jobs ps --label hf-sandbox=1.

Le jeton est transmis au serveur via un secret de Job. Le client le re-dérive à la demande à partir du nonce public dans l'étiquette. Cela a quelques conséquences avantageuses :

  • Reconnexion sans état. Sandbox.connect(id) fonctionne depuis n'importe quelle machine disposant du même jeton HF — lisez le nonce depuis l'étiquette, recomputez le jeton. Aucun fichier local, aucun état à copier.
  • Le jeton HF n’est pas transmis au bac à sable en tant que variable d’environnement ou secret de tâche (sauf si vous optez pour forward_hf_token=True). Il ne s’agit pas d’une garantie absolue que vos identifiants restent inaccessibles : le processus qui écoute sur le port du bac à sable est celui que l’image démarre en premier, de sorte qu’une image non fiable peut être en mesure d’observer les requêtes envoyées par le client — y compris leur en-tête Authorization. Considérez les identifiants accessibles depuis un bac à sable comme potentiellement exposés à celui-ci.

Portée du jeton

Un nonce est émis par Job, le jeton dérivé est donc une job credential. En mode dédié, le job est le bac à sable, les deux coïncident donc. En mode pool, le job est l'hôte, et un hôte héberge de nombreux bacs à sable — un bac à sable en pool reçoit donc un second jeton, plus restreint, qui lui est propre :

credentialcomment dérivéautorise
jeton de bac à sable dédiéHMAC(hf_token, nonce) à partir de l'étiquette du jobcette sandbox (qui est l'ensemble du travail)
jeton d'hôte du poolHMAC(hf_token, nonce) à partir de l'étiquette du travail hôtegestion du pool : créer, lister et supprimer des sandboxes sur cet hôte, et récupérer leurs jetons
jeton de bac à sable agrégé256 bits aléatoires, générés par le serveur hôte pour chaque bac à sablecette sandbox — ni un pair, ni le pool

Le client utilise automatiquement le plus restreint : pool.create() le reçoit dans la réponse de création, Sandbox.connect("<host>.<id>") le récupère avec le jeton de l’hôte, et proxy_headers distribue le jeton du bac à sable plutôt que celui de l’hôte (en résolvant le jeton bearer HF au moment de la lecture, afin qu’un handle de longue durée ne distribue pas un jeton périmé) — ces en-têtes finissent généralement dans un navigateur ou un client WebSocket, ils doivent donc accorder l’accès à un seul bac à sable et à rien d’autre.

Ce que cela implique en cas de fuite : un jeton de bac à sable mutualisé compromet ce bac à sable. Un jeton d’hôte compromet l’hôte — tous les bacs à sable qui y sont exécutés, actuels et futurs, ainsi que ses routes de gestion — il doit donc être traité comme l’identifiant administrateur du pool. Les membres de votre espace de noms disposent d’un jeton HF différent et ne peuvent pas dériver le vôtre, mais consultez Limitations connues pour comprendre comment un jeton peut être transmis à un endroit inapproprié.

Le client exige un jeton non vide, limité au périmètre du bac à sable, pour les opérations mutualisées et refuse les hôtes qui n’en fournissent pas. Il ne remplace jamais les identifiants de gestion de l’hôte. Mettez à jour le client et recyclez les hôtes de pool plus anciens avant d’utiliser cette API.

Sables dédiés ( Sandbox.create )

Le modèle le plus simple : un Job par bac à sable. Un Job est une véritable VM, ce qui offre l’isolation la plus forte (au niveau VM), prend en charge tout type de matériel, y compris les GPU, et constitue le bon choix pour du code mutuellement non fiable. Ses routes API se trouvent sous /v1/* et les chemins de fichiers sont absolus sur le système de fichiers du conteneur. kill() annule simplement le Job.

La limite qu’une sandbox dédiée vous offre se situe entre le Job et tout ce qui se trouve à l’extérieur. À l’intérieur de la VM, votre code et sbx-server s’exécutent tous deux en tant que root dans le même conteneur : sbx-server ne sandboxe pas la commande qu’il exécute pour vous, il ne fait que l’exposer via HTTP. Ainsi, dès qu’un code non fiable a été exécuté dans une sandbox dédiée, considérez cette VM — y compris son plan de contrôle — comme appartenant à ce code : il peut tuer ou remplacer le serveur, et tout ce que vous faites ensuite dans cette sandbox s’exécute à sa merci. Utilisez une sandbox dédiée par charge de travail non fiable et kill()-la une fois terminé, plutôt que de la réutiliser à travers différentes frontières de confiance.

Le coût est clairement visible dans le diagramme : chaque sandbox subit un démarrage à froid complet de la VM d’environ 6 s et facture une machine entière. Pour un sandbox unique ou une charge de travail GPU, c’est exactement ce que vous voulez. Pour 100 à 1000 tâches CPU courtes, c’est gaspillé — c’est précisément à quoi servent les pools.

Pools : de nombreuses sandboxes dans un seul Job ( SandboxPool )

Les pools sont destinés aux charges de travail au sein d’une même frontière de confiance. Un sandbox poolisé est un uid + un ensemble de règles Landlock au sein d’une VM partagée, et non une VM dédiée, et son hôte exécute un plan de contrôle privilégié auquel tous ses sandboxes se connectent. Utilisez les pools pour déployer à moindre coût votre propre code. Pour des charges de travail mutuellement méfiantes — tout ce que vous ne permettriez pas de lire les fichiers de vos autres sandboxes — utilisez Sandbox.create(), qui attribue à chaque charge de travail sa propre VM. Consultez Limitations connues pour les lacunes spécifiques.

Un rollout RL typique ou un bac à sable d’exécution d’outils nécessite quelques Mo de RAM et un cœur pendant quelques secondes. Payer une VM 2 vCPU et un démarrage à froid de 6 s pour chacun — et déclencher une rafale de planification de 1000 VM — est un mauvais compromis. Ainsi, SandboxPool exécute un seul Job en tant qu’hôte et multiplexe de nombreux bacs à sable à l’intérieur.

Un bac à sable mutualisé n'est ni une machine virtuelle imbriquée ni un conteneur. C'est le primitif multi-utilisateurs classique de l'Unix :

  • un uid dédié (≥ 20000),
  • un dossier personnel 0700 appartenant à cet uid,
  • les commandes execuées avec cet uid dans un environnement assaini (env_clear, afin que les secrets de l'hôte ne fuient jamais), NO_NEW_PRIVS, des rlimits par processus et un ensemble de règles Landlock par bac à sable.

La création d'un bac à sable est donc mkdir + chown + build ruleset ≈ 1 ms côté serveur — sans redémarrage d'une seconde VM. La seule latence visible côté client est l'aller-retour du proxy.

L’identifiant public d’un bac à sable mutualisé est <host_job_id>.<local_id>, de sorte que connect/exec/kill fonctionnent de manière sans état, tout comme pour les bacs à sable dédiés. kill() sur un bac à sable mutualisé envoie une requête DELETE à son hôte (libérant ainsi un emplacement) ; l’hôte continue de fonctionner.

Isolation dans un pool : uid + Landlock

C'est le cœur de la conception du pool, il est donc important d'être précis sur ce qui est isolé et ce qui ne l'est pas.

Un Job standard s’exécute en tant que root dans un espace de noms utilisateur qui ne mappe que les uids 0..65535, avec un filtre seccomp activé et sans CAP_SYS_ADMIN / CAP_NET_ADMIN / CAP_NET_RAW. Cela exclut les outils d’isolation lourds habituels : pas d’espaces de noms imbriqués, pas de nouveaux montages, pas de délégation de cgroup (unshare, mount, l’écriture dans /sys/fs/cgroup/... échouent tous). Ce que le noyau propose est Landlock (ABI 6), un module de sécurité Linux qui permet à tout processus non privilégié de se restreindre lui-même ainsi que ses enfants — exactement la frontière par bac à sable dont nous avons besoin. Pour chaque bac à sable, le serveur construit un ensemble de règles ; l’enfant exécuté passe à l’uid/gid du bac à sable et applique NO_NEW_PRIVS, landlock_restrict_self et les rlimits avant d’exécuter la commande.

La combinaison d’uid distincts (contrôle d’accès discrétionnaire) avec Landlock est conçue et testée pour fournir :

  • ✅ A ne peut pas lire l'environ d'un autre processus, ce qui empêche l'accès direct aux jetons HF et sandbox entre les sandbox.
  • ✅ A ne peut pas SIGKILL / ptrace / lire la mémoire des processus de B, setuid en B, ou lire le dossier personnel 0700 de B.
  • ✅ L'accès à /tmp et /dev/shm est refusé — chaque bac à sable est confiné par Landlock à son propre répertoire personnel (son TMPDIR pointe à l'intérieur de $HOME).
  • ✅ A ne peut pas bind un port TCP, il n'y a donc pas de service localhost inter-sandbox (la connect sortante reste autorisée, l'internet fonctionne donc).
  • ✅ Les sockets unix abstraits inter-sandboxes sont bloqués (LANDLOCK_SCOPED_ABSTRACT_UNIX_SOCKET ; l'isolation par uid seule ne les bloque pas).

Ces contrôles contraignent la charge de travail. Le plan de contrôle racine reste une frontière de confiance distincte : ses opérations de fichiers en mode hôte refusent les liens symboliques, son proxy de socket fixe l'inœud de la socket et vérifie l'uid de l'émetteur, et les routes dédiées ne sont pas disponibles en mode hôte. Les canaux partagés résiduels sont décrits dans Limitations connues.

Pourquoi ce n'est pas un substitut à une VM. L'isolation Landlock et par uid est destinée aux charges de travail au sein de la même frontière de confiance. Étant donné que les sandboxes mutualisées partagent un noyau, une VM et un plan de contrôle, la protection contre toutes les attaques inter-sandboxes n'est pas garantie ; les ressources et certaines métadonnées de la liste des processus restent également partagées. Pour du code mutuellement non fiable, ou pour le GPU, utilisez Sandbox.create(), qui attribue à chaque sandbox sa propre VM.

Le fichier modèle dans un pool

Étant donné que la seule zone accessible en écriture d’un bac à sable mutualisé est son répertoire personnel confiné par Landlock (qui est aussi son répertoire de travail par défaut), l’API fichier enracine tous les chemins dans ce répertoire : files.write("data/in.txt", ...) écrit dans $HOME/data/in.txt, un / initial est interprété relativement au répertoire personnel, et .. ne permet pas de s’en échapper. Les fichiers écrits via l’API sont chownés à l’uid du bac à sable afin que son propre code puisse les lire. Cela offre un modèle propre de « système de fichiers enraciné dans le bac à sable » qui correspond exactement à ce que le code à l’intérieur du bac à sable peut toucher — et diffère des bacs à sable dédiés, où les chemins sont absolus sur le système de fichiers du conteneur.

Les chemins sont normalisés lexicalement, puis résolus par rapport à un descripteur de répertoire personnel ouvert, avec un refus des liens symboliques sur chaque composante. L'API de fichiers ne suit pas les liens symboliques, y compris les liens au sein du même répertoire personnel ; le code de la sandbox peut néanmoins les utiliser sous ses règles Landlock.

Les pools n'ont pas d'état local autoritaire

Un pool n’est pas volontairement un fichier de configuration local. Un pool est l’ensemble de ses Jobs d’hôtes en cours d’exécution, tous partageant l’étiquette hf-sandbox-pool=<id>. Cela permet de maintenir les pools cohérents avec le reste de l’API sandbox (tout est découvrable à partir des étiquettes et rattachable depuis n’importe quelle machine), et cela signifie qu’un pool cesse tout simplement d’exister dès que son dernier hôte a disparu.

  • Un hôte porte la configuration du pool (image, saveur, sandboxes_per_host, délai d’inactivité) dans les variables d’environnement de son job — les étiquettes ne servent qu’au filtrage. Lorsqu’un client doit démarrer un hôte en double, il relit cette configuration depuis un hôte en cours d’exécution (inspect_job), de sorte que tous les hôtes d’un pool restent cohérents sans enregistrement central.
  • Les variables d’environnement et les secrets sont propres à chaque bac à sable et sont passés lors de sa création — jamais au niveau du pool. L’environnement d’un bac à sable en pool est conservé en mémoire du serveur hôte pendant toute la durée de vie du bac à sable (il est réappliqué à chaque commande exécutée) ; il n’est jamais écrit sur le disque et n’apparaît jamais dans les métadonnées d’un Job. Il n’existe pas de canal de secrets chiffrés pour les bacs à sable en pool — utilisez env et considérez ces valeurs comme « non au repos, mais non chiffrées non plus ». Pour les valeurs nécessitant le stockage chiffré, utilisez les secrets d’un bac à sable dédié.
  • La capacité est autoritaire côté serveur. Un hôte refuse les créations au-delà de sandboxes_per_host (en répondant {"rejected": N}) ; le client répartit le dépassement sur un autre hôte ou démarre un doublon. Cela garantit un remplissage exact, même lorsque plusieurs processus créent des éléments dans le même pool de manière concurrente.
  • L’éviction en état d’attente est à deux niveaux. Chaque bac à sable est évincé après son propre idle_timeout d’inactivité (sauf s’il contient encore un processus en cours d’exécution) ; une fois qu’un hôte n’a plus de bac à sable pendant la durée du délai d’attente d’inactivité de l’hôte, il s’éteint — un filet de sécurité de facturation, même si tous les clients disparaissent.

Un cache de type « best-effort » permet de maintenir la rapidité de create --pool

L’absence d’état local faisant autorité est excellente pour la justesse, mais elle a un coût en latence. Un hf sandbox create --pool <id> à froid (un nouveau processus CLI) devrait autrement tout redécouvrir via le réseau avant de pouvoir créer un bac à sable : list_jobs pour balayer l’espace de noms → inspect_job pour chaque hôte afin de reconstruire son URL et son jeton → GET /v1/sandboxes pour voir le taux de remplissage de chacun → enfin POST pour créer. Plusieurs allers-retours de surcharge pure, à chaque appel.

Un cache de type « best-effort » situé dans $HF_HOME/sandbox/pools/<context>/<pool-id>.json supprime ce problème. Après toute opération de création ou de mise en chauffe, un processus enregistre la configuration du pool ainsi que, pour chaque hôte, son URL de proxy, son jeton d’authentification et les derniers slots libres observés. Le processus suivant reconstruit le transport de l’hôte directement à partir du fichier (sans HTTP) et passe directement à la requête POST.

<context> est un condensé de l’endpoint, de l’identifiant et de l’espace de noms pour lesquels l’entrée a été écrite — l’identifiant sous forme d’empreinte ; votre jeton lui-même n’est jamais écrit dans le cache. À la fois le nom du répertoire et le fichier le contiennent, et une lecture qui ne correspond pas aux trois est un simple échec de cache, de sorte qu’un autre utilisateur de la machine, un autre endpoint, ou un connect(pool, namespace="other-org") obtient le chemin froid au lieu des hôtes qui avaient été mis en cache pour autre chose. Les fichiers sont créés en 0600, les répertoires en 0700.

Ce pour quoi le cache est et n'est pas fiable :

  • Non autoritaire en matière de capacité. Le serveur dans le job reste autoritaire, de sorte qu'un comptage live obsolète ne coûte qu'une requête gaspillée, jamais un hôte mal rempli.
  • Non fiable pour déterminer l’emplacement d’un hôte. Une URL mise en cache n’est utilisée que si elle correspond à l’URL du proxy de tâches HTTPS de la tâche désignée par la même entrée, de sorte qu’un fichier de cache ne peut pas choisir la destination vers laquelle vos jetons bearer HF et de sandbox sont envoyés ; toute entrée désignant autre chose est supprimée du fichier. Au-delà de 15 minutes, une entrée n’est plus prise en compte de manière autonome : inspect_job doit confirmer que la tâche est toujours en cours d’exécution, toujours étiquetée pour ce pool et toujours sur cette URL avant que le transport ne soit construit.
  • Non fiable en raison de sa forme. Les types et les plages sont vérifiés à la lecture, de sorte qu'un fichier modifié manuellement, tronqué ou de type chaîne est un échec de cache plutôt qu'une erreur en cours d'exécution de create().
  • Auto-réparation. Un hôte mis en cache qui a disparu est supprimé dès la première requête échouée et retiré du fichier ; la création bascule alors transparentement sur la découverte par étiquettes.
  • Concurrentiel. Les écritures sont fusionnées sous un verrou de fichier (clé job_id) et validées de manière atomique, de sorte que les processus create parallèles ne s'écrasent pas mutuellement et que les lecteurs ne voient jamais un fichier partiellement écrit.
  • Jetable. Supprimez-le et vous obtenez un échec de cache : le chemin froid s’exécute et tout fonctionne toujours. Il n’est jamais partagé entre les machines.

Deux points à garder en tête. Traitez $HF_HOME comme sensible : une entrée reste une URL d’hôte réutilisable plus le nonce public dont son jeton est dérivé, et bien qu’un fichier écrit sous un autre identifiant soit ignoré, celui écrit sous le vôtre est crédité pour la fenêtre de 15 minutes décrite ci-dessus. Et comme l’espace de noms fait partie de la clé, un pool mis en cache sous un namespace= explicite ne sera retrouvé que si le même namespace= est passé — hf sandbox create --pool <id> exige le même --namespace que celui utilisé pour créer le pool, sinon il emprunte le chemin froid et cherche dans votre propre espace de noms.

Limitations connues

Les pools sont destinés aux charges de travail au sein d'une même frontière de confiance. Utilisez des sandbox dédiées pour le code que vous ne pouvez pas faire confiance. Les vérifications ci-dessous réduisent des risques spécifiques ; elles ne transforment pas les sandbox mutualisées en VM distinctes.

Vue d'ensemble du modèle de menace

ActeurDédiéPool
Utilisateur anonymeLe proxy Jobs nécessite un jeton HF avec un accès en lecture à l'espace de noms.Idem.
Membre de l'espace de noms avec accès en lecturePeut atteindre le proxy et la vérification publique de l’état de santé, mais a besoin du jeton de bac à sable pour les opérations API.Idem.
Membre de l'espace de noms pouvant créer des JobsImpossible de dériver le jeton d'un autre utilisateur à partir de son nonce.Il est possible de copier les étiquettes du pool, mais l’adoption par défaut vérifie l’initiateur de l’API Jobs. adopt_hosts="namespace" permet de faire confiance à d’autres créateurs.
Code exécuté dans le bac à sablePartage la racine avec le serveur à l'intérieur de la VM ; ne réutilisez pas cette VM à travers des frontières de confiance.Confiné par l'uid et Landlock, mais partage le noyau, la VM et le plan de contrôle privilégié.
Votre clientContient les identifiants HF et les identifiants du bac à sable dédié.Contient les identifiants de gestion de l'hôte et les capacités distinctes par bac à sable.

Isolation et lacunes en matière de ressources

  • La découverte d’hôtes n’est pas une attestation. La valeur par défaut adopt_hosts="own" vérifie l’initiateur, l’image, la saveur, la commande et l’URL exposée. Elle n’atteste pas le binaire en cours d’exécution ni ne rend une image mutable digne de confiance. N’utilisez adopt_hosts="namespace" qu’avec des membres de l’espace de noms que vous faites confiance.
  • Les identifiants de l’hôte restent puissants. Ils gèrent le pool et récupèrent le jeton de chaque bac à sable. Ce client ne les substitue jamais aux jetons d’étendue manquants. Les hôtes démarrés par cette version exécutent sbx-server 0.7.0, qui les refuse sur les routes d’étendue ; les hôtes démarrés par des clients plus anciens conservent leur serveur (et sa compatibilité avec le jeton d’hôte) jusqu’à leur recyclage.
  • Les canaux partagés subsistent. La sortie TCP, l’accès en boucle à l’ serveur de contrôle, l’UDP, l’IPC du noyau et les métadonnées lisibles de la liste des processus ne sont pas isolés. Les répertoires système et certains nœuds de périphériques restent accessibles. L’isolation du pool GPU n’a pas été testée ; utilisez le mode dédié pour le GPU.
  • Les limites ne sont pas des quotas agrégés. Les rlimits par processus et les files de sortie bornées réduisent l’amplification des ressources. Elles ne partitionnent pas les parts de CPU, la mémoire totale, l’espace disque, les inodes ou la capacité réseau entre les sandbox. La pagination des répertoires borne les réponses, mais le répertoire est tout de même matérialisé sur le serveur. Les écritures de fichiers via l’API ne constituent pas un quota disque, et les écritures en mode dédié vers des fichiers spéciaux peuvent bloquer.
  • Le confinement peut être explicitement affaibli. Le mode hôte exige par défaut l’ABI Landlock 6 et refuse un ensemble de règles en échec. Abaisser SBX_MIN_LANDLOCK_ABI ou utiliser le drapeau de développement --allow-unconfined du serveur accepte moins de garanties ; inspectez /health authentifié pour l’ABI et les fonctionnalités effectives.
  • Les connexions par proxy sont authentifiées une seule fois. Après la première requête, le proxy copie les octets vers ce backend jusqu'à la fin du flux (EOF). Les requêtes HTTP ultérieures sur cette connexion ne sont pas authentifiées ni routées de manière indépendante. La compatibilité WebSocket/SSE n'établit pas de sécurité contre la réutilisation de la connexion amont.

Cycle de vie et informations d’identification

  • Les descendants détachés peuvent survivre à la fin du processus ou à une expiration de délai. Les deux envoient un signal au groupe de processus ; un descendant qui appelle setsid() s’échappe de ce groupe. Supprimez le bac à sable mis en pool ou terminez le Job dédié pour mettre fin à l’ensemble de la charge de travail.
  • max_hosts est appliqué sur une base « best-effort ». Des clients distincts peuvent compter les mêmes hôtes et chacun provisionner davantage. Une admission par le backend ou une idempotence est nécessaire pour imposer une limite partagée stricte.
  • La démolition peut échouer. Les annulations d’hôtes échouées lèvent et conservent les enregistrements du cache. close() limite l’attente des créations en cours ; une création qui dépasse cette durée d’attente peut laisser un Job facturable. Utilisez l’API Jobs ou le CLI pour réconcilier les ressources en cours d’exécution.
  • La rotation des jetons Bearer ne modifie pas les capacités du bac à sable. L’authentification du proxy résout le jeton HF actuel pour chaque requête, mais la reconnexion sans état dérive un jeton d’hôte à partir de la valeur Bearer d’origine exacte. La rotation de cette valeur peut empêcher la reconnexion aux hôtes existants.
  • Le cache local reste une entrée de confiance pendant 15 minutes. Son URL et son contexte de sécurité sont vérifiés, mais il ne dispose pas de MAC d’intégrité ; protégez $HF_HOME contre les écritures par du code non fiable.
  • Un épinglage par empreinte n’est pas une provenance signée. Le client vérifie un binaire serveur spécifique. La publication et la mise à jour de cet épinglage restent des étapes de publication ; le manifeste du flux de travail n’est pas une attestation cryptographique. La soupape de sécurité explicite SBX_ALLOW_UNVERIFIED_SERVER=1 contourne la vérification uniquement lorsque les deux outils de hachage sont absents.

L'endpoint /health non authentifié expose uniquement l'état de vie et le protocole. Les métadonnées détaillées nécessitent les identifiants de l'hôte. Les identifiants de sandbox et les jetons requièrent la source aléatoire du noyau ; la réutilisation des UID suit le nettoyage vérifié du processus et du répertoire personnel.

Performance

Tous les chiffres sont mesurés par rapport à des Jobs HF réels sur cpu-basic, avec le client sur un ordinateur portable et tout le trafic passant par le proxy des Jobs.

Sandbox dédié :

métriquevalue
démarrage à froid (create() retourne, le serveur répond)~5,8 s médiane
aller-retour de run()p50 ~110ms (le plancher de RTT du proxy est ~105ms ; surcharge client ≈ 0)
transfert de fichiers (parallèle par plages, >8 Mio)~340 Mo/s en réception, ~441 Mo/s en émission

Pool (mode partagé/hôte) :

N sandboxeshôtes (50/hôte)provision + créer toutexec dans toustuer touttotal
10026.1s1.5s0.6s8.2s
1000207.4s4.2s4.2s15.8s

1000 sandboxes créés, exécutés et supprimés en ~16 s coûtent environ un démarrage à froid d’un hôte (~6 s) amorti sur l’ensemble — soit 0,0009 $ au total (20 × cpu-basic), contre ~0,06 $ et une rafale de planification de 1000 VM pour un Job par sandbox. La création, l’exécution et la suppression côté serveur prennent chacune ~1 ms ; le budget est entièrement constitué par l’aller-retour réseau.

Décisions de conception, récapitulatif

décisionpourquoi
Construire sur Jobs, aucun nouveau servicehérite de la facturation, du matériel et des autorisations ; fonctionne dans n'importe quelle image
Binaire Rust statique, téléchargé au démarrageaucun Python/pip ; démarrage à froid d'environ 6 s contre 30 à 90 s pour un bootstrap basé sur pip
HTTP/1.1 fait mainles frameworks minimaux tamponnent les réponses par lots et interrompent le streaming en direct (vérifié)
Authentification HMAC sans étatreconnecter depuis n'importe où ; un jeton dérivé plutôt que le jeton HF lui-même (portée par tâche — voir Portée du jeton)
run() lève une exception en cas de code de sortie non nul (check=False pour désactiver ce comportement)meilleure DX pour les boucles « exécuter le code, voir l'erreur » (style E2B)
idle_timeout watchdog plutôt que le nettoyage côté clientles sandboxes persistantes sont une fonctionnalité ; celles qui fuient meurent quand même
Pools = uid + Landlock, capacité autoritaire côté serveur, aucun état localdiffusion rapide aux mêmes utilisateurs ; correcte en cas de concurrence ; réattachable n'importe où
Update on GitHub