diff --git a/internal/headers/hcs/operation_result_test.go b/internal/headers/hcs/operation_result_test.go new file mode 100644 index 00000000..8e222642 --- /dev/null +++ b/internal/headers/hcs/operation_result_test.go @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build windows + +package hcs + +import ( + "testing" + "unsafe" + + "github.com/stretchr/testify/require" + "golang.org/x/sys/windows" +) + +func TestOperationResultDocumentCleanup(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + result uintptr + document bool + }{ + {"successful document", 0, true}, + {"successful empty", 0, false}, + {"failure diagnostic", 0x80070005, true}, + {"failure empty", 0x80070005, false}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + var document *uint16 + if tc.document { + encoded, err := windows.UTF16FromString("diagnostic") + require.NoError(t, err) + + document = &encoded[0] + } + + releases := 0 + release := func(pointer unsafe.Pointer) { + require.Equal(t, unsafe.Pointer(document), pointer) + + releases++ + } + + result, err := consumeOperationResult(tc.result, document, release) + if tc.result != 0 { + require.ErrorIs(t, err, windows.ERROR_ACCESS_DENIED) + require.Empty(t, result) + } else { + require.NoError(t, err) + + if tc.document { + require.Equal(t, "diagnostic", result) + } else { + require.Empty(t, result) + } + } + + if tc.document { + require.Equal(t, 1, releases) + } else { + require.Zero(t, releases) + } + }) + } +} diff --git a/internal/headers/hcs/syscall.go b/internal/headers/hcs/syscall.go index 04971521..12b95cf0 100644 --- a/internal/headers/hcs/syscall.go +++ b/internal/headers/hcs/syscall.go @@ -60,14 +60,22 @@ func WaitForOperationResult(operation Operation, timeout uint32) (string, error) var resultDocument *uint16 r1, _, _ := procHcsWaitForOperationResult.Call(uintptr(operation), uintptr(timeout), uintptr(unsafe.Pointer(&resultDocument))) - if r1 != 0 { - return "", fmt.Errorf("HcsWaitForOperationResult failed: HRESULT 0x%X: %w", r1, Win32FromHResult(r1)) + + return consumeOperationResult(r1, resultDocument, windows.CoTaskMemFree) +} + +// consumeOperationResult releases every caller-owned result document, including +// the diagnostic documents returned by failed operations. +func consumeOperationResult(resultCode uintptr, resultDocument *uint16, release func(unsafe.Pointer)) (string, error) { + if resultDocument != nil { + defer release(unsafe.Pointer(resultDocument)) } - result := windows.UTF16PtrToString(resultDocument) - windows.CoTaskMemFree(unsafe.Pointer(resultDocument)) + if resultCode != 0 { + return "", fmt.Errorf("HcsWaitForOperationResult failed: HRESULT 0x%X: %w", resultCode, Win32FromHResult(resultCode)) + } - return result, nil + return windows.UTF16PtrToString(resultDocument), nil } // CloseOperation closes an operation.