Backends

Apple Container backends

Run digest-pinned OCI workloads in Apple virtual machines, natively or through the Bubblewrap composite.

Warden conserve deux backends OCI optionnels fondés sur le service Apple Container sur Apple Silicon : apple-container et apple-container-bubblewrap. Ils utilisent Apple Container 1.3.0, où chaque conteneur s'exécute dans une VM Linux légère. Ils ne remplacent pas Seatbelt pour les exécutables macOS locaux.

Statut 0.40.0 / ABI 10 : apple-container-bubblewrap est sélectionnable par la CLI, la façade C et les SDK. Apple dimensionne exactement la VM ; le bootstrap invité Warden applique, observe et détruit le cgroup du workload, que Bubblewrap rend non modifiable. Le candidat simple apple-container reste inéligible parce qu'un workload root peut modifier son cgroup.

Ce qui est livré

Fonctionapple-containerapple-container-bubblewrap
cibleOCI linux/arm64 ou linux/amd64 scelléemême cible avec Bubblewrap injecté par Warden
frontière externeVM Apple ContainerVM Apple Container
confinement intérieurracine OCI read-onlyuser/PID/mount namespaces, procfs privé et vue Bubblewrap invitée
mappings VirtioFS / tmpfsouioui
règles MIR READ/WRITEnonrécursives, ERROR/SKIP
DENY / PROTECT_CREATEnonnon
réseau noneaucune attache réseaumême absence d'attache ; pas de netns intérieur revendiqué
réseau directoui, explicitementoui, explicitement
réseau médiéproxy Egress via UDS/vsockmême pont, relay hors sandbox intérieur
frontend workload fd 4nonnon
budget VM ABI 10non revendiquéexact, service-attesté par inspect
cgroup workload ABI 10non protégéexact, Warden-owned, observé et supprimé
receipts RunSpec ABI 10/v7non, candidat refuséoui

Les détails bit-à-bit sont dans la matrice des backends. La CLI, la façade C et les SDK Python/Node construisent tous un TrustedRunSpec ABI 10. Le budget élimine le candidat simple et rend le composite éligible lorsque son bundle invité est présent et que la forme du plan est représentable.

Le spike ressources physique distingue ce qui est impossible avec l'API épinglée de ce qui manque seulement dans Warden. Il prouve notamment que le candidat simple laisse root modifier cgroupfs et que le composite protège la hiérarchie. La 0.40 rend cette protection structurelle en réservant /sys/fs/cgroup avant d'annoncer les deux capacités de ressources.

Deux backends, pas deux modes implicites

Le planner ne transforme pas tardivement le candidat simple en composite : ce sont deux candidats explicites. maelys_executor_create_default() enregistre le composite seulement si le bundle invité Warden est présent, sûr et attestable. Une fois sélectionné, toute mutation du bundle ou tout échec du probe invité fait échouer le lancement ; il n'y a jamais de reprise avec le candidat simple.

Le bundle de confiance installé à côté du driver contient un Bubblewrap Linux/arm64 statique, maelys-guest-supervisor (relay L4 ou superviseur) et un probe amd64 minimal. Warden vérifie propriétaire, mode, architecture statique et SHA-256 au probe, à la préparation et au spawn. Le driver vérifie de nouveau les digests avant de les monter en lecture seule sur /run/maelys-warden, après les mappings utilisateur. Le bootstrap exécute enfin un préflight borné avec exactement la commande Bubblewrap qui lancera le workload ; la readiness n'est publiée qu'après cette preuve. L'image OCI ne fournit donc aucune partie du TCB Warden.

Ressources : service pour la VM, Warden pour le workload

Le budget ABI 10 sépare le dimensionnement de la VM de la comptabilité du workload. Le driver demande au service Apple N vCPU avec un overhead CPU nul et VM memory - 128 MiB; la version Containerization épinglée ajoute son overhead mémoire fixe de 128 MiB. Une relecture inspect doit retrouver exactement ces trois valeurs, sinon le lancement échoue avant READY.

Dans l'invité, le bootstrap Warden construit ensuite une hiérarchie distincte :

