Non-legacy console as of 10.0.15063 no longer destroys abandoned screen buffers #140

Closed
opened 2026-01-30 21:43:33 +00:00 by claunia · 5 comments
Owner

Originally created by @bitcrazed on GitHub (Feb 16, 2018).

From @rprichard on May 21, 2017 1:46

In previous versions of Windows, once a screen buffer is no longer referenced, the console frees the memory associated with it, and if it was the active buffer, it activates a different buffer. As of 15063, in certain situations, the non-legacy console doesn't do this anymore. Maybe the new behavior is intentional, but it's at least surprising.

Example 1:

#include <windows.h>

int main() {
    const HANDLE buf = CreateConsoleScreenBuffer(
                GENERIC_READ | GENERIC_WRITE,
                FILE_SHARE_READ | FILE_SHARE_WRITE,
                NULL,
                CONSOLE_TEXTMODE_BUFFER, NULL);
    SetConsoleActiveScreenBuffer(buf);
    const HANDLE buf2 = CreateFileW(L"CONOUT$",
                GENERIC_READ | GENERIC_WRITE,
                FILE_SHARE_READ | FILE_SHARE_WRITE,
                NULL,
                OPEN_EXISTING, 0, NULL);
    CloseHandle(buf2);
}

This test program creates a new buffer, activates it, opens and closes CONOUT$, then exits. It does not restore the originally active screen buffer. Normally, Windows would restore the original buffer, but as of 15063's non-legacy console, it doesn't. If I run this test program from cmd.exe or powershell.exe, I'm left with a console where I can still issue commands, but much of the output is hidden. (Everything is hidden with PowerShell, but with cmd.exe, I can only see what I'm typing. I can't see dir, but I can see a child process' output.)

Here's what cmd.exe looks like:

example1

Example 2:

#include <windows.h>
#include <assert.h>
#include <algorithm>

static COORD get_size(HANDLE handle) {
    CONSOLE_SCREEN_BUFFER_INFO info = {};
    assert(GetConsoleScreenBufferInfo(handle, &info));
    return info.dwSize;
}

int main() {
    const HANDLE buf = CreateConsoleScreenBuffer(
                GENERIC_READ | GENERIC_WRITE,
                FILE_SHARE_READ | FILE_SHARE_WRITE,
                NULL,
                CONSOLE_TEXTMODE_BUFFER, NULL);
    COORD size = get_size(buf);
    size.X = std::max<SHORT>(size.X, 1000);
    size.Y = std::max<SHORT>(size.Y, 30000);
    assert(SetConsoleScreenBufferSize(buf, size));

    SetConsoleActiveScreenBuffer(buf);

    // It's necessary to create either a \Device\ConDrv\CurrentOut or a
    // \Device\ConDrv\Output that references the buffer.  Otherwise, the
    // console will successfully deallocate the unused buffer.
    const HANDLE buf2 = CreateFileW(L"CONOUT$",
                GENERIC_READ | GENERIC_WRITE,
                FILE_SHARE_READ | FILE_SHARE_WRITE,
                NULL,
                OPEN_EXISTING, 0, NULL);
    CloseHandle(buf2);

    // Ordinarily, we wouldn't need to restore the original active buffer;
    // Windows would do it automatically.  It doesn't as of the 15063
    // non-legacy console.
    SetConsoleActiveScreenBuffer(GetStdHandle(STD_OUTPUT_HANDLE));

    CloseHandle(buf);
}

Every time this command-line program runs, it leaks ~100MB of memory in the conhost.exe process it's communicating with. The memory isn't freed until conhost.exe exits.


This issue doesn't affect the legacy console in 15063, and it doesn't affect either the legacy or non-legacy consoles in 14393.

I noticed this issue because my test suite for win32-console-docs failed: https://github.com/rprichard/win32-console-docs/issues/3.


Aside: I also noticed that as of the Win8 console changes, it's apparently impossible to free the console's initial screen buffer. It seems possible to leak the initial buffer using these steps:

  1. [Parent] Create a new buffer and make it active.
  2. [Parent] Create a child process with bInheritHandles of either TRUE or FALSE.
  3. [Parent] Exit.

At this point, there is a console with one process attached, and there is no way to access the initial buffer, but it's still taking up memory.

I logged this curiosity at https://github.com/rprichard/win32-console-docs/issues/4.

Copied from original issue: Microsoft/WSL#2135

