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