package storage import ( "fmt" "os" "path/filepath" "sort" "strings" ) // Durable BLOCK-DEVICE identity for the slice-10B operator-signed wipe (anti-retarget). The // signed op binds a DURABLE id (a WWN / hardware serial, falling back to a filesystem UUID), and // execution re-resolves it to the CURRENT /dev path — so "wipe device X" wipes that exact physical // device, never whatever happens to be at /dev/sdb now. These reads are world-readable udev // symlinks under /dev/disk/by-id and /dev/disk/by-uuid — NO privilege and NO subprocess (the // root-CLI fence is untouched). // // devDiskRoot is overridable for tests (a fake /dev/disk layout). var devDiskRoot = "/dev/disk" // durable-id scheme prefixes. byid: a /dev/disk/by-id/ entry (preferred — wwn/serial, // survives reformat + re-cabling); byuuid: a filesystem UUID (fallback — survives re-cabling but // not a reformat, acceptable for a one-shot wipe resolved immediately before wiping). const ( durableByID = "byid:" durableByUUID = "byuuid:" ) // byIDPriority ranks /dev/disk/by-id link prefixes most-stable-first: wwn (hardware world-wide // name) > nvme-eui (NVMe EUI) > nvme-/ata-/scsi-/usb- (model+serial). dm-/lvm-/md- names are // excluded (they are mapper constructs, not the physical disk we want to bind a wipe to). var byIDPriority = []string{"wwn-", "nvme-eui.", "nvme-", "ata-", "scsi-", "usb-"} // DeviceDurableID derives a stable durable identifier for a block device by scanning the // world-readable udev symlinks. Prefers a by-id (wwn/serial) link, then a filesystem UUID. Errors // when neither is resolvable (a device with no durable identity cannot be safely wipe-bound). func DeviceDurableID(device string) (string, error) { target, err := filepath.EvalSymlinks(device) if err != nil { return "", fmt.Errorf("storage: resolve %s: %w", device, err) } // 1. best by-id link pointing at this device. if name := bestByIDLink(target); name != "" { return durableByID + name, nil } // 2. fallback: a filesystem UUID symlink. if uuid := byUUIDFor(target); uuid != "" { return durableByUUID + uuid, nil } return "", fmt.Errorf("storage: no durable id (wwn/serial/uuid) for device %s", device) } // ResolveDurableDevice resolves a durable id back to the CURRENT canonical /dev path, or errors if // it no longer resolves (the device was physically removed/replaced — the wipe must then refuse). func ResolveDurableDevice(durableID string) (string, error) { switch { case strings.HasPrefix(durableID, durableByID): name := strings.TrimPrefix(durableID, durableByID) if !safeLinkName(name) { return "", fmt.Errorf("storage: unsafe durable id %q", durableID) } return filepath.EvalSymlinks(filepath.Join(devDiskRoot, "by-id", name)) case strings.HasPrefix(durableID, durableByUUID): uuid := strings.TrimPrefix(durableID, durableByUUID) if !safeLinkName(uuid) { return "", fmt.Errorf("storage: unsafe durable id %q", durableID) } return filepath.EvalSymlinks(filepath.Join(devDiskRoot, "by-uuid", uuid)) default: // A bare path or unknown scheme is REFUSED — a wipe must bind to a durable id, never a // mutable /dev path (the anti-retarget invariant). return "", fmt.Errorf("storage: durable id %q is not a durable scheme (byid:/byuuid:) — refusing path-only binding", durableID) } } // ResolveStorageDevice resolves an enrolled STORAGE durable-id (the `uuid:` scheme that // deriveDurableID emits for usb/local-dir drives) to its CURRENT backing /dev path by re-scanning // /dev/disk/by-uuid. This is the load-bearing host-reboot fix: kernel re-enumeration can move a drive // from /dev/sdb to /dev/sdc, so a remount MUST re-resolve by UUID (never trust a remembered node) — the // reshuffle is then a no-op. Errors if the UUID no longer resolves (the drive is genuinely absent), so a // caller can skip re-mounting a gone drive instead of failing on a stale node. func ResolveStorageDevice(durableID string) (string, error) { const scheme = "uuid:" if !strings.HasPrefix(durableID, scheme) { return "", fmt.Errorf("storage: durable id %q is not the uuid: scheme — cannot resolve by filesystem UUID", durableID) } uuid := strings.TrimPrefix(durableID, scheme) if !safeLinkName(uuid) { return "", fmt.Errorf("storage: unsafe durable id %q", durableID) } return filepath.EvalSymlinks(filepath.Join(devDiskRoot, "by-uuid", uuid)) } // bestByIDLink returns the highest-priority /dev/disk/by-id link name whose target is `device` // (already symlink-resolved), or "" if none. func bestByIDLink(device string) string { dir := filepath.Join(devDiskRoot, "by-id") entries, err := os.ReadDir(dir) if err != nil { return "" } // collect matching names, then pick by priority (then lexically for determinism). var matches []string for _, e := range entries { name := e.Name() if tgt, err := filepath.EvalSymlinks(filepath.Join(dir, name)); err == nil && tgt == device { matches = append(matches, name) } } if len(matches) == 0 { return "" } sort.Strings(matches) for _, pfx := range byIDPriority { for _, name := range matches { if strings.HasPrefix(name, pfx) { return name } } } return matches[0] // some by-id link exists but not a recognized prefix — still durable enough } // byUUIDFor returns the filesystem UUID whose by-uuid link targets `device`, or "". func byUUIDFor(device string) string { dir := filepath.Join(devDiskRoot, "by-uuid") entries, err := os.ReadDir(dir) if err != nil { return "" } for _, e := range entries { if tgt, err := filepath.EvalSymlinks(filepath.Join(dir, e.Name())); err == nil && tgt == device { return e.Name() } } return "" } // safeLinkName rejects a durable-id component that could escape the by-id/by-uuid dir (path // traversal / separators) — defense even though the value is operator-signed. func safeLinkName(s string) bool { if s == "" || strings.ContainsAny(s, "/\x00") || s == "." || s == ".." { return false } return true }