Skip to content

Commit b9a8aad

Browse files
TBBlegabriel-samfira
authored andcommitted
Implement Windows mounting for bind and windows-layer mounts
Using symlinks for bind mounts means we are not protecting an RO-mounted layer against modification. Windows doesn't currently appear to offer a better approach though, as we cannot create arbitrary empty WCOW scratch layers at this time. For windows-layer mounts, Unmount does not have access to the mounts used to create it. So we store the relevant data in an Alternate Data Stream on the mountpoint in order to be able to Unmount later. Based on approach in #2366, with sign-offs recorded as 'Based-on-work-by' trailers below. This also partially-reverts some changes made in #6034 as they are not needed with this mounting implmentation, which no longer needs to be handled specially by the caller compared to non-Windows mounts. Signed-off-by: Paul "TBBle" Hampson <[email protected]> Based-on-work-by: Michael Crosby <[email protected]> Based-on-work-by: Darren Stahl <[email protected]>
1 parent 1a64ee1 commit b9a8aad

3 files changed

Lines changed: 191 additions & 47 deletions

File tree

mount/mount_windows.go

Lines changed: 98 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package mount
1818

1919
import (
2020
"encoding/json"
21+
"errors"
2122
"fmt"
2223
"os"
2324
"path/filepath"
@@ -26,8 +27,22 @@ import (
2627
"github.com/Microsoft/hcsshim"
2728
)
2829

30+
const sourceStreamName = "containerd.io-source"
31+
32+
var (
33+
// ErrNotImplementOnWindows is returned when an action is not implemented for windows
34+
ErrNotImplementOnWindows = errors.New("not implemented under windows")
35+
)
36+
2937
// Mount to the provided target.
3038
func (m *Mount) mount(target string) error {
39+
if m.Type == "bind" {
40+
if err := m.bindMount(target); err != nil {
41+
return fmt.Errorf("failed to bind-mount to %s: %w", target, err)
42+
}
43+
return nil
44+
}
45+
3146
if m.Type != "windows-layer" {
3247
return fmt.Errorf("invalid windows mount type: '%s'", m.Type)
3348
}
@@ -46,22 +61,42 @@ func (m *Mount) mount(target string) error {
4661
if err = hcsshim.ActivateLayer(di, layerID); err != nil {
4762
return fmt.Errorf("failed to activate layer %s: %w", m.Source, err)
4863
}
64+
defer func() {
65+
if err != nil {
66+
hcsshim.DeactivateLayer(di, layerID)
67+
}
68+
}()
4969

5070
if err = hcsshim.PrepareLayer(di, layerID, parentLayerPaths); err != nil {
5171
return fmt.Errorf("failed to prepare layer %s: %w", m.Source, err)
5272
}
73+
defer func() {
74+
if err != nil {
75+
hcsshim.UnprepareLayer(di, layerID)
76+
}
77+
}()
5378

54-
// We can link the layer mount path to the given target. It is an UNC path, and it needs
55-
// a trailing backslash.
56-
mountPath, err := hcsshim.GetLayerMountPath(di, layerID)
79+
volume, err := hcsshim.GetLayerMountPath(di, layerID)
5780
if err != nil {
58-
return fmt.Errorf("failed to get layer mount path for %s: %w", m.Source, err)
81+
return fmt.Errorf("failed to get volume path for layer %s: %w", m.Source, err)
82+
}
83+
84+
if err = setVolumeMountPoint(target, volume); err != nil {
85+
return fmt.Errorf("failed to set volume mount path for layer %s: %w", m.Source, err)
5986
}
60-
mountPath = mountPath + `\`
87+
defer func() {
88+
if err != nil {
89+
deleteVolumeMountPoint(target)
90+
}
91+
}()
6192

62-
if err = os.Symlink(mountPath, target); err != nil {
63-
return fmt.Errorf("failed to link mount to target %s: %w", target, err)
93+
// Add an Alternate Data Stream to record the layer source.
94+
// See https://docs.microsoft.com/en-au/archive/blogs/askcore/alternate-data-streams-in-ntfs
95+
// for details on Alternate Data Streams.
96+
if err = os.WriteFile(filepath.Clean(target)+":"+sourceStreamName, []byte(m.Source), 0666); err != nil {
97+
return fmt.Errorf("failed to record source for layer %s: %w", m.Source, err)
6498
}
99+
65100
return nil
66101
}
67102

@@ -85,8 +120,37 @@ func (m *Mount) GetParentPaths() ([]string, error) {
85120

86121
// Unmount the mount at the provided path
87122
func Unmount(mount string, flags int) error {
123+
mount = filepath.Clean(mount)
124+
125+
// Helpfully, both reparse points and symlinks look like links to Go
126+
// Less-helpfully, ReadLink cannot return \\?\Volume{GUID} for a volume mount,
127+
// and ends up returning the directory we gave it for some reason.
128+
if mountTarget, err := os.Readlink(mount); err != nil {
129+
// Not a mount point.
130+
// This isn't an error, per the EINVAL handling in the Linux version
131+
return nil
132+
} else if mount != filepath.Clean(mountTarget) {
133+
// Directory symlink
134+
if err := bindUnmount(mount); err != nil {
135+
return fmt.Errorf("failed to bind-unmount from %s: %w", mount, err)
136+
}
137+
return nil
138+
}
139+
140+
layerPathb, err := os.ReadFile(mount + ":" + sourceStreamName)
141+
142+
if err != nil {
143+
return fmt.Errorf("failed to retrieve source for layer %s: %w", mount, err)
144+
}
145+
146+
layerPath := string(layerPathb)
147+
148+
if err := deleteVolumeMountPoint(mount); err != nil {
149+
return fmt.Errorf("failed failed to release volume mount path for layer %s: %w", mount, err)
150+
}
151+
88152
var (
89-
home, layerID = filepath.Split(mount)
153+
home, layerID = filepath.Split(layerPath)
90154
di = hcsshim.DriverInfo{
91155
HomeDir: home,
92156
}
@@ -95,6 +159,7 @@ func Unmount(mount string, flags int) error {
95159
if err := hcsshim.UnprepareLayer(di, layerID); err != nil {
96160
return fmt.Errorf("failed to unprepare layer %s: %w", mount, err)
97161
}
162+
98163
if err := hcsshim.DeactivateLayer(di, layerID); err != nil {
99164
return fmt.Errorf("failed to deactivate layer %s: %w", mount, err)
100165
}
@@ -104,10 +169,35 @@ func Unmount(mount string, flags int) error {
104169

105170
// UnmountAll unmounts from the provided path
106171
func UnmountAll(mount string, flags int) error {
172+
if mount == "" {
173+
// This isn't an error, per the EINVAL handling in the Linux version
174+
return nil
175+
}
176+
107177
return Unmount(mount, flags)
108178
}
109179

110180
// UnmountRecursive unmounts from the provided path
111181
func UnmountRecursive(mount string, flags int) error {
112182
return UnmountAll(mount, flags)
113183
}
184+
185+
func (m *Mount) bindMount(target string) error {
186+
for _, option := range m.Options {
187+
if option == "ro" {
188+
return fmt.Errorf("read-only bind mount: %w", ErrNotImplementOnWindows)
189+
}
190+
}
191+
192+
if err := os.Remove(target); err != nil {
193+
return err
194+
}
195+
196+
// TODO: We don't honour the Read-Only flag.
197+
// It's possible that Windows simply lacks this.
198+
return os.Symlink(m.Source, target)
199+
}
200+
201+
func bindUnmount(target string) error {
202+
return os.Remove(target)
203+
}

mount/volumemountutils_windows.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package mount
18+
19+
// Simple wrappers around SetVolumeMountPoint and DeleteVolumeMountPoint
20+
21+
import (
22+
"fmt"
23+
"path/filepath"
24+
"strings"
25+
"syscall"
26+
27+
"github.com/containerd/containerd/errdefs"
28+
"golang.org/x/sys/windows"
29+
)
30+
31+
// Mount volumePath (in format '\\?\Volume{GUID}' at targetPath.
32+
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setvolumemountpointw
33+
func setVolumeMountPoint(targetPath string, volumePath string) error {
34+
if !strings.HasPrefix(volumePath, "\\\\?\\Volume{") {
35+
return fmt.Errorf("unable to mount non-volume path %s: %w", volumePath, errdefs.ErrInvalidArgument)
36+
}
37+
38+
// Both must end in a backslash
39+
slashedTarget := filepath.Clean(targetPath) + string(filepath.Separator)
40+
slashedVolume := volumePath + string(filepath.Separator)
41+
42+
targetP, err := syscall.UTF16PtrFromString(slashedTarget)
43+
if err != nil {
44+
return fmt.Errorf("unable to utf16-ise %s: %w", slashedTarget, err)
45+
}
46+
47+
volumeP, err := syscall.UTF16PtrFromString(slashedVolume)
48+
if err != nil {
49+
return fmt.Errorf("unable to utf16-ise %s: %w", slashedVolume, err)
50+
}
51+
52+
if err := windows.SetVolumeMountPoint(targetP, volumeP); err != nil {
53+
return fmt.Errorf("failed calling SetVolumeMount('%s', '%s'): %w", slashedTarget, slashedVolume, err)
54+
}
55+
56+
return nil
57+
}
58+
59+
// Remove the volume mount at targetPath
60+
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-deletevolumemountpointa
61+
func deleteVolumeMountPoint(targetPath string) error {
62+
// Must end in a backslash
63+
slashedTarget := filepath.Clean(targetPath) + string(filepath.Separator)
64+
65+
targetP, err := syscall.UTF16PtrFromString(slashedTarget)
66+
if err != nil {
67+
return fmt.Errorf("unable to utf16-ise %s: %w", slashedTarget, err)
68+
}
69+
70+
if err := windows.DeleteVolumeMountPoint(targetP); err != nil {
71+
return fmt.Errorf("failed calling DeleteVolumeMountPoint('%s'): %w", slashedTarget, err)
72+
}
73+
74+
return nil
75+
}

pkg/cri/opts/container.go

Lines changed: 18 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ import (
2121
"errors"
2222
"fmt"
2323
"os"
24-
"path/filepath"
25-
goruntime "runtime"
2624
"strings"
2725

2826
"github.com/containerd/continuity/fs"
@@ -86,53 +84,34 @@ func WithVolumes(volumeMounts map[string]string) containerd.NewContainerOpts {
8684
// https://github.com/containerd/containerd/pull/1785
8785
defer os.Remove(root)
8886

89-
unmounter := func(mountPath string) {
90-
if uerr := mount.Unmount(mountPath, 0); uerr != nil {
87+
if err := mount.All(mounts, root); err != nil {
88+
return fmt.Errorf("failed to mount: %w", err)
89+
}
90+
defer func() {
91+
if uerr := mount.Unmount(root, 0); uerr != nil {
9192
log.G(ctx).WithError(uerr).Errorf("Failed to unmount snapshot %q", root)
9293
if err == nil {
9394
err = uerr
9495
}
9596
}
96-
}
97-
98-
var mountPaths []string
99-
if goruntime.GOOS == "windows" {
100-
for _, m := range mounts {
101-
// appending the layerID to the root.
102-
mountPath := filepath.Join(root, filepath.Base(m.Source))
103-
mountPaths = append(mountPaths, mountPath)
104-
if err := m.Mount(mountPath); err != nil {
105-
return err
106-
}
107-
108-
defer unmounter(m.Source)
109-
}
110-
} else {
111-
mountPaths = append(mountPaths, root)
112-
if err := mount.All(mounts, root); err != nil {
113-
return fmt.Errorf("failed to mount: %w", err)
114-
}
115-
defer unmounter(root)
116-
}
97+
}()
11798

11899
for host, volume := range volumeMounts {
119100
// The volume may have been defined with a C: prefix, which we can't use here.
120101
volume = strings.TrimPrefix(volume, "C:")
121-
for _, mountPath := range mountPaths {
122-
src, err := fs.RootPath(mountPath, volume)
123-
if err != nil {
124-
return fmt.Errorf("rootpath on mountPath %s, volume %s: %w", mountPath, volume, err)
125-
}
126-
if _, err := os.Stat(src); err != nil {
127-
if os.IsNotExist(err) {
128-
// Skip copying directory if it does not exist.
129-
continue
130-
}
131-
return fmt.Errorf("stat volume in rootfs: %w", err)
132-
}
133-
if err := copyExistingContents(src, host); err != nil {
134-
return fmt.Errorf("taking runtime copy of volume: %w", err)
102+
src, err := fs.RootPath(root, volume)
103+
if err != nil {
104+
return fmt.Errorf("rootpath on mountPath %s, volume %s: %w", root, volume, err)
105+
}
106+
if _, err := os.Stat(src); err != nil {
107+
if os.IsNotExist(err) {
108+
// Skip copying directory if it does not exist.
109+
continue
135110
}
111+
return fmt.Errorf("stat volume in rootfs: %w", err)
112+
}
113+
if err := copyExistingContents(src, host); err != nil {
114+
return fmt.Errorf("taking runtime copy of volume: %w", err)
136115
}
137116
}
138117
return nil

0 commit comments

Comments
 (0)