[PR #88] [MERGED] fix(reuse): re-clone warm rootfs when a :latest tag is repointed to a new digest #88

Closed
opened 2026-10-15 12:51:56 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/games-on-whales/LXC2Docker/pull/88
Author: @JBailes
Created: 6/16/2026
Status: ✅ Merged
Merged: 6/16/2026
Merged by: @JBailes

Base: main ← Head: fix/warm-reuse-digest-drift


📝 Commits (1)

  • 7be8a07 fix(reuse): re-clone warm rootfs when a mutable tag is repointed to a new digest

📊 Changes

3 files changed (+73 additions, -1 deletions)

View changed files

📝 internal/api/containers.go (+27 -1)
📝 internal/api/containers_reuse_test.go (+40 -0)
📝 internal/store/store.go (+6 -0)

📄 Description

The bug

Warm container reuse (DLD_REUSE_CONTAINERS) adopts a stopped container's warm rootfs instead of cloning fresh, gated by reusableContainer. That guard only compared the image ref:

if old.ImageID != imageRef { return false } // "image changed" — but only catches a TAG change

For a mutable tag like :latest, the ref string is identical after the image is updated to a new digest — so the stale warm rootfs is reused and the container silently runs the OLD image content, even though the daemon already pulled the new image and refreshed the template.

Observed in the field: an aimee-kb container kept running a pre-update binary (missing a merged change) after its :latest image was bumped — it was recreated-with-reuse, so it adopted the old rootfs rather than cloning from the current template. (Diagnosed by grep-ing the dispatch-string in the image binary [present] vs the running container binary [absent], with local image digest == ghcr :latest.)

The fix

Track the digest each warm rootfs was cloned from and reuse only when it still matches:

  • store.ContainerRecord.ImageDigest — the image's resolved manifest digest at create time.
  • createContainer records it (= image.RepoDigest, which bdf5f12 already refreshes on pull).
  • reusableContainer reuses only when old.ImageDigest is known, equals the image's current RepoDigest; otherwise (drift, or either side unknown) clone fresh. Logs on drift.

This is conservative by design: a rootfs we can't confirm is current is never adopted. (One-time effect: pre-existing containers with no recorded digest clone fresh on their next reuse — which also picks up any image they were stale against.)

Tests

TestReusableContainerDigestDrift: digest drift, unknown clone-time digest (legacy), and unknown current digest all correctly return not-reusable. go build, go vet, and go test ./... all green.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/games-on-whales/LXC2Docker/pull/88 **Author:** [@JBailes](https://github.com/JBailes) **Created:** 6/16/2026 **Status:** ✅ Merged **Merged:** 6/16/2026 **Merged by:** [@JBailes](https://github.com/JBailes) **Base:** `main` ← **Head:** `fix/warm-reuse-digest-drift` --- ### 📝 Commits (1) - [`7be8a07`](https://github.com/games-on-whales/LXC2Docker/commit/7be8a0705cfd2e17046e578bf27804e7e1e46584) fix(reuse): re-clone warm rootfs when a mutable tag is repointed to a new digest ### 📊 Changes **3 files changed** (+73 additions, -1 deletions) <details> <summary>View changed files</summary> 📝 `internal/api/containers.go` (+27 -1) 📝 `internal/api/containers_reuse_test.go` (+40 -0) 📝 `internal/store/store.go` (+6 -0) </details> ### 📄 Description ## The bug Warm container reuse (`DLD_REUSE_CONTAINERS`) adopts a stopped container's warm rootfs instead of cloning fresh, gated by `reusableContainer`. That guard only compared the image **ref**: ```go if old.ImageID != imageRef { return false } // "image changed" — but only catches a TAG change ``` For a **mutable tag** like `:latest`, the ref string is identical after the image is updated to a new digest — so the **stale warm rootfs is reused and the container silently runs the OLD image content**, even though the daemon already pulled the new image and refreshed the template. **Observed in the field:** an `aimee-kb` container kept running a pre-update binary (missing a merged change) after its `:latest` image was bumped — it was recreated-with-reuse, so it adopted the old rootfs rather than cloning from the current template. (Diagnosed by `grep`-ing the dispatch-string in the *image* binary [present] vs the *running container* binary [absent], with local image digest == ghcr `:latest`.) ## The fix Track the digest each warm rootfs was cloned from and reuse only when it still matches: - `store.ContainerRecord.ImageDigest` — the image's resolved manifest digest at create time. - `createContainer` records it (`= image.RepoDigest`, which `bdf5f12` already refreshes on pull). - `reusableContainer` reuses **only** when `old.ImageDigest` is known, equals the image's current `RepoDigest`; otherwise (drift, or either side unknown) clone fresh. Logs on drift. This is conservative by design: a rootfs we can't *confirm* is current is never adopted. (One-time effect: pre-existing containers with no recorded digest clone fresh on their next reuse — which also picks up any image they were stale against.) ## Tests `TestReusableContainerDigestDrift`: digest drift, unknown clone-time digest (legacy), and unknown current digest all correctly return not-reusable. `go build`, `go vet`, and `go test ./...` all green. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-15 12:51:56 +00:00
Sign in to join this conversation.
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
games-on-whales/LXC2Docker#88
No description provided.