Avant d'activer les contrôleurs cgroup-v2, le bootstrap déplace le plan de contrôle pré-workload dans .maelys-supervisor; cette étape respecte la règle « no internal process » de cgroup v2. Le fils rejoint .maelys-workload avant execve(bwrap). À la fin, Warden borne l'arrêt, mesure memory.peak, cpu.stat, pids.peak et memory.events, détruit le groupe, remet le PID 1 dans le domaine suivi par le service et écrit un rapport privé MWAR/2.

Le répertoire de rapport appartient à l'exécution, est 0700 sur l'hôte et monté après tous les mounts utilisateur. Le fichier est créé une seule fois en 0600; le driver vérifie type, propriétaire, mode, nombre de liens, taille et framing avant de produire EXIT. Un arrêt forcé détruisant la VM constitue la barrière de cleanup et produit une observation synthétique explicitement nulle.

Une règle WRITE ou un mount utilisateur intersectant /sys/fs/cgroup est refusé deux fois, dans le backend C puis dans le driver. La vue récursive read-only de Bubblewrap devient ainsi une propriété du candidat, et non un effet accidentel de l'ordre des mounts.

Composants et frontière C/Swift

Warden ne lie ni Swift, ni Virtualization.framework, ni les bibliothèques Apple dans son ABI C. Le driver externe possède la traduction vers ContainerAPIClient. Le backend C possède la validation, le planner, l'artefact scellé, les délais, la supervision et les receipts.

La façade produit utilise un délai de readiness borné à 180 secondes pour une VM froide, puis 2 secondes d'arrêt gracieux et 3 secondes d'arrêt forcé. Ces valeurs sont distinctes du défaut local de 1 seconde et peuvent être resserrées par l'API C ; aucune attente n'est illimitée.

Le fd 4 de ce schéma est créé pour le processus driver hôte : Warden duplique l'extrémité enfant d'un socketpair sur l'entrée numéro 4 de la table de descripteurs du driver. Il transporte MWDP/1 et n'est jamais exposé au workload invité.

Sur une exécution native distincte, Warden peut aussi installer un canal réseau sur l'entrée numéro 4 de la table de descripteurs du workload. Le même nombre ne signifie pas le même objet : chaque processus possède sa propre table. Ce frontend workload n'est pas livré à travers la VM Apple. Voir warden-driver-protocol.md pour les trois espaces et le protocole commun Apple/Krun.

Installation et disponibilité

Le paquet Warden de base suffit pour Seatbelt :

CODE
brew install maelys-dev/tap/maelys-warden

Pour les cibles OCI, installez et configurez Apple Container 1.3.0, puis le driver optionnel :

CODE
brew install maelys-dev/tap/maelys-warden-apple-container-driver

Le driver de release est un binaire arm64 ad-hoc signé. Xcode et Swift ne sont requis que pour le compiler depuis les sources ; ils ne sont pas requis pour installer ou exécuter l'artefact publié. Consultez le guide d'installation.

Si le driver n'est pas installé, create_default() continue avec Seatbelt et aucun candidat OCI Apple n'est disponible. Si le driver est présent mais Apple Container 1.3.0 est absent ou non configuré, son probe échoue avec MAELYS_EXECUTOR_ERR_UNSUPPORTED. Une demande Apple explicite ne retombe jamais sur Seatbelt ou POSIX.

Contrat RunSpec ABI 10

La séquence publique est volontairement explicite :

CODE
TargetSpec OCI + plan scellé + DeploymentContext + SelectionPolicy
                              │
                              ▼
                      TrustedRunSpec
                              │
                  plan + rapport des candidats
                              │
                              ▼
                    artefact scellé one-shot
                              │
                              ▼
                            spawn

Plateforme OCI

La plateforme identifie le système et l'architecture dans l'invité, pas ceux du processus Warden hôte. Elle sert notamment à choisir la bonne variante d'une image OCI multi-architecture et entre dans le digest du TargetSpec :

CODE
hôte                     plateforme de la cible
macOS / Apple Silicon -> linux/arm64

L'API générique TargetSpec reconnaît actuellement linux/arm64 et linux/amd64. Apple Container sait exécuter la seconde variante dans une VM Linux arm64 grâce à la traduction Intel de Virtualization.framework. Dans la version 1.3 épinglée, Utility.containerConfigFromFlags active déjà config.rosetta lorsque l'hôte est arm64 et que la plateforme demandée est amd64.

