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