From f317c81abd08da537f0aae1860ae583bf966a0e8 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Tue, 1 Sep 2026 03:02:11 +0500 Subject: [PATCH] fix(image): let bwrap actually create its sandbox, and prove it as the job user The b19 conformance measurement came back "cannot run this local phase without network isolation" on a worker that carried /usr/bin/bwrap. The binary was never the capability: Ubuntu 24.04 ships kernel.apparmor_restrict_unprivileged_userns=1, so an unprivileged bwrap dies at "setting up uid map: Permission denied" -- inside every worker, docker-capable or not, because security.nesting does not lift a kernel-wide userns restriction. The image now carries Ubuntu's own answer for exactly this case: an AppArmor profile granting `userns,` to /usr/bin/bwrap alone. The restriction stays in force for everything else on the worker, untrusted pools are not widened, and the host sysctl is untouched. Provisioning loads the profile and runs the consumer's own probe as the runner; both smokes repeat it at boot, so a binary-present-capability-absent image can never pass again. Aliases move to b20/b13 (and the dormant VM manifests follow), because a recipe that produces different bytes must not build under a promoted name. Claude-Session: https://claude.ai/code/session_01LsGid6U5RrQdFvJmvYdGCF --- config/golden-image-container-integration.yaml | 2 +- config/golden-image-container.yaml | 2 +- config/golden-image-integration.yaml | 2 +- config/golden-image.yaml | 2 +- internal/garmbootstrap/types.go | 10 +++++----- internal/imagebuild/assets/provision.sh | 18 ++++++++++++++++++ .../imagebuild/assets/smoke-integration.sh | 5 +++++ internal/imagebuild/assets/smoke.sh | 5 +++++ internal/imagebuild/orchestrator_test.go | 2 +- internal/imagemanifest/manifest_test.go | 2 +- 10 files changed, 39 insertions(+), 11 deletions(-) diff --git a/config/golden-image-container-integration.yaml b/config/golden-image-container-integration.yaml index 653ad3fd..bc03c9cd 100644 --- a/config/golden-image-container-integration.yaml +++ b/config/golden-image-container-integration.yaml @@ -2,7 +2,7 @@ schema_version: 1 image: type: container - alias: nddev-u24-amd64-ctr-docker-runner-2.336.0-r20260801-b12 + alias: nddev-u24-amd64-ctr-docker-runner-2.336.0-r20260801-b13 current_alias: nddev-ubuntu-24.04-amd64-container-docker-current previous_alias: nddev-ubuntu-24.04-amd64-container-docker-previous source_alias: nddev-source-ubuntu-24.04-amd64-container-r20260801 diff --git a/config/golden-image-container.yaml b/config/golden-image-container.yaml index f3757efb..7018e098 100644 --- a/config/golden-image-container.yaml +++ b/config/golden-image-container.yaml @@ -2,7 +2,7 @@ schema_version: 1 image: type: container - alias: nddev-ubuntu-24.04-amd64-container-runner-2.336.0-r20260801-b19 + alias: nddev-ubuntu-24.04-amd64-container-runner-2.336.0-r20260801-b20 current_alias: nddev-ubuntu-24.04-amd64-container-current previous_alias: nddev-ubuntu-24.04-amd64-container-previous source_alias: nddev-source-ubuntu-24.04-amd64-container-r20260801 diff --git a/config/golden-image-integration.yaml b/config/golden-image-integration.yaml index bbd78eba..0c12a31d 100644 --- a/config/golden-image-integration.yaml +++ b/config/golden-image-integration.yaml @@ -1,7 +1,7 @@ schema_version: 1 image: - alias: nddev-ubuntu-24.04-amd64-docker-runner-2.336.0-r20260801-b15 + alias: nddev-ubuntu-24.04-amd64-docker-runner-2.336.0-r20260801-b16 current_alias: nddev-ubuntu-24.04-amd64-docker-current previous_alias: nddev-ubuntu-24.04-amd64-docker-previous source_alias: nddev-source-ubuntu-24.04-amd64-r20260801 diff --git a/config/golden-image.yaml b/config/golden-image.yaml index 7c7a7e78..160e6c0a 100644 --- a/config/golden-image.yaml +++ b/config/golden-image.yaml @@ -1,7 +1,7 @@ schema_version: 1 image: - alias: nddev-ubuntu-24.04-amd64-runner-2.336.0-r20260801-b16 + alias: nddev-ubuntu-24.04-amd64-runner-2.336.0-r20260801-b17 current_alias: nddev-ubuntu-24.04-amd64-current previous_alias: nddev-ubuntu-24.04-amd64-previous source_alias: nddev-source-ubuntu-24.04-amd64-r20260801 diff --git a/internal/garmbootstrap/types.go b/internal/garmbootstrap/types.go index 6469b6d0..d07f701b 100644 --- a/internal/garmbootstrap/types.go +++ b/internal/garmbootstrap/types.go @@ -36,17 +36,17 @@ const ( DefaultPoolBalancerType = "roundrobin" DefaultProviderName = "nddev-incus" DefaultImage = "nddev-ubuntu-24.04-amd64-container-current" - IntegrationImage = "nddev-u24-amd64-ctr-docker-runner-2.336.0-r20260801-b11" - PriorityStandardImage = "nddev-ubuntu-24.04-amd64-container-runner-2.336.0-r20260801-b18" - PriorityIntegrationImage = "nddev-u24-amd64-ctr-docker-runner-2.336.0-r20260801-b11" + IntegrationImage = "nddev-u24-amd64-ctr-docker-runner-2.336.0-r20260801-b13" + PriorityStandardImage = "nddev-ubuntu-24.04-amd64-container-runner-2.336.0-r20260801-b20" + PriorityIntegrationImage = "nddev-u24-amd64-ctr-docker-runner-2.336.0-r20260801-b13" // Every Linux class is an ephemeral Incus container. Docker-capable classes // use their nested-runtime image; release uses a separately stage-smoked // standard image so OIDC authority does not inherit Docker/nesting. FastImage = ContainerCanaryImage UntrustedImage = IntegrationImage - ReleaseImage = "nddev-ubuntu-24.04-amd64-container-runner-2.336.0-r20260801-b18" + ReleaseImage = "nddev-ubuntu-24.04-amd64-container-runner-2.336.0-r20260801-b20" ContainerCanaryImage = "nddev-ubuntu-24.04-amd64-container-current" - DockerContainerCanaryImage = "nddev-u24-amd64-ctr-docker-runner-2.336.0-r20260801-b11" + DockerContainerCanaryImage = "nddev-u24-amd64-ctr-docker-runner-2.336.0-r20260801-b13" DefaultFlavor = "nddev-linux-standard" IntegrationFlavor = "nddev-linux-integration" FastFlavor = "nddev-linux-fast" diff --git a/internal/imagebuild/assets/provision.sh b/internal/imagebuild/assets/provision.sh index 4a281ac6..b122f4dd 100644 --- a/internal/imagebuild/assets/provision.sh +++ b/internal/imagebuild/assets/provision.sh @@ -44,6 +44,24 @@ ln -sfn /usr/bin/pip3 /usr/local/bin/pip3 python --version >/dev/null pip --version >/dev/null +# bubblewrap is on the image for consumers that need a network isolator, but +# a binary on disk is not a capability: Ubuntu 24.04 ships +# kernel.apparmor_restrict_unprivileged_userns=1, and without an AppArmor +# profile granting userns, bwrap dies at "setting up uid map" for every +# unprivileged caller -- measured on a live worker while the conformance +# consumer read it as "no isolator". This is Ubuntu's own mechanism for +# exactly this case; the restriction stays in force for everything else. +cat > /etc/apparmor.d/bwrap-userns <<'APPARMOR' +abi , +include +profile bwrap /usr/bin/bwrap flags=(unconfined) { + userns, +} +APPARMOR +chmod 0644 /etc/apparmor.d/bwrap-userns +apparmor_parser --replace /etc/apparmor.d/bwrap-userns +runuser -u runner -- env HOME=/home/runner bwrap --ro-bind / / true + systemctl disable --now apt-daily.timer apt-daily-upgrade.timer unattended-upgrades.service 2>/dev/null || true git lfs install --system groupadd --force docker diff --git a/internal/imagebuild/assets/smoke-integration.sh b/internal/imagebuild/assets/smoke-integration.sh index 8dbaafe2..d139f643 100644 --- a/internal/imagebuild/assets/smoke-integration.sh +++ b/internal/imagebuild/assets/smoke-integration.sh @@ -160,6 +160,11 @@ for smoke_toolchain in "${smoke_toolchain_names[@]}"; do esac done +# bubblewrap must be able to actually create its sandbox as the job user; the +# binary being present has already lied about this once. +test -f /etc/apparmor.d/bwrap-userns +runuser -u runner -- env HOME=/home/runner bwrap --ro-bind / / true + # Browser bytes are qualification input, not image content. Launch the pinned # Chrome-for-Testing archive against the baked OS libraries as the unprivileged # runner, then let cleanup delete the entire extracted tree and profile. diff --git a/internal/imagebuild/assets/smoke.sh b/internal/imagebuild/assets/smoke.sh index 95b3bac0..26a573b6 100644 --- a/internal/imagebuild/assets/smoke.sh +++ b/internal/imagebuild/assets/smoke.sh @@ -147,6 +147,11 @@ for smoke_toolchain in "${smoke_toolchain_names[@]}"; do esac done +# bubblewrap must be able to actually create its sandbox as the job user; the +# binary being present has already lied about this once. +test -f /etc/apparmor.d/bwrap-userns +runuser -u runner -- env HOME=/home/runner bwrap --ro-bind / / true + python --version >/dev/null python3 --version >/dev/null python3 -m pip --version >/dev/null diff --git a/internal/imagebuild/orchestrator_test.go b/internal/imagebuild/orchestrator_test.go index af4bd095..ede8f26d 100644 --- a/internal/imagebuild/orchestrator_test.go +++ b/internal/imagebuild/orchestrator_test.go @@ -164,7 +164,7 @@ func TestRecipeFingerprintIsDeterministic(t *testing.T) { // alias is part of the recipe, so a manifest whose contents changed under an // unchanged alias would otherwise ask the builder to produce different bytes // for a name that is already promoted. - if first != "sha256:522a61fbf8567198ac81be2a0b6ffe5cdbc1a7cdf5e8beec13d415db470ab83e" { + if first != "sha256:835c7ed10be62b3ac1cca36572c08b8661b0df5451f1b594bbb4ce7cf19f9ebf" { t.Fatalf("deployed standard recipe fingerprint drifted: %q", first) } smoke, err := SmokeFingerprint(plan) diff --git a/internal/imagemanifest/manifest_test.go b/internal/imagemanifest/manifest_test.go index c05f1c03..fbfefeef 100644 --- a/internal/imagemanifest/manifest_test.go +++ b/internal/imagemanifest/manifest_test.go @@ -42,7 +42,7 @@ func TestRepositoryManifestIsValidAndPinned(t *testing.T) { if !strings.HasPrefix(fingerprint, "sha256:") || len(fingerprint) != len("sha256:")+64 { t.Fatalf("unexpected fingerprint %q", fingerprint) } - if fingerprint != "sha256:d7fdc01a61f2afc9991ef658511ea3cf73f73c251d0043c04c392f06b6397d1a" { + if fingerprint != "sha256:4c76a6c764a529923aa4dcd0327863aa43a4700b3a5e5d99aa30426a7db96217" { t.Fatalf("standard manifest fingerprint drifted: %q", fingerprint) } }