Le probe MAELYS-APPLE-PROBE/6 atteste explicitement control=mwdp/1, launch=6, resource-contract=abi10-warden-cgroup pour le composite et inner-procfs=private, puis rapporte séparément la plateforme native, les plateformes traduites et l'identité stable du traducteur. Le planner compare cette preuve au TargetSpec; linux/amd64 est refusé avant prepare si Rosetta n'est pas attestée. L'identité de traduction est copiée dans l'artefact et revalidée au spawn. Une mutation exige une nouvelle sélection.

La CLI accepte --platform linux/arm64|linux/amd64. L'omission est permise uniquement si le DeploymentContext n'observe qu'une plateforme native sans ambiguïté ; elle ne signifie jamais « activer l'émulation si nécessaire ».

Pour apple-container-bubblewrap, la forme recommandée reste un Bubblewrap Linux/arm64 statique appartenant à Warden : il s'exécute nativement dans le noyau invité arm64, construit les namespaces, puis lance le workload amd64 que le binfmt Rosetta traduit. Le relay Egress invité suit la même règle. Ainsi, les composants de confiance Warden ne passent pas eux-mêmes par la traduction.

Procfs privé du composite

Apple Container 1.3.0 peut laisser des chemins OCI masqués ou read-only sous /proc. Un montage procfs depuis un user namespace serait alors refusé par le noyau comme trop révélateur. Le driver ne dépend pas des valeurs par défaut du CLI : il construit explicitement maskedPaths: [] et readonlyPaths: []. Bubblewrap peut ensuite conserver --unshare-user, créer son PID namespace et monter --proc /proc sans recevoir de CAP_SYS_ADMIN ambiant dans le user namespace extérieur. Comme pour tout montage rootless, Bubblewrap possède temporairement l'autorité d'administration limitée au user namespace qu'il vient de créer ; le workload final ne conserve aucune capability :

Le montage est une condition d'admission : son échec empêche l'exécution du workload, sans reprise avec le procfs extérieur. Cette propriété améliore la vue et l'isolation des processus ; elle n'implémente pas par elle-même les règles filesystem DENY/PROTECT_CREATE ni un filtre seccomp intérieur.

Architecture cible arm64 native, workload amd64 traduit

La plateforme du TargetSpec décrit le workload OCI. Elle ne change pas l'architecture des composants de confiance fournis par Warden. Sur Apple Silicon, le driver hôte, le noyau invité, Bubblewrap et le relay restent arm64 ; seul le processus issu de l'image linux/amd64 passe par Rosetta :

CODE
Mac Apple Silicon
└── Warden arm64
    └── Apple Container driver arm64
        └── VM Linux avec noyau arm64
            └── bwrap statique arm64
                ├── crée namespaces et mounts
                ├── applique l’isolation
                └── execve(workload amd64)
                         │
                         ▼
                 Linux reconnaît ELF amd64
                         │
                         ▼
                   binfmt_misc
                         │
                         ▼
                 Rosetta for Linux
                         │
                         ▼
                workload amd64 traduit

Cette composition fonctionne parce que l'architecture du processus appelant n'impose pas celle de la nouvelle image passée à execve(2). Bubblewrap établit d'abord l'isolation avec les appels système natifs arm64. Le noyau reconnaît ensuite l'ELF amd64 et invoque l'interpréteur enregistré par binfmt_misc. Apple Container enregistre Rosetta avec le flag F, qui garde l'interpréteur ouvert et disponible malgré les mount namespaces et les changements de racine créés par Bubblewrap.

Références normatives : images multiplateformes Apple Container et contrat Linux binfmt_misc.

Les helpers invités doivent être des ELF Linux/arm64 statiques. Une image OCI amd64 ne contient normalement ni loader ni bibliothèques arm64 ; un helper dynamique arm64 ne serait donc pas autonome. Le paquet cible contient un seul bundle de confiance utilisable pour les deux variantes de workload :

CODE
libexec/maelys-warden/guest/linux-arm64/
├── bwrap
├── maelys-init
├── maelys-guest-supervisor
├── probe-amd64
└── manifest.json
    ├── architecture = linux/arm64
    ├── mode et propriétaire attendus
    └── SHA-256 de chaque helper

