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-bubblewrapest 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 simpleapple-containerreste inéligible parce qu'un workload root peut modifier son cgroup.
Ce qui est livré
| Fonction | apple-container | apple-container-bubblewrap |
|---|---|---|
| cible | OCI linux/arm64 ou linux/amd64 scellée | même cible avec Bubblewrap injecté par Warden |
| frontière externe | VM Apple Container | VM Apple Container |
| confinement intérieur | racine OCI read-only | user/PID/mount namespaces, procfs privé et vue Bubblewrap invitée |
| mappings VirtioFS / tmpfs | oui | oui |
| règles MIR READ/WRITE | non | récursives, ERROR/SKIP |
| DENY / PROTECT_CREATE | non | non |
réseau none | aucune attache réseau | même absence d'attache ; pas de netns intérieur revendiqué |
| réseau direct | oui, explicitement | oui, explicitement |
| réseau médié | proxy Egress via UDS/vsock | même pont, relay hors sandbox intérieur |
| frontend workload fd 4 | non | non |
| budget VM ABI 10 | non revendiqué | exact, service-attesté par inspect |
| cgroup workload ABI 10 | non protégé | exact, Warden-owned, observé et supprimé |
| receipts RunSpec ABI 10/v7 | non, 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 :
brew install maelys-dev/tap/maelys-wardenPour les cibles OCI, installez et configurez Apple Container 1.3.0, puis le driver optionnel :
brew install maelys-dev/tap/maelys-warden-apple-container-driverLe 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 :
TargetSpec OCI + plan scellé + DeploymentContext + SelectionPolicy
│
▼
TrustedRunSpec
│
plan + rapport des candidats
│
▼
artefact scellé one-shot
│
▼
spawnPlateforme 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 :
hôte plateforme de la cible
macOS / Apple Silicon -> linux/arm64L'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 :
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 traduitCette 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 :
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 helpermaelys-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 » :
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 fallbackCette 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 :
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
TargetSpecOCI, 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_CREATEou seccomp intérieur dans le composite 1.3.0 ; - pas de
PROCESS_TREE_LIFECYCLEannoncé ; linux/arm64natif etlinux/amd64traduit 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.