6

I have been writing a program that ideally will run on a server in the background without ever closing - therefore it is important that any memory leaks are non existent. My program involves retrieving live session information using the Windows Terminal Services API (wtsapi32.dll) and since the information must be live the function is being run every few seconds, I have found that calling the WTSEnumerateSessionsEx function has lead to a fairly sizable memory leak. It seems the call to WTSFreeMemoryEx as instructed in the MSDN documentation seems to have no impact yet I receive no error messages from either call.

To summarize: the problem is not in execution of WTSEnumerateSessionsEx since valid data is returned; the memory is simply not being freed and this leads to problems when left to run for extended periods of time.

Currently the short-term solution has been to restart the process when used memory exceeds a threshold however this doesn't seem to be a satisfactory solution and rectifying this leak would be most desirable.

The enumeration types have been taken directly from the Microsoft MSDN documentation.

Attached is the relevant source file.

unit WtsAPI32;

interface

uses Windows, Classes, Dialogs, SysUtils, StrUtils;

const
  WTS_CURRENT_SERVER_HANDLE = 0;

type
  WTS_CONNECTSTATE_CLASS = (WTSActive, WTSConnected, WTSConnectQuery,
    WTSShadow, WTSDisconnected, WTSIdle, WTSListen, WTSReset, WTSDown,
    WTSInit);

type
  WTS_TYPE_CLASS = (WTSTypeProcessInfoLevel0, WTSTypeProcessInfoLevel1,
    WTSTypeSessionInfoLevel1);

type
  WTS_SESSION_INFO_1 = record
    ExecEnvId: DWord;
    State: WTS_CONNECTSTATE_CLASS;
    SessionId: DWord;
    pSessionName: LPtStr;
    pHostName: LPtStr;
    pUserName: LPtStr;
    pDomainName: LPtStr;
    pFarmName: LPtStr;
  end;

type
  TSessionInfoEx = record
    ExecEnvId: DWord;
    State: WTS_CONNECTSTATE_CLASS;
    SessionId: DWord;
    pSessionName: string;
    pHostName: string;
    pUserName: string;
    pDomainName: string;
    pFarmName: string;
  end;

  TSessions = array of TSessionInfoEx;

function FreeMemoryEx(WTSTypeClass: WTS_TYPE_CLASS; pMemory: Pointer;
  NumberOfEntries: Integer): BOOL; stdcall;
external 'wtsapi32.dll' name 'WTSFreeMemoryExW';

function FreeMemory(pMemory: Pointer): DWord; stdcall;
external 'wtsapi32.dll' name 'WTSFreeMemory';

function EnumerateSessionsEx(hServer: THandle; var pLevel: DWord;
  Filter: DWord; var ppSessionInfo: Pointer; var pCount: DWord): BOOL;
  stdcall; external 'wtsapi32.dll' name 'WTSEnumerateSessionsExW';

function EnumerateSessions(var Sessions: TSessions): Boolean;

implementation

function EnumerateSessions(var Sessions: TSessions): Boolean;
type
   TSessionInfoExArr = array[0..2000 div SizeOf(WTS_SESSION_INFO_1)] of WTS_SESSION_INFO_1;
var
  ppSessionInfo: Pointer;
  pCount: DWord;
  hServer: THandle;
  level: DWord;
  i: Integer;
  ErrCode: Integer;
  Return: DWord;
begin
  pCount := 0;
  level := 1;
  hServer := WTS_CURRENT_SERVER_HANDLE;
  ppSessionInfo := NIL;
  if not EnumerateSessionsEx(hServer, level, 0, ppSessionInfo, pCount) then
  begin
   ErrCode := GetLastError;
   ShowMessage('Error in EnumerateSessionsEx - Code: ' + IntToStr(ErrCode)
        + ' Message: ' + SysErrorMessage(ErrCode));
  en
  else
  begin
    SetLength(Sessions, pCount);
    for i := 0 to pCount - 1 do
    begin
      Sessions[i].ExecEnvId := TSessionInfoExArr(ppSessionInfo^)[i].ExecEnvId;
      Sessions[i].State := TSessionInfoExArr(ppSessionInfo^)[i].State;
      Sessions[i].SessionId := TSessionInfoExArr(ppSessionInfo^)[i].SessionId;
      Sessions[i].pSessionName := WideCharToString
        (TSessionInfoExArr(ppSessionInfo^)[i].pSessionName);
      Sessions[i].pHostName := WideCharToString
        (TSessionInfoExArr(ppSessionInfo^)[i].pHostName);
      Sessions[i].pUserName := WideCharToString
        (TSessionInfoExArr(ppSessionInfo^)[i].pUserName);
      Sessions[i].pDomainName := WideCharToString
        (TSessionInfoExArr(ppSessionInfo^)[i].pDomainName);
      Sessions[i].pFarmName := WideCharToString
        (TSessionInfoExArr(ppSessionInfo^)[i].pFarmName);
    end;

    if not FreeBufferEx(WTSTypeSessionInfoLevel1, ppSessionInfo, pCount);
      begin
      ErrCode := GetLastError;
      ShowMessage('Error in EnumerateSessionsEx - Code: ' + IntToStr(ErrCode)
           + ' Message: ' + SysErrorMessage(ErrCode));
      end;
      ppSessionInfo := nil;
  end;

