package storage import ( "os" "path/filepath" "reflect" "strconv" "strings" "testing" ) // Fixture /sys/class/block + /run/udev/data + /proc/mounts trees, one per `16` §3 case. The tree mimics the // kernel: /sys/devices/...// real dirs (a partition carries a "partition" file), /sys/class/block // symlinks, a "dev" file per node, udev "E:" lines keyed by major:minor. The cases are the ones the spike // measured on real devices (felhom.eu documentation/audits/storage-identity-2026-10-15/). type fx struct { t *testing.T root string minor int mounts []string links map[string]string // /dev path -> kernel name virt string } func newFx(t *testing.T) *fx { t.Helper() r := t.TempDir() for _, d := range []string{"sys/class/block", "sys/devices", "udev"} { if err := os.MkdirAll(filepath.Join(r, d), 0o755); err != nil { t.Fatal(err) } } return &fx{t: t, root: r, links: map[string]string{}, virt: "none"} } func (f *fx) write(p, s string) { f.t.Helper() if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { f.t.Fatal(err) } if err := os.WriteFile(p, []byte(s), 0o644); err != nil { f.t.Fatal(err) } } // node makes a block node. parent "" → a top-level device. udev lines are "KEY=VAL". func (f *fx) node(name, parent string, udev ...string) { f.t.Helper() real := filepath.Join(f.root, "sys/devices", name) if parent != "" { pr, _ := filepath.EvalSymlinks(filepath.Join(f.root, "sys/class/block", parent)) real = filepath.Join(pr, name) f.write(filepath.Join(real, "partition"), "1\n") } f.minor++ dev := "8:" + strconv.Itoa(f.minor) f.write(filepath.Join(real, "dev"), dev+"\n") if err := os.Symlink(real, filepath.Join(f.root, "sys/class/block", name)); err != nil { f.t.Fatal(err) } var b strings.Builder for _, u := range udev { b.WriteString("E:" + u + "\n") } f.write(filepath.Join(f.root, "udev", "b"+dev), b.String()) f.links["/dev/"+name] = name } func (f *fx) slaves(name string, slaves ...string) { f.t.Helper() real, _ := filepath.EvalSymlinks(filepath.Join(f.root, "sys/class/block", name)) for _, s := range slaves { if err := os.MkdirAll(filepath.Join(real, "slaves", s), 0o755); err != nil { f.t.Fatal(err) } } } func (f *fx) loop(name, file string) { real, _ := filepath.EvalSymlinks(filepath.Join(f.root, "sys/class/block", name)) f.write(filepath.Join(real, "loop", "backing_file"), file+"\n") } func (f *fx) mount(dev, mp, fstype string) { f.mounts = append(f.mounts, dev+" "+mp+" "+fstype+" rw 0 0") } func (f *fx) resolver() *IdentityResolver { f.write(filepath.Join(f.root, "mounts"), strings.Join(f.mounts, "\n")+"\n") return &IdentityResolver{Host: &SysfsIdentityHost{ SysClass: filepath.Join(f.root, "sys/class/block"), UdevData: filepath.Join(f.root, "udev"), ProcMounts: filepath.Join(f.root, "mounts"), DevResolve: func(p string) (string, error) { if n, ok := f.links[p]; ok { return "/dev/" + n, nil } return p, nil }, VirtFn: func() string { return f.virt }, }} } var sanDisk = []string{"ID_BUS=ata", "ID_MODEL=SanDisk_X600", "ID_SERIAL_SHORT=182195804614", "ID_WWN=0x5001b448b60a9d6e"} // oneDiskLVM builds the PVE default layout on one SATA SSD: sda3 → pve-root, pve-data tmeta/tdata → tpool → // a guest volume (the shape of all three test boxes). func oneDiskLVM(f *fx) { f.node("sda", "", sanDisk...) f.node("sda3", "sda") for _, d := range []string{"dm-1", "dm-2", "dm-3", "dm-4", "dm-6"} { f.node(d, "") } f.slaves("dm-1", "sda3") f.slaves("dm-2", "sda3") f.slaves("dm-3", "sda3") f.slaves("dm-4", "dm-2", "dm-3") f.slaves("dm-6", "dm-4") f.links["/dev/mapper/pve-root"] = "dm-1" f.links["/dev/mapper/pve-data-tpool"] = "dm-4" f.links["/dev/pve/vm-9201-disk-1"] = "dm-6" f.mount("/dev/mapper/pve-root", "/", "ext4") } func TestIdentity_OneDiskLVM_AllPlacesSameProvenDisk(t *testing.T) { f := newFx(t) oneDiskLVM(f) r := f.resolver() want := []string{"serial:ata:SanDisk_X600:182195804614"} for _, p := range []PlaceIdentity{r.HostPath("/var/lib/vz"), r.Device("/dev/mapper/pve-data-tpool"), r.Device("/dev/pve/vm-9201-disk-1")} { if p.Identity != IdentityProven || !reflect.DeepEqual(p.PhysicalDisks, want) { t.Fatalf("want proven %v, got %+v", want, p) } } diff, proven, why := Relation(r.HostPath("/var/lib/vz"), r.Device("/dev/pve/vm-9201-disk-1")) if diff || !proven || why != "same_disk" { t.Fatalf("root vs guest volume: want same+proven, got diff=%v proven=%v why=%q", diff, proven, why) } } func TestIdentity_TwoDisks_ProvenDifferent(t *testing.T) { f := newFx(t) oneDiskLVM(f) f.node("sdb", "", "ID_BUS=ata", "ID_MODEL=TOSHIBA_MQ04ABF100", "ID_SERIAL_SHORT=65NOP3HDT", "ID_WWN=0x5000039ddb108568", "DEVPATH=/devices/pci0000:00/0000:00:14.0/usb1/1-3/block/sdb") f.mount("/dev/sdb", "/mnt/hdd_1", "ext4") f.mount("/dev/sdb", "/mnt/felhom-drives/hdd_1", "ext4") // the felhom-data bind shows the source device r := f.resolver() drive := r.HostPath("/mnt/felhom-drives/hdd_1/userdata") if drive.Identity != IdentityProven || drive.PhysicalDisks[0] != "serial:ata:TOSHIBA_MQ04ABF100:65NOP3HDT" { t.Fatalf("USB drive passing its identity: want proven, got %+v", drive) } diff, proven, why := Relation(r.Device("/dev/pve/vm-9201-disk-1"), drive) if !diff || !proven || why != "" { t.Fatalf("system vs drive: want different+proven, got %v %v %q", diff, proven, why) } } func TestIdentity_TwoPartitionsOneDisk_Same(t *testing.T) { f := newFx(t) f.node("sdc", "", "ID_BUS=usb", "ID_MODEL=Portable", "ID_SERIAL_SHORT=NAAB7C21") f.node("sdc1", "sdc") f.node("sdc2", "sdc") f.mount("/dev/sdc1", "/mnt/a", "ext4") f.mount("/dev/sdc2", "/mnt/b", "ext4") r := f.resolver() diff, proven, why := Relation(r.HostPath("/mnt/a"), r.HostPath("/mnt/b")) if diff || !proven || why != "same_disk" { t.Fatalf("two partitions of one disk: want same+proven, got %v %v %q", diff, proven, why) } } func TestIdentity_LoopPartitionsOnAFile_ResolveToTheFilesDisk(t *testing.T) { f := newFx(t) f.node("nvme0n1", "", "ID_MODEL=KXG50PNV1T02", "ID_SERIAL_SHORT=58BS11AFT8MQ", "ID_WWN=eui.000000000000001000080d020046c07b") f.mount("/dev/nvme0n1", "/mnt/hdd_1", "ext4") f.node("loop2", "") f.loop("loop2", "/mnt/hdd_1/scratch.img") f.node("loop2p1", "loop2") // the partition step must run BEFORE the loop step (the spike's probe bug) f.mount("/dev/loop2p1", "/tmp/scrA", "ext4") r := f.resolver() p := r.HostPath("/tmp/scrA") if p.Identity != IdentityProven || !reflect.DeepEqual(p.PhysicalDisks, []string{"eui:000000000000001000080d020046c07b"}) { t.Fatalf("loop partition: want the NVMe's eui, got %+v", p) } } func TestIdentity_WWNWithoutVendor(t *testing.T) { f := newFx(t) f.node("sda", "", "ID_BUS=ata", "ID_MODEL=AirDisk_512GB_SSD", "ID_SERIAL_SHORT=QDF922W009654S30EX", "ID_WWN=0x5000000123456f1f") f.node("sdb", "", "ID_BUS=ata", "ID_MODEL=Cheap", "ID_WWN=0x5000000123456f1f") r := f.resolver() if p := r.Device("/dev/sda"); p.Identity != IdentityProven || p.PhysicalDisks[0] != "serial:ata:AirDisk_512GB_SSD:QDF922W009654S30EX" { t.Fatalf("a real serial wins over a vendor-less WWN: got %+v", p) } if p := r.Device("/dev/sdb"); p.Identity != IdentityUnknown || p.Reason != ReasonWeakWWN { t.Fatalf("no serial + vendor-less WWN: want unknown/weak_wwn, got %+v", p) } } func TestIdentity_NoSerial(t *testing.T) { f := newFx(t) f.node("vdb", "", "ID_MODEL=") if p := f.resolver().Device("/dev/vdb"); p.Identity != IdentityUnknown || p.Reason != ReasonNoSerial { t.Fatalf("want unknown/no_serial, got %+v", p) } } func TestIdentity_InsideAVM_EverythingUnknown(t *testing.T) { f := newFx(t) oneDiskLVM(f) f.virt = "kvm" f.node("sdb", "", "ID_BUS=scsi", "ID_MODEL=QEMU_HARDDISK", "ID_SERIAL_SHORT=drive-scsi1") f.mount("/dev/sdb", "/mnt/felhom-drives/adatlemez", "ext4") r := f.resolver() a, b := r.Device("/dev/pve/vm-9201-disk-1"), r.HostPath("/mnt/felhom-drives/adatlemez") for _, p := range []PlaceIdentity{a, b} { if p.Identity != IdentityUnknown || p.Reason != ReasonVM { t.Fatalf("inside a VM: want unknown/vm, got %+v", p) } } diff, proven, why := Relation(a, b) if proven || why != ReasonVM || !diff { t.Fatalf("two virtual disks: want not proven (vm), got %v %v %q", diff, proven, why) } } func TestIdentity_USBBridgeSerial(t *testing.T) { f := newFx(t) f.node("sdd", "", "ID_BUS=usb", "ID_MODEL=External_USB3.0", "ID_SERIAL_SHORT=000000000000") f.node("sde", "", "ID_BUS=usb", "ID_MODEL=External_USB3.0", "ID_SERIAL_SHORT=20231106A1B2") r := f.resolver() if p := r.Device("/dev/sdd"); p.Identity != IdentityUnknown || p.Reason != ReasonUSBBridge { t.Fatalf("a bridge placeholder serial: want unknown/usb_bridge, got %+v", p) } if p := r.Device("/dev/sde"); p.Identity != IdentityProven { t.Fatalf("a USB disk with a plausible serial: want proven, got %+v", p) } } func TestIdentity_NetworkShareAndUnmapped(t *testing.T) { f := newFx(t) oneDiskLVM(f) f.mount("//192.168.0.123/TesztFilmek", "/mnt/felhom-drives/omv-filmek", "cifs") r := f.resolver() n := r.HostPath("/mnt/felhom-drives/omv-filmek/x") if n.Identity != IdentityUnknown || n.Reason != ReasonNetwork || len(n.PhysicalDisks) != 0 { t.Fatalf("network share: want unknown/network with no disks, got %+v", n) } _, proven, why := Relation(r.HostPath("/"), n) if proven || why != ReasonNetwork { t.Fatalf("system vs network: want not proven (network), got %v %q", proven, why) } if _, proven, why := Relation(PlaceIdentity{}, n); proven || why != "unmapped" { t.Fatalf("an uncomputed side: want unmapped, got %v %q", proven, why) } }