jkroepke
jkroepke

Reputation: 131

Application is leaking event handle after calling syscalls on Windows

I have a go application that calling the Windows Management Infrastructure (MI).

After some amount of time, I notice that the amount of handle is quite high. I inspect the process with the handles program which reports tons of opened events handle.

The main leak comes from Operation.GetInstance(). Even after closing the operation, session and application, the event handle are still present.

I have no idea, what I can do to indicate the root cause.

For debugging reasons, I move all core logic to a single go file that everyone can reproduce it on they own.

//go:build windows

package main

import (
    "fmt"
    "log"
    "syscall"
    "unsafe"

    "golang.org/x/sys/windows"
)

var (
    modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
    modMi       = windows.NewLazySystemDLL("mi.dll")

    procGetProcessHandleCount   = modkernel32.NewProc("GetProcessHandleCount")
    procMIApplicationInitialize = modMi.NewProc("MI_Application_InitializeV1")
)

func GetProcessHandleCount(handle windows.Handle) uint32 {
    var count uint32
    r1, _, err := procGetProcessHandleCount.Call(
        uintptr(handle),
        uintptr(unsafe.Pointer(&count)),
    )
    if r1 != 1 {
        panic(err)
    } else {
        return count
    }
}

// Application represents the MI application.
// https://learn.microsoft.com/de-de/windows/win32/api/mi/ns-mi-mi_application
type Application struct {
    reserved1 uint64
    reserved2 uintptr
    ft        *ApplicationFT
}

// ApplicationFT represents the function table of the MI application.
// https://learn.microsoft.com/de-de/windows/win32/api/mi/ns-mi-mi_applicationft
type ApplicationFT struct {
    Close                          uintptr
    NewSession                     uintptr
    NewHostedProvider              uintptr
    NewInstance                    uintptr
    NewDestinationOptions          uintptr
    NewOperationOptions            uintptr
    NewSubscriptionDeliveryOptions uintptr
    NewSerializer                  uintptr
    NewDeserializer                uintptr
    NewInstanceFromClass           uintptr
    NewClass                       uintptr
}

// Session represents a session.
//
// https://learn.microsoft.com/en-us/windows/win32/api/mi/ns-mi-mi_session
type Session struct {
    reserved1 uint64
    reserved2 uintptr
    ft        *SessionFT
}

// SessionFT represents the function table for Session.
//
// https://learn.microsoft.com/en-us/windows/win32/api/mi/ns-mi-mi_session
type SessionFT struct {
    Close               uintptr
    GetApplication      uintptr
    GetInstance         uintptr
    ModifyInstance      uintptr
    CreateInstance      uintptr
    DeleteInstance      uintptr
    Invoke              uintptr
    EnumerateInstances  uintptr
    QueryInstances      uintptr
    AssociatorInstances uintptr
    ReferenceInstances  uintptr
    Subscribe           uintptr
    GetClass            uintptr
    EnumerateClasses    uintptr
    TestConnection      uintptr
}

// Operation represents an operation.
// https://learn.microsoft.com/en-us/windows/win32/api/mi/ns-mi-mi_operation
type Operation struct {
    reserved1 uint64
    reserved2 uintptr
    ft        *OperationFT
}

// OperationFT represents the function table for Operation.
// https://learn.microsoft.com/en-us/windows/win32/api/mi/ns-mi-mi_operationft
type OperationFT struct {
    Close         uintptr
    Cancel        uintptr
    GetSession    uintptr
    GetInstance   uintptr
    GetIndication uintptr
    GetClass      uintptr
}

type Instance struct {
    ft         *InstanceFT
    classDecl  uintptr
    serverName *uint16
    nameSpace  *uint16
    _          [4]uintptr
}

type InstanceFT struct {
    Clone           uintptr
    Destruct        uintptr
    Delete          uintptr
    IsA             uintptr
    GetClassName    uintptr
    SetNameSpace    uintptr
    GetNameSpace    uintptr
    GetElementCount uintptr
    AddElement      uintptr
    SetElement      uintptr
    SetElementAt    uintptr
    GetElement      uintptr
    GetElementAt    uintptr
    ClearElement    uintptr
    ClearElementAt  uintptr
    GetServerName   uintptr
    SetServerName   uintptr
    GetClass        uintptr
}

// Application_Initialize initializes the MI [Application].
// It is recommended to have only one Application per process.
//
// https://learn.microsoft.com/en-us/windows/win32/api/mi/nf-mi-mi_application_initializev1
func Application_Initialize() (*Application, error) {
    application := &Application{}

    r0, _, _ := procMIApplicationInitialize.Call(0, 0, 0, uintptr(unsafe.Pointer(application)))

    if r0 != 0 {
        return nil, fmt.Errorf("failed: %d", r0)
    }

    return application, nil
}

