From adc9183cf62973886e0b68e5cff84abee3cf508a Mon Sep 17 00:00:00 2001 From: Dominik Eisenberg <64131471+Dominik-esb@users.noreply.github.com> Date: Sun, 11 Oct 2026 04:55:40 +0200 Subject: [PATCH] mscluster: accept short resource class information (#2669) Co-authored-by: EisenbergD Co-authored-by: Claude Opus 5.5 Signed-off-by: EisenbergD --- internal/headers/clusapi/clusapi.go | 29 ++++++++++++++++++------ internal/headers/clusapi/clusapi_test.go | 24 ++++++++++++++++++++ internal/headers/clusapi/cluster.go | 7 ++---- internal/headers/clusapi/storage.go | 6 +---- 4 files changed, 49 insertions(+), 17 deletions(-) diff --git a/internal/headers/clusapi/clusapi.go b/internal/headers/clusapi/clusapi.go index 5876342e..5d96dc85 100644 --- a/internal/headers/clusapi/clusapi.go +++ b/internal/headers/clusapi/clusapi.go @@ -515,14 +515,10 @@ func readResource(handle uintptr, resource *Resource, deadline time.Time) error } data, err := resourceBuffer(handle, resourceGetClassInfo, 8, deadline) - switch { - case err != nil: + if err != nil { resultErr = errors.Join(resultErr, fmt.Errorf("class information: %w", err)) - case len(data) != 8: - resultErr = errors.Join(resultErr, errors.New("invalid resource class information size")) - default: - resource.Values["ResourceClass"] = binary.LittleEndian.Uint32(data) - resource.Values["Subclass"] = binary.LittleEndian.Uint32(data[4:]) + } else { + resource.Values["ResourceClass"], resource.Values["Subclass"] = parseResourceClassInfo(data) } // The property lists normally carry the type; only ask separately without it. @@ -548,6 +544,25 @@ func readResource(handle uintptr, resource *Resource, deadline time.Time) error return resultErr } +// parseResourceClassInfo decodes CLUS_RESOURCE_CLASS_INFO. Some resource DLLs, +// such as the Storage Spaces Direct Health Service, succeed but return only +// the class DWORD without the subclass; the WMI provider reports their +// ResourceClass as CLUS_RESCLASS_UNKNOWN (0), so missing fields decode as 0 +// instead of failing. +func parseResourceClassInfo(data []byte) (uint32, uint32) { + var class, subclass uint32 + + if len(data) >= 4 { + class = binary.LittleEndian.Uint32(data) + } + + if len(data) >= 8 { + subclass = binary.LittleEndian.Uint32(data[4:]) + } + + return class, subclass +} + func resourceBuffer(handle uintptr, code uint32, size int, deadline time.Time) ([]byte, error) { return controlBuffer(deadline, size, func(buffer []byte) (uint32, error) { var ( diff --git a/internal/headers/clusapi/clusapi_test.go b/internal/headers/clusapi/clusapi_test.go index 6ef8f0e8..37fd4f03 100644 --- a/internal/headers/clusapi/clusapi_test.go +++ b/internal/headers/clusapi/clusapi_test.go @@ -269,3 +269,27 @@ func TestUnopenedClusterExpiredBudget(t *testing.T) { t.Fatal("unopened cluster was not closed") } } + +func TestParseResourceClassInfo(t *testing.T) { + for _, test := range []struct { + name string + data []byte + class uint32 + subclass uint32 + }{ + {"complete", []byte{1, 0, 0, 0, 0, 0, 0, 0x80}, 1, 0x80000000}, + // Captured from the Storage Spaces Direct Health Service resource: only + // the class DWORD, without the subclass. WMI reports ResourceClass 0. + {"health service", []byte{0, 0, 0, 0}, 0, 0}, + {"empty", nil, 0, 0}, + {"class only", []byte{2, 0, 0, 0}, 2, 0}, + {"truncated class", []byte{1, 0}, 0, 0}, + } { + t.Run(test.name, func(t *testing.T) { + class, subclass := parseResourceClassInfo(test.data) + if class != test.class || subclass != test.subclass { + t.Fatalf("class=%#x subclass=%#x, want %#x/%#x", class, subclass, test.class, test.subclass) + } + }) + } +} diff --git a/internal/headers/clusapi/cluster.go b/internal/headers/clusapi/cluster.go index d156832a..d1617501 100644 --- a/internal/headers/clusapi/cluster.go +++ b/internal/headers/clusapi/cluster.go @@ -19,7 +19,6 @@ package clusapi import ( "context" - "encoding/binary" "errors" "fmt" "time" @@ -163,11 +162,9 @@ func (c *Cluster) resourceClass(name string, deadline time.Time) (_ uint32, resu return 0, fmt.Errorf("class information: %w", err) } - if len(data) != 8 { - return 0, errors.New("invalid resource class information size") - } + class, _ := parseResourceClassInfo(data) - return binary.LittleEndian.Uint32(data), nil + return class, nil } // readClusterName calls GetClusterInformation without CLUSTERVERSIONINFO. diff --git a/internal/headers/clusapi/storage.go b/internal/headers/clusapi/storage.go index 0fc522c4..6f6aa678 100644 --- a/internal/headers/clusapi/storage.go +++ b/internal/headers/clusapi/storage.go @@ -100,11 +100,7 @@ func (c *Cluster) readResourcePartitions(name objectName, deadline time.Time) (_ return nil, fmt.Errorf("class information: %w", err) } - if len(data) != 8 { - return nil, errors.New("invalid resource class information size") - } - - if binary.LittleEndian.Uint32(data) != resourceClassStorage { + if class, _ := parseResourceClassInfo(data); class != resourceClassStorage { return nil, nil }