//go:build linux package storage import ( "fmt" "os" "path/filepath" "strings" "syscall" "time" "golang.org/x/sys/unix" ) // IsSystemDisk checks if the given device path overlaps with the root filesystem device. // Returns true if the device is (or is the parent of) the system disk. func IsSystemDisk(devicePath string) (bool, error) { // Get the block device major number of the root filesystem var rootStat syscall.Stat_t if err := syscall.Stat("/", &rootStat); err != nil { return false, fmt.Errorf("cannot stat /: %w", err) } // Get block device info of the target device (via /host-dev — Docker overrides /dev) var devStat syscall.Stat_t if err := syscall.Stat(HostDevicePath(devicePath), &devStat); err != nil { return false, fmt.Errorf("cannot stat %s: %w", devicePath, err) } // C5: Use unix.Major/Minor for correct 12-bit extraction (old 0xff mask truncated high bits). // Also compare the disk portion of the minor to distinguish separate physical disks of the // same type (e.g., sda and sdb both have major 8, but different disk-minor groups of 16). rootMajor := unix.Major(rootStat.Dev) rootMinor := unix.Minor(rootStat.Dev) devMajor := unix.Major(devStat.Rdev) devMinor := unix.Minor(devStat.Rdev) if rootMajor != devMajor { return false, nil } // Same major — compare disk groups (each disk gets 16 minor numbers on SCSI/SATA, // e.g., sda=0-15, sdb=16-31; NVMe uses similar grouping). rootDiskGroup := rootMinor / 16 devDiskGroup := devMinor / 16 return rootDiskGroup == devDiskGroup, nil } // IsDeviceMounted checks if a device or any of its partitions is currently mounted. func IsDeviceMounted(devicePath string) (bool, error) { data, err := os.ReadFile("/proc/mounts") if err != nil { return false, fmt.Errorf("cannot read /proc/mounts: %w", err) } base := filepath.Base(devicePath) for _, line := range strings.Split(string(data), "\n") { fields := strings.Fields(line) if len(fields) < 2 { continue } dev := fields[0] devBase := filepath.Base(dev) // H9: Require exact match or that the suffix after base is a digit or 'p' (partition marker). // Prevents /dev/sdb matching /dev/sdba (hypothetical device) or /dev/sdb_backup (bind). if devBase == base { return true, nil } if strings.HasPrefix(devBase, base) { next := devBase[len(base)] if next >= '0' && next <= '9' || next == 'p' { return true, nil } } } return false, nil } // IsMountPathInUse checks if a path is already used as a mount point. func IsMountPathInUse(mountPath string) (bool, error) { data, err := os.ReadFile("/proc/mounts") if err != nil { return false, fmt.Errorf("cannot read /proc/mounts: %w", err) } mountPath = filepath.Clean(mountPath) for _, line := range strings.Split(string(data), "\n") { fields := strings.Fields(line) if len(fields) < 2 { continue } if filepath.Clean(fields[1]) == mountPath { return true, nil } } return false, nil } // BackupFstab creates a dated backup of the fstab file. func BackupFstab(fstabPath string) error { data, err := os.ReadFile(fstabPath) if err != nil { return fmt.Errorf("cannot read %s: %w", fstabPath, err) } backupPath := fstabPath + ".bak." + time.Now().Format("20060102") return os.WriteFile(backupPath, data, 0644) } // AppendFstabEntry appends a UUID-based fstab entry atomically (write tmp + rename). // H8: Direct write to /etc/fstab risks corruption on crash — use atomic write pattern. func AppendFstabEntry(fstabPath, uuid, mountPoint, fsType, options string) error { // Read existing content existing, err := os.ReadFile(fstabPath) if err != nil && !os.IsNotExist(err) { return fmt.Errorf("cannot read fstab: %w", err) } entry := fmt.Sprintf("\nUUID=%s\t%s\t%s\t%s\t0 2\n", uuid, mountPoint, fsType, options) newContent := append(existing, []byte(entry)...) // Write to .tmp then rename — atomic on same filesystem tmpPath := fstabPath + ".tmp" if err := os.WriteFile(tmpPath, newContent, 0644); err != nil { return fmt.Errorf("cannot write fstab tmp file: %w", err) } if err := os.Rename(tmpPath, fstabPath); err != nil { os.Remove(tmpPath) return fmt.Errorf("cannot rename fstab tmp file: %w", err) } return nil } // RemoveFstabEntry removes any line containing the given UUID from fstab, atomically. // H19: Called as rollback if mount fails after fstab was written. func RemoveFstabEntry(fstabPath, uuid string) error { data, err := os.ReadFile(fstabPath) if err != nil { return fmt.Errorf("cannot read fstab: %w", err) } var kept []string for _, line := range strings.Split(string(data), "\n") { if !strings.Contains(line, "UUID="+uuid) { kept = append(kept, line) } } newContent := strings.Join(kept, "\n") tmpPath := fstabPath + ".tmp" if err := os.WriteFile(tmpPath, []byte(newContent), 0644); err != nil { return fmt.Errorf("cannot write fstab tmp file: %w", err) } if err := os.Rename(tmpPath, fstabPath); err != nil { os.Remove(tmpPath) return fmt.Errorf("cannot rename fstab tmp file: %w", err) } return nil }