maelys-init et maelys-guest-supervisor restent deux ELF aux rôles distincts mais lient le même libmaelys-guest-isolation.a. Le premier possède PID 1 sur les drivers directs ; le second est l'entrypoint supervisé par vminitd sur le service Apple.

Compiler ces helpers en amd64 serait techniquement possible une fois Rosetta active, mais introduirait une dépendance inutile à la traduction dans le TCB : un workload arm64 ne pourrait plus démarrer sans Rosetta, l'isolation serait elle-même traduite et deux bundles seraient nécessaires. Une distribution macOS Universal 2 de Warden reste possible ; sur Apple Silicon, sa tranche arm64 et le driver arm64 doivent être utilisés.

Le support livré n'installe et n'autorise jamais Rosetta pendant spawn. Une preuve absente, malformée ou non stable rend le candidat traduit inéligible. Les gates physiques couvrent natif/traduit, simple/composite et proxy Egress.

Sélection automatique et backend épinglé

Sans contrainte de backend, le planner compare tous les candidats enregistrés qui acceptent TargetSpec OCI. La MIR élimine ceux qui n'ont pas les capacités requises ; la SelectionPolicy départage ensuite les candidats restants.

Une option CLI --backend NAME doit signifier autoriser exactement ce backend, et non « essayer celui-ci puis choisir autre chose » :

CODE
aucun --backend
    -> le budget élimine apple-container
    -> le planner peut choisir apple-container-bubblewrap ou un backend direct

--backend apple-container
    -> seul le candidat simple est autorisé
    -> RESOURCE_GUEST_CGROUP manque : échec avant RUN

--backend apple-container-bubblewrap
    -> seul le composite est autorisé
    -> driver/helper/probe indisponible : échec, aucun fallback

Cette option sert à la reproductibilité, aux diagnostics et aux déploiements qui imposent une technologie précise. L'usage normal doit laisser le planner choisir à partir des garanties demandées.

Extrait C pour le composite :

CODE
const char *command[] = {"/bin/sh", "-c", "echo hello"};

maelys_executor_create(&executor, &error);
maelys_executor_register_apple_container_bubblewrap(
    executor,
    "/opt/homebrew/opt/maelys-warden-apple-container-driver/libexec/"
    "maelys-warden-apple-container-driver",
    &error);

