Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(inputs.win_perf_counter): Do not rely on returned buffer size #14241

Merged
merged 5 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions plugins/inputs/win_perf_counters/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,10 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
## e.g. IgnoredErrors = ["PDH_NO_DATA"]
# IgnoredErrors = []

## Maximum size of the buffer for values returned by the API
## Increase this value if you experience "buffer limit reached" errors.
# MaxBufferSize = "4MiB"

## NOTE: Due to the way TOML is parsed, tables must be at the END of the
## plugin definition, otherwise additional config options are read as part of
## the table
Expand Down
145 changes: 95 additions & 50 deletions plugins/inputs/win_perf_counters/performance_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import (
"unsafe"
)

// Initial buffer size for return buffers
const initialBufferSize = uint32(1024) // 1kB

var errBufferLimitReached = errors.New("buffer limit reached")

// CounterValue is abstraction for PdhFmtCountervalueItemDouble
type CounterValue struct {
InstanceName string
Expand All @@ -36,7 +41,7 @@ type PerformanceQuery interface {
}

type PerformanceQueryCreator interface {
NewPerformanceQuery(string) PerformanceQuery
NewPerformanceQuery(string, uint32) PerformanceQuery
}

// PdhError represents error returned from Performance Counters API
Expand All @@ -58,14 +63,14 @@ func NewPdhError(code uint32) error {

// PerformanceQueryImpl is implementation of PerformanceQuery interface, which calls phd.dll functions
type PerformanceQueryImpl struct {
query pdhQueryHandle
maxBufferSize uint32
query pdhQueryHandle
}

type PerformanceQueryCreatorImpl struct {
}
type PerformanceQueryCreatorImpl struct{}

func (m PerformanceQueryCreatorImpl) NewPerformanceQuery(string) PerformanceQuery {
return &PerformanceQueryImpl{}
func (m PerformanceQueryCreatorImpl) NewPerformanceQuery(_ string, maxBufferSize uint32) PerformanceQuery {
return &PerformanceQueryImpl{maxBufferSize: maxBufferSize}
}

// Open creates a new counterPath that is used to manage the collection of performance data.
Expand Down Expand Up @@ -124,64 +129,82 @@ func (m *PerformanceQueryImpl) AddEnglishCounterToQuery(counterPath string) (pdh

// GetCounterPath return counter information for given handle
func (m *PerformanceQueryImpl) GetCounterPath(counterHandle pdhCounterHandle) (string, error) {
var bufSize uint32
var buff []byte
var ret uint32
if ret = PdhGetCounterInfo(counterHandle, 0, &bufSize, nil); ret == PdhMoreData {
buff = make([]byte, bufSize)
bufSize = uint32(len(buff))
if ret = PdhGetCounterInfo(counterHandle, 0, &bufSize, &buff[0]); ret == ErrorSuccess {
ci := (*PdhCounterInfo)(unsafe.Pointer(&buff[0])) //nolint:gosec // G103: Valid use of unsafe call to create PDH_COUNTER_INFO
for buflen := initialBufferSize; buflen <= m.maxBufferSize; buflen *= 2 {
buf := make([]byte, buflen)

// Get the info with the current buffer size
size := buflen
ret := PdhGetCounterInfo(counterHandle, 0, &size, &buf[0])
if ret == ErrorSuccess {
ci := (*PdhCounterInfo)(unsafe.Pointer(&buf[0])) //nolint:gosec // G103: Valid use of unsafe call to create PDH_COUNTER_INFO
return UTF16PtrToString(ci.SzFullPath), nil
}

// Use the size as a hint if it exceeds the current buffer size
if size > buflen {
buflen = size
}

// We got a non-recoverable error so exit here
if ret != PdhMoreData {
return "", NewPdhError(ret)
}
}
return "", NewPdhError(ret)

return "", errBufferLimitReached
}

// ExpandWildCardPath examines local computer and returns those counter paths that match the given counter path which contains wildcard characters.
func (m *PerformanceQueryImpl) ExpandWildCardPath(counterPath string) ([]string, error) {
var bufSize uint32
var buff []uint16
var ret uint32
for buflen := initialBufferSize; buflen <= m.maxBufferSize; buflen *= 2 {
buf := make([]uint16, buflen)

if ret = PdhExpandWildCardPath(counterPath, nil, &bufSize); ret == PdhMoreData {
buff = make([]uint16, bufSize)
bufSize = uint32(len(buff))
ret = PdhExpandWildCardPath(counterPath, &buff[0], &bufSize)
// Get the info with the current buffer size
size := buflen
ret := PdhExpandWildCardPath(counterPath, &buf[0], &size)
if ret == ErrorSuccess {
list := UTF16ToStringArray(buff)
return list, nil
return UTF16ToStringArray(buf), nil
}

// Use the size as a hint if it exceeds the current buffer size
if size > buflen {
buflen = size
}

// We got a non-recoverable error so exit here
if ret != PdhMoreData {
return nil, NewPdhError(ret)
}
}
return nil, NewPdhError(ret)

return nil, errBufferLimitReached
}

// GetFormattedCounterValueDouble computes a displayable value for the specified counter
func (m *PerformanceQueryImpl) GetFormattedCounterValueDouble(hCounter pdhCounterHandle) (float64, error) {
var counterType uint32
var value PdhFmtCountervalueDouble
var ret uint32

if ret = PdhGetFormattedCounterValueDouble(hCounter, &counterType, &value); ret == ErrorSuccess {
if value.CStatus == PdhCstatusValidData || value.CStatus == PdhCstatusNewData {
return value.DoubleValue, nil
}
return 0, NewPdhError(value.CStatus)
if ret := PdhGetFormattedCounterValueDouble(hCounter, &counterType, &value); ret != ErrorSuccess {
return 0, NewPdhError(ret)
}
return 0, NewPdhError(ret)
if value.CStatus == PdhCstatusValidData || value.CStatus == PdhCstatusNewData {
return value.DoubleValue, nil
}
return 0, NewPdhError(value.CStatus)
}

func (m *PerformanceQueryImpl) GetFormattedCounterArrayDouble(hCounter pdhCounterHandle) ([]CounterValue, error) {
var buffSize uint32
var itemCount uint32
var ret uint32
for buflen := initialBufferSize; buflen <= m.maxBufferSize; buflen *= 2 {
buf := make([]byte, buflen)

if ret = PdhGetFormattedCounterArrayDouble(hCounter, &buffSize, &itemCount, nil); ret == PdhMoreData {
buff := make([]byte, buffSize)

if ret = PdhGetFormattedCounterArrayDouble(hCounter, &buffSize, &itemCount, &buff[0]); ret == ErrorSuccess {
// Get the info with the current buffer size
var itemCount uint32
size := buflen
ret := PdhGetFormattedCounterArrayDouble(hCounter, &size, &itemCount, &buf[0])
if ret == ErrorSuccess {
//nolint:gosec // G103: Valid use of unsafe call to create PDH_FMT_COUNTERVALUE_ITEM_DOUBLE
items := (*[1 << 20]PdhFmtCountervalueItemDouble)(unsafe.Pointer(&buff[0]))[:itemCount]
items := (*[1 << 20]PdhFmtCountervalueItemDouble)(unsafe.Pointer(&buf[0]))[:itemCount]
values := make([]CounterValue, 0, itemCount)
for _, item := range items {
if item.FmtValue.CStatus == PdhCstatusValidData || item.FmtValue.CStatus == PdhCstatusNewData {
Expand All @@ -191,21 +214,32 @@ func (m *PerformanceQueryImpl) GetFormattedCounterArrayDouble(hCounter pdhCounte
}
return values, nil
}

// Use the size as a hint if it exceeds the current buffer size
if size > buflen {
buflen = size
}

// We got a non-recoverable error so exit here
if ret != PdhMoreData {
return nil, NewPdhError(ret)
}
}
return nil, NewPdhError(ret)

return nil, errBufferLimitReached
}

func (m *PerformanceQueryImpl) GetRawCounterArray(hCounter pdhCounterHandle) ([]CounterValue, error) {
var buffSize uint32
var itemCount uint32
var ret uint32
for buflen := initialBufferSize; buflen <= m.maxBufferSize; buflen *= 2 {
buf := make([]byte, buflen)

if ret = PdhGetRawCounterArray(hCounter, &buffSize, &itemCount, nil); ret == PdhMoreData {
buff := make([]byte, buffSize)

if ret = PdhGetRawCounterArray(hCounter, &buffSize, &itemCount, &buff[0]); ret == ErrorSuccess {
// Get the info with the current buffer size
var itemCount uint32
size := buflen
ret := PdhGetRawCounterArray(hCounter, &size, &itemCount, &buf[0])
if ret == ErrorSuccess {
//nolint:gosec // G103: Valid use of unsafe call to create PDH_RAW_COUNTER_ITEM
items := (*[1 << 20]PdhRawCounterItem)(unsafe.Pointer(&buff[0]))[:itemCount]
items := (*[1 << 20]PdhRawCounterItem)(unsafe.Pointer(&buf[0]))[:itemCount]
values := make([]CounterValue, 0, itemCount)
for _, item := range items {
if item.RawValue.CStatus == PdhCstatusValidData || item.RawValue.CStatus == PdhCstatusNewData {
Expand All @@ -215,8 +249,19 @@ func (m *PerformanceQueryImpl) GetRawCounterArray(hCounter pdhCounterHandle) ([]
}
return values, nil
}

// Use the size as a hint if it exceeds the current buffer size
if size > buflen {
buflen = size
}

// We got a non-recoverable error so exit here
if ret != PdhMoreData {
return nil, NewPdhError(ret)
}
}
return nil, NewPdhError(ret)

return nil, errBufferLimitReached
}

func (m *PerformanceQueryImpl) CollectData() error {
Expand Down
4 changes: 4 additions & 0 deletions plugins/inputs/win_perf_counters/sample.conf
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
## e.g. IgnoredErrors = ["PDH_NO_DATA"]
# IgnoredErrors = []

## Maximum size of the buffer for values returned by the API
## Increase this value if you experience "buffer limit reached" errors.
# MaxBufferSize = "4MiB"

## NOTE: Due to the way TOML is parsed, tables must be at the END of the
## plugin definition, otherwise additional config options are read as part of
## the table
Expand Down
16 changes: 14 additions & 2 deletions plugins/inputs/win_perf_counters/win_perf_counters.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
_ "embed"
"errors"
"fmt"
"math"
"os"
"strings"
"sync"
Expand All @@ -20,6 +21,8 @@ import (
//go:embed sample.conf
var sampleConfig string

var defaultMaxBufferSize = config.Size(100 * 1024 * 1024)

type WinPerfCounters struct {
PrintValid bool `toml:"PrintValid"`
PreVistaSupport bool `toml:"PreVistaSupport" deprecated:"1.7.0;determined dynamically"`
Expand All @@ -29,6 +32,7 @@ type WinPerfCounters struct {
UseWildcardsExpansion bool
LocalizeWildcardsExpansion bool
IgnoredErrors []string `toml:"IgnoredErrors"`
MaxBufferSize config.Size
Sources []string

Log telegraf.Logger
Expand Down Expand Up @@ -207,7 +211,7 @@ func (m *WinPerfCounters) AddItem(counterPath, computer, objectName, instance, c
if !ok {
hostCounter = &hostCountersInfo{computer: computer, tag: sourceTag}
m.hostCounters[computer] = hostCounter
hostCounter.query = m.queryCreator.NewPerformanceQuery(computer)
hostCounter.query = m.queryCreator.NewPerformanceQuery(computer, uint32(m.MaxBufferSize))
if err := hostCounter.query.Open(); err != nil {
return err
}
Expand Down Expand Up @@ -579,9 +583,16 @@ func isKnownCounterDataError(err error) bool {
}

func (m *WinPerfCounters) Init() error {
// Check the buffer size
if m.MaxBufferSize < config.Size(initialBufferSize) {
return fmt.Errorf("maximum buffer size should at least be %d", 2*initialBufferSize)
}
if m.MaxBufferSize > math.MaxUint32 {
return fmt.Errorf("maximum buffer size should be smaller than %d", uint32(math.MaxUint32))
}

if m.UseWildcardsExpansion && !m.LocalizeWildcardsExpansion {
// Counters must not have wildcards with this option

found := false
wildcards := []string{"*", "?"}

Expand Down Expand Up @@ -614,6 +625,7 @@ func init() {
return &WinPerfCounters{
CountersRefreshInterval: config.Duration(time.Second * 60),
LocalizeWildcardsExpansion: true,
MaxBufferSize: defaultMaxBufferSize,
queryCreator: &PerformanceQueryCreatorImpl{},
}
})
Expand Down
Loading
Loading