...

Source file src/github.com/Microsoft/hcsshim/internal/uvm/vsmb.go

Documentation: github.com/Microsoft/hcsshim/internal/uvm

     1  //go:build windows
     2  
     3  package uvm
     4  
     5  import (
     6  	"context"
     7  	"fmt"
     8  	"os"
     9  	"path/filepath"
    10  	"strconv"
    11  	"unsafe"
    12  
    13  	"github.com/sirupsen/logrus"
    14  	"golang.org/x/sys/windows"
    15  
    16  	"github.com/Microsoft/hcsshim/internal/hcs"
    17  	"github.com/Microsoft/hcsshim/internal/hcs/resourcepaths"
    18  	hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2"
    19  	"github.com/Microsoft/hcsshim/internal/log"
    20  	"github.com/Microsoft/hcsshim/internal/protocol/guestrequest"
    21  	"github.com/Microsoft/hcsshim/internal/winapi"
    22  	"github.com/Microsoft/hcsshim/osversion"
    23  )
    24  
    25  const (
    26  	vsmbSharePrefix = `\\?\VMSMB\VSMB-{dcc079ae-60ba-4d07-847c-3493609c0870}\`
    27  )
    28  
    29  // VSMBShare contains the host path for a Vsmb Mount
    30  type VSMBShare struct {
    31  	// UVM the resource belongs to
    32  	vm           *UtilityVM
    33  	HostPath     string
    34  	refCount     uint32
    35  	name         string
    36  	allowedFiles []string
    37  	guestPath    string
    38  	options      hcsschema.VirtualSmbShareOptions
    39  }
    40  
    41  // Release frees the resources of the corresponding vsmb Mount
    42  func (vsmb *VSMBShare) Release(ctx context.Context) error {
    43  	if err := vsmb.vm.RemoveVSMB(ctx, vsmb.HostPath, vsmb.options.ReadOnly); err != nil {
    44  		return fmt.Errorf("failed to remove VSMB share: %s", err)
    45  	}
    46  	return nil
    47  }
    48  
    49  // DefaultVSMBOptions returns the default VSMB options. If readOnly is specified,
    50  // returns the default VSMB options for a readonly share.
    51  func (uvm *UtilityVM) DefaultVSMBOptions(readOnly bool) *hcsschema.VirtualSmbShareOptions {
    52  	opts := &hcsschema.VirtualSmbShareOptions{
    53  		NoDirectmap: uvm.DevicesPhysicallyBacked() || uvm.VSMBNoDirectMap(),
    54  	}
    55  	if readOnly {
    56  		opts.ShareRead = true
    57  		opts.CacheIo = true
    58  		opts.ReadOnly = true
    59  		opts.PseudoOplocks = true
    60  	}
    61  	return opts
    62  }
    63  
    64  // findVSMBShare finds a share by `hostPath`. If not found returns `ErrNotAttached`.
    65  func (uvm *UtilityVM) findVSMBShare(ctx context.Context, m map[string]*VSMBShare, shareKey string) (*VSMBShare, error) {
    66  	share, ok := m[shareKey]
    67  	if !ok {
    68  		return nil, ErrNotAttached
    69  	}
    70  	return share, nil
    71  }
    72  
    73  // openHostPath opens the given path and returns the handle. The handle is opened with
    74  // full sharing and no access mask. The directory must already exist. This
    75  // function is intended to return a handle suitable for use with GetFileInformationByHandleEx.
    76  //
    77  // We are not able to use builtin Go functionality for opening a directory path:
    78  //
    79  //   - os.Open on a directory returns a os.File where Fd() is a search handle from FindFirstFile.
    80  //   - syscall.Open does not provide a way to specify FILE_FLAG_BACKUP_SEMANTICS, which is needed to
    81  //     open a directory.
    82  //
    83  // We could use os.Open if the path is a file, but it's easier to just use the same code for both.
    84  // Therefore, we call windows.CreateFile directly.
    85  func openHostPath(path string) (windows.Handle, error) {
    86  	u16, err := windows.UTF16PtrFromString(path)
    87  	if err != nil {
    88  		return 0, err
    89  	}
    90  	h, err := windows.CreateFile(
    91  		u16,
    92  		0,
    93  		windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
    94  		nil,
    95  		windows.OPEN_EXISTING,
    96  		windows.FILE_FLAG_BACKUP_SEMANTICS,
    97  		0)
    98  	if err != nil {
    99  		return 0, &os.PathError{
   100  			Op:   "CreateFile",
   101  			Path: path,
   102  			Err:  err,
   103  		}
   104  	}
   105  	return h, nil
   106  }
   107  
   108  // In 19H1, a change was made to VSMB to require querying file ID for the files being shared in
   109  // order to support direct map. This change was made to ensure correctness in cases where direct
   110  // map is used with saving/restoring VMs.
   111  //
   112  // However, certain file systems (such as Azure Files SMB shares) don't support the FileIdInfo
   113  // query that is used. Azure Files in particular fails with ERROR_INVALID_PARAMETER. This issue
   114  // affects at least 19H1, 19H2, 20H1, and 20H2.
   115  //
   116  // To work around this, we attempt to query for FileIdInfo ourselves if on an affected build. If
   117  // the query fails, we override the specified options to force no direct map to be used.
   118  func forceNoDirectMap(path string) (bool, error) {
   119  	if ver := osversion.Build(); ver < osversion.V19H1 || ver > osversion.V20H2 {
   120  		return false, nil
   121  	}
   122  	h, err := openHostPath(path)
   123  	if err != nil {
   124  		return false, err
   125  	}
   126  	defer func() {
   127  		_ = windows.CloseHandle(h)
   128  	}()
   129  	var info winapi.FILE_ID_INFO
   130  	// We check for any error, rather than just ERROR_INVALID_PARAMETER. It seems better to also
   131  	// fall back if e.g. some other backing filesystem is used which returns a different error.
   132  	if err := windows.GetFileInformationByHandleEx(h, winapi.FileIdInfo, (*byte)(unsafe.Pointer(&info)), uint32(unsafe.Sizeof(info))); err != nil {
   133  		return true, nil
   134  	}
   135  	return false, nil
   136  }
   137  
   138  // AddVSMB adds a VSMB share to a Windows utility VM. Each VSMB share is ref-counted and
   139  // only added if it isn't already. This is used for read-only layers, mapped directories
   140  // to a container, and for mapped pipes.
   141  func (uvm *UtilityVM) AddVSMB(ctx context.Context, hostPath string, options *hcsschema.VirtualSmbShareOptions) (*VSMBShare, error) {
   142  	if uvm.operatingSystem != "windows" {
   143  		return nil, errNotSupported
   144  	}
   145  
   146  	if !options.ReadOnly && uvm.NoWritableFileShares() {
   147  		return nil, fmt.Errorf("adding writable shares is denied: %w", hcs.ErrOperationDenied)
   148  	}
   149  
   150  	uvm.m.Lock()
   151  	defer uvm.m.Unlock()
   152  
   153  	// Temporary support to allow single-file mapping. If `hostPath` is a
   154  	// directory, map it without restriction. However, if it is a file, map the
   155  	// directory containing the file, and use `AllowedFileList` to only allow
   156  	// access to that file. If the directory has been mapped before for
   157  	// single-file use, add the new file to the `AllowedFileList` and issue an
   158  	// Update operation.
   159  	st, err := os.Stat(hostPath)
   160  	if err != nil {
   161  		return nil, err
   162  	}
   163  	var file string
   164  	m := uvm.vsmbDirShares
   165  	if !st.IsDir() {
   166  		m = uvm.vsmbFileShares
   167  		file = hostPath
   168  		hostPath = filepath.Dir(hostPath)
   169  		options.RestrictFileAccess = true
   170  		options.SingleFileMapping = true
   171  	}
   172  	hostPath = filepath.Clean(hostPath)
   173  
   174  	if force, err := forceNoDirectMap(hostPath); err != nil {
   175  		return nil, err
   176  	} else if force {
   177  		log.G(ctx).WithField("path", hostPath).Info("Forcing NoDirectmap for VSMB mount")
   178  		options.NoDirectmap = true
   179  	}
   180  
   181  	var requestType = guestrequest.RequestTypeUpdate
   182  	shareKey := getVSMBShareKey(hostPath, options.ReadOnly)
   183  	share, err := uvm.findVSMBShare(ctx, m, shareKey)
   184  	if err == ErrNotAttached {
   185  		requestType = guestrequest.RequestTypeAdd
   186  		uvm.vsmbCounter++
   187  		shareName := "s" + strconv.FormatUint(uvm.vsmbCounter, 16)
   188  
   189  		share = &VSMBShare{
   190  			vm:        uvm,
   191  			name:      shareName,
   192  			guestPath: vsmbSharePrefix + shareName,
   193  			HostPath:  hostPath,
   194  		}
   195  	}
   196  	newAllowedFiles := share.allowedFiles
   197  	if options.RestrictFileAccess {
   198  		newAllowedFiles = append(newAllowedFiles, file)
   199  	}
   200  
   201  	// Update on a VSMB share currently only supports updating the
   202  	// AllowedFileList, and in fact will return an error if RestrictFileAccess
   203  	// isn't set (e.g. if used on an unrestricted share). So we only call Modify
   204  	// if we are either doing an Add, or if RestrictFileAccess is set.
   205  	if requestType == guestrequest.RequestTypeAdd || options.RestrictFileAccess {
   206  		log.G(ctx).WithFields(logrus.Fields{
   207  			"name":      share.name,
   208  			"path":      hostPath,
   209  			"options":   fmt.Sprintf("%+#v", options),
   210  			"operation": requestType,
   211  		}).Info("Modifying VSMB share")
   212  		modification := &hcsschema.ModifySettingRequest{
   213  			RequestType: requestType,
   214  			Settings: hcsschema.VirtualSmbShare{
   215  				Name:         share.name,
   216  				Options:      options,
   217  				Path:         hostPath,
   218  				AllowedFiles: newAllowedFiles,
   219  			},
   220  			ResourcePath: resourcepaths.VSMBShareResourcePath,
   221  		}
   222  		if err := uvm.modify(ctx, modification); err != nil {
   223  			return nil, err
   224  		}
   225  	}
   226  
   227  	share.allowedFiles = newAllowedFiles
   228  	share.refCount++
   229  	share.options = *options
   230  	m[shareKey] = share
   231  	return share, nil
   232  }
   233  
   234  // RemoveVSMB removes a VSMB share from a utility VM. Each VSMB share is ref-counted
   235  // and only actually removed when the ref-count drops to zero.
   236  func (uvm *UtilityVM) RemoveVSMB(ctx context.Context, hostPath string, readOnly bool) error {
   237  	if uvm.operatingSystem != "windows" {
   238  		return errNotSupported
   239  	}
   240  
   241  	uvm.m.Lock()
   242  	defer uvm.m.Unlock()
   243  
   244  	st, err := os.Stat(hostPath)
   245  	if err != nil {
   246  		return err
   247  	}
   248  	m := uvm.vsmbDirShares
   249  	if !st.IsDir() {
   250  		m = uvm.vsmbFileShares
   251  		hostPath = filepath.Dir(hostPath)
   252  	}
   253  	hostPath = filepath.Clean(hostPath)
   254  	shareKey := getVSMBShareKey(hostPath, readOnly)
   255  	share, err := uvm.findVSMBShare(ctx, m, shareKey)
   256  	if err != nil {
   257  		return fmt.Errorf("%s is not present as a VSMB share in %s, cannot remove", hostPath, uvm.id)
   258  	}
   259  
   260  	share.refCount--
   261  	if share.refCount > 0 {
   262  		return nil
   263  	}
   264  
   265  	modification := &hcsschema.ModifySettingRequest{
   266  		RequestType:  guestrequest.RequestTypeRemove,
   267  		Settings:     hcsschema.VirtualSmbShare{Name: share.name},
   268  		ResourcePath: resourcepaths.VSMBShareResourcePath,
   269  	}
   270  	if err := uvm.modify(ctx, modification); err != nil {
   271  		return fmt.Errorf("failed to remove vsmb share %s from %s: %+v: %s", hostPath, uvm.id, modification, err)
   272  	}
   273  
   274  	delete(m, shareKey)
   275  	return nil
   276  }
   277  
   278  // GetVSMBUvmPath returns the guest path of a VSMB mount.
   279  func (uvm *UtilityVM) GetVSMBUvmPath(ctx context.Context, hostPath string, readOnly bool) (string, error) {
   280  	if hostPath == "" {
   281  		return "", fmt.Errorf("no hostPath passed to GetVSMBUvmPath")
   282  	}
   283  
   284  	uvm.m.Lock()
   285  	defer uvm.m.Unlock()
   286  
   287  	st, err := os.Stat(hostPath)
   288  	if err != nil {
   289  		return "", err
   290  	}
   291  	m := uvm.vsmbDirShares
   292  	f := ""
   293  	if !st.IsDir() {
   294  		m = uvm.vsmbFileShares
   295  		hostPath, f = filepath.Split(hostPath)
   296  	}
   297  	hostPath = filepath.Clean(hostPath)
   298  	shareKey := getVSMBShareKey(hostPath, readOnly)
   299  	share, err := uvm.findVSMBShare(ctx, m, shareKey)
   300  	if err != nil {
   301  		return "", err
   302  	}
   303  	return filepath.Join(share.guestPath, f), nil
   304  }
   305  
   306  // getVSMBShareKey returns a string key which encapsulates the information that is used to
   307  // look up an existing VSMB share. If a share is being added, but there is an existing
   308  // share with the same key, the existing share will be used instead (and its ref count
   309  // incremented).
   310  func getVSMBShareKey(hostPath string, readOnly bool) string {
   311  	return fmt.Sprintf("%v-%v", hostPath, readOnly)
   312  }
   313  

View as plain text