Skip to content

fix(image): layer analysis correctness — missing blobs, dir metadata, hardlink waste - #107

Merged
deveshctl merged 3 commits into
mainfrom
fix/layer-analysis-correctness
Oct 3, 2026
Merged

deveshctl merged 3 commits into
mainfrom
fix/layer-analysis-correctness

Conversation

@deveshctl

Copy link
Copy Markdown
Owner

Summary

Three independent correctness bugs in the layer analysis pipeline, plus follow-up fixes from code review:

Missing layer blob detection
parseLayers previously treated a manifest-referenced layer whose blob was absent from the outer tar as an empty tree. Any CI efficiency rule would pass on the incomplete input. The check now returns an error with the layer index and ID.

Implicit parent directory metadata preservation
When a layer adds a file without including an explicit directory header for its parent, insertNode creates a placeholder directory node with structural defaults (mode 0755, uid/gid 0). That placeholder was reaching mergeLayerWith and overwriting the real mode/UID/GID recorded by an earlier layer. Placeholder nodes are now marked Inferred; the merge skips metadata propagation from them.

Hardlink alias waste accounting
If a file's original name was deleted by a whiteout but a hardlink alias pointing at the same content survived in the final image, the original's bytes were charged as waste. The payload remained reachable through the alias. computeEfficiency now checks the final stacked snapshot for surviving hardlink targets and skips waste charging when the content is still live.

Follow-up (code review)

  • hdr.Linkname now receives the same cleanTarPath normalization as hdr.Name in ParseLayerTar, closing a latent gap for images from Windows-native build tools that emit ./-prefixed hardlink targets.
  • Inferred is propagated in all five FileNode clone functions so the field's semantics hold on cloned/stacked nodes.
  • Restored four explanatory comments in computeEfficiency (O(1) index rationale; charge rules for deleted vs live runs; LayerCount accounting invariant) that were accidentally removed during the hardlink refactor.
  • TestParseLayers_MissingLayerBlob_ReturnsError now uses a valid empty tar for the present layer so the test exercises the missing-blob guard specifically.

Test plan

  • go build ./... and go vet ./... clean
  • New tests: TestParseLayers_MissingLayerBlob_ReturnsError, TestStack_ImplicitParentDoesNotOverwriteMetadata, TestEfficiency_HardlinkAlias_DeletedOriginal_NotWasted
  • Full go test ./... suite runs on CI

…a, fix hardlink waste

Three correctness fixes to the layer analysis pipeline:

1. parseLayers now returns an error when a manifest-referenced layer blob is
   absent from the outer tar. Previously the missing layer was silently treated
   as an empty tree, which could cause CI efficiency rules to pass on genuinely
   incomplete input.

2. Implicit parent directory nodes (created by insertNode when a tar entry lacks
   an explicit header for its ancestors) are now marked Inferred. mergeLayerWith
   skips metadata propagation from inferred nodes, so explicit mode/UID/GID set
   by an earlier layer is no longer overwritten by a structural placeholder.

3. Hardlink-aliased content is no longer counted as wasted bytes when the
   original filename is deleted but a hardlink pointing at the same payload
   survives in the final image. computeEfficiency now builds a set of surviving
   hardlink targets from the final stacked snapshot and skips waste charging for
   any deleted path whose content remains reachable through an alias.
… comments

Follow-up to the layer analysis correctness fixes:

- Apply cleanTarPath to hdr.Linkname in ParseLayerTar so hardlink target
  paths receive the same ./- and backslash-normalization as hdr.Name. Without
  this, images from Windows-native or some older build tools emit ./data/a
  as the linkname, which failed to match the stored /data/a key in
  walkSurvivingHardlinks and caused the deleted original to be incorrectly
  charged as waste.

- Propagate Inferred in all four FileNode clone functions (cloneAsUnchanged,
  cloneAsRemoved, cloneAsAdded, cloneWithDiffType, cloneStructure). The field
  is only read from raw layer-side nodes today, but omitting it from clones
  would silently break any future reader inspecting the cumulative tree.

- Restore the four explanatory comments removed during the hardlink-waste
  refactor in computeEfficiency: the O(1) index rationale and the LayerCount
  accounting rule. The LayerCount comment prevents a future reader from
  treating the run.occ loop as a copy-paste error.

- Fix TestParseLayers_MissingLayerBlob_ReturnsError: replace the 1 KiB of
  raw zeros for layer0 with a valid empty tar so the test exercises the
  missing-blob guard specifically, not the corrupt-tar parse error.
…red size

The previous guard used l.Size > 0 (the declared tar header size) to
distinguish a legitimately empty layer from a missing one. A layer absent
from the archive entirely gets Size == 0 from a missing-key map lookup, so
the check never fired and parseLayers returned nil error.

Use the headers map from Pass 1 instead: every entry that appeared in the
outer tar has a key there. A manifest-referenced path with no key in headers
was never in the archive, regardless of its declared size.
@deveshctl
deveshctl merged commit e76c289 into main Oct 3, 2026
13 checks passed
@deveshctl
deveshctl deleted the fix/layer-analysis-correctness branch October 3, 2026 05:56
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.

1 participant