diff --git a/internal/hcs/schema2/cim_mount.go b/internal/hcs/schema2/cimfs.go similarity index 70% rename from internal/hcs/schema2/cim_mount.go rename to internal/hcs/schema2/cimfs.go index 81865e7ea4..52fb62a829 100644 --- a/internal/hcs/schema2/cim_mount.go +++ b/internal/hcs/schema2/cimfs.go @@ -9,14 +9,6 @@ package hcsschema -const ( - CimMountFlagNone uint32 = 0x0 - CimMountFlagChildOnly uint32 = 0x1 - CimMountFlagEnableDax uint32 = 0x2 - CimMountFlagCacheFiles uint32 = 0x4 - CimMountFlagCacheRegions uint32 = 0x8 -) - type CimMount struct { ImagePath string `json:"ImagePath,omitempty"` FileSystemName string `json:"FileSystemName,omitempty"` diff --git a/internal/winapi/cimfs.go b/internal/winapi/cimfs.go index 21664577b7..df888a4767 100644 --- a/internal/winapi/cimfs.go +++ b/internal/winapi/cimfs.go @@ -32,11 +32,17 @@ type CimFsFileMetadata struct { EACount uint32 } +type CimFsImagePath struct { + ImageDir *uint16 + ImageName *uint16 +} + //sys CimMountImage(imagePath string, fsName string, flags uint32, volumeID *g) (hr error) = cimfs.CimMountImage? //sys CimDismountImage(volumeID *g) (hr error) = cimfs.CimDismountImage? //sys CimCreateImage(imagePath string, oldFSName *uint16, newFSName *uint16, cimFSHandle *FsHandle) (hr error) = cimfs.CimCreateImage? -//sys CimCloseImage(cimFSHandle FsHandle) = cimfs.CimCloseImage? +//sys CimCreateImage2(imagePath string, flags uint32, oldFSName *uint16, newFSName *uint16, cimFSHandle *FsHandle) (hr error) = cimfs.CimCreateImage2? +//sys CimCloseImage(cimFSHandle FsHandle) = cimfs.CimCloseImage //sys CimCommitImage(cimFSHandle FsHandle) (hr error) = cimfs.CimCommitImage? //sys CimCreateFile(cimFSHandle FsHandle, path string, file *CimFsFileMetadata, cimStreamHandle *StreamHandle) (hr error) = cimfs.CimCreateFile? @@ -45,3 +51,8 @@ type CimFsFileMetadata struct { //sys CimDeletePath(cimFSHandle FsHandle, path string) (hr error) = cimfs.CimDeletePath? //sys CimCreateHardLink(cimFSHandle FsHandle, newPath string, oldPath string) (hr error) = cimfs.CimCreateHardLink? //sys CimCreateAlternateStream(cimFSHandle FsHandle, path string, size uint64, cimStreamHandle *StreamHandle) (hr error) = cimfs.CimCreateAlternateStream? +//sys CimAddFsToMergedImage(cimFSHandle FsHandle, path string) (hr error) = cimfs.CimAddFsToMergedImage? +//sys CimAddFsToMergedImage2(cimFSHandle FsHandle, path string, flags uint32) (hr error) = cimfs.CimAddFsToMergedImage2? +//sys CimMergeMountImage(numCimPaths uint32, backingImagePaths *CimFsImagePath, flags uint32, volumeID *g) (hr error) = cimfs.CimMergeMountImage? +//sys CimTombstoneFile(cimFSHandle FsHandle, path string) (hr error) = cimfs.CimTombstoneFile? +//sys CimCreateMergeLink(cimFSHandle FsHandle, newPath string, oldPath string) (hr error) = cimfs.CimCreateMergeLink? diff --git a/internal/winapi/zsyscall_windows.go b/internal/winapi/zsyscall_windows.go index ecdded312e..220e9eb2c8 100644 --- a/internal/winapi/zsyscall_windows.go +++ b/internal/winapi/zsyscall_windows.go @@ -53,6 +53,8 @@ var ( procCM_Get_Device_ID_ListA = modcfgmgr32.NewProc("CM_Get_Device_ID_ListA") procCM_Get_Device_ID_List_SizeA = modcfgmgr32.NewProc("CM_Get_Device_ID_List_SizeA") procCM_Locate_DevNodeW = modcfgmgr32.NewProc("CM_Locate_DevNodeW") + procCimAddFsToMergedImage = modcimfs.NewProc("CimAddFsToMergedImage") + procCimAddFsToMergedImage2 = modcimfs.NewProc("CimAddFsToMergedImage2") procCimCloseImage = modcimfs.NewProc("CimCloseImage") procCimCloseStream = modcimfs.NewProc("CimCloseStream") procCimCommitImage = modcimfs.NewProc("CimCommitImage") @@ -60,9 +62,13 @@ var ( procCimCreateFile = modcimfs.NewProc("CimCreateFile") procCimCreateHardLink = modcimfs.NewProc("CimCreateHardLink") procCimCreateImage = modcimfs.NewProc("CimCreateImage") + procCimCreateImage2 = modcimfs.NewProc("CimCreateImage2") + procCimCreateMergeLink = modcimfs.NewProc("CimCreateMergeLink") procCimDeletePath = modcimfs.NewProc("CimDeletePath") procCimDismountImage = modcimfs.NewProc("CimDismountImage") + procCimMergeMountImage = modcimfs.NewProc("CimMergeMountImage") procCimMountImage = modcimfs.NewProc("CimMountImage") + procCimTombstoneFile = modcimfs.NewProc("CimTombstoneFile") procCimWriteStream = modcimfs.NewProc("CimWriteStream") procSetJobCompartmentId = modiphlpapi.NewProc("SetJobCompartmentId") procClosePseudoConsole = modkernel32.NewProc("ClosePseudoConsole") @@ -181,11 +187,55 @@ func _CMLocateDevNode(pdnDevInst *uint32, pDeviceID *uint16, uFlags uint32) (hr return } -func CimCloseImage(cimFSHandle FsHandle) (err error) { - err = procCimCloseImage.Find() - if err != nil { +func CimAddFsToMergedImage(cimFSHandle FsHandle, path string) (hr error) { + var _p0 *uint16 + _p0, hr = syscall.UTF16PtrFromString(path) + if hr != nil { + return + } + return _CimAddFsToMergedImage(cimFSHandle, _p0) +} + +func _CimAddFsToMergedImage(cimFSHandle FsHandle, path *uint16) (hr error) { + hr = procCimAddFsToMergedImage.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimAddFsToMergedImage.Addr(), uintptr(cimFSHandle), uintptr(unsafe.Pointer(path))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimAddFsToMergedImage2(cimFSHandle FsHandle, path string, flags uint32) (hr error) { + var _p0 *uint16 + _p0, hr = syscall.UTF16PtrFromString(path) + if hr != nil { return } + return _CimAddFsToMergedImage2(cimFSHandle, _p0, flags) +} + +func _CimAddFsToMergedImage2(cimFSHandle FsHandle, path *uint16, flags uint32) (hr error) { + hr = procCimAddFsToMergedImage2.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimAddFsToMergedImage2.Addr(), uintptr(cimFSHandle), uintptr(unsafe.Pointer(path)), uintptr(flags)) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimCloseImage(cimFSHandle FsHandle) { syscall.SyscallN(procCimCloseImage.Addr(), uintptr(cimFSHandle)) return } @@ -321,6 +371,59 @@ func _CimCreateImage(imagePath *uint16, oldFSName *uint16, newFSName *uint16, ci return } +func CimCreateImage2(imagePath string, flags uint32, oldFSName *uint16, newFSName *uint16, cimFSHandle *FsHandle) (hr error) { + var _p0 *uint16 + _p0, hr = syscall.UTF16PtrFromString(imagePath) + if hr != nil { + return + } + return _CimCreateImage2(_p0, flags, oldFSName, newFSName, cimFSHandle) +} + +func _CimCreateImage2(imagePath *uint16, flags uint32, oldFSName *uint16, newFSName *uint16, cimFSHandle *FsHandle) (hr error) { + hr = procCimCreateImage2.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimCreateImage2.Addr(), uintptr(unsafe.Pointer(imagePath)), uintptr(flags), uintptr(unsafe.Pointer(oldFSName)), uintptr(unsafe.Pointer(newFSName)), uintptr(unsafe.Pointer(cimFSHandle))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimCreateMergeLink(cimFSHandle FsHandle, newPath string, oldPath string) (hr error) { + var _p0 *uint16 + _p0, hr = syscall.UTF16PtrFromString(newPath) + if hr != nil { + return + } + var _p1 *uint16 + _p1, hr = syscall.UTF16PtrFromString(oldPath) + if hr != nil { + return + } + return _CimCreateMergeLink(cimFSHandle, _p0, _p1) +} + +func _CimCreateMergeLink(cimFSHandle FsHandle, newPath *uint16, oldPath *uint16) (hr error) { + hr = procCimCreateMergeLink.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimCreateMergeLink.Addr(), uintptr(cimFSHandle), uintptr(unsafe.Pointer(newPath)), uintptr(unsafe.Pointer(oldPath))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + func CimDeletePath(cimFSHandle FsHandle, path string) (hr error) { var _p0 *uint16 _p0, hr = syscall.UTF16PtrFromString(path) @@ -360,6 +463,21 @@ func CimDismountImage(volumeID *g) (hr error) { return } +func CimMergeMountImage(numCimPaths uint32, backingImagePaths *CimFsImagePath, flags uint32, volumeID *g) (hr error) { + hr = procCimMergeMountImage.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimMergeMountImage.Addr(), uintptr(numCimPaths), uintptr(unsafe.Pointer(backingImagePaths)), uintptr(flags), uintptr(unsafe.Pointer(volumeID))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + func CimMountImage(imagePath string, fsName string, flags uint32, volumeID *g) (hr error) { var _p0 *uint16 _p0, hr = syscall.UTF16PtrFromString(imagePath) @@ -389,6 +507,30 @@ func _CimMountImage(imagePath *uint16, fsName *uint16, flags uint32, volumeID *g return } +func CimTombstoneFile(cimFSHandle FsHandle, path string) (hr error) { + var _p0 *uint16 + _p0, hr = syscall.UTF16PtrFromString(path) + if hr != nil { + return + } + return _CimTombstoneFile(cimFSHandle, _p0) +} + +func _CimTombstoneFile(cimFSHandle FsHandle, path *uint16) (hr error) { + hr = procCimTombstoneFile.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimTombstoneFile.Addr(), uintptr(cimFSHandle), uintptr(unsafe.Pointer(path))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + func CimWriteStream(cimStreamHandle StreamHandle, buffer uintptr, bufferSize uint32) (hr error) { hr = procCimWriteStream.Find() if hr != nil { diff --git a/pkg/cimfs/cim_test.go b/pkg/cimfs/cim_test.go index c1e2bc4028..921784c44d 100644 --- a/pkg/cimfs/cim_test.go +++ b/pkg/cimfs/cim_test.go @@ -5,19 +5,19 @@ package cimfs import ( "bytes" - "context" "errors" "fmt" "io" + "syscall" "os" "path/filepath" "testing" "time" - "github.com/Microsoft/go-winio" + winio "github.com/Microsoft/go-winio" "github.com/Microsoft/go-winio/pkg/guid" - hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2" + vhd "github.com/Microsoft/go-winio/vhd" "golang.org/x/sys/windows" ) @@ -29,6 +29,30 @@ type tuple struct { isDir bool } +// a test interface for representing both forked & block CIMs +type testCIM interface { + // returns a full CIM path + cimPath() string +} + +type testForkedCIM struct { + imageDir string + parentName string + imageName string +} + +func (t *testForkedCIM) cimPath() string { + return filepath.Join(t.imageDir, t.imageName) +} + +type testBlockCIM struct { + BlockCIM +} + +func (t *testBlockCIM) cimPath() string { + return filepath.Join(t.BlockPath, t.CimName) +} + // A utility function to create a file/directory and write data to it in the given cim. func createCimFileUtil(c *CimFsWriter, fileTuple tuple) error { // create files inside the cim @@ -60,6 +84,99 @@ func createCimFileUtil(c *CimFsWriter, fileTuple tuple) error { return nil } +// openNewCIM creates a new CIM and returns a writer to that CIM. The caller MUST close +// the writer. +func openNewCIM(t *testing.T, newCIM testCIM) *CimFsWriter { + t.Helper() + + var ( + writer *CimFsWriter + err error + ) + + switch val := newCIM.(type) { + case *testForkedCIM: + writer, err = Create(val.imageDir, val.parentName, val.imageName) + case *testBlockCIM: + writer, err = CreateBlockCIM(val.BlockPath, val.CimName, val.Type) + } + if err != nil { + t.Fatalf("failed while creating a cim: %s", err) + } + t.Cleanup(func() { + writer.Close() + // add 3 second sleep before test cleanup remove the cim directory + // otherwise, that removal fails due to some handles still being open + time.Sleep(3 * time.Second) + }) + return writer +} + +// compareContent takes in path to a directory (which is usually a volume at which a CIM is +// mounted) and ensures that every file/directory in the `testContents` shows up exactly +// as it is under that directory. +func compareContent(t *testing.T, root string, testContents []tuple) { + t.Helper() + + for _, ft := range testContents { + if ft.isDir { + _, err := os.Stat(filepath.Join(root, ft.filepath)) + if err != nil { + t.Fatalf("stat directory %s from cim: %s", ft.filepath, err) + } + } else { + f, err := os.Open(filepath.Join(root, ft.filepath)) + if err != nil { + t.Fatalf("open file %s: %s", filepath.Join(root, ft.filepath), err) + } + defer f.Close() + + fileContents := make([]byte, len(ft.fileContents)) + + // it is a file - read contents + rc, err := f.Read(fileContents) + if err != nil && !errors.Is(err, io.EOF) { + t.Fatalf("failure while reading file %s from cim: %s", ft.filepath, err) + } else if !bytes.Equal(fileContents[:rc], ft.fileContents) { + t.Fatalf("contents of file %s don't match", ft.filepath) + } + } + } +} + +func writeCIM(t *testing.T, writer *CimFsWriter, testContents []tuple) { + t.Helper() + for _, ft := range testContents { + err := createCimFileUtil(writer, ft) + if err != nil { + t.Fatalf("failed to create the file %s inside the cim:%s", ft.filepath, err) + } + } + if err := writer.Close(); err != nil { + t.Fatalf("cim close: %s", err) + } +} + +func mountCIM(t *testing.T, testCIM testCIM, mountFlags uint32) string { + t.Helper() + // mount and read the contents of the cim + volumeGUID, err := guid.NewV4() + if err != nil { + t.Fatalf("generate cim mount GUID: %s", err) + } + + mountvol, err := Mount(testCIM.cimPath(), volumeGUID, mountFlags) + if err != nil { + t.Fatalf("mount cim : %s", err) + } + t.Cleanup(func() { + if err := Unmount(mountvol); err != nil { + t.Logf("CIM unmount failed: %s", err) + } + }) + return mountvol +} + // This test creates a cim, writes some files to it and then reads those files back. // The cim created by this test has only 3 files in the following tree // / @@ -78,72 +195,429 @@ func TestCimReadWrite(t *testing.T) { } tempDir := t.TempDir() + testCIM := &testForkedCIM{ + imageDir: tempDir, + parentName: "", + imageName: "test.cim", + } + + writer := openNewCIM(t, testCIM) + writeCIM(t, writer, testContents) + mountvol := mountCIM(t, testCIM, CimMountFlagNone) + compareContent(t, mountvol, testContents) +} + +func TestBlockCIMInvalidCimName(t *testing.T) { + if !IsBlockCimSupported() { + t.Skip("blockCIM not supported on this OS version") + } + + blockPath := "C:\\Windows" + cimName := "" + _, err := CreateBlockCIM(blockPath, cimName, BlockCIMTypeSingleFile) + if !errors.Is(err, os.ErrInvalid) { + t.Fatalf("expected error `%s`, got `%s`", err, os.ErrInvalid) + } +} + +func TestBlockCIMInvalidBlockPath(t *testing.T) { + if !IsBlockCimSupported() { + t.Skip("blockCIM not supported on this OS version") + } + + blockPath := "" + cimName := "foo.bcim" + _, err := CreateBlockCIM(blockPath, cimName, BlockCIMTypeSingleFile) + if !errors.Is(err, os.ErrInvalid) { + t.Fatalf("expected error `%s`, got `%s", os.ErrInvalid, err) + } +} + +func TestBlockCIMInvalidType(t *testing.T) { + if !IsBlockCimSupported() { + t.Skip("blockCIM not supported on this OS version") + } + + blockPath := "" + cimName := "foo.bcim" + _, err := CreateBlockCIM(blockPath, cimName, BlockCIMTypeNone) + if !errors.Is(err, os.ErrInvalid) { + t.Fatalf("expected error `%s`, got `%s", os.ErrInvalid, err) + } +} + +func TestCIMMergeInvalidType(t *testing.T) { + if !IsBlockCimSupported() { + t.Skip("blockCIM not supported on this OS version") + } + + mergedCIM := &BlockCIM{ + Type: 0, + BlockPath: "C:\\fake\\path", + CimName: "fakename.cim", + } + // doesn't matter what we pass in the source CIM array as long as it has 2+ elements + err := MergeBlockCIMs(mergedCIM, []*BlockCIM{mergedCIM, mergedCIM}) + if !errors.Is(err, os.ErrInvalid) { + t.Fatalf("expected error `%s`, got `%s", os.ErrInvalid, err) + } +} + +func TestCIMMergeInvalidLength(t *testing.T) { + if !IsBlockCimSupported() { + t.Skip("blockCIM not supported on this OS version") + } + + mergedCIM := &BlockCIM{ + Type: 0, + BlockPath: "C:\\fake\\path", + CimName: "fakename.cim", + } + err := MergeBlockCIMs(mergedCIM, []*BlockCIM{mergedCIM}) + if !errors.Is(err, os.ErrInvalid) { + t.Fatalf("expected error `%s`, got `%s", os.ErrInvalid, err) + } +} - cimName := "test.cim" - cimPath := filepath.Join(tempDir, cimName) - c, err := Create(tempDir, "", cimName) +func TestBlockCIMEmpty(t *testing.T) { + if !IsBlockCimSupported() { + t.Skip("blockCIM not supported on this OS version") + } + + root := t.TempDir() + blockPath := filepath.Join(root, "layer.bcim") + cimName := "layer.cim" + w, err := CreateBlockCIM(blockPath, cimName, BlockCIMTypeSingleFile) if err != nil { - t.Fatalf("failed while creating a cim: %s", err) + t.Fatalf("unexpected error: %s", err) } - defer func() { - // destroy cim sometimes fails if tried immediately after accessing & unmounting the cim so - // give some time and then remove. - time.Sleep(3 * time.Second) - if err := DestroyCim(context.Background(), cimPath); err != nil { - t.Fatalf("destroy cim failed: %s", err) + err = w.Close() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } +} + +func TestBlockCIMSingleFileReadWrite(t *testing.T) { + if !IsBlockCimSupported() { + t.Skip("blockCIM not supported on this OS version") + } + + root := t.TempDir() + testCIM := &testBlockCIM{ + BlockCIM: BlockCIM{ + Type: BlockCIMTypeSingleFile, + BlockPath: filepath.Join(root, "layer.bcim"), + CimName: "layer.cim", + }, + } + + testContents := []tuple{ + {"foobar.txt", []byte("foobar test data"), false}, + {"foo", []byte(""), true}, + {"foo\\bar.txt", []byte("bar test data"), false}, + } + + writer := openNewCIM(t, testCIM) + writeCIM(t, writer, testContents) + mountvol := mountCIM(t, testCIM, CimMountSingleFileCim) + compareContent(t, mountvol, testContents) +} + +// creates a block device for storing a blockCIM. returns a volume path to the block +// device that can be used for writing the CIM. +func createBlockDevice(t *testing.T, dir string) string { + t.Helper() + // create a VHD for storing our block CIM + vhdPath := filepath.Join(dir, "layer.vhdx") + if err := vhd.CreateVhdx(vhdPath, 1, 1); err != nil { + t.Fatalf("failed to create VHD: %s", err) + } + + diskHandle, err := vhd.OpenVirtualDisk(vhdPath, vhd.VirtualDiskAccessNone, vhd.OpenVirtualDiskFlagNone) + if err != nil { + t.Fatalf("failed to open VHD: %s", err) + } + t.Cleanup(func() { + closeErr := syscall.CloseHandle(diskHandle) + if closeErr != nil { + t.Logf("failed to close VHD handle: %s", closeErr) } - }() + }) - for _, ft := range testContents { - err := createCimFileUtil(c, ft) - if err != nil { - t.Fatalf("failed to create the file %s inside the cim:%s", ft.filepath, err) + if err = vhd.AttachVirtualDisk(diskHandle, vhd.AttachVirtualDiskFlagNone, &vhd.AttachVirtualDiskParameters{Version: 2}); err != nil { + t.Fatalf("failed to attach VHD: %s", err) + } + t.Cleanup(func() { + detachErr := vhd.DetachVirtualDisk(diskHandle) + if detachErr != nil { + t.Logf("failed to detach VHD: %s", detachErr) } + }) + + physicalPath, err := vhd.GetVirtualDiskPhysicalPath(diskHandle) + if err != nil { + t.Fatalf("failed to get physical path of VHD: %s", err) } - if err := c.Close(); err != nil { - t.Fatalf("cim close: %s", err) + return physicalPath +} + +func TestBlockCIMBlockDeviceReadWrite(t *testing.T) { + if !IsBlockCimSupported() { + t.Skip("blockCIM not supported on this OS version") + } + + root := t.TempDir() + + physicalPath := createBlockDevice(t, root) + + testCIM := &testBlockCIM{ + BlockCIM: BlockCIM{ + Type: BlockCIMTypeDevice, + BlockPath: physicalPath, + CimName: "layer.cim", + }, + } + + testContents := []tuple{ + {"foobar.txt", []byte("foobar test data"), false}, + {"foo", []byte(""), true}, + {"foo\\bar.txt", []byte("bar test data"), false}, + } + + writer := openNewCIM(t, testCIM) + writeCIM(t, writer, testContents) + mountvol := mountCIM(t, testCIM, CimMountBlockDeviceCim) + compareContent(t, mountvol, testContents) +} + +func TestMergedBlockCIMs(rootT *testing.T) { + // A slice of 3 slices, 1 slice for contents of each CIM + testContents := [][]tuple{ + {{"foo.txt", []byte("foo1"), false}}, + {{"bar.txt", []byte("bar"), false}}, + {{"foo.txt", []byte("foo2"), false}}, + } + // create 3 separate block CIMs + nCIMs := len(testContents) + + // test merging for both SingleFile & BlockDevice type of block CIMs + type testBlock struct { + name string + blockType BlockCIMType + mountFlag uint32 + blockPathGenerator func(t *testing.T, dir string) string + } + + tests := []testBlock{ + { + name: "single file", + blockType: BlockCIMTypeSingleFile, + mountFlag: CimMountSingleFileCim, + blockPathGenerator: func(t *testing.T, dir string) string { + t.Helper() + return filepath.Join(dir, "layer.bcim") + }, + }, + { + name: "block device", + blockType: BlockCIMTypeDevice, + mountFlag: CimMountBlockDeviceCim, + blockPathGenerator: func(t *testing.T, dir string) string { + t.Helper() + return createBlockDevice(t, dir) + }, + }, + } + + for _, test := range tests { + rootT.Run(test.name, func(t *testing.T) { + sourceCIMs := make([]*BlockCIM, 0, nCIMs) + for i := 0; i < nCIMs; i++ { + root := t.TempDir() + blockPath := test.blockPathGenerator(t, root) + tc := &testBlockCIM{ + BlockCIM: BlockCIM{ + Type: test.blockType, + BlockPath: blockPath, + CimName: "layer.cim", + }} + writer := openNewCIM(t, tc) + writeCIM(t, writer, testContents[i]) + sourceCIMs = append(sourceCIMs, &tc.BlockCIM) + } + + mergedBlockPath := test.blockPathGenerator(t, t.TempDir()) + // prepare a merged CIM + mergedCIM := &BlockCIM{ + Type: test.blockType, + BlockPath: mergedBlockPath, + CimName: "merged.cim", + } + + if err := MergeBlockCIMs(mergedCIM, sourceCIMs); err != nil { + t.Fatalf("failed to merge block CIMs: %s", err) + } + + // mount and read the contents of the cim + volumeGUID, err := guid.NewV4() + if err != nil { + t.Fatalf("generate cim mount GUID: %s", err) + } + + mountvol, err := MountMergedBlockCIMs(mergedCIM, sourceCIMs, test.mountFlag, volumeGUID) + if err != nil { + t.Fatalf("failed to mount merged block CIMs: %s\n", err) + } + defer func() { + if err := Unmount(mountvol); err != nil { + t.Logf("CIM unmount failed: %s", err) + } + }() + // since we are merging, only 1 foo.txt (from the 1st CIM) should + // show up + compareContent(t, mountvol, []tuple{testContents[0][0], testContents[1][0]}) + }) + } +} + +func TestTombstoneInMergedBlockCIMs(rootT *testing.T) { + root := rootT.TempDir() + + testContents := []tuple{ + {"foobar.txt", []byte("foobar test data"), false}, + {"foo", []byte(""), true}, + {"foo\\bar.txt", []byte("bar test data"), false}, + } + + cim1 := &testBlockCIM{ + BlockCIM: BlockCIM{ + Type: BlockCIMTypeSingleFile, + BlockPath: filepath.Join(root, "1.bcim"), + CimName: "test.cim", + }, + } + writer := openNewCIM(rootT, cim1) + writeCIM(rootT, writer, testContents) + + cim2 := &testBlockCIM{ + BlockCIM: BlockCIM{ + Type: BlockCIMTypeSingleFile, + BlockPath: filepath.Join(root, "2.bcim"), + CimName: "test.cim", + }, + } + + cim2writer := openNewCIM(rootT, cim2) + + if err := cim2writer.AddTombstone("foobar.txt"); err != nil { + rootT.Fatalf("failed to add tombstone: %s", err) + } + cim2writer.Close() + + mergedCIM := &BlockCIM{ + Type: BlockCIMTypeSingleFile, + BlockPath: filepath.Join(root, "merged.cim"), + CimName: "merged.cim", + } + + sourceCIMs := []*BlockCIM{&cim2.BlockCIM, &cim1.BlockCIM} + if err := MergeBlockCIMs(mergedCIM, sourceCIMs); err != nil { + rootT.Fatalf("failed to merge block CIMs: %s", err) } // mount and read the contents of the cim volumeGUID, err := guid.NewV4() if err != nil { - t.Fatalf("generate cim mount GUID: %s", err) + rootT.Fatalf("generate cim mount GUID: %s", err) } - mountvol, err := Mount(cimPath, volumeGUID, hcsschema.CimMountFlagCacheFiles) + mountvol, err := MountMergedBlockCIMs(mergedCIM, sourceCIMs, CimMountSingleFileCim, volumeGUID) if err != nil { - t.Fatalf("mount cim : %s", err) + rootT.Fatalf("failed to mount merged block CIMs: %s\n", err) } defer func() { if err := Unmount(mountvol); err != nil { - t.Fatalf("unmount failed: %s", err) + rootT.Logf("CIM unmount failed: %s", err) } }() - for _, ft := range testContents { - if ft.isDir { - _, err := os.Stat(filepath.Join(mountvol, ft.filepath)) - if err != nil { - t.Fatalf("stat directory %s from cim: %s", ft.filepath, err) - } - } else { - f, err := os.Open(filepath.Join(mountvol, ft.filepath)) - if err != nil { - t.Fatalf("open file %s: %s", filepath.Join(mountvol, ft.filepath), err) - } - defer f.Close() + // verify that foobar.txt doesn't show up + _, err = os.Stat(filepath.Join(mountvol, "foobar.txt")) + if err == nil || !os.IsNotExist(err) { + rootT.Fatalf("expected 'file not found' error, got: %s", err) + } +} - fileContents := make([]byte, len(ft.fileContents)) +func TestMergedLinksInMergedBlockCIMs(rootT *testing.T) { + root := rootT.TempDir() - // it is a file - read contents - rc, err := f.Read(fileContents) - if err != nil && !errors.Is(err, io.EOF) { - t.Fatalf("failure while reading file %s from cim: %s", ft.filepath, err) - } else if rc != len(ft.fileContents) { - t.Fatalf("couldn't read complete file contents for file: %s, read %d bytes, expected: %d", ft.filepath, rc, len(ft.fileContents)) - } else if !bytes.Equal(fileContents[:rc], ft.fileContents) { - t.Fatalf("contents of file %s don't match", ft.filepath) - } + testContents := []tuple{ + {"foobar.txt", []byte("foobar test data"), false}, + {"foo", []byte(""), true}, + {"foo\\bar.txt", []byte("bar test data"), false}, + } + + cim1 := &testBlockCIM{ + BlockCIM: BlockCIM{ + Type: BlockCIMTypeSingleFile, + BlockPath: filepath.Join(root, "1.bcim"), + CimName: "test.cim", + }, + } + writer := openNewCIM(rootT, cim1) + writeCIM(rootT, writer, testContents) + + cim2 := &testBlockCIM{ + BlockCIM: BlockCIM{ + Type: BlockCIMTypeSingleFile, + BlockPath: filepath.Join(root, "2.bcim"), + CimName: "test.cim", + }, + } + + cim2writer := openNewCIM(rootT, cim2) + + if err := cim2writer.AddMergedLink("foobar.txt", "b_link.txt"); err != nil { + rootT.Fatalf("failed to add merged link: %s", err) + } + if err := cim2writer.AddMergedLink("b_link.txt", "a_link.txt"); err != nil { + rootT.Fatalf("failed to add merged link: %s", err) + } + cim2writer.Close() + + mergedCIM := &BlockCIM{ + Type: BlockCIMTypeSingleFile, + BlockPath: filepath.Join(root, "merged.cim"), + CimName: "merged.cim", + } + + sourceCIMs := []*BlockCIM{&cim2.BlockCIM, &cim1.BlockCIM} + if err := MergeBlockCIMs(mergedCIM, sourceCIMs); err != nil { + rootT.Fatalf("failed to merge block CIMs: %s", err) + } + + // mount and read the contents of the cim + volumeGUID, err := guid.NewV4() + if err != nil { + rootT.Fatalf("generate cim mount GUID: %s", err) + } + + mountvol, err := MountMergedBlockCIMs(mergedCIM, sourceCIMs, CimMountSingleFileCim, volumeGUID) + if err != nil { + rootT.Fatalf("failed to mount merged block CIMs: %s\n", err) + } + defer func() { + if err := Unmount(mountvol); err != nil { + rootT.Logf("CIM unmount failed: %s", err) } + }() + + // read contents of "a_link.txt", they should match that of "foobar.txt" + data, err := os.ReadFile(filepath.Join(mountvol, "a_link.txt")) + if err != nil { + rootT.Logf("read file failed: %s", err) + } + if !bytes.Equal(data, testContents[0].fileContents) { + rootT.Logf("file contents don't match!") } } diff --git a/pkg/cimfs/cim_writer_windows.go b/pkg/cimfs/cim_writer_windows.go index 8e88216bfc..3aaf366d18 100644 --- a/pkg/cimfs/cim_writer_windows.go +++ b/pkg/cimfs/cim_writer_windows.go @@ -35,7 +35,8 @@ type CimFsWriter struct { } // Create creates a new cim image. The CimFsWriter returned can then be used to do -// operations on this cim. +// operations on this cim. If `oldFSName` is provided the new image is "forked" from the +// CIM with name `oldFSName` located under `imagePath`. func Create(imagePath string, oldFSName string, newFSName string) (_ *CimFsWriter, err error) { var oldNameBytes *uint16 // CimCreateImage API call has different behavior if the value of oldNameBytes / newNameBytes @@ -62,6 +63,41 @@ func Create(imagePath string, oldFSName string, newFSName string) (_ *CimFsWrite return &CimFsWriter{handle: handle, name: filepath.Join(imagePath, fsName)}, nil } +// Create creates a new blocked CIM and opens it for writing. The CimFsWriter +// returned can then be used to add/remove files to/from this CIM. +func CreateBlockCIM(blockPath, name string, blockType BlockCIMType) (_ *CimFsWriter, err error) { + if !IsBlockCimSupported() { + return nil, fmt.Errorf("block CIM not supported on this OS version") + } + if blockPath == "" || name == "" { + return nil, fmt.Errorf("both blockPath & name must be non empty: %w", os.ErrInvalid) + } + + // When creating block CIMs we always want them to be consistent CIMs i.e a CIMs + // created from the same layer tar will always be identical. + var createFlags uint32 = CimCreateFlagConsistentCim + switch blockType { + case BlockCIMTypeDevice: + createFlags |= CimCreateFlagBlockDeviceCim + case BlockCIMTypeSingleFile: + createFlags |= CimCreateFlagSingleFileCim + default: + return nil, fmt.Errorf("invalid block CIM type `%d`: %w", blockType, os.ErrInvalid) + } + + var newNameUTF16 *uint16 + newNameUTF16, err = windows.UTF16PtrFromString(name) + if err != nil { + return nil, err + } + + var handle winapi.FsHandle + if err := winapi.CimCreateImage2(blockPath, createFlags, nil, newNameUTF16, &handle); err != nil { + return nil, fmt.Errorf("failed to create block CIM at path %s,%s: %w", blockPath, name, err) + } + return &CimFsWriter{handle: handle, name: name}, nil +} + // CreateAlternateStream creates alternate stream of given size at the given path inside the cim. This will // replace the current active stream. Always, finish writing current active stream and then create an // alternate stream. @@ -160,7 +196,7 @@ func (c *CimFsWriter) Write(p []byte) (int, error) { return len(p), nil } -// AddLink adds a hard link from `oldPath` to `newPath` in the image. +// AddLink adds a hard link at `newPath` that points to `oldPath`. func (c *CimFsWriter) AddLink(oldPath string, newPath string) error { err := c.closeStream() if err != nil { @@ -173,21 +209,41 @@ func (c *CimFsWriter) AddLink(oldPath string, newPath string) error { return err } -// Unlink deletes the file at `path` from the image. +// AddMergedLink adds a hard link from `oldPath` to `newPath` in the image. However unlike +// AddLink this link is resolved at merge time. This allows us to create links to files +// that are in other CIMs. +func (c *CimFsWriter) AddMergedLink(oldPath string, newPath string) error { + err := c.closeStream() + if err != nil { + return err + } + err = winapi.CimCreateMergeLink(c.handle, newPath, oldPath) + if err != nil { + err = &LinkError{Cim: c.name, Op: "addMergedLink", Old: oldPath, New: newPath, Err: err} + } + return err +} + +// Unlink deletes the file at `path` from the image. Note that the file MUST have been +// already added to the image. func (c *CimFsWriter) Unlink(path string) error { err := c.closeStream() if err != nil { return err } - //TODO(ambarve): CimDeletePath currently returns an error if the file isn't found but we ideally want - // to put a tombstone at that path so that when cims are merged it removes that file from the lower - // layer - err = winapi.CimDeletePath(c.handle, path) - if err != nil && !os.IsNotExist(err) { - err = &PathError{Cim: c.name, Op: "unlink", Path: path, Err: err} + return winapi.CimDeletePath(c.handle, path) +} + +// Adds a tombstone at given path. This ensures that when the the CIMs are merged, the +// file at this path from lower layers won't show up in a mounted CIM. In case of Unlink, +// the file from the lower layers still shows up after merge. +func (c *CimFsWriter) AddTombstone(path string) error { + err := c.closeStream() + if err != nil { return err } - return nil + + return winapi.CimTombstoneFile(c.handle, path) } func (c *CimFsWriter) commit() error { @@ -210,15 +266,15 @@ func (c *CimFsWriter) Close() error { if err := c.commit(); err != nil { return &OpError{Cim: c.name, Op: "commit", Err: err} } - if err := winapi.CimCloseImage(c.handle); err != nil { - return &OpError{Cim: c.name, Op: "close", Err: err} - } + winapi.CimCloseImage(c.handle) c.handle = 0 return nil } -// DestroyCim finds out the region files, object files of this cim and then delete -// the region files, object files and the .cim file itself. +// DestroyCim finds out the region files, object files of this cim and then delete the +// region files, object files and the .cim file itself. Note that any other +// CIMs that were forked off of this CIM would become unusable after this operation. This +// should not be used for block CIMs, os.Remove is sufficient for block CIMs. func DestroyCim(ctx context.Context, cimPath string) (retErr error) { regionFilePaths, err := getRegionFilePaths(ctx, cimPath) if err != nil { @@ -289,3 +345,47 @@ func GetCimUsage(ctx context.Context, cimPath string) (uint64, error) { } return totalUsage, nil } + +// MergeBlockCIMs creates a new merged BlockCIM from the provided source BlockCIMs. CIM +// at index 0 is considered to be topmost CIM and the CIM at index `length-1` is +// considered the base CIM. (i.e file with the same path in CIM at index 0 will shadow +// files with the same path at all other CIMs) +// When mounting this merged CIM the source CIMs MUST be provided in the exact same order. +func MergeBlockCIMs(mergedCIM *BlockCIM, sourceCIMs []*BlockCIM) (err error) { + if !IsMergedCimSupported() { + return fmt.Errorf("merged CIMs aren't supported on this OS version") + } else if len(sourceCIMs) < 2 { + return fmt.Errorf("need at least 2 source CIMs, got %d: %w", len(sourceCIMs), os.ErrInvalid) + } + + var mergeFlag uint32 + switch mergedCIM.Type { + case BlockCIMTypeDevice: + mergeFlag = CimMergeFlagBlockDevice + case BlockCIMTypeSingleFile: + mergeFlag = CimMergeFlagSingleFile + default: + return fmt.Errorf("invalid block CIM type `%d`: %w", mergedCIM.Type, os.ErrInvalid) + } + + cim, err := CreateBlockCIM(mergedCIM.BlockPath, mergedCIM.CimName, mergedCIM.Type) + if err != nil { + return fmt.Errorf("create merged CIM: %w", err) + } + defer func() { + cErr := cim.Close() + if err == nil { + err = cErr + } + }() + + // CimAddFsToMergedImage expects that topmost CIM is added first and the bottom + // most CIM is added last. + for _, sCIM := range sourceCIMs { + fullPath := filepath.Join(sCIM.BlockPath, sCIM.CimName) + if err := winapi.CimAddFsToMergedImage2(cim.handle, fullPath, mergeFlag); err != nil { + return fmt.Errorf("add cim to merged image: %w", err) + } + } + return nil +} diff --git a/pkg/cimfs/cimfs.go b/pkg/cimfs/cimfs.go index 21cdf109bc..5f72d5ef40 100644 --- a/pkg/cimfs/cimfs.go +++ b/pkg/cimfs/cimfs.go @@ -4,6 +4,8 @@ package cimfs import ( + "path/filepath" + "github.com/Microsoft/hcsshim/osversion" "github.com/sirupsen/logrus" ) @@ -13,5 +15,77 @@ func IsCimFSSupported() bool { if err != nil { logrus.WithError(err).Warn("get build revision") } - return osversion.Build() == 20348 && rv >= 2031 + // TODO(ambarve): add proper version for post iron builds + return true || (osversion.Build() == 20348 && rv >= 2031) +} + +// IsBlockCimSupported returns true if block formatted CIMs (i.e block device CIM & +// single file CIM) are supported on the current OS build. +func IsBlockCimSupported() bool { + // TODO(ambarve): add proper version here + return true +} + +func IsMergedCimSupported() bool { + // TODO(ambarve): add proper version here + return true +} + +type BlockCIMType uint32 + +const ( + BlockCIMTypeNone BlockCIMType = iota + BlockCIMTypeSingleFile + BlockCIMTypeDevice + + CimMountFlagNone uint32 = 0x0 + CimMountFlagEnableDax uint32 = 0x2 + CimMountBlockDeviceCim uint32 = 0x10 + CimMountSingleFileCim uint32 = 0x20 + + CimCreateFlagNone uint32 = 0x0 + CimCreateFlagDoNotExpandPEImages uint32 = 0x1 + CimCreateFlagFixedSizeChunks uint32 = 0x2 + CimCreateFlagBlockDeviceCim uint32 = 0x4 + CimCreateFlagSingleFileCim uint32 = 0x8 + CimCreateFlagConsistentCim uint32 = 0x10 + + CimMergeFlagNone uint32 = 0x0 + CimMergeFlagSingleFile uint32 = 0x1 + CimMergeFlagBlockDevice uint32 = 0x2 +) + +// BlockCIM represents a CIM stored in a block formatted way. +// +// A CIM usually is made up of a .cim file and multiple region & objectID +// files. Currently, all of these files are stored together in the same directory. To +// refer to such a CIM, we provide the path to the `.cim` file and the corresponding +// region & objectID files are assumed to be present right next to it. In this case the +// directory on the host's filesystem which holds one or more such CIMs is the container +// for those CIMs. +// +// Using multiple files for a single CIM can be very limiting. (For example, if you want +// to do a remote mount for a CIM layer, you now need to mount multiple files for a single +// layer). In such cases having a single container which contains all of the CIM related +// data is a great option. For this reason, CimFS has added support for a new type of a +// CIM named BlockCIM. A BlockCIM is a CIM for which the container used to store all of +// the CIM files is a block device or a binary file formatted like a block device. Such a +// block device (or a binary file) doesn't have a separate filesystem (like NTFS or FAT32) +// on it. Instead it is formatted in such a way that CimFS driver can read the blocks and +// find out which CIMs are present on that block device. The CIMs stored on a raw block +// device are sometimes referred to as block device CIMs and CIMs stored on the block +// formatted single file are referred as single file CIMs. +type BlockCIM struct { + Type BlockCIMType + // BlockPath is a path to the block device or the single file which contains the + // CIM. + BlockPath string + // Since a block device CIM or a single file CIM can container multiple CIMs, we + // refer to an individual CIM using its name. + CimName string +} + +// added for logging convenience +func (b *BlockCIM) String() string { + return filepath.Join(b.BlockPath, b.CimName) } diff --git a/pkg/cimfs/doc.go b/pkg/cimfs/doc.go index 9b5476cb6c..bb9ce57717 100644 --- a/pkg/cimfs/doc.go +++ b/pkg/cimfs/doc.go @@ -1,3 +1,89 @@ -// This package provides simple go wrappers on top of the win32 CIMFS mount APIs. -// The mounting/unmount of cim layers is done by the cim mount functions the internal/wclayer/cim package. +/* +This package provides simple go wrappers on top of the win32 CIMFS APIs. + +Details about CimFS & related win32 APIs can be found here: +https://learn.microsoft.com/en-us/windows/win32/api/_cimfs/ + +Details about how CimFS is being used in containerd can be found here: +https://github.com/containerd/containerd/issues/8346 + +CIM types: +Currently we support 2 types of CIMs: + - Standard/classic (for the lack of a better term) CIMs. + - Block CIMs. + +Standard CIMs store all the contents of a CIM in one or more region & objectID files. This +means a single CIM is made up of a `.cim` file, one or more region files and one or more +objectID files. All of these files MUST be present in the same directory in order for that +CIM to work. Block CIMs store all the data of a CIM in a single block device. A VHD can be +such a block device. For convenience CimFS also allows using a block formatted file as a +block device. + +Standard CIMs can be created with the `func Create(imagePath string, oldFSName string, +newFSName string) (_ *CimFsWriter, err error)` function defined in this package, whereas +block CIMs can be created with the `func CreateBlockCIM(blockPath, oldName, newName +string, blockType BlockCIMType) (_ *CimFsWriter, err error)` function. + +Forking & Merging CIMs: +In container world, CIMs are used for storing container image layers. Usually, one layer +is stored in one CIM. This means we need a way to combine multiple CIMs to create the +rootfs of a container. This can be achieved either by forking the CIMs or merging the +CIMs. + +Forking CIMs: +Forking means every time a CIM is created for a non-base layer, we fork it off of a parent +layer CIM. This ensures that contents that are written to this CIM are merged with that of +parent layer CIMs at the time of CIM creation itself. When such a CIM is mounted we get a +combined view of the contents of this CIM as well as the parent CIM from which this CIM +was forked. However, this means that all the CIMs MUST be stored in the same directory in +order for forked CIMs to work. And every non-base layer CIM is dependent on all of its +parent layer CIMs. + +Merging CIMs: +If we create one or more CIMs without forking them at the time of creation, we can still +merge those CIMs later to create a new special type of CIM called merged CIM. When +mounted, this merged CIM provides a view of the combined contents of all the layers that +were merged. The advantage of this approach is that each layer CIM (also referred to as +source CIMs in the context of merging CIMs) can be created & stored independent of its +parent CIMs. (Currently we only support merging block CIMs). + +In order to create a merged CIM we need at least 2 non-forked block CIMs (we can not merge +forked & non-forked CIMs), these CIMs are also referred to as source CIMs. We first create +a new CIM (for storing the merge) via the `CreateBlockCIM` API, then call +`CimAddFsToMergedImage2` repeatedly to add the source CIMs one by one to the merged +CIM. Closing the handle on this new CIM commits it automatically. The order in which +source CIMs are added matters. A source CIM that was added before another source CIM takes +precedence when merging the CIM contents. Crating this merged CIM only combines the +metadata of all the source CIMs, however the actual data isn't copied to the merged +CIM. This is why when mounting the merged CIM, we still need to provide paths to the +source CIMs. + +`CimMergeMountImage` is used to mount a merged CIM. This API expects an array of paths of +the merged CIM and all the source CIMs. Note that the array MUST include the merged CIM +path at the 0th index and all the source CIMs in the same order in which they were added +at the time of creation of the merged CIM. For example, if we merged CIMs 1.cim & 2.cim by +first adding 1.cim (via CimAddFsToMergedImage) and then adding 2.cim, then the array +should be [merged.cim, 1.cim, 2.cim] + +Merged CIM specific APIs. + +`CimTombstoneFile`: is used for creating a tombstone file in a CIM. Tombstone file is +similar to a whiteout file used in case of overlayFS. A tombstone's primary use case is +for merged CIMs. When multiple source CIMs are merged, a tombstone file/directory ensures +that any files with the same path in the lower layers (i.e source CIMs that are added +after the CIM that has a tombstone) do not show up in the mounted filesystem view. For +example, imagine 1.cim has a file at path `foo/bar.txt` and 2.cim has a tombstone at path +`foo/bar.txt`. If a merged CIM is created by first adding 2.cim (via +CimAddFsToMergedImage) and then adding 1.cim and then when that merged CIM is mounted, +`foo/bar.txt` will not show up in the mounted filesystem. A tombstone isn't required when +using forked CIMs, because we can just call `CimDeletePath` to remove a file from the +lower layers in that case. However, that doesn't work for merged CIMs since at the time of +writing one of the source CIMs, we can't delete files from other source CIMs. + +`CimCreateMergeLink`: is used to create a file link that is resolved at the time of +merging CIMs. This is required if we want to create a hardlink in one source CIM that +points to a file in another source CIM. Such a hardlink can not be resolved at the time of +writing the source CIM. It can only be resolved at the time of merge. This API allows us +to create such cross layer hard links. +*/ package cimfs diff --git a/pkg/cimfs/mount_cim.go b/pkg/cimfs/mount_cim.go index ea7341b2f0..52d2a430ae 100644 --- a/pkg/cimfs/mount_cim.go +++ b/pkg/cimfs/mount_cim.go @@ -11,6 +11,7 @@ import ( "github.com/Microsoft/go-winio/pkg/guid" "github.com/Microsoft/hcsshim/internal/winapi" "github.com/pkg/errors" + "golang.org/x/sys/windows" ) type MountError struct { @@ -63,3 +64,44 @@ func Unmount(volumePath string) error { return nil } + +// MountMergedBlockCIMs mounts the given merged BlockCIM (usually created with +// `MergeBlockCIMs`) at a volume with given GUID. The `sourceCIMs` MUST be identical +// to the `sourceCIMs` passed to `MergeBlockCIMs` when creating this merged CIM. +func MountMergedBlockCIMs(mergedCIM *BlockCIM, sourceCIMs []*BlockCIM, mountFlags uint32, volumeGUID guid.GUID) (string, error) { + switch mergedCIM.Type { + case BlockCIMTypeDevice: + mountFlags |= CimMountBlockDeviceCim + case BlockCIMTypeSingleFile: + mountFlags |= CimMountSingleFileCim + default: + return "", fmt.Errorf("invalid block CIM type `%d`", mergedCIM.Type) + } + + // win32 mount merged CIM API expects an array of all CIMs. 0th entry in the array + // should be the merged CIM. All remaining entries should be the source CIM paths + // in the same order that was used while creating the merged CIM. + allcims := append([]*BlockCIM{mergedCIM}, sourceCIMs...) + cimsToMerge := []winapi.CimFsImagePath{} + for _, bcim := range allcims { + // Trailing backslashes cause problems-remove those + imageDir, err := windows.UTF16PtrFromString(strings.TrimRight(bcim.BlockPath, `\`)) + if err != nil { + return "", fmt.Errorf("convert string to utf16: %w", err) + } + cimName, err := windows.UTF16PtrFromString(bcim.CimName) + if err != nil { + return "", fmt.Errorf("convert string to utf16: %w", err) + } + + cimsToMerge = append(cimsToMerge, winapi.CimFsImagePath{ + ImageDir: imageDir, + ImageName: cimName, + }) + } + + if err := winapi.CimMergeMountImage(uint32(len(cimsToMerge)), &cimsToMerge[0], mountFlags, &volumeGUID); err != nil { + return "", &MountError{Cim: filepath.Join(mergedCIM.BlockPath, mergedCIM.CimName), Op: "MountMerged", Err: err} + } + return fmt.Sprintf("\\\\?\\Volume{%s}\\", volumeGUID.String()), nil +}