Build the run-phase image in CI and hold that exact image to the suites - #20
Merged
MarkusPaulsen merged 3 commits intoSep 11, 2026
Merged
Conversation
The run-phase image is the artefact: it is where both C products are compiled and what an exercise runs in. Nothing built it, so the acceptance suites had only ever run by hand, and the claim they exist to make, that the sandbox works in an ordinary container, rested on someone having run them. The image is built and exercised in one job. Loading an image populates one runner's Docker daemon and no other, so a separate acceptance job would quietly build a second image and test that instead. Three things had to be repaired before any of it could run. The unit runner built its instrumented copy in a directory it removed on the way out, so asking for coverage and then looking for the data found nothing; --coverage now takes a directory and keeps it. The run-phase compose file pointed at a context holding no Dockerfile and no file its COPY lines match. And the python prune image could not build at all: Ubuntu 26.04 ships a PEP 668 Python and pip stops with externally-managed-environment. The acceptance suites now run with no network at all. The phase test compiled online, because the probe POM named no compiler plugin version and Maven's default is 3.13.0 while the image carries only 3.14.0; naming it lets every Maven call run offline. A network failure can no longer arrive dressed as a Landlock regression. Nothing is mounted over /root/.m2 either. The base image ships a populated repository and a cache volume mounted there hides it, which makes the unrestricted clean steps fail for want of a plugin. The build context is assembled by one script that both CI and the acceptance README call, so the recipe cannot drift from the Dockerfile again. It refuses a destination it did not create and clears one it did. The weekly run builds with no cache and pulls its base images, since a cache hit is exactly what would hide the drift it exists to find.
added 2 commits
September 10, 2026 05:57
The Build workflow never started. This organisation allows only actions it owns and actions GitHub publishes, so docker/setup-buildx-action and docker/build-push-action were refused before any job ran, whatever their pinned commit. Every workflow already here uses actions/* alone, which is why nothing had run into this before. Plain docker build does the same work: buildx is on the runner, --pull and --no-cache carry the weekly freshness the cache backend would have undermined anyway, and the run-phase image still lands in this runner's own daemon, so the suites still exercise the image the job just built. The matrix values reach the shell through the environment rather than by interpolation, which is the shape to avoid having anywhere, safe values or not. Lint and the template check also filter their pull_request trigger on main, so neither ran on a pull request stacked on the feature branch. Both now name that branch too, with the same note to remove it once the branch has landed.
The unit suite wraps open and close through the linker, which is how it injects syscall failures. The gcov runtime writes its .gcda files with those same calls, so an instrumented run has gcov failing to write and reporting so on stderr. That output breaks the test asserting that a run which goes through says nothing, and it means any figure would have come from a perturbed run. It passed locally and failed in CI, which is the worst shape for a check to have. Removed rather than silenced: tests/unit/mutation.sh already measures this suite, and it measures the thing coverage only gestures at, namely whether a test would notice the code being wrong. The --coverage directory argument stays. It fixes a real defect, that the instrumented build was deleted before anything could read it, and it is still what someone investigating locally would reach for.
MarkusPaulsen
merged commit Sep 11, 2026
e4d7e16
into
feat/landlock-instead-of-bubblewrap
15 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Builds the run-phase image in CI and runs the three Landlock acceptance suites against that exact image, in an ordinary container with no security flags and no network. The claim this branch is built on stops being something a reviewer takes on trust. Repairs three things that stood in the way, including a prune image that could not build at all, and adds unit tests, coverage and a weekly drift run.
Linked issues
Stacked on #8, and targets its branch rather than main.
1. Problem
This branch's central claim is that the filesystem sandbox now works inside a stock unprivileged container. Nothing in CI built the image or ran a suite, so the claim rested on someone having run them by hand and reported the numbers.
Three defects stood in the way. The unit runner built its instrumented copy in a directory it removed on the way out, so asking for coverage and then looking for the data found nothing. The run-phase compose file pointed at a context holding no Dockerfile and no file its
COPYlines match, so it could not build. And the python prune image could not build either: Ubuntu 26.04 ships a PEP 668 Python andpip3 installstops withexternally-managed-environment.The phase test also reached the network. Its probe POM named no compiler plugin version, so Maven asked for its default 3.13.0 while the image carries only 3.14.0. A registry outage could therefore have arrived looking like a sandbox regression.
2. Improvement from the user's perspective
No Improvement from the user's perspective.
3. Improvement from the maintainer's perspective
A reviewer no longer has to take the acceptance numbers on trust, or build the image by hand to check them. The suites run on every pull request against the image the same job just built, and a failure names which of the three it was.
The build context is assembled by one script that both CI and the acceptance README call. That recipe had already drifted once, when the wrapper was split into modules and the README kept listing a single
.cfile, and this is what stops it happening again. The script refuses a destination it did not create and clears one it did, so a stale file from an earlier context cannot be built in by a wildcard.The mutation suite in this branch runs weekly rather than never. There is deliberately no coverage step: the unit suite interposes
openandcloseto inject failures, which is also how gcov writes its data, so an instrumented run breaks the test asserting that a clean run says nothing.--coverage DIRstill keeps the build for measuring by hand.4. Testing manual
Prerequisites
--privileged,--cap-addor--security-opt.gccandpython3for the unit and coverage steps.Steps
.github/scripts/assemble-run-phase-context.sh /tmp/ctx && docker build -f docker/run_phase/java/Dockerfile -t phobos-landlock:test /tmp/ctxdocker run --rm --network none -v "$PWD/tests/landlock-acceptance:/testsuite:ro" phobos-landlock:test bash /testsuite/run-tests.sh, then the same forextra-tests.shandphase-test.sh.bash tests/unit/run.shand thenbash tests/unit/run.sh --coverage /tmp/cov.for d in c java python orchestrate; do docker build -t "prune-$d" "docker/prune_phase/$d"; done.Expected result
bestanden: 11, fehlgeschlagen: 0, then15, 0, then13, 0. All three with--network none, so nothing in them depends on a registry being reachable.134 passed, 0 failed./tmp/covthen holds the.gcnoand.gcdafiles, which is the point: before this branch that directory was deleted before anything could read it.pythonis the one to watch: on this branch's parent it stops withexternally-managed-environment.Negative case (what must still be rejected)
The suites carry their own negative half and it is the reason they are worth running: a path outside the allow-list stays unreadable and unwritable, a write into a read-only tree is refused, and both hold for a child process and for a second JVM, as root and as a non-root user.
extra-tests.shalso reads the forbidden file without the sandbox, so a denial cannot be a file permission wearing Landlock's clothes.For this change specifically, the guard that must not be weakened is the container invocation. Adding
--privileged,--cap-addor--security-optto any of the three would make them pass while proving nothing, which is why the workflow spells all three out in a comment.One honest limit:
--network noneguarantees the suites pass without a network, but it does not police the-oflags. Maven falls back to the local repository when it cannot reach a registry, so a removed-ostill passes. The flag pins the property, not the spelling.Layers exercised
run-tests.shexercises all three in one run: denied and permitted paths, denied and permitted endpoints, and the timeout terminating a JVM that blocksSIGTERM.5. Test case coverage regarding this PR
tests/landlock-acceptance/run-tests.shtests/landlock-acceptance/extra-tests.shtests/landlock-acceptance/phase-test.shtests/unit/run.shtests/network_cache_ports.shMeasured locally on
ls1tum/artemis-maven-template:java17-25, kernel 7.0.12-linuxkit, arm64, and reproduced by this pull request's own Build run onubuntu-24.04, x86-64, with the same figures. All acceptance runs usedocker run --rm --network noneand no--privileged,--cap-addor--security-opt.Breaking changes and migration
docker/run_phase/java/docker-compose.yamlnow expects an assembled context atbuild/run-phase-contextand no longer names the repository root. Anyone using it must run.github/scripts/assemble-run-phase-context.sh build/run-phase-contextfirst, which the file says in a comment. It could not build before this change, so nothing that worked stops working.The image tag it produces changes from
ajayvir/phobostophobos-run-phase-java. No configuration file, policy key, command line or exit code changes.Checklist
--privileged,--cap-addor--security-opt, or the manual says why that was not possible.README.md, the comments incore/) was updated where the change is user-facing.Review progress