maelys_executor_plan_create(&plan, &error);
maelys_executor_plan_set_decision_digest(
    plan, "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    &error);
maelys_executor_plan_set_network(
    plan, MAELYS_EXECUTOR_NETWORK_NONE, &error);
maelys_executor_plan_seal(plan, &error);

maelys_target_spec_create_oci(
    "ghcr.io/acme/tool",
    "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "linux/arm64", 3, command, "/", &target, &error);

maelys_deployment_context_create("macos", "arm64", &deployment, &error);
maelys_deployment_context_observe_registered_candidate(
    deployment, executor, "apple-container-bubblewrap", 0, 700, &error);
maelys_deployment_context_seal(deployment, &error);

maelys_selection_policy_create(&selection, &error);
maelys_selection_policy_allow_candidate(
    selection, "apple-container-bubblewrap", 100, &error);
maelys_selection_policy_set_vm_preference(
    selection, MAELYS_VM_REQUIRE, &error);
maelys_selection_policy_seal(selection, &error);

maelys_trusted_runspec_create(
    target, plan, deployment, selection, &runspec, &error);

maelys_execution_request_create(&request, &error);
maelys_execution_request_apply_target(request, target, &error);
maelys_execution_request_set_environment_mode(
    request, MAELYS_EXEC_ENV_EMPTY, &error);
maelys_execution_request_set_io_mode(
    request, MAELYS_EXEC_IO_CAPTURED_STDIO, -1, &error);
maelys_execution_request_seal(request, &error);

maelys_executor_prepare_runspec(
    executor, runspec, request, &artifact, &report, &error);
maelys_run_artifact_spawn(artifact, &execution, &error);

Le code de production doit vérifier chaque résultat, afficher le rapport des candidats en cas de refus, attendre ou arrêter l'exécution, puis libérer tous les handles. Le test physique tests/test_apple_container_e2e.c constitue l'exemple complet de lifecycle et de preuve Executor ABI 10 ; la façade et la CLI sérialisent cette preuve en receipt JSON v7.

Pour le composite, enregistrez et autorisez apple-container-bubblewrap, puis ajoutez seulement des règles filesystem READ/WRITE récursives avec MISSING_ERROR ou MISSING_SKIP. Le bundle invité Warden doit être installé ; l'image OCI n'a pas besoin de contenir Bubblewrap.

Réseau Egress à travers la VM

SCM_RIGHTS ne traverse pas deux noyaux. Le frontend Apple livré cible donc les programmes HTTP/SOCKS standards :

Le driver monte le helper invité et la socket privée après les mappings utilisateur, attend MWGB-READY/1, puis seulement admet le workload. En mode médié, la VM n'a pas de réseau ambiant. Dans le composite, le relay reste à l'extérieur du Bubblewrap intérieur ; le workload n'atteint que son port loopback.

Depuis 0.25.0, cette route AF_UNIX privée ne transporte aucun credential : un principal Egress immuable est lié à l'identité du listener, dont le backend épingle device et inode avant de les revalider au spawn ; la validation du pair de même EUID reste une condition additive. Les URLs proxy injectées dans le workload sont donc sans userinfo, et Egress applique un quota de connexions par principal.

Le chemin fd4/vsock décrit dans d'anciens schémas est un objectif futur. Il nécessiterait un broker invité qui crée un stream local et utilise SCM_RIGHTS seulement dans le noyau invité, puis relaie les octets sur vsock jusqu'au même moteur Egress hôte.

Lifecycle et receipts v7

Le contrôle du driver est événementiel : HELLO, LAUNCH, READY, SIGNAL, STOP, ACK et ERROR. Warden impose les délais et utilise un SIGKILL hôte uniquement en dernier recours contre un driver bloqué. La mort du parent ferme le canal privé ; le driver arrête et supprime alors le conteneur.

Le receipt OCI v7 lie :

  • le digest MIR/décision et le plan mécanique ;
  • le TargetSpec OCI, son image digest et sa plateforme ;
  • le TrustedRunSpec, l'artefact et la preuve du driver ;
  • le candidat sélectionné et les refus des autres candidats ;
  • l'identité du backend et ses couches ordonnées : VM, Bubblewrap éventuel et traduction éventuelle ;
  • le masque de capacités ABI 10 et les noms dérivés de ce masque ;
  • le frontend réseau et le résultat connu.

Dans v3/v4, names est dérivé de mask selon le registre ABI déclaré ; un désaccord invalide le receipt. Le masque effectif dans selection est la garantie normative du candidat. Les contributions de couches en sont des sous-ensembles attribuables et ne reconstruisent pas nécessairement une garantie produite par leur composition ; la traduction contribue zéro. Les plateformes restent structurelles : source: linux/amd64 et destination: linux/arm64, jamais un faux token OCI composé. Le format v2 reste gelé et n'est jamais réinterprété.

Il ne contient ni credential Egress, ni token vsock, ni chemin temporaire privé, ni environnement brut. Voir le schéma JSON v4. Les exemples Apple v3 restent disponibles comme golden files gelés ; v4 conserve leurs couches et ajoute uniquement la fermeture runtime lorsqu'une couche Krun existe. Le schéma v5 reste le dernier format historique antérieur au budget obligatoire. Les schémas v6 et v7 exigent la preuve ABI 10 de budget. Le candidat simple ne peut donc en émettre aucun ; depuis 0.40, le composite émet v7 avec ses limites demandées/appliquées, les observations MWAR/2, la preuve de cleanup et les compteurs Egress lorsqu'il est médié.

Limites explicites

  • pas de cible locale convertie automatiquement en OCI ;
  • pas de frontend réseau workload fd 4 à travers la VM ;
  • pas de règles filesystem dans le candidat simple ;
  • pas de DENY, PROTECT_CREATE ou seccomp intérieur dans le composite 1.3.0 ;
  • pas de PROCESS_TREE_LIFECYCLE annoncé ;
  • linux/arm64 natif et linux/amd64 traduit sont couverts par les gates ;
  • les tests physiques macOS 26 restent nécessaires : une CI hébergée sans Virtualization.framework utilisable ne prouve pas le confinement VM.