diff --git a/pkg/fsverity/fsverity_linux.go b/pkg/fsverity/fsverity_linux.go new file mode 100644 index 000000000000..e08d7e6fe173 --- /dev/null +++ b/pkg/fsverity/fsverity_linux.go @@ -0,0 +1,148 @@ +/* + 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 fsverity + +import ( + "fmt" + "os" + "path/filepath" + "sync" + "syscall" + "unsafe" + + "github.com/containerd/containerd/v2/pkg/kernelversion" + "golang.org/x/sys/unix" +) + +type fsverityEnableArg struct { + version uint32 + hashAlgorithm uint32 + blockSize uint32 + saltSize uint32 + saltPtr uint64 + sigSize uint32 + reserved1 uint32 + sigPtr uint64 + reserved2 [11]uint64 +} + +const ( + defaultBlockSize int = 4096 + maxDigestSize uint16 = 64 +) + +var ( + once sync.Once + supported bool + supportedErr error +) + +func IsSupported(rootPath string) (bool, error) { + once.Do(func() { + minKernelVersion := kernelversion.KernelVersion{Kernel: 5, Major: 4} + s, err := kernelversion.GreaterEqualThan(minKernelVersion) + if err != nil { + supported = s + supportedErr = err + return + } + + integrityDir := filepath.Join(rootPath, "integrity") + if err = os.MkdirAll(integrityDir, 0755); err != nil { + supported = false + supportedErr = err + return + } + + digestPath := filepath.Join(integrityDir, "supported") + digestFile, err := os.Create(digestPath) + if err != nil { + supported = false + supportedErr = err + return + } + + digestFile.Close() + defer os.RemoveAll(integrityDir) + + eerr := Enable(digestPath) + if eerr != nil { + supported = false + supportedErr = eerr + return + } + + supported = true + supportedErr = nil + }) + return supported, supportedErr +} + +func IsEnabled(path string) (bool, error) { + f, err := os.Open(path) + if err != nil { + return false, err + } + + var attr int + + _, _, flagErr := unix.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(unix.FS_IOC_GETFLAGS), uintptr(unsafe.Pointer(&attr))) + if flagErr != 0 { + return false, fmt.Errorf("error getting inode flags: %w", flagErr) + } + + if attr&unix.FS_VERITY_FL == unix.FS_VERITY_FL { + return true, nil + } + + return false, nil +} + +func Enable(path string) error { + f, err := os.Open(path) + if err != nil { + return err + } + + var args = &fsverityEnableArg{} + args.version = 1 + args.hashAlgorithm = 1 + + // fsverity block size should be the minimum between the page size + // and the file system block size + // If neither value is retrieved successfully, set fsverity block size to the default value + blockSize := unix.Getpagesize() + + s := unix.Stat_t{} + serr := unix.Stat(path, &s) + if serr == nil && int(s.Blksize) < blockSize { + blockSize = int(s.Blksize) + } + + if blockSize <= 0 { + blockSize = defaultBlockSize + } + + args.blockSize = uint32(blockSize) + + _, _, errno := unix.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(unix.FS_IOC_ENABLE_VERITY), uintptr(unsafe.Pointer(args))) + if errno != 0 { + return fmt.Errorf("enable fsverity failed: %w", errno) + } + + return nil +} diff --git a/pkg/fsverity/fsverity_other.go b/pkg/fsverity/fsverity_other.go new file mode 100644 index 000000000000..5815e5b4552f --- /dev/null +++ b/pkg/fsverity/fsverity_other.go @@ -0,0 +1,29 @@ +//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 fsverity + +import "fmt" + +func IsSupported(rootPath string) (bool, error) { + return false, fmt.Errorf("fsverity is only supported on Linux systems") +} + +func Enable(_ string) error { + return fmt.Errorf("fsverity is only supported on Linux systems") +} diff --git a/plugins/content/local/writer.go b/plugins/content/local/writer.go index 26cef0aaed5b..e8c2f3c7b19b 100644 --- a/plugins/content/local/writer.go +++ b/plugins/content/local/writer.go @@ -27,6 +27,7 @@ import ( "time" "github.com/containerd/containerd/v2/core/content" + "github.com/containerd/containerd/v2/pkg/fsverity" "github.com/containerd/errdefs" "github.com/containerd/log" "github.com/opencontainers/go-digest" @@ -137,6 +138,20 @@ func (w *writer) Commit(ctx context.Context, size int64, expected digest.Digest, return err } + // Enable content blob integrity verification if supported + + var ( + integritySupported bool + supportErr error + ) + if integritySupported, supportErr = fsverity.IsSupported(w.s.root); integritySupported { + if err := fsverity.Enable(target); err != nil { + log.G(ctx).Warnf("failed to enable integrity of blob %v: %s", target, err.Error()) + } + } else { + log.G(ctx).WithError(supportErr).Debug("fsverity integrity verification is not supported") + } + // Ingest has now been made available in the content store, attempt to complete // setting metadata but errors should only be logged and not returned since // the content store cannot be cleanly rolled back.