// Close deinitializes the management infrastructure client API that was initialized through a call to Application_Initialize.
//
// https://learn.microsoft.com/en-us/windows/win32/api/mi/nf-mi-mi_application_close
func (application *Application) Close() error {
    r0, _, _ := syscall.SyscallN(application.ft.Close, uintptr(unsafe.Pointer(application)))

    if r0 != 0 {
        return fmt.Errorf("failed: %d", r0)
    }

    return nil
}

// NewSession creates a session used to share connections for a set of operations to a single destination.
//
// https://learn.microsoft.com/en-us/windows/win32/api/mi/nf-mi-mi_application_newsession
func (application *Application) NewSession() (*Session, error) {
    session := &Session{}

    r0, _, _ := syscall.SyscallN(
        application.ft.NewSession, uintptr(unsafe.Pointer(application)), 0, 0, 0, 0, 0, uintptr(unsafe.Pointer(session)),
    )

    if r0 != 0 {
        return nil, fmt.Errorf("failed: %d", r0)
    }

    return session, nil
}

// Close closes a session and releases all associated memory.
//
// https://learn.microsoft.com/en-us/windows/win32/api/mi/nf-mi-mi_session_close
func (s *Session) Close() error {
    r0, _, _ := syscall.SyscallN(s.ft.Close, uintptr(unsafe.Pointer(s)), 0, 0)

    if r0 != 0 {
        return fmt.Errorf("failed: %d", r0)
    }

    return nil
}

// QueryInstances queries for a set of instances based on a query expression.
//
// https://learn.microsoft.com/en-us/windows/win32/api/mi/nf-mi-mi_session_queryinstances
func (s *Session) QueryInstances(namespaceName, queryDialect, queryExpression string) (*Operation, error) {
    namespaceNameUTF16, err := windows.UTF16PtrFromString(namespaceName)
    if err != nil {
        return nil, err
    }

    queryDialectUTF16, err := windows.UTF16PtrFromString(queryDialect)
    if err != nil {
        return nil, err
    }

    queryExpressionUTF16, err := windows.UTF16PtrFromString(queryExpression)
    if err != nil {
        return nil, err
    }

    operation := &Operation{}

    r0, _, _ := syscall.SyscallN(
        s.ft.QueryInstances,
        uintptr(unsafe.Pointer(s)),
        0,
        0,
        uintptr(unsafe.Pointer(namespaceNameUTF16)),
        uintptr(unsafe.Pointer(queryDialectUTF16)),
        uintptr(unsafe.Pointer(queryExpressionUTF16)),
        0,
        uintptr(unsafe.Pointer(operation)),
    )

    if r0 != 0 {
        return nil, fmt.Errorf("failed: %d", r0)
    }

    return operation, nil
}

// Close closes an operation handle.
//
// https://learn.microsoft.com/en-us/windows/win32/api/mi/nf-mi-mi_operation_close
func (o *Operation) Close() error {
    r0, _, _ := syscall.SyscallN(o.ft.Close, uintptr(unsafe.Pointer(o)))

    if r0 != 0 {
        return fmt.Errorf("failed: %d", r0)
    }

    return nil
}

func (o *Operation) GetInstance() (*Instance, bool, error) {
    var (
        instance          *Instance
        errorDetails      *Instance
        moreResults       uint8
        instanceResult    uint32
        errorMessageUTF16 *uint16
    )

    r0, _, _ := syscall.SyscallN(
        o.ft.GetInstance,
        uintptr(unsafe.Pointer(o)),
        uintptr(unsafe.Pointer(&instance)),
        uintptr(unsafe.Pointer(&moreResults)),
        uintptr(unsafe.Pointer(&instanceResult)),
        uintptr(unsafe.Pointer(&errorMessageUTF16)),
        uintptr(unsafe.Pointer(&errorDetails)),
    )

    if instanceResult != 0 {
        return nil, false, fmt.Errorf("instance result: %d (%s)", instanceResult, windows.UTF16PtrToString(errorMessageUTF16))
    }

    if r0 != 0 {
        return nil, false, fmt.Errorf("failed: %d", r0)
    }

    return instance, moreResults == 1, nil
}

func (instance *Instance) GetElement(elementName string) (uintptr, error) {
    elementNameUTF16, err := windows.UTF16PtrFromString(elementName)
    if err != nil {
        return 0, fmt.Errorf("failed to convert element name %s to UTF-16: %w", elementName, err)
    }

    var value uintptr

    r0, _, _ := syscall.SyscallN(
        instance.ft.GetElement,
        uintptr(unsafe.Pointer(instance)),
        uintptr(unsafe.Pointer(elementNameUTF16)),
        uintptr(unsafe.Pointer(&value)),
        0,
        0,
        0,
    )

    if r0 != 0 {
        return 0, fmt.Errorf("failed: %d", r0)
    }

    return value, nil
}

func main() {
    log.Printf("1 Process handle count: %d", GetProcessHandleCount(windows.CurrentProcess()))

    app, err := Application_Initialize()
    if err != nil {
        panic(err)
    }

    log.Printf("2 Process handle count: %d", GetProcessHandleCount(windows.CurrentProcess()))

    session, err := app.NewSession()
    if err != nil {
        panic(err)
    }

    log.Printf("3 Process handle count: %d", GetProcessHandleCount(windows.CurrentProcess()))

    for range 1000 {
        operation, err := session.QueryInstances("root/cimv2", "WQL",
            "SELECT Architecture, DeviceId, Description, Family, L2CacheSize, L3CacheSize, Name, ThreadCount, NumberOfCores, NumberOfEnabledCore, NumberOfLogicalProcessors FROM Win32_Processor",
        )
        if err != nil {
            panic(err)
        }

        for {
            instance, moreResults, err := operation.GetInstance()
            if err != nil {
                panic(err)
            }

            description, err := instance.GetElement("Description")
            if err != nil {
                panic(err)
            }

            name, err := instance.GetElement("Name")
            if err != nil {
                panic(err)
            }

            log.Printf("Description: %s, Name: %s",
                windows.UTF16PtrToString((*uint16)(unsafe.Pointer(description))),
                windows.UTF16PtrToString((*uint16)(unsafe.Pointer(name))),
            )

            if !moreResults {
                break
            }
        }

        if err = operation.Close(); err != nil {
            panic(err)
        }

        log.Printf("4 Process handle count: %d", GetProcessHandleCount(windows.CurrentProcess()))

    }

    log.Printf("5 Process handle count: %d", GetProcessHandleCount(windows.CurrentProcess()))

    if err = session.Close(); err != nil {
        panic(err)
    }

    if err = app.Close(); err != nil {
        panic(err)
    }

    log.Printf("6 Process handle count: %d", GetProcessHandleCount(windows.CurrentProcess()))
}

results into:

2024/10/20 18:49:08 1 Process handle count: 88
2024/10/20 18:49:08 2 Process handle count: 130
2024/10/20 18:49:08 3 Process handle count: 152
2024/10/20 18:49:08 Description: AMD64 Family 25 Model 33 Stepping 2, Name: AMD Ryzen 9 5900X 12-Core Processor            
2024/10/20 18:49:08 4 Process handle count: 205
2024/10/20 18:49:08 Description: AMD64 Family 25 Model 33 Stepping 2, Name: AMD Ryzen 9 5900X 12-Core Processor            
2024/10/20 18:49:08 4 Process handle count: 207
2024/10/20 18:49:08 Description: AMD64 Family 25 Model 33 Stepping 2, Name: AMD Ryzen 9 5900X 12-Core Processor            
2024/10/20 18:49:08 4 Process handle count: 209
2024/10/20 18:49:08 Description: AMD64 Family 25 Model 33 Stepping 2, Name: AMD Ryzen 9 5900X 12-Core Processor            
2024/10/20 18:49:08 4 Process handle count: 211
2024/10/20 18:49:08 Description: AMD64 Family 25 Model 33 Stepping 2, Name: AMD Ryzen 9 5900X 12-Core Processor            
2024/10/20 18:49:08 4 Process handle count: 212
2024/10/20 18:49:08 Description: AMD64 Family 25 Model 33 Stepping 2, Name: AMD Ryzen 9 5900X 12-Core Processor            
2024/10/20 18:49:08 4 Process handle count: 213
[...]
2024/10/20 18:49:08 Description: AMD64 Family 25 Model 33 Stepping 2, Name: AMD Ryzen 9 5900X 12-Core Processor            
2024/10/20 18:49:10 4 Process handle count: 263
2024/10/20 18:49:10 Description: AMD64 Family 25 Model 33 Stepping 2, Name: AMD Ryzen 9 5900X 12-Core Processor
2024/10/20 18:49:10 4 Process handle count: 263
2024/10/20 18:49:10 Description: AMD64 Family 25 Model 33 Stepping 2, Name: AMD Ryzen 9 5900X 12-Core Processor
2024/10/20 18:49:10 4 Process handle count: 263
2024/10/20 18:49:10 5 Process handle count: 263
2024/10/20 18:49:10 6 Process handle count: 237

Process finished with the exit code 0

How to reproduce the issue:

On a Windows system:

  1. Copy the code above block begin with //go:build windows in a empty directory and name it main.go
  2. Create a go.mod and write module main
  3. Run go get from command line
  4. Run go run main.go. It runs the code and logs the amount of handles. Even after closing the application, event handles are still open.

Upvotes: 1

Views: 89

Answers (0)

Related Questions