From 62ec7ec3367233823c09befddc5ad312aa607822 Mon Sep 17 00:00:00 2001 From: Andrey Smirnov Date: Tue, 8 Oct 2024 16:01:40 +0400 Subject: [PATCH] refactor: replace the old v1 mount package with new one Re-design some methods, simplify flows and allow more simple interactions. Learn from mistakes and design better methods. Fixes #9471 Signed-off-by: Andrey Smirnov --- internal/app/init/main.go | 62 ++- internal/app/machined/main.go | 2 +- .../runtime/v1alpha1/v1alpha1_sequencer.go | 1 - .../v1alpha1/v1alpha1_sequencer_tasks.go | 67 +-- .../machined/pkg/system/services/extension.go | 24 +- internal/pkg/mount/bpffs.go | 14 - internal/pkg/mount/cgroups.go | 65 --- internal/pkg/mount/iter.go | 115 ---- internal/pkg/mount/mount.go | 504 ------------------ internal/pkg/mount/mount_test.go | 120 ----- internal/pkg/mount/options.go | 143 ----- internal/pkg/mount/pseudo.go | 45 -- internal/pkg/mount/squashfs.go | 27 - internal/pkg/mount/switchroot/switchroot.go | 7 +- internal/pkg/mount/unmount.go | 77 --- internal/pkg/mount/{ => v2}/all.go | 0 internal/pkg/mount/v2/cgroups.go | 62 +++ internal/pkg/mount/v2/mount.go | 86 ++- internal/pkg/mount/{ => v2}/overlay.go | 14 +- internal/pkg/mount/v2/points.go | 12 + internal/pkg/mount/v2/pseudo.go | 46 ++ internal/pkg/mount/v2/repair_test.go | 116 ++++ internal/pkg/mount/v2/squashfs.go | 20 + 23 files changed, 407 insertions(+), 1222 deletions(-) delete mode 100644 internal/pkg/mount/bpffs.go delete mode 100644 internal/pkg/mount/cgroups.go delete mode 100644 internal/pkg/mount/iter.go delete mode 100644 internal/pkg/mount/mount.go delete mode 100644 internal/pkg/mount/mount_test.go delete mode 100644 internal/pkg/mount/options.go delete mode 100644 internal/pkg/mount/pseudo.go delete mode 100644 internal/pkg/mount/squashfs.go delete mode 100644 internal/pkg/mount/unmount.go rename internal/pkg/mount/{ => v2}/all.go (100%) create mode 100644 internal/pkg/mount/v2/cgroups.go rename internal/pkg/mount/{ => v2}/overlay.go (61%) create mode 100644 internal/pkg/mount/v2/pseudo.go create mode 100644 internal/pkg/mount/v2/repair_test.go create mode 100644 internal/pkg/mount/v2/squashfs.go diff --git a/internal/app/init/main.go b/internal/app/init/main.go index 60ee28542a..227316dd6f 100644 --- a/internal/app/init/main.go +++ b/internal/app/init/main.go @@ -13,18 +13,16 @@ import ( "os/signal" "path/filepath" "runtime" - "strings" "syscall" "time" - "github.com/freddierice/go-losetup/v2" "github.com/klauspost/cpuid/v2" "github.com/siderolabs/go-kmsg" "github.com/siderolabs/go-procfs/procfs" "golang.org/x/sys/unix" - "github.com/siderolabs/talos/internal/pkg/mount" "github.com/siderolabs/talos/internal/pkg/mount/switchroot" + "github.com/siderolabs/talos/internal/pkg/mount/v2" "github.com/siderolabs/talos/internal/pkg/rng" "github.com/siderolabs/talos/internal/pkg/secureboot" "github.com/siderolabs/talos/internal/pkg/secureboot/tpm2" @@ -38,31 +36,27 @@ func init() { runtime.MemProfileRate = 0 } -func run() (err error) { +func run() error { // Mount the pseudo devices. - pseudo, err := mount.PseudoMountPoints() - if err != nil { - return err - } + pseudoMountPoints := mount.Pseudo() - if err = mount.Mount(pseudo); err != nil { + if _, err := pseudoMountPoints.Mount(); err != nil { return err } // Setup logging to /dev/kmsg. - err = kmsg.SetupLogger(nil, "[talos] [initramfs]", nil) - if err != nil { + if err := kmsg.SetupLogger(nil, "[talos] [initramfs]", nil); err != nil { return err } // Seed RNG. - if err = rng.TPMSeed(); err != nil { + if err := rng.TPMSeed(); err != nil { // not making this fatal error log.Printf("failed to seed from the TPM: %s", err) } // extend PCR 11 with enter-initrd - if err = tpm2.PCRExtend(secureboot.UKIPCR, []byte(secureboot.EnterInitrd)); err != nil { + if err := tpm2.PCRExtend(secureboot.UKIPCR, []byte(secureboot.EnterInitrd)); err != nil { return fmt.Errorf("failed to extend PCR %d with enter-initrd: %v", secureboot.UKIPCR, err) } @@ -71,24 +65,24 @@ func run() (err error) { cpuInfo() // Mount the rootfs. - if err = mountRootFS(); err != nil { + if err := mountRootFS(); err != nil { return err } // Bind mount the lib/firmware if needed. - if err = bindMountFirmware(); err != nil { + if err := bindMountFirmware(); err != nil { return err } // Bind mount /.extra if needed. - if err = bindMountExtra(); err != nil { + if err := bindMountExtra(); err != nil { return err } // Switch into the new rootfs. log.Println("entering the rootfs") - return switchroot.Switch(constants.NewRoot, pseudo) + return switchroot.Switch(constants.NewRoot, pseudoMountPoints) } func recovery() { @@ -131,12 +125,14 @@ func mountRootFS() error { // if no extensions found use plain squashfs mount if len(extensionsConfig.Layers) == 0 { - squashfs, err := mount.SquashfsMountPoints(constants.NewRoot) + squashfs, err := mount.Squashfs(constants.NewRoot, "/"+constants.RootfsAsset) if err != nil { return err } - return mount.Mount(squashfs) + _, err = squashfs.Mount() + + return err } // otherwise compose overlay mounts @@ -145,9 +141,10 @@ func mountRootFS() error { image string } - var layers []layer - - squashfs := mount.NewMountPoints() + var ( + layers []layer + squashfsPoints mount.Points + ) // going in the inverse order as earlier layers are overlayed on top of the latter ones for i := len(extensionsConfig.Layers) - 1; i >= 0; i-- { @@ -167,29 +164,30 @@ func mountRootFS() error { overlays := make([]string, 0, len(layers)) for _, layer := range layers { - dev, err := losetup.Attach(layer.image, 0, true) + target := filepath.Join(constants.ExtensionLayers, layer.name) + + point, err := mount.Squashfs(target, layer.image) if err != nil { return err } - p := mount.NewMountPoint(dev.Path(), "/"+layer.name, "squashfs", unix.MS_RDONLY|unix.MS_I_VERSION, "", mount.WithPrefix(constants.ExtensionLayers), mount.WithFlags(mount.ReadOnly|mount.Shared)) - - overlays = append(overlays, p.Target()) - squashfs.Set(layer.name, p) + squashfsPoints = append(squashfsPoints, point) + overlays = append(overlays, target) } - if err := mount.Mount(squashfs); err != nil { + squashfsUnmounter, err := squashfsPoints.Mount() + if err != nil { return err } - overlay := mount.NewMountPoints() - overlay.Set(constants.NewRoot, mount.NewMountPoint(strings.Join(overlays, ":"), constants.NewRoot, "", unix.MS_I_VERSION, "", mount.WithFlags(mount.ReadOnly|mount.ReadonlyOverlay|mount.Shared))) + overlayPoint := mount.NewReadonlyOverlay(overlays, constants.NewRoot, mount.WithShared(), mount.WithFlags(unix.MS_I_VERSION)) - if err := mount.Mount(overlay); err != nil { + _, err = overlayPoint.Mount() + if err != nil { return err } - if err := mount.Unmount(squashfs); err != nil { + if err = squashfsUnmounter(); err != nil { return err } diff --git a/internal/app/machined/main.go b/internal/app/machined/main.go index eba4c9c72a..fb2d01aada 100644 --- a/internal/app/machined/main.go +++ b/internal/app/machined/main.go @@ -35,7 +35,7 @@ import ( "github.com/siderolabs/talos/internal/app/maintenance" "github.com/siderolabs/talos/internal/app/poweroff" "github.com/siderolabs/talos/internal/app/trustd" - "github.com/siderolabs/talos/internal/pkg/mount" + "github.com/siderolabs/talos/internal/pkg/mount/v2" "github.com/siderolabs/talos/pkg/httpdefaults" "github.com/siderolabs/talos/pkg/machinery/api/common" "github.com/siderolabs/talos/pkg/machinery/api/machine" diff --git a/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer.go b/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer.go index 4733f6d380..994591df94 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer.go @@ -94,7 +94,6 @@ func (*Sequencer) Initialize(r runtime.Runtime) []runtime.Phase { "systemRequirements", EnforceKSPPRequirements, SetupSystemDirectory, - MountBPFFS, MountCgroups, MountPseudoFilesystems, SetRLimit, diff --git a/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer_tasks.go b/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer_tasks.go index 8957db77b1..ddf9e3f26e 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer_tasks.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer_tasks.go @@ -168,7 +168,7 @@ func CreateSystemCgroups(runtime.Sequence, any) (runtime.TaskExecutionFunc, stri if r.State().Platform().Mode() != runtime.ModeContainer { // assert that cgroupsv2 is being used when running not in container mode, // as Talos sets up cgroupsv2 on its own - if cgroups.Mode() != cgroups.Unified && !mount.ForceGGroupsV1() { + if cgroups.Mode() != cgroups.Unified && !mountv2.ForceGGroupsV1() { return errors.New("cgroupsv2 should be used") } } @@ -342,45 +342,21 @@ func CreateSystemCgroups(runtime.Sequence, any) (runtime.TaskExecutionFunc, stri }, "CreateSystemCgroups" } -// MountBPFFS represents the MountBPFFS task. -func MountBPFFS(runtime.Sequence, any) (runtime.TaskExecutionFunc, string) { - return func(ctx context.Context, logger *log.Logger, r runtime.Runtime) (err error) { - var mountpoints *mount.Points - - mountpoints, err = mount.BPFMountPoints() - if err != nil { - return err - } - - return mount.Mount(mountpoints) - }, "mountBPFFS" -} - // MountCgroups represents the MountCgroups task. func MountCgroups(runtime.Sequence, any) (runtime.TaskExecutionFunc, string) { - return func(ctx context.Context, logger *log.Logger, r runtime.Runtime) (err error) { - var mountpoints *mount.Points - - mountpoints, err = mount.CGroupMountPoints() - if err != nil { - return err - } + return func(ctx context.Context, logger *log.Logger, r runtime.Runtime) error { + _, err := mountv2.CGroupMountPoints().Mount() - return mount.Mount(mountpoints) + return err }, "mountCgroups" } // MountPseudoFilesystems represents the MountPseudoFilesystems task. func MountPseudoFilesystems(runtime.Sequence, any) (runtime.TaskExecutionFunc, string) { - return func(ctx context.Context, logger *log.Logger, r runtime.Runtime) (err error) { - var mountpoints *mount.Points - - mountpoints, err = mount.PseudoSubMountPoints() - if err != nil { - return err - } + return func(ctx context.Context, logger *log.Logger, r runtime.Runtime) error { + _, err := mountv2.PseudoSubMountPoints().Mount() - return mount.Mount(mountpoints) + return err }, "mountPseudoFilesystems" } @@ -722,10 +698,9 @@ func StartDashboard(_ runtime.Sequence, _ any) (runtime.TaskExecutionFunc, strin // StartUdevd represents the task to start udevd. func StartUdevd(runtime.Sequence, any) (runtime.TaskExecutionFunc, string) { return func(ctx context.Context, logger *log.Logger, r runtime.Runtime) (err error) { - mp := mount.NewMountPoints() - mp.Set("udev-data", mount.NewMountPoint("", constants.UdevDir, "", unix.MS_I_VERSION, "", mount.WithFlags(mount.Overlay|mount.SystemOverlay|mount.Shared))) + mp := mountv2.NewSystemOverlay([]string{constants.UdevDir}, constants.UdevDir, mountv2.WithShared(), mountv2.WithFlags(unix.MS_I_VERSION)) - if err = mount.Mount(mp); err != nil { + if _, err = mp.Mount(); err != nil { return err } @@ -864,14 +839,9 @@ func StopAllServices(runtime.Sequence, any) (runtime.TaskExecutionFunc, string) // MountOverlayFilesystems represents the MountOverlayFilesystems task. func MountOverlayFilesystems(runtime.Sequence, any) (runtime.TaskExecutionFunc, string) { return func(ctx context.Context, logger *log.Logger, r runtime.Runtime) (err error) { - var mountpoints *mount.Points - - mountpoints, err = mount.OverlayMountPoints() - if err != nil { - return err - } + _, err = mountv2.OverlayMountPoints().Mount() - return mount.Mount(mountpoints) + return err }, "mountOverlayFilesystems" } @@ -1191,14 +1161,7 @@ func existsAndIsFile(p string) (err error) { // UnmountOverlayFilesystems represents the UnmountOverlayFilesystems task. func UnmountOverlayFilesystems(runtime.Sequence, any) (runtime.TaskExecutionFunc, string) { return func(ctx context.Context, logger *log.Logger, r runtime.Runtime) (err error) { - var mountpoints *mount.Points - - mountpoints, err = mount.OverlayMountPoints() - if err != nil { - return err - } - - return mount.Unmount(mountpoints) + return mountv2.OverlayMountPoints().Unmount() }, "unmountOverlayFilesystems" } @@ -1266,7 +1229,7 @@ func UnmountPodMounts(runtime.Sequence, any) (runtime.TaskExecutionFunc, string) if strings.HasPrefix(mountpoint, constants.EphemeralMountPoint+"/") { logger.Printf("unmounting %s\n", mountpoint) - if err = mount.SafeUnmount(ctx, logger, mountpoint); err != nil { + if err = mountv2.SafeUnmount(ctx, logger.Printf, mountpoint); err != nil { if errors.Is(err, syscall.EINVAL) { log.Printf("ignoring unmount error %s: %v", mountpoint, err) } else { @@ -1315,7 +1278,7 @@ func UnmountSystemDiskBindMounts(runtime.Sequence, any) (runtime.TaskExecutionFu if strings.HasPrefix(device, devname) && device != devname { logger.Printf("unmounting %s\n", mountpoint) - if err = mount.SafeUnmount(ctx, logger, mountpoint); err != nil { + if err = mountv2.SafeUnmount(ctx, logger.Printf, mountpoint); err != nil { if errors.Is(err, syscall.EINVAL) { log.Printf("ignoring unmount error %s: %v", mountpoint, err) } else { @@ -2257,7 +2220,7 @@ func ForceCleanup(runtime.Sequence, any) (runtime.TaskExecutionFunc, string) { logger.Printf("error killing all procs: %s", err) } - if err := mount.UnmountAll(); err != nil { + if err := mountv2.UnmountAll(); err != nil { logger.Printf("error unmounting: %s", err) } diff --git a/internal/app/machined/pkg/system/services/extension.go b/internal/app/machined/pkg/system/services/extension.go index c477047b59..ab09ee43e1 100644 --- a/internal/app/machined/pkg/system/services/extension.go +++ b/internal/app/machined/pkg/system/services/extension.go @@ -26,7 +26,7 @@ import ( "github.com/siderolabs/talos/internal/app/machined/pkg/system/runner/restart" "github.com/siderolabs/talos/internal/pkg/capability" "github.com/siderolabs/talos/internal/pkg/environment" - "github.com/siderolabs/talos/internal/pkg/mount" + "github.com/siderolabs/talos/internal/pkg/mount/v2" "github.com/siderolabs/talos/pkg/conditions" "github.com/siderolabs/talos/pkg/machinery/constants" extservices "github.com/siderolabs/talos/pkg/machinery/extensions/services" @@ -39,7 +39,7 @@ import ( type Extension struct { Spec extservices.Spec - overlay *mount.Point + overlayUnmounter func() error } // ID implements the Service interface. @@ -50,21 +50,23 @@ func (svc *Extension) ID(r runtime.Runtime) string { // PreFunc implements the Service interface. func (svc *Extension) PreFunc(ctx context.Context, r runtime.Runtime) error { // re-mount service rootfs as overlay rw mount to allow containerd to mount there /dev, /proc, etc. - svc.overlay = mount.NewMountPoint( - "", - filepath.Join(constants.ExtensionServiceRootfsPath, svc.Spec.Name), - "", - 0, - "", - mount.WithFlags(mount.Overlay|mount.SystemOverlay), + rootfsPath := filepath.Join(constants.ExtensionServiceRootfsPath, svc.Spec.Name) + + overlay := mount.NewSystemOverlay( + []string{rootfsPath}, + rootfsPath, ) - return svc.overlay.Mount() + var err error + + svc.overlayUnmounter, err = overlay.Mount() + + return err } // PostFunc implements the Service interface. func (svc *Extension) PostFunc(r runtime.Runtime, state events.ServiceState) (err error) { - return svc.overlay.Unmount() + return svc.overlayUnmounter() } // Condition implements the Service interface. diff --git a/internal/pkg/mount/bpffs.go b/internal/pkg/mount/bpffs.go deleted file mode 100644 index 2ac458c4ed..0000000000 --- a/internal/pkg/mount/bpffs.go +++ /dev/null @@ -1,14 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package mount - -// BPFMountPoints returns the bpf mount points. -func BPFMountPoints() (mountpoints *Points, err error) { - base := "/sys/fs/bpf" - bpf := NewMountPoints() - bpf.Set("bpf", NewMountPoint("bpffs", base, "bpf", 0, "")) - - return bpf, nil -} diff --git a/internal/pkg/mount/cgroups.go b/internal/pkg/mount/cgroups.go deleted file mode 100644 index 9924eea174..0000000000 --- a/internal/pkg/mount/cgroups.go +++ /dev/null @@ -1,65 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package mount - -import ( - "path/filepath" - - "github.com/siderolabs/go-procfs/procfs" - "golang.org/x/sys/unix" - - "github.com/siderolabs/talos/pkg/machinery/constants" -) - -// ForceGGroupsV1 returns the cgroup version to be used (only for !container mode). -func ForceGGroupsV1() bool { - value := procfs.ProcCmdline().Get(constants.KernelParamCGroups).First() - - return value != nil && *value == "0" -} - -// CGroupMountPoints returns the cgroup mount points. -func CGroupMountPoints() (mountpoints *Points, err error) { - if ForceGGroupsV1() { - return cgroupMountPointsV1() - } - - return cgroupMountPointsV2() -} - -func cgroupMountPointsV2() (mountpoints *Points, err error) { - cgroups := NewMountPoints() - - cgroups.Set("cgroup2", NewMountPoint("cgroup", constants.CgroupMountPath, "cgroup2", unix.MS_NOSUID|unix.MS_NODEV|unix.MS_NOEXEC|unix.MS_RELATIME, "nsdelegate,memory_recursiveprot")) - - return cgroups, nil -} - -func cgroupMountPointsV1() (mountpoints *Points, err error) { - cgroups := NewMountPoints() - cgroups.Set("dev", NewMountPoint("tmpfs", constants.CgroupMountPath, "tmpfs", unix.MS_NOSUID|unix.MS_NODEV|unix.MS_NOEXEC|unix.MS_RELATIME, "mode=755")) - - controllers := []string{ - "blkio", - "cpu", - "cpuacct", - "cpuset", - "devices", - "freezer", - "hugetlb", - "memory", - "net_cls", - "net_prio", - "perf_event", - "pids", - } - - for _, controller := range controllers { - p := filepath.Join(constants.CgroupMountPath, controller) - cgroups.Set(controller, NewMountPoint(controller, p, "cgroup", unix.MS_NOSUID|unix.MS_NODEV|unix.MS_NOEXEC|unix.MS_RELATIME, controller)) - } - - return cgroups, nil -} diff --git a/internal/pkg/mount/iter.go b/internal/pkg/mount/iter.go deleted file mode 100644 index 86c7b255d6..0000000000 --- a/internal/pkg/mount/iter.go +++ /dev/null @@ -1,115 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package mount - -// PointsIterator represents an iteratable group of mount points. -type PointsIterator struct { - p *Points - value *Point - key string - index int - end int - err error - reverse bool -} - -// Iter initializes and returns a mount point iterator. -func (p *Points) Iter() *PointsIterator { - return &PointsIterator{ - p: p, - index: -1, - end: len(p.order) - 1, - value: nil, - } -} - -// IterRev initializes and returns a mount point iterator that advances in -// reverse. -func (p *Points) IterRev() *PointsIterator { - return &PointsIterator{ - p: p, - reverse: true, - index: len(p.points), - end: 0, - value: nil, - } -} - -// Set sets an ordered value. -func (p *Points) Set(key string, value *Point) { - if _, ok := p.points[key]; ok { - for i := range p.order { - if p.order[i] == key { - p.order = append(p.order[:i], p.order[i+1:]...) - } - } - } - - p.order = append(p.order, key) - p.points[key] = value -} - -// Get gets an ordered value. -func (p *Points) Get(key string) (value *Point, ok bool) { - if value, ok = p.points[key]; ok { - return value, true - } - - return nil, false -} - -// Len returns number of mount points. -func (p *Points) Len() int { - return len(p.points) -} - -// Key returns the current key. -func (i *PointsIterator) Key() string { - return i.key -} - -// Value returns current mount point. -func (i *PointsIterator) Value() *Point { - if i.err != nil || i.index > len(i.p.points) { - panic("invoked Value on expired iterator") - } - - return i.value -} - -// Err returns an error. -func (i *PointsIterator) Err() error { - return i.err -} - -// Next advances the iterator to the next value. -func (i *PointsIterator) Next() bool { - if i.err != nil { - return false - } - - if i.reverse { - i.index-- - - if i.index < i.end { - return false - } - } else { - i.index++ - - if i.index > i.end { - return false - } - } - - i.key = i.p.order[i.index] - i.value = i.p.points[i.key] - - if i.reverse { - return i.index >= i.end - } - - return i.index <= i.end -} diff --git a/internal/pkg/mount/mount.go b/internal/pkg/mount/mount.go deleted file mode 100644 index 79a83ed36f..0000000000 --- a/internal/pkg/mount/mount.go +++ /dev/null @@ -1,504 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package mount - -import ( - "bufio" - "context" - "errors" - "fmt" - "os" - "path/filepath" - "strings" - "time" - - "github.com/siderolabs/go-blockdevice/blockdevice" - "github.com/siderolabs/go-blockdevice/blockdevice/filesystem" - "github.com/siderolabs/go-blockdevice/blockdevice/util" - "github.com/siderolabs/go-retry/retry" - "golang.org/x/sys/unix" - - "github.com/siderolabs/talos/pkg/machinery/constants" - "github.com/siderolabs/talos/pkg/makefs" -) - -// RetryFunc defines the requirements for retrying a mount point operation. -type RetryFunc func(*Point) error - -// Mount mounts the device(s). -func Mount(mountpoints *Points) (err error) { - iter := mountpoints.Iter() - - // Mount the device(s). - - for iter.Next() { - if _, err = mountMountpoint(iter.Value()); err != nil { - return fmt.Errorf("error mounting %q: %w", iter.Value().Source(), err) - } - } - - if iter.Err() != nil { - return iter.Err() - } - - return nil -} - -//nolint:gocyclo -func mountMountpoint(mountpoint *Point) (skipMount bool, err error) { - // Repair the disk's partition table. - if mountpoint.MountFlags.Check(Resize) { - if _, err = mountpoint.ResizePartition(); err != nil { - return false, fmt.Errorf("error resizing %w", err) - } - } - - if mountpoint.MountFlags.Check(SkipIfMounted) { - skipMount, err = mountpoint.IsMounted() - if err != nil { - return false, fmt.Errorf("mountpoint is set to skip if mounted, but the mount check failed: %w", err) - } - } - - if mountpoint.MountFlags.Check(SkipIfNoFilesystem) && mountpoint.Fstype() == filesystem.Unknown { - skipMount = true - } - - if !skipMount { - if err = mountpoint.Mount(); err != nil { - if mountpoint.MountFlags.Check(SkipIfNoDevice) && errors.Is(err, unix.ENODEV) { - if mountpoint.Logger != nil { - mountpoint.Logger.Printf("error mounting: %q: %s", mountpoint.Source(), err) - } - - return true, nil - } - - return false, fmt.Errorf("error mounting: %w", err) - } - } - - // Grow the filesystem to the maximum allowed size. - // - // Growfs is called always, even if ResizePartition returns false to workaround failure scenario - // when partition was resized, but growfs never got called. - if mountpoint.MountFlags.Check(Resize) { - if err = mountpoint.GrowFilesystem(); err != nil { - return false, fmt.Errorf("error resizing filesystem: %w", err) - } - } - - return skipMount, nil -} - -// Unmount unmounts the device(s). -func Unmount(mountpoints *Points) (err error) { - iter := mountpoints.IterRev() - for iter.Next() { - mountpoint := iter.Value() - if err = mountpoint.Unmount(); err != nil { - return fmt.Errorf("unmount: %w", err) - } - } - - if iter.Err() != nil { - return iter.Err() - } - - return nil -} - -// Move moves the device(s). -// TODO(andrewrynhard): We need to skip calling the move method on mountpoints -// that are a child of another mountpoint. The kernel will handle moving the -// child mountpoints for us. -func Move(mountpoints *Points, prefix string) (err error) { - iter := mountpoints.Iter() - for iter.Next() { - mountpoint := iter.Value() - if err = mountpoint.Move(prefix); err != nil { - return fmt.Errorf("move: %w", err) - } - } - - if iter.Err() != nil { - return iter.Err() - } - - return nil -} - -// PrefixMountTargets prefixes all mountpoints targets with fixed path. -func PrefixMountTargets(mountpoints *Points, targetPrefix string) error { - iter := mountpoints.Iter() - for iter.Next() { - mountpoint := iter.Value() - mountpoint.target = filepath.Join(targetPrefix, mountpoint.target) - } - - return iter.Err() -} - -//nolint:gocyclo -func mountRetry(f RetryFunc, p *Point, isUnmount bool) (err error) { - err = retry.Constant(5*time.Second, retry.WithUnits(50*time.Millisecond)).Retry(func() error { - if err = f(p); err != nil { - switch err { - case unix.EBUSY: - return retry.ExpectedError(err) - case unix.ENOENT, unix.ENXIO: - // if udevd triggers BLKRRPART ioctl, partition device entry might disappear temporarily - return retry.ExpectedError(err) - case unix.EUCLEAN, unix.EIO: - if errRepair := p.Repair(); errRepair != nil { - return fmt.Errorf("error repairing: %w", errRepair) - } - - return retry.ExpectedError(err) - case unix.EINVAL: - isMounted, checkErr := p.IsMounted() - if checkErr != nil { - return retry.ExpectedError(checkErr) - } - - if !isMounted && !isUnmount { - if errRepair := p.Repair(); errRepair != nil { - return fmt.Errorf("error repairing: %w", errRepair) - } - - return retry.ExpectedError(err) - } - - if !isMounted && isUnmount { // if partition is already unmounted, ignore EINVAL - return nil - } - - return err - default: - return err - } - } - - return nil - }) - - return err -} - -// Point represents a Linux mount point. -type Point struct { - source string - target string - fstype string - flags uintptr - data string - *Options -} - -// PointMap represents a unique set of mount points. -type PointMap = map[string]*Point - -// Points represents an ordered set of mount points. -type Points struct { - points PointMap - order []string -} - -// NewMountPoint initializes and returns a Point struct. -func NewMountPoint(source, target, fstype string, flags uintptr, data string, setters ...Option) *Point { - opts := NewDefaultOptions(setters...) - - p := &Point{ - source: source, - target: target, - fstype: fstype, - flags: flags, - data: data, - Options: opts, - } - - if p.Prefix != "" { - p.target = filepath.Join(p.Prefix, p.target) - } - - if p.Options.ProjectQuota { - if len(p.data) > 0 { - p.data += "," - } - - p.data += "prjquota" - } - - return p -} - -// NewMountPoints initializes and returns a Points struct. -func NewMountPoints() *Points { - return &Points{ - points: make(PointMap), - } -} - -// Source returns the mount points source field. -func (p *Point) Source() string { - return p.source -} - -// Target returns the mount points target field. -func (p *Point) Target() string { - return p.target -} - -// Fstype returns the mount points fstype field. -func (p *Point) Fstype() string { - return p.fstype -} - -// Flags returns the mount points flags field. -func (p *Point) Flags() uintptr { - return p.flags -} - -// Data returns the mount points data field. -func (p *Point) Data() string { - return p.data -} - -// Mount attempts to retry a mount on EBUSY. It will attempt a retry -// every 100 milliseconds over the course of 5 seconds. -func (p *Point) Mount() (err error) { - for _, hook := range p.Options.PreMountHooks { - if err = hook(p); err != nil { - return err - } - } - - if err = ensureDirectory(p.target); err != nil { - return err - } - - if p.MountFlags.Check(ReadOnly) { - p.flags |= unix.MS_RDONLY - } - - switch { - case p.MountFlags.Check(Overlay): - err = mountRetry(overlay, p, false) - case p.MountFlags.Check(ReadonlyOverlay): - err = mountRetry(readonlyOverlay, p, false) - default: - err = mountRetry(mount, p, false) - } - - if err != nil { - return err - } - - if p.MountFlags.Check(Shared) { - if err = mountRetry(share, p, false); err != nil { - return fmt.Errorf("error sharing mount point %s: %+v", p.target, err) - } - } - - return nil -} - -// Unmount attempts to retry an unmount on EBUSY. It will attempt a -// retry every 100 milliseconds over the course of 5 seconds. -func (p *Point) Unmount() (err error) { - var mounted bool - - if mounted, err = p.IsMounted(); err != nil { - return err - } - - if mounted { - if err = mountRetry(unmount, p, true); err != nil { - return err - } - } - - for _, hook := range p.Options.PostUnmountHooks { - if err = hook(p); err != nil { - return err - } - } - - return nil -} - -// IsMounted checks whether mount point is active under /proc/mounts. -func (p *Point) IsMounted() (bool, error) { - f, err := os.Open("/proc/mounts") - if err != nil { - return false, err - } - - defer f.Close() //nolint:errcheck - - scanner := bufio.NewScanner(f) - for scanner.Scan() { - fields := strings.Fields(scanner.Text()) - - if len(fields) < 2 { - continue - } - - mountpoint := fields[1] - - if mountpoint == p.target { - return true, nil - } - } - - return false, scanner.Err() -} - -// Move moves a mountpoint to a new location with a prefix. -func (p *Point) Move(prefix string) (err error) { - target := p.Target() - mountpoint := NewMountPoint(target, target, "", unix.MS_MOVE, "", WithPrefix(prefix)) - - if err = mountpoint.Mount(); err != nil { - return fmt.Errorf("error moving mount point %s: %w", target, err) - } - - return nil -} - -// ResizePartition resizes a partition to the maximum size allowed. -func (p *Point) ResizePartition() (resized bool, err error) { - var devname string - - if devname, err = util.DevnameFromPartname(p.Source()); err != nil { - return false, err - } - - bd, err := blockdevice.Open("/dev/"+devname, blockdevice.WithExclusiveLock(true)) - if err != nil { - return false, fmt.Errorf("error opening block device %q: %w", devname, err) - } - - //nolint:errcheck - defer bd.Close() - - pt, err := bd.PartitionTable() - if err != nil { - return false, err - } - - if err := pt.Repair(); err != nil { - return false, err - } - - for _, partition := range pt.Partitions().Items() { - if partition.Name == constants.EphemeralPartitionLabel { - resized, err := pt.Resize(partition) - if err != nil { - return false, err - } - - if !resized { - return false, nil - } - } - } - - if err := pt.Write(); err != nil { - return false, err - } - - return true, nil -} - -// GrowFilesystem grows a partition's filesystem to the maximum size allowed. -// NB: An XFS partition MUST be mounted, or this will fail. -func (p *Point) GrowFilesystem() (err error) { - if err = makefs.XFSGrow(p.Target()); err != nil { - return fmt.Errorf("xfs_growfs: %w", err) - } - - return nil -} - -// Repair repairs a partition's filesystem. -func (p *Point) Repair() error { - if p.Logger != nil { - p.Logger.Printf("filesystem on %s needs cleaning, running repair", p.Source()) - } - - if err := makefs.XFSRepair(p.Source(), p.Fstype()); err != nil { - return fmt.Errorf("xfs_repair: %w", err) - } - - if p.Logger != nil { - p.Logger.Printf("filesystem successfully repaired on %s", p.Source()) - } - - return nil -} - -func mount(p *Point) (err error) { - return unix.Mount(p.source, p.target, p.fstype, p.flags, p.data) -} - -func unmount(p *Point) error { - return SafeUnmount(context.Background(), p.Logger, p.target) -} - -func share(p *Point) error { - return unix.Mount("", p.target, "", unix.MS_SHARED|unix.MS_REC, "") -} - -func overlay(p *Point) error { - parts := strings.Split(p.target, "/") - prefix := strings.Join(parts[1:], "-") - - basePath := constants.VarSystemOverlaysPath - - if p.MountFlags.Check(SystemOverlay) { - basePath = constants.SystemOverlaysPath - } - - diff := fmt.Sprintf(filepath.Join(basePath, "%s-diff"), prefix) - workdir := fmt.Sprintf(filepath.Join(basePath, "%s-workdir"), prefix) - - for _, target := range []string{diff, workdir} { - if err := ensureDirectory(target); err != nil { - return err - } - } - - lowerDir := p.target - if p.source != "" { - lowerDir = p.source - } - - opts := fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", lowerDir, diff, workdir) - if err := unix.Mount("overlay", p.target, "overlay", 0, opts); err != nil { - return fmt.Errorf("error creating overlay mount to %s: %w", p.target, err) - } - - return nil -} - -func readonlyOverlay(p *Point) error { - opts := fmt.Sprintf("lowerdir=%s", p.source) - if err := unix.Mount("overlay", p.target, "overlay", p.flags, opts); err != nil { - return fmt.Errorf("error creating overlay mount to %s: %w", p.target, err) - } - - return nil -} - -func ensureDirectory(target string) (err error) { - if _, err := os.Stat(target); os.IsNotExist(err) { - if err = os.MkdirAll(target, 0o755); err != nil { - return fmt.Errorf("error creating mount point directory %s: %w", target, err) - } - } - - return nil -} diff --git a/internal/pkg/mount/mount_test.go b/internal/pkg/mount/mount_test.go deleted file mode 100644 index a0b1c5780a..0000000000 --- a/internal/pkg/mount/mount_test.go +++ /dev/null @@ -1,120 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package mount_test - -import ( - "log" - "os" - "os/exec" - "path/filepath" - "testing" - - "github.com/siderolabs/go-blockdevice/blockdevice/loopback" - "github.com/stretchr/testify/suite" - "golang.org/x/sys/unix" - - "github.com/siderolabs/talos/internal/pkg/mount" - "github.com/siderolabs/talos/pkg/makefs" -) - -// Some tests in this package cannot be run under buildkit, as buildkit doesn't propagate partition devices -// like /dev/loopXpY into the sandbox. To run the tests on your local computer, do the following: -// -// go test -exec sudo -v --count 1 github.com/siderolabs/talos/internal/pkg/mount - -type manifestSuite struct { - suite.Suite - - disk *os.File - loopbackDevice *os.File -} - -const ( - diskSize = 4 * 1024 * 1024 * 1024 // 4 GiB -) - -func TestManifestSuite(t *testing.T) { - suite.Run(t, new(manifestSuite)) -} - -func (suite *manifestSuite) SetupTest() { - suite.skipIfNotRoot() - - var err error - - suite.disk, err = os.CreateTemp("", "talos") - suite.Require().NoError(err) - - suite.Require().NoError(suite.disk.Truncate(diskSize)) - - suite.loopbackDevice, err = loopback.NextLoopDevice() - suite.Require().NoError(err) - - suite.T().Logf("Using %s", suite.loopbackDevice.Name()) - - suite.Require().NoError(loopback.Loop(suite.loopbackDevice, suite.disk)) - - suite.Require().NoError(loopback.LoopSetReadWrite(suite.loopbackDevice)) -} - -func (suite *manifestSuite) TearDownTest() { - if suite.loopbackDevice != nil { - suite.Assert().NoError(loopback.Unloop(suite.loopbackDevice)) - } - - if suite.disk != nil { - suite.Assert().NoError(os.Remove(suite.disk.Name())) - suite.Assert().NoError(suite.disk.Close()) - } -} - -func (suite *manifestSuite) skipIfNotRoot() { - if os.Getuid() != 0 { - suite.T().Skip("can't run the test as non-root") - } -} - -func (suite *manifestSuite) skipUnderBuildkit() { - hostname, _ := os.Hostname() //nolint:errcheck - - if hostname == "buildkitsandbox" { - suite.T().Skip("test not supported under buildkit as partition devices are not propagated from /dev") - } -} - -func (suite *manifestSuite) TestCleanCorrupedXFSFileSystem() { - suite.skipUnderBuildkit() - - tempDir := suite.T().TempDir() - - mountDir := filepath.Join(tempDir, "var") - - suite.Assert().NoError(os.MkdirAll(mountDir, 0o700)) - suite.Require().NoError(makefs.XFS(suite.loopbackDevice.Name())) - - logger := log.New(os.Stderr, "", log.LstdFlags) - - mountpoint := mount.NewMountPoint(suite.loopbackDevice.Name(), mountDir, "xfs", unix.MS_NOATIME, "", mount.WithLogger(logger)) - - suite.Assert().NoError(mountpoint.Mount()) - - defer func() { - suite.Assert().NoError(mountpoint.Unmount()) - }() - - suite.Assert().NoError(mountpoint.Unmount()) - - // // now corrupt the disk - cmd := exec.Command("xfs_db", []string{ - "-x", - "-c blockget", - "-c blocktrash -s 512109 -n 1000", - suite.loopbackDevice.Name(), - }...) - - suite.Assert().NoError(cmd.Run()) - - suite.Assert().NoError(mountpoint.Mount()) -} diff --git a/internal/pkg/mount/options.go b/internal/pkg/mount/options.go deleted file mode 100644 index 42e76d1036..0000000000 --- a/internal/pkg/mount/options.go +++ /dev/null @@ -1,143 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package mount - -import ( - "log" - - "github.com/siderolabs/talos/internal/pkg/encryption/helpers" - "github.com/siderolabs/talos/pkg/machinery/config/config" -) - -const ( - // ReadOnly is a flag for setting the mount point as readonly. - ReadOnly Flags = 1 << iota - // Shared is a flag for setting the mount point as shared. - Shared - // Resize indicates that a the partition for a given mount point should be - // resized to the maximum allowed. - Resize - // Overlay indicates that a the partition for a given mount point should be - // mounted using overlayfs. - Overlay - // SystemOverlay indicates that overlay directory should be created under tmpfs. - // - // SystemOverlay should be combined with Overlay option. - SystemOverlay - // ReadonlyOverlay indicates that a the partition for a given mount point should be - // mounted using multi-layer readonly overlay from multiple partitions given as sources. - ReadonlyOverlay - // SkipIfMounted is a flag for skipping mount if the mountpoint is already mounted. - SkipIfMounted - // SkipIfNoFilesystem is a flag for skipping formatting and mounting if the mountpoint has not filesystem. - SkipIfNoFilesystem - // SkipIfNoDevice is a flag for skipping errors when the device is not found. - SkipIfNoDevice -) - -// Flags is the mount flags. -type Flags uint - -// Options is the functional options struct. -type Options struct { - Loopback string - Prefix string - MountFlags Flags - PreMountHooks []Hook - PostUnmountHooks []Hook - Encryption config.Encryption - SystemInformationGetter helpers.SystemInformationGetter - Logger *log.Logger - ProjectQuota bool -} - -// Option is the functional option func. -type Option func(*Options) - -// Check checks if all provided flags are set. -func (f Flags) Check(flags Flags) bool { - return (f & flags) == flags -} - -// Intersects checks if at least one flag is set. -func (f Flags) Intersects(flags Flags) bool { - return (f & flags) != 0 -} - -// WithPrefix is a functional option for setting the mount point prefix. -func WithPrefix(o string) Option { - return func(args *Options) { - args.Prefix = o - } -} - -// WithFlags is a functional option to set up mount flags. -func WithFlags(flags Flags) Option { - return func(args *Options) { - args.MountFlags |= flags - } -} - -// WithPreMountHooks adds functions to be called before mounting the partition. -func WithPreMountHooks(hooks ...Hook) Option { - return func(args *Options) { - args.PreMountHooks = append(args.PreMountHooks, hooks...) - } -} - -// WithPostUnmountHooks adds functions to be called after unmounting the partition. -func WithPostUnmountHooks(hooks ...Hook) Option { - return func(args *Options) { - args.PostUnmountHooks = append(args.PostUnmountHooks, hooks...) - } -} - -// WithEncryptionConfig partition encryption configuration. -func WithEncryptionConfig(cfg config.Encryption) Option { - return func(args *Options) { - args.Encryption = cfg - } -} - -// WithLogger sets the logger. -func WithLogger(logger *log.Logger) Option { - return func(args *Options) { - args.Logger = logger - } -} - -// WithProjectQuota enables project quota mount option. -func WithProjectQuota(enable bool) Option { - return func(args *Options) { - args.ProjectQuota = enable - } -} - -// WithSystemInformationGetter the function to get system information on the node. -func WithSystemInformationGetter(getter helpers.SystemInformationGetter) Option { - return func(args *Options) { - args.SystemInformationGetter = getter - } -} - -// Hook represents pre/post mount hook. -type Hook func(p *Point) error - -// NewDefaultOptions initializes a Options struct with default values. -func NewDefaultOptions(setters ...Option) *Options { - opts := &Options{ - Loopback: "", - Prefix: "", - MountFlags: 0, - PreMountHooks: []Hook{}, - PostUnmountHooks: []Hook{}, - } - - for _, setter := range setters { - setter(opts) - } - - return opts -} diff --git a/internal/pkg/mount/pseudo.go b/internal/pkg/mount/pseudo.go deleted file mode 100644 index d89f642b8d..0000000000 --- a/internal/pkg/mount/pseudo.go +++ /dev/null @@ -1,45 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package mount - -import ( - "os" - - "golang.org/x/sys/unix" - - "github.com/siderolabs/talos/pkg/machinery/constants" -) - -// PseudoMountPoints returns the mountpoints required to boot the system. -func PseudoMountPoints() (mountpoints *Points, err error) { - pseudo := NewMountPoints() - pseudo.Set("dev", NewMountPoint("devtmpfs", "/dev", "devtmpfs", unix.MS_NOSUID, "mode=0755")) - pseudo.Set("proc", NewMountPoint("proc", "/proc", "proc", unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NODEV, "")) - pseudo.Set("sys", NewMountPoint("sysfs", "/sys", "sysfs", 0, "")) - pseudo.Set("run", NewMountPoint("tmpfs", "/run", "tmpfs", unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_RELATIME, "mode=755")) - pseudo.Set("system", NewMountPoint("tmpfs", "/system", "tmpfs", 0, "mode=755")) - pseudo.Set("tmp", NewMountPoint("tmpfs", "/tmp", "tmpfs", unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NODEV, "size=64M,mode=755")) - - return pseudo, nil -} - -// PseudoSubMountPoints returns the mountpoints required to boot the system. -func PseudoSubMountPoints() (mountpoints *Points, err error) { - pseudo := NewMountPoints() - pseudo.Set("devshm", NewMountPoint("tmpfs", "/dev/shm", "tmpfs", unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NODEV|unix.MS_RELATIME, "")) - pseudo.Set("devpts", NewMountPoint("devpts", "/dev/pts", "devpts", unix.MS_NOSUID|unix.MS_NOEXEC, "ptmxmode=000,mode=620,gid=5")) - pseudo.Set("hugetlb", NewMountPoint("hugetlbfs", "/dev/hugepages", "hugetlbfs", unix.MS_NOSUID|unix.MS_NODEV, "")) - pseudo.Set("securityfs", NewMountPoint("securityfs", "/sys/kernel/security", "securityfs", unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NODEV|unix.MS_RELATIME, "")) - pseudo.Set("tracefs", NewMountPoint("securityfs", "/sys/kernel/tracing", "tracefs", unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NODEV, "")) - - if _, err := os.Stat(constants.EFIVarsMountPoint); err == nil { - // mount EFI vars if they exist - pseudo.Set("efivars", NewMountPoint("efivarfs", constants.EFIVarsMountPoint, "efivarfs", unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NODEV|unix.MS_RELATIME|unix.MS_RDONLY, "", - WithFlags(SkipIfNoDevice), - )) - } - - return pseudo, nil -} diff --git a/internal/pkg/mount/squashfs.go b/internal/pkg/mount/squashfs.go deleted file mode 100644 index 25ebfcc493..0000000000 --- a/internal/pkg/mount/squashfs.go +++ /dev/null @@ -1,27 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package mount - -import ( - "github.com/freddierice/go-losetup/v2" - "golang.org/x/sys/unix" - - "github.com/siderolabs/talos/pkg/machinery/constants" -) - -// SquashfsMountPoints returns the mountpoints required to boot the system. -func SquashfsMountPoints(prefix string) (mountpoints *Points, err error) { - var dev losetup.Device - - dev, err = losetup.Attach("/"+constants.RootfsAsset, 0, true) - if err != nil { - return nil, err - } - - squashfs := NewMountPoints() - squashfs.Set("squashfs", NewMountPoint(dev.Path(), "/", "squashfs", unix.MS_RDONLY|unix.MS_I_VERSION, "", WithPrefix(prefix), WithFlags(ReadOnly|Shared))) - - return squashfs, nil -} diff --git a/internal/pkg/mount/switchroot/switchroot.go b/internal/pkg/mount/switchroot/switchroot.go index 1be2458b38..b3185391f6 100644 --- a/internal/pkg/mount/switchroot/switchroot.go +++ b/internal/pkg/mount/switchroot/switchroot.go @@ -2,6 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Package switchroot provides the switching root filesystem functionality. package switchroot import ( @@ -14,7 +15,7 @@ import ( "github.com/siderolabs/go-procfs/procfs" "golang.org/x/sys/unix" - "github.com/siderolabs/talos/internal/pkg/mount" + "github.com/siderolabs/talos/internal/pkg/mount/v2" "github.com/siderolabs/talos/internal/pkg/secureboot" "github.com/siderolabs/talos/internal/pkg/secureboot/tpm2" "github.com/siderolabs/talos/pkg/machinery/constants" @@ -31,10 +32,10 @@ var preservedPaths = map[string]struct{}{ // https://github.com/karelzak/util-linux/blob/master/sys-utils/switch_root.c. // //nolint:gocyclo -func Switch(prefix string, mountpoints *mount.Points) (err error) { +func Switch(prefix string, mountpoints mount.Points) (err error) { log.Println("moving mounts to the new rootfs") - if err = mount.Move(mountpoints, prefix); err != nil { + if err = mountpoints.Move(prefix); err != nil { return err } diff --git a/internal/pkg/mount/unmount.go b/internal/pkg/mount/unmount.go deleted file mode 100644 index e877ada251..0000000000 --- a/internal/pkg/mount/unmount.go +++ /dev/null @@ -1,77 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package mount - -import ( - "context" - "fmt" - "log" - "time" - - "golang.org/x/sys/unix" -) - -func unmountLoop(ctx context.Context, logger *log.Logger, target string, flags int, timeout time.Duration, extraMessage string) (bool, error) { - errCh := make(chan error, 1) - - go func() { - errCh <- unix.Unmount(target, flags) - }() - - start := time.Now() - - progessTicker := time.NewTicker(timeout / 5) - defer progessTicker.Stop() - -unmountLoop: - for { - select { - case <-ctx.Done(): - return true, ctx.Err() - case err := <-errCh: - return true, err - case <-progessTicker.C: - timeLeft := timeout - time.Since(start) - - if timeLeft <= 0 { - break unmountLoop - } - - if logger != nil { - logger.Printf("unmounting %s%s is taking longer than expected, still waiting for %s", target, extraMessage, timeLeft) - } - } - } - - return false, nil -} - -// SafeUnmount unmounts the target path, first without force, then with force if the first attempt fails. -// -// It makes sure that unmounting has a finite operation timeout. -func SafeUnmount(ctx context.Context, logger *log.Logger, target string) error { - const ( - unmountTimeout = 90 * time.Second - unmountForceTimeout = 10 * time.Second - ) - - ok, err := unmountLoop(ctx, logger, target, 0, unmountTimeout, "") - - if ok { - return err - } - - if logger != nil { - logger.Printf("unmounting %s with force", target) - } - - ok, err = unmountLoop(ctx, logger, target, unix.MNT_FORCE, unmountTimeout, " with force flag") - - if ok { - return err - } - - return fmt.Errorf("unmounting %s with force flag timed out", target) -} diff --git a/internal/pkg/mount/all.go b/internal/pkg/mount/v2/all.go similarity index 100% rename from internal/pkg/mount/all.go rename to internal/pkg/mount/v2/all.go diff --git a/internal/pkg/mount/v2/cgroups.go b/internal/pkg/mount/v2/cgroups.go new file mode 100644 index 0000000000..fb2bb72a05 --- /dev/null +++ b/internal/pkg/mount/v2/cgroups.go @@ -0,0 +1,62 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package mount + +import ( + "path/filepath" + + "github.com/siderolabs/go-pointer" + "github.com/siderolabs/go-procfs/procfs" + "golang.org/x/sys/unix" + + "github.com/siderolabs/talos/pkg/machinery/constants" +) + +// ForceGGroupsV1 returns the cgroup version to be used (only for !container mode). +func ForceGGroupsV1() bool { + return pointer.SafeDeref(procfs.ProcCmdline().Get(constants.KernelParamCGroups).First()) == "0" +} + +// CGroupMountPoints returns the cgroup mount points. +func CGroupMountPoints() Points { + if ForceGGroupsV1() { + return cgroupMountPointsV1() + } + + return cgroupMountPointsV2() +} + +func cgroupMountPointsV2() Points { + return Points{ + NewPoint("cgroup", constants.CgroupMountPath, "cgroup2", WithFlags(unix.MS_NOSUID|unix.MS_NODEV|unix.MS_NOEXEC|unix.MS_RELATIME), WithData("nsdelegate,memory_recursiveprot")), + } +} + +func cgroupMountPointsV1() Points { + points := Points{ + NewPoint("tmpfs", constants.CgroupMountPath, "tmpfs", WithFlags(unix.MS_NOSUID|unix.MS_NODEV|unix.MS_NOEXEC|unix.MS_RELATIME), WithData("mode=755")), + } + + for _, controller := range []string{ + "blkio", + "cpu", + "cpuacct", + "cpuset", + "devices", + "freezer", + "hugetlb", + "memory", + "net_cls", + "net_prio", + "perf_event", + "pids", + } { + points = append(points, + NewPoint("cgroup", filepath.Join(constants.CgroupMountPath, controller), "cgroup", WithFlags(unix.MS_NOSUID|unix.MS_NODEV|unix.MS_NOEXEC|unix.MS_RELATIME), WithData(controller)), + ) + } + + return points +} diff --git a/internal/pkg/mount/v2/mount.go b/internal/pkg/mount/v2/mount.go index a2e75b724b..96f1a81bfe 100644 --- a/internal/pkg/mount/v2/mount.go +++ b/internal/pkg/mount/v2/mount.go @@ -10,11 +10,15 @@ import ( "context" "fmt" "os" + "path/filepath" + "slices" "strings" "time" "github.com/siderolabs/go-retry/retry" "golang.org/x/sys/unix" + + "github.com/siderolabs/talos/pkg/machinery/constants" ) // Point represents a mount point. @@ -24,6 +28,9 @@ type Point struct { fstype string flags uintptr data string + + shared bool + extraDirs []string } // NewPointOption is a mount point option. @@ -36,11 +43,18 @@ func WithProjectQuota(enabled bool) NewPointOption { return } + WithData("prjquota")(p) + } +} + +// WithData sets the mount data. +func WithData(data string) NewPointOption { + return func(p *Point) { if len(p.data) > 0 { p.data += "," } - p.data += "prjquota" + p.data += data } } @@ -56,6 +70,20 @@ func WithReadonly() NewPointOption { return WithFlags(unix.MS_RDONLY) } +// WithShared sets the shared flag. +func WithShared() NewPointOption { + return func(p *Point) { + p.shared = true + } +} + +// WithExtraDirs sets the extra directories to be created on mount. +func WithExtraDirs(dirs ...string) NewPointOption { + return func(p *Point) { + p.extraDirs = append(p.extraDirs, dirs...) + } +} + // NewPoint creates a new mount point. func NewPoint(source, target, fstype string, opts ...NewPointOption) *Point { p := &Point{ @@ -71,6 +99,41 @@ func NewPoint(source, target, fstype string, opts ...NewPointOption) *Point { return p } +// NewReadonlyOverlay creates a new read-only overlay mount point. +func NewReadonlyOverlay(sources []string, target string, opts ...NewPointOption) *Point { + opts = append(opts, WithReadonly(), WithData("lowerdir="+strings.Join(sources, ":"))) + + return NewPoint("overlay", target, "overlay", opts...) +} + +// NewVarOverlay creates a new /var overlay mount point. +func NewVarOverlay(sources []string, target string, opts ...NewPointOption) *Point { + return NewOverlayWithBasePath(sources, target, constants.VarSystemOverlaysPath, opts...) +} + +// NewSystemOverlay creates a new /system overlay mount point. +func NewSystemOverlay(sources []string, target string, opts ...NewPointOption) *Point { + return NewOverlayWithBasePath(sources, target, constants.SystemOverlaysPath, opts...) +} + +// NewOverlayWithBasePath creates a new overlay mount point with a base path. +func NewOverlayWithBasePath(sources []string, target, basePath string, opts ...NewPointOption) *Point { + _, overlayPrefix, _ := strings.Cut(target, "/") + overlayPrefix = strings.ReplaceAll(overlayPrefix, "/", "-") + + diff := fmt.Sprintf(filepath.Join(basePath, "%s-diff"), overlayPrefix) + workdir := fmt.Sprintf(filepath.Join(basePath, "%s-workdir"), overlayPrefix) + + opts = append(opts, + WithData("lowerdir="+strings.Join(sources, ":")), + WithData("upperdir="+diff), + WithData("workdir="+workdir), + WithExtraDirs(diff, workdir), + ) + + return NewPoint("overlay", target, "overlay", opts...) +} + // PrinterOptions are printer options. type PrinterOptions struct { Printer func(string, ...any) @@ -177,8 +240,10 @@ func (p *Point) Mount(opts ...OperationOption) (unmounter func() error, err erro } } - if err = os.MkdirAll(p.target, options.TargetMode); err != nil { - return nil, fmt.Errorf("error creating mount point directory %s: %w", p.target, err) + for _, dir := range slices.Concat(p.extraDirs, []string{p.target}) { + if err = os.MkdirAll(dir, options.TargetMode); err != nil { + return nil, fmt.Errorf("error creating mount point directory %s: %w", dir, err) + } } err = p.retry(p.mount, false, options.PrinterOptions) @@ -186,6 +251,12 @@ func (p *Point) Mount(opts ...OperationOption) (unmounter func() error, err erro return nil, fmt.Errorf("error mounting %s: %w", p.source, err) } + if p.shared { + if err = p.share(); err != nil { + return nil, fmt.Errorf("error sharing %s: %w", p.target, err) + } + } + return func() error { return p.Unmount(WithUnmountPrinter(options.Printer)) }, nil @@ -213,6 +284,11 @@ func (p *Point) Unmount(opts ...UnmountOption) error { }, true, options.PrinterOptions) } +// Move the mount point to a new target. +func (p *Point) Move(newTarget string) error { + return unix.Mount(p.target, newTarget, "", unix.MS_MOVE, "") +} + func (p *Point) mount() error { return unix.Mount(p.source, p.target, p.fstype, p.flags, p.data) } @@ -221,6 +297,10 @@ func (p *Point) unmount(printer func(string, ...any)) error { return SafeUnmount(context.Background(), printer, p.target) } +func (p *Point) share() error { + return unix.Mount("", p.target, "", unix.MS_SHARED|unix.MS_REC, "") +} + //nolint:gocyclo func (p *Point) retry(f func() error, isUnmount bool, printerOptions PrinterOptions) error { return retry.Constant(5*time.Second, retry.WithUnits(50*time.Millisecond)).Retry(func() error { diff --git a/internal/pkg/mount/overlay.go b/internal/pkg/mount/v2/overlay.go similarity index 61% rename from internal/pkg/mount/overlay.go rename to internal/pkg/mount/v2/overlay.go index 19c8dd986c..053998c0b4 100644 --- a/internal/pkg/mount/overlay.go +++ b/internal/pkg/mount/v2/overlay.go @@ -5,6 +5,7 @@ package mount import ( + "github.com/siderolabs/gen/xslices" "golang.org/x/sys/unix" "github.com/siderolabs/talos/pkg/machinery/constants" @@ -12,13 +13,8 @@ import ( // OverlayMountPoints returns the mountpoints required to boot the system. // These mountpoints are used as overlays on top of the read only rootfs. -func OverlayMountPoints() (mountpoints *Points, err error) { - mountpoints = NewMountPoints() - - for _, target := range constants.Overlays { - mountpoint := NewMountPoint("", target, "", unix.MS_I_VERSION, "", WithFlags(Overlay)) - mountpoints.Set(target, mountpoint) - } - - return mountpoints, nil +func OverlayMountPoints() Points { + return xslices.Map(constants.Overlays, func(target string) *Point { + return NewVarOverlay([]string{target}, target, WithFlags(unix.MS_I_VERSION)) + }) } diff --git a/internal/pkg/mount/v2/points.go b/internal/pkg/mount/v2/points.go index 4232bbdf25..8072ae5f39 100644 --- a/internal/pkg/mount/v2/points.go +++ b/internal/pkg/mount/v2/points.go @@ -6,6 +6,7 @@ package mount import ( "errors" + "path/filepath" "slices" ) @@ -55,3 +56,14 @@ func (points Points) Unmount() error { return nil } + +// Move all mount points to a new prefix. +func (points Points) Move(prefix string) error { + for _, point := range points { + if err := point.Move(filepath.Join(prefix, point.target)); err != nil { + return err + } + } + + return nil +} diff --git a/internal/pkg/mount/v2/pseudo.go b/internal/pkg/mount/v2/pseudo.go new file mode 100644 index 0000000000..157a19c757 --- /dev/null +++ b/internal/pkg/mount/v2/pseudo.go @@ -0,0 +1,46 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package mount + +import ( + "os" + + "golang.org/x/sys/unix" + + "github.com/siderolabs/talos/pkg/machinery/constants" +) + +// Pseudo returns the mountpoints required to boot the system. +func Pseudo() Points { + return Points{ + NewPoint("devtmpfs", "/dev", "devtmpfs", WithFlags(unix.MS_NOSUID), WithData("mode=0755")), + NewPoint("proc", "/proc", "proc", WithFlags(unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NODEV)), + NewPoint("sysfs", "/sys", "sysfs"), + NewPoint("tmpfs", "/run", "tmpfs", WithFlags(unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_RELATIME), WithData("mode=0755")), + NewPoint("tmpfs", "/system", "tmpfs", WithData("mode=0755")), + NewPoint("tmpfs", "/tmp", "tmpfs", WithFlags(unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NODEV), WithData("size=64M"), WithData("mode=0755")), + } +} + +// PseudoSubMountPoints returns the sub-mountpoints under Pseudo(). +func PseudoSubMountPoints() Points { + points := Points{ + NewPoint("devshm", "/dev/shm", "tmpfs", WithFlags(unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NODEV|unix.MS_RELATIME)), + NewPoint("devpts", "/dev/pts", "devpts", WithFlags(unix.MS_NOSUID|unix.MS_NOEXEC), WithData("ptmxmode=000,mode=620,gid=5")), + NewPoint("hugetlb", "/dev/hugepages", "hugetlbfs", WithFlags(unix.MS_NOSUID|unix.MS_NODEV)), + NewPoint("bpf", "/sys/fs/bpf", "bpf"), + NewPoint("securityfs", "/sys/kernel/security", "securityfs", WithFlags(unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NODEV|unix.MS_RELATIME)), + NewPoint("tracefs", "/sys/kernel/tracing", "tracefs", WithFlags(unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NODEV)), + } + + if _, err := os.Stat(constants.EFIVarsMountPoint); err == nil { + // mount EFI vars if they exist + points = append(points, + NewPoint("efivars", constants.EFIVarsMountPoint, "efivarfs", WithFlags(unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NODEV|unix.MS_RELATIME|unix.MS_RDONLY)), + ) + } + + return points +} diff --git a/internal/pkg/mount/v2/repair_test.go b/internal/pkg/mount/v2/repair_test.go new file mode 100644 index 0000000000..4ce4c6347e --- /dev/null +++ b/internal/pkg/mount/v2/repair_test.go @@ -0,0 +1,116 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package mount_test + +import ( + "errors" + randv2 "math/rand/v2" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "github.com/freddierice/go-losetup/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" + + "github.com/siderolabs/talos/internal/pkg/mount/v2" + "github.com/siderolabs/talos/pkg/makefs" +) + +// Some tests in this package cannot be run under buildkit, as buildkit doesn't propagate partition devices +// like /dev/loopXpY into the sandbox. To run the tests on your local computer, do the following: +// +// go test -exec sudo -v --count 1 github.com/siderolabs/talos/internal/pkg/mount/v2 + +const diskSize = 4 * 1024 * 1024 * 1024 // 4 GiB + +func losetupAttachHelper(t *testing.T, rawImage string, readonly bool) losetup.Device { + t.Helper() + + for range 10 { + loDev, err := losetup.Attach(rawImage, 0, readonly) + if err != nil { + if errors.Is(err, unix.EBUSY) { + spraySleep := max(randv2.ExpFloat64(), 2.0) + + t.Logf("retrying after %v seconds", spraySleep) + + time.Sleep(time.Duration(spraySleep * float64(time.Second))) + + continue + } + } + + require.NoError(t, err) + + return loDev + } + + t.Fatal("failed to attach loop device") //nolint:revive + + panic("unreachable") +} + +func TestRepair(t *testing.T) { + if os.Getuid() != 0 { + t.Skip("can't run the test as non-root") + } + + hostname, _ := os.Hostname() //nolint:errcheck + + if hostname == "buildkitsandbox" { + t.Skip("test not supported under buildkit as partition devices are not propagated from /dev") + } + + tmpDir := t.TempDir() + + rawImage := filepath.Join(tmpDir, "image.raw") + + f, err := os.Create(rawImage) + require.NoError(t, err) + + require.NoError(t, f.Truncate(int64(diskSize))) + require.NoError(t, f.Close()) + + loDev := losetupAttachHelper(t, rawImage, false) + + t.Cleanup(func() { + assert.NoError(t, loDev.Detach()) + }) + + mountDir := filepath.Join(tmpDir, "var") + + require.NoError(t, os.MkdirAll(mountDir, 0o700)) + require.NoError(t, makefs.XFS(loDev.Path())) + + mountPoint := mount.NewPoint(loDev.Path(), mountDir, "xfs", mount.WithFlags(unix.MS_NOATIME)) + + unmounter1, err := mountPoint.Mount(mount.WithMountPrinter(t.Logf)) + require.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, unmounter1()) + }) + + assert.NoError(t, unmounter1()) + + // // now corrupt the disk + cmd := exec.Command("xfs_db", []string{ + "-x", + "-c blockget", + "-c blocktrash -s 512109 -n 100", + loDev.Path(), + }...) + + assert.NoError(t, cmd.Run()) + + unmounter2, err := mountPoint.Mount(mount.WithMountPrinter(t.Logf)) + require.NoError(t, err) + + require.NoError(t, unmounter2()) +} diff --git a/internal/pkg/mount/v2/squashfs.go b/internal/pkg/mount/v2/squashfs.go new file mode 100644 index 0000000000..940b719f97 --- /dev/null +++ b/internal/pkg/mount/v2/squashfs.go @@ -0,0 +1,20 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package mount + +import ( + "github.com/freddierice/go-losetup/v2" + "golang.org/x/sys/unix" +) + +// Squashfs binds the squashfs to the loop device and returns the mountpoint for it to the specified target. +func Squashfs(target, squashfsFile string) (*Point, error) { + dev, err := losetup.Attach(squashfsFile, 0, true) + if err != nil { + return nil, err + } + + return NewPoint(dev.Path(), target, "squashfs", WithFlags(unix.MS_RDONLY|unix.MS_I_VERSION), WithShared()), nil +}