Skip to content

Say what failed a build, not that its stream closed - #2185

Closed
MayCXC wants to merge 7 commits into
apple:mainfrom
MayCXC:build-reports-its-failure
Closed

Say what failed a build, not that its stream closed#2185
MayCXC wants to merge 7 commits into
apple:mainfrom
MayCXC:build-reports-its-failure

Conversation

@MayCXC

@MayCXC MayCXC commented Aug 27, 2026

Copy link
Copy Markdown

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Motivation and Context

A build's host side serves the builder over one stream, and a handler that throws ends it. The ending is what the call reports, so a build whose context walk, content proxy, or image resolve failed said only that its stream had closed unexpectedly, and the failure that closed it was never shown: every one of them read the same, whatever went wrong.

The first handler failure is kept and raised in place of the closure.

This is the reason several build reports in this repository are hard to act on: an unreadable context directory, a registry that refuses a base image, and a blob the store cannot serve all surface as the same sentence about a closed stream. #2089 is one instance of the pattern, where the real cause is an unreadable context.

Dependency and Merge Order

Depends on #2184 (carried in this branch, and through it on #2183 and #2020), which is where the failure-recording sits in the pipeline this raises from. Merge order: apple/container-builder-shim#88, #2020, apple/container-builder-shim#99, #2183, apple/container-builder-shim#98, #2184, then this.

Testing

  • Tested locally
  • Added/updated tests
  • Added/updated docs

A build against an unreadable context directory, a base image that cannot be resolved, and a blob the store cannot serve each report their own cause rather than the closed stream.

Integration suite: 397 passed. Unit suite: 772 passed. make fmt, make check clean.

0Itsuki0 and others added 7 commits August 27, 2026 20:16
A named build context crosses the shim boundary as the local-dir name
BuildKit assigned it, and the host resolves that name against the
directories the command line declared: BuildFSSync validates each
declared directory at init exactly as it validates the primary context,
resolves every walk, read and info transfer through the one
name-to-root lookup, and refuses a name that was never declared. The
CLI resolves local context values to absolute paths when it validates
the flag, so neither the API server's nor the builder VM's view of a
relative path can change which directory is served, and non-local
references (image, git, URL, oci-layout) pass through to the builder
untouched.
The dockerfile frontend takes a set of per-RUN sandbox settings that
container build had no way to ask for: extra host entries, a sandbox
hostname, the size of /dev/shm, resource limits, a cgroup parent, and
the network mode. Each is validated against the shape the frontend
parses, so a malformed value is refused before a builder VM starts, and
--shm-size additionally accepts the suffixed sizes the other size flags
take and hands the frontend the plain byte count it wants.
https://github.com/moby/buildkit/blob/v0.29.0/frontend/dockerui/attr.go
Root selection from the relayed dir-name: a declared name serves from
its root, the reserved names and absent metadata serve from the primary
context, an undeclared name is refused with unknownNamedContext. The
symlink boundary enforcement is shown holding under a named root, and
init refuses a named directory that does not exist.
An oci-layout build-context names a layout directory on this machine;
when the directory does not exist the value used to fall through to
image-reference parsing and fail with a misleading invalid-domain
error. Name the actual problem before a builder starts.
A build exports its image somewhere, and exporting it into the store the
runtime reads from is what makes the image runnable without a trip
through a tar. The exporter writes the blobs and the build records the
images naming them.

Between a blob arriving in the store and a record claiming it, the blob
belongs to nobody, and the sweep that reclaims blobs no image references
takes it: a build running alongside `image rm` lost the manifest it had
just exported. So the blobs a build exports gather in an ingest session,
which the store does not hold and no sweep lists, and the store takes the
whole set in the same held lock that records the images naming them. The
blobs appear at a moment when no sweep can run, and the records claiming
them are there when the lock is released. Pulling an image and loading an
OCI layout land their content the same way and for the same reason.

The lock is what makes that a moment rather than a window. The store's
operations suspend mid-flight and the actor admits other calls at every
suspension, so a sweep overlapping a landing computed its keep set
without the arriving image; it is held across each whole mutating
operation now, the way containerd guards the same window with leases.
https://github.com/containerd/containerd/blob/main/docs/garbage-collection.md

Only an export into the store gathers, so only then is there a session,
and that export is what hands it over: a build that reaches its end has
nothing left to give back, and one that does not is what the catch
returns it for.

Two answers the store owes a writer follow from where the blobs are. A
writer asks what the store holds before writing and skips whatever it
says it has, so a blob it asks about is one this build will not write and
still needs: the gathering takes a copy, the way importing an image
copies the blobs the store already holds into its own ingest. And a blob
already gathered is answered for out of the gathering, since the store
does not hold it yet, which is how importing an image resolves a blob it
may itself have only just written.

A blob is named for its digest alone under the store's algorithm
directory, which is the name a later read resolves; a name carrying the
algorithm again lands a file nothing looks for.
A build's host side serves the builder over one stream, and a handler
that throws ends it. The ending is what the call reports, so a build
whose context walk, content proxy, or image resolve failed said only
that its stream had closed unexpectedly, and the failure that closed it
was never shown: every one of them read the same, whatever went wrong.

Keep the first handler failure and raise it in place of the closure.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants