diff --git a/cmd/containerd/builtins/builtins.go b/cmd/containerd/builtins/builtins.go index f6792266dd225..4a1cbd33aa2dd 100644 --- a/cmd/containerd/builtins/builtins.go +++ b/cmd/containerd/builtins/builtins.go @@ -18,6 +18,8 @@ package builtins // register containerd builtins here import ( + _ "github.com/containerd/containerd/v2/plugins/mount/fsview/erofs" + _ "github.com/containerd/containerd/v2/core/runtime/v2" _ "github.com/containerd/containerd/v2/plugins/content/local/plugin" _ "github.com/containerd/containerd/v2/plugins/events" diff --git a/go.mod b/go.mod index 8ce4ae4c6ea28..834689ab98ffc 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,8 @@ go 1.26.2 require ( dario.cat/mergo v1.0.2 github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 - github.com/Microsoft/go-winio v0.6.2 - github.com/Microsoft/hcsshim v0.14.0-rc.1 + github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 + github.com/Microsoft/hcsshim v0.15.0-rc.1 github.com/checkpoint-restore/checkpointctl v1.5.0 github.com/checkpoint-restore/go-criu/v7 v7.2.0 github.com/containerd/btrfs/v2 v2.0.0 @@ -37,6 +37,7 @@ require ( github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c github.com/docker/go-metrics v0.0.1 github.com/docker/go-units v0.5.0 + github.com/erofs/go-erofs v0.2.0 github.com/fsnotify/fsnotify v1.9.0 github.com/google/certtostore v1.0.6 github.com/google/go-cmp v0.7.0 diff --git a/go.sum b/go.sum index 1358c4c12c6b5..707528bb44044 100644 --- a/go.sum +++ b/go.sum @@ -8,10 +8,10 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Microsoft/hcsshim v0.14.0-rc.1 h1:qAPXKwGOkVn8LlqgBN8GS0bxZ83hOJpcjxzmlQKxKsQ= -github.com/Microsoft/hcsshim v0.14.0-rc.1/go.mod h1:hTKFGbnDtQb1wHiOWv4v0eN+7boSWAHyK/tNAaYZL0c= +github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 h1:0kQAzHq8vLs7Pptv+7TxjdETLf/nIqJpIB4oC6Ba4vY= +github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0= +github.com/Microsoft/hcsshim v0.15.0-rc.1 h1:FbbwtQmiD+BVHynGkx5S65JkLyhkEiiTP8nrpmg2SZw= +github.com/Microsoft/hcsshim v0.15.0-rc.1/go.mod h1:HWvvUPIy9HF6LotILj1G4VyS065rcLQ6tqj6tMUdOfI= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -105,6 +105,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/erofs/go-erofs v0.2.0 h1:LoqBN0t85zH74ozeSS6eyw6sRGRjYdR5T0Z2LweoXvo= +github.com/erofs/go-erofs v0.2.0/go.mod h1:XkSeN9MHszGd4+3gcEjadJLYHCQpWzJ7/8yznzMuzJs= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= diff --git a/internal/fsview/mount.go b/internal/fsview/mount.go new file mode 100644 index 0000000000000..3d88ccbbaf9ea --- /dev/null +++ b/internal/fsview/mount.go @@ -0,0 +1,327 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package fsview + +import ( + "errors" + "fmt" + "io" + "io/fs" + "os" + "strings" + "text/template" + + "github.com/containerd/containerd/v2/core/mount" + "github.com/containerd/errdefs" +) + +// View is an interface for temporarily viewing a filesystem, +// implementing the fs.FS interface with a close method. +type View interface { + fs.FS + Close() error +} + +type view struct { + fs.FS + cleanup func() error +} + +func (v view) Close() error { + if v.cleanup != nil { + return v.cleanup() + } + return nil +} + +// readLinkView wraps an fs.ReadLinkFS with a cleanup function, +// implementing both View and fs.ReadLinkFS. +type readLinkView struct { + fs.ReadLinkFS + cleanup func() error +} + +func (v readLinkView) Close() error { + if v.cleanup != nil { + return v.cleanup() + } + return nil +} + +// newView creates a View that preserves fs.ReadLinkFS if the underlying +// fs.FS implements it. +func newView(fsys fs.FS, cleanup func() error) View { + if rl, ok := fsys.(fs.ReadLinkFS); ok { + return readLinkView{ReadLinkFS: rl, cleanup: cleanup} + } + return view{FS: fsys, cleanup: cleanup} +} + +// FSMounts returns a View for the provided mounts if possible to open +// the mounts directly without mounting. +// +// If not supported, a nil View and an error will be returned. +func FSMounts(m []mount.Mount) (View, error) { + if len(m) == 0 { + return nil, nil + } + return resolveMount(m[len(m)-1], m[:len(m)-1]) +} + +// resolveMount tries registered handlers first, then built-in handlers. +func resolveMount(m mount.Mount, preceding []mount.Mount) (View, error) { + for _, h := range registered { + if h.HandleMount == nil { + continue + } + v, err := h.HandleMount(m) + if errors.Is(err, errdefs.ErrNotImplemented) { + continue + } + return v, err + } + + switch { + case m.Type == "bind" || m.Type == "rbind": + return openBind(m) + case m.Type == "overlay": + return openOverlay(m) + case strings.HasPrefix(m.Type, "format/"): + return openFormatMount(m, preceding) + } + + return nil, fmt.Errorf("mount type %s cannot be directly viewed: %w", m.Type, errdefs.ErrNotImplemented) +} + +func openBind(m mount.Mount) (View, error) { + r, err := os.OpenRoot(m.Source) + if err != nil { + return nil, err + } + return newView(r.FS(), r.Close), nil +} + +func openOverlay(m mount.Mount) (View, error) { + layers, err := openOverlayPaths(m.Options) + if err != nil { + return nil, err + } + return newOverlayView(layers) +} + +func openFormatMount(m mount.Mount, preceding []mount.Mount) (View, error) { + types := strings.Split(m.Type, "/") + if len(types) < 2 || types[0] != "format" || types[len(types)-1] != "overlay" { + return nil, errdefs.ErrNotImplemented + } + + var layers []View + closeLayers := func() { + for _, l := range layers { + l.Close() + } + } + for _, opt := range m.Options { + if val, ok := strings.CutPrefix(opt, "upperdir="); ok { + upper, err := resolveOverlayValue(val, preceding) + if err != nil { + if errors.Is(err, errdefs.ErrNotImplemented) { + continue + } + closeLayers() + return nil, fmt.Errorf("failed to handle upperdir option: %w", err) + } + if len(layers) > 0 { + layers = append(upper, layers...) + } else { + layers = upper + } + } + if val, ok := strings.CutPrefix(opt, "lowerdir="); ok { + for l := range strings.SplitSeq(val, ":") { + lowers, err := resolveOverlayValue(l, preceding) + if err != nil { + closeLayers() + return nil, fmt.Errorf("failed to handle lowerdir option: %w", err) + } + layers = append(layers, lowers...) + } + } + } + + return newOverlayView(layers) +} + +func newOverlayView(layers []View) (View, error) { + var fsList []fs.FS + for _, layer := range layers { + fsList = append(fsList, layer) + } + + ofs, err := NewOverlayFS(fsList) + if err != nil { + for _, layer := range layers { + layer.Close() + } + return nil, err + } + + return newView(ofs, func() error { + var errs []error + for _, layer := range layers { + errs = append(errs, layer.Close()) + } + return errors.Join(errs...) + }), nil +} + +// resolveOverlayValue resolves a single overlay option value, which may be +// a plain directory path or a Go template expression like "{{ mount 0 }}". +func resolveOverlayValue(s string, preceding []mount.Mount) ([]View, error) { + if !strings.Contains(s, "{{") { + r, err := os.OpenRoot(s) + if err != nil { + return nil, err + } + return []View{newView(r.FS(), r.Close)}, nil + } + + tmplExpr, suffix := splitTemplateSuffix(s) + + var layers []View + addLayer := func(v View) { layers = append(layers, v) } + boundsCheck := func(i int) error { + if i < 0 || i >= len(preceding) { + return fmt.Errorf("index out of bounds: %d, has %d preceding mounts", i, len(preceding)) + } + return nil + } + + fm := template.FuncMap{ + "source": func(i int) (string, error) { + if err := boundsCheck(i); err != nil { + return "", err + } + r, err := os.OpenRoot(preceding[i].Source) + if err != nil { + return "", fmt.Errorf("failed to open source of mount %d: %w", i, err) + } + addLayer(newView(r.FS(), r.Close)) + return "", nil + }, + "mount": func(i int) (string, error) { + if err := boundsCheck(i); err != nil { + return "", err + } + v, err := resolveMount(preceding[i], preceding[:i]) + if err != nil { + return "", fmt.Errorf("failed to resolve mount %d: %w", i, err) + } + addLayer(v) + return "", nil + }, + "overlay": func(start, end int) (string, error) { + i := start + for { + if err := boundsCheck(i); err != nil { + return "", err + } + v, err := resolveMount(preceding[i], preceding[:i]) + if err != nil { + return "", fmt.Errorf("failed to resolve mount %d: %w", i, err) + } + addLayer(v) + if i == end { + break + } + if start > end { + i-- + } else { + i++ + } + } + return "", nil + }, + } + + t, err := template.New("").Funcs(fm).Parse(tmplExpr) + if err != nil { + return nil, err + } + + if err := t.Execute(io.Discard, nil); err != nil { + for _, l := range layers { + l.Close() + } + return nil, err + } + + if suffix != "" { + for i, l := range layers { + subFS, err := fs.Sub(l, suffix) + if err != nil { + for _, l := range layers { + l.Close() + } + return nil, fmt.Errorf("failed to create sub view for path %q: %w", suffix, err) + } + layers[i] = newView(subFS, l.Close) + } + } + + return layers, nil +} + +func splitTemplateSuffix(s string) (string, string) { + i := strings.LastIndex(s, "}}") + if i < 0 { + return s, "" + } + tmpl := s[:i+2] + suffix := strings.TrimPrefix(s[i+2:], "/") + return tmpl, suffix +} + +func openOverlayPaths(options []string) ([]View, error) { + var ( + lower string + paths []string + ) + for _, o := range options { + if val, ok := strings.CutPrefix(o, "lowerdir="); ok { + lower = val + } else if val, ok := strings.CutPrefix(o, "upperdir="); ok { + paths = append(paths, val) + } + } + if lower != "" { + paths = append(paths, strings.Split(lower, ":")...) + } + + var layers []View + for _, p := range paths { + r, err := os.OpenRoot(p) + if err != nil { + for _, l := range layers { + l.Close() + } + return nil, err + } + layers = append(layers, newView(r.FS(), r.Close)) + } + return layers, nil +} diff --git a/internal/fsview/mount_format_test.go b/internal/fsview/mount_format_test.go new file mode 100644 index 0000000000000..cc8eeecf1399d --- /dev/null +++ b/internal/fsview/mount_format_test.go @@ -0,0 +1,168 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package fsview_test + +import ( + "io/fs" + "testing" + + "github.com/containerd/containerd/v2/core/mount" + "github.com/containerd/containerd/v2/internal/fsview" + "github.com/containerd/errdefs" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestFormatMountTemplates tests template formatting in mount options. +func TestFormatMountTemplates(t *testing.T) { + basePath := makeBaseErofs(t) + upperPath := makeUpper1Erofs(t) + + mounts := []mount.Mount{ + { + Source: basePath, + Type: "erofs", + Options: []string{"ro", "loop"}, + }, + { + Source: upperPath, + Type: "erofs", + Options: []string{"ro", "loop"}, + }, + { + Type: "format/mkdir/overlay", + Source: "overlay", + Options: []string{ + // overlay 1 0 reverses the order, putting upper (1) before base (0) + // In overlay lowerdir, leftmost has highest priority + "lowerdir={{ overlay 1 0 }}", + }, + }, + } + + viewFS, err := fsview.FSMounts(mounts) + require.NoError(t, err) + defer viewFS.Close() + + // Verify we can read files from both layers + t.Run("file from base layer", func(t *testing.T) { + data, err := fs.ReadFile(viewFS, "dir2/file2.txt") + require.NoError(t, err) + assert.Equal(t, "file2 content\n", string(data)) + }) + + t.Run("file from upper layer", func(t *testing.T) { + data, err := fs.ReadFile(viewFS, "dir1/newfile.txt") + require.NoError(t, err) + assert.Equal(t, "new file content\n", string(data)) + }) + + t.Run("whited out file should not exist", func(t *testing.T) { + _, err := viewFS.Open("dir1/file1.txt") + require.Error(t, err) + assert.ErrorIs(t, err, fs.ErrNotExist) + }) +} + +// TestFormatMountReversedRange tests {{ overlay }} with reversed range. +func TestFormatMountReversedRange(t *testing.T) { + basePath := makeBaseErofs(t) + upperPath := makeUpper1Erofs(t) + + mounts := []mount.Mount{ + { + Source: basePath, + Type: "erofs", + Options: []string{"ro", "loop"}, + }, + { + Source: upperPath, + Type: "erofs", + Options: []string{"ro", "loop"}, + }, + { + Type: "format/overlay", + Source: "overlay", + Options: []string{ + "lowerdir={{ overlay 1 0 }}", + }, + }, + } + + viewFS, err := fsview.FSMounts(mounts) + require.NoError(t, err) + defer viewFS.Close() + + // With reversed range, upper layer (1) comes before base layer (0) + // So the overlay should prioritize upper layer + t.Run("file from upper layer takes precedence", func(t *testing.T) { + data, err := fs.ReadFile(viewFS, "dir1/newfile.txt") + require.NoError(t, err) + assert.Equal(t, "new file content\n", string(data)) + }) +} + +// TestFormatMountIndexes tests {{ mount }} template with single mount. +func TestFormatMountIndexes(t *testing.T) { + basePath := makeBaseErofs(t) + + mounts := []mount.Mount{ + { + Source: basePath, + Target: "/mnt/base", + Type: "erofs", + Options: []string{"ro", "loop"}, + }, + { + Type: "format/overlay", + Source: "overlay", + Options: []string{ + "lowerdir={{ mount 0 }}", + }, + }, + } + + viewFS, err := fsview.FSMounts(mounts) + require.NoError(t, err) + defer viewFS.Close() + + // Verify we can read from the mount + data, err := fs.ReadFile(viewFS, "dir1/file1.txt") + require.NoError(t, err) + assert.Equal(t, "file1 content\n", string(data)) +} + +// TestFormatMountUnsupportedType tests that unsupported mount types return an error. +func TestFormatMountUnsupportedType(t *testing.T) { + mounts := []mount.Mount{ + { + Source: "/dev/sda1", + Type: "ext4", // Not supported in fsview + }, + { + Type: "format/overlay", + Source: "overlay", + Options: []string{ + "lowerdir={{ mount 0 }}", + }, + }, + } + + _, err := fsview.FSMounts(mounts) + require.Error(t, err) + assert.ErrorIs(t, err, errdefs.ErrNotImplemented) +} diff --git a/internal/fsview/mount_test.go b/internal/fsview/mount_test.go new file mode 100644 index 0000000000000..7ddc162d131ed --- /dev/null +++ b/internal/fsview/mount_test.go @@ -0,0 +1,390 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package fsview_test + +import ( + "context" + "io" + "io/fs" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "github.com/containerd/containerd/v2/core/mount" + "github.com/containerd/containerd/v2/internal/erofsutils" + "github.com/containerd/containerd/v2/internal/fsview" + _ "github.com/containerd/containerd/v2/plugins/mount/fsview/erofs" + + "github.com/containerd/containerd/v2/pkg/archive/tartest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFSMountsLast(t *testing.T) { + tmp := t.TempDir() + dir1 := filepath.Join(tmp, "dir1") + dir2 := filepath.Join(tmp, "dir2") + if err := os.Mkdir(dir1, 0755); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(dir2, 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir1, "f1"), []byte("1"), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir2, "f2"), []byte("2"), 0644); err != nil { + t.Fatal(err) + } + + mounts := []mount.Mount{ + {Type: "bind", Source: dir1, Target: "/mnt/dir1"}, + {Type: "bind", Source: dir2, Target: "/mnt/dir2"}, + } + + // Should pick the last one (dir2) + fs, err := fsview.FSMounts(mounts) + if err != nil { + t.Fatal(err) + } + defer fs.Close() + + if _, err := fs.Open("f2"); err != nil { + t.Errorf("expected to find f2 in last mount, got %v", err) + } + if _, err := fs.Open("f1"); err == nil { + t.Error("expected NOT to find f1 (should only have dir2)") + } +} + +func skipIfNoMkfsErofs(t *testing.T) { + t.Helper() + if _, err := exec.LookPath("mkfs.erofs"); err != nil { + t.Skip("mkfs.erofs not found in PATH") + } + supported, err := erofsutils.SupportGenerateFromTar() + if err != nil { + t.Skipf("failed to check mkfs.erofs tar support: %v", err) + } + if !supported { + t.Skip("mkfs.erofs does not support --tar= mode") + } +} + +func makeEROFSFromTar(t *testing.T, wt tartest.WriterToTar) string { + t.Helper() + layerPath := filepath.Join(t.TempDir(), "layer.erofs") + rc := tartest.TarFromWriterTo(wt) + defer rc.Close() + if err := erofsutils.ConvertTarErofs(context.Background(), rc, layerPath, "", nil); err != nil { + t.Fatalf("ConvertTarErofs failed: %v", err) + } + return layerPath +} + +func mergeEROFSLayers(t *testing.T, blobs ...string) string { + t.Helper() + output := filepath.Join(t.TempDir(), "merged.erofs") + args := append([]string{"--aufs", "--ovlfs-strip=1", "--quiet", output}, blobs...) + cmd := exec.Command("mkfs.erofs", args...) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("mkfs.erofs merge failed: %s: %v", string(out), err) + } + return output +} + +// makeBaseErofs builds the base layer EROFS image (replaces testdata/base.erofs). +func makeBaseErofs(t *testing.T) string { + t.Helper() + skipIfNoMkfsErofs(t) + tc := tartest.TarContext{}.WithModTime(time.Now().UTC()) + return makeEROFSFromTar(t, tartest.TarAll( + tc.Dir("dir1", 0755), + tc.File("dir1/file1.txt", []byte("file1 content\n"), 0644), + tc.File("dir1/file2.txt", []byte("file2 content\n"), 0644), + tc.Dir("dir2", 0755), + tc.File("dir2/file2.txt", []byte("file2 content\n"), 0644), + tc.Dir("dir2/subdir", 0755), + tc.File("dir2/subdir/subfile.txt", []byte("subfile content\n"), 0644), + )) +} + +// makeUpper1Erofs builds the first upper layer EROFS image (replaces testdata/upper1.erofs). +// Contains an opaque dir1 with a new file and whiteouts for file1.txt and file2.txt. +func makeUpper1Erofs(t *testing.T) string { + t.Helper() + skipIfNoMkfsErofs(t) + tc := tartest.TarContext{}.WithModTime(time.Now().UTC()) + return makeEROFSFromTar(t, tartest.TarAll( + tc.Dir("dir1", 0755), + tc.File("dir1/.wh..wh..opq", []byte{}, 0644), + tc.File("dir1/newfile.txt", []byte("new file content\n"), 0644), + tc.File("dir1/.wh.file1.txt", []byte{}, 0644), + tc.File("dir1/.wh.file2.txt", []byte{}, 0644), + tc.Dir("dir2", 0755), + )) +} + +// makeUpper2Erofs builds the second upper layer EROFS image (replaces testdata/upper2.erofs). +// Contains only a whiteout for dir1/file1.txt. +func makeUpper2Erofs(t *testing.T) string { + t.Helper() + skipIfNoMkfsErofs(t) + tc := tartest.TarContext{}.WithModTime(time.Now().UTC()) + return makeEROFSFromTar(t, tartest.TarAll( + tc.Dir("dir1", 0755), + tc.File("dir1/.wh.file1.txt", []byte{}, 0644), + )) +} + +func TestFSMountsEROFSWithDevices(t *testing.T) { + skipIfNoMkfsErofs(t) + + tc := tartest.TarContext{}.WithModTime(time.Now().UTC()) + + layer1Path := makeEROFSFromTar(t, tartest.TarAll( + tc.Dir("dir", 0755), + tc.File("dir/file1.txt", []byte("file1 content"), 0644), + tc.File("dir/file2.txt", []byte("file2 content"), 0644), + )) + + layer2Path := makeEROFSFromTar(t, tartest.TarAll( + tc.Dir("dir", 0755), + tc.File("dir/file3.txt", []byte("file3 content"), 0644), + )) + + metaPath := mergeEROFSLayers(t, layer1Path, layer2Path) + + v, err := fsview.FSMounts([]mount.Mount{ + { + Type: "erofs", + Source: metaPath, + Options: []string{"ro", "device=" + layer1Path, "device=" + layer2Path}, + }, + }) + if err != nil { + t.Fatalf("FSMounts failed: %v", err) + } + defer v.Close() + + for _, tt := range []struct { + path string + content string + }{ + {"dir/file1.txt", "file1 content"}, + {"dir/file2.txt", "file2 content"}, + {"dir/file3.txt", "file3 content"}, + } { + f, err := v.Open(tt.path) + if err != nil { + t.Errorf("failed to open %s: %v", tt.path, err) + continue + } + data, err := io.ReadAll(f) + f.Close() + if err != nil { + t.Errorf("failed to read %s: %v", tt.path, err) + continue + } + if string(data) != tt.content { + t.Errorf("%s: got %q, want %q", tt.path, string(data), tt.content) + } + } +} + +func TestFSMountsOverlayAbsoluteSymlinkEtcGroup(t *testing.T) { + skipIfNoMkfsErofs(t) + + expectedContent := "dummygroup:x:1001:root\n" + tc := tartest.TarContext{}.WithModTime(time.Now().UTC()) + layerPath := makeEROFSFromTar(t, tartest.TarAll( + tc.Dir("etc", 0o755), + tc.Dir("nix", 0o755), + tc.Dir("nix/store", 0o755), + tc.Dir("nix/store/abcd", 0o755), + tc.File("nix/store/abcd/group", []byte(expectedContent), 0o644), + tc.Symlink("/nix/store/abcd/group", "etc/group"), + )) + + v, err := fsview.FSMounts([]mount.Mount{ + { + Type: "erofs", + Source: layerPath, + }, + { + Type: "format/overlay", + Source: "overlay", + Options: []string{ + "lowerdir={{ mount 0 }}", + }, + }, + }) + if err != nil { + t.Fatalf("FSMounts failed: %v", err) + } + defer v.Close() + + f, err := v.Open("etc/group") + if err != nil { + t.Fatalf("open etc/group through fsview failed: %v", err) + } + defer f.Close() + + content, err := io.ReadAll(f) + if err != nil { + t.Fatal(err) + } + if string(content) != expectedContent { + t.Errorf("expected content %q, got %q", expectedContent, string(content)) + } +} + +func TestFSMountsOverlayWithEROFSDevices(t *testing.T) { + skipIfNoMkfsErofs(t) + + tc := tartest.TarContext{}.WithModTime(time.Now().UTC()) + + // Layer 1 (bottom): regular files including ones that will be hidden + layer1Path := makeEROFSFromTar(t, tartest.TarAll( + tc.Dir("dir", 0755), + tc.File("dir/file1.txt", []byte("file1 content"), 0644), + tc.File("dir/file2.txt", []byte("file2 content"), 0644), + tc.File("dir/file5.txt", []byte("file5 content"), 0644), + tc.Dir("opaquedir", 0755), + tc.File("opaquedir/old.txt", []byte("old content"), 0644), + )) + + // Layer 2 (top): new files, a whiteout for file5.txt, and an opaque opaquedir. + // AUFS-style markers (.wh.*) are converted by mkfs.erofs --aufs to + // overlay-native whiteouts (char dev 0:0) and opaque xattrs. + layer2Path := makeEROFSFromTar(t, tartest.TarAll( + tc.Dir("dir", 0755), + tc.File("dir/file3.txt", []byte("file3 content"), 0644), + tc.File("dir/.wh.file5.txt", []byte{}, 0644), + tc.Dir("opaquedir", 0755), + tc.File("opaquedir/.wh..wh..opq", []byte{}, 0644), + tc.File("opaquedir/new.txt", []byte("new content"), 0644), + )) + + // Merge with layer1 as lower (first arg) and layer2 as upper (last arg) + // so whiteouts in layer2 apply to layer1. + metaPath := mergeEROFSLayers(t, layer1Path, layer2Path) + + // Create upper directory with additional and override files + upperDir := filepath.Join(t.TempDir(), "upper") + if err := os.MkdirAll(filepath.Join(upperDir, "dir"), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(upperDir, "dir", "file4.txt"), []byte("file4 content"), 0644); err != nil { + t.Fatal(err) + } + // Override file1.txt from lower layer + if err := os.WriteFile(filepath.Join(upperDir, "dir", "file1.txt"), []byte("file1 override"), 0644); err != nil { + t.Fatal(err) + } + + v, err := fsview.FSMounts([]mount.Mount{ + { + Type: "erofs", + Source: metaPath, + Options: []string{"ro", "device=" + layer1Path, "device=" + layer2Path}, + }, + { + Type: "format/overlay", + Source: "overlay", + Options: []string{ + "upperdir=" + upperDir, + "lowerdir={{ mount 0 }}", + }, + }, + }) + if err != nil { + t.Fatalf("FSMounts failed: %v", err) + } + defer v.Close() + + // Verify readable files + for _, tt := range []struct { + path string + content string + }{ + {"dir/file1.txt", "file1 override"}, // upper overrides lower + {"dir/file2.txt", "file2 content"}, // from erofs layer 1 + {"dir/file3.txt", "file3 content"}, // from erofs layer 2 + {"dir/file4.txt", "file4 content"}, // from upper only + {"opaquedir/new.txt", "new content"}, // from erofs layer 2 (survives opaque) + } { + f, err := v.Open(tt.path) + if err != nil { + t.Errorf("failed to open %s: %v", tt.path, err) + continue + } + data, err := io.ReadAll(f) + f.Close() + if err != nil { + t.Errorf("failed to read %s: %v", tt.path, err) + continue + } + if string(data) != tt.content { + t.Errorf("%s: got %q, want %q", tt.path, string(data), tt.content) + } + } + + // Verify whiteout: file5.txt was in layer 1 but whited out by layer 2 + if _, err := v.Open("dir/file5.txt"); err == nil { + t.Error("dir/file5.txt should not exist (whiteout)") + } + + // Verify opaque: old.txt was in layer 1's opaquedir but hidden by opaque marker in layer 2 + if _, err := v.Open("opaquedir/old.txt"); err == nil { + t.Error("opaquedir/old.txt should not exist (opaque directory)") + } +} + +// TestFormatMountIndexWithSuffix tests {{ mount }} templates that include a path suffix. +func TestFormatMountIndexWithSuffix(t *testing.T) { + root := filepath.Join(t.TempDir(), "root") + require.NoError(t, os.MkdirAll(filepath.Join(root, "etc"), 0755)) + require.NoError(t, os.MkdirAll(filepath.Join(root, "sub", "etc"), 0755)) + require.NoError(t, os.WriteFile(filepath.Join(root, "etc", "passwd"), []byte("root\n"), 0644)) + require.NoError(t, os.WriteFile(filepath.Join(root, "sub", "etc", "passwd"), []byte("guest\n"), 0644)) + + mounts := []mount.Mount{ + { + Source: root, + Type: "bind", + }, + { + Type: "format/overlay", + Source: "overlay", + Options: []string{ + "lowerdir={{ mount 0 }}/sub", + }, + }, + } + + viewFS, err := fsview.FSMounts(mounts) + require.NoError(t, err) + defer viewFS.Close() + + data, err := fs.ReadFile(viewFS, "etc/passwd") + require.NoError(t, err) + assert.Equal(t, "guest\n", string(data)) +} diff --git a/internal/fsview/overlay.go b/internal/fsview/overlay.go new file mode 100644 index 0000000000000..36fc5131949b9 --- /dev/null +++ b/internal/fsview/overlay.go @@ -0,0 +1,457 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package fsview + +import ( + "errors" + "io" + "io/fs" + "path" + "sort" + "strings" + "syscall" +) + +// OverlayOpaqueXattrs are the xattr names used to indicate an opaque directory. +// "trusted.overlay.opaque" is the traditional xattr used by overlay. +// "user.overlay.opaque" is available since Linux 5.11. +// See https://github.com/torvalds/linux/commit/2d2f2d7322ff43e0fe92bf8cccdc0b09449bf2e1 +var OverlayOpaqueXattrs = []string{ + "trusted.overlay.opaque", + "user.overlay.opaque", +} + +// maxSymlinks is the maximum number of symlinks that will be followed +// when resolving a path, to prevent infinite loops. +const maxSymlinks = 255 + +// NewOverlayFS returns a new fs.FS that overlays the provided layers. +// The layers should be provided in order from upper to lower. +func NewOverlayFS(layers []fs.FS) (fs.FS, error) { + return &overlayFS{layers: layers}, nil +} + +type overlayFS struct { + layers []fs.FS +} + +// hasOpaqueParent checks if any parent directory of the given path is opaque in the layer. +// If a parent is opaque, it means we should not look in lower layers for this path. +func hasOpaqueParent(layer fs.FS, name string) bool { + // Check all parent directories + p := name + for p != "." && p != "/" { + p = path.Dir(p) + f, err := layer.Open(p) + if err != nil { + continue + } + opaque := isOpaque(f) + f.Close() + if opaque { + return true + } + } + return false +} + +// lstatLayer returns the FileInfo for name in the layer without following +// the final symlink component. If the layer implements fs.ReadLinkFS, it +// uses Lstat directly. Otherwise it falls back to Open+Stat which follows +// symlinks (degraded behavior). +func lstatLayer(layer fs.FS, name string) (fs.FileInfo, error) { + if rl, ok := layer.(fs.ReadLinkFS); ok { + return rl.Lstat(name) + } + f, err := layer.Open(name) + if err != nil { + return nil, err + } + defer f.Close() + return f.Stat() +} + +// readlinkLayer returns the symlink target for name in the layer. +// The layer must implement fs.ReadLinkFS. +func readlinkLayer(layer fs.FS, name string) (string, error) { + if rl, ok := layer.(fs.ReadLinkFS); ok { + return rl.ReadLink(name) + } + return "", &fs.PathError{Op: "readlink", Path: name, Err: syscall.EINVAL} +} + +func (o *overlayFS) Open(name string) (fs.File, error) { + if !fs.ValidPath(name) { + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid} + } + return o.openFollow(name, 0) +} + +// Lstat returns a FileInfo describing the named file without following +// the final symlink component. Intermediate symlinks are resolved through +// the overlay so that cross-layer symlink targets are found correctly. +func (o *overlayFS) Lstat(name string) (fs.FileInfo, error) { + if !fs.ValidPath(name) { + return nil, &fs.PathError{Op: "lstat", Path: name, Err: fs.ErrInvalid} + } + resolved, err := o.resolve(name, false, 0) + if err != nil { + return nil, err + } + return o.lstatDirect(resolved) +} + +// ReadLink returns the destination of the named symbolic link. +// Intermediate path components are resolved through the overlay. +func (o *overlayFS) ReadLink(name string) (string, error) { + if !fs.ValidPath(name) { + return "", &fs.PathError{Op: "readlink", Path: name, Err: fs.ErrInvalid} + } + resolved, err := o.resolve(name, false, 0) + if err != nil { + return "", err + } + return o.readlinkDirect(resolved) +} + +// openFollow opens a file, resolving symlinks at the overlay level. +// depth tracks the number of symlinks followed to detect loops. +func (o *overlayFS) openFollow(name string, depth int) (fs.File, error) { + resolved, err := o.resolve(name, true, depth) + if err != nil { + return nil, err + } + return o.openDirect(resolved) +} + +// resolve walks the path component by component, resolving symlinks at the +// overlay level. When follow is true, symlinks in the final component are +// also resolved. Returns the fully resolved path with no symlinks. +func (o *overlayFS) resolve(name string, follow bool, depth int) (string, error) { + if name == "." { + return ".", nil + } + parts := strings.Split(name, "/") + resolved := "" + + for i, part := range parts { + isLast := i == len(parts)-1 + + var candidate string + if resolved == "" { + candidate = part + } else { + candidate = resolved + "/" + part + } + + // Lstat this component across the overlay layers + fi, err := o.lstatDirect(candidate) + if err != nil { + return "", err + } + + if fi.Mode()&fs.ModeSymlink != 0 { + if isLast && !follow { + // Don't follow the final component for Lstat/ReadLink + resolved = candidate + continue + } + + depth++ + if depth > maxSymlinks { + return "", &fs.PathError{Op: "open", Path: name, Err: syscall.ELOOP} + } + + target, err := o.readlinkDirect(candidate) + if err != nil { + return "", err + } + + // Build the new path: target + remaining components + remaining := "" + if !isLast { + remaining = strings.Join(parts[i+1:], "/") + } + + var newPath string + if path.IsAbs(target) { + // Absolute symlink: resolve from root + target = strings.TrimPrefix(target, "/") + if remaining != "" { + newPath = target + "/" + remaining + } else { + newPath = target + } + } else { + // Relative symlink: resolve from parent of current component + parent := path.Dir(candidate) + joined := path.Join(parent, target) + if remaining != "" { + newPath = joined + "/" + remaining + } else { + newPath = joined + } + } + newPath = path.Clean(newPath) + if newPath == "." { + return ".", nil + } + + // Restart resolution from the root of the overlay + return o.resolve(newPath, follow, depth) + } + + if !fi.IsDir() && !isLast { + // Non-directory, non-symlink in an intermediate position + return "", &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} + } + + resolved = candidate + } + + return resolved, nil +} + +// lstatDirect does an Lstat across overlay layers, respecting whiteouts +// and opaque directories. It does NOT resolve symlinks - the path must +// already have intermediate symlinks resolved. +func (o *overlayFS) lstatDirect(name string) (fs.FileInfo, error) { + var firstErr error + var opaque bool + + for _, layer := range o.layers { + if opaque { + break + } + if hasOpaqueParent(layer, name) { + opaque = true + } + + fi, err := lstatLayer(layer, name) + if err != nil { + var pe *fs.PathError + if !errors.As(err, &pe) && firstErr == nil { + firstErr = err + } + continue + } + + if isWhiteout(fi) { + return nil, &fs.PathError{Op: "lstat", Path: name, Err: fs.ErrNotExist} + } + + return fi, nil + } + + if firstErr != nil { + return nil, firstErr + } + return nil, &fs.PathError{Op: "lstat", Path: name, Err: fs.ErrNotExist} +} + +// readlinkDirect reads the symlink target from the first matching layer, +// respecting whiteouts and opaque directories. The path must already have +// intermediate symlinks resolved. +func (o *overlayFS) readlinkDirect(name string) (string, error) { + var opaque bool + + for _, layer := range o.layers { + if opaque { + break + } + if hasOpaqueParent(layer, name) { + opaque = true + } + + fi, err := lstatLayer(layer, name) + if err != nil { + continue + } + + if isWhiteout(fi) { + return "", &fs.PathError{Op: "readlink", Path: name, Err: fs.ErrNotExist} + } + + if fi.Mode()&fs.ModeSymlink == 0 { + return "", &fs.PathError{Op: "readlink", Path: name, Err: syscall.EINVAL} + } + + return readlinkLayer(layer, name) + } + + return "", &fs.PathError{Op: "readlink", Path: name, Err: fs.ErrNotExist} +} + +// openDirect opens a fully-resolved path (no symlinks) using the +// original layer-by-layer logic with directory merging. +func (o *overlayFS) openDirect(name string) (fs.File, error) { + if !fs.ValidPath(name) { + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid} + } + + var dirLayers []fs.FS + var firstErr error + var opaque bool + + for _, layer := range o.layers { + if opaque { + // A parent directory in a higher layer is opaque, so we stop looking in lower layers + break + } + if hasOpaqueParent(layer, name) { + // Set opaqueness but continue to check this layer + opaque = true + } + + f, err := layer.Open(name) + if err != nil { + // Path errors (not found, not a directory, etc.) are expected + // when a path doesn't resolve in a given layer. Only record + // non-path errors (e.g., I/O failures) for later reporting. + var pe *fs.PathError + if !errors.As(err, &pe) && firstErr == nil { + firstErr = err + } + continue + } + + fi, errStat := f.Stat() + if errStat != nil { + f.Close() + if firstErr == nil { + firstErr = errStat + } + continue + } + + if isWhiteout(fi) { + f.Close() + // A whiteout hides this path in all lower layers. + // If we already found directories above, stop merging. + if len(dirLayers) > 0 { + break + } + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} + } + + if !fi.IsDir() { + if len(dirLayers) > 0 { + // Directory on upper covers file on lower + f.Close() + break + } + return f, nil + } + + // Directory — accumulate for merging + dirLayers = append(dirLayers, layer) + if !opaque && isOpaque(f) { + opaque = true + } + f.Close() + } + + if len(dirLayers) > 0 { + return &overlayDir{fs: o, path: name, layers: dirLayers}, nil + } + + if firstErr != nil { + return nil, firstErr + } + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} +} + +type overlayDir struct { + fs *overlayFS + path string + layers []fs.FS + entries []fs.DirEntry + offset int + read bool +} + +func (d *overlayDir) Stat() (fs.FileInfo, error) { + // Stat should return info from the top-most layer + if len(d.layers) == 0 { + return nil, fs.ErrNotExist + } + f, err := d.layers[0].Open(d.path) + if err != nil { + return nil, err + } + defer f.Close() + return f.Stat() +} + +func (d *overlayDir) Read([]byte) (int, error) { + return 0, &fs.PathError{Op: "read", Path: d.path, Err: errors.New("is a directory")} +} + +func (d *overlayDir) Close() error { + return nil +} + +func (d *overlayDir) ReadDir(n int) ([]fs.DirEntry, error) { + if !d.read { + seen := make(map[string]bool) + + for _, layer := range d.layers { + // ReadDir from the layer + entries, err := fs.ReadDir(layer, d.path) + if err == nil { + for _, e := range entries { + name := e.Name() + if seen[name] { + continue + } + + // Check for whiteout in this layer + if (e.Type() & fs.ModeCharDevice) != 0 { + info, err := e.Info() + if err == nil && isWhiteout(info) { + seen[name] = true + continue + } + } + + seen[name] = true + d.entries = append(d.entries, e) + } + } + } + sort.Slice(d.entries, func(i, j int) bool { return d.entries[i].Name() < d.entries[j].Name() }) + d.read = true + } + + if n <= 0 { + if d.offset >= len(d.entries) { + return []fs.DirEntry{}, nil + } + res := d.entries[d.offset:] + d.offset = len(d.entries) + return res, nil + } + + if d.offset >= len(d.entries) { + return nil, io.EOF + } + + end := min(d.offset+n, len(d.entries)) + res := d.entries[d.offset:end] + d.offset = end + return res, nil +} diff --git a/internal/fsview/overlay_erofs_test.go b/internal/fsview/overlay_erofs_test.go new file mode 100644 index 0000000000000..7add7e1d69aca --- /dev/null +++ b/internal/fsview/overlay_erofs_test.go @@ -0,0 +1,263 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package fsview_test + +import ( + "io/fs" + "os" + "testing" + + "github.com/containerd/containerd/v2/core/mount" + "github.com/containerd/containerd/v2/internal/fsview" + _ "github.com/containerd/containerd/v2/plugins/mount/fsview/erofs" + + "github.com/erofs/go-erofs" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestEROFSBaseLayer tests reading files from a base EROFS layer. +func TestEROFSBaseLayer(t *testing.T) { + basePath := makeBaseErofs(t) + + f, err := os.Open(basePath) + require.NoError(t, err) + defer f.Close() + + efs, err := erofs.Open(f) + require.NoError(t, err) + + testCases := []struct { + name string + path string + wantErr bool + wantData string + }{ + { + name: "file1 exists in dir1", + path: "dir1/file1.txt", + wantData: "file1 content\n", + }, + { + name: "file2 exists in dir2", + path: "dir2/file2.txt", + wantData: "file2 content\n", + }, + { + name: "subfile exists in dir2/subdir", + path: "dir2/subdir/subfile.txt", + wantData: "subfile content\n", + }, + { + name: "nonexistent file", + path: "nonexistent.txt", + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + data, err := fs.ReadFile(efs, tc.path) + if tc.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tc.wantData, string(data)) + } + }) + } +} + +// TestEROFSOverlayWithWhiteout tests overlay with EROFS layers including whiteout handling. +func TestEROFSOverlayWithWhiteout(t *testing.T) { + basePath := makeBaseErofs(t) + upperPath := makeUpper1Erofs(t) + + mounts := []mount.Mount{ + { + Type: "erofs", + Source: basePath, + }, + { + Type: "erofs", + Source: upperPath, + }, + } + + viewFS, err := fsview.FSMounts(mounts) + require.NoError(t, err) + defer viewFS.Close() + + layers := []fs.FS{viewFS} + overlayFS, err := fsview.NewOverlayFS(layers) + require.NoError(t, err) + + testCases := []struct { + name string + path string + wantErr bool + desc string + }{ + { + name: "whited out file should not exist", + path: "dir1/file1.txt", + wantErr: true, + desc: "file1.txt has .wh.file1.txt in upper layer", + }, + { + name: "new file from upper layer exists", + path: "dir1/newfile.txt", + wantErr: false, + desc: "newfile.txt was added in upper layer", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := overlayFS.Open(tc.path) + if tc.wantErr { + require.Error(t, err, tc.desc) + } else { + require.NoError(t, err, tc.desc) + } + }) + } +} + +// TestEROFSWithFSMounts tests using FSMounts to handle EROFS overlay. +func TestEROFSWithFSMounts(t *testing.T) { + basePath := makeBaseErofs(t) + + mounts := []mount.Mount{ + { + Type: "erofs", + Source: basePath, + }, + } + + viewFS, err := fsview.FSMounts(mounts) + require.NoError(t, err) + defer viewFS.Close() + + // Test that we can read files through FSMounts + testCases := []struct { + name string + path string + }{ + { + name: "read file1", + path: "dir1/file1.txt", + }, + { + name: "read file2", + path: "dir2/file2.txt", + }, + { + name: "read subfile", + path: "dir2/subdir/subfile.txt", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := fs.ReadFile(viewFS, tc.path) + require.NoError(t, err) + }) + } +} + +// TestEROFSMultipleLayersOverlay tests overlay with multiple EROFS layers. +func TestEROFSMultipleLayersOverlay(t *testing.T) { + basePath := makeBaseErofs(t) + upperPath := makeUpper1Erofs(t) + + // Open base layer + baseFile, err := os.Open(basePath) + require.NoError(t, err) + defer baseFile.Close() + + baseFS, err := erofs.Open(baseFile) + require.NoError(t, err) + + // Open upper layer + upperFile, err := os.Open(upperPath) + require.NoError(t, err) + defer upperFile.Close() + + upperFS, err := erofs.Open(upperFile) + require.NoError(t, err) + + // Create overlay with upper layer first (highest priority) + layers := []fs.FS{upperFS, baseFS} + overlayFS, err := fsview.NewOverlayFS(layers) + require.NoError(t, err) + + t.Run("file from upper layer", func(t *testing.T) { + data, err := fs.ReadFile(overlayFS, "dir1/newfile.txt") + require.NoError(t, err) + assert.Equal(t, "new file content\n", string(data)) + }) + + t.Run("file from base layer", func(t *testing.T) { + data, err := fs.ReadFile(overlayFS, "dir2/file2.txt") + require.NoError(t, err) + assert.Equal(t, "file2 content\n", string(data)) + }) + + t.Run("file in opaque directory should not exist", func(t *testing.T) { + _, err := overlayFS.Open("dir1/file1.txt") + require.Error(t, err) + assert.ErrorIs(t, err, fs.ErrNotExist) + }) + + t.Run("directory listing respects whiteout", func(t *testing.T) { + entries, err := fs.ReadDir(overlayFS, "dir1") + require.NoError(t, err) + + // Should only have newfile.txt, not file1.txt + names := make([]string, 0, len(entries)) + for _, e := range entries { + names = append(names, e.Name()) + } + + assert.Contains(t, names, "newfile.txt") + assert.NotContains(t, names, "file1.txt") + assert.NotContains(t, names, "file2.txt") + }) +} + +// TestEROFSWhiteoutDetection tests that whiteout files are correctly detected. +func TestEROFSWhiteoutDetection(t *testing.T) { + upperPath := makeUpper2Erofs(t) + + upperFile, err := os.Open(upperPath) + require.NoError(t, err) + defer upperFile.Close() + + upperFS, err := erofs.Open(upperFile) + require.NoError(t, err) + + // Check the whiteout file + fi, err := fs.Stat(upperFS, "dir1/file1.txt") + require.NoError(t, err) + + // Verify it's detected as a whiteout + assert.Equal(t, fs.ModeCharDevice, fi.Mode()&fs.ModeCharDevice, "should be char device") + estatfi, ok := fi.Sys().(*erofs.Stat) + assert.True(t, ok, "should be erofs stat") + assert.Equal(t, uint32(0), estatfi.Rdev, "file1.txt should be a whiteout") +} diff --git a/internal/fsview/overlay_linux.go b/internal/fsview/overlay_linux.go new file mode 100644 index 0000000000000..0ae075e14a33b --- /dev/null +++ b/internal/fsview/overlay_linux.go @@ -0,0 +1,69 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package fsview + +import ( + "io/fs" + "os" + "syscall" + + "golang.org/x/sys/unix" +) + +func getxattr(f fs.File, name string) (string, bool) { + if osf, ok := f.(*os.File); ok { + var dest [256]byte + sz, err := unix.Fgetxattr(int(osf.Fd()), name, dest[:]) + if err != nil || sz <= 0 { + return "", false + } + return string(dest[:sz]), true //nolint:gosec // G602: sz is bounded by dest size + } + + for _, h := range registered { + if h.Getxattr != nil { + if val, ok := h.Getxattr(f, name); ok { + return val, true + } + } + } + return "", false +} + +func isOpaque(f fs.File) bool { + for _, xattr := range OverlayOpaqueXattrs { + if val, ok := getxattr(f, xattr); ok && val == "y" { + return true + } + } + return false +} + +func isWhiteout(fi fs.FileInfo) bool { + if (fi.Mode() & fs.ModeCharDevice) == 0 { + return false + } + if sys, ok := fi.Sys().(*syscall.Stat_t); ok { + return sys.Rdev == 0 + } + for _, h := range registered { + if h.IsWhiteout != nil && h.IsWhiteout(fi) { + return true + } + } + return false +} diff --git a/internal/fsview/overlay_linux_test.go b/internal/fsview/overlay_linux_test.go new file mode 100644 index 0000000000000..33da00247f611 --- /dev/null +++ b/internal/fsview/overlay_linux_test.go @@ -0,0 +1,368 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package fsview_test + +import ( + "errors" + "io/fs" + "os" + "path/filepath" + "strings" + "syscall" + "testing" + "testing/fstest" + + "github.com/containerd/containerd/v2/core/mount" + "github.com/containerd/containerd/v2/internal/fsview" + + "golang.org/x/sys/unix" +) + +func TestOverlayFS(t *testing.T) { + lower := fstest.MapFS{ + "file1": &fstest.MapFile{Data: []byte("lower-file1")}, + "dir1/file2": &fstest.MapFile{Data: []byte("lower-file2")}, + "dir1/hidden": &fstest.MapFile{Data: []byte("lower-hidden")}, + "dir2/file3": &fstest.MapFile{Data: []byte("lower-file3")}, + } + + // upper has a whiteout for "dir1/hidden" + // To simulate whiteout, we need ModeCharDevice and Rdev=0. + // fstest.MapFile Mode is fs.FileMode. + upper := fstest.MapFS{ + "file1": &fstest.MapFile{Data: []byte("upper-file1")}, + "dir1/hidden": &fstest.MapFile{Mode: fs.ModeCharDevice, Sys: &syscall.Stat_t{Rdev: 0}}, + "dir3/file4": &fstest.MapFile{Data: []byte("upper-file4")}, + } + + ofs, err := fsview.NewOverlayFS([]fs.FS{upper, lower}) + if err != nil { + t.Fatal(err) + } + + // Test file1 (upper overrides lower) + data, err := fs.ReadFile(ofs, "file1") + if err != nil { + t.Fatal(err) + } + if string(data) != "upper-file1" { + t.Errorf("expected upper-file1, got %s", data) + } + + // Test dir1/file2 (from lower, merged dir) + data, err = fs.ReadFile(ofs, "dir1/file2") + if err != nil { + t.Fatal(err) + } + if string(data) != "lower-file2" { + t.Errorf("expected lower-file2, got %s", data) + } + + // Test dir1/hidden (whiteout in upper should hide lower) + // fs.ReadFile fails on device files anyway, but here it shouldn't even find it (NotExist). + // Wait, isWhiteout in Open returns NotExist. + _, err = fs.ReadFile(ofs, "dir1/hidden") + if err == nil { + t.Fatal("expected error for whiteout, got nil") + } + if !errors.Is(err, fs.ErrNotExist) { + t.Errorf("expected NotExist, got %v", err) + } + + // Test dir2/file3 (only in lower) + data, err = fs.ReadFile(ofs, "dir2/file3") + if err != nil { + t.Fatal(err) + } + if string(data) != "lower-file3" { + t.Errorf("expected lower-file3, got %s", data) + } + + // Test ReadDir merging in dir1 + entries, err := fs.ReadDir(ofs, "dir1") + if err != nil { + t.Fatal(err) + } + // Should contain: file2. hidden is whiteout so it should NOT appear. + foundFile2 := false + foundHidden := false + for _, e := range entries { + if e.Name() == "file2" { + foundFile2 = true + } + if e.Name() == "hidden" { + foundHidden = true + } + } + if !foundFile2 { + t.Error("dir1/file2 not found in ReadDir") + } + if foundHidden { + t.Error("dir1/hidden should not be found in ReadDir") + } + + // Test dir3/file4 (only in upper) + data, err = fs.ReadFile(ofs, "dir3/file4") + if err != nil { + t.Fatal(err) + } + if string(data) != "upper-file4" { + t.Errorf("expected upper-file4, got %s", data) + } +} + +// TestOverlayFSDirReplacedByFile tests that when an upper layer replaces a +// directory with a regular file, child paths return ErrNotExist rather than +// a confusing "not a directory" error. +// TestOverlayFSCrossLayerSymlink tests that a symlink in a lower layer +// correctly resolves to a file in an upper layer when the symlink target +// has been replaced by the upper layer. +func TestOverlayFSCrossLayerSymlink(t *testing.T) { + base := t.TempDir() + upper := filepath.Join(base, "upper") + lower := filepath.Join(base, "lower") + + // Lower layer: has a symlink etc/config -> ../opt/config + // and the original target file + if err := os.MkdirAll(filepath.Join(lower, "etc"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(lower, "opt"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.Symlink("../opt/config", filepath.Join(lower, "etc", "config")); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(lower, "opt", "config"), []byte("old-config"), 0o644); err != nil { + t.Fatal(err) + } + + // Upper layer: replaces opt/config with new content + if err := os.MkdirAll(filepath.Join(upper, "opt"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(upper, "opt", "config"), []byte("new-config"), 0o644); err != nil { + t.Fatal(err) + } + + lowerdir := strings.Join([]string{upper, lower}, ":") + v, err := fsview.FSMounts([]mount.Mount{ + { + Type: "overlay", + Source: "overlay", + Options: []string{ + "lowerdir=" + lowerdir, + }, + }, + }) + if err != nil { + t.Fatalf("FSMounts failed: %v", err) + } + defer v.Close() + + // Opening etc/config should follow the symlink through the overlay + // and find opt/config from the upper layer (new-config), not the lower. + data, err := fs.ReadFile(v, "etc/config") + if err != nil { + t.Fatalf("ReadFile etc/config: %v", err) + } + if string(data) != "new-config" { + t.Errorf("expected new-config, got %s", data) + } + + // Direct open of opt/config should also return upper layer content + data, err = fs.ReadFile(v, "opt/config") + if err != nil { + t.Fatalf("ReadFile opt/config: %v", err) + } + if string(data) != "new-config" { + t.Errorf("expected new-config, got %s", data) + } +} + +// TestOverlayFSAbsoluteSymlinkCrossLayer tests absolute symlinks that +// resolve across layers. +func TestOverlayFSAbsoluteSymlinkCrossLayer(t *testing.T) { + base := t.TempDir() + upper := filepath.Join(base, "upper") + lower := filepath.Join(base, "lower") + + // Lower layer: has /etc/group -> /nix/store/abcd/group + // and the original target file + if err := os.MkdirAll(filepath.Join(lower, "etc"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(lower, "nix", "store", "abcd"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.Symlink("/nix/store/abcd/group", filepath.Join(lower, "etc", "group")); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(lower, "nix", "store", "abcd", "group"), []byte("old-group"), 0o644); err != nil { + t.Fatal(err) + } + + // Upper layer: replaces nix/store/abcd/group with new content + if err := os.MkdirAll(filepath.Join(upper, "nix", "store", "abcd"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(upper, "nix", "store", "abcd", "group"), []byte("new-group"), 0o644); err != nil { + t.Fatal(err) + } + + lowerdir := strings.Join([]string{upper, lower}, ":") + v, err := fsview.FSMounts([]mount.Mount{ + { + Type: "overlay", + Source: "overlay", + Options: []string{ + "lowerdir=" + lowerdir, + }, + }, + }) + if err != nil { + t.Fatalf("FSMounts failed: %v", err) + } + defer v.Close() + + // Opening etc/group should follow the absolute symlink through the overlay + // and find nix/store/abcd/group from the upper layer. + data, err := fs.ReadFile(v, "etc/group") + if err != nil { + t.Fatalf("ReadFile etc/group: %v", err) + } + if string(data) != "new-group" { + t.Errorf("expected new-group, got %s", data) + } +} + +// TestOverlayFSSymlinkChain tests chained symlinks across layers. +func TestOverlayFSSymlinkChain(t *testing.T) { + base := t.TempDir() + upper := filepath.Join(base, "upper") + lower := filepath.Join(base, "lower") + + // Lower layer: a -> b (symlink), b -> c (symlink), c is a file + if err := os.MkdirAll(lower, 0o755); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(upper, 0o755); err != nil { + t.Fatal(err) + } + if err := os.Symlink("b", filepath.Join(lower, "a")); err != nil { + t.Fatal(err) + } + if err := os.Symlink("c", filepath.Join(lower, "b")); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(lower, "c"), []byte("lower-c"), 0o644); err != nil { + t.Fatal(err) + } + + // Upper layer: replaces c + if err := os.WriteFile(filepath.Join(upper, "c"), []byte("upper-c"), 0o644); err != nil { + t.Fatal(err) + } + + lowerdir := strings.Join([]string{upper, lower}, ":") + v, err := fsview.FSMounts([]mount.Mount{ + { + Type: "overlay", + Source: "overlay", + Options: []string{ + "lowerdir=" + lowerdir, + }, + }, + }) + if err != nil { + t.Fatalf("FSMounts failed: %v", err) + } + defer v.Close() + + // Opening a should follow a -> b -> c through the overlay, + // finding c from the upper layer. + data, err := fs.ReadFile(v, "a") + if err != nil { + t.Fatalf("ReadFile a: %v", err) + } + if string(data) != "upper-c" { + t.Errorf("expected upper-c, got %s", data) + } +} + +func TestOverlayFSDirReplacedByFile(t *testing.T) { + base := t.TempDir() + layer1 := filepath.Join(base, "layer1") + layer2 := filepath.Join(base, "layer2") + layer3 := filepath.Join(base, "layer3") + + // Layer 1: mkdir -p /dir1/dir2 + if err := os.MkdirAll(filepath.Join(layer1, "dir1", "dir2"), 0o755); err != nil { + t.Fatal(err) + } + + // Layer 2: touch /dir1/dir2/foo + if err := os.MkdirAll(filepath.Join(layer2, "dir1", "dir2"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(layer2, "dir1", "dir2", "foo"), []byte("foo"), 0o644); err != nil { + t.Fatal(err) + } + + // Layer 3: rm -r /dir1 && mkdir -p /dir1 && touch /dir1/dir2 + // (dir1 is opaque, dir2 is now a file instead of a directory) + if err := os.MkdirAll(filepath.Join(layer3, "dir1"), 0o755); err != nil { + t.Fatal(err) + } + if err := unix.Setxattr(filepath.Join(layer3, "dir1"), "user.overlay.opaque", []byte{'y'}, 0); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(layer3, "dir1", "dir2"), nil, 0o644); err != nil { + t.Fatal(err) + } + + lowerdir := strings.Join([]string{layer3, layer2, layer1}, ":") + v, err := fsview.FSMounts([]mount.Mount{ + { + Type: "overlay", + Source: "overlay", + Options: []string{ + "lowerdir=" + lowerdir, + }, + }, + }) + if err != nil { + t.Fatalf("FSMounts failed: %v", err) + } + defer v.Close() + + // dir1/dir2 should be a regular file (from layer3) + fi, err := fs.Stat(v, "dir1/dir2") + if err != nil { + t.Fatalf("stat dir1/dir2: %v", err) + } + if fi.IsDir() { + t.Fatal("expected dir1/dir2 to be a file, got directory") + } + + // dir1/dir2/foo should not exist — and the error should be ErrNotExist, + // not a confusing "not a directory" error. + _, err = v.Open("dir1/dir2/foo") + if !errors.Is(err, fs.ErrNotExist) { + t.Fatalf("expected dir1/dir2/foo to be ErrNotExist, got %v", err) + } +} diff --git a/internal/fsview/overlay_other.go b/internal/fsview/overlay_other.go new file mode 100644 index 0000000000000..a01005a7e3eeb --- /dev/null +++ b/internal/fsview/overlay_other.go @@ -0,0 +1,53 @@ +//go:build !linux + +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package fsview + +import "io/fs" + +func getxattr(f fs.File, name string) (string, bool) { + for _, h := range registered { + if h.Getxattr != nil { + if val, ok := h.Getxattr(f, name); ok { + return val, true + } + } + } + return "", false +} + +func isOpaque(f fs.File) bool { + for _, xattr := range OverlayOpaqueXattrs { + if val, ok := getxattr(f, xattr); ok && val == "y" { + return true + } + } + return false +} + +func isWhiteout(fi fs.FileInfo) bool { + if (fi.Mode() & fs.ModeCharDevice) == 0 { + return false + } + for _, h := range registered { + if h.IsWhiteout != nil && h.IsWhiteout(fi) { + return true + } + } + return false +} diff --git a/internal/fsview/register.go b/internal/fsview/register.go new file mode 100644 index 0000000000000..82154d7afaa0d --- /dev/null +++ b/internal/fsview/register.go @@ -0,0 +1,46 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package fsview + +import ( + "io/fs" + + "github.com/containerd/containerd/v2/core/mount" +) + +// FSHandler extends fsview with support for additional filesystem types. +// All fields are optional — set only the capabilities the handler provides. +type FSHandler struct { + // HandleMount converts a mount into a View. It should return + // errdefs.ErrNotImplemented if it cannot handle the mount type. + HandleMount func(m mount.Mount) (View, error) + + // Getxattr returns the value of the named extended attribute on the + // given file. The boolean indicates whether the attribute was found. + Getxattr func(f fs.File, name string) (string, bool) + + // IsWhiteout checks if a file info represents a whiteout entry. + IsWhiteout func(fi fs.FileInfo) bool +} + +var registered []FSHandler + +// Register adds a filesystem handler to extend fsview with support +// for additional filesystem types. +func Register(h FSHandler) { + registered = append(registered, h) +} diff --git a/pkg/oci/spec_opts.go b/pkg/oci/spec_opts.go index abc8e3d6812d1..18d6f43f90689 100644 --- a/pkg/oci/spec_opts.go +++ b/pkg/oci/spec_opts.go @@ -45,6 +45,7 @@ import ( "github.com/containerd/containerd/v2/core/content" "github.com/containerd/containerd/v2/core/images" "github.com/containerd/containerd/v2/core/mount" + "github.com/containerd/containerd/v2/internal/fsview" "github.com/containerd/containerd/v2/pkg/namespaces" ) @@ -619,9 +620,7 @@ func WithUser(userstr string) SpecOpts { // The `Username` field on the runtime spec is marked by Platform as only for Windows, and in this case it // *is* being set on a Windows host at least, but will be used as a temporary holding spot until the guest // can use the string to perform these same operations to grab the uid:gid inside. - // - // Mounts are not supported on Darwin, so using the same workaround. - if (s.Windows != nil && s.Linux != nil) || runtime.GOOS == "darwin" { + if s.Windows != nil && s.Linux != nil { s.Process.User.Username = userstr return nil } @@ -748,7 +747,7 @@ func WithUserID(uid uint32) SpecOpts { return u.Uid == int(uid) }) if err != nil { - if os.IsNotExist(err) || errors.Is(err, ErrNoUsersFound) { + if errors.Is(err, fs.ErrNotExist) || errors.Is(err, ErrNoUsersFound) { s.Process.User.UID, s.Process.User.GID = uid, 0 return nil } @@ -852,8 +851,8 @@ func WithUsername(username string) SpecOpts { // The passed in user can be either a uid or a username. func WithAdditionalGIDs(userstr string) SpecOpts { return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) { - // For LCOW or on Darwin additional GID's not supported - if s.Windows != nil || runtime.GOOS == "darwin" { + // For LCOW additional GID's not supported + if s.Windows != nil { return nil } setProcess(s) @@ -867,7 +866,7 @@ func WithAdditionalGIDs(userstr string) SpecOpts { return u.Uid == uid }) if err != nil { - if os.IsNotExist(err) || errors.Is(err, ErrNoUsersFound) { + if errors.Is(err, fs.ErrNotExist) || errors.Is(err, ErrNoUsersFound) { return nil } return err @@ -884,7 +883,7 @@ func WithAdditionalGIDs(userstr string) SpecOpts { return slices.Contains(g.List, username) }) if err != nil { - if os.IsNotExist(err) { + if errors.Is(err, fs.ErrNotExist) { return nil } return err @@ -924,6 +923,15 @@ func WithAdditionalGIDs(userstr string) SpecOpts { } func withReadonlyFS(ctx context.Context, client Client, mounts []mount.Mount, fn func(fs.FS) error) error { + // Try to avoid mount if possible by using fsview to directly open + // overlay/erofs/bind mounts without actually mounting them + if viewFS, err := fsview.FSMounts(mounts); err == nil && viewFS != nil { + defer viewFS.Close() + return fn(viewFS) + } else if !errors.Is(err, errdefs.ErrNotImplemented) || runtime.GOOS == "darwin" { + return err + } + var mm mount.Manager if cwm, ok := client.(interface{ MountManager() mount.Manager }); ok { mm = cwm.MountManager() @@ -959,8 +967,8 @@ func withReadonlyFS(ctx context.Context, client Client, mounts []mount.Mount, fn // The passed in groups can be either a gid or a groupname. func WithAppendAdditionalGroups(groups ...string) SpecOpts { return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) { - // For LCOW or on Darwin additional GID's are not supported - if s.Windows != nil || runtime.GOOS == "darwin" { + // For LCOW additional GID's are not supported + if s.Windows != nil { return nil } setProcess(s) @@ -975,7 +983,7 @@ func WithAppendAdditionalGroups(groups ...string) SpecOpts { if groupErr != nil { return groupErr } - } else if !os.IsNotExist(groupErr) { + } else if !errors.Is(groupErr, fs.ErrNotExist) { return groupErr } diff --git a/pkg/oci/spec_opts_linux_test.go b/pkg/oci/spec_opts_linux_test.go index d30f6257072d3..29e5008d4cd74 100644 --- a/pkg/oci/spec_opts_linux_test.go +++ b/pkg/oci/spec_opts_linux_test.go @@ -18,294 +18,19 @@ package oci import ( "context" - "fmt" "os" "path/filepath" "testing" - "github.com/containerd/continuity/fs/fstest" "github.com/opencontainers/runtime-spec/specs-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/sys/unix" - "github.com/containerd/containerd/v2/core/containers" "github.com/containerd/containerd/v2/pkg/cap" "github.com/containerd/containerd/v2/pkg/testutil" ) -//nolint:gosec -func TestWithUser(t *testing.T) { - t.Parallel() - - expectedPasswd := `root:x:0:0:root:/root:/bin/ash -guest:x:405:100:guest:/dev/null:/sbin/nologin -` - expectedGroup := `root:x:0:root -bin:x:1:root,bin,daemon -daemon:x:2:root,bin,daemon -sys:x:3:root,bin,adm -guest:x:100:guest -` - td := t.TempDir() - apply := fstest.Apply( - fstest.CreateDir("/etc", 0777), - fstest.CreateFile("/etc/passwd", []byte(expectedPasswd), 0777), - fstest.CreateFile("/etc/group", []byte(expectedGroup), 0777), - ) - if err := apply.Apply(td); err != nil { - t.Fatalf("failed to apply: %v", err) - } - c := containers.Container{ID: t.Name()} - testCases := []struct { - user string - expectedUID uint32 - expectedGID uint32 - err string - }{ - { - user: "0", - expectedUID: 0, - expectedGID: 0, - }, - { - user: "root:root", - expectedUID: 0, - expectedGID: 0, - }, - { - user: "guest", - expectedUID: 405, - expectedGID: 100, - }, - { - user: "guest:guest", - expectedUID: 405, - expectedGID: 100, - }, - { - user: "guest:nobody", - err: "no groups found", - }, - { - user: "405:100", - expectedUID: 405, - expectedGID: 100, - }, - { - user: "405:2147483648", - err: "no groups found", - }, - { - user: "-1000", - err: "no users found", - }, - { - user: "2147483648", - err: "no users found", - }, - } - for _, testCase := range testCases { - t.Run(testCase.user, func(t *testing.T) { - t.Parallel() - s := Spec{ - Version: specs.Version, - Root: &specs.Root{ - Path: td, - }, - Linux: &specs.Linux{}, - } - err := WithUser(testCase.user)(context.Background(), nil, &c, &s) - if err != nil { - assert.EqualError(t, err, testCase.err) - } - assert.Equal(t, testCase.expectedUID, s.Process.User.UID) - assert.Equal(t, testCase.expectedGID, s.Process.User.GID) - }) - } -} - -//nolint:gosec -func TestWithUserID(t *testing.T) { - t.Parallel() - - expectedPasswd := `root:x:0:0:root:/root:/bin/ash -guest:x:405:100:guest:/dev/null:/sbin/nologin -` - td := t.TempDir() - apply := fstest.Apply( - fstest.CreateDir("/etc", 0777), - fstest.CreateFile("/etc/passwd", []byte(expectedPasswd), 0777), - ) - if err := apply.Apply(td); err != nil { - t.Fatalf("failed to apply: %v", err) - } - c := containers.Container{ID: t.Name()} - testCases := []struct { - userID uint32 - expectedUID uint32 - expectedGID uint32 - }{ - { - userID: 0, - expectedUID: 0, - expectedGID: 0, - }, - { - userID: 405, - expectedUID: 405, - expectedGID: 100, - }, - { - userID: 1000, - expectedUID: 1000, - expectedGID: 0, - }, - } - for _, testCase := range testCases { - t.Run(fmt.Sprintf("user %d", testCase.userID), func(t *testing.T) { - t.Parallel() - s := Spec{ - Version: specs.Version, - Root: &specs.Root{ - Path: td, - }, - Linux: &specs.Linux{}, - } - err := WithUserID(testCase.userID)(context.Background(), nil, &c, &s) - assert.NoError(t, err) - assert.Equal(t, testCase.expectedUID, s.Process.User.UID) - assert.Equal(t, testCase.expectedGID, s.Process.User.GID) - }) - } -} - -//nolint:gosec -func TestWithUsername(t *testing.T) { - t.Parallel() - - expectedPasswd := `root:x:0:0:root:/root:/bin/ash -guest:x:405:100:guest:/dev/null:/sbin/nologin -` - td := t.TempDir() - apply := fstest.Apply( - fstest.CreateDir("/etc", 0777), - fstest.CreateFile("/etc/passwd", []byte(expectedPasswd), 0777), - ) - if err := apply.Apply(td); err != nil { - t.Fatalf("failed to apply: %v", err) - } - c := containers.Container{ID: t.Name()} - testCases := []struct { - user string - expectedUID uint32 - expectedGID uint32 - err string - }{ - { - user: "root", - expectedUID: 0, - expectedGID: 0, - }, - { - user: "guest", - expectedUID: 405, - expectedGID: 100, - }, - { - user: "1000", - err: "no users found", - }, - { - user: "unknown", - err: "no users found", - }, - } - for _, testCase := range testCases { - t.Run(testCase.user, func(t *testing.T) { - t.Parallel() - s := Spec{ - Version: specs.Version, - Root: &specs.Root{ - Path: td, - }, - Linux: &specs.Linux{}, - } - err := WithUsername(testCase.user)(context.Background(), nil, &c, &s) - if err != nil { - assert.EqualError(t, err, testCase.err) - } - assert.Equal(t, testCase.expectedUID, s.Process.User.UID) - assert.Equal(t, testCase.expectedGID, s.Process.User.GID) - }) - } - -} - -//nolint:gosec -func TestWithAdditionalGIDs(t *testing.T) { - t.Parallel() - expectedPasswd := `root:x:0:0:root:/root:/bin/ash -bin:x:1:1:bin:/bin:/sbin/nologin -daemon:x:2:2:daemon:/sbin:/sbin/nologin -` - expectedGroup := `root:x:0:root -bin:x:1:root,bin,daemon -daemon:x:2:root,bin,daemon -sys:x:3:root,bin,adm -` - td := t.TempDir() - apply := fstest.Apply( - fstest.CreateDir("/etc", 0777), - fstest.CreateFile("/etc/passwd", []byte(expectedPasswd), 0777), - fstest.CreateFile("/etc/group", []byte(expectedGroup), 0777), - ) - if err := apply.Apply(td); err != nil { - t.Fatalf("failed to apply: %v", err) - } - c := containers.Container{ID: t.Name()} - - testCases := []struct { - user string - expected []uint32 - }{ - { - user: "root", - expected: []uint32{0, 1, 2, 3}, - }, - { - user: "1000", - expected: []uint32{0}, - }, - { - user: "bin", - expected: []uint32{0, 2, 3}, - }, - { - user: "bin:root", - expected: []uint32{0}, - }, - { - user: "daemon", - expected: []uint32{0, 1}, - }, - } - for _, testCase := range testCases { - t.Run(testCase.user, func(t *testing.T) { - t.Parallel() - s := Spec{ - Version: specs.Version, - Root: &specs.Root{ - Path: td, - }, - } - err := WithAdditionalGIDs(testCase.user)(context.Background(), nil, &c, &s) - assert.NoError(t, err) - assert.Equal(t, testCase.expected, s.Process.User.AdditionalGids) - }) - } -} - // withAllKnownCaps sets all known capabilities. // This function differs from the exported function // by also setting inheritable capabilities. @@ -636,143 +361,6 @@ func TestGetDevices(t *testing.T) { }) } -func TestWithAppendAdditionalGroups(t *testing.T) { - t.Parallel() - expectedContent := `root:x:0:root -bin:x:1:root,bin,daemon -daemon:x:2:root,bin,daemon -` - td := t.TempDir() - apply := fstest.Apply( - fstest.CreateDir("/etc", 0777), - fstest.CreateFile("/etc/group", []byte(expectedContent), 0777), - ) - if err := apply.Apply(td); err != nil { - t.Fatalf("failed to apply: %v", err) - } - c := containers.Container{ID: t.Name()} - - testCases := []struct { - name string - additionalGIDs []uint32 - groups []string - expected []uint32 - err string - }{ - { - name: "no additional gids", - groups: []string{}, - expected: []uint32{0}, - }, - { - name: "no additional gids, append root gid", - groups: []string{"root"}, - expected: []uint32{0}, - }, - { - name: "no additional gids, append bin and daemon gids", - groups: []string{"bin", "daemon"}, - expected: []uint32{0, 1, 2}, - }, - { - name: "has root additional gids, append bin and daemon gids", - additionalGIDs: []uint32{0}, - groups: []string{"bin", "daemon"}, - expected: []uint32{0, 1, 2}, - }, - { - name: "append group id", - groups: []string{"999"}, - expected: []uint32{0, 999}, - }, - { - name: "unknown group", - groups: []string{"unknown"}, - err: "unable to find group unknown", - expected: []uint32{0}, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - s := Spec{ - Version: specs.Version, - Root: &specs.Root{ - Path: td, - }, - Process: &specs.Process{ - User: specs.User{ - AdditionalGids: testCase.additionalGIDs, - }, - }, - } - err := WithAppendAdditionalGroups(testCase.groups...)(context.Background(), nil, &c, &s) - if err != nil { - assert.EqualError(t, err, testCase.err) - } - assert.Equal(t, testCase.expected, s.Process.User.AdditionalGids) - }) - } -} - -func TestWithAppendAdditionalGroupsNoEtcGroup(t *testing.T) { - t.Parallel() - td := t.TempDir() - apply := fstest.Apply() - if err := apply.Apply(td); err != nil { - t.Fatalf("failed to apply: %v", err) - } - c := containers.Container{ID: t.Name()} - - testCases := []struct { - name string - additionalGIDs []uint32 - groups []string - expected []uint32 - err string - }{ - { - name: "no additional gids", - groups: []string{}, - expected: []uint32{0}, - }, - { - name: "no additional gids, append root group", - groups: []string{"root"}, - err: "unable to find group root: openat etc/group: no such file or directory", - expected: []uint32{0}, - }, - { - name: "append group id", - groups: []string{"999"}, - expected: []uint32{0, 999}, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - s := Spec{ - Version: specs.Version, - Root: &specs.Root{ - Path: td, - }, - Process: &specs.Process{ - User: specs.User{ - AdditionalGids: testCase.additionalGIDs, - }, - }, - } - err := WithAppendAdditionalGroups(testCase.groups...)(context.Background(), nil, &c, &s) - if err != nil { - assert.EqualError(t, err, testCase.err) - } - assert.Equal(t, testCase.expected, s.Process.User.AdditionalGids) - }) - } -} - func TestWithLinuxDeviceFollowSymlinks(t *testing.T) { // Create symlink to /dev/zero for the symlink test case diff --git a/pkg/oci/spec_opts_user_test.go b/pkg/oci/spec_opts_user_test.go new file mode 100644 index 0000000000000..06398d0094ecc --- /dev/null +++ b/pkg/oci/spec_opts_user_test.go @@ -0,0 +1,438 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package oci + +import ( + "context" + "fmt" + "testing" + + "github.com/containerd/continuity/fs/fstest" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/stretchr/testify/assert" + + "github.com/containerd/containerd/v2/core/containers" +) + +//nolint:gosec +func TestWithUser(t *testing.T) { + t.Parallel() + + expectedPasswd := `root:x:0:0:root:/root:/bin/ash +guest:x:405:100:guest:/dev/null:/sbin/nologin +` + expectedGroup := `root:x:0:root +bin:x:1:root,bin,daemon +daemon:x:2:root,bin,daemon +sys:x:3:root,bin,adm +guest:x:100:guest +` + td := t.TempDir() + apply := fstest.Apply( + fstest.CreateDir("/etc", 0777), + fstest.CreateFile("/etc/passwd", []byte(expectedPasswd), 0777), + fstest.CreateFile("/etc/group", []byte(expectedGroup), 0777), + ) + if err := apply.Apply(td); err != nil { + t.Fatalf("failed to apply: %v", err) + } + c := containers.Container{ID: t.Name()} + testCases := []struct { + user string + expectedUID uint32 + expectedGID uint32 + err string + }{ + { + user: "0", + expectedUID: 0, + expectedGID: 0, + }, + { + user: "root:root", + expectedUID: 0, + expectedGID: 0, + }, + { + user: "guest", + expectedUID: 405, + expectedGID: 100, + }, + { + user: "guest:guest", + expectedUID: 405, + expectedGID: 100, + }, + { + user: "guest:nobody", + err: "no groups found", + }, + { + user: "405:100", + expectedUID: 405, + expectedGID: 100, + }, + { + user: "405:2147483648", + err: "no groups found", + }, + { + user: "-1000", + err: "no users found", + }, + { + user: "2147483648", + err: "no users found", + }, + } + for _, testCase := range testCases { + t.Run(testCase.user, func(t *testing.T) { + t.Parallel() + s := Spec{ + Version: specs.Version, + Root: &specs.Root{ + Path: td, + }, + Linux: &specs.Linux{}, + } + err := WithUser(testCase.user)(context.Background(), nil, &c, &s) + if err != nil { + assert.EqualError(t, err, testCase.err) + } + assert.Equal(t, testCase.expectedUID, s.Process.User.UID) + assert.Equal(t, testCase.expectedGID, s.Process.User.GID) + }) + } +} + +//nolint:gosec +func TestWithUserID(t *testing.T) { + t.Parallel() + + expectedPasswd := `root:x:0:0:root:/root:/bin/ash +guest:x:405:100:guest:/dev/null:/sbin/nologin +` + td := t.TempDir() + apply := fstest.Apply( + fstest.CreateDir("/etc", 0777), + fstest.CreateFile("/etc/passwd", []byte(expectedPasswd), 0777), + ) + if err := apply.Apply(td); err != nil { + t.Fatalf("failed to apply: %v", err) + } + c := containers.Container{ID: t.Name()} + testCases := []struct { + userID uint32 + expectedUID uint32 + expectedGID uint32 + }{ + { + userID: 0, + expectedUID: 0, + expectedGID: 0, + }, + { + userID: 405, + expectedUID: 405, + expectedGID: 100, + }, + { + userID: 1000, + expectedUID: 1000, + expectedGID: 0, + }, + } + for _, testCase := range testCases { + t.Run(fmt.Sprintf("user %d", testCase.userID), func(t *testing.T) { + t.Parallel() + s := Spec{ + Version: specs.Version, + Root: &specs.Root{ + Path: td, + }, + Linux: &specs.Linux{}, + } + err := WithUserID(testCase.userID)(context.Background(), nil, &c, &s) + assert.NoError(t, err) + assert.Equal(t, testCase.expectedUID, s.Process.User.UID) + assert.Equal(t, testCase.expectedGID, s.Process.User.GID) + }) + } +} + +//nolint:gosec +func TestWithUsername(t *testing.T) { + t.Parallel() + + expectedPasswd := `root:x:0:0:root:/root:/bin/ash +guest:x:405:100:guest:/dev/null:/sbin/nologin +` + td := t.TempDir() + apply := fstest.Apply( + fstest.CreateDir("/etc", 0777), + fstest.CreateFile("/etc/passwd", []byte(expectedPasswd), 0777), + ) + if err := apply.Apply(td); err != nil { + t.Fatalf("failed to apply: %v", err) + } + c := containers.Container{ID: t.Name()} + testCases := []struct { + user string + expectedUID uint32 + expectedGID uint32 + err string + }{ + { + user: "root", + expectedUID: 0, + expectedGID: 0, + }, + { + user: "guest", + expectedUID: 405, + expectedGID: 100, + }, + { + user: "1000", + err: "no users found", + }, + { + user: "unknown", + err: "no users found", + }, + } + for _, testCase := range testCases { + t.Run(testCase.user, func(t *testing.T) { + t.Parallel() + s := Spec{ + Version: specs.Version, + Root: &specs.Root{ + Path: td, + }, + Linux: &specs.Linux{}, + } + err := WithUsername(testCase.user)(context.Background(), nil, &c, &s) + if err != nil { + assert.EqualError(t, err, testCase.err) + } + assert.Equal(t, testCase.expectedUID, s.Process.User.UID) + assert.Equal(t, testCase.expectedGID, s.Process.User.GID) + }) + } + +} + +//nolint:gosec +func TestWithAdditionalGIDs(t *testing.T) { + t.Parallel() + expectedPasswd := `root:x:0:0:root:/root:/bin/ash +bin:x:1:1:bin:/bin:/sbin/nologin +daemon:x:2:2:daemon:/sbin:/sbin/nologin +` + expectedGroup := `root:x:0:root +bin:x:1:root,bin,daemon +daemon:x:2:root,bin,daemon +sys:x:3:root,bin,adm +` + td := t.TempDir() + apply := fstest.Apply( + fstest.CreateDir("/etc", 0777), + fstest.CreateFile("/etc/passwd", []byte(expectedPasswd), 0777), + fstest.CreateFile("/etc/group", []byte(expectedGroup), 0777), + ) + if err := apply.Apply(td); err != nil { + t.Fatalf("failed to apply: %v", err) + } + c := containers.Container{ID: t.Name()} + + testCases := []struct { + user string + expected []uint32 + }{ + { + user: "root", + expected: []uint32{0, 1, 2, 3}, + }, + { + user: "1000", + expected: []uint32{0}, + }, + { + user: "bin", + expected: []uint32{0, 2, 3}, + }, + { + user: "bin:root", + expected: []uint32{0}, + }, + { + user: "daemon", + expected: []uint32{0, 1}, + }, + } + for _, testCase := range testCases { + t.Run(testCase.user, func(t *testing.T) { + t.Parallel() + s := Spec{ + Version: specs.Version, + Root: &specs.Root{ + Path: td, + }, + } + err := WithAdditionalGIDs(testCase.user)(context.Background(), nil, &c, &s) + assert.NoError(t, err) + assert.Equal(t, testCase.expected, s.Process.User.AdditionalGids) + }) + } +} + +func TestWithAppendAdditionalGroups(t *testing.T) { + t.Parallel() + expectedContent := `root:x:0:root +bin:x:1:root,bin,daemon +daemon:x:2:root,bin,daemon +` + td := t.TempDir() + apply := fstest.Apply( + fstest.CreateDir("/etc", 0777), + fstest.CreateFile("/etc/group", []byte(expectedContent), 0777), + ) + if err := apply.Apply(td); err != nil { + t.Fatalf("failed to apply: %v", err) + } + c := containers.Container{ID: t.Name()} + + testCases := []struct { + name string + additionalGIDs []uint32 + groups []string + expected []uint32 + err string + }{ + { + name: "no additional gids", + groups: []string{}, + expected: []uint32{0}, + }, + { + name: "no additional gids, append root gid", + groups: []string{"root"}, + expected: []uint32{0}, + }, + { + name: "no additional gids, append bin and daemon gids", + groups: []string{"bin", "daemon"}, + expected: []uint32{0, 1, 2}, + }, + { + name: "has root additional gids, append bin and daemon gids", + additionalGIDs: []uint32{0}, + groups: []string{"bin", "daemon"}, + expected: []uint32{0, 1, 2}, + }, + { + name: "append group id", + groups: []string{"999"}, + expected: []uint32{0, 999}, + }, + { + name: "unknown group", + groups: []string{"unknown"}, + err: "unable to find group unknown", + expected: []uint32{0}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + s := Spec{ + Version: specs.Version, + Root: &specs.Root{ + Path: td, + }, + Process: &specs.Process{ + User: specs.User{ + AdditionalGids: testCase.additionalGIDs, + }, + }, + } + err := WithAppendAdditionalGroups(testCase.groups...)(context.Background(), nil, &c, &s) + if err != nil { + assert.EqualError(t, err, testCase.err) + } + assert.Equal(t, testCase.expected, s.Process.User.AdditionalGids) + }) + } +} + +func TestWithAppendAdditionalGroupsNoEtcGroup(t *testing.T) { + t.Parallel() + td := t.TempDir() + apply := fstest.Apply() + if err := apply.Apply(td); err != nil { + t.Fatalf("failed to apply: %v", err) + } + c := containers.Container{ID: t.Name()} + + testCases := []struct { + name string + additionalGIDs []uint32 + groups []string + expected []uint32 + errContains string + }{ + { + name: "no additional gids", + groups: []string{}, + expected: []uint32{0}, + }, + { + name: "no additional gids, append root group", + groups: []string{"root"}, + errContains: "unable to find group root", + expected: []uint32{0}, + }, + { + name: "append group id", + groups: []string{"999"}, + expected: []uint32{0, 999}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + s := Spec{ + Version: specs.Version, + Root: &specs.Root{ + Path: td, + }, + Process: &specs.Process{ + User: specs.User{ + AdditionalGids: testCase.additionalGIDs, + }, + }, + } + err := WithAppendAdditionalGroups(testCase.groups...)(context.Background(), nil, &c, &s) + if err != nil { + assert.ErrorContains(t, err, testCase.errContains) + } + assert.Equal(t, testCase.expected, s.Process.User.AdditionalGids) + }) + } +} diff --git a/plugins/mount/fsview/erofs/erofs.go b/plugins/mount/fsview/erofs/erofs.go new file mode 100644 index 0000000000000..8039b33500ebe --- /dev/null +++ b/plugins/mount/fsview/erofs/erofs.go @@ -0,0 +1,134 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package erofs + +import ( + "errors" + "fmt" + "io" + "io/fs" + "os" + "strings" + + "github.com/containerd/containerd/v2/core/mount" + "github.com/containerd/containerd/v2/internal/fsview" + "github.com/containerd/errdefs" + "github.com/erofs/go-erofs" +) + +func init() { + fsview.Register(fsview.FSHandler{ + HandleMount: handleMount, + Getxattr: getxattr, + IsWhiteout: isWhiteout, + }) +} + +func handleMount(m mount.Mount) (fsview.View, error) { + if m.Type != "erofs" { + return nil, errdefs.ErrNotImplemented + } + + f, err := os.Open(m.Source) + if err != nil { + return nil, err + } + + var extraDevices []io.ReaderAt + var closers []io.Closer + closers = append(closers, f) + + for _, opt := range m.Options { + if devPath, ok := strings.CutPrefix(opt, "device="); ok { + if devPath == "" { + continue + } + df, err := os.Open(devPath) + if err != nil { + for _, c := range closers { + c.Close() + } + return nil, err + } + closers = append(closers, df) + extraDevices = append(extraDevices, df) + } + } + + var opts []erofs.OpenOpt + if len(extraDevices) > 0 { + opts = append(opts, erofs.WithExtraDevices(extraDevices...)) + } + + efs, err := erofs.Open(f, opts...) + if err != nil { + for _, c := range closers { + c.Close() + } + return nil, err + } + + rlfs, ok := efs.(fs.ReadLinkFS) + if !ok { + for _, c := range closers { + c.Close() + } + return nil, fmt.Errorf("erofs: filesystem does not implement fs.ReadLinkFS: %w", errdefs.ErrNotImplemented) + } + + return &erofsView{ + ReadLinkFS: rlfs, + closers: closers, + }, nil +} + +type erofsView struct { + fs.ReadLinkFS + closers []io.Closer +} + +func (v *erofsView) Close() error { + var errs []error + for _, c := range v.closers { + errs = append(errs, c.Close()) + } + return errors.Join(errs...) +} + +func getxattr(f fs.File, name string) (string, bool) { + fi, err := f.Stat() + if err != nil { + return "", false + } + estatfi, ok := fi.Sys().(*erofs.Stat) + if !ok { + return "", false + } + val, ok := estatfi.Xattrs[name] + return val, ok +} + +func isWhiteout(fi fs.FileInfo) bool { + if (fi.Mode() & fs.ModeCharDevice) == 0 { + return false + } + estatfi, ok := fi.Sys().(*erofs.Stat) + if !ok { + return false + } + return estatfi.Rdev == 0 +} diff --git a/vendor/github.com/Microsoft/go-winio/.golangci.yml b/vendor/github.com/Microsoft/go-winio/.golangci.yml index faedfe937a7a2..924b5ce5968c6 100644 --- a/vendor/github.com/Microsoft/go-winio/.golangci.yml +++ b/vendor/github.com/Microsoft/go-winio/.golangci.yml @@ -59,6 +59,10 @@ issues: text: "^directive `//nolint:errorlint` should provide explanation" source: '[=|!]= io.EOF' + - linters: + - gosec + text: "^G115: integer overflow conversion" + linters-settings: exhaustive: diff --git a/vendor/github.com/Microsoft/go-winio/backup.go b/vendor/github.com/Microsoft/go-winio/backup.go index b54341daacb74..7c2e4ba233228 100644 --- a/vendor/github.com/Microsoft/go-winio/backup.go +++ b/vendor/github.com/Microsoft/go-winio/backup.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package winio diff --git a/vendor/github.com/Microsoft/go-winio/backuptar/tar.go b/vendor/github.com/Microsoft/go-winio/backuptar/tar.go index 7f852bbf81bd1..a4d2202f9cd32 100644 --- a/vendor/github.com/Microsoft/go-winio/backuptar/tar.go +++ b/vendor/github.com/Microsoft/go-winio/backuptar/tar.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package backuptar diff --git a/vendor/github.com/Microsoft/go-winio/file.go b/vendor/github.com/Microsoft/go-winio/file.go index fe82a180dbddb..f38213763e097 100644 --- a/vendor/github.com/Microsoft/go-winio/file.go +++ b/vendor/github.com/Microsoft/go-winio/file.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package winio diff --git a/vendor/github.com/Microsoft/go-winio/fileinfo.go b/vendor/github.com/Microsoft/go-winio/fileinfo.go index c860eb9917a5c..01cd891e840dc 100644 --- a/vendor/github.com/Microsoft/go-winio/fileinfo.go +++ b/vendor/github.com/Microsoft/go-winio/fileinfo.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package winio diff --git a/vendor/github.com/Microsoft/go-winio/hvsock.go b/vendor/github.com/Microsoft/go-winio/hvsock.go index c4fdd9d4aec23..d4960f5b0a114 100644 --- a/vendor/github.com/Microsoft/go-winio/hvsock.go +++ b/vendor/github.com/Microsoft/go-winio/hvsock.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package winio diff --git a/vendor/github.com/Microsoft/go-winio/pipe.go b/vendor/github.com/Microsoft/go-winio/pipe.go index a2da6639d00d0..58c68e9522ec9 100644 --- a/vendor/github.com/Microsoft/go-winio/pipe.go +++ b/vendor/github.com/Microsoft/go-winio/pipe.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package winio diff --git a/vendor/github.com/Microsoft/go-winio/pkg/bindfilter/bind_filter.go b/vendor/github.com/Microsoft/go-winio/pkg/bindfilter/bind_filter.go index 7c7f145f445a6..f3e9d7b29eebc 100644 --- a/vendor/github.com/Microsoft/go-winio/pkg/bindfilter/bind_filter.go +++ b/vendor/github.com/Microsoft/go-winio/pkg/bindfilter/bind_filter.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package bindfilter @@ -109,7 +108,7 @@ func GetBindMappings(volumePath string) ([]BindMapping, error) { } if outBuffSize < 12 { - return nil, fmt.Errorf("invalid buffer returned") + return nil, errors.New("invalid buffer returned") } result := buf[:outBuffSize] @@ -185,7 +184,7 @@ func decodeEntry(buffer []byte) (string, error) { func getTargetsFromBuffer(buffer []byte, offset, count int) ([]string, error) { if len(buffer) < offset+count*6 { - return nil, fmt.Errorf("invalid buffer") + return nil, errors.New("invalid buffer") } targets := make([]string, count) @@ -193,7 +192,7 @@ func getTargetsFromBuffer(buffer []byte, offset, count int) ([]string, error) { entryBuf := buffer[offset+i*8 : offset+i*8+8] tgt := *(*mappingTargetEntry)(unsafe.Pointer(&entryBuf[0])) if len(buffer) < int(tgt.TargetRootOffset)+int(tgt.TargetRootLength) { - return nil, fmt.Errorf("invalid buffer") + return nil, errors.New("invalid buffer") } decoded, err := decodeEntry(buffer[tgt.TargetRootOffset : tgt.TargetRootOffset+tgt.TargetRootLength]) if err != nil { @@ -259,7 +258,7 @@ func getFinalPath(pth string) (string, error) { func getBindMappingFromBuffer(buffer []byte, entry mappingEntry) (BindMapping, error) { if len(buffer) < int(entry.VirtRootOffset)+int(entry.VirtRootLength) { - return BindMapping{}, fmt.Errorf("invalid buffer") + return BindMapping{}, errors.New("invalid buffer") } src, err := decodeEntry(buffer[entry.VirtRootOffset : entry.VirtRootOffset+entry.VirtRootLength]) diff --git a/vendor/github.com/Microsoft/go-winio/pkg/etw/eventdata.go b/vendor/github.com/Microsoft/go-winio/pkg/etw/eventdata.go index f971cc77b596f..7ac6b50414479 100644 --- a/vendor/github.com/Microsoft/go-winio/pkg/etw/eventdata.go +++ b/vendor/github.com/Microsoft/go-winio/pkg/etw/eventdata.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package etw diff --git a/vendor/github.com/Microsoft/go-winio/pkg/etw/eventopt.go b/vendor/github.com/Microsoft/go-winio/pkg/etw/eventopt.go index 73403220c9fb8..9c06fef8246ab 100644 --- a/vendor/github.com/Microsoft/go-winio/pkg/etw/eventopt.go +++ b/vendor/github.com/Microsoft/go-winio/pkg/etw/eventopt.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package etw diff --git a/vendor/github.com/Microsoft/go-winio/pkg/etw/fieldopt.go b/vendor/github.com/Microsoft/go-winio/pkg/etw/fieldopt.go index 57114d8da31db..2798e979ff1b5 100644 --- a/vendor/github.com/Microsoft/go-winio/pkg/etw/fieldopt.go +++ b/vendor/github.com/Microsoft/go-winio/pkg/etw/fieldopt.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package etw diff --git a/vendor/github.com/Microsoft/go-winio/pkg/etw/newprovider.go b/vendor/github.com/Microsoft/go-winio/pkg/etw/newprovider.go index 3669b4f783a80..0c3e301db8cec 100644 --- a/vendor/github.com/Microsoft/go-winio/pkg/etw/newprovider.go +++ b/vendor/github.com/Microsoft/go-winio/pkg/etw/newprovider.go @@ -1,6 +1,4 @@ //go:build windows && (amd64 || arm64 || 386) -// +build windows -// +build amd64 arm64 386 package etw diff --git a/vendor/github.com/Microsoft/go-winio/pkg/etw/newprovider_unsupported.go b/vendor/github.com/Microsoft/go-winio/pkg/etw/newprovider_unsupported.go index e0057cfe0d078..18f9dfef0852c 100644 --- a/vendor/github.com/Microsoft/go-winio/pkg/etw/newprovider_unsupported.go +++ b/vendor/github.com/Microsoft/go-winio/pkg/etw/newprovider_unsupported.go @@ -1,5 +1,4 @@ //go:build windows && arm -// +build windows,arm package etw diff --git a/vendor/github.com/Microsoft/go-winio/pkg/etw/provider.go b/vendor/github.com/Microsoft/go-winio/pkg/etw/provider.go index 8174bff1b0870..089808a929b07 100644 --- a/vendor/github.com/Microsoft/go-winio/pkg/etw/provider.go +++ b/vendor/github.com/Microsoft/go-winio/pkg/etw/provider.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package etw diff --git a/vendor/github.com/Microsoft/go-winio/pkg/etw/providerglobal.go b/vendor/github.com/Microsoft/go-winio/pkg/etw/providerglobal.go index 0a1d90dda0283..831697a6592c8 100644 --- a/vendor/github.com/Microsoft/go-winio/pkg/etw/providerglobal.go +++ b/vendor/github.com/Microsoft/go-winio/pkg/etw/providerglobal.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package etw diff --git a/vendor/github.com/Microsoft/go-winio/pkg/etw/ptr64_32.go b/vendor/github.com/Microsoft/go-winio/pkg/etw/ptr64_32.go index 26c9f1948af37..e89da492c74da 100644 --- a/vendor/github.com/Microsoft/go-winio/pkg/etw/ptr64_32.go +++ b/vendor/github.com/Microsoft/go-winio/pkg/etw/ptr64_32.go @@ -1,6 +1,4 @@ //go:build windows && (386 || arm) -// +build windows -// +build 386 arm package etw diff --git a/vendor/github.com/Microsoft/go-winio/pkg/etw/ptr64_64.go b/vendor/github.com/Microsoft/go-winio/pkg/etw/ptr64_64.go index 1524c643fd29d..d991938872789 100644 --- a/vendor/github.com/Microsoft/go-winio/pkg/etw/ptr64_64.go +++ b/vendor/github.com/Microsoft/go-winio/pkg/etw/ptr64_64.go @@ -1,6 +1,4 @@ //go:build windows && (amd64 || arm64) -// +build windows -// +build amd64 arm64 package etw diff --git a/vendor/github.com/Microsoft/go-winio/pkg/etw/wrapper_32.go b/vendor/github.com/Microsoft/go-winio/pkg/etw/wrapper_32.go index 14c49984208cf..eba93fd79b3f6 100644 --- a/vendor/github.com/Microsoft/go-winio/pkg/etw/wrapper_32.go +++ b/vendor/github.com/Microsoft/go-winio/pkg/etw/wrapper_32.go @@ -1,6 +1,4 @@ //go:build windows && (386 || arm) -// +build windows -// +build 386 arm package etw diff --git a/vendor/github.com/Microsoft/go-winio/pkg/etw/wrapper_64.go b/vendor/github.com/Microsoft/go-winio/pkg/etw/wrapper_64.go index 8cfe2e8cab6c2..8f53ad7bcf53c 100644 --- a/vendor/github.com/Microsoft/go-winio/pkg/etw/wrapper_64.go +++ b/vendor/github.com/Microsoft/go-winio/pkg/etw/wrapper_64.go @@ -1,6 +1,4 @@ //go:build windows && (amd64 || arm64) -// +build windows -// +build amd64 arm64 package etw diff --git a/vendor/github.com/Microsoft/go-winio/pkg/etwlogrus/hook.go b/vendor/github.com/Microsoft/go-winio/pkg/etwlogrus/hook.go index 76f6239a54dc3..71f658ed99598 100644 --- a/vendor/github.com/Microsoft/go-winio/pkg/etwlogrus/hook.go +++ b/vendor/github.com/Microsoft/go-winio/pkg/etwlogrus/hook.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package etwlogrus diff --git a/vendor/github.com/Microsoft/go-winio/pkg/guid/guid_nonwindows.go b/vendor/github.com/Microsoft/go-winio/pkg/guid/guid_nonwindows.go index 805bd35484241..b933821a54c88 100644 --- a/vendor/github.com/Microsoft/go-winio/pkg/guid/guid_nonwindows.go +++ b/vendor/github.com/Microsoft/go-winio/pkg/guid/guid_nonwindows.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows package guid diff --git a/vendor/github.com/Microsoft/go-winio/pkg/guid/guid_windows.go b/vendor/github.com/Microsoft/go-winio/pkg/guid/guid_windows.go index 27e45ee5ccf9e..4aa95a7068749 100644 --- a/vendor/github.com/Microsoft/go-winio/pkg/guid/guid_windows.go +++ b/vendor/github.com/Microsoft/go-winio/pkg/guid/guid_windows.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package guid diff --git a/vendor/github.com/Microsoft/go-winio/pkg/security/grantvmgroupaccess.go b/vendor/github.com/Microsoft/go-winio/pkg/security/grantvmgroupaccess.go index 2cef49d9cff98..a75235201c493 100644 --- a/vendor/github.com/Microsoft/go-winio/pkg/security/grantvmgroupaccess.go +++ b/vendor/github.com/Microsoft/go-winio/pkg/security/grantvmgroupaccess.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package security diff --git a/vendor/github.com/Microsoft/go-winio/privilege.go b/vendor/github.com/Microsoft/go-winio/privilege.go index d9b90b6e8614c..747eefd326d6b 100644 --- a/vendor/github.com/Microsoft/go-winio/privilege.go +++ b/vendor/github.com/Microsoft/go-winio/privilege.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package winio diff --git a/vendor/github.com/Microsoft/go-winio/reparse.go b/vendor/github.com/Microsoft/go-winio/reparse.go index 67d1a104a63f6..b03e517f3cad2 100644 --- a/vendor/github.com/Microsoft/go-winio/reparse.go +++ b/vendor/github.com/Microsoft/go-winio/reparse.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package winio diff --git a/vendor/github.com/Microsoft/go-winio/sd.go b/vendor/github.com/Microsoft/go-winio/sd.go index c3685e98e14d4..7834c6adcb9b4 100644 --- a/vendor/github.com/Microsoft/go-winio/sd.go +++ b/vendor/github.com/Microsoft/go-winio/sd.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package winio diff --git a/vendor/github.com/Microsoft/go-winio/vhd/vhd.go b/vendor/github.com/Microsoft/go-winio/vhd/vhd.go index b54cad112703e..7305cb8879b93 100644 --- a/vendor/github.com/Microsoft/go-winio/vhd/vhd.go +++ b/vendor/github.com/Microsoft/go-winio/vhd/vhd.go @@ -1,11 +1,14 @@ //go:build windows -// +build windows package vhd import ( + "bytes" + "encoding/binary" "fmt" + "strings" "syscall" + "unsafe" "github.com/Microsoft/go-winio/pkg/guid" "golang.org/x/sys/windows" @@ -18,6 +21,8 @@ import ( //sys attachVirtualDisk(handle syscall.Handle, securityDescriptor *uintptr, attachVirtualDiskFlag uint32, providerSpecificFlags uint32, parameters *AttachVirtualDiskParameters, overlapped *syscall.Overlapped) (win32err error) = virtdisk.AttachVirtualDisk //sys detachVirtualDisk(handle syscall.Handle, detachVirtualDiskFlags uint32, providerSpecificFlags uint32) (win32err error) = virtdisk.DetachVirtualDisk //sys getVirtualDiskPhysicalPath(handle syscall.Handle, diskPathSizeInBytes *uint32, buffer *uint16) (win32err error) = virtdisk.GetVirtualDiskPhysicalPath +//sys getVirtualDiskInformation(handle syscall.Handle, bufferSize *uint32, info *virtualDiskInfo, sizeUsed *uint32) (win32err error) = virtdisk.GetVirtualDiskInformation +//sys setVirtualDiskInformation(handle syscall.Handle, info *virtualDiskInfo) (win32err error) = virtdisk.SetVirtualDiskInformation type ( CreateVirtualDiskFlag uint32 @@ -86,6 +91,20 @@ type AttachVirtualDiskParameters struct { Version2 AttachVersion2 } +// `virtualDiskInfo` struct is used to represent both GET_VIRTUAL_DISK_INFO +// (https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/ns-virtdisk-get_virtual_disk_info) +// and SET_VIRTUAL_DISK_INFO +// (https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/ns-virtdisk-set_virtual_disk_info) +// win32 types. Both of these win32 types have the same size and a very similar +// structure. These types use tagged unions which aren't directly supported in Go, so we +// keep this type unexported, and provide a cleaner interface to our callers by parsing +// the data buffer for the right type. +type virtualDiskInfo struct { + version uint32 + _ [4]byte // padding + data [24]byte // union of various types +} + const ( //revive:disable-next-line:var-naming ALL_CAPS VIRTUAL_STORAGE_TYPE_DEVICE_VHDX = 0x3 @@ -143,6 +162,34 @@ const ( // Flags for detaching a VHD. DetachVirtualDiskFlagNone DetachVirtualDiskFlag = 0x0 + + // Flags for setting information about a VHD - these should remain unexported as we provide APIs to directly get/set a particular field. + setVirtualDiskInfoUnspecified uint32 = 0x0 + setVirtualDiskInfoParentPath uint32 = 0x1 + setVirtualDiskInfoIdentifier uint32 = 0x2 + setVirtualDiskInfoParentPathWithDepth uint32 = 0x3 + setVirtualDiskInfoPhysicalSectorSize uint32 = 0x4 + setVirtualDiskInfoVirtualDiskID uint32 = 0x5 + setVirtualDiskInfoChangeTrackingState uint32 = 0x6 + setVirtualDiskInfoParentLocator uint32 = 0x7 + + // Flags for getting information about a VHD - these should remain unexported as we provide APIs to directly get/set a particular field. + getVirtualDiskInfoUnspecified uint32 = 0x0 + getVirtualDiskInfoSize uint32 = 0x1 + getVirtualDiskInfoIdentifier uint32 = 0x2 + getVirtualDiskInfoParentLocation uint32 = 0x3 + getVirtualDiskInfoParentIdentifier uint32 = 0x4 + getVirtualDiskInfoParentTimestamp uint32 = 0x5 + getVirtualDiskInfoVirtualStorageType uint32 = 0x6 + getVirtualDiskInfoProviderSubtype uint32 = 0x7 + getVirtualDiskInfoIs4kAligned uint32 = 0x8 + getVirtualDiskInfoPhysicalDisk uint32 = 0x9 + getVirtualDiskInfoVHDPhysicalSectorSize uint32 = 0xA + getVirtualDiskInfoSmallestSafeVirtualSize uint32 = 0xB + getVirtualDiskInfoFragmentation uint32 = 0xC + getVirtualDiskInfoIsLoaded uint32 = 0xD + getVirtualDiskInfoVirtualDiskID uint32 = 0xE + getVirtualDiskInfoChangeTrackingState uint32 = 0xF ) // CreateVhdx is a helper function to create a simple vhdx file at the given path using @@ -375,3 +422,60 @@ func CreateDiffVhd(diffVhdPath, baseVhdPath string, blockSizeInMB uint32) error } return nil } + +// SetVirtualDiskIdentifier sets the virtual disk identifier for the specified virtual disk. +func SetVirtualDiskIdentifier(vhdPath string, identifier guid.GUID) error { + handle, err := OpenVirtualDisk(vhdPath, VirtualDiskAccessNone, OpenVirtualDiskFlagNone) + if err != nil { + return fmt.Errorf("failed to open %s: %w", vhdPath, err) + } + defer syscall.Close(handle) + + info := &virtualDiskInfo{ + version: setVirtualDiskInfoIdentifier, + } + if strings.HasSuffix(vhdPath, ".vhdx") { + // VHDx requires a different version to set disk id + info.version = setVirtualDiskInfoVirtualDiskID + } + + if _, err := binary.Encode(info.data[:], binary.LittleEndian, identifier); err != nil { + return fmt.Errorf("failed to serialize virtual disk identifier: %w", err) + } + + if err := setVirtualDiskInformation(handle, info); err != nil { + return fmt.Errorf("failed to set virtual disk identifier: %w", err) + } + return nil +} + +// GetVirtualDiskIdentifier retrieves the virtual disk identifier for the specified virtual disk. +func GetVirtualDiskIdentifier(vhdPath string) (guid.GUID, error) { + handle, err := OpenVirtualDisk(vhdPath, VirtualDiskAccessNone, OpenVirtualDiskFlagNone) + if err != nil { + return guid.GUID{}, fmt.Errorf("failed to open %s: %w", vhdPath, err) + } + defer syscall.Close(handle) + + info := &virtualDiskInfo{ + version: getVirtualDiskInfoIdentifier, + } + if strings.HasSuffix(vhdPath, ".vhdx") { + // VHDx requires a different version to get disk id + info.version = getVirtualDiskInfoVirtualDiskID + } + + var sizeUsed uint32 + bufferSize := uint32(unsafe.Sizeof(*info)) + if err := getVirtualDiskInformation(handle, &bufferSize, info, &sizeUsed); err != nil { + return guid.GUID{}, fmt.Errorf("failed to get virtual disk identifier: %w", err) + } + + // Parse the response + id := &guid.GUID{} + reader := bytes.NewReader(info.data[:]) + if err := binary.Read(reader, binary.LittleEndian, id); err != nil { + return guid.GUID{}, fmt.Errorf("failed to parse virtual disk identifier: %w", err) + } + return *id, nil +} diff --git a/vendor/github.com/Microsoft/go-winio/vhd/zvhd_windows.go b/vendor/github.com/Microsoft/go-winio/vhd/zvhd_windows.go index 95c0407433f99..e9d202ed5d7e2 100644 --- a/vendor/github.com/Microsoft/go-winio/vhd/zvhd_windows.go +++ b/vendor/github.com/Microsoft/go-winio/vhd/zvhd_windows.go @@ -42,8 +42,10 @@ var ( procAttachVirtualDisk = modvirtdisk.NewProc("AttachVirtualDisk") procCreateVirtualDisk = modvirtdisk.NewProc("CreateVirtualDisk") procDetachVirtualDisk = modvirtdisk.NewProc("DetachVirtualDisk") + procGetVirtualDiskInformation = modvirtdisk.NewProc("GetVirtualDiskInformation") procGetVirtualDiskPhysicalPath = modvirtdisk.NewProc("GetVirtualDiskPhysicalPath") procOpenVirtualDisk = modvirtdisk.NewProc("OpenVirtualDisk") + procSetVirtualDiskInformation = modvirtdisk.NewProc("SetVirtualDiskInformation") ) func attachVirtualDisk(handle syscall.Handle, securityDescriptor *uintptr, attachVirtualDiskFlag uint32, providerSpecificFlags uint32, parameters *AttachVirtualDiskParameters, overlapped *syscall.Overlapped) (win32err error) { @@ -79,6 +81,14 @@ func detachVirtualDisk(handle syscall.Handle, detachVirtualDiskFlags uint32, pro return } +func getVirtualDiskInformation(handle syscall.Handle, bufferSize *uint32, info *virtualDiskInfo, sizeUsed *uint32) (win32err error) { + r0, _, _ := syscall.SyscallN(procGetVirtualDiskInformation.Addr(), uintptr(handle), uintptr(unsafe.Pointer(bufferSize)), uintptr(unsafe.Pointer(info)), uintptr(unsafe.Pointer(sizeUsed))) + if r0 != 0 { + win32err = syscall.Errno(r0) + } + return +} + func getVirtualDiskPhysicalPath(handle syscall.Handle, diskPathSizeInBytes *uint32, buffer *uint16) (win32err error) { r0, _, _ := syscall.SyscallN(procGetVirtualDiskPhysicalPath.Addr(), uintptr(handle), uintptr(unsafe.Pointer(diskPathSizeInBytes)), uintptr(unsafe.Pointer(buffer))) if r0 != 0 { @@ -103,3 +113,11 @@ func _openVirtualDisk(virtualStorageType *VirtualStorageType, path *uint16, virt } return } + +func setVirtualDiskInformation(handle syscall.Handle, info *virtualDiskInfo) (win32err error) { + r0, _, _ := syscall.SyscallN(procSetVirtualDiskInformation.Addr(), uintptr(handle), uintptr(unsafe.Pointer(info))) + if r0 != 0 { + win32err = syscall.Errno(r0) + } + return +} diff --git a/vendor/github.com/Microsoft/hcsshim/Makefile b/vendor/github.com/Microsoft/hcsshim/Makefile index 9a9f5b4014758..0c4642226688b 100644 --- a/vendor/github.com/Microsoft/hcsshim/Makefile +++ b/vendor/github.com/Microsoft/hcsshim/Makefile @@ -1,9 +1,8 @@ include Makefile.bootfiles -GO:=go -GO_FLAGS:=-ldflags "-s -w" # strip Go binaries -CGO_ENABLED:=0 -GOMODVENDOR:= +# C settings + +# enable loading kernel modules in init KMOD:=0 CFLAGS:=-O2 -Wall @@ -11,20 +10,46 @@ LDFLAGS:=-static -s #strip C binaries LDLIBS:= PREPROCESSORFLAGS:= ifeq "$(KMOD)" "1" -LDFLAGS:= -s -LDLIBS:= -lkmod +LDFLAGS:=-s +LDLIBS:=-lkmod PREPROCESSORFLAGS:=-DMODULES=1 endif +# Go settings + +# if Go is from the Microsoft Go fork +MSGO:=0 +# explicitly use vendored modules when building +GOMODVENDOR:= +# Go tags to enable +GO_BUILD_TAGS:= +# additional Go build flags GO_FLAGS_EXTRA:= +# use CGO +CGO_ENABLED:=0 + +GO:=go +GO_FLAGS:=-ldflags "-s -w" # strip Go binaries ifeq "$(GOMODVENDOR)" "1" -GO_FLAGS_EXTRA += -mod=vendor +GO_FLAGS+=-mod=vendor endif -GO_BUILD_TAGS:= ifneq ($(strip $(GO_BUILD_TAGS)),) -GO_FLAGS_EXTRA += -tags="$(GO_BUILD_TAGS)" +GO_FLAGS+=-tags="$(GO_BUILD_TAGS)" +endif +GO_BUILD_ENV:=CGO_ENABLED=$(CGO_ENABLED) +# starting with ms-go1.25, systemcrypto (for FIPS compliance) is enabled by default, which +# requires CGo. +# disable it for non-CGo builds. +# +# https://github.com/microsoft/go/blob/microsoft/main/eng/doc/MigrationGuide.md#cgo-is-not-enabled +# https://github.com/microsoft/go/blob/microsoft/main/eng/doc/MigrationGuide.md#disabling-systemcrypto +ifeq "$(MSGO)" "1" +ifneq "$(CGO_ENABLED)" "1" +# MS_GO_NOSYSTEMCRYPTO only works for >=ms-go1.25.2 +GO_BUILD_ENV+=MS_GO_NOSYSTEMCRYPTO=1 GOEXPERIMENT=nosystemcrypto endif -GO_BUILD:=CGO_ENABLED=$(CGO_ENABLED) $(GO) build $(GO_FLAGS) $(GO_FLAGS_EXTRA) +endif +GO_BUILD:= $(GO_BUILD_ENV) $(GO) build $(GO_FLAGS) $(GO_FLAGS_EXTRA) SRCROOT=$(dir $(abspath $(firstword $(MAKEFILE_LIST)))) # additional directories to search for rule prerequisites and targets @@ -81,10 +106,17 @@ out/delta.tar.gz: bin/init bin/vsockexec bin/cmd/gcs bin/cmd/gcstools bin/cmd/ho tar -zcf $@ -C rootfs . rm -rf rootfs -bin/cmd/gcs bin/cmd/gcstools bin/cmd/hooks/wait-paths bin/cmd/tar2ext4 bin/internal/tools/snp-report: +# Use force target to always call `go build` per make invocation and rely on Go's build cache +# to decide if binaries should be (re)built. +# Note: don't use `.PHONY` since the targets are actual files. +# +# www.gnu.org/software/make/manual/html_node/Force-Targets.html +bin/cmd/gcs bin/cmd/gcstools bin/cmd/hooks/wait-paths bin/cmd/tar2ext4 bin/internal/tools/snp-report: FORCE @mkdir -p $(dir $@) GOOS=linux $(GO_BUILD) -o $@ $(SRCROOT)/$(@:bin/%=%) +FORCE: + bin/vsockexec: vsockexec/vsockexec.o vsockexec/vsock.o @mkdir -p bin $(CC) $(LDFLAGS) -o $@ $^ diff --git a/vendor/github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/options/runhcs.pb.go b/vendor/github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/options/runhcs.pb.go index ce28dda6f5fba..57706556eef3c 100644 --- a/vendor/github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/options/runhcs.pb.go +++ b/vendor/github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/options/runhcs.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 +// protoc-gen-go v1.36.10 // protoc v5.26.0 // source: github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/options/runhcs.proto diff --git a/vendor/github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/stats/stats.pb.go b/vendor/github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/stats/stats.pb.go index 9545a679ec418..37c85abc81d18 100644 --- a/vendor/github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/stats/stats.pb.go +++ b/vendor/github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/stats/stats.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 +// protoc-gen-go v1.36.10 // protoc v5.26.0 // source: github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/stats/stats.proto diff --git a/vendor/github.com/Microsoft/hcsshim/computestorage/storage.go b/vendor/github.com/Microsoft/hcsshim/computestorage/storage.go index 5af931f2f485f..cf3c3b296d3e1 100644 --- a/vendor/github.com/Microsoft/hcsshim/computestorage/storage.go +++ b/vendor/github.com/Microsoft/hcsshim/computestorage/storage.go @@ -7,7 +7,7 @@ import ( hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2" ) -//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go storage.go +//go:generate go tool github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go storage.go //sys hcsImportLayer(layerPath string, sourceFolderPath string, layerData string) (hr error) = computestorage.HcsImportLayer? //sys hcsExportLayer(layerPath string, exportFolderPath string, layerData string, options string) (hr error) = computestorage.HcsExportLayer? diff --git a/vendor/github.com/Microsoft/hcsshim/hcn/hcn.go b/vendor/github.com/Microsoft/hcsshim/hcn/hcn.go index 671da24443d27..ecef66caf6cbf 100644 --- a/vendor/github.com/Microsoft/hcsshim/hcn/hcn.go +++ b/vendor/github.com/Microsoft/hcsshim/hcn/hcn.go @@ -10,7 +10,7 @@ import ( "github.com/Microsoft/go-winio/pkg/guid" ) -//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go hcn.go +//go:generate go tool github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go hcn.go /// HNS V1 API diff --git a/vendor/github.com/Microsoft/hcsshim/hcn/hcnnetwork.go b/vendor/github.com/Microsoft/hcsshim/hcn/hcnnetwork.go index c31920ced9645..c24d2abc848bd 100644 --- a/vendor/github.com/Microsoft/hcsshim/hcn/hcnnetwork.go +++ b/vendor/github.com/Microsoft/hcsshim/hcn/hcnnetwork.go @@ -11,6 +11,15 @@ import ( "github.com/sirupsen/logrus" ) +// SubnetFlags represents the flags that can be set on a subnet +type SubnetFlags uint32 + +// SubnetFlags constants (based on HNS API documentation) +const ( + SubnetFlagsNone SubnetFlags = 0 + SubnetFlagsDoNotReserveGatewayAddress SubnetFlags = 1 // This flag is needed to support scenario GatewayAddress == ManagementIP +) + // Route is associated with a subnet. type Route struct { NextHop string `json:",omitempty"` @@ -23,6 +32,7 @@ type Subnet struct { IpAddressPrefix string `json:",omitempty"` Policies []json.RawMessage `json:",omitempty"` Routes []Route `json:",omitempty"` + Flags SubnetFlags `json:",omitempty"` } // Ipam (Internet Protocol Address Management) is associated with a network diff --git a/vendor/github.com/Microsoft/hcsshim/hcsshim.go b/vendor/github.com/Microsoft/hcsshim/hcsshim.go index 13f80e4a81c95..4557c7cf5f68e 100644 --- a/vendor/github.com/Microsoft/hcsshim/hcsshim.go +++ b/vendor/github.com/Microsoft/hcsshim/hcsshim.go @@ -11,7 +11,7 @@ import ( "github.com/Microsoft/hcsshim/internal/hcserror" ) -//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go hcsshim.go +//go:generate go tool github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go hcsshim.go //sys SetCurrentThreadCompartmentId(compartmentId uint32) (hr error) = iphlpapi.SetCurrentThreadCompartmentId diff --git a/vendor/github.com/Microsoft/hcsshim/internal/hcs/schema1/schema1.go b/vendor/github.com/Microsoft/hcsshim/internal/hcs/schema1/schema1.go index d1f219cfad006..e0a3010f8d750 100644 --- a/vendor/github.com/Microsoft/hcsshim/internal/hcs/schema1/schema1.go +++ b/vendor/github.com/Microsoft/hcsshim/internal/hcs/schema1/schema1.go @@ -221,6 +221,7 @@ type GuestDefinedCapabilities struct { DumpStacksSupported bool `json:",omitempty"` DeleteContainerStateSupported bool `json:",omitempty"` UpdateContainerSupported bool `json:",omitempty"` + LogForwardingSupported bool `json:",omitempty"` } // GuestConnectionInfo is the structure of an iterm return by a GuestConnection call on a utility VM diff --git a/vendor/github.com/Microsoft/hcsshim/internal/hcs/system.go b/vendor/github.com/Microsoft/hcsshim/internal/hcs/system.go index b1597466f698a..823e27b0b7864 100644 --- a/vendor/github.com/Microsoft/hcsshim/internal/hcs/system.go +++ b/vendor/github.com/Microsoft/hcsshim/internal/hcs/system.go @@ -37,6 +37,7 @@ type System struct { exitError error os, typ, owner string startTime time.Time + stopTime time.Time } var _ cow.Container = &System{} @@ -292,6 +293,7 @@ func (computeSystem *System) waitBackground() { } computeSystem.closedWaitOnce.Do(func() { computeSystem.waitError = err + computeSystem.stopTime = time.Now() close(computeSystem.waitBlock) }) oc.SetSpanStatus(span, err) @@ -871,3 +873,11 @@ func (computeSystem *System) Modify(ctx context.Context, config interface{}) err return nil } + +func (computeSystem *System) StoppedTime() time.Time { + return computeSystem.stopTime +} + +func (computeSystem *System) StartedTime() time.Time { + return computeSystem.startTime +} diff --git a/vendor/github.com/Microsoft/hcsshim/internal/hns/hns.go b/vendor/github.com/Microsoft/hcsshim/internal/hns/hns.go index ec4c907d1f5d4..8c9aa15c04f5f 100644 --- a/vendor/github.com/Microsoft/hcsshim/internal/hns/hns.go +++ b/vendor/github.com/Microsoft/hcsshim/internal/hns/hns.go @@ -2,7 +2,7 @@ package hns import "fmt" -//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go hns.go +//go:generate go tool github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go hns.go //sys _hnsCall(method string, path string, object string, response **uint16) (hr error) = vmcompute.HNSCall? diff --git a/vendor/github.com/Microsoft/hcsshim/internal/interop/interop.go b/vendor/github.com/Microsoft/hcsshim/internal/interop/interop.go index a564696568236..0f8f676c58a7a 100644 --- a/vendor/github.com/Microsoft/hcsshim/internal/interop/interop.go +++ b/vendor/github.com/Microsoft/hcsshim/internal/interop/interop.go @@ -7,7 +7,7 @@ import ( "unsafe" ) -//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go interop.go +//go:generate go tool github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go interop.go //sys coTaskMemFree(buffer unsafe.Pointer) = api_ms_win_core_com_l1_1_0.CoTaskMemFree diff --git a/vendor/github.com/Microsoft/hcsshim/internal/jobobject/jobobject.go b/vendor/github.com/Microsoft/hcsshim/internal/jobobject/jobobject.go index 3afa240aa69cd..5f062e5c5e49c 100644 --- a/vendor/github.com/Microsoft/hcsshim/internal/jobobject/jobobject.go +++ b/vendor/github.com/Microsoft/hcsshim/internal/jobobject/jobobject.go @@ -32,6 +32,7 @@ type JobObject struct { type JobLimits struct { CPULimit uint32 CPUWeight uint32 + CPUAffinity uint64 MemoryLimitInBytes uint64 MaxIOPS int64 MaxBandwidth int64 diff --git a/vendor/github.com/Microsoft/hcsshim/internal/jobobject/limits.go b/vendor/github.com/Microsoft/hcsshim/internal/jobobject/limits.go index fedf8add6c54d..5bb20df00f3dd 100644 --- a/vendor/github.com/Microsoft/hcsshim/internal/jobobject/limits.go +++ b/vendor/github.com/Microsoft/hcsshim/internal/jobobject/limits.go @@ -38,6 +38,12 @@ func (job *JobObject) SetResourceLimits(limits *JobLimits) error { } } + if limits.CPUAffinity != 0 { + if err := job.SetCPUAffinity(limits.CPUAffinity); err != nil { + return fmt.Errorf("failed to set job object cpu affinity: %w", err) + } + } + if limits.MaxBandwidth != 0 || limits.MaxIOPS != 0 { if err := job.SetIOLimit(limits.MaxBandwidth, limits.MaxIOPS); err != nil { return fmt.Errorf("failed to set io limit on job object: %w", err) diff --git a/vendor/github.com/Microsoft/hcsshim/internal/log/scrub.go b/vendor/github.com/Microsoft/hcsshim/internal/log/scrub.go index 5346f9b7cf1bc..83ee97f85315b 100644 --- a/vendor/github.com/Microsoft/hcsshim/internal/log/scrub.go +++ b/vendor/github.com/Microsoft/hcsshim/internal/log/scrub.go @@ -11,7 +11,7 @@ import ( // This package scrubs objects of potentially sensitive information to pass to logging -type genMap = map[string]interface{} +type genMap = map[string]any type scrubberFunc func(genMap) error const _scrubbedReplacement = "" @@ -20,7 +20,11 @@ var ( ErrUnknownType = errors.New("encoded object is of unknown type") // case sensitive keywords, so "env" is not a substring on "Environment" - _scrubKeywords = [][]byte{[]byte("env"), []byte("Environment")} + _scrubKeywords = [][]byte{ + []byte("env"), + []byte("Environment"), + []byte("annotations"), + } _scrub atomic.Bool ) @@ -32,7 +36,7 @@ func SetScrubbing(enable bool) { _scrub.Store(enable) } func IsScrubbingEnabled() bool { return _scrub.Load() } // ScrubProcessParameters scrubs HCS Create Process requests with config parameters of -// type internal/hcs/schema2.ScrubProcessParameters (aka hcsshema.ScrubProcessParameters) +// type [hcsschema.ProcessParameters]. func ScrubProcessParameters(s string) (string, error) { // todo: deal with v1 ProcessConfig b := []byte(s) @@ -81,19 +85,34 @@ func scrubBridgeCreate(m genMap) error { func scrubLinuxHostedSystem(m genMap) error { if m, ok := index(m, "OciSpecification"); ok { //nolint:govet // shadow - if _, ok := m["annotations"]; ok { - m["annotations"] = map[string]string{_scrubbedReplacement: _scrubbedReplacement} - } - if m, ok := index(m, "process"); ok { //nolint:govet // shadow - if _, ok := m["env"]; ok { - m["env"] = []string{_scrubbedReplacement} - return nil - } - } + return scrubOCISpec(m) } return ErrUnknownType } +// ScrubOCISpec scrubs a JSON encoded [github.com/opencontainers/runtime-spec/specs-go.Spec]. +// +// Ideally the spec struct would be scrubbed directly, but that would need a deep clone to +// prevent modifying the original, and, absent one implemented on the Spec +// (e.g., [google.golang.org/protobuf/proto.CloneOf]), unmarshalling a marshalled struct +// functions as a deep clone. +func ScrubOCISpec(b []byte) ([]byte, error) { + return scrubBytes(b, scrubOCISpec) +} + +func scrubOCISpec(m genMap) error { + if _, ok := m["annotations"]; ok { + m["annotations"] = map[string]string{_scrubbedReplacement: _scrubbedReplacement} + } + if m, ok := index(m, "process"); ok { //nolint:govet // shadow + if _, ok := m["env"]; ok { + m["env"] = []string{_scrubbedReplacement} + } + } + + return nil +} + // ScrubBridgeExecProcess scrubs requests sent over the bridge of type // internal/gcs/protocol.containerExecuteProcess func ScrubBridgeExecProcess(b []byte) ([]byte, error) { diff --git a/vendor/github.com/Microsoft/hcsshim/internal/logfields/fields.go b/vendor/github.com/Microsoft/hcsshim/internal/logfields/fields.go index cceb3e2d1870a..dac5a708e5f69 100644 --- a/vendor/github.com/Microsoft/hcsshim/internal/logfields/fields.go +++ b/vendor/github.com/Microsoft/hcsshim/internal/logfields/fields.go @@ -8,12 +8,12 @@ const ( Operation = "operation" ID = "id" - SandboxID = "sid" ContainerID = "cid" ExecID = "eid" ProcessID = "pid" TaskID = "tid" UVMID = "uvm-id" + SandboxID = "sandbox-id" // networking and IO @@ -50,6 +50,40 @@ const ( Uint32 = "uint32" Uint64 = "uint64" + // task / process lifecycle + + Bundle = "bundle" + Terminal = "terminal" + Stdin = "stdin" + Stdout = "stdout" + Stderr = "stderr" + Checkpoint = "checkpoint" + ParentCheckpoint = "parent-checkpoint" + Status = "status" + ExitStatus = "exit-status" + ExitedAt = "exited-at" + Signal = "signal" + All = "all" + Width = "width" + Height = "height" + Version = "version" + ShimPid = "shim-pid" + TaskPid = "task-pid" + + // sandbox + + NetNsPath = "net-ns-path" + Verbose = "verbose" + + // shimdiag + + Args = "args" + Workdir = "workdir" + HostPath = "host-path" + UVMPath = "uvm-path" + ReadOnly = "readonly" + Execs = "execs" + // runhcs VMShimOperation = "vmshim-op" diff --git a/vendor/github.com/Microsoft/hcsshim/internal/oc/exporter.go b/vendor/github.com/Microsoft/hcsshim/internal/oc/exporter.go index 66fc9f4b27be3..ffa02f1c1f972 100644 --- a/vendor/github.com/Microsoft/hcsshim/internal/oc/exporter.go +++ b/vendor/github.com/Microsoft/hcsshim/internal/oc/exporter.go @@ -9,7 +9,7 @@ import ( "github.com/Microsoft/hcsshim/internal/logfields" ) -const spanMessage = "Span" +const spanMessage = "Span %s" var _errorCodeKey = logrus.ErrorKey + "Code" @@ -82,5 +82,5 @@ func (le *LogrusExporter) ExportSpan(s *trace.SpanData) { entry.Data = data entry.Time = s.StartTime - entry.Log(level, spanMessage) + entry.Logf(level, spanMessage, s.Name) } diff --git a/vendor/github.com/Microsoft/hcsshim/internal/protocol/guestrequest/types.go b/vendor/github.com/Microsoft/hcsshim/internal/protocol/guestrequest/types.go index aa27e5badcf7b..dc35ceaf8a9b9 100644 --- a/vendor/github.com/Microsoft/hcsshim/internal/protocol/guestrequest/types.go +++ b/vendor/github.com/Microsoft/hcsshim/internal/protocol/guestrequest/types.go @@ -74,3 +74,17 @@ const ( STDErrHandle STDIOHandle = "StdErr" AllHandles STDIOHandle = "All" ) + +type LogForwardServiceRPCRequest struct { + RPCType RPCType `json:"RPCType,omitempty"` // "LogForwardService" + Settings string `json:"Settings,omitempty"` +} + +type RPCType string + +const ( + // LogForwardServiceRPC is the RPC type for the log forward service. + RPCModifyServiceSettings RPCType = "ModifyServiceSettings" + RPCStartLogForwarding RPCType = "StartLogForwarding" + RPCStopLogForwarding RPCType = "StopLogForwarding" +) diff --git a/vendor/github.com/Microsoft/hcsshim/internal/regstate/regstate.go b/vendor/github.com/Microsoft/hcsshim/internal/regstate/regstate.go index 1ccce1944cb0e..f10b367ad46a6 100644 --- a/vendor/github.com/Microsoft/hcsshim/internal/regstate/regstate.go +++ b/vendor/github.com/Microsoft/hcsshim/internal/regstate/regstate.go @@ -16,7 +16,7 @@ import ( "golang.org/x/sys/windows/registry" ) -//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go regstate.go +//go:generate go tool github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go regstate.go //sys regCreateKeyEx(key syscall.Handle, subkey *uint16, reserved uint32, class *uint16, options uint32, desired uint32, sa *syscall.SecurityAttributes, result *syscall.Handle, disposition *uint32) (regerrno error) = advapi32.RegCreateKeyExW diff --git a/vendor/github.com/Microsoft/hcsshim/internal/security/syscall_windows.go b/vendor/github.com/Microsoft/hcsshim/internal/security/syscall_windows.go index 71326e4e46fec..81ec7df43a694 100644 --- a/vendor/github.com/Microsoft/hcsshim/internal/security/syscall_windows.go +++ b/vendor/github.com/Microsoft/hcsshim/internal/security/syscall_windows.go @@ -1,6 +1,6 @@ package security -//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go syscall_windows.go +//go:generate go tool github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go syscall_windows.go //sys getSecurityInfo(handle syscall.Handle, objectType uint32, si uint32, ppsidOwner **uintptr, ppsidGroup **uintptr, ppDacl *uintptr, ppSacl *uintptr, ppSecurityDescriptor *uintptr) (win32err error) = advapi32.GetSecurityInfo //sys setSecurityInfo(handle syscall.Handle, objectType uint32, si uint32, psidOwner uintptr, psidGroup uintptr, pDacl uintptr, pSacl uintptr) (win32err error) = advapi32.SetSecurityInfo diff --git a/vendor/github.com/Microsoft/hcsshim/internal/timeout/timeout.go b/vendor/github.com/Microsoft/hcsshim/internal/timeout/timeout.go index eaf39fa513274..1c6e5fbb0d69b 100644 --- a/vendor/github.com/Microsoft/hcsshim/internal/timeout/timeout.go +++ b/vendor/github.com/Microsoft/hcsshim/internal/timeout/timeout.go @@ -47,6 +47,11 @@ var ( // TestDRetryLoop is the timeout for testd retry loop when onlining a SCSI disk in LCOW TestDRetryLoop = defaultTimeoutTestdRetry + + // This timeout is used for GCS connection after uvm boot as well as the entropy + // and log connection setup during uvm boot. This is different than the + // SystemStart timeout defined above. + GCSConnectionTimeout = 2 * time.Minute ) func init() { @@ -60,6 +65,7 @@ func init() { ExternalCommandToStart = durationFromEnvironment("HCSSHIM_TIMEOUT_EXTERNALCOMMANDSTART", ExternalCommandToStart) ExternalCommandToComplete = durationFromEnvironment("HCSSHIM_TIMEOUT_EXTERNALCOMMANDCOMPLETE", ExternalCommandToComplete) TestDRetryLoop = durationFromEnvironment("HCSSHIM_TIMEOUT_TESTDRETRYLOOP", TestDRetryLoop) + GCSConnectionTimeout = durationFromEnvironment("HCSSHIM_TIMEOUT_GCSCONNECTION", GCSConnectionTimeout) } func durationFromEnvironment(env string, defaultValue time.Duration) time.Duration { diff --git a/vendor/github.com/Microsoft/hcsshim/internal/vmcompute/vmcompute.go b/vendor/github.com/Microsoft/hcsshim/internal/vmcompute/vmcompute.go index 965086a580449..5819dc6df3a2f 100644 --- a/vendor/github.com/Microsoft/hcsshim/internal/vmcompute/vmcompute.go +++ b/vendor/github.com/Microsoft/hcsshim/internal/vmcompute/vmcompute.go @@ -17,7 +17,7 @@ import ( "github.com/Microsoft/hcsshim/internal/timeout" ) -//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go vmcompute.go +//go:generate go tool github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go vmcompute.go //sys hcsEnumerateComputeSystems(query string, computeSystems **uint16, result **uint16) (hr error) = vmcompute.HcsEnumerateComputeSystems? //sys hcsCreateComputeSystem(id string, configuration string, identity syscall.Handle, computeSystem *HcsSystem, result **uint16) (hr error) = vmcompute.HcsCreateComputeSystem? diff --git a/vendor/github.com/Microsoft/hcsshim/internal/wclayer/cim/block_cim_writer.go b/vendor/github.com/Microsoft/hcsshim/internal/wclayer/cim/block_cim_writer.go index 334e391366909..16f7c9552dfe1 100644 --- a/vendor/github.com/Microsoft/hcsshim/internal/wclayer/cim/block_cim_writer.go +++ b/vendor/github.com/Microsoft/hcsshim/internal/wclayer/cim/block_cim_writer.go @@ -100,13 +100,16 @@ func (cw *BlockCIMLayerWriter) Add(name string, fileInfo *winio.FileBasicInfo, f if name == wclayer.UtilityVMPath && len(cw.parentLayers) > 0 { // If there are UtilityVM files in non base layers, we will have to merge // those files with the parent layer UtilityVM files - either during image - // pull or at runtime (i.e when starting the UVM). In order to merge at image pull time, we will have - // to read parent layer block CIMs and copy all the UtilityVM files from - // those CIMs into this block CIM one by one i.e effectively merge all - // parent layer UtilityVM files in this layer. Or we will need to be able - // to boot the UtilityVM with merged block CIMs. None of these options are - // implemented yet so error out if we see that. - return fmt.Errorf("UtilityVM files in non base layers is not supported for block CIMs") + // pull or at runtime (i.e when starting the UVM). In order to merge at + // image pull time, we will have to read parent layer block CIMs and copy + // all the UtilityVM files from those CIMs into this block CIM one by one + // i.e effectively merge all parent layer UtilityVM files in this + // layer. Or we will need to be able to boot the UtilityVM with merged + // block CIMs. None of these options are implemented yet so log a + // warning. However, this shouldn't cause issues with most of the standard + // use cases because usually the pod is a nanoserver image and that is + // always a single layer. + log.G(cw.ctx).Warn("UtilityVM files in non base layers is not supported for block CIMs") } return cw.cimLayerWriter.Add(name, fileInfo, fileSize, securityDescriptor, extendedAttributes, reparseData) } diff --git a/vendor/github.com/Microsoft/hcsshim/internal/wclayer/cim/mount.go b/vendor/github.com/Microsoft/hcsshim/internal/wclayer/cim/mount.go index 56d0d0ac7daaf..89fa13ccdccc0 100644 --- a/vendor/github.com/Microsoft/hcsshim/internal/wclayer/cim/mount.go +++ b/vendor/github.com/Microsoft/hcsshim/internal/wclayer/cim/mount.go @@ -108,6 +108,7 @@ func MergeMountBlockCIMLayer(ctx context.Context, mergedLayer *cimfs.BlockCIM, p if err != nil { return "", fmt.Errorf("generated cim mount GUID: %w", err) } + return cimfs.MountMergedBlockCIMs(mergedLayer, parentLayers, mountFlags, volumeGUID) } diff --git a/vendor/github.com/Microsoft/hcsshim/internal/wclayer/wclayer.go b/vendor/github.com/Microsoft/hcsshim/internal/wclayer/wclayer.go index 39682b8171916..8cc661cbfe5ed 100644 --- a/vendor/github.com/Microsoft/hcsshim/internal/wclayer/wclayer.go +++ b/vendor/github.com/Microsoft/hcsshim/internal/wclayer/wclayer.go @@ -4,7 +4,7 @@ package wclayer import "github.com/Microsoft/go-winio/pkg/guid" -//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go wclayer.go +//go:generate go tool github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go wclayer.go //sys activateLayer(info *driverInfo, id string) (hr error) = vmcompute.ActivateLayer? //sys copyLayer(info *driverInfo, srcId string, dstId string, descriptors []WC_LAYER_DESCRIPTOR) (hr error) = vmcompute.CopyLayer? diff --git a/vendor/github.com/Microsoft/hcsshim/internal/winapi/amdsnp.go b/vendor/github.com/Microsoft/hcsshim/internal/winapi/amdsnp.go new file mode 100644 index 0000000000000..5e0f9cd2c58d5 --- /dev/null +++ b/vendor/github.com/Microsoft/hcsshim/internal/winapi/amdsnp.go @@ -0,0 +1,11 @@ +//go:build windows + +package winapi + +type SNPPSPGuestRequestResult struct { + DriverStatus uint32 + PspStatus uint64 +} + +//sys SnpPspIsSnpMode(snpMode *uint8) (ret uint32, err error) [failretval>0] = amdsnppspapi.SnpPspIsSnpMode? +//sys SnpPspFetchAttestationReport(reportData *uint8, guestRequestResult *SNPPSPGuestRequestResult, report *uint8) (ret uint32, err error) [failretval>0] = amdsnppspapi.SnpPspFetchAttestationReport? diff --git a/vendor/github.com/Microsoft/hcsshim/internal/winapi/cimfs.go b/vendor/github.com/Microsoft/hcsshim/internal/winapi/cimfs.go index cc3d2541203e4..3e3c15412f355 100644 --- a/vendor/github.com/Microsoft/hcsshim/internal/winapi/cimfs.go +++ b/vendor/github.com/Microsoft/hcsshim/internal/winapi/cimfs.go @@ -3,59 +3,161 @@ package winapi import ( - "unsafe" - "github.com/Microsoft/go-winio/pkg/guid" - "golang.org/x/sys/windows" + "github.com/sirupsen/logrus" + + "github.com/Microsoft/hcsshim/internal/winapi/cimfs" + "github.com/Microsoft/hcsshim/internal/winapi/cimwriter" + "github.com/Microsoft/hcsshim/internal/winapi/types" ) -type g = guid.GUID -type FsHandle uintptr -type StreamHandle uintptr +// LogCimDLLSupport logs which DLL is being used for CIM write operations. +func LogCimDLLSupport() { + if cimwriter.Supported() { + logrus.Info("using cimwriter.dll for CIM write operations") + } else if cimfs.Supported() { + logrus.Info("using cimfs.dll for CIM write operations") + } else { + logrus.Warn("no CIM DLL available for write operations") + } +} + +// pickSupported makes sure we use appropriate syscalls depending on which DLLs are present. +func pickSupported[F any](cimWriterFunc, cimfsFunc F) F { + if cimwriter.Supported() { + return cimWriterFunc + } + return cimfsFunc +} + +func CimMountImage(imagePath string, fsName string, flags uint32, volumeID *guid.GUID) error { + return cimfs.CimMountImage(imagePath, fsName, flags, volumeID) +} + +func CimDismountImage(volumeID *guid.GUID) error { + return cimfs.CimDismountImage(volumeID) +} + +func CimCreateImage(imagePath string, oldFSName *uint16, newFSName *uint16, cimFSHandle *types.FsHandle) error { + return pickSupported( + cimwriter.CimCreateImage, + cimfs.CimCreateImage, + )(imagePath, oldFSName, newFSName, cimFSHandle) +} + +func CimCreateImage2(imagePath string, flags uint32, oldFSName *uint16, newFSName *uint16, cimFSHandle *types.FsHandle) error { + return pickSupported( + cimwriter.CimCreateImage2, + cimfs.CimCreateImage2, + )(imagePath, flags, oldFSName, newFSName, cimFSHandle) +} + +func CimCloseImage(cimFSHandle types.FsHandle) error { + return pickSupported( + cimwriter.CimCloseImage, + cimfs.CimCloseImage, + )(cimFSHandle) +} + +func CimCommitImage(cimFSHandle types.FsHandle) error { + return pickSupported( + cimwriter.CimCommitImage, + cimfs.CimCommitImage, + )(cimFSHandle) +} + +func CimCreateFile(cimFSHandle types.FsHandle, path string, file *types.CimFsFileMetadata, cimStreamHandle *types.StreamHandle) error { + return pickSupported( + cimwriter.CimCreateFile, + cimfs.CimCreateFile, + )(cimFSHandle, path, file, cimStreamHandle) +} + +func CimCloseStream(cimStreamHandle types.StreamHandle) error { + return pickSupported( + cimwriter.CimCloseStream, + cimfs.CimCloseStream, + )(cimStreamHandle) +} + +func CimWriteStream(cimStreamHandle types.StreamHandle, buffer uintptr, bufferSize uint32) error { + return pickSupported( + cimwriter.CimWriteStream, + cimfs.CimWriteStream, + )(cimStreamHandle, buffer, bufferSize) +} + +func CimDeletePath(cimFSHandle types.FsHandle, path string) error { + return pickSupported( + cimwriter.CimDeletePath, + cimfs.CimDeletePath, + )(cimFSHandle, path) +} + +func CimCreateHardLink(cimFSHandle types.FsHandle, newPath string, oldPath string) error { + return pickSupported( + cimwriter.CimCreateHardLink, + cimfs.CimCreateHardLink, + )(cimFSHandle, newPath, oldPath) +} -type CimFsFileMetadata struct { - Attributes uint32 - FileSize int64 +func CimCreateAlternateStream(cimFSHandle types.FsHandle, path string, size uint64, cimStreamHandle *types.StreamHandle) error { + return pickSupported( + cimwriter.CimCreateAlternateStream, + cimfs.CimCreateAlternateStream, + )(cimFSHandle, path, size, cimStreamHandle) +} - CreationTime windows.Filetime - LastWriteTime windows.Filetime - ChangeTime windows.Filetime - LastAccessTime windows.Filetime +func CimAddFsToMergedImage(cimFSHandle types.FsHandle, path string) error { + return pickSupported( + cimwriter.CimAddFsToMergedImage, + cimfs.CimAddFsToMergedImage, + )(cimFSHandle, path) +} - SecurityDescriptorBuffer unsafe.Pointer - SecurityDescriptorSize uint32 +func CimAddFsToMergedImage2(cimFSHandle types.FsHandle, path string, flags uint32) error { + return pickSupported( + cimwriter.CimAddFsToMergedImage2, + cimfs.CimAddFsToMergedImage2, + )(cimFSHandle, path, flags) +} - ReparseDataBuffer unsafe.Pointer - ReparseDataSize uint32 +func CimMergeMountImage(numCimPaths uint32, backingImagePaths *types.CimFsImagePath, flags uint32, volumeID *guid.GUID) error { + return cimfs.CimMergeMountImage(numCimPaths, backingImagePaths, flags, volumeID) +} - ExtendedAttributes unsafe.Pointer - EACount uint32 +func CimTombstoneFile(cimFSHandle types.FsHandle, path string) error { + return pickSupported( + cimwriter.CimTombstoneFile, + cimfs.CimTombstoneFile, + )(cimFSHandle, path) } -type CimFsImagePath struct { - ImageDir *uint16 - ImageName *uint16 +func CimCreateMergeLink(cimFSHandle types.FsHandle, newPath string, oldPath string) (hr error) { + return pickSupported( + cimwriter.CimCreateMergeLink, + cimfs.CimCreateMergeLink, + )(cimFSHandle, newPath, oldPath) } -//sys CimMountImage(imagePath string, fsName string, flags uint32, volumeID *g) (hr error) = cimfs.CimMountImage? -//sys CimDismountImage(volumeID *g) (hr error) = cimfs.CimDismountImage? +func CimSealImage(blockCimPath string, hashSize *uint64, fixedHeaderSize *uint64, hash *byte) (hr error) { + return pickSupported( + cimwriter.CimSealImage, + cimfs.CimSealImage, + )(blockCimPath, hashSize, fixedHeaderSize, hash) +} -//sys CimCreateImage(imagePath string, oldFSName *uint16, newFSName *uint16, cimFSHandle *FsHandle) (hr error) = cimfs.CimCreateImage? -//sys CimCreateImage2(imagePath string, flags uint32, oldFSName *uint16, newFSName *uint16, cimFSHandle *FsHandle) (hr error) = cimfs.CimCreateImage2? -//sys CimCloseImage(cimFSHandle FsHandle) = cimfs.CimCloseImage? -//sys CimCommitImage(cimFSHandle FsHandle) (hr error) = cimfs.CimCommitImage? +func CimGetVerificationInformation(blockCimPath string, isSealed *uint32, hashSize *uint64, signatureSize *uint64, fixedHeaderSize *uint64, hash *byte, signature *byte) (hr error) { + return pickSupported( + cimwriter.CimGetVerificationInformation, + cimfs.CimGetVerificationInformation, + )(blockCimPath, isSealed, hashSize, signatureSize, fixedHeaderSize, hash, signature) +} + +func CimMountVerifiedImage(imagePath string, fsName string, flags uint32, volumeID *guid.GUID, hashSize uint16, hash *byte) error { + return cimfs.CimMountVerifiedImage(imagePath, fsName, flags, volumeID, hashSize, hash) +} -//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? -//sys CimAddFsToMergedImage(cimFSHandle FsHandle, path string) (hr error) = cimfs.CimAddFsToMergedImage? -//sys CimAddFsToMergedImage2(cimFSHandle FsHandle, path string, flags uint32) (hr error) = cimfs.CimAddFsToMergedImage2? -//sys CimMergeMountImage(numCimPaths uint32, backingImagePaths *CimFsImagePath, flags uint32, volumeID *g) (hr error) = cimfs.CimMergeMountImage? -//sys CimTombstoneFile(cimFSHandle FsHandle, path string) (hr error) = cimfs.CimTombstoneFile? -//sys CimCreateMergeLink(cimFSHandle FsHandle, newPath string, oldPath string) (hr error) = cimfs.CimCreateMergeLink? -//sys CimSealImage(blockCimPath string, hashSize *uint64, fixedHeaderSize *uint64, hash *byte) (hr error) = cimfs.CimSealImage? -//sys CimGetVerificationInformation(blockCimPath string, isSealed *uint32, hashSize *uint64, signatureSize *uint64, fixedHeaderSize *uint64, hash *byte, signature *byte) (hr error) = cimfs.CimGetVerificationInformation? -//sys CimMountVerifiedImage(imagePath string, fsName string, flags uint32, volumeID *g, hashSize uint16, hash *byte) (hr error) = cimfs.CimMountVerifiedImage? +func CimMergeMountVerifiedImage(numCimPaths uint32, backingImagePaths *types.CimFsImagePath, flags uint32, volumeID *guid.GUID, hashSize uint16, hash *byte) error { + return cimfs.CimMergeMountVerifiedImage(numCimPaths, backingImagePaths, flags, volumeID, hashSize, hash) +} diff --git a/vendor/github.com/Microsoft/hcsshim/internal/winapi/cimfs/cimfs.go b/vendor/github.com/Microsoft/hcsshim/internal/winapi/cimfs/cimfs.go new file mode 100644 index 0000000000000..0317c73a78475 --- /dev/null +++ b/vendor/github.com/Microsoft/hcsshim/internal/winapi/cimfs/cimfs.go @@ -0,0 +1,62 @@ +//go:build windows + +package cimfs + +import ( + "sync" + + "github.com/Microsoft/go-winio/pkg/guid" + "github.com/sirupsen/logrus" + "golang.org/x/sys/windows" + + "github.com/Microsoft/hcsshim/internal/winapi/types" +) + +// Type aliases + +type GUID = guid.GUID +type FsHandle = types.FsHandle +type StreamHandle = types.StreamHandle +type FileMetadata = types.CimFsFileMetadata +type ImagePath = types.CimFsImagePath + +//sys CimMountImage(imagePath string, fsName string, flags uint32, volumeID *GUID) (hr error) = cimfs.CimMountImage? +//sys CimDismountImage(volumeID *GUID) (hr error) = cimfs.CimDismountImage? + +//sys CimCreateImage(imagePath string, oldFSName *uint16, newFSName *uint16, cimFSHandle *FsHandle) (hr error) = cimfs.CimCreateImage? +//sys CimCreateImage2(imagePath string, flags uint32, oldFSName *uint16, newFSName *uint16, cimFSHandle *FsHandle) (hr error) = cimfs.CimCreateImage2? +//sys CimCloseImage(cimFSHandle FsHandle) = cimfs.CimCloseImage? +//sys CimCommitImage(cimFSHandle FsHandle) (hr error) = cimfs.CimCommitImage? + +//sys CimCreateFile(cimFSHandle FsHandle, path string, file *FileMetadata, 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? +//sys CimAddFsToMergedImage(cimFSHandle FsHandle, path string) (hr error) = cimfs.CimAddFsToMergedImage? +//sys CimAddFsToMergedImage2(cimFSHandle FsHandle, path string, flags uint32) (hr error) = cimfs.CimAddFsToMergedImage2? +//sys CimMergeMountImage(numCimPaths uint32, backingImagePaths *ImagePath, flags uint32, volumeID *GUID) (hr error) = cimfs.CimMergeMountImage? +//sys CimTombstoneFile(cimFSHandle FsHandle, path string) (hr error) = cimfs.CimTombstoneFile? +//sys CimCreateMergeLink(cimFSHandle FsHandle, newPath string, oldPath string) (hr error) = cimfs.CimCreateMergeLink? +//sys CimSealImage(blockCimPath string, hashSize *uint64, fixedHeaderSize *uint64, hash *byte) (hr error) = cimfs.CimSealImage? +//sys CimGetVerificationInformation(blockCimPath string, isSealed *uint32, hashSize *uint64, signatureSize *uint64, fixedHeaderSize *uint64, hash *byte, signature *byte) (hr error) = cimfs.CimGetVerificationInformation? +//sys CimMountVerifiedImage(imagePath string, fsName string, flags uint32, volumeID *GUID, hashSize uint16, hash *byte) (hr error) = cimfs.CimMountVerifiedImage? +//sys CimMergeMountVerifiedImage(numCimPaths uint32, backingImagePaths *ImagePath, flags uint32, volumeID *GUID, hashSize uint16, hash *byte) (hr error) = cimfs.CimMergeMountVerifiedImage? + +var load = sync.OnceValue(func() error { + if err := modcimfs.Load(); err != nil { + return err + } + var buf [windows.MAX_PATH]uint16 + n, _ := windows.GetModuleFileName(windows.Handle(modcimfs.Handle()), &buf[0], uint32(len(buf))) + if n > 0 { + logrus.WithField("path", windows.UTF16ToString(buf[:n])).Info("loaded cimfs.dll") + } + return nil +}) + +// Supported checks if cimfs.dll is present on the system. +func Supported() bool { + return load() == nil +} diff --git a/vendor/github.com/Microsoft/hcsshim/internal/winapi/cimfs/syscall.go b/vendor/github.com/Microsoft/hcsshim/internal/winapi/cimfs/syscall.go new file mode 100644 index 0000000000000..c33ca9b738396 --- /dev/null +++ b/vendor/github.com/Microsoft/hcsshim/internal/winapi/cimfs/syscall.go @@ -0,0 +1,3 @@ +package cimfs + +//go:generate go tool github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go ./*.go diff --git a/vendor/github.com/Microsoft/hcsshim/internal/winapi/cimfs/zsyscall_windows.go b/vendor/github.com/Microsoft/hcsshim/internal/winapi/cimfs/zsyscall_windows.go new file mode 100644 index 0000000000000..d1c36868bff17 --- /dev/null +++ b/vendor/github.com/Microsoft/hcsshim/internal/winapi/cimfs/zsyscall_windows.go @@ -0,0 +1,518 @@ +//go:build windows + +// Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT. + +package cimfs + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var _ unsafe.Pointer + +// Do the interface allocations only once for common +// Errno values. +const ( + errnoERROR_IO_PENDING = 997 +) + +var ( + errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) + errERROR_EINVAL error = syscall.EINVAL +) + +// errnoErr returns common boxed Errno values, to prevent +// allocations at runtime. +func errnoErr(e syscall.Errno) error { + switch e { + case 0: + return errERROR_EINVAL + case errnoERROR_IO_PENDING: + return errERROR_IO_PENDING + } + return e +} + +var ( + modcimfs = windows.NewLazySystemDLL("cimfs.dll") + + procCimAddFsToMergedImage = modcimfs.NewProc("CimAddFsToMergedImage") + procCimAddFsToMergedImage2 = modcimfs.NewProc("CimAddFsToMergedImage2") + 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") + procCimCreateImage2 = modcimfs.NewProc("CimCreateImage2") + procCimCreateMergeLink = modcimfs.NewProc("CimCreateMergeLink") + procCimDeletePath = modcimfs.NewProc("CimDeletePath") + procCimDismountImage = modcimfs.NewProc("CimDismountImage") + procCimGetVerificationInformation = modcimfs.NewProc("CimGetVerificationInformation") + procCimMergeMountImage = modcimfs.NewProc("CimMergeMountImage") + procCimMergeMountVerifiedImage = modcimfs.NewProc("CimMergeMountVerifiedImage") + procCimMountImage = modcimfs.NewProc("CimMountImage") + procCimMountVerifiedImage = modcimfs.NewProc("CimMountVerifiedImage") + procCimSealImage = modcimfs.NewProc("CimSealImage") + procCimTombstoneFile = modcimfs.NewProc("CimTombstoneFile") + procCimWriteStream = modcimfs.NewProc("CimWriteStream") +) + +func CimAddFsToMergedImage(cimFSHandle FsHandle, path string) (hr error) { + var _p0 *uint16 + _p0, hr = syscall.UTF16PtrFromString(path) + if hr != nil { + return + } + return _CimAddFsToMergedImage(cimFSHandle, _p0) +} + +func _CimAddFsToMergedImage(cimFSHandle FsHandle, path *uint16) (hr error) { + hr = procCimAddFsToMergedImage.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimAddFsToMergedImage.Addr(), uintptr(cimFSHandle), uintptr(unsafe.Pointer(path))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimAddFsToMergedImage2(cimFSHandle FsHandle, path string, flags uint32) (hr error) { + var _p0 *uint16 + _p0, hr = syscall.UTF16PtrFromString(path) + if hr != nil { + return + } + return _CimAddFsToMergedImage2(cimFSHandle, _p0, flags) +} + +func _CimAddFsToMergedImage2(cimFSHandle FsHandle, path *uint16, flags uint32) (hr error) { + hr = procCimAddFsToMergedImage2.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimAddFsToMergedImage2.Addr(), uintptr(cimFSHandle), uintptr(unsafe.Pointer(path)), uintptr(flags)) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimCloseImage(cimFSHandle FsHandle) (err error) { + err = procCimCloseImage.Find() + if err != nil { + return + } + syscall.SyscallN(procCimCloseImage.Addr(), uintptr(cimFSHandle)) + return +} + +func CimCloseStream(cimStreamHandle StreamHandle) (hr error) { + hr = procCimCloseStream.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimCloseStream.Addr(), uintptr(cimStreamHandle)) + 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.SyscallN(procCimCommitImage.Addr(), uintptr(cimFSHandle)) + 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.SyscallN(procCimCreateAlternateStream.Addr(), uintptr(cimFSHandle), uintptr(unsafe.Pointer(path)), uintptr(size), uintptr(unsafe.Pointer(cimStreamHandle))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimCreateFile(cimFSHandle FsHandle, path string, file *FileMetadata, 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 *FileMetadata, cimStreamHandle *StreamHandle) (hr error) { + hr = procCimCreateFile.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimCreateFile.Addr(), uintptr(cimFSHandle), uintptr(unsafe.Pointer(path)), uintptr(unsafe.Pointer(file)), uintptr(unsafe.Pointer(cimStreamHandle))) + 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.SyscallN(procCimCreateHardLink.Addr(), 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.SyscallN(procCimCreateImage.Addr(), uintptr(unsafe.Pointer(imagePath)), uintptr(unsafe.Pointer(oldFSName)), uintptr(unsafe.Pointer(newFSName)), uintptr(unsafe.Pointer(cimFSHandle))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimCreateImage2(imagePath string, flags uint32, oldFSName *uint16, newFSName *uint16, cimFSHandle *FsHandle) (hr error) { + var _p0 *uint16 + _p0, hr = syscall.UTF16PtrFromString(imagePath) + if hr != nil { + return + } + return _CimCreateImage2(_p0, flags, oldFSName, newFSName, cimFSHandle) +} + +func _CimCreateImage2(imagePath *uint16, flags uint32, oldFSName *uint16, newFSName *uint16, cimFSHandle *FsHandle) (hr error) { + hr = procCimCreateImage2.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimCreateImage2.Addr(), uintptr(unsafe.Pointer(imagePath)), uintptr(flags), uintptr(unsafe.Pointer(oldFSName)), uintptr(unsafe.Pointer(newFSName)), uintptr(unsafe.Pointer(cimFSHandle))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimCreateMergeLink(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 _CimCreateMergeLink(cimFSHandle, _p0, _p1) +} + +func _CimCreateMergeLink(cimFSHandle FsHandle, newPath *uint16, oldPath *uint16) (hr error) { + hr = procCimCreateMergeLink.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimCreateMergeLink.Addr(), 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 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.SyscallN(procCimDeletePath.Addr(), uintptr(cimFSHandle), uintptr(unsafe.Pointer(path))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimDismountImage(volumeID *GUID) (hr error) { + hr = procCimDismountImage.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimDismountImage.Addr(), uintptr(unsafe.Pointer(volumeID))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimGetVerificationInformation(blockCimPath string, isSealed *uint32, hashSize *uint64, signatureSize *uint64, fixedHeaderSize *uint64, hash *byte, signature *byte) (hr error) { + var _p0 *uint16 + _p0, hr = syscall.UTF16PtrFromString(blockCimPath) + if hr != nil { + return + } + return _CimGetVerificationInformation(_p0, isSealed, hashSize, signatureSize, fixedHeaderSize, hash, signature) +} + +func _CimGetVerificationInformation(blockCimPath *uint16, isSealed *uint32, hashSize *uint64, signatureSize *uint64, fixedHeaderSize *uint64, hash *byte, signature *byte) (hr error) { + hr = procCimGetVerificationInformation.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimGetVerificationInformation.Addr(), uintptr(unsafe.Pointer(blockCimPath)), uintptr(unsafe.Pointer(isSealed)), uintptr(unsafe.Pointer(hashSize)), uintptr(unsafe.Pointer(signatureSize)), uintptr(unsafe.Pointer(fixedHeaderSize)), uintptr(unsafe.Pointer(hash)), uintptr(unsafe.Pointer(signature))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimMergeMountImage(numCimPaths uint32, backingImagePaths *ImagePath, flags uint32, volumeID *GUID) (hr error) { + hr = procCimMergeMountImage.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimMergeMountImage.Addr(), uintptr(numCimPaths), uintptr(unsafe.Pointer(backingImagePaths)), uintptr(flags), uintptr(unsafe.Pointer(volumeID))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimMergeMountVerifiedImage(numCimPaths uint32, backingImagePaths *ImagePath, flags uint32, volumeID *GUID, hashSize uint16, hash *byte) (hr error) { + hr = procCimMergeMountVerifiedImage.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimMergeMountVerifiedImage.Addr(), uintptr(numCimPaths), uintptr(unsafe.Pointer(backingImagePaths)), uintptr(flags), uintptr(unsafe.Pointer(volumeID)), uintptr(hashSize), uintptr(unsafe.Pointer(hash))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimMountImage(imagePath string, fsName string, flags uint32, volumeID *GUID) (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 *GUID) (hr error) { + hr = procCimMountImage.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimMountImage.Addr(), uintptr(unsafe.Pointer(imagePath)), uintptr(unsafe.Pointer(fsName)), uintptr(flags), uintptr(unsafe.Pointer(volumeID))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimMountVerifiedImage(imagePath string, fsName string, flags uint32, volumeID *GUID, hashSize uint16, hash *byte) (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 _CimMountVerifiedImage(_p0, _p1, flags, volumeID, hashSize, hash) +} + +func _CimMountVerifiedImage(imagePath *uint16, fsName *uint16, flags uint32, volumeID *GUID, hashSize uint16, hash *byte) (hr error) { + hr = procCimMountVerifiedImage.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimMountVerifiedImage.Addr(), uintptr(unsafe.Pointer(imagePath)), uintptr(unsafe.Pointer(fsName)), uintptr(flags), uintptr(unsafe.Pointer(volumeID)), uintptr(hashSize), uintptr(unsafe.Pointer(hash))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimSealImage(blockCimPath string, hashSize *uint64, fixedHeaderSize *uint64, hash *byte) (hr error) { + var _p0 *uint16 + _p0, hr = syscall.UTF16PtrFromString(blockCimPath) + if hr != nil { + return + } + return _CimSealImage(_p0, hashSize, fixedHeaderSize, hash) +} + +func _CimSealImage(blockCimPath *uint16, hashSize *uint64, fixedHeaderSize *uint64, hash *byte) (hr error) { + hr = procCimSealImage.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimSealImage.Addr(), uintptr(unsafe.Pointer(blockCimPath)), uintptr(unsafe.Pointer(hashSize)), uintptr(unsafe.Pointer(fixedHeaderSize)), uintptr(unsafe.Pointer(hash))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimTombstoneFile(cimFSHandle FsHandle, path string) (hr error) { + var _p0 *uint16 + _p0, hr = syscall.UTF16PtrFromString(path) + if hr != nil { + return + } + return _CimTombstoneFile(cimFSHandle, _p0) +} + +func _CimTombstoneFile(cimFSHandle FsHandle, path *uint16) (hr error) { + hr = procCimTombstoneFile.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimTombstoneFile.Addr(), uintptr(cimFSHandle), uintptr(unsafe.Pointer(path))) + 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.SyscallN(procCimWriteStream.Addr(), uintptr(cimStreamHandle), uintptr(buffer), uintptr(bufferSize)) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} diff --git a/vendor/github.com/Microsoft/hcsshim/internal/winapi/cimwriter/cimwriter.go b/vendor/github.com/Microsoft/hcsshim/internal/winapi/cimwriter/cimwriter.go new file mode 100644 index 0000000000000..5d58e487d6f8c --- /dev/null +++ b/vendor/github.com/Microsoft/hcsshim/internal/winapi/cimwriter/cimwriter.go @@ -0,0 +1,64 @@ +//go:build windows + +package cimwriter + +import ( + "sync" + + "github.com/sirupsen/logrus" + "golang.org/x/sys/windows" + + "github.com/Microsoft/hcsshim/internal/winapi/types" +) + +type FsHandle = types.FsHandle +type StreamHandle = types.StreamHandle +type FileMetadata = types.CimFsFileMetadata +type ImagePath = types.CimFsImagePath + +//sys CimCreateImage(imagePath string, oldFSName *uint16, newFSName *uint16, cimFSHandle *FsHandle) (hr error) = cimwriter.CimCreateImage? +//sys CimCreateImage2(imagePath string, flags uint32, oldFSName *uint16, newFSName *uint16, cimFSHandle *FsHandle) (hr error) = cimwriter.CimCreateImage2? +//sys CimCloseImage(cimFSHandle FsHandle) = cimwriter.CimCloseImage? +//sys CimCommitImage(cimFSHandle FsHandle) (hr error) = cimwriter.CimCommitImage? + +//sys CimCreateFile(cimFSHandle FsHandle, path string, file *FileMetadata, cimStreamHandle *StreamHandle) (hr error) = cimwriter.CimCreateFile? +//sys CimCloseStream(cimStreamHandle StreamHandle) (hr error) = cimwriter.CimCloseStream? +//sys CimWriteStream(cimStreamHandle StreamHandle, buffer uintptr, bufferSize uint32) (hr error) = cimwriter.CimWriteStream? +//sys CimDeletePath(cimFSHandle FsHandle, path string) (hr error) = cimwriter.CimDeletePath? +//sys CimCreateHardLink(cimFSHandle FsHandle, newPath string, oldPath string) (hr error) = cimwriter.CimCreateHardLink? +//sys CimCreateAlternateStream(cimFSHandle FsHandle, path string, size uint64, cimStreamHandle *StreamHandle) (hr error) = cimwriter.CimCreateAlternateStream? +//sys CimAddFsToMergedImage(cimFSHandle FsHandle, path string) (hr error) = cimwriter.CimAddFsToMergedImage? +//sys CimAddFsToMergedImage2(cimFSHandle FsHandle, path string, flags uint32) (hr error) = cimwriter.CimAddFsToMergedImage2? + +//sys CimTombstoneFile(cimFSHandle FsHandle, path string) (hr error) = cimwriter.CimTombstoneFile? +//sys CimCreateMergeLink(cimFSHandle FsHandle, newPath string, oldPath string) (hr error) = cimwriter.CimCreateMergeLink? +//sys CimSealImage(blockCimPath string, hashSize *uint64, fixedHeaderSize *uint64, hash *byte) (hr error) = cimwriter.CimSealImage? + +//sys CimGetVerificationInformation(blockCimPath string, isSealed *uint32, hashSize *uint64, signatureSize *uint64, fixedHeaderSize *uint64, hash *byte, signature *byte) (hr error) = cimwriter.CimGetVerificationInformation? + +var load = sync.OnceValue(func() error { + // Pre-load the DLL with a restricted search path (System32 + application directory only) + // to prevent loading from untrusted locations (e.g., CWD or arbitrary PATH entries). + // The subsequent modcimwriter.Load() will reuse the already-loaded module. + h, err := windows.LoadLibraryEx("cimwriter.dll", 0, windows.LOAD_LIBRARY_SEARCH_SYSTEM32|windows.LOAD_LIBRARY_SEARCH_APPLICATION_DIR) + if err != nil { + return err + } + if err := modcimwriter.Load(); err != nil { + if freeErr := windows.FreeLibrary(h); freeErr != nil { + logrus.WithError(freeErr).Warn("failed to free cimwriter.dll after load failure") + } + return err + } + var buf [windows.MAX_PATH]uint16 + n, _ := windows.GetModuleFileName(windows.Handle(modcimwriter.Handle()), &buf[0], uint32(len(buf))) + if n > 0 { + logrus.WithField("path", windows.UTF16ToString(buf[:n])).Info("loaded cimwriter.dll") + } + return nil +}) + +// Supported checks if cimwriter.dll is present on the system. +func Supported() bool { + return load() == nil +} diff --git a/vendor/github.com/Microsoft/hcsshim/internal/winapi/cimwriter/syscall.go b/vendor/github.com/Microsoft/hcsshim/internal/winapi/cimwriter/syscall.go new file mode 100644 index 0000000000000..cfed62fb65c0c --- /dev/null +++ b/vendor/github.com/Microsoft/hcsshim/internal/winapi/cimwriter/syscall.go @@ -0,0 +1,3 @@ +package cimwriter + +//go:generate go tool github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go -systemdll=false ./*.go diff --git a/vendor/github.com/Microsoft/hcsshim/internal/winapi/cimwriter/zsyscall_windows.go b/vendor/github.com/Microsoft/hcsshim/internal/winapi/cimwriter/zsyscall_windows.go new file mode 100644 index 0000000000000..3dbafa1a29c99 --- /dev/null +++ b/vendor/github.com/Microsoft/hcsshim/internal/winapi/cimwriter/zsyscall_windows.go @@ -0,0 +1,408 @@ +//go:build windows + +// Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT. + +package cimwriter + +import ( + "syscall" + "unsafe" +) + +var _ unsafe.Pointer + +// Do the interface allocations only once for common +// Errno values. +const ( + errnoERROR_IO_PENDING = 997 +) + +var ( + errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) + errERROR_EINVAL error = syscall.EINVAL +) + +// errnoErr returns common boxed Errno values, to prevent +// allocations at runtime. +func errnoErr(e syscall.Errno) error { + switch e { + case 0: + return errERROR_EINVAL + case errnoERROR_IO_PENDING: + return errERROR_IO_PENDING + } + return e +} + +var ( + modcimwriter = syscall.NewLazyDLL("cimwriter.dll") + + procCimAddFsToMergedImage = modcimwriter.NewProc("CimAddFsToMergedImage") + procCimAddFsToMergedImage2 = modcimwriter.NewProc("CimAddFsToMergedImage2") + procCimCloseImage = modcimwriter.NewProc("CimCloseImage") + procCimCloseStream = modcimwriter.NewProc("CimCloseStream") + procCimCommitImage = modcimwriter.NewProc("CimCommitImage") + procCimCreateAlternateStream = modcimwriter.NewProc("CimCreateAlternateStream") + procCimCreateFile = modcimwriter.NewProc("CimCreateFile") + procCimCreateHardLink = modcimwriter.NewProc("CimCreateHardLink") + procCimCreateImage = modcimwriter.NewProc("CimCreateImage") + procCimCreateImage2 = modcimwriter.NewProc("CimCreateImage2") + procCimCreateMergeLink = modcimwriter.NewProc("CimCreateMergeLink") + procCimDeletePath = modcimwriter.NewProc("CimDeletePath") + procCimGetVerificationInformation = modcimwriter.NewProc("CimGetVerificationInformation") + procCimSealImage = modcimwriter.NewProc("CimSealImage") + procCimTombstoneFile = modcimwriter.NewProc("CimTombstoneFile") + procCimWriteStream = modcimwriter.NewProc("CimWriteStream") +) + +func CimAddFsToMergedImage(cimFSHandle FsHandle, path string) (hr error) { + var _p0 *uint16 + _p0, hr = syscall.UTF16PtrFromString(path) + if hr != nil { + return + } + return _CimAddFsToMergedImage(cimFSHandle, _p0) +} + +func _CimAddFsToMergedImage(cimFSHandle FsHandle, path *uint16) (hr error) { + hr = procCimAddFsToMergedImage.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimAddFsToMergedImage.Addr(), uintptr(cimFSHandle), uintptr(unsafe.Pointer(path))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimAddFsToMergedImage2(cimFSHandle FsHandle, path string, flags uint32) (hr error) { + var _p0 *uint16 + _p0, hr = syscall.UTF16PtrFromString(path) + if hr != nil { + return + } + return _CimAddFsToMergedImage2(cimFSHandle, _p0, flags) +} + +func _CimAddFsToMergedImage2(cimFSHandle FsHandle, path *uint16, flags uint32) (hr error) { + hr = procCimAddFsToMergedImage2.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimAddFsToMergedImage2.Addr(), uintptr(cimFSHandle), uintptr(unsafe.Pointer(path)), uintptr(flags)) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimCloseImage(cimFSHandle FsHandle) (err error) { + err = procCimCloseImage.Find() + if err != nil { + return + } + syscall.SyscallN(procCimCloseImage.Addr(), uintptr(cimFSHandle)) + return +} + +func CimCloseStream(cimStreamHandle StreamHandle) (hr error) { + hr = procCimCloseStream.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimCloseStream.Addr(), uintptr(cimStreamHandle)) + 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.SyscallN(procCimCommitImage.Addr(), uintptr(cimFSHandle)) + 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.SyscallN(procCimCreateAlternateStream.Addr(), uintptr(cimFSHandle), uintptr(unsafe.Pointer(path)), uintptr(size), uintptr(unsafe.Pointer(cimStreamHandle))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimCreateFile(cimFSHandle FsHandle, path string, file *FileMetadata, 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 *FileMetadata, cimStreamHandle *StreamHandle) (hr error) { + hr = procCimCreateFile.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimCreateFile.Addr(), uintptr(cimFSHandle), uintptr(unsafe.Pointer(path)), uintptr(unsafe.Pointer(file)), uintptr(unsafe.Pointer(cimStreamHandle))) + 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.SyscallN(procCimCreateHardLink.Addr(), 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.SyscallN(procCimCreateImage.Addr(), uintptr(unsafe.Pointer(imagePath)), uintptr(unsafe.Pointer(oldFSName)), uintptr(unsafe.Pointer(newFSName)), uintptr(unsafe.Pointer(cimFSHandle))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimCreateImage2(imagePath string, flags uint32, oldFSName *uint16, newFSName *uint16, cimFSHandle *FsHandle) (hr error) { + var _p0 *uint16 + _p0, hr = syscall.UTF16PtrFromString(imagePath) + if hr != nil { + return + } + return _CimCreateImage2(_p0, flags, oldFSName, newFSName, cimFSHandle) +} + +func _CimCreateImage2(imagePath *uint16, flags uint32, oldFSName *uint16, newFSName *uint16, cimFSHandle *FsHandle) (hr error) { + hr = procCimCreateImage2.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimCreateImage2.Addr(), uintptr(unsafe.Pointer(imagePath)), uintptr(flags), uintptr(unsafe.Pointer(oldFSName)), uintptr(unsafe.Pointer(newFSName)), uintptr(unsafe.Pointer(cimFSHandle))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimCreateMergeLink(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 _CimCreateMergeLink(cimFSHandle, _p0, _p1) +} + +func _CimCreateMergeLink(cimFSHandle FsHandle, newPath *uint16, oldPath *uint16) (hr error) { + hr = procCimCreateMergeLink.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimCreateMergeLink.Addr(), 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 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.SyscallN(procCimDeletePath.Addr(), uintptr(cimFSHandle), uintptr(unsafe.Pointer(path))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimGetVerificationInformation(blockCimPath string, isSealed *uint32, hashSize *uint64, signatureSize *uint64, fixedHeaderSize *uint64, hash *byte, signature *byte) (hr error) { + var _p0 *uint16 + _p0, hr = syscall.UTF16PtrFromString(blockCimPath) + if hr != nil { + return + } + return _CimGetVerificationInformation(_p0, isSealed, hashSize, signatureSize, fixedHeaderSize, hash, signature) +} + +func _CimGetVerificationInformation(blockCimPath *uint16, isSealed *uint32, hashSize *uint64, signatureSize *uint64, fixedHeaderSize *uint64, hash *byte, signature *byte) (hr error) { + hr = procCimGetVerificationInformation.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimGetVerificationInformation.Addr(), uintptr(unsafe.Pointer(blockCimPath)), uintptr(unsafe.Pointer(isSealed)), uintptr(unsafe.Pointer(hashSize)), uintptr(unsafe.Pointer(signatureSize)), uintptr(unsafe.Pointer(fixedHeaderSize)), uintptr(unsafe.Pointer(hash)), uintptr(unsafe.Pointer(signature))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimSealImage(blockCimPath string, hashSize *uint64, fixedHeaderSize *uint64, hash *byte) (hr error) { + var _p0 *uint16 + _p0, hr = syscall.UTF16PtrFromString(blockCimPath) + if hr != nil { + return + } + return _CimSealImage(_p0, hashSize, fixedHeaderSize, hash) +} + +func _CimSealImage(blockCimPath *uint16, hashSize *uint64, fixedHeaderSize *uint64, hash *byte) (hr error) { + hr = procCimSealImage.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimSealImage.Addr(), uintptr(unsafe.Pointer(blockCimPath)), uintptr(unsafe.Pointer(hashSize)), uintptr(unsafe.Pointer(fixedHeaderSize)), uintptr(unsafe.Pointer(hash))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimTombstoneFile(cimFSHandle FsHandle, path string) (hr error) { + var _p0 *uint16 + _p0, hr = syscall.UTF16PtrFromString(path) + if hr != nil { + return + } + return _CimTombstoneFile(cimFSHandle, _p0) +} + +func _CimTombstoneFile(cimFSHandle FsHandle, path *uint16) (hr error) { + hr = procCimTombstoneFile.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimTombstoneFile.Addr(), uintptr(cimFSHandle), uintptr(unsafe.Pointer(path))) + 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.SyscallN(procCimWriteStream.Addr(), uintptr(cimStreamHandle), uintptr(buffer), uintptr(bufferSize)) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} diff --git a/vendor/github.com/Microsoft/hcsshim/internal/winapi/devices.go b/vendor/github.com/Microsoft/hcsshim/internal/winapi/devices.go index 36d3cb7000b92..364ba6c71cb67 100644 --- a/vendor/github.com/Microsoft/hcsshim/internal/winapi/devices.go +++ b/vendor/github.com/Microsoft/hcsshim/internal/winapi/devices.go @@ -4,13 +4,15 @@ package winapi import "github.com/Microsoft/go-winio/pkg/guid" -//sys CMGetDeviceInterfaceListSize(listlen *uint32, classGUID *g, deviceID *uint16, ulFlags uint32) (hr error) = cfgmgr32.CM_Get_Device_Interface_List_SizeW -//sys CMGetDeviceInterfaceList(classGUID *g, deviceID *uint16, buffer *uint16, bufLen uint32, ulFlags uint32) (hr error) = cfgmgr32.CM_Get_Device_Interface_ListW +//sys CMGetDeviceInterfaceListSize(listlen *uint32, classGUID *GUID, deviceID *uint16, ulFlags uint32) (hr error) = cfgmgr32.CM_Get_Device_Interface_List_SizeW +//sys CMGetDeviceInterfaceList(classGUID *GUID, deviceID *uint16, buffer *uint16, bufLen uint32, ulFlags uint32) (hr error) = cfgmgr32.CM_Get_Device_Interface_ListW //sys CMGetDeviceIDListSize(pulLen *uint32, pszFilter *byte, uFlags uint32) (hr error) = cfgmgr32.CM_Get_Device_ID_List_SizeA //sys CMGetDeviceIDList(pszFilter *byte, buffer *byte, bufferLen uint32, uFlags uint32) (hr error)= cfgmgr32.CM_Get_Device_ID_ListA //sys CMLocateDevNode(pdnDevInst *uint32, pDeviceID string, uFlags uint32) (hr error) = cfgmgr32.CM_Locate_DevNodeW //sys CMGetDevNodeProperty(dnDevInst uint32, propertyKey *DevPropKey, propertyType *uint32, propertyBuffer *uint16, propertyBufferSize *uint32, uFlags uint32) (hr error) = cfgmgr32.CM_Get_DevNode_PropertyW +type GUID = guid.GUID + type DevPropKey struct { Fmtid guid.GUID Pid uint32 diff --git a/vendor/github.com/Microsoft/hcsshim/internal/winapi/filesystem.go b/vendor/github.com/Microsoft/hcsshim/internal/winapi/filesystem.go index 3dcb3faa0b698..fec34f1f6c7f2 100644 --- a/vendor/github.com/Microsoft/hcsshim/internal/winapi/filesystem.go +++ b/vendor/github.com/Microsoft/hcsshim/internal/winapi/filesystem.go @@ -8,6 +8,7 @@ package winapi //sys NtOpenDirectoryObject(handle *uintptr, accessMask uint32, oa *ObjectAttributes) (status uint32) = ntdll.NtOpenDirectoryObject //sys NtQueryDirectoryObject(handle uintptr, buffer *byte, length uint32, singleEntry bool, restartScan bool, context *uint32, returnLength *uint32)(status uint32) = ntdll.NtQueryDirectoryObject +//sys NtFsControlFile(file windows.Handle, event windows.Handle, apcRoutine uintptr, apcCtx uintptr, iosb *IOStatusBlock, fsControlCode uint32, in []byte, out []byte) (status uint32) = ntdll.NtFsControlFile const ( FileLinkInformationClass = 11 diff --git a/vendor/github.com/Microsoft/hcsshim/internal/winapi/types/cimfs.go b/vendor/github.com/Microsoft/hcsshim/internal/winapi/types/cimfs.go new file mode 100644 index 0000000000000..2fa8cfc71dd03 --- /dev/null +++ b/vendor/github.com/Microsoft/hcsshim/internal/winapi/types/cimfs.go @@ -0,0 +1,36 @@ +//go:build windows + +package types + +import ( + "unsafe" + + "golang.org/x/sys/windows" +) + +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 +} + +type CimFsImagePath struct { + ImageDir *uint16 + ImageName *uint16 +} diff --git a/vendor/github.com/Microsoft/hcsshim/internal/winapi/winapi.go b/vendor/github.com/Microsoft/hcsshim/internal/winapi/winapi.go index 6a90e3a69ac9c..009e70ab19589 100644 --- a/vendor/github.com/Microsoft/hcsshim/internal/winapi/winapi.go +++ b/vendor/github.com/Microsoft/hcsshim/internal/winapi/winapi.go @@ -1,3 +1,3 @@ package winapi -//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go ./*.go +//go:generate go tool github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go ./*.go diff --git a/vendor/github.com/Microsoft/hcsshim/internal/winapi/zsyscall_windows.go b/vendor/github.com/Microsoft/hcsshim/internal/winapi/zsyscall_windows.go index a7eea44ec7e13..28b28d1c69b0d 100644 --- a/vendor/github.com/Microsoft/hcsshim/internal/winapi/zsyscall_windows.go +++ b/vendor/github.com/Microsoft/hcsshim/internal/winapi/zsyscall_windows.go @@ -37,17 +37,19 @@ func errnoErr(e syscall.Errno) error { } 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") - modntdll = windows.NewLazySystemDLL("ntdll.dll") - modoffreg = windows.NewLazySystemDLL("offreg.dll") + modadvapi32 = windows.NewLazySystemDLL("advapi32.dll") + modamdsnppspapi = windows.NewLazySystemDLL("amdsnppspapi.dll") + modbindfltapi = windows.NewLazySystemDLL("bindfltapi.dll") + modcfgmgr32 = windows.NewLazySystemDLL("cfgmgr32.dll") + modiphlpapi = windows.NewLazySystemDLL("iphlpapi.dll") + modkernel32 = windows.NewLazySystemDLL("kernel32.dll") + modnetapi32 = windows.NewLazySystemDLL("netapi32.dll") + modntdll = windows.NewLazySystemDLL("ntdll.dll") + modoffreg = windows.NewLazySystemDLL("offreg.dll") procLogonUserW = modadvapi32.NewProc("LogonUserW") + procSnpPspFetchAttestationReport = modamdsnppspapi.NewProc("SnpPspFetchAttestationReport") + procSnpPspIsSnpMode = modamdsnppspapi.NewProc("SnpPspIsSnpMode") procBfSetupFilter = modbindfltapi.NewProc("BfSetupFilter") procCM_Get_DevNode_PropertyW = modcfgmgr32.NewProc("CM_Get_DevNode_PropertyW") procCM_Get_Device_ID_ListA = modcfgmgr32.NewProc("CM_Get_Device_ID_ListA") @@ -55,26 +57,6 @@ var ( procCM_Get_Device_Interface_ListW = modcfgmgr32.NewProc("CM_Get_Device_Interface_ListW") procCM_Get_Device_Interface_List_SizeW = modcfgmgr32.NewProc("CM_Get_Device_Interface_List_SizeW") procCM_Locate_DevNodeW = modcfgmgr32.NewProc("CM_Locate_DevNodeW") - procCimAddFsToMergedImage = modcimfs.NewProc("CimAddFsToMergedImage") - procCimAddFsToMergedImage2 = modcimfs.NewProc("CimAddFsToMergedImage2") - 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") - procCimCreateImage2 = modcimfs.NewProc("CimCreateImage2") - procCimCreateMergeLink = modcimfs.NewProc("CimCreateMergeLink") - procCimDeletePath = modcimfs.NewProc("CimDeletePath") - procCimDismountImage = modcimfs.NewProc("CimDismountImage") - procCimGetVerificationInformation = modcimfs.NewProc("CimGetVerificationInformation") - procCimMergeMountImage = modcimfs.NewProc("CimMergeMountImage") - procCimMountImage = modcimfs.NewProc("CimMountImage") - procCimMountVerifiedImage = modcimfs.NewProc("CimMountVerifiedImage") - procCimSealImage = modcimfs.NewProc("CimSealImage") - procCimTombstoneFile = modcimfs.NewProc("CimTombstoneFile") - procCimWriteStream = modcimfs.NewProc("CimWriteStream") procSetJobCompartmentId = modiphlpapi.NewProc("SetJobCompartmentId") procClosePseudoConsole = modkernel32.NewProc("ClosePseudoConsole") procCopyFileW = modkernel32.NewProc("CopyFileW") @@ -96,6 +78,7 @@ var ( procNetUserDel = modnetapi32.NewProc("NetUserDel") procNtCreateFile = modntdll.NewProc("NtCreateFile") procNtCreateJobObject = modntdll.NewProc("NtCreateJobObject") + procNtFsControlFile = modntdll.NewProc("NtFsControlFile") procNtOpenDirectoryObject = modntdll.NewProc("NtOpenDirectoryObject") procNtOpenJobObject = modntdll.NewProc("NtOpenJobObject") procNtQueryDirectoryObject = modntdll.NewProc("NtQueryDirectoryObject") @@ -124,6 +107,32 @@ func LogonUser(username *uint16, domain *uint16, password *uint16, logonType uin return } +func SnpPspFetchAttestationReport(reportData *uint8, guestRequestResult *SNPPSPGuestRequestResult, report *uint8) (ret uint32, err error) { + err = procSnpPspFetchAttestationReport.Find() + if err != nil { + return + } + r0, _, e1 := syscall.SyscallN(procSnpPspFetchAttestationReport.Addr(), uintptr(unsafe.Pointer(reportData)), uintptr(unsafe.Pointer(guestRequestResult)), uintptr(unsafe.Pointer(report))) + ret = uint32(r0) + if ret > 0 { + err = errnoErr(e1) + } + return +} + +func SnpPspIsSnpMode(snpMode *uint8) (ret uint32, err error) { + err = procSnpPspIsSnpMode.Find() + if err != nil { + return + } + r0, _, e1 := syscall.SyscallN(procSnpPspIsSnpMode.Addr(), uintptr(unsafe.Pointer(snpMode))) + ret = uint32(r0) + if ret > 0 { + err = errnoErr(e1) + } + return +} + func BfSetupFilter(jobHandle windows.Handle, flags uint32, virtRootPath *uint16, virtTargetPath *uint16, virtExceptions **uint16, virtExceptionPathCount uint32) (hr error) { hr = procBfSetupFilter.Find() if hr != nil { @@ -172,7 +181,7 @@ func CMGetDeviceIDListSize(pulLen *uint32, pszFilter *byte, uFlags uint32) (hr e return } -func CMGetDeviceInterfaceList(classGUID *g, deviceID *uint16, buffer *uint16, bufLen uint32, ulFlags uint32) (hr error) { +func CMGetDeviceInterfaceList(classGUID *GUID, deviceID *uint16, buffer *uint16, bufLen uint32, ulFlags uint32) (hr error) { r0, _, _ := syscall.SyscallN(procCM_Get_Device_Interface_ListW.Addr(), uintptr(unsafe.Pointer(classGUID)), uintptr(unsafe.Pointer(deviceID)), uintptr(unsafe.Pointer(buffer)), uintptr(bufLen), uintptr(ulFlags)) if int32(r0) < 0 { if r0&0x1fff0000 == 0x00070000 { @@ -183,7 +192,7 @@ func CMGetDeviceInterfaceList(classGUID *g, deviceID *uint16, buffer *uint16, bu return } -func CMGetDeviceInterfaceListSize(listlen *uint32, classGUID *g, deviceID *uint16, ulFlags uint32) (hr error) { +func CMGetDeviceInterfaceListSize(listlen *uint32, classGUID *GUID, deviceID *uint16, ulFlags uint32) (hr error) { r0, _, _ := syscall.SyscallN(procCM_Get_Device_Interface_List_SizeW.Addr(), uintptr(unsafe.Pointer(listlen)), uintptr(unsafe.Pointer(classGUID)), uintptr(unsafe.Pointer(deviceID)), uintptr(ulFlags)) if int32(r0) < 0 { if r0&0x1fff0000 == 0x00070000 { @@ -214,446 +223,6 @@ func _CMLocateDevNode(pdnDevInst *uint32, pDeviceID *uint16, uFlags uint32) (hr return } -func CimAddFsToMergedImage(cimFSHandle FsHandle, path string) (hr error) { - var _p0 *uint16 - _p0, hr = syscall.UTF16PtrFromString(path) - if hr != nil { - return - } - return _CimAddFsToMergedImage(cimFSHandle, _p0) -} - -func _CimAddFsToMergedImage(cimFSHandle FsHandle, path *uint16) (hr error) { - hr = procCimAddFsToMergedImage.Find() - if hr != nil { - return - } - r0, _, _ := syscall.SyscallN(procCimAddFsToMergedImage.Addr(), uintptr(cimFSHandle), uintptr(unsafe.Pointer(path))) - if int32(r0) < 0 { - if r0&0x1fff0000 == 0x00070000 { - r0 &= 0xffff - } - hr = syscall.Errno(r0) - } - return -} - -func CimAddFsToMergedImage2(cimFSHandle FsHandle, path string, flags uint32) (hr error) { - var _p0 *uint16 - _p0, hr = syscall.UTF16PtrFromString(path) - if hr != nil { - return - } - return _CimAddFsToMergedImage2(cimFSHandle, _p0, flags) -} - -func _CimAddFsToMergedImage2(cimFSHandle FsHandle, path *uint16, flags uint32) (hr error) { - hr = procCimAddFsToMergedImage2.Find() - if hr != nil { - return - } - r0, _, _ := syscall.SyscallN(procCimAddFsToMergedImage2.Addr(), uintptr(cimFSHandle), uintptr(unsafe.Pointer(path)), uintptr(flags)) - if int32(r0) < 0 { - if r0&0x1fff0000 == 0x00070000 { - r0 &= 0xffff - } - hr = syscall.Errno(r0) - } - return -} - -func CimCloseImage(cimFSHandle FsHandle) (err error) { - err = procCimCloseImage.Find() - if err != nil { - return - } - syscall.SyscallN(procCimCloseImage.Addr(), uintptr(cimFSHandle)) - return -} - -func CimCloseStream(cimStreamHandle StreamHandle) (hr error) { - hr = procCimCloseStream.Find() - if hr != nil { - return - } - r0, _, _ := syscall.SyscallN(procCimCloseStream.Addr(), uintptr(cimStreamHandle)) - 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.SyscallN(procCimCommitImage.Addr(), uintptr(cimFSHandle)) - 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.SyscallN(procCimCreateAlternateStream.Addr(), uintptr(cimFSHandle), uintptr(unsafe.Pointer(path)), uintptr(size), uintptr(unsafe.Pointer(cimStreamHandle))) - 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.SyscallN(procCimCreateFile.Addr(), uintptr(cimFSHandle), uintptr(unsafe.Pointer(path)), uintptr(unsafe.Pointer(file)), uintptr(unsafe.Pointer(cimStreamHandle))) - 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.SyscallN(procCimCreateHardLink.Addr(), 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.SyscallN(procCimCreateImage.Addr(), uintptr(unsafe.Pointer(imagePath)), uintptr(unsafe.Pointer(oldFSName)), uintptr(unsafe.Pointer(newFSName)), uintptr(unsafe.Pointer(cimFSHandle))) - if int32(r0) < 0 { - if r0&0x1fff0000 == 0x00070000 { - r0 &= 0xffff - } - hr = syscall.Errno(r0) - } - return -} - -func CimCreateImage2(imagePath string, flags uint32, oldFSName *uint16, newFSName *uint16, cimFSHandle *FsHandle) (hr error) { - var _p0 *uint16 - _p0, hr = syscall.UTF16PtrFromString(imagePath) - if hr != nil { - return - } - return _CimCreateImage2(_p0, flags, oldFSName, newFSName, cimFSHandle) -} - -func _CimCreateImage2(imagePath *uint16, flags uint32, oldFSName *uint16, newFSName *uint16, cimFSHandle *FsHandle) (hr error) { - hr = procCimCreateImage2.Find() - if hr != nil { - return - } - r0, _, _ := syscall.SyscallN(procCimCreateImage2.Addr(), uintptr(unsafe.Pointer(imagePath)), uintptr(flags), uintptr(unsafe.Pointer(oldFSName)), uintptr(unsafe.Pointer(newFSName)), uintptr(unsafe.Pointer(cimFSHandle))) - if int32(r0) < 0 { - if r0&0x1fff0000 == 0x00070000 { - r0 &= 0xffff - } - hr = syscall.Errno(r0) - } - return -} - -func CimCreateMergeLink(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 _CimCreateMergeLink(cimFSHandle, _p0, _p1) -} - -func _CimCreateMergeLink(cimFSHandle FsHandle, newPath *uint16, oldPath *uint16) (hr error) { - hr = procCimCreateMergeLink.Find() - if hr != nil { - return - } - r0, _, _ := syscall.SyscallN(procCimCreateMergeLink.Addr(), 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 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.SyscallN(procCimDeletePath.Addr(), uintptr(cimFSHandle), uintptr(unsafe.Pointer(path))) - 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.SyscallN(procCimDismountImage.Addr(), uintptr(unsafe.Pointer(volumeID))) - if int32(r0) < 0 { - if r0&0x1fff0000 == 0x00070000 { - r0 &= 0xffff - } - hr = syscall.Errno(r0) - } - return -} - -func CimGetVerificationInformation(blockCimPath string, isSealed *uint32, hashSize *uint64, signatureSize *uint64, fixedHeaderSize *uint64, hash *byte, signature *byte) (hr error) { - var _p0 *uint16 - _p0, hr = syscall.UTF16PtrFromString(blockCimPath) - if hr != nil { - return - } - return _CimGetVerificationInformation(_p0, isSealed, hashSize, signatureSize, fixedHeaderSize, hash, signature) -} - -func _CimGetVerificationInformation(blockCimPath *uint16, isSealed *uint32, hashSize *uint64, signatureSize *uint64, fixedHeaderSize *uint64, hash *byte, signature *byte) (hr error) { - hr = procCimGetVerificationInformation.Find() - if hr != nil { - return - } - r0, _, _ := syscall.SyscallN(procCimGetVerificationInformation.Addr(), uintptr(unsafe.Pointer(blockCimPath)), uintptr(unsafe.Pointer(isSealed)), uintptr(unsafe.Pointer(hashSize)), uintptr(unsafe.Pointer(signatureSize)), uintptr(unsafe.Pointer(fixedHeaderSize)), uintptr(unsafe.Pointer(hash)), uintptr(unsafe.Pointer(signature))) - if int32(r0) < 0 { - if r0&0x1fff0000 == 0x00070000 { - r0 &= 0xffff - } - hr = syscall.Errno(r0) - } - return -} - -func CimMergeMountImage(numCimPaths uint32, backingImagePaths *CimFsImagePath, flags uint32, volumeID *g) (hr error) { - hr = procCimMergeMountImage.Find() - if hr != nil { - return - } - r0, _, _ := syscall.SyscallN(procCimMergeMountImage.Addr(), uintptr(numCimPaths), uintptr(unsafe.Pointer(backingImagePaths)), uintptr(flags), uintptr(unsafe.Pointer(volumeID))) - 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.SyscallN(procCimMountImage.Addr(), uintptr(unsafe.Pointer(imagePath)), uintptr(unsafe.Pointer(fsName)), uintptr(flags), uintptr(unsafe.Pointer(volumeID))) - if int32(r0) < 0 { - if r0&0x1fff0000 == 0x00070000 { - r0 &= 0xffff - } - hr = syscall.Errno(r0) - } - return -} - -func CimMountVerifiedImage(imagePath string, fsName string, flags uint32, volumeID *g, hashSize uint16, hash *byte) (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 _CimMountVerifiedImage(_p0, _p1, flags, volumeID, hashSize, hash) -} - -func _CimMountVerifiedImage(imagePath *uint16, fsName *uint16, flags uint32, volumeID *g, hashSize uint16, hash *byte) (hr error) { - hr = procCimMountVerifiedImage.Find() - if hr != nil { - return - } - r0, _, _ := syscall.SyscallN(procCimMountVerifiedImage.Addr(), uintptr(unsafe.Pointer(imagePath)), uintptr(unsafe.Pointer(fsName)), uintptr(flags), uintptr(unsafe.Pointer(volumeID)), uintptr(hashSize), uintptr(unsafe.Pointer(hash))) - if int32(r0) < 0 { - if r0&0x1fff0000 == 0x00070000 { - r0 &= 0xffff - } - hr = syscall.Errno(r0) - } - return -} - -func CimSealImage(blockCimPath string, hashSize *uint64, fixedHeaderSize *uint64, hash *byte) (hr error) { - var _p0 *uint16 - _p0, hr = syscall.UTF16PtrFromString(blockCimPath) - if hr != nil { - return - } - return _CimSealImage(_p0, hashSize, fixedHeaderSize, hash) -} - -func _CimSealImage(blockCimPath *uint16, hashSize *uint64, fixedHeaderSize *uint64, hash *byte) (hr error) { - hr = procCimSealImage.Find() - if hr != nil { - return - } - r0, _, _ := syscall.SyscallN(procCimSealImage.Addr(), uintptr(unsafe.Pointer(blockCimPath)), uintptr(unsafe.Pointer(hashSize)), uintptr(unsafe.Pointer(fixedHeaderSize)), uintptr(unsafe.Pointer(hash))) - if int32(r0) < 0 { - if r0&0x1fff0000 == 0x00070000 { - r0 &= 0xffff - } - hr = syscall.Errno(r0) - } - return -} - -func CimTombstoneFile(cimFSHandle FsHandle, path string) (hr error) { - var _p0 *uint16 - _p0, hr = syscall.UTF16PtrFromString(path) - if hr != nil { - return - } - return _CimTombstoneFile(cimFSHandle, _p0) -} - -func _CimTombstoneFile(cimFSHandle FsHandle, path *uint16) (hr error) { - hr = procCimTombstoneFile.Find() - if hr != nil { - return - } - r0, _, _ := syscall.SyscallN(procCimTombstoneFile.Addr(), uintptr(cimFSHandle), uintptr(unsafe.Pointer(path))) - 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.SyscallN(procCimWriteStream.Addr(), 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.SyscallN(procSetJobCompartmentId.Addr(), uintptr(handle), uintptr(compartmentId)) if r0 != 0 { @@ -823,6 +392,20 @@ func NtCreateJobObject(jobHandle *windows.Handle, desiredAccess uint32, objAttri return } +func NtFsControlFile(file windows.Handle, event windows.Handle, apcRoutine uintptr, apcCtx uintptr, iosb *IOStatusBlock, fsControlCode uint32, in []byte, out []byte) (status uint32) { + var _p0 *byte + if len(in) > 0 { + _p0 = &in[0] + } + var _p1 *byte + if len(out) > 0 { + _p1 = &out[0] + } + r0, _, _ := syscall.SyscallN(procNtFsControlFile.Addr(), uintptr(file), uintptr(event), uintptr(apcRoutine), uintptr(apcCtx), uintptr(unsafe.Pointer(iosb)), uintptr(fsControlCode), uintptr(unsafe.Pointer(_p0)), uintptr(len(in)), uintptr(unsafe.Pointer(_p1)), uintptr(len(out))) + status = uint32(r0) + return +} + func NtOpenDirectoryObject(handle *uintptr, accessMask uint32, oa *ObjectAttributes) (status uint32) { r0, _, _ := syscall.SyscallN(procNtOpenDirectoryObject.Addr(), uintptr(unsafe.Pointer(handle)), uintptr(accessMask), uintptr(unsafe.Pointer(oa))) status = uint32(r0) diff --git a/vendor/github.com/Microsoft/hcsshim/pkg/cimfs/cim_writer_windows.go b/vendor/github.com/Microsoft/hcsshim/pkg/cimfs/cim_writer_windows.go index 62e45841f7b1e..0117b9f139587 100644 --- a/vendor/github.com/Microsoft/hcsshim/pkg/cimfs/cim_writer_windows.go +++ b/vendor/github.com/Microsoft/hcsshim/pkg/cimfs/cim_writer_windows.go @@ -14,6 +14,7 @@ import ( "github.com/Microsoft/go-winio" "github.com/Microsoft/hcsshim/internal/log" "github.com/Microsoft/hcsshim/internal/winapi" + winapitypes "github.com/Microsoft/hcsshim/internal/winapi/types" "github.com/sirupsen/logrus" "golang.org/x/sys/windows" ) @@ -25,11 +26,11 @@ 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 + handle winapitypes.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 + activeStream winapitypes.StreamHandle // amount of bytes that can be written to the activeStream. activeLeft uint64 // if true the CIM will be sealed after the writer is closed. @@ -58,7 +59,7 @@ func Create(imagePath string, oldFSName string, newFSName string) (_ *CimFsWrite return nil, err } } - var handle winapi.FsHandle + var handle winapitypes.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) } @@ -117,11 +118,11 @@ func CreateBlockCIMWithOptions(ctx context.Context, bCIM *BlockCIM, options ...B } // Check OS support - if !IsBlockCimSupported() { + if !IsBlockCimWriteSupported() { return nil, fmt.Errorf("block CIM not supported on this OS version") } - if config.dataIntegrity && !IsVerifiedCimSupported() { + if config.dataIntegrity && !IsVerifiedCimWriteSupported() { return nil, fmt.Errorf("verified CIMs are not supported on this OS version") } @@ -143,13 +144,15 @@ func CreateBlockCIMWithOptions(ctx context.Context, bCIM *BlockCIM, options ...B return nil, fmt.Errorf("invalid block CIM type `%d`: %w", bCIM.Type, os.ErrInvalid) } + winapi.LogCimDLLSupport() + var newNameUTF16 *uint16 newNameUTF16, err = windows.UTF16PtrFromString(bCIM.CimName) if err != nil { return nil, err } - var handle winapi.FsHandle + var handle winapitypes.FsHandle if err := winapi.CimCreateImage2(bCIM.BlockPath, createFlags, nil, newNameUTF16, &handle); err != nil { return nil, fmt.Errorf("failed to create block CIM at path %s,%s: %w", bCIM.BlockPath, bCIM.CimName, err) } @@ -216,7 +219,7 @@ func (c *CimFsWriter) AddFile(path string, info *winio.FileBasicInfo, fileSize i if err != nil { return err } - fileMetadata := &winapi.CimFsFileMetadata{ + fileMetadata := &winapitypes.CimFsFileMetadata{ Attributes: info.FileAttributes, FileSize: fileSize, CreationTime: info.CreationTime, @@ -346,7 +349,7 @@ func (c *CimFsWriter) Close() (err error) { } if c.sealOnClose { if err = sealBlockCIM(filepath.Dir(c.name)); err != nil { - return &OpError{Cim: c.name, Op: "seal", Err: err} + return &OpError{Cim: filepath.Dir(c.name), Op: "seal", Err: err} } } return nil @@ -433,7 +436,7 @@ func GetCimUsage(ctx context.Context, cimPath string) (uint64, error) { // files with the same path at all other CIMs) When mounting this merged CIM the source // CIMs MUST be provided in the exact same order. func MergeBlockCIMsWithOpts(ctx context.Context, mergedCIM *BlockCIM, sourceCIMs []*BlockCIM, opts ...BlockCIMOpt) (err error) { - if !IsMergedCimSupported() { + if !IsMergedCimWriteSupported() { return fmt.Errorf("merged CIMs aren't supported on this OS version") } else if len(sourceCIMs) < 2 { return fmt.Errorf("need at least 2 source CIMs, got %d: %w", len(sourceCIMs), os.ErrInvalid) @@ -506,6 +509,10 @@ func MergeBlockCIMs(mergedCIM *BlockCIM, sourceCIMs []*BlockCIM) (err error) { func sealBlockCIM(blockPath string) error { var hashSize, fixedHeaderSize uint64 hashBuf := make([]byte, cimHashSize) + + // the blockPath could be a path to a block device or a file. In either case there should be no trailing backslash. + blockPath = strings.TrimSuffix(blockPath, "\\") + if err := winapi.CimSealImage(blockPath, &hashSize, &fixedHeaderSize, &hashBuf[0]); err != nil { return fmt.Errorf("failed to seal block CIM: %w", err) } else if hashSize != cimHashSize { diff --git a/vendor/github.com/Microsoft/hcsshim/pkg/cimfs/cimfs.go b/vendor/github.com/Microsoft/hcsshim/pkg/cimfs/cimfs.go index 3fb95df011d3f..beae1f289bbc1 100644 --- a/vendor/github.com/Microsoft/hcsshim/pkg/cimfs/cimfs.go +++ b/vendor/github.com/Microsoft/hcsshim/pkg/cimfs/cimfs.go @@ -4,6 +4,9 @@ package cimfs import ( + "github.com/Microsoft/hcsshim/internal/winapi/cimfs" + "github.com/Microsoft/hcsshim/internal/winapi/cimwriter" + "path/filepath" "github.com/Microsoft/hcsshim/osversion" @@ -18,7 +21,7 @@ func IsCimFSSupported() bool { build := osversion.Build() // CimFS support is backported to LTSC2022 starting with revision 2031 and should // otherwise be available on all builds >= V25H1Server - return build >= osversion.V25H1Server || (build == osversion.V21H2Server && rv >= 2031) + return (build >= osversion.V25H1Server || (build == osversion.V21H2Server && rv >= 2031)) && cimfs.Supported() } // IsBlockCimSupported returns true if block formatted CIMs (i.e block device CIM & @@ -28,7 +31,25 @@ func IsBlockCimSupported() bool { // TODO(ambarve): Currently we are checking against a higher build number since there is no // official build with block CIM support yet. Once we have that build, we should // update the build number here. - return build >= 27766 + return build >= 27766 && cimfs.Supported() +} + +// IsBlockCimWriteSupported returns true if block formatted CIMs (i.e block device CIM & +// single file CIM) are supported on the current OS build or if CimWriter is present. +func IsBlockCimWriteSupported() bool { + // TODO(ambarve): Currently we are checking against a higher build number since there is no + // official build with block CIM support yet. Once we have that build, we should + // update the build number here. + return IsBlockCimSupported() || cimwriter.Supported() +} + +// IsBlockCimMountSupported returns true if block formatted CIMs (i.e block device CIM & +// single file CIM) are supported on the current OS build. +func IsBlockCimMountSupported() bool { + // TODO(ambarve): Currently we are checking against a higher build number since there is no + // official build with block CIM support yet. Once we have that build, we should + // update the build number here. + return IsBlockCimSupported() } // IsVerifiedCimSupported returns true if block CIM format supports also writing verification information in the CIM. @@ -37,7 +58,23 @@ func IsVerifiedCimSupported() bool { // TODO(ambarve): Currently we are checking against a higher build number since there is no // official build with block CIM support yet. Once we have that build, we should // update the build number here. - return build >= 27800 + return build >= 27800 && cimfs.Supported() +} + +// IsVerifiedCimWriteSupported returns true if block CIM format supports also writing verification information in the CIM. +func IsVerifiedCimWriteSupported() bool { + // TODO(ambarve): Currently we are checking against a higher build number since there is no + // official build with block CIM support yet. Once we have that build, we should + // update the build number here. + return IsVerifiedCimSupported() || cimwriter.Supported() +} + +// IsVerifiedCimMountSupported returns true if block CIM format supports mounting. +func IsVerifiedCimMountSupported() bool { + // TODO(ambarve): Currently we are checking against a higher build number since there is no + // official build with block CIM support yet. Once we have that build, we should + // update the build number here. + return IsVerifiedCimSupported() } func IsMergedCimSupported() bool { @@ -47,6 +84,20 @@ func IsMergedCimSupported() bool { return IsBlockCimSupported() } +func IsMergedCimWriteSupported() bool { + // The merged CIM support was originally added before block CIM support. However, + // some of the merged CIM features that we use (e.g. merged hard links) were added + // later along with block CIM support. So use the same check as block CIM here. + return IsBlockCimWriteSupported() +} + +func IsMergedCimMountSupported() bool { + // The merged CIM support was originally added before block CIM support. However, + // some of the merged CIM features that we use (e.g. merged hard links) were added + // later along with block CIM support. So use the same check as block CIM here. + return IsBlockCimMountSupported() +} + type BlockCIMType uint32 const ( diff --git a/vendor/github.com/Microsoft/hcsshim/pkg/cimfs/mount_cim.go b/vendor/github.com/Microsoft/hcsshim/pkg/cimfs/mount_cim.go index e9c305f60462c..bfb5e753e03f2 100644 --- a/vendor/github.com/Microsoft/hcsshim/pkg/cimfs/mount_cim.go +++ b/vendor/github.com/Microsoft/hcsshim/pkg/cimfs/mount_cim.go @@ -11,6 +11,7 @@ import ( "github.com/Microsoft/go-winio/pkg/guid" "github.com/Microsoft/hcsshim/internal/winapi" + winapitypes "github.com/Microsoft/hcsshim/internal/winapi/types" "github.com/pkg/errors" "golang.org/x/sys/windows" ) @@ -31,13 +32,17 @@ func (e *MountError) Error() string { return s } +const ( + VolumePathFormat = "\\\\?\\Volume{%s}\\" +) + // Mount mounts the given cim at a volume with given GUID. Returns the full volume // path if mount is successful. func Mount(cimPath string, volumeGUID guid.GUID, mountFlags uint32) (string, error) { if err := winapi.CimMountImage(filepath.Dir(cimPath), filepath.Base(cimPath), mountFlags, &volumeGUID); err != nil { return "", &MountError{Cim: cimPath, Op: "Mount", VolumeGUID: volumeGUID, Err: err} } - return fmt.Sprintf("\\\\?\\Volume{%s}\\", volumeGUID.String()), nil + return fmt.Sprintf(VolumePathFormat, volumeGUID.String()), nil } // Unmount unmounts the cim at mounted at path `volumePath`. @@ -70,7 +75,7 @@ func Unmount(volumePath string) error { // `MergeBlockCIMs`) at a volume with given GUID. The `sourceCIMs` MUST be identical // to the `sourceCIMs` passed to `MergeBlockCIMs` when creating this merged CIM. func MountMergedBlockCIMs(mergedCIM *BlockCIM, sourceCIMs []*BlockCIM, mountFlags uint32, volumeGUID guid.GUID) (string, error) { - if !IsMergedCimSupported() { + if !IsMergedCimMountSupported() { return "", fmt.Errorf("merged CIMs aren't supported on this OS version") } else if len(sourceCIMs) < 2 { return "", fmt.Errorf("need at least 2 source CIMs, got %d: %w", len(sourceCIMs), os.ErrInvalid) @@ -95,7 +100,7 @@ func MountMergedBlockCIMs(mergedCIM *BlockCIM, sourceCIMs []*BlockCIM, mountFlag // should be the merged CIM. All remaining entries should be the source CIM paths // in the same order that was used while creating the merged CIM. allcims := append([]*BlockCIM{mergedCIM}, sourceCIMs...) - cimsToMerge := []winapi.CimFsImagePath{} + cimsToMerge := []winapitypes.CimFsImagePath{} for _, bcim := range allcims { // Trailing backslashes cause problems-remove those imageDir, err := windows.UTF16PtrFromString(strings.TrimRight(bcim.BlockPath, `\`)) @@ -107,7 +112,7 @@ func MountMergedBlockCIMs(mergedCIM *BlockCIM, sourceCIMs []*BlockCIM, mountFlag return "", fmt.Errorf("convert string to utf16: %w", err) } - cimsToMerge = append(cimsToMerge, winapi.CimFsImagePath{ + cimsToMerge = append(cimsToMerge, winapitypes.CimFsImagePath{ ImageDir: imageDir, ImageName: cimName, }) @@ -116,7 +121,7 @@ func MountMergedBlockCIMs(mergedCIM *BlockCIM, sourceCIMs []*BlockCIM, mountFlag if err := winapi.CimMergeMountImage(uint32(len(cimsToMerge)), &cimsToMerge[0], mountFlags, &volumeGUID); err != nil { return "", &MountError{Cim: filepath.Join(mergedCIM.BlockPath, mergedCIM.CimName), Op: "MountMerged", Err: err} } - return fmt.Sprintf("\\\\?\\Volume{%s}\\", volumeGUID.String()), nil + return fmt.Sprintf(VolumePathFormat, volumeGUID.String()), nil } // Mounts a verified block CIM with the provided root hash. The root hash is usually @@ -147,3 +152,63 @@ func MountVerifiedBlockCIM(bCIM *BlockCIM, mountFlags uint32, volumeGUID guid.GU } return fmt.Sprintf("\\\\?\\Volume{%s}\\", volumeGUID.String()), nil } + +// MountMergedVerifiedBlockCIMs mounts the given merged verified BlockCIM (usually created +// with `MergeBlockCIMs`) at a volume with given GUID, with the given root hash. The +// `sourceCIMs` MUST be identical to the `sourceCIMs` passed to `MergeBlockCIMs` when +// creating this merged CIM. The root hash is usually returned when the CIM is sealed or +// the root hash can be queried from a block CIM. In case of merged CIMs, the root hash of +// the merged CIM should be passed here. Every read on the mounted volume will be verified +// to match against the provided root hash if it doesn't, the read will fail. The source +// CIMs and the merged CIM MUST have been created with the verified creation flag. +func MountMergedVerifiedBlockCIMs(mergedCIM *BlockCIM, sourceCIMs []*BlockCIM, mountFlags uint32, volumeGUID guid.GUID, rootHash []byte) (string, error) { + if !IsVerifiedCimMountSupported() { + return "", fmt.Errorf("verified CIMs aren't supported on this OS version") + } else if len(sourceCIMs) < 2 { + return "", fmt.Errorf("need at least 2 source CIMs, got %d: %w", len(sourceCIMs), os.ErrInvalid) + } else if len(rootHash) != cimHashSize { + return "", fmt.Errorf("unexpected root hash size %d, expected size is %d", len(rootHash), cimHashSize) + } + + switch mergedCIM.Type { + case BlockCIMTypeDevice: + mountFlags |= CimMountBlockDeviceCim + case BlockCIMTypeSingleFile: + mountFlags |= CimMountSingleFileCim + default: + return "", fmt.Errorf("invalid block CIM type `%d`", mergedCIM.Type) + } + + for _, sCIM := range sourceCIMs { + if sCIM.Type != mergedCIM.Type { + return "", fmt.Errorf("source CIM (%s) type doesn't match with merged CIM type: %w", sCIM.String(), os.ErrInvalid) + } + } + + // win32 mount merged CIM API expects an array of all CIMs. 0th entry in the array + // should be the merged CIM. All remaining entries should be the source CIM paths + // in the same order that was used while creating the merged CIM. + allcims := append([]*BlockCIM{mergedCIM}, sourceCIMs...) + cimsToMerge := []winapitypes.CimFsImagePath{} + for _, bcim := range allcims { + // Trailing backslashes cause problems-remove those + imageDir, err := windows.UTF16PtrFromString(strings.TrimRight(bcim.BlockPath, `\`)) + if err != nil { + return "", fmt.Errorf("convert string to utf16: %w", err) + } + cimName, err := windows.UTF16PtrFromString(bcim.CimName) + if err != nil { + return "", fmt.Errorf("convert string to utf16: %w", err) + } + + cimsToMerge = append(cimsToMerge, winapitypes.CimFsImagePath{ + ImageDir: imageDir, + ImageName: cimName, + }) + } + + if err := winapi.CimMergeMountVerifiedImage(uint32(len(cimsToMerge)), &cimsToMerge[0], mountFlags, &volumeGUID, cimHashSize, &rootHash[0]); err != nil { + return "", &MountError{Cim: filepath.Join(mergedCIM.BlockPath, mergedCIM.CimName), Op: "MountMergedVerified", Err: err} + } + return fmt.Sprintf(VolumePathFormat, volumeGUID.String()), nil +} diff --git a/vendor/github.com/Microsoft/hcsshim/pkg/ociwclayer/cim/import.go b/vendor/github.com/Microsoft/hcsshim/pkg/ociwclayer/cim/import.go index a16326ada2151..5816bc737edde 100644 --- a/vendor/github.com/Microsoft/hcsshim/pkg/ociwclayer/cim/import.go +++ b/vendor/github.com/Microsoft/hcsshim/pkg/ociwclayer/cim/import.go @@ -7,7 +7,7 @@ import ( "archive/tar" "bufio" "context" - "encoding/base64" + "encoding/hex" "errors" "fmt" "io" @@ -96,28 +96,33 @@ func WithParentLayers(parentLayers []*cimfs.BlockCIM) BlockCIMLayerImportOpt { } } -func writeIntegrityChecksumInfoFile(ctx context.Context, blockPath string) error { +func GetIntegrityChecksum(ctx context.Context, blockPath string, pathName string) (string, error) { log.G(ctx).Debugf("writing integrity checksum file for block CIM `%s`", blockPath) - // for convenience write a file that has the base64 encoded root digest of the generated verified CIM. - // this same base64 string can be used in the confidential policy. + // for convenience write a file that has the hex encoded root digest of the generated verified CIM. + // this same hex string can be used in the confidential policy. + // also return the integrity checksum as a string for integrity-vhd tooling. digest, err := cimfs.GetVerificationInfo(blockPath) if err != nil { - return fmt.Errorf("failed to query verified info of the CIM layer: %w", err) + return "", fmt.Errorf("failed to query verified info of the CIM layer: %w", err) } - digestFile, err := os.Create(filepath.Join(filepath.Dir(blockPath), "integrity_checksum")) - if err != nil { - return fmt.Errorf("failed to create verification info file: %w", err) - } - defer digestFile.Close() + digestStr := hex.EncodeToString(digest) - digestStr := base64.URLEncoding.EncodeToString(digest) - if wn, err := digestFile.WriteString(digestStr); err != nil { - return fmt.Errorf("failed to write verification info: %w", err) - } else if wn != len(digestStr) { - return fmt.Errorf("incomplete write of verification info: %w", err) + // only create a file if a path name is provided + if pathName != "" { + digestFile, err := os.Create(filepath.Join(filepath.Dir(blockPath), pathName)) + if err != nil { + return "", fmt.Errorf("failed to create verification info file: %w", err) + } + defer digestFile.Close() + + if wn, err := digestFile.WriteString(digestStr); err != nil { + return "", fmt.Errorf("failed to write verification info: %w", err) + } else if wn != len(digestStr) { + return "", fmt.Errorf("incomplete write of verification info: %w", err) + } } - return nil + return digestStr, nil } func ImportBlockCIMLayerWithOpts(ctx context.Context, r io.Reader, layer *cimfs.BlockCIM, opts ...BlockCIMLayerImportOpt) (_ int64, err error) { @@ -164,7 +169,7 @@ func ImportBlockCIMLayerWithOpts(ctx context.Context, r io.Reader, layer *cimfs. } if config.dataIntegrity { - if err = writeIntegrityChecksumInfoFile(ctx, layer.BlockPath); err != nil { + if _, err = GetIntegrityChecksum(ctx, layer.BlockPath, "integrity_checksum"); err != nil { return 0, err } } @@ -358,5 +363,10 @@ func MergeBlockCIMLayersWithOpts(ctx context.Context, sourceCIMs []*cimfs.BlockC return fmt.Errorf("append VHD footer to block CIM: %w", err) } } + if config.dataIntegrity { + if _, err = GetIntegrityChecksum(ctx, mergedCIM.BlockPath, "merged_integrity_checksum"); err != nil { + return err + } + } return nil } diff --git a/vendor/github.com/erofs/go-erofs/.golangci.yml b/vendor/github.com/erofs/go-erofs/.golangci.yml new file mode 100644 index 0000000000000..d4f8fd09d5cf0 --- /dev/null +++ b/vendor/github.com/erofs/go-erofs/.golangci.yml @@ -0,0 +1,25 @@ +version: "2" + +run: + timeout: 5m + +linters: + enable: + - misspell + - gocritic + - revive + - unconvert + - unparam + settings: + revive: + rules: + - name: exported + disabled: true + +formatters: + enable: + - goimports + settings: + goimports: + local-prefixes: + - github.com/erofs/go-erofs diff --git a/vendor/github.com/erofs/go-erofs/LICENSE b/vendor/github.com/erofs/go-erofs/LICENSE new file mode 100644 index 0000000000000..be4c94b186aae --- /dev/null +++ b/vendor/github.com/erofs/go-erofs/LICENSE @@ -0,0 +1,191 @@ + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright The go-erofs Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/erofs/go-erofs/README.md b/vendor/github.com/erofs/go-erofs/README.md new file mode 100644 index 0000000000000..430a8aaec891e --- /dev/null +++ b/vendor/github.com/erofs/go-erofs/README.md @@ -0,0 +1,56 @@ +# go-erofs + +A Go library for opening erofs files as a Go stdlib [fs.FS](https://pkg.go.dev/io/fs#FS). + +## Scope + +This library is designed to allow erofs files to be usable in any Go operation that uses +the standard filesystem interface. This could be useful for accessing an erofs file just +as you would a plain directory without needing to unpack. In the future this library +could provide an interface to create erofs files as well. + +## Current state + +- [x] Read erofs files created with default `mkfs.erofs` options +- [x] Read chunk-based erofs files (without indexes) +- [x] Xattr support +- [x] Long xattr prefix support +- [x] Extra devices for chunked data and chunk indexes +- [ ] Read erofs files with compression +- [ ] Creating erofs files +- [ ] Tar to erofs conversion + +## Example use + +Print out all the files in an erofs file + +``` +package main + +import ( + "fmt" + "io/fs" + "log" + "os" + + "github.com/erofs/go-erofs" +) + +func main() { + f, err := os.Open("testdata/basic-default.erofs") + if err != nil { + log.Fatal(err) + } + defer f.Close() + + img, err := erofs.EroFS(f) + if err != nil { + log.Fatal(err) + } + + fs.WalkDir(img, "/", func(path string, entry fs.DirEntry, err error) error { + fmt.Println(path) + return nil + }) +} +``` diff --git a/vendor/github.com/erofs/go-erofs/block.go b/vendor/github.com/erofs/go-erofs/block.go new file mode 100644 index 0000000000000..cea7da84a5d75 --- /dev/null +++ b/vendor/github.com/erofs/go-erofs/block.go @@ -0,0 +1,22 @@ +package erofs + +type block struct { + buf []byte + offset int32 + end int32 +} + +func (b *block) bytes() []byte { + if b.buf == nil || b.offset == -1 { + return nil + } + return b.buf[b.offset:b.end] +} + +func calculateBlocks(blockBits uint8, size int64) int { + blockNum := size >> blockBits + if size > blockNum< 16 { + return nil, fmt.Errorf("unsupported block size bits %d: %w", i.sb.BlkSizeBits, ErrInvalidSuperblock) + } + unknownFeat := i.sb.FeatureIncompat &^ disk.FeatureIncompatAll + if unknownFeat != 0 { + return nil, fmt.Errorf("unsupported incompatible feature 0x%x: %w", unknownFeat, ErrNotImplemented) + } + ondiskExtraDevices := uint32(0) + if i.sb.FeatureIncompat&disk.FeatureIncompatDeviceTable != 0 { + ondiskExtraDevices = uint32(i.sb.ExtraDevices) + // Calculate device_id_mask + // sbi->device_id_mask = roundup_pow_of_two(ondisk_extradevs + 1) - 1; + i.deviceIDMask = uint16(roundupPowerOfTwo(uint32(i.sb.ExtraDevices)+1) - 1) + } + + if int(ondiskExtraDevices) != len(o.extraDevices) { + // TODO: Provide options for skipping extra devices and error out later? + return nil, fmt.Errorf("invalid super block: extra devices count %d does not match provided %d", ondiskExtraDevices, len(o.extraDevices)) + } + + // Parse the device table if extra devices exist + if ondiskExtraDevices > 0 { + devTableOffset := int64(i.sb.DevtSlotOff) * disk.SizeDeviceSlot + i.devices = make([]deviceInfo, int(ondiskExtraDevices)) + for idx := range i.devices { + var slotBuf [disk.SizeDeviceSlot]byte + offset := devTableOffset + int64(idx)*disk.SizeDeviceSlot + if _, err := r.ReadAt(slotBuf[:], offset); err != nil { + return nil, fmt.Errorf("failed to read device slot %d at offset %d: %w", idx, offset, err) + } + var slot disk.DeviceSlot + if _, err := binary.Decode(slotBuf[:], binary.LittleEndian, &slot); err != nil { + return nil, fmt.Errorf("failed to decode device slot %d: %w", idx, err) + } + i.devices[idx] = deviceInfo{ + device: o.extraDevices[idx], + mappedBlkAddr: slot.MappedBlkAddr, + blocks: slot.Blocks, + } + } + } + + // Error out filesystems with unsupported compressed inodes + if i.sb.FeatureIncompat&disk.FeatureIncompatLZ4_0Padding != 0 || + i.sb.ComprAlgs != 0 { + return nil, fmt.Errorf("unsupported compressed filesystem (FeatureIncompat=0x%x, ComprAlgs=0x%x): %w", + i.sb.FeatureIncompat, i.sb.ComprAlgs, ErrNotImplemented) + } + + i.blkPool.New = func() any { + return &block{ + buf: make([]byte, 1<> 1 + v |= v >> 2 + v |= v >> 4 + v |= v >> 8 + v |= v >> 16 + v++ + return v +} + +// deviceInfo holds the parsed mapped address range for a device table entry. +type deviceInfo struct { + device io.ReaderAt + mappedBlkAddr uint32 // starting mapped block address + blocks uint32 // total block count for this device +} + +type image struct { + sb disk.SuperBlock + + meta io.ReaderAt + devices []deviceInfo // parsed device table entries + deviceIDMask uint16 + blkPool sync.Pool + longPrefixes []string // cached long xattr prefixes + prefixesOnce sync.Once + prefixesErr error +} + +// start physical offset of the separate metadata zone +func (img *image) metaStartPos() int64 { + return int64(img.sb.MetaBlkAddr) << int64(img.sb.BlkSizeBits) +} + +// mapDev resolves map->m_bdev and map->m_pa mapping for go-erofs. +// It works similarly to erofs_map_dev in the linux kernel. +func (img *image) mapDev(deviceID uint16, pa int64) (io.ReaderAt, int64, error) { + if deviceID > 0 { + if int(deviceID) > len(img.devices) { + return nil, 0, fmt.Errorf("invalid device id %d", deviceID) + } + return img.devices[deviceID-1].device, pa, nil + } + + if len(img.devices) > 0 { + for _, dev := range img.devices { + if dev.mappedBlkAddr == 0 { + continue + } + + startOff := int64(dev.mappedBlkAddr) << img.sb.BlkSizeBits + length := int64(dev.blocks) << img.sb.BlkSizeBits + + if pa >= startOff && pa < startOff+length { + return dev.device, pa - startOff, nil + } + } + } + + return img.meta, pa, nil +} + +func (img *image) readMetadata(r io.Reader) ([]byte, error) { + // - A 2-byte little-endian length field, which is aligned to a 4-byte boundary + // - The length bytes of payload data + var lenBuf [2]byte + if _, err := io.ReadFull(r, lenBuf[:]); err != nil { + return nil, fmt.Errorf("failed to read metadata length %v: %w", lenBuf, err) + } + + dataLen := int(binary.LittleEndian.Uint16(lenBuf[:])) + if dataLen < 1 { + dataLen = 65536 + } + + data := make([]byte, dataLen) + if _, err := io.ReadFull(r, data); err != nil { + return nil, fmt.Errorf("failed to read metadata payload: %w", err) + } + + // Align to 4-byte boundary except for hitting EOF + totalLen := 2 + dataLen + if rem := totalLen % 4; rem != 0 { + padding := int64(4 - rem) + if _, err := io.CopyN(io.Discard, r, padding); err != nil && + !errors.Is(err, io.EOF) && + !errors.Is(err, io.ErrUnexpectedEOF) { + return nil, fmt.Errorf("failed to discard padding of %d bytes: %w", padding, err) + } + } + return data, nil +} + +// loadLongPrefixes loads and caches the long xattr prefixes from the packed inode +// using the regular inode read logic to handle compressed/non-inline data. +// +// Long xattr name prefixes are used to optimize storage of xattrs with common +// prefixes. They are stored sequentially in a special "packed inode" or +// "meta inode". +// See: https://docs.kernel.org/filesystems/erofs.html#extended-attributes +func (img *image) loadLongPrefixes() error { + img.prefixesOnce.Do(func() { + if img.sb.XattrPrefixCount == 0 { + return + } + + var r io.Reader + + // Calculate the starting offset. XattrPrefixStart is defined in the + // superblock as being in units of 4 bytes from the start of the corresponding inode + startOffset := int64(img.sb.XattrPrefixStart) * 4 + + if (img.sb.FeatureIncompat&disk.FeatureIncompatFragments != 0) && img.sb.PackedNid > 0 { + // The packed inode (identified by PackedNid in the superblock) is a special + // inode used for shared data and metadata. + // We use ".packed" as a descriptive name for this internal inode. + f := &file{ + img: img, + name: ".packed", + nid: img.sb.PackedNid, + ftype: 0, // regular file + } + + // Read inode info to determine size and layout + fi, err := f.readInfo(false) + if err != nil { + img.prefixesErr = fmt.Errorf("failed to read packed inode: %w", err) + return + } + + if startOffset > fi.size { + img.prefixesErr = fmt.Errorf("xattr prefix start offset %d exceeds packed inode size %d", startOffset, fi.size) + return + } + + // Set the read offset + f.offset = startOffset + r = bufio.NewReader(f) + } else { + // FIXME(hsiangkao): should avoid hacky 1<<32 here since we don't care about the end + r = io.NewSectionReader(img.meta, startOffset, 1<<32) + } + + img.longPrefixes = make([]string, img.sb.XattrPrefixCount) + for i := 0; i < int(img.sb.XattrPrefixCount); i++ { + data, err := img.readMetadata(r) + if err != nil { + img.prefixesErr = + fmt.Errorf("failed to read long xattr prefix %d: %w", i, err) + return + } + + // First byte is the base_index referencing a standard xattr prefix + baseIndex := xattrIndex(data[0]) + + // Remaining bytes are the infix to be appended to the base prefix + infix := string(data[1:]) + + // Construct full prefix: base prefix + infix + img.longPrefixes[i] = baseIndex.String() + infix + } + }) + return img.prefixesErr +} + +// getLongPrefix returns the long xattr prefix at the given index +func (img *image) getLongPrefix(index uint8) (string, error) { + if err := img.loadLongPrefixes(); err != nil { + return "", err + } + + if int(index) >= len(img.longPrefixes) { + return "", fmt.Errorf("long xattr prefix index %d out of range (max %d)", index, len(img.longPrefixes)-1) + } + + return img.longPrefixes[index], nil +} + +func (img *image) loadAt(addr, size int64) (*block, error) { + blkSize := int64(1 << img.sb.BlkSizeBits) + if size > blkSize { + size = blkSize + } + + b := img.getBlock() + if n, err := img.meta.ReadAt(b.buf[:size], addr); err != nil { + img.putBlock(b) + return nil, fmt.Errorf("failed to read %d bytes at %d: %w", size, addr, err) + } else { + b.offset = 0 + b.end = int32(n) + } + + return b, nil +} + +// loadBlock loads the block with the given data +func (img *image) loadBlock(fi *fileInfo, pos int64) (*block, error) { + nblocks := calculateBlocks(img.sb.BlkSizeBits, fi.size) + bn := int(pos >> int(img.sb.BlkSizeBits)) + if bn >= nblocks { + return nil, fmt.Errorf("block position larger than number of blocks for inode: %w", io.EOF) + } + var addr int64 + blockSize := int(1 << img.sb.BlkSizeBits) + blockOffset := 0 + blockEnd := blockSize + switch fi.inodeLayout { + case disk.LayoutFlatPlain: + // flat plain has no holes + addr = int64(int(fi.inodeData)+bn) << img.sb.BlkSizeBits + blockOffset = int(pos % int64(blockSize)) + if bn == nblocks-1 { + blockEnd = int(fi.size - int64(bn)*int64(1< blockSize { + return nil, fmt.Errorf("inline data cross block boundary for nid %d: %w", fi.nid, ErrInvalid) + } + } else { + addr = int64(int(fi.inodeData)+bn) << img.sb.BlkSizeBits + blockOffset = int(pos % int64(blockSize)) + } + case disk.LayoutChunkBased: + // first 2 le bytes for format, second 2 bytes are reserved + format := uint16(fi.inodeData) + if format&disk.LayoutChunkFormat48Bit != 0 { + return nil, fmt.Errorf("48-bit chunk format for nid %d: %w", fi.nid, ErrNotImplemented) + } + if format&^(disk.LayoutChunkFormatBits|disk.LayoutChunkFormatIndexes) != 0 { + return nil, fmt.Errorf("unsupported chunk format %x for nid %d: %w", format, fi.nid, ErrInvalid) + } + + chunkbits := img.sb.BlkSizeBits + uint8(format&disk.LayoutChunkFormatBits) + chunkn := int((fi.size-1)>>chunkbits) + 1 + cn := int(pos >> chunkbits) + + if cn >= chunkn { + return nil, fmt.Errorf("chunk format does not fit into allocated bytes for nid %d: %w", fi.nid, ErrInvalid) + } + + inodeStart := img.metaStartPos() + int64(fi.nid*disk.SizeInodeCompact) + baseOffset := inodeStart + fi.flatDataOffset() + + unit := 4 + if format&disk.LayoutChunkFormatIndexes == disk.LayoutChunkFormatIndexes { + unit = 8 + // Align to 8 bytes + if baseOffset%8 != 0 { + baseOffset = (baseOffset + 7) & ^int64(7) + } + } + + entryPos := baseOffset + int64(cn*unit) + var entryBuf [8]byte + if n, err := img.meta.ReadAt(entryBuf[:unit], entryPos); err != nil { + return nil, fmt.Errorf("failed to read chunk entry at %d: %w", entryPos, err) + } else if n != unit { + return nil, fmt.Errorf("short read of chunk entry at %d: read %d bytes, expected %d", entryPos, n, unit) + } + + var addr int64 + var deviceID uint16 + + if unit == 8 { + startBlkLo := binary.LittleEndian.Uint32(entryBuf[4:8]) + if ^startBlkLo == 0 { + addr = -1 + } else { + addr = int64(startBlkLo) << img.sb.BlkSizeBits + deviceID = binary.LittleEndian.Uint16(entryBuf[2:4]) & img.deviceIDMask + } + } else { + rawAddr := binary.LittleEndian.Uint32(entryBuf[:4]) + if ^rawAddr == 0 { + addr = -1 + } else { + addr = int64(rawAddr) << img.sb.BlkSizeBits + } + } + + if bn == nblocks-1 { + blockEnd = int(fi.size - int64(bn)*int64(1< 0 { + addr += (blockPos - int64(cn<= blockEnd { + return nil, fmt.Errorf("no remaining items in block: %w", io.EOF) + } + + b := img.getBlock() + b.offset = int32(blockOffset) + b.end = int32(blockEnd) + if n, err := img.meta.ReadAt(b.bytes(), addr+int64(blockOffset)); err != nil { + img.putBlock(b) + return nil, fmt.Errorf("failed to read block for nid %d: %w", fi.nid, err) + } else if n != blockEnd-blockOffset { + img.putBlock(b) + return nil, fmt.Errorf("failed to read full block for nid %d: %w, expected %d, actual %d", fi.nid, ErrInvalid, blockEnd-blockOffset, n) + } + return b, nil +} + +func (img *image) getBlock() *block { + return img.blkPool.Get().(*block) +} + +// putBlock returns a block after complete so its +// buffer can be put back into the buffer pool +func (img *image) putBlock(b *block) { + img.blkPool.Put(b) +} + +const maxSymlinks = 255 + +// readLink reads the symlink target for the given nid. +func (i *image) readLink(nid uint64, name string) (string, error) { + f := &file{img: i, name: name, nid: nid, ftype: fs.ModeSymlink} + fi, err := f.readInfo(false) + if err != nil { + return "", err + } + buf := make([]byte, fi.size) + if fi.size > 0 { + if _, err = f.Read(buf); err != nil && err != io.EOF { + return "", err + } + } + return string(buf), nil +} + +// resolve cleans the path and walks directory entries to find the target inode. +// When follow is true, symlinks are followed (including the final component). +// When follow is false, the final component is not followed (for Lstat/ReadLink). +// Intermediate symlinks are always followed. +func (i *image) resolve(op, name string, follow bool) (nid uint64, ftype fs.FileMode, basename string, err error) { + original := name + if path.IsAbs(name) { + name = name[1:] + } + name = path.Clean(name) + if name == "." { + name = "" + } + + nid = uint64(i.sb.RootNid) + ftype = fs.ModeDir + + // curPath tracks the full resolved path of the current directory + // so that relative symlink targets can be resolved correctly. + linksFollowed := 0 + curPath := "" + basename = name + for name != "" { + var sep int + for sep < len(name) && name[sep] != '/' { + sep++ + } + var rest string + if sep < len(name) { + basename = name[:sep] + rest = name[sep+1:] + } else { + basename = name + rest = "" + } + + if ftype != fs.ModeDir { + return 0, 0, "", &fs.PathError{Op: op, Path: original, Err: errors.New("not a directory")} + } + d := &dir{ + file: file{ + img: i, + name: basename, + nid: nid, + ftype: ftype, + }, + } + // TODO: Lookup in directory instead of reading all + entries, err := d.ReadDir(-1) + if err != nil { + return 0, 0, "", fmt.Errorf("failed to read dir: %w", err) + } + var found bool + for _, e := range entries { + if e.Name() == basename { + nid = e.(*direntry).nid + ftype = e.(*direntry).ftype & fs.ModeType + found = true + } + } + if !found { + return 0, 0, "", &fs.PathError{Op: op, Path: original, Err: fs.ErrNotExist} + } + + // Follow symlinks for intermediate components always, + // and for the final component only when follow is true. + isFinal := rest == "" + if ftype&fs.ModeSymlink != 0 && (follow || !isFinal) { + linksFollowed++ + if linksFollowed > maxSymlinks { + return 0, 0, "", &fs.PathError{Op: op, Path: original, Err: errors.New("too many symlinks")} + } + target, err := i.readLink(nid, basename) + if err != nil { + return 0, 0, "", err + } + // Prepend the symlink target to the remaining path + if rest != "" { + target = target + "/" + rest + } + // Resolve relative to the parent directory's full path + if !path.IsAbs(target) { + target = curPath + "/" + target + } + // Clean and re-resolve from root + target = path.Clean(target) + if len(target) > 0 && target[0] == '/' { + target = target[1:] + } + nid = uint64(i.sb.RootNid) + ftype = fs.ModeDir + curPath = "" + name = target + if name == "." { + name = "" + } + basename = name + continue + } + + if curPath == "" { + curPath = basename + } else { + curPath = curPath + "/" + basename + } + name = rest + } + + if basename == "" { + basename = original + } + return nid, ftype, basename, nil +} + +func (i *image) Open(name string) (fs.File, error) { + nid, ftype, basename, err := i.resolve("open", name, true) + if err != nil { + return nil, err + } + b := file{img: i, name: basename, nid: nid, ftype: ftype} + if ftype.IsDir() { + return &dir{file: b}, nil + } + return &b, nil +} + +func (i *image) Stat(name string) (fs.FileInfo, error) { + nid, ftype, basename, err := i.resolve("stat", name, true) + if err != nil { + return nil, err + } + f := &file{img: i, name: basename, nid: nid, ftype: ftype} + return f.readInfo(true) +} + +func (i *image) ReadFile(name string) ([]byte, error) { + nid, ftype, basename, err := i.resolve("readfile", name, true) + if err != nil { + return nil, err + } + if ftype.IsDir() { + return nil, &fs.PathError{Op: "read", Path: name, Err: errors.New("is a directory")} + } + f := &file{img: i, name: basename, nid: nid, ftype: ftype} + fi, err := f.readInfo(false) + if err != nil { + return nil, err + } + buf := make([]byte, fi.size) + if fi.size > 0 { + if _, err = f.Read(buf); err != nil && err != io.EOF { + return nil, err + } + } + return buf, nil +} + +func (i *image) ReadDir(name string) ([]fs.DirEntry, error) { + nid, ftype, basename, err := i.resolve("readdir", name, true) + if err != nil { + return nil, err + } + if !ftype.IsDir() { + return nil, &fs.PathError{Op: "readdir", Path: name, Err: errors.New("not a directory")} + } + d := &dir{file: file{img: i, name: basename, nid: nid, ftype: ftype}} + entries, err := d.ReadDir(-1) + if err != nil { + return nil, err + } + slices.SortFunc(entries, func(a, b fs.DirEntry) int { + return cmp.Compare(a.Name(), b.Name()) + }) + return entries, nil +} + +func (i *image) ReadLink(name string) (string, error) { + nid, ftype, basename, err := i.resolve("readlink", name, false) + if err != nil { + return "", err + } + if ftype&fs.ModeSymlink == 0 { + return "", &fs.PathError{Op: "readlink", Path: name, Err: errors.New("not a symlink")} + } + return i.readLink(nid, basename) +} + +func (i *image) Lstat(name string) (fs.FileInfo, error) { + nid, ftype, basename, err := i.resolve("lstat", name, false) + if err != nil { + return nil, err + } + f := &file{img: i, name: basename, nid: nid, ftype: ftype} + return f.readInfo(true) +} + +type file struct { + img *image + name string + nid uint64 + ftype fs.FileMode + + // Mutable fields, open file should not be accessed concurrently + offset int64 // current offset for read operations + info *fileInfo // cached fileInfo +} + +func (b *file) readInfo(infoOnly bool) (fi *fileInfo, err error) { + if b.info != nil { + return b.info, nil + } + + addr := b.img.metaStartPos() + int64(b.nid*disk.SizeInodeCompact) + blkSize := int32(1 << b.img.sb.BlkSizeBits) + blk := b.img.getBlock() + blk.offset = int32(addr & int64(blkSize-1)) + blk.end = blkSize + if blk.end-blk.offset < disk.SizeInodeExtended { + // Use buffer starting from beginning of inode, do not use the position + // in the block since an extended inode may span multiple blocks + blk.offset = 0 + blk.end = disk.SizeInodeExtended + } + + defer func() { + v := recover() + if v != nil { + err = fmt.Errorf("file format error: %v", v) + } + if err != nil { + b.img.putBlock(blk) + } + + }() + + ino := blk.bytes() + _, err = b.img.meta.ReadAt(ino, addr) + if err != nil { + return nil, err + } + + var format, xcnt uint16 + if _, err = binary.Decode(ino[:2], binary.LittleEndian, &format); err != nil { + return nil, err + } + + layout := uint8((format & 0x0E) >> 1) + if format&0x01 == 0 { + var inode disk.InodeCompact + if _, err := binary.Decode(ino[:disk.SizeInodeCompact], binary.LittleEndian, &inode); err != nil { + return nil, err + } + b.info = &fileInfo{ + name: b.name, + nid: b.nid, + icsize: disk.SizeInodeCompact, + inodeLayout: layout, + inodeData: inode.InodeData, + size: int64(inode.Size), + mode: (fs.FileMode(inode.Mode) & ^fs.ModeType) | b.ftype, + modTime: time.Unix(int64(b.img.sb.BuildTime), int64(b.img.sb.BuildTimeNs)), + } + xcnt = inode.XattrCount + if infoOnly { + b.info.stat = &Stat{ + Mode: disk.EroFSModeToGoFileMode(inode.Mode), + Size: int64(inode.Size), + InodeLayout: layout, + Inode: int64(b.nid), + Rdev: disk.RdevFromMode(inode.Mode, inode.InodeData), + UID: uint32(inode.UID), + GID: uint32(inode.GID), + Nlink: int(inode.Nlink), + Mtime: b.img.sb.BuildTime, + MtimeNs: b.img.sb.BuildTimeNs, + } + } + addr += disk.SizeInodeCompact + } else { + var inode disk.InodeExtended + if _, err = binary.Decode(ino[:disk.SizeInodeExtended], binary.LittleEndian, &inode); err != nil { + return nil, err + } + b.info = &fileInfo{ + name: b.name, + nid: b.nid, + icsize: disk.SizeInodeExtended, + inodeLayout: layout, + inodeData: inode.InodeData, + size: int64(inode.Size), + mode: (fs.FileMode(inode.Mode) & ^fs.ModeType) | b.ftype, + modTime: time.Unix(int64(inode.Mtime), int64(inode.MtimeNs)), + } + xcnt = inode.XattrCount + if infoOnly { + b.info.stat = &Stat{ + Mode: disk.EroFSModeToGoFileMode(inode.Mode), + Size: int64(inode.Size), + InodeLayout: layout, + Inode: int64(b.nid), + Rdev: disk.RdevFromMode(inode.Mode, inode.InodeData), + UID: inode.UID, + GID: inode.GID, + Nlink: int(inode.Nlink), + Mtime: inode.Mtime, + MtimeNs: inode.MtimeNs, + } + } + addr += disk.SizeInodeExtended + } + + if xcnt > 0 { + b.info.xsize = int(xcnt-1)*disk.SizeXattrEntry + disk.SizeXattrBodyHeader + } + + switch { + case infoOnly && b.info.xsize > 0: + if err = setXattrs(b, addr, blk); err != nil { + return nil, err + } + case infoOnly || b.info.inodeLayout == disk.LayoutFlatPlain || b.info.size == 0 || blk.end != blkSize: + b.img.putBlock(blk) + default: + // If the inode has trailing data used later, cache it + b.info.cached = blk + } + return b.info, nil +} + +func (b *file) Stat() (fs.FileInfo, error) { + return b.readInfo(true) +} + +func (b *file) Read(p []byte) (int, error) { + fi, err := b.readInfo(false) + if err != nil { + return 0, err + } + + var n int + for len(p) > 0 { + if b.offset >= fi.size { + return n, io.EOF + } + blk, err := b.img.loadBlock(fi, b.offset) + if err != nil { + if errors.Is(err, io.EOF) { + err = io.EOF + b.offset += int64(n) + } + return n, err + } + buf := blk.bytes() + copied := copy(p, buf) + n += copied + p = p[copied:] + b.offset += int64(copied) + + b.img.putBlock(blk) + } + return n, nil +} + +func (b *file) Close() error { + b.info.cached = nil + return nil +} + +type direntry struct { + file +} + +func (d *direntry) Name() string { + return d.name +} + +func (d *direntry) IsDir() bool { + return d.ftype.IsDir() +} + +func (d *direntry) Type() fs.FileMode { + return d.ftype +} + +func (d *direntry) Info() (fs.FileInfo, error) { + return d.readInfo(true) +} + +type dir struct { + file + + // bn is the current block to read from (relative to file start) + bn int + + // consumed is how many have been returned in the current block + consumed uint16 +} + +func (d *dir) ReadDir(n int) ([]fs.DirEntry, error) { + fi, err := d.readInfo(false) + if err != nil { + return nil, fmt.Errorf("readInfo failed: %w", err) + } + + var ents []fs.DirEntry + pos := int64(d.bn << d.img.sb.BlkSizeBits) + for pos < fi.size { + b, err := d.img.loadBlock(fi, pos) + if err != nil { + if errors.Is(err, io.EOF) { + return ents, nil + } + return nil, err + } + buf := b.bytes() + if len(buf) < 12 { + return ents, nil + } + + var dirents [2]disk.Dirent + + readN, err := binary.Decode(buf[:12], binary.LittleEndian, &dirents[0]) + if err != nil { + return nil, fmt.Errorf("decode failed: %w", err) + } + if readN != 12 { + return nil, errors.New("invalid dirent: not fully decoded") + } + + entryN := dirents[0].NameOff / disk.SizeDirent + + for i := uint16(0); i < entryN; i++ { + var name string + if i < entryN-1 { + start := 12 * (i + 1) + readN, err := binary.Decode(buf[start:start+12], binary.LittleEndian, &dirents[1]) + if err != nil { + return nil, fmt.Errorf("decode failed: %w", err) + } + if readN != 12 { + return nil, errors.New("invalid dirent: not fully decoded") + } + name = string(buf[dirents[0].NameOff:dirents[1].NameOff]) + } else { + name = string(buf[dirents[0].NameOff:]) + } + + if i >= d.consumed && name != "." && name != ".." { + b := file{ + img: d.img, + name: name, + nid: dirents[0].Nid, + ftype: disk.EroFSFtypeToFileMode(dirents[0].FileType), + } + ents = append(ents, &direntry{b}) + d.consumed = i + 1 + + if n > 0 && len(ents) == n { + if i == entryN-1 { + d.consumed = 0 + d.bn++ + } + return ents, nil + } + } + + // Rotate next to current + dirents[0] = dirents[1] + } + + d.consumed = 0 + d.bn++ + pos = int64(d.bn << d.img.sb.BlkSizeBits) + } + + return ents, nil +} + +type fileInfo struct { + name string + nid uint64 + icsize int8 + xsize int + inodeLayout uint8 + inodeData uint32 + size int64 + mode fs.FileMode + modTime time.Time + stat *Stat + cached *block +} + +func (fi *fileInfo) Name() string { + return fi.name +} + +func (fi *fileInfo) Size() int64 { + return fi.size +} + +func (fi *fileInfo) Mode() fs.FileMode { + return fi.mode +} +func (fi *fileInfo) ModTime() time.Time { + return fi.modTime +} + +func (fi *fileInfo) IsDir() bool { + return fi.mode.IsDir() +} + +func (fi *fileInfo) Sys() any { + // Return erofs stat object with extra fields and call for xattrs + return fi.stat +} + +func (fi *fileInfo) flatDataOffset() int64 { + // inode core size + xattr size + return int64(fi.icsize) + int64(fi.xsize) +} +func decodeSuperBlock(b [disk.SizeSuperBlock]byte, sb *disk.SuperBlock) error { + n, err := binary.Decode(b[:], binary.LittleEndian, sb) + if err != nil { + return err + } + if n != disk.SizeSuperBlock { + return fmt.Errorf("invalid super block: decoded %d bytes", n) + } + if sb.MagicNumber != disk.MagicNumber { + return fmt.Errorf("invalid super block: invalid magic number %x", sb.MagicNumber) + } + return nil +} diff --git a/vendor/github.com/erofs/go-erofs/internal/disk/ftypes.go b/vendor/github.com/erofs/go-erofs/internal/disk/ftypes.go new file mode 100644 index 0000000000000..1fdeaa6dd1f67 --- /dev/null +++ b/vendor/github.com/erofs/go-erofs/internal/disk/ftypes.go @@ -0,0 +1,88 @@ +package disk + +import "io/fs" + +const ( + FileTypeReg = 1 + FileTypeDir = 2 + FileTypeChrdev = 3 + FileTypeBlkdev = 4 + FileTypeFifo = 5 + FileTypeSock = 6 + FileTypeSymlink = 7 + + StatTypeMask = 0170000 // Mask for the type bits + StatTypeReg = 0100000 // Regular file + StatTypeDir = 0040000 // Directory + StatTypeChrdev = 0020000 // Character device + StatTypeBlkdev = 0060000 // Block device + StatTypeFifo = 0010000 // FIFO + StatTypeSock = 0140000 // Socket + StatTypeSymlink = 0120000 // Symlink + StatTypeIsUID = 0004000 // Setuid on execution + StatTypeIsGID = 0002000 // Setgid on execution + StatTypeIsVTX = 0001000 // Sticky bit +) + +// Converts EroFS filetypes to Go FileMode +func EroFSFtypeToFileMode(ftype uint8) fs.FileMode { + switch ftype { + case FileTypeDir: + return fs.ModeDir + case FileTypeChrdev: + return fs.ModeCharDevice + case FileTypeBlkdev: + return fs.ModeDevice + case FileTypeFifo: + return fs.ModeNamedPipe + case FileTypeSock: + return fs.ModeSocket + case FileTypeSymlink: + return fs.ModeSymlink + default: + return 0 + } +} + +func EroFSModeToGoFileMode(mode uint16) fs.FileMode { + var m fs.FileMode + m |= fs.FileMode(mode & 0777) + switch mode & StatTypeMask { + case StatTypeReg: + case StatTypeDir: + m |= fs.ModeDir + case StatTypeChrdev: + m |= fs.ModeCharDevice + case StatTypeBlkdev: + m |= fs.ModeDevice + case StatTypeFifo: + m |= fs.ModeNamedPipe + case StatTypeSock: + m |= fs.ModeSocket + case StatTypeSymlink: + m |= fs.ModeSymlink + default: + m |= fs.ModeIrregular // Unknown type, treat as irregular file + } + if mode&StatTypeIsUID != 0 { + m |= fs.ModeSetuid + } + if mode&StatTypeIsGID != 0 { + m |= fs.ModeSetgid + } + if mode&StatTypeIsVTX != 0 { + m |= fs.ModeSticky + } + + return m +} + +func RdevFromMode(mode uint16, inodeData uint32) uint32 { + switch mode & StatTypeMask { + case StatTypeChrdev, StatTypeBlkdev, StatTypeFifo, StatTypeSock: + // inodeData field is device number for some file types + return inodeData + default: + return 0 // Not a device type + } +} diff --git a/vendor/github.com/erofs/go-erofs/internal/disk/types.go b/vendor/github.com/erofs/go-erofs/internal/disk/types.go new file mode 100644 index 0000000000000..3928cc0ee2b8b --- /dev/null +++ b/vendor/github.com/erofs/go-erofs/internal/disk/types.go @@ -0,0 +1,154 @@ +package disk + +const ( + MagicNumber = 0xe0f5e1e2 + SuperBlockOffset = 1024 + + FeatureIncompatLZ4_0Padding = 0x1 + FeatureIncompatChunkedFile = 0x4 + FeatureIncompatDeviceTable = 0x8 + FeatureIncompatFragments = 0x20 + FeatureIncompatXattrPrefixes = 0x40 + FeatureIncompatAll uint32 = FeatureIncompatLZ4_0Padding | + FeatureIncompatChunkedFile | FeatureIncompatDeviceTable | + FeatureIncompatFragments | FeatureIncompatXattrPrefixes + + SizeSuperBlock = 128 + SizeInodeCompact = 32 + SizeInodeExtended = 64 + SizeDirent = 12 + SizeXattrBodyHeader = 12 + SizeXattrEntry = 4 + SizeDeviceSlot = 128 + + LayoutFlatPlain = 0 + LayoutCompressedFull = 1 + LayoutFlatInline = 2 + LayoutCompressedCompact = 3 + LayoutChunkBased = 4 + + LayoutChunkFormatBits = 0x001F + LayoutChunkFormatIndexes = 0x0020 + LayoutChunkFormat48Bit = 0x0040 +) + +// SuperBlock represents the EROFS on-disk superblock. +// See: https://docs.kernel.org/filesystems/erofs.html#on-disk-layout +type SuperBlock struct { + MagicNumber uint32 + Checksum uint32 + FeatureCompat uint32 + BlkSizeBits uint8 + ExtSlots uint8 + RootNid uint16 + Inos uint64 + BuildTime uint64 + BuildTimeNs uint32 + Blocks uint32 + MetaBlkAddr uint32 + XattrBlkAddr uint32 + UUID [16]uint8 + VolumeName [16]uint8 + FeatureIncompat uint32 + ComprAlgs uint16 + ExtraDevices uint16 + DevtSlotOff uint16 + DirBlkBits uint8 + XattrPrefixCount uint8 + XattrPrefixStart uint32 + PackedNid uint64 // Nid of the special "packed" inode for shared data/prefixes + XattrFilterRes uint8 + Reserved [23]uint8 +} + +// InodeCompact represents the 32-byte on-disk compact inode. +type InodeCompact struct { + Format uint16 // i_format + XattrCount uint16 // i_xattr_icount + Mode uint16 // i_mode + Nlink uint16 // i_nlink + Size uint32 // i_size + Reserved uint32 // i_reserved + InodeData uint32 // i_u (i_raw_blkaddr, i_rdev, etc.) + Inode uint32 // i_ino + UID uint16 // i_uid + GID uint16 // i_gid + Reserved2 uint32 // i_reserved2 +} + +// InodeExtended represents the 64-byte on-disk extended inode. +type InodeExtended struct { + Format uint16 // i_format + XattrCount uint16 // i_xattr_icount + Mode uint16 // i_mode + Reserved uint16 // i_reserved + Size uint64 // i_size + InodeData uint32 // i_u (i_raw_blkaddr, i_rdev, etc.) + Inode uint32 // i_ino + UID uint32 // i_uid + GID uint32 // i_gid + Mtime uint64 // i_mtime + MtimeNs uint32 // i_mtime_nsec + Nlink uint32 // i_nlink + Reserved2 [16]uint8 +} + +type Dirent struct { + Nid uint64 + NameOff uint16 + FileType uint8 + Reserved uint8 +} + +// XattrHeader is the header after an inode containing xattr information +// +// Original definition: +// inline xattrs (n == i_xattr_icount): +// erofs_xattr_ibody_header(1) + (n - 1) * 4 bytes +// +// 12 bytes / \ +// / \ +// /-----------------------\ +// | erofs_xattr_entries+ | +// +-----------------------+ +// +// inline xattrs must starts in erofs_xattr_ibody_header, +// for read-only fs, no need to introduce h_refcount +// Actual name is prefix | long prefix (prefix + infix) + name +type XattrHeader struct { + NameFilter uint32 // bit value 1 indicate not-present + SharedCount uint8 + Reserved [7]uint8 +} + +type XattrEntry struct { + NameLen uint8 // length of name + NameIndex uint8 // index of name in XattrHeader, 0x80 set indicates long prefix at index&0x7F + XattrPrefixStart + ValueLen uint16 // length of value + // Name+Value +} + +type XattrLongPrefixitem struct { + PrefixAddr uint32 // address of the long prefix + PrefixLen uint8 // length of the long prefix +} + +type XattrLongPrefix struct { + BaseIndex uint8 // short xattr name prefix index + // Infix part after short prefix +} + +type InodeChunkIndex struct { + StartBlkHi uint16 // part of 48-bit support (not yet implemented) + DeviceID uint16 + StartBlkLo uint32 +} + +// DeviceSlot represents the on-disk device table entry (erofs_deviceslot). +// See: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/erofs/erofs_fs.h +type DeviceSlot struct { + Tag [64]uint8 // digest(sha256), etc. + Blocks uint32 // total fs blocks of this device + MappedBlkAddr uint32 // map starting at mapped_blkaddr + Reserved [56]uint8 +} diff --git a/vendor/github.com/erofs/go-erofs/xattr.go b/vendor/github.com/erofs/go-erofs/xattr.go new file mode 100644 index 0000000000000..83dd061248f79 --- /dev/null +++ b/vendor/github.com/erofs/go-erofs/xattr.go @@ -0,0 +1,202 @@ +package erofs + +import ( + "encoding/binary" + "fmt" + "io" + "strings" + + "github.com/erofs/go-erofs/internal/disk" +) + +/* +#define EROFS_XATTR_INDEX_USER 1 +#define EROFS_XATTR_INDEX_POSIX_ACL_ACCESS 2 +#define EROFS_XATTR_INDEX_POSIX_ACL_DEFAULT 3 +#define EROFS_XATTR_INDEX_TRUSTED 4 +#define EROFS_XATTR_INDEX_LUSTRE 5 +#define EROFS_XATTR_INDEX_SECURITY 6 +*/ + +type xattrIndex uint8 + +func (idx xattrIndex) String() string { + switch idx { + case 1: + return "user." + case 2: + return "system.posix_acl_access." + case 3: + return "system.posix_acl_default." + case 4: + return "trusted." + case 5: + return "lustre." + case 6: + return "security." + default: + return "" + } +} + +func setXattrs(b *file, addr int64, blk *block) (err error) { + b.info.stat.Xattrs = map[string]string{} + blkSize := int32(1 << b.img.sb.BlkSizeBits) + + blk.offset = int32(addr & int64(blkSize-1)) + if blk.end != blkSize || blk.end-blk.offset < disk.SizeXattrBodyHeader { + b.img.putBlock(blk) + blk, err = b.img.loadAt(addr, int64(b.info.xsize)) + if err != nil { + return fmt.Errorf("failed to read xattr body for nid %d: %w", b.nid, err) + } + } + var ( + xb = blk.bytes() + xh disk.XattrHeader + ) + if _, err := binary.Decode(xb[:disk.SizeXattrBodyHeader], binary.LittleEndian, &xh); err != nil { + return err + } + xb = xb[disk.SizeXattrBodyHeader:] + + for i := 0; i < int(xh.SharedCount); i++ { + if len(xb) < 4 { + pos := disk.SizeXattrBodyHeader + int64(i)*4 + b.img.putBlock(blk) + blk, err = b.img.loadAt(addr+pos, int64(b.info.xsize)-pos) + if err != nil { + return fmt.Errorf("failed to read xattr body for nid %d: %w", b.nid, err) + } + xb = blk.bytes() + } + var xattrAddr uint32 + if _, err := binary.Decode(xb[:4], binary.LittleEndian, &xattrAddr); err != nil { + return err + } + + // TODO: Cache shared xattr blocks + blk, err := b.img.loadAt(int64(b.img.sb.XattrBlkAddr)< 0 { + copySize := len(xb) + if copySize == 0 { + if err := reload(); err != nil { + return err + } + copySize = len(xb) + } + if remaining < copySize { + copySize = remaining + } + n, err := b.Write(xb[:copySize]) + if err != nil { + return err + } else if n != copySize { + return io.ErrShortWrite + } + remaining -= n + pos += n + xb = xb[copySize:] + } + value = b.String() + } else { + value = string(xb[:xattrEntry.ValueLen]) + pos += int(xattrEntry.ValueLen) + xb = xb[xattrEntry.ValueLen:] + } + b.info.stat.Xattrs[name] = value + + // Round up to next 4 byte boundary + if rem := pos % 4; rem != 0 { + pad := 4 - rem + pos += pad + if len(xb) < pad { + xb = nil + } else { + xb = xb[pad:] + } + } + } + return nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index ca6210895013e..10876da56e174 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -10,8 +10,8 @@ dario.cat/mergo # github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 ## explicit; go 1.20 github.com/AdaLogics/go-fuzz-headers -# github.com/Microsoft/go-winio v0.6.2 -## explicit; go 1.21 +# github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 +## explicit; go 1.23.0 github.com/Microsoft/go-winio github.com/Microsoft/go-winio/backuptar github.com/Microsoft/go-winio/internal/fs @@ -24,8 +24,8 @@ github.com/Microsoft/go-winio/pkg/fs github.com/Microsoft/go-winio/pkg/guid github.com/Microsoft/go-winio/pkg/security github.com/Microsoft/go-winio/vhd -# github.com/Microsoft/hcsshim v0.14.0-rc.1 -## explicit; go 1.23.0 +# github.com/Microsoft/hcsshim v0.15.0-rc.1 +## explicit; go 1.24.0 github.com/Microsoft/hcsshim github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/options github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/stats @@ -61,6 +61,9 @@ github.com/Microsoft/hcsshim/internal/vmcompute github.com/Microsoft/hcsshim/internal/wclayer github.com/Microsoft/hcsshim/internal/wclayer/cim github.com/Microsoft/hcsshim/internal/winapi +github.com/Microsoft/hcsshim/internal/winapi/cimfs +github.com/Microsoft/hcsshim/internal/winapi/cimwriter +github.com/Microsoft/hcsshim/internal/winapi/types github.com/Microsoft/hcsshim/osversion github.com/Microsoft/hcsshim/pkg/cimfs github.com/Microsoft/hcsshim/pkg/cimfs/format @@ -293,6 +296,10 @@ github.com/docker/go-units ## explicit; go 1.13 github.com/emicklei/go-restful/v3 github.com/emicklei/go-restful/v3/log +# github.com/erofs/go-erofs v0.2.0 +## explicit; go 1.23 +github.com/erofs/go-erofs +github.com/erofs/go-erofs/internal/disk # github.com/felixge/httpsnoop v1.0.4 ## explicit; go 1.13 github.com/felixge/httpsnoop