end;

end.

Here's is a minimal SSCCE that demonstrates the issue. When this program executes, it exhausts available memory in short time.

program SO17839270;

{$APPTYPE CONSOLE}

uses
  SysUtils, Windows;

const
  WTS_CURRENT_SERVER_HANDLE = 0;

type
  WTS_TYPE_CLASS = (WTSTypeProcessInfoLevel0, WTSTypeProcessInfoLevel1,
    WTSTypeSessionInfoLevel1);

function WTSEnumerateSessionsEx(hServer: THandle; var pLevel: DWORD;
  Filter: DWORD; var ppSessionInfo: Pointer; var pCount: DWORD): BOOL; stdcall;
  external 'wtsapi32.dll' name 'WTSEnumerateSessionsExW';

function WTSFreeMemoryEx(WTSTypeClass: WTS_TYPE_CLASS; pMemory: Pointer;
  NumberOfEntries: Integer): BOOL; stdcall;
  external 'wtsapi32.dll' name 'WTSFreeMemoryExW';

procedure EnumerateSessionsEx;
var
  ppSessionInfo: Pointer;
  pCount: DWORD;
  level: DWORD;
begin
  level := 1;
  if not WTSEnumerateSessionsEx(WTS_CURRENT_SERVER_HANDLE, level, 0,
    ppSessionInfo, pCount) then
    RaiseLastOSError;
  if not WTSFreeMemoryEx(WTSTypeSessionInfoLevel1, ppSessionInfo, pCount) then
    RaiseLastOSError;
end;

begin
  while True do
    EnumerateSessionsEx;
