From cd9242c4c771d857cda6c64fda8538b9537531d2 Mon Sep 17 00:00:00 2001 From: Alexandre Possebom Date: Fri, 9 Oct 2026 10:12:27 -0300 Subject: [PATCH] pdh: add slack to counter array buffers for Windows Server 2012 R2 (#2615) Signed-off-by: Alexandre Possebom --- internal/pdh/collector.go | 46 +++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/internal/pdh/collector.go b/internal/pdh/collector.go index b3fcfde8..2aa82a5c 100644 --- a/internal/pdh/collector.go +++ b/internal/pdh/collector.go @@ -289,6 +289,13 @@ func (c *Collector) Collect(dst any) error { return <-c.errorCh } +// pdhBufferSlack is extra space allocated beyond the buffer size reported by PDH. +// On Windows Server 2012 R2, PdhGetRawCounterArrayW reports the size of the item array of a single-instance +// object (e.g. System) without the terminator of the empty instance name, and writes that terminator right after +// the array: 48 bytes reported, 2 bytes written past them. Without the slack, the write corrupts the neighboring +// heap object and SzName points into it, which the GC reports as "found pointer to free object". +const pdhBufferSlack = 8 + func (c *Collector) collectWorkerRaw() { var ( err error @@ -297,7 +304,10 @@ func (c *Collector) collectWorkerRaw() { bytesNeeded uint32 ) - buf := make([]byte, 1) + // buf starts empty so that the first call only queries the required size with a nil buffer. + // PdhGetRawCounterArrayW writes 8 bytes into a non-nil buffer, even if lpdwBufferSize is smaller, + // which corrupts the neighboring heap memory of a tiny allocation. + var buf []byte for data := range c.collectCh { err = (func() error { @@ -334,12 +344,13 @@ func (c *Collector) collectWorkerRaw() { stringMap := map[*uint16]string{} for _, counter := range c.counters { + instances: for _, instance := range counter.Instances { // Get the info with the current buffer size - bytesNeeded = uint32(cap(buf)) + bytesNeeded = uint32(len(buf)) for { - ret := GetRawCounterArray(instance, &bytesNeeded, &itemCount, &buf[0]) + ret := GetRawCounterArray(instance, &bytesNeeded, &itemCount, unsafe.SliceData(buf)) if ret == ErrorSuccess { break @@ -347,20 +358,20 @@ func (c *Collector) collectWorkerRaw() { if err := NewPdhError(ret); ret != MoreData { if isKnownCounterDataError(err) { - break + continue instances } return fmt.Errorf("GetRawCounterArray: %w", err) } - if bytesNeeded <= uint32(cap(buf)) { - return fmt.Errorf("GetRawCounterArray reports buffer too small (%d), but buffer is large enough (%d): %w", uint32(cap(buf)), bytesNeeded, NewPdhError(ret)) + if bytesNeeded <= uint32(len(buf)) { + return fmt.Errorf("GetRawCounterArray reports buffer too small (%d), but buffer is large enough (%d): %w", uint32(len(buf)), bytesNeeded, NewPdhError(ret)) } - buf = make([]byte, bytesNeeded) + buf = make([]byte, bytesNeeded+pdhBufferSlack) } - items = unsafe.Slice((*RawCounterItem)(unsafe.Pointer(&buf[0])), itemCount) + items = unsafe.Slice((*RawCounterItem)(unsafe.Pointer(unsafe.SliceData(buf))), itemCount) var ( instanceName string @@ -458,7 +469,9 @@ func (c *Collector) collectWorkerFormatted() { bytesNeeded uint32 ) - buf := make([]byte, 1) + // buf starts empty so that the first call only queries the required size with a nil buffer. + // See collectWorkerRaw. + var buf []byte for data := range c.collectCh { err = (func() error { @@ -495,12 +508,13 @@ func (c *Collector) collectWorkerFormatted() { stringMap := map[*uint16]string{} for _, counter := range c.counters { + instances: for _, instance := range counter.Instances { // Get the info with the current buffer size - bytesNeeded = uint32(cap(buf)) + bytesNeeded = uint32(len(buf)) for { - ret := GetFormattedCounterArrayDouble(instance, &bytesNeeded, &itemCount, &buf[0]) + ret := GetFormattedCounterArrayDouble(instance, &bytesNeeded, &itemCount, unsafe.SliceData(buf)) if ret == ErrorSuccess { break @@ -508,20 +522,20 @@ func (c *Collector) collectWorkerFormatted() { if err := NewPdhError(ret); ret != MoreData { if isKnownCounterDataError(err) { - break + continue instances } return fmt.Errorf("GetFormattedCounterArrayDouble: %w", err) } - if bytesNeeded <= uint32(cap(buf)) { - return fmt.Errorf("GetFormattedCounterArrayDouble reports buffer too small (%d), but buffer is large enough (%d): %w", uint32(cap(buf)), bytesNeeded, NewPdhError(ret)) + if bytesNeeded <= uint32(len(buf)) { + return fmt.Errorf("GetFormattedCounterArrayDouble reports buffer too small (%d), but buffer is large enough (%d): %w", uint32(len(buf)), bytesNeeded, NewPdhError(ret)) } - buf = make([]byte, bytesNeeded) + buf = make([]byte, bytesNeeded+pdhBufferSlack) } - items = unsafe.Slice((*FmtCounterValueItemDouble)(unsafe.Pointer(&buf[0])), itemCount) + items = unsafe.Slice((*FmtCounterValueItemDouble)(unsafe.Pointer(unsafe.SliceData(buf))), itemCount) var ( instanceName string