Originally created by @bitcrazed on GitHub (Feb 16, 2018). _From @rprichard on May 21, 2017 1:46_ In previous versions of Windows, once a screen buffer is no longer referenced, the console frees the memory associated with it, and if it was the active buffer, it activates a different buffer. As of 15063, in certain situations, the non-legacy console doesn't do this anymore. Maybe the new behavior is intentional, but it's at least surprising. # Example 1: ``` #include <windows.h> int main() { const HANDLE buf = CreateConsoleScreenBuffer( GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CONSOLE_TEXTMODE_BUFFER, NULL); SetConsoleActiveScreenBuffer(buf); const HANDLE buf2 = CreateFileW(L"CONOUT$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); CloseHandle(buf2); } ``` This test program creates a new buffer, activates it, opens and closes `CONOUT$`, then exits. It does *not* restore the originally active screen buffer. Normally, Windows would restore the original buffer, but as of 15063's non-legacy console, it doesn't. If I run this test program from `cmd.exe` or `powershell.exe`, I'm left with a console where I can still issue commands, but much of the output is hidden. (Everything is hidden with PowerShell, but with cmd.exe, I can only see what I'm typing. I can't see `dir`, but I can see a child process' output.) Here's what `cmd.exe` looks like: ![example1](https://cloud.githubusercontent.com/assets/1572855/26280439/dd07a452-3d99-11e7-91a5-da3fa9a687f0.png) # Example 2: ``` #include <windows.h> #include <assert.h> #include <algorithm> static COORD get_size(HANDLE handle) { CONSOLE_SCREEN_BUFFER_INFO info = {}; assert(GetConsoleScreenBufferInfo(handle, &info)); return info.dwSize; } int main() { const HANDLE buf = CreateConsoleScreenBuffer( GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CONSOLE_TEXTMODE_BUFFER, NULL); COORD size = get_size(buf); size.X = std::max<SHORT>(size.X, 1000); size.Y = std::max<SHORT>(size.Y, 30000); assert(SetConsoleScreenBufferSize(buf, size)); SetConsoleActiveScreenBuffer(buf); // It's necessary to create either a \Device\ConDrv\CurrentOut or a // \Device\ConDrv\Output that references the buffer. Otherwise, the // console will successfully deallocate the unused buffer. const HANDLE buf2 = CreateFileW(L"CONOUT$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); CloseHandle(buf2); // Ordinarily, we wouldn't need to restore the original active buffer; // Windows would do it automatically. It doesn't as of the 15063 // non-legacy console. SetConsoleActiveScreenBuffer(GetStdHandle(STD_OUTPUT_HANDLE)); CloseHandle(buf); } ``` Every time this command-line program runs, it leaks ~100MB of memory in the `conhost.exe` process it's communicating with. The memory isn't freed until `conhost.exe` exits. --- This issue doesn't affect the legacy console in 15063, and it doesn't affect either the legacy or non-legacy consoles in 14393. I noticed this issue because my test suite for win32-console-docs failed: https://github.com/rprichard/win32-console-docs/issues/3. --- Aside: I also noticed that as of the Win8 console changes, it's apparently impossible to free the console's initial screen buffer. It *seems* possible to leak the initial buffer using these steps: 1. [Parent] Create a new buffer and make it active. 2. [Parent] Create a child process with bInheritHandles of either TRUE or FALSE. 3. [Parent] Exit. At this point, there is a console with one process attached, and there is no way to access the initial buffer, but it's still taking up memory. I logged this curiosity at https://github.com/rprichard/win32-console-docs/issues/4. _Copied from original issue: Microsoft/WSL#2135_
claunia added the Product-ConhostResolution-Fix-Available labels 2026-01-30 21:43:33 +00:00
Author
Owner

@bitcrazed commented on GitHub (Feb 16, 2018):

From @rprichard on May 21, 2017 2:3

Can I get a confirmation that this behavior change is intentional or not?

I think it may affect my winpty project for programs that use their own screen buffers. I haven't received any bug reports yet, and I'm not sure off hand which programs do create their own screen buffers. (Some full screen editors, I guess?) winpty scrapes the active console screen buffer -- on each poll, it opens CONOUT$, then closes it. This issue would increase the likelihood of (a) memory leaks and (b) leaving a program's screen buffer active, especially if that program crashed.

@bitcrazed commented on GitHub (Feb 16, 2018): _From @rprichard on May 21, 2017 2:3_ Can I get a confirmation that this behavior change is intentional or not? I think it may affect my winpty project for programs that use their own screen buffers. I haven't received any bug reports yet, and I'm not sure off hand which programs *do* create their own screen buffers. (Some full screen editors, I guess?) winpty scrapes the active console screen buffer -- on each poll, it opens `CONOUT$`, then closes it. This issue would increase the likelihood of (a) memory leaks and (b) leaving a program's screen buffer active, especially if that program crashed.
Author
Owner

@bitcrazed commented on GitHub (Feb 16, 2018):

From @fpqc on May 21, 2017 4:37

@zadjii-msft ping

@bitcrazed commented on GitHub (Feb 16, 2018): _From @fpqc on May 21, 2017 4:37_ @zadjii-msft ping
Author
Owner

@bitcrazed commented on GitHub (Feb 16, 2018):

From @rprichard on May 22, 2017 6:45

FWIW, my winver is: Version 1703 (OS Build 15063.296)

@bitcrazed commented on GitHub (Feb 16, 2018): _From @rprichard on May 22, 2017 6:45_ FWIW, my winver is: `Version 1703 (OS Build 15063.296)`
Author
Owner

@bitcrazed commented on GitHub (Feb 16, 2018):

From @zadjii-msft on May 22, 2017 16:49

Yikes, that probably shouldn't happen. A behavior difference between legacy and v2 like this is almost certainly a bug.

This is being tracked by MSFT:12092883.

@bitcrazed commented on GitHub (Feb 16, 2018): _From @zadjii-msft on May 22, 2017 16:49_ Yikes, that probably shouldn't happen. A behavior difference between legacy and v2 like this is almost certainly a bug. This is being tracked by MSFT:12092883.
Author
Owner

@bitcrazed commented on GitHub (Feb 16, 2018):

From @zadjii-msft on May 25, 2017 21:51

Got this one figured out!

There was a macro that wrapped a macro that wrapped the allocate handle function. That would cause the refcount to increase twice on opening CONOUT$.

We're reviewing the fix now. Thanks for the repro cases and detailed bug report @rprichard ! It's a ton easier to fix bugs where there are great repros like this.

@bitcrazed commented on GitHub (Feb 16, 2018): _From @zadjii-msft on May 25, 2017 21:51_ Got this one figured out! There was a macro that wrapped a macro that wrapped the allocate handle function. That would cause the refcount to increase twice on opening CONOUT$. We're reviewing the fix now. Thanks for the repro cases and detailed bug report @rprichard ! It's a ton easier to fix bugs where there are great repros like this.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/terminal#140