From ed568992424936364d7ffa7e9bc4e90c62bf3e7c Mon Sep 17 00:00:00 2001 From: Emanuel Oprea <2664342+EmanuelOprea@users.noreply.github.com> Date: Fri, 12 Jun 2026 13:40:20 +0300 Subject: [PATCH] Fix C-WCOW dm-verity hash mismatch vs cplat runtime dmverity-vhd produced per-layer and merged block-CIM dm-verity hashes that did not match the values the cplat runtime (containerd blockcim snapshotter) computes at pull time, which blocks hash-locked confidential Windows containers. Two inputs to the hash differed: 1. CIM name: the CIM name is part of the dm-verity-hashed content. confcom named each layer CIM ".cim" while the runtime names every layer CIM "layer.cim". Use the constant "layer.cim" to match. 2. Parent-layer order: hcsshim's processNonBaseLayer merges each layer's registry delta against parentLayerPaths[0], and MergeBlockCIMLayersWithOpts expects source CIMs topmost-first (base last). The runtime supplies parents immediate-parent-first (containerd parent-chain order), producing a cumulative/chained registry merge; confcom accumulated them base-first (a flat merge against the original base), diverging from the 3rd layer on. Add orderedParentsForMerge() and apply it in tarToCim and generateMergedCim. --- cmd/dmverity-vhd/cim.go | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/cmd/dmverity-vhd/cim.go b/cmd/dmverity-vhd/cim.go index 40b5adc..5af5c37 100644 --- a/cmd/dmverity-vhd/cim.go +++ b/cmd/dmverity-vhd/cim.go @@ -108,6 +108,27 @@ func checkWindowsVersion() error { return windowsVersionError } +// orderedParentsForMerge returns the parent layers in immediate-parent-first order +// (the reverse of confcom's base-first accumulation). +// +// hcsshim's processNonBaseLayer merges each layer's registry delta against +// parentLayerPaths[0] (see internal/wclayer/cim/process.go), and +// MergeBlockCIMLayersWithOpts expects source CIMs in topmost-first order (base at +// the last index). The cplat runtime (blockcim snapshotter) supplies parents in +// containerd parent-chain order, i.e. immediate-parent-first, so [0] is the +// previous layer's already-merged hive, producing a cumulative/chained registry +// merge. confcom accumulates parents base-first, which would merge every delta +// against the original base hive (a flat merge) and diverge from the runtime at +// the third layer onward. Reversing to immediate-parent-first makes confcom's +// per-layer and merged block-CIM hashes match the runtime exactly. +func orderedParentsForMerge(parentLayers ParentLayers) ParentLayers { + reversed := make(ParentLayers, len(parentLayers)) + for i, pl := range parentLayers { + reversed[len(parentLayers)-1-i] = pl + } + return reversed +} + func tarToCim(tarReader io.Reader, parentLayers ParentLayers, out string, layerName string) (string, ParentLayers, error) { log.Trace("tarToCim called") @@ -130,7 +151,10 @@ func tarToCim(tarReader io.Reader, parentLayers ParentLayers, out string, layerN } layerName = sanitizeCimLayerName(layerName) blockFileName := fmt.Sprintf("%s.bcim", layerName) - cimName := fmt.Sprintf("%s.cim", layerName) + // The CIM name is part of the dm-verity-hashed content. The cplat runtime + // blockcim snapshotter names every layer CIM "layer.cim"; match it so the + // per-layer roothash equals the runtime's integrity_checksum. + cimName := "layer.cim" blockPath := filepath.Join(out, blockFileName) blockCIM := &cimfs.BlockCIM{ @@ -140,7 +164,7 @@ func tarToCim(tarReader io.Reader, parentLayers ParentLayers, out string, layerN } importOpts := []cimimport.BlockCIMLayerImportOpt{ - cimimport.WithParentLayers(parentLayers), + cimimport.WithParentLayers(orderedParentsForMerge(parentLayers)), cimimport.WithVHDFooter(), cimimport.WithLayerIntegrity(), } @@ -191,7 +215,11 @@ func generateMergedCim(parentLayers ParentLayers, out string, mergedName string) } log.Tracef("before cimimport.MergeBlockCIMLayersWithOpts for merged layer %s", mergedName) - importErr := cimimport.MergeBlockCIMLayersWithOpts(context.Background(), parentLayers, mergedBlockCIM, importOpts...) + // MergeBlockCIMLayersWithOpts (like the cplat snapshotter's prepareMergedCIM) + // expects source CIMs in topmost-first order (base at the last index); confcom + // accumulates parentLayers base-first, so reverse to match the runtime's merged + // CIM bytes and hash. + importErr := cimimport.MergeBlockCIMLayersWithOpts(context.Background(), orderedParentsForMerge(parentLayers), mergedBlockCIM, importOpts...) log.Tracef("after cimimport.MergeBlockCIMLayersWithOpts for merged layer %s", mergedName) if importErr != nil {