Dockerfile forces its stage dependency by copying over /dev/null, leaving a regular file at that path in the image #30

Closed
opened 2026-09-05 03:43:49 +02:00 by clawbot · 1 comment
Collaborator

Raised as a non-blocking observation during the independent review of PR #28 (comment) and deliberately NOT charged against that PR: the line is carried over verbatim from the previous COPY --from=lint /src/go.sum /dev/null, and #28 only renames the stage. #28 is green and correct. Filing separately so the observation is not lost when that PR merges.

Problem

Dockerfile:16, in the final build stage:

COPY --from=check /src/go.sum /dev/null

The intent is legitimate and it works: it creates a build-graph edge so docker build . cannot produce the default target without the check stage completing. That gating is exactly what issue #27 DoD 4 required, and it was verified end to end.

The problem is the destination. COPY to /dev/null does not discard — it writes a regular file at /dev/null, shadowing the character device in that layer. Because this is the last stage, the resulting image has a /dev/null that is a copy of go.sum rather than the null device.

Why it is low severity today, and why it should still be fixed

Today the image is a build gate with no ENTRYPOINT and nothing runs in it, so nothing observes the broken device. The blast radius is entirely latent.

It stops being latent the moment anyone runs this image, adds a runtime stage on top of it, or copies the pattern into a repo where the image IS the deliverable. A process redirecting to /dev/null in such an image does not discard its output — it appends to a file and grows the container's writable layer, silently. That is a difficult failure to diagnose precisely because > /dev/null is the one construct everybody assumes cannot fail.

It is also load-bearing but unexplained: the idiom reads as a no-op to anyone who does not already know it is a dependency stamp, so it invites removal by someone tidying the Dockerfile, which would silently delete the gate that issue #27 exists to establish.

Definition of done

  • The build stage still cannot be produced without the check stage completing. This is the load-bearing property — do not weaken it while cleaning up the idiom.
  • The copy targets a named path that is inert, e.g. COPY --from=check /src/go.sum /tmp/.check-stamp, rather than shadowing a device node.
  • A comment states that the line exists to force the stage dependency, so it is not mistaken for dead weight and deleted.
  • Prove the gate still holds the way PR #28 proved it — empirically, not by reading: run a plain docker build --no-cache --progress=plain . with no --target and show make check executing before go build. Reading the Dockerfile is not evidence that the edge exists.
  • Negative control: temporarily break a check (e.g. introduce a fmt violation), confirm docker build . goes RED, then revert and confirm green. A dependency edge that is present but not actually gating is worse than none, because it reads as coverage.
  • Confirm /dev/null is a character device in the built image: docker run --rm <img> test -c /dev/null.

Implementation requirements

  • Do NOT modify .golangci.yml — standardized, owner-only.
  • Every FROM stays digest-pinned.
  • Sequence AFTER PR #28 merges — it rewrites this exact region of the Dockerfile, and landing this first would conflict.
  • Landing commit title must end with (closes #<this issue>), bare, not a markdown link — Gitea's auto-close only matches the bare form.
Raised as a non-blocking observation during the independent review of [PR #28](https://git.eeqj.de/sneak/simplelog/pulls/28) ([comment](https://git.eeqj.de/sneak/simplelog/pulls/28#issuecomment-86813)) and deliberately NOT charged against that PR: the line is carried over verbatim from the previous `COPY --from=lint /src/go.sum /dev/null`, and #28 only renames the stage. #28 is green and correct. Filing separately so the observation is not lost when that PR merges. ## Problem `Dockerfile:16`, in the final `build` stage: ```dockerfile COPY --from=check /src/go.sum /dev/null ``` The intent is legitimate and it works: it creates a build-graph edge so `docker build .` cannot produce the default target without the `check` stage completing. That gating is exactly what [issue #27](https://git.eeqj.de/sneak/simplelog/issues/27) DoD 4 required, and it was verified end to end. The problem is the destination. `COPY` to `/dev/null` does not discard — it writes a **regular file** at `/dev/null`, shadowing the character device in that layer. Because this is the last stage, the resulting image has a `/dev/null` that is a copy of `go.sum` rather than the null device. ## Why it is low severity today, and why it should still be fixed Today the image is a build gate with no `ENTRYPOINT` and nothing runs in it, so nothing observes the broken device. The blast radius is entirely latent. It stops being latent the moment anyone runs this image, adds a runtime stage on top of it, or copies the pattern into a repo where the image IS the deliverable. A process redirecting to `/dev/null` in such an image does not discard its output — it appends to a file and grows the container's writable layer, silently. That is a difficult failure to diagnose precisely because `> /dev/null` is the one construct everybody assumes cannot fail. It is also load-bearing but unexplained: the idiom reads as a no-op to anyone who does not already know it is a dependency stamp, so it invites removal by someone tidying the Dockerfile, which would silently delete the gate that [issue #27](https://git.eeqj.de/sneak/simplelog/issues/27) exists to establish. ## Definition of done - The `build` stage still cannot be produced without the `check` stage completing. **This is the load-bearing property — do not weaken it while cleaning up the idiom.** - The copy targets a named path that is inert, e.g. `COPY --from=check /src/go.sum /tmp/.check-stamp`, rather than shadowing a device node. - A comment states that the line exists to force the stage dependency, so it is not mistaken for dead weight and deleted. - Prove the gate still holds the way [PR #28](https://git.eeqj.de/sneak/simplelog/pulls/28) proved it — empirically, not by reading: run a plain `docker build --no-cache --progress=plain .` with no `--target` and show `make check` executing before `go build`. Reading the Dockerfile is not evidence that the edge exists. - Negative control: temporarily break a check (e.g. introduce a fmt violation), confirm `docker build .` goes RED, then revert and confirm green. A dependency edge that is present but not actually gating is worse than none, because it reads as coverage. - Confirm `/dev/null` is a character device in the built image: `docker run --rm <img> test -c /dev/null`. ## Implementation requirements - Do NOT modify `.golangci.yml` — standardized, owner-only. - Every `FROM` stays digest-pinned. - Sequence AFTER [PR #28](https://git.eeqj.de/sneak/simplelog/pulls/28) merges — it rewrites this exact region of the `Dockerfile`, and landing this first would conflict. - Landing commit title must end with ` (closes #<this issue>)`, bare, not a markdown link — Gitea's auto-close only matches the bare form.
Author
Collaborator

Closed at sneak's instruction, 2026-09-05: this was opened by an agent running on another machine outside the managed fleet, under superseded rules. Reopen if the content is wanted.

Model: fable-5-1

Closed at sneak's instruction, 2026-09-05: this was opened by an agent running on another machine outside the managed fleet, under superseded rules. Reopen if the content is wanted. Model: fable-5-1
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/simplelog#30