end.
tjenks
  • 236
  • 4
  • 12
  • How do you diagnose this supposed leak? – David Heffernan Jul 24 '13 at 17:03
  • 1
    Note that your error checking is all wrong. Only call GetLastError when the function calls fail. You must check function return values. – David Heffernan Jul 24 '13 at 17:05
  • I noticed the memory leak by tracking the memory usage for the process in Task manager over a period of time. When an application starts out using ~2/3MB at runtime and is using 27MB 3 hours later then you know something is wrong. Regarding the error checking, I will amend that in the morning, thank you. – tjenks Jul 24 '13 at 17:08
  • 3
    Task Manager is not a valid memory leak detector. How do you know that WTS is the issue? How about an SSCCE that demonstrates the issue? – David Heffernan Jul 24 '13 at 17:09
  • 1
    Without wanting to be rude, why not? It gives you information about the memory usage of a process. Whilst not being the most precise tool in diagnostics it is surely good enough if you have a good idea of the memory usage in the rest of the application. I guess WTS is the issue as when I comment out the API call (Result := EnumerateSessionsEx(...)) and force constant data into my TSessions structure, the memory usage remains constant (again tracked by Task Manager) if Task manager is not sufficient, could you recommend a valid memory leak detector? I will attach an example tomorrow morning. – tjenks Jul 24 '13 at 17:19
  • 1
    Problem is that apps may choose not to return memory to system even when you free. I'm not saying that you don't have a leak. Just that your diagnosis is imprecise. – David Heffernan Jul 24 '13 at 17:25
  • 2
    I've got a 40 line SSCCE and I agree that code leaks. One workaround is only to enumerate sessions when you receive notification that a new one has been created. – David Heffernan Jul 24 '13 at 17:57
  • Your declaration of ` WTS_SESSION_INFO_1` is wrong, string members would be `LPWSTR` instead of `LPTSTR`. – Sertac Akyuz Jul 24 '13 at 18:54
  • @Sertac It's a Unicode Delphi so `LPTSTR` is `PWideChar`. You can tell its a Unicode Delphi since the calls to `WideCharToString` compile. – David Heffernan Jul 24 '13 at 19:15
  • 1
    OK, I added an SSCCE that more clearly illustrates the issue, and removes all possible confounding elements. I note that `WTSEnumerateSessions` appears not to have any problems. I don't know whether or not you can use that instead. – David Heffernan Jul 24 '13 at 19:17
  • @tjenks: take a look at http://www.eurekalog.com. I use it for more than 3 years and it helps me a lot! – AlexSC Jul 24 '13 at 19:35
  • @AlexSC How would that help with memory leak in Win API? – David Heffernan Jul 24 '13 at 19:36
  • @David: indeed, not helpful for WinApi, only Delphi-allocated memory. My mistake, sorry. – AlexSC Jul 24 '13 at 19:54
  • did you set minenumsize to 4? – Remko Jul 24 '13 at 20:07
  • what does WtsFreeMemoryEx return, bool or boolean, try integer and check what it returns (see http://blog.delphi-jedi.net/2008/09/25/bool-boolean-and-integer/) – Remko Jul 24 '13 at 20:10
  • @Remko `WTSFreeMemoryEx` returns `BOOL`. Forget the first block of code, just look at the second block. – David Heffernan Jul 24 '13 at 20:36
  • Thank you for the responses. I would be using WTSEnumerateSessions if it provided username information since I need to know WHO is logged in. I will try a combination of `WTSEnumerateSessions` and `WTSQuerySessionInformation` to see if that is leak-free. Am I to understand that since the memory isn't being returned to system even with error free calls to `WTSFreeMemoryEx` that that memory is out of my control? And yes, sorry - I'm not sure how I came to take BOOL=DWORD, it will be corrected. – tjenks Jul 25 '13 at 08:26
  • 1
    I think my SSCCE is strong evidence that there is a fault in the Windows code. I've added an answer now along these lines. I don't think we can do much better than the couple of work arounds I suggested above, and repeated in the answer. – David Heffernan Jul 25 '13 at 09:38
  • I can't reproduce this issue on Windows 10 Pro version 2004 64-bit. That implies that Microsoft has fixed it at some point in the last 7 or 8 years. – Simon Kissane May 07 '21 at 04:21

3 Answers3

4

To summarise the comment trail, I think that there is a fault in the WTS library code, that afflicts the WTSEnumerateSessionsEx and WTSFreeMemoryEx functions. The SSCCE that I added to the question gives a pretty clear demonstration of that.

So, your options to work around the fault would appear to be:

  1. Only call WTSEnumerateSessionsEx when you get notified that a session is created or destroyed. That would minimise the number of calls you make. You'd still be left with a leak, but I suspect that it would take a very long time before you encountered problems.
  2. Switch to WTSEnumerateSessions and then call WTSQuerySessionInformation to obtain any extra information that you need. From my trials, WTSEnumerateSessions would appear not to be afflicted by the same problem as WTSEnumerateSessionsEx.
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
2

I created the same sample in MSVC:

#include <Windows.h>
#include <WtsApi32.h>
#pragma comment(lib, "wtsapi32")

int _tmain(int argc, _TCHAR* argv[])
{
    DWORD Level = 1;
    PWTS_SESSION_INFO_1 pSessionInfo;
    DWORD Count = 0;
    BOOL bRes;
    while (WTSEnumerateSessionsEx(WTS_CURRENT_SERVER_HANDLE, &Level, 0, &pSessionInfo, &Count))
    {
        if (!WTSFreeMemoryEx(WTSTypeSessionInfoLevel1, pSessionInfo, Count))
        {
            break;
        }
    }

    return 0;
}

I am observing the same behaviour in Task Manager and even though Task Manager is not a tool to track memory leaks this behaviour is clearly a leak and it seems like a bug. It happens both in x86 and x64 build (x64 uses the x64 version of WtsApi32.dll).

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
Remko
  • 7,214
  • 2
  • 32
  • 52
  • 1
    I'm not seeing any evidence of a memory leak when I run this code on Windows 10 Pro (version 2004) 64-bit. I suspect Microsoft has fixed this bug in more recent versions of Windows. – Simon Kissane May 07 '21 at 04:18
-1

When you have finished using the array, free it by calling the WTSFreeMemoryEx function. You should also set the pointer to NULL. (C) https://learn.microsoft.com/en-us/windows/desktop/api/wtsapi32/nf-wtsapi32-wtsenumeratesessionsexa

guest
  • 1
  • 1
  • I don't see any mention of "NULL"s on the documentation page you linked. Besides it makes no difference, I bothered to run the test program at the end of the question incorporating your suggestion. You can do the same. – Sertac Akyuz Sep 21 '18 at 00:10
  • Can you please explain the purpose of setting the pointer to NULL – Kingsley Sep 21 '18 at 00:11
  • https://learn.microsoft.com/en-us/windows/desktop/api/wtsapi32/nf-wtsapi32-wtsenumeratesessionsexa – guest Sep 24 '18 at 19:56