diff --git a/internal/hcs/schema2/cim_mount.go b/internal/hcs/schema2/cim_mount.go new file mode 100644 index 0000000000..81865e7ea4 --- /dev/null +++ b/internal/hcs/schema2/cim_mount.go @@ -0,0 +1,25 @@ +/* + * HCS API + * + * No description provided (generated by Swagger Codegen https://github.com/swagger-api/swagger-codegen) + * + * API version: 2.5 + * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) + */ + +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"` + VolumeGuid string `json:"VolumeGuid,omitempty"` + MountFlags uint32 `json:"MountFlags,omitempty"` +} diff --git a/internal/winapi/cimfs.go b/internal/winapi/cimfs.go new file mode 100644 index 0000000000..d04bffc1f9 --- /dev/null +++ b/internal/winapi/cimfs.go @@ -0,0 +1,45 @@ +package winapi + +import ( + "unsafe" + + "github.com/Microsoft/go-winio/pkg/guid" + "golang.org/x/sys/windows" +) + +type g = guid.GUID +type FsHandle uintptr +type StreamHandle uintptr + +type CimFsFileMetadata struct { + Attributes uint32 + FileSize int64 + + CreationTime windows.Filetime + LastWriteTime windows.Filetime + ChangeTime windows.Filetime + LastAccessTime windows.Filetime + + SecurityDescriptorBuffer unsafe.Pointer + SecurityDescriptorSize uint32 + + ReparseDataBuffer unsafe.Pointer + ReparseDataSize uint32 + + ExtendedAttributes unsafe.Pointer + EACount uint32 +} + +//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) (hr error) = cimfs.CimCloseImage? +//sys CimCommitImage(cimFSHandle FsHandle) (hr error) = cimfs.CimCommitImage? + +//sys CimCreateFile(cimFSHandle FsHandle, path string, file *CimFsFileMetadata, cimStreamHandle *StreamHandle) (hr error) = cimfs.CimCreateFile? +//sys CimCloseStream(cimStreamHandle StreamHandle) (hr error) = cimfs.CimCloseStream? +//sys CimWriteStream(cimStreamHandle StreamHandle, buffer uintptr, bufferSize uint32) (hr error) = cimfs.CimWriteStream? +//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? diff --git a/internal/winapi/utils.go b/internal/winapi/utils.go index a2da570707..93d633d490 100644 --- a/internal/winapi/utils.go +++ b/internal/winapi/utils.go @@ -80,3 +80,9 @@ func ConvertStringSetToSlice(buf []byte) ([]string, error) { } return nil, errors.New("string set malformed: missing null terminator at end of buffer") } + +// ParseUtf16LE parses a UTF-16LE byte array into a string (without passing +// through a uint16 or rune array). +func ParseUtf16LE(b []byte) string { + return windows.UTF16PtrToString((*uint16)(unsafe.Pointer(&b[0]))) +} diff --git a/internal/winapi/zsyscall_windows.go b/internal/winapi/zsyscall_windows.go index c607245eb3..5b467c4db4 100644 --- a/internal/winapi/zsyscall_windows.go +++ b/internal/winapi/zsyscall_windows.go @@ -43,6 +43,7 @@ var ( modadvapi32 = windows.NewLazySystemDLL("advapi32.dll") modbindfltapi = windows.NewLazySystemDLL("bindfltapi.dll") modcfgmgr32 = windows.NewLazySystemDLL("cfgmgr32.dll") + modcimfs = windows.NewLazySystemDLL("cimfs.dll") modiphlpapi = windows.NewLazySystemDLL("iphlpapi.dll") modkernel32 = windows.NewLazySystemDLL("kernel32.dll") modnetapi32 = windows.NewLazySystemDLL("netapi32.dll") @@ -55,6 +56,17 @@ 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") + procCimCloseImage = modcimfs.NewProc("CimCloseImage") + procCimCloseStream = modcimfs.NewProc("CimCloseStream") + procCimCommitImage = modcimfs.NewProc("CimCommitImage") + procCimCreateAlternateStream = modcimfs.NewProc("CimCreateAlternateStream") + procCimCreateFile = modcimfs.NewProc("CimCreateFile") + procCimCreateHardLink = modcimfs.NewProc("CimCreateHardLink") + procCimCreateImage = modcimfs.NewProc("CimCreateImage") + procCimDeletePath = modcimfs.NewProc("CimDeletePath") + procCimDismountImage = modcimfs.NewProc("CimDismountImage") + procCimMountImage = modcimfs.NewProc("CimMountImage") + procCimWriteStream = modcimfs.NewProc("CimWriteStream") procSetJobCompartmentId = modiphlpapi.NewProc("SetJobCompartmentId") procClosePseudoConsole = modkernel32.NewProc("ClosePseudoConsole") procCopyFileW = modkernel32.NewProc("CopyFileW") @@ -164,6 +176,235 @@ func _CMLocateDevNode(pdnDevInst *uint32, pDeviceID *uint16, uFlags uint32) (hr return } +func CimCloseImage(cimFSHandle FsHandle) (hr error) { + hr = procCimCloseImage.Find() + if hr != nil { + return + } + r0, _, _ := syscall.Syscall(procCimCloseImage.Addr(), 1, uintptr(cimFSHandle), 0, 0) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimCloseStream(cimStreamHandle StreamHandle) (hr error) { + hr = procCimCloseStream.Find() + if hr != nil { + return + } + r0, _, _ := syscall.Syscall(procCimCloseStream.Addr(), 1, uintptr(cimStreamHandle), 0, 0) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimCommitImage(cimFSHandle FsHandle) (hr error) { + hr = procCimCommitImage.Find() + if hr != nil { + return + } + r0, _, _ := syscall.Syscall(procCimCommitImage.Addr(), 1, uintptr(cimFSHandle), 0, 0) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimCreateAlternateStream(cimFSHandle FsHandle, path string, size uint64, cimStreamHandle *StreamHandle) (hr error) { + var _p0 *uint16 + _p0, hr = syscall.UTF16PtrFromString(path) + if hr != nil { + return + } + return _CimCreateAlternateStream(cimFSHandle, _p0, size, cimStreamHandle) +} + +func _CimCreateAlternateStream(cimFSHandle FsHandle, path *uint16, size uint64, cimStreamHandle *StreamHandle) (hr error) { + hr = procCimCreateAlternateStream.Find() + if hr != nil { + return + } + r0, _, _ := syscall.Syscall6(procCimCreateAlternateStream.Addr(), 4, uintptr(cimFSHandle), uintptr(unsafe.Pointer(path)), uintptr(size), uintptr(unsafe.Pointer(cimStreamHandle)), 0, 0) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimCreateFile(cimFSHandle FsHandle, path string, file *CimFsFileMetadata, cimStreamHandle *StreamHandle) (hr error) { + var _p0 *uint16 + _p0, hr = syscall.UTF16PtrFromString(path) + if hr != nil { + return + } + return _CimCreateFile(cimFSHandle, _p0, file, cimStreamHandle) +} + +func _CimCreateFile(cimFSHandle FsHandle, path *uint16, file *CimFsFileMetadata, cimStreamHandle *StreamHandle) (hr error) { + hr = procCimCreateFile.Find() + if hr != nil { + return + } + r0, _, _ := syscall.Syscall6(procCimCreateFile.Addr(), 4, uintptr(cimFSHandle), uintptr(unsafe.Pointer(path)), uintptr(unsafe.Pointer(file)), uintptr(unsafe.Pointer(cimStreamHandle)), 0, 0) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimCreateHardLink(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 _CimCreateHardLink(cimFSHandle, _p0, _p1) +} + +func _CimCreateHardLink(cimFSHandle FsHandle, newPath *uint16, oldPath *uint16) (hr error) { + hr = procCimCreateHardLink.Find() + if hr != nil { + return + } + r0, _, _ := syscall.Syscall(procCimCreateHardLink.Addr(), 3, 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 CimCreateImage(imagePath string, oldFSName *uint16, newFSName *uint16, cimFSHandle *FsHandle) (hr error) { + var _p0 *uint16 + _p0, hr = syscall.UTF16PtrFromString(imagePath) + if hr != nil { + return + } + return _CimCreateImage(_p0, oldFSName, newFSName, cimFSHandle) +} + +func _CimCreateImage(imagePath *uint16, oldFSName *uint16, newFSName *uint16, cimFSHandle *FsHandle) (hr error) { + hr = procCimCreateImage.Find() + if hr != nil { + return + } + r0, _, _ := syscall.Syscall6(procCimCreateImage.Addr(), 4, uintptr(unsafe.Pointer(imagePath)), uintptr(unsafe.Pointer(oldFSName)), uintptr(unsafe.Pointer(newFSName)), uintptr(unsafe.Pointer(cimFSHandle)), 0, 0) + 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) + if hr != nil { + return + } + return _CimDeletePath(cimFSHandle, _p0) +} + +func _CimDeletePath(cimFSHandle FsHandle, path *uint16) (hr error) { + hr = procCimDeletePath.Find() + if hr != nil { + return + } + r0, _, _ := syscall.Syscall(procCimDeletePath.Addr(), 2, uintptr(cimFSHandle), uintptr(unsafe.Pointer(path)), 0) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimDismountImage(volumeID *g) (hr error) { + hr = procCimDismountImage.Find() + if hr != nil { + return + } + r0, _, _ := syscall.Syscall(procCimDismountImage.Addr(), 1, uintptr(unsafe.Pointer(volumeID)), 0, 0) + 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) + if hr != nil { + return + } + var _p1 *uint16 + _p1, hr = syscall.UTF16PtrFromString(fsName) + if hr != nil { + return + } + return _CimMountImage(_p0, _p1, flags, volumeID) +} + +func _CimMountImage(imagePath *uint16, fsName *uint16, flags uint32, volumeID *g) (hr error) { + hr = procCimMountImage.Find() + if hr != nil { + return + } + r0, _, _ := syscall.Syscall6(procCimMountImage.Addr(), 4, uintptr(unsafe.Pointer(imagePath)), uintptr(unsafe.Pointer(fsName)), uintptr(flags), uintptr(unsafe.Pointer(volumeID)), 0, 0) + 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 { + return + } + r0, _, _ := syscall.Syscall(procCimWriteStream.Addr(), 3, uintptr(cimStreamHandle), uintptr(buffer), uintptr(bufferSize)) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + func SetJobCompartmentId(handle windows.Handle, compartmentId uint32) (win32Err error) { r0, _, _ := syscall.Syscall(procSetJobCompartmentId.Addr(), 2, uintptr(handle), uintptr(compartmentId), 0) if r0 != 0 { diff --git a/osversion/osversion_windows.go b/osversion/osversion_windows.go index 6c435d2b64..56df44551f 100644 --- a/osversion/osversion_windows.go +++ b/osversion/osversion_windows.go @@ -5,6 +5,7 @@ import ( "sync" "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" ) // OSVersion is a wrapper for Windows version information @@ -57,3 +58,18 @@ func (osv OSVersion) String() string { func (osv OSVersion) ToString() string { return osv.String() } + +// Running `cmd /c ver` shows something like "10.0.20348.1000". The last component ("1000") is the revision +// number +func BuildRevision() (uint32, error) { + k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) + if err != nil { + return 0, fmt.Errorf("open `CurrentVersion` registry key: %w", err) + } + defer k.Close() + s, _, err := k.GetIntegerValue("UBR") + if err != nil { + return 0, fmt.Errorf("read `UBR` from registry: %w", err) + } + return uint32(s), nil +} diff --git a/pkg/cimfs/cim_test.go b/pkg/cimfs/cim_test.go new file mode 100644 index 0000000000..c87585cf87 --- /dev/null +++ b/pkg/cimfs/cim_test.go @@ -0,0 +1,144 @@ +//go:build windows +// +build windows + +package cimfs + +import ( + "bytes" + "context" + "fmt" + "io" + + "os" + "path/filepath" + "testing" + "time" + + "github.com/Microsoft/go-winio" + "golang.org/x/sys/windows" +) + +// A simple tuple type used to hold information about a file/directory that is created +// during a test. +type tuple struct { + filepath string + fileContents []byte + isDir bool +} + +// 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 + fileInfo := &winio.FileBasicInfo{ + CreationTime: windows.NsecToFiletime(time.Now().UnixNano()), + LastAccessTime: windows.NsecToFiletime(time.Now().UnixNano()), + LastWriteTime: windows.NsecToFiletime(time.Now().UnixNano()), + ChangeTime: windows.NsecToFiletime(time.Now().UnixNano()), + FileAttributes: 0, + } + if fileTuple.isDir { + fileInfo.FileAttributes = windows.FILE_ATTRIBUTE_DIRECTORY + } + + if err := c.AddFile(filepath.FromSlash(fileTuple.filepath), fileInfo, int64(len(fileTuple.fileContents)), []byte{}, []byte{}, []byte{}); err != nil { + return err + } + + if !fileTuple.isDir { + wc, err := c.Write(fileTuple.fileContents) + if err != nil || wc != len(fileTuple.fileContents) { + if err == nil { + return fmt.Errorf("unable to finish writing to file %s", fileTuple.filepath) + } else { + return err + } + } + } + return nil +} + +// 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 +// / +// |- foobar.txt +// |- foo +// |--- bar.txt +func TestCimReadWrite(t *testing.T) { + if IsCimFsSupported() { + t.Skipf("CimFs not supported") + } + + testContents := []tuple{ + {"foobar.txt", []byte("foobar test data"), false}, + {"foo", []byte(""), true}, + {"foo\\bar.txt", []byte("bar test data"), false}, + } + + tempDir := t.TempDir() + + cimName := "test.cim" + cimPath := filepath.Join(tempDir, cimName) + c, err := Create(tempDir, "", cimName) + if err != nil { + t.Fatalf("failed while creating a cim: %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 { + if err != nil { + t.Fatalf("destroy cim failed: %s", err) + } + } + }() + + 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 := c.Close(); err != nil { + t.Fatalf("cim close: %s", err) + } + + // mount and read the contents of the cim + mountvol, err := Mount(cimPath) + if err != nil { + t.Fatalf("mount cim : %s", err) + } + defer func() { + if err := Unmount(mountvol); err != nil { + t.Fatalf("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() + + fileContents := make([]byte, len(ft.fileContents)) + + // it is a file - read contents + rc, err := f.Read(fileContents) + if err != nil && 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) + } + } + } + +} diff --git a/pkg/cimfs/cim_writer_windows.go b/pkg/cimfs/cim_writer_windows.go new file mode 100644 index 0000000000..b07728f9e3 --- /dev/null +++ b/pkg/cimfs/cim_writer_windows.go @@ -0,0 +1,281 @@ +//go:build windows +// +build windows + +package cimfs + +import ( + "context" + "fmt" + "os" + "path/filepath" + "unsafe" + + "github.com/Microsoft/go-winio" + "github.com/Microsoft/hcsshim/internal/log" + "github.com/Microsoft/hcsshim/internal/winapi" + "golang.org/x/sys/windows" +) + +// CimFsWriter represents a writer to a single CimFS filesystem instance. On disk, the +// image is composed of a filesystem file and several object ID and region files. +// Note: The CimFsWriter isn't thread safe! +type CimFsWriter struct { + // name of this cim. Usually a .cim file will be created to represent this cim. + name string + // handle is the CIMFS_IMAGE_HANDLE that must be passed when calling CIMFS APIs. + handle winapi.FsHandle + // name of the active file i.e the file to which we are currently writing. + activeName string + // stream to currently active file. + activeStream winapi.StreamHandle + // amount of bytes that can be written to the activeStream. + activeLeft uint64 +} + +// Create creates a new cim image. The CimFsWriter returned can then be used to do +// operations on this cim. +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 + // is empty than if it is nil. So we have to convert those strings into *uint16 here. + fsName := oldFSName + if oldFSName != "" { + oldNameBytes, err = windows.UTF16PtrFromString(oldFSName) + if err != nil { + return nil, err + } + } + var newNameBytes *uint16 + if newFSName != "" { + fsName = newFSName + newNameBytes, err = windows.UTF16PtrFromString(newFSName) + if err != nil { + return nil, err + } + } + var handle winapi.FsHandle + if err := winapi.CimCreateImage(imagePath, oldNameBytes, newNameBytes, &handle); err != nil { + return nil, fmt.Errorf("failed to create cim image at path %s, oldName: %s, newName: %s: %w", imagePath, oldFSName, newFSName, err) + } + return &CimFsWriter{handle: handle, name: filepath.Join(imagePath, fsName)}, 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. +func (c *CimFsWriter) CreateAlternateStream(path string, size uint64) (err error) { + err = c.closeStream() + if err != nil { + return err + } + err = winapi.CimCreateAlternateStream(c.handle, path, size, &c.activeStream) + if err != nil { + return fmt.Errorf("failed to create alternate stream for path %s: %w", path, err) + } + c.activeName = path + return nil +} + +// closes the currently active stream +func (c *CimFsWriter) closeStream() error { + if c.activeStream == 0 { + return nil + } + err := winapi.CimCloseStream(c.activeStream) + if err == nil && c.activeLeft > 0 { + // Validate here because CimCloseStream does not and this improves error + // reporting. Otherwise the error will occur in the context of + // cimWriteStream. + err = fmt.Errorf("incomplete write, %d bytes left in the stream %s", c.activeLeft, c.activeName) + } + if err != nil { + err = &PathError{Cim: c.name, Op: "closeStream", Path: c.activeName, Err: err} + } + c.activeLeft = 0 + c.activeStream = 0 + c.activeName = "" + return err +} + +// AddFile adds a new file to the image. The file is added at the specified path. After +// calling this function, the file is set as the active stream for the image, so data can +// be written by calling `Write`. +func (c *CimFsWriter) AddFile(path string, info *winio.FileBasicInfo, fileSize int64, securityDescriptor []byte, extendedAttributes []byte, reparseData []byte) error { + err := c.closeStream() + if err != nil { + return err + } + fileMetadata := &winapi.CimFsFileMetadata{ + Attributes: info.FileAttributes, + FileSize: fileSize, + CreationTime: info.CreationTime, + LastWriteTime: info.LastWriteTime, + ChangeTime: info.ChangeTime, + LastAccessTime: info.LastAccessTime, + } + if len(securityDescriptor) == 0 { + // Passing an empty security descriptor creates a CIM in a weird state. + // Pass the NULL DACL. + securityDescriptor = nullSd + } + fileMetadata.SecurityDescriptorBuffer = unsafe.Pointer(&securityDescriptor[0]) + fileMetadata.SecurityDescriptorSize = uint32(len(securityDescriptor)) + if len(reparseData) > 0 { + fileMetadata.ReparseDataBuffer = unsafe.Pointer(&reparseData[0]) + fileMetadata.ReparseDataSize = uint32(len(reparseData)) + } + if len(extendedAttributes) > 0 { + fileMetadata.ExtendedAttributes = unsafe.Pointer(&extendedAttributes[0]) + fileMetadata.EACount = uint32(len(extendedAttributes)) + } + err = winapi.CimCreateFile(c.handle, path, fileMetadata, &c.activeStream) + if err != nil { + return &PathError{Cim: c.name, Op: "addFile", Path: path, Err: err} + } + c.activeName = path + if info.FileAttributes&(windows.FILE_ATTRIBUTE_DIRECTORY) == 0 { + c.activeLeft = uint64(fileSize) + } + return nil +} + +// Write writes bytes to the active stream. +func (c *CimFsWriter) Write(p []byte) (int, error) { + if c.activeStream == 0 { + return 0, fmt.Errorf("no active stream") + } + if uint64(len(p)) > c.activeLeft { + return 0, &PathError{Cim: c.name, Op: "write", Path: c.activeName, Err: fmt.Errorf("wrote too much")} + } + err := winapi.CimWriteStream(c.activeStream, uintptr(unsafe.Pointer(&p[0])), uint32(len(p))) + if err != nil { + err = &PathError{Cim: c.name, Op: "write", Path: c.activeName, Err: err} + return 0, err + } + c.activeLeft -= uint64(len(p)) + return len(p), nil +} + +// AddLink adds a hard link from `oldPath` to `newPath` in the image. +func (c *CimFsWriter) AddLink(oldPath string, newPath string) error { + err := c.closeStream() + if err != nil { + return err + } + err = winapi.CimCreateHardLink(c.handle, newPath, oldPath) + if err != nil { + err = &LinkError{Cim: c.name, Op: "addLink", Old: oldPath, New: newPath, Err: err} + } + return err +} + +// Unlink deletes the file at `path` from 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 err + } + return nil +} + +func (c *CimFsWriter) commit() error { + err := c.closeStream() + if err != nil { + return err + } + err = winapi.CimCommitImage(c.handle) + if err != nil { + err = &OpError{Cim: c.name, Op: "commit", Err: err} + } + return err +} + +// Close closes the CimFS filesystem. +func (c *CimFsWriter) Close() error { + if c.handle == 0 { + return fmt.Errorf("invalid writer") + } + 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} + } + 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. +func DestroyCim(ctx context.Context, cimPath string) (retErr error) { + regionFilePaths, err := getRegionFilePaths(ctx, cimPath) + if err != nil { + log.G(ctx).WithError(err).Warnf("get region files for cim %s", cimPath) + if retErr == nil { + retErr = err + } + } + objectFilePaths, err := getObjectIDFilePaths(ctx, cimPath) + if err != nil { + log.G(ctx).WithError(err).Warnf("get objectid file for cim %s", cimPath) + if retErr == nil { + retErr = err + } + } + + for _, regFilePath := range regionFilePaths { + if err := os.Remove(regFilePath); err != nil { + log.G(ctx).WithError(err).Warnf("remove file %s", regFilePath) + if retErr == nil { + retErr = err + } + } + } + + for _, objFilePath := range objectFilePaths { + if err := os.Remove(objFilePath); err != nil { + log.G(ctx).WithError(err).Warnf("remove file %s", objFilePath) + if retErr == nil { + retErr = err + } + } + } + + if err := os.Remove(cimPath); err != nil { + log.G(ctx).WithError(err).Warnf("remove file %s", cimPath) + if retErr == nil { + retErr = err + } + } + return retErr +} + +// GetCimUsage returns the total disk usage in bytes by the cim at path `cimPath`. +func GetCimUsage(ctx context.Context, cimPath string) (uint64, error) { + regionFilePaths, err := getRegionFilePaths(ctx, cimPath) + if err != nil { + return 0, fmt.Errorf("get region file paths for cim %s: %w", cimPath, err) + } + objectFilePaths, err := getObjectIDFilePaths(ctx, cimPath) + if err != nil { + return 0, fmt.Errorf("get objectid file for cim %s: %w", cimPath, err) + } + + var totalUsage uint64 + for _, f := range append(regionFilePaths, objectFilePaths...) { + fi, err := os.Stat(f) + if err != nil { + return 0, fmt.Errorf("stat file %s: %w", f, err) + } + totalUsage += uint64(fi.Size()) + } + return totalUsage, nil +} diff --git a/pkg/cimfs/cimfs.go b/pkg/cimfs/cimfs.go new file mode 100644 index 0000000000..2d80e23023 --- /dev/null +++ b/pkg/cimfs/cimfs.go @@ -0,0 +1,17 @@ +//go:build windows +// +build windows + +package cimfs + +import ( + "github.com/Microsoft/hcsshim/osversion" + "github.com/sirupsen/logrus" +) + +func IsCimFsSupported() bool { + rv, err := osversion.BuildRevision() + if err != nil { + logrus.WithError(err).Warn("get build revision") + } + return osversion.Get().Version == 20348 && rv >= 1700 +} diff --git a/pkg/cimfs/common.go b/pkg/cimfs/common.go new file mode 100644 index 0000000000..9b53183265 --- /dev/null +++ b/pkg/cimfs/common.go @@ -0,0 +1,134 @@ +//go:build windows +// +build windows + +package cimfs + +import ( + "bytes" + "context" + "encoding/binary" + "fmt" + "os" + "path/filepath" + + "github.com/Microsoft/hcsshim/internal/log" + "github.com/Microsoft/hcsshim/pkg/cimfs/format" +) + +var ( + // Equivalent to SDDL of "D:NO_ACCESS_CONTROL" + nullSd = []byte{1, 0, 4, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} +) + +type OpError struct { + Cim string + Op string + Err error +} + +func (e *OpError) Error() string { + s := "cim " + e.Op + " " + e.Cim + s += ": " + e.Err.Error() + return s +} + +// PathError is the error type returned by most functions in this package. +type PathError struct { + Cim string + Op string + Path string + Err error +} + +func (e *PathError) Error() string { + s := "cim " + e.Op + " " + e.Cim + s += ":" + e.Path + s += ": " + e.Err.Error() + return s +} + +type LinkError struct { + Cim string + Op string + Old string + New string + Err error +} + +func (e *LinkError) Error() string { + return "cim " + e.Op + " " + e.Old + " " + e.New + ": " + e.Err.Error() +} + +func validateHeader(h *format.CommonHeader) error { + if !bytes.Equal(h.Magic[:], format.MagicValue[:]) { + return fmt.Errorf("not a cim file") + } + if h.Version.Major > format.CurrentVersion.Major || h.Version.Major < format.MinSupportedVersion.Major { + return fmt.Errorf("unsupported cim version. cim version %v must be between %v & %v", h.Version, format.MinSupportedVersion, format.CurrentVersion) + } + return nil +} + +func readFilesystemHeader(f *os.File) (format.FilesystemHeader, error) { + var fsh format.FilesystemHeader + + if err := binary.Read(f, binary.LittleEndian, &fsh); err != nil { + return fsh, fmt.Errorf("reading filesystem header: %w", err) + } + + if err := validateHeader(&fsh.Common); err != nil { + return fsh, fmt.Errorf("validating filesystem header: %s", err) + } + return fsh, nil +} + +// Returns the paths of all the objectID files associated with the cim at `cimPath`. +func getObjectIDFilePaths(ctx context.Context, cimPath string) ([]string, error) { + f, err := os.Open(cimPath) + if err != nil { + return []string{}, fmt.Errorf("open cim file %s: %w", cimPath, err) + } + defer f.Close() + + fsh, err := readFilesystemHeader(f) + if err != nil { + return []string{}, fmt.Errorf("readingp cim header: %w", err) + } + + paths := []string{} + for i := 0; i < int(fsh.Regions.Count); i++ { + path := filepath.Join(filepath.Dir(cimPath), fmt.Sprintf("%s_%v_%d", format.ObjectIDFileName, fsh.Regions.ID, i)) + if _, err := os.Stat(path); err == nil { + paths = append(paths, path) + } else { + log.G(ctx).WithError(err).Warnf("stat for object file %s", path) + } + + } + return paths, nil +} + +// Returns the paths of all the region files associated with the cim at `cimPath`. +func getRegionFilePaths(ctx context.Context, cimPath string) ([]string, error) { + f, err := os.Open(cimPath) + if err != nil { + return []string{}, fmt.Errorf("open cim file %s: %w", cimPath, err) + } + defer f.Close() + + fsh, err := readFilesystemHeader(f) + if err != nil { + return []string{}, fmt.Errorf("reading cim header: %w", err) + } + + paths := []string{} + for i := 0; i < int(fsh.Regions.Count); i++ { + path := filepath.Join(filepath.Dir(cimPath), fmt.Sprintf("%s_%v_%d", format.RegionFileName, fsh.Regions.ID, i)) + if _, err := os.Stat(path); err == nil { + paths = append(paths, path) + } else { + log.G(ctx).WithError(err).Warnf("stat for region file %s", path) + } + } + return paths, nil +} diff --git a/pkg/cimfs/format/format.go b/pkg/cimfs/format/format.go new file mode 100644 index 0000000000..1579beff3e --- /dev/null +++ b/pkg/cimfs/format/format.go @@ -0,0 +1,64 @@ +//go:build windows +// +build windows + +// format package maintains some basic structures to allows us to read header of a cim file. This is mostly +// required to understand the region & objectid files associated with a particular cim. Otherwise, we don't +// need to parse the cim format. +package format + +import "github.com/Microsoft/go-winio/pkg/guid" + +const ( + RegionFileName = "region" + ObjectIDFileName = "objectid" +) + +// Magic specifies the magic number at the beginning of a file. +type Magic [8]uint8 + +var MagicValue = Magic([8]uint8{'c', 'i', 'm', 'f', 'i', 'l', 'e', '0'}) + +type Version struct { + Major, Minor uint32 +} + +var CurrentVersion = Version{3, 0} + +var MinSupportedVersion = Version{2, 0} + +type FileType uint8 + +// RegionOffset encodes an offset to objects as index of the region file +// containing the object and the byte offset within that file. +type RegionOffset uint64 + +// CommonHeader is the common header for all CIM-related files. +type CommonHeader struct { + Magic Magic + HeaderLength uint32 + Type FileType + Reserved uint8 + Reserved2 uint16 + Version Version + Reserved3 uint64 +} + +type RegionSet struct { + ID guid.GUID + Count uint16 + Reserved uint16 + Reserved1 uint32 +} + +// FilesystemHeader is the header for a filesystem file. +// +// The filesystem file points to the filesystem object inside a region +// file and specifies regions sets. +type FilesystemHeader struct { + Common CommonHeader + Regions RegionSet + FilesystemOffset RegionOffset + Reserved uint32 + Reserved1 uint16 + ParentCount uint16 +} diff --git a/pkg/cimfs/mount_cim.go b/pkg/cimfs/mount_cim.go new file mode 100644 index 0000000000..0e7d8f9ac0 --- /dev/null +++ b/pkg/cimfs/mount_cim.go @@ -0,0 +1,78 @@ +//go:build windows +// +build windows + +package cimfs + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/Microsoft/go-winio/pkg/guid" + hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2" + "github.com/Microsoft/hcsshim/internal/winapi" + "github.com/pkg/errors" +) + +// 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 in internal/wclayer/cim package. + +type MountError struct { + Cim string + Op string + VolumeGUID guid.GUID + Err error +} + +func (e *MountError) Error() string { + s := "cim " + e.Op + if e.Cim != "" { + s += " " + e.Cim + } + s += " " + e.VolumeGUID.String() + ": " + e.Err.Error() + return s +} + +func MountWithFlags(cimPath string, mountFlags uint32) (string, error) { + layerGUID, err := guid.NewV4() + if err != nil { + return "", &MountError{Cim: cimPath, Op: "Mount", Err: err} + } + if err := winapi.CimMountImage(filepath.Dir(cimPath), filepath.Base(cimPath), mountFlags, &layerGUID); err != nil { + return "", &MountError{Cim: cimPath, Op: "Mount", VolumeGUID: layerGUID, Err: err} + } + return fmt.Sprintf("\\\\?\\Volume{%s}\\", layerGUID.String()), nil +} + +// Mount mounts the cim at path `cimPath` and returns the mount location of that cim. This method uses the +// `CimMountFlagCacheRegions` mount flag when mounting the cim, if some other mount flag is desired use the +// `MountWithFlags` method. +func Mount(cimPath string) (string, error) { + return MountWithFlags(cimPath, hcsschema.CimMountFlagCacheFiles) +} + +// Unmount unmounts the cim at mounted at path `volumePath`. +func Unmount(volumePath string) error { + // The path is expected to be in the \\?\Volume{GUID}\ format + if volumePath[len(volumePath)-1] != '\\' { + volumePath += "\\" + } + + if !(strings.HasPrefix(volumePath, "\\\\?\\Volume{") && strings.HasSuffix(volumePath, "}\\")) { + return errors.Errorf("volume path %s is not in the expected format", volumePath) + } + + trimmedStr := strings.TrimPrefix(volumePath, "\\\\?\\Volume{") + trimmedStr = strings.TrimSuffix(trimmedStr, "}\\") + + volGUID, err := guid.FromString(trimmedStr) + if err != nil { + return errors.Wrapf(err, "guid parsing failed for %s", trimmedStr) + } + + if err := winapi.CimDismountImage(&volGUID); err != nil { + return &MountError{VolumeGUID: volGUID, Op: "Unmount", Err: err} + } + + return nil +}