[NTDLL_APITEST] Add a test for Nt[Query|Set]InformationThread(ThreadNameInformation) (#8484)

Thanks to 'artDev', 'HP-12C', and 'julenuri' Discord contributors for their help!
This commit is contained in:
Hermès Bélusca-Maïto
2025-11-24 18:01:22 +01:00
parent 2e1a0b73c9
commit 401f3a8a79

View File

@@ -1,13 +1,20 @@
/*
* PROJECT: ReactOS API tests
* LICENSE: GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later)
* PURPOSE: Tests for the NtQueryInformationThread API
* COPYRIGHT: Copyright 2020 George Bișoc <george.bisoc@reactos.org>
* PROJECT: ReactOS API tests
* LICENSE: GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later)
* PURPOSE: Tests for the NtQueryInformationThread API
* COPYRIGHT: Copyright 2020 George Bișoc <george.bisoc@reactos.org>
* Copyright 2025-2026 Hermès Bélusca-Maïto <hermes.belusca-maito@reactos.org>
*/
#include "precomp.h"
#include <internal/ps_i.h>
#define ok_wchar(x, y) ok_hex((unsigned)(unsigned short)(x), (unsigned)(unsigned short)(y))
#define ok_nwstr(str1, str2, count) \
ok(wcsncmp((PWCHAR)(str1), (PWCHAR)(str2), (count)) == 0, \
"Wrong string. Expected '%.*S', got '%.*S'\n", (count), (str1), (count), (str2))
static
void
Test_ThreadBasicInformationClass(void)
@@ -101,6 +108,14 @@ Test_ThreadBasicInformationClass(void)
HeapFree(GetProcessHeap(), 0, ThreadInfoBasic);
}
/**
* @brief
* Dummy thread used by the Test_ThreadHideFromDebuggerClass()
* and the Test_ThreadNameInformation() API tests.
*
* @param[in] Parameter
* Handle to an event that tells the thread to terminate itself when signaled.
**/
static ULONG
NTAPI
DummyThread(
@@ -295,9 +310,316 @@ Test_ThreadQueryAlignmentProbe(void)
}
}
/**
* @brief An NT-equivalent of kernel32!GetThreadDescription().
**/
static NTSTATUS
ntGetThreadName(
_In_ HANDLE hThread,
_Outptr_ PTHREAD_NAME_INFORMATION* ppThreadNameInfo,
_Out_ PULONG pLength,
_In_ UCHAR Fill)
{
PTHREAD_NAME_INFORMATION NameInfo = NULL;
PVOID ptr;
NTSTATUS Status;
ULONG Length;
*ppThreadNameInfo = NULL;
*pLength = 0;
/* Since there may be concurrent SetThreadDescription() invocations being
* executed, we need to loop over the buffer allocation size until we can
* successfully capture the thread name. */
Length = 0;
Status = NtQueryInformationThread(hThread, ThreadNameInformation, NULL, 0, &Length);
if (Status != STATUS_BUFFER_TOO_SMALL)
return Status;
/* Loop again only if we get buffer-too-small types of errors;
* otherwise, break the loop: any error will be handled below. */
while (Status == STATUS_INFO_LENGTH_MISMATCH ||
Status == STATUS_BUFFER_TOO_SMALL)
{
/* (Re-)allocate the information buffer */
ptr = (NameInfo ? RtlReAllocateHeap(RtlGetProcessHeap(), 0, NameInfo, Length)
: RtlAllocateHeap(RtlGetProcessHeap(), 0, Length));
if (!ptr)
{
Status = STATUS_NO_MEMORY;
break;
}
RtlFillMemory(ptr, Length, Fill);
NameInfo = ptr;
Status = NtQueryInformationThread(hThread, ThreadNameInformation,
NameInfo, Length, &Length);
// Status should be STATUS_SUCCESS, unless concurrent SetThreadDescription() happens.
}
if (NT_SUCCESS(Status))
{
*ppThreadNameInfo = NameInfo;
*pLength = Length;
}
else
{
RtlFreeHeap(RtlGetProcessHeap(), 0, NameInfo);
}
return Status;
}
static void
DoThreadNameTest(
_In_ UINT i,
_In_ HANDLE hThread,
_In_opt_ PCWSTR TestName)
{
BOOL bEmptyName = ((TestName == (PCWSTR)(ULONG_PTR)-1) || !TestName || !*TestName);
ULONG ExpectedNameLength = (ULONG)(bEmptyName ? 0 : wcslen(TestName) * sizeof(WCHAR));
ULONG ExpectedLength = sizeof(UNICODE_STRING) + ExpectedNameLength;
NTSTATUS Status;
ULONG Length;
PTHREAD_NAME_INFORMATION pNameInfo;
/* IMPORTANT REMARK: The fixed Buffer is made large enough
* to hold all the TestName's being tested in this file. */
struct { THREAD_NAME_INFORMATION Info; WCHAR Buffer[MAX_PATH]; } NameInfo;
ASSERT(sizeof(NameInfo.Buffer) >= ExpectedNameLength);
winetest_push_context("Test %lu", i);
/* Set a new thread name only if it's not '-1'; otherwise we
* just query the existing name without setting it before. */
if (TestName != (PCWSTR)(ULONG_PTR)-1)
{
/* Set a non-empty thread name, to reset it to something different
* from the previous case and distinguish with the next one. */
RtlFillMemory(&NameInfo, sizeof(NameInfo), 0xEF);
NameInfo.Info.ThreadName.Length = NameInfo.Info.ThreadName.MaximumLength = sizeof(NameInfo.Buffer);
NameInfo.Info.ThreadName.Buffer = NameInfo.Buffer;
Status = NtSetInformationThread(hThread, ThreadNameInformation,
&NameInfo.Info, sizeof(NameInfo.Info));
ok_ntstatus(Status, STATUS_SUCCESS);
/* Now set the thread name to be tested */
RtlFillMemory(&NameInfo, sizeof(NameInfo), 0xAB);
RtlInitUnicodeString(&NameInfo.Info.ThreadName, TestName);
Status = NtSetInformationThread(hThread, ThreadNameInformation,
&NameInfo.Info, sizeof(NameInfo.Info));
ok_ntstatus(Status, STATUS_SUCCESS);
}
/* Verify that ThreadNameInformation call with NULL buffer returns the expected length */
Length = 0;
Status = NtQueryInformationThread(hThread, ThreadNameInformation, NULL, 0, &Length);
ok_ntstatus(Status, STATUS_BUFFER_TOO_SMALL);
ok_long(Length, ExpectedLength);
pNameInfo = &NameInfo.Info;
/* Test ThreadNameInformation call with minimally-sized buffer */
RtlFillMemory(&NameInfo, sizeof(NameInfo), 0xAB);
Length = 0;
Status = NtQueryInformationThread(hThread, ThreadNameInformation,
pNameInfo, sizeof(*pNameInfo), &Length);
if (bEmptyName)
{
ok_ntstatus(Status, STATUS_SUCCESS);
ok_long(Length, ExpectedLength); // == sizeof(UNICODE_STRING);
ok_long((ULONG)pNameInfo->ThreadName.Length, 0);
}
else
{
ok_ntstatus(Status, STATUS_BUFFER_TOO_SMALL);
ok_long(Length, ExpectedLength); // == sizeof(UNICODE_STRING) + ExpectedNameLength;
ok_long((ULONG)pNameInfo->ThreadName.Length, 0xABAB);
}
ok_long((ULONG)pNameInfo->ThreadName.MaximumLength, (ULONG)pNameInfo->ThreadName.Length);
if (bEmptyName)
{
ok(pNameInfo->ThreadName.Buffer == NULL || /* ReactOS will set the Buffer to NULL */
/* All Win10+ versions erroneously point the Buffer past the end of the data */
broken(pNameInfo->ThreadName.Buffer == (PVOID)(&pNameInfo->ThreadName + 1)),
"Expected NULL pointer, got 0x%p\n", pNameInfo->ThreadName.Buffer);
/* NOTE: pNameInfo->ThreadName.Buffer[0] is invalid */
}
else
{
ok_ptr(pNameInfo->ThreadName.Buffer, (PVOID)(ULONG_PTR)0xABABABABABABABABULL);
}
RtlFillMemory(&NameInfo, sizeof(NameInfo), 0xAB);
Length = 0;
if (bEmptyName)
{
/* Retest the same with a slightly-larger buffer */
Status = NtQueryInformationThread(hThread, ThreadNameInformation,
&NameInfo,
sizeof(UNICODE_STRING) + sizeof(UNICODE_NULL),
&Length);
}
else
{
/* Test direct ThreadNameInformation call with correctly-sized buffer */
Status = NtQueryInformationThread(hThread, ThreadNameInformation,
&NameInfo,
ExpectedLength,
&Length);
}
ok_ntstatus(Status, STATUS_SUCCESS);
ok_long(Length, ExpectedLength);
ok_long((ULONG)pNameInfo->ThreadName.Length, ExpectedNameLength);
ok_long((ULONG)pNameInfo->ThreadName.MaximumLength, (ULONG)pNameInfo->ThreadName.Length);
if (bEmptyName)
{
ok(pNameInfo->ThreadName.Buffer == NULL || /* ReactOS will set the Buffer to NULL */
/* All Win10+ versions erroneously point the Buffer past the end of the data */
broken(pNameInfo->ThreadName.Buffer == (PVOID)(&pNameInfo->ThreadName + 1)),
"Expected NULL pointer, got 0x%p\n", pNameInfo->ThreadName.Buffer);
/* NOTE: pNameInfo->ThreadName.Buffer[0] should be invalid, but let's
* check past it to see what data there is (in the "broken" Windows case). */
if (pNameInfo->ThreadName.Buffer == (PVOID)(&pNameInfo->ThreadName + 1))
ok_wchar(pNameInfo->ThreadName.Buffer[0], 0xABAB);
}
else
{
ok_ptr(pNameInfo->ThreadName.Buffer, (PVOID)(&pNameInfo->ThreadName + 1));
if (pNameInfo->ThreadName.Buffer == (PVOID)(&pNameInfo->ThreadName + 1))
ok_nwstr(pNameInfo->ThreadName.Buffer, TestName, ExpectedNameLength / sizeof(WCHAR));
else
skip("pNameInfo->ThreadName.Buffer is invalid (0x%p)\n", pNameInfo->ThreadName.Buffer);
}
/* Test ThreadNameInformation with dynamically allocated buffer */
pNameInfo = NULL;
Length = 0;
Status = ntGetThreadName(hThread, &pNameInfo, &Length, 0xCD);
ok_ntstatus(Status, STATUS_SUCCESS);
ok_long(Length, ExpectedLength);
ok(pNameInfo != NULL, "Expected not NULL, got NULL\n");
if (pNameInfo)
{
ok_long((ULONG)pNameInfo->ThreadName.Length, ExpectedNameLength);
ok_long((ULONG)pNameInfo->ThreadName.MaximumLength, (ULONG)pNameInfo->ThreadName.Length);
if (bEmptyName)
{
ok(pNameInfo->ThreadName.Buffer == NULL || /* ReactOS will set the Buffer to NULL */
/* All Win10+ versions erroneously point the Buffer past the end of the data */
broken(pNameInfo->ThreadName.Buffer == (PVOID)(&pNameInfo->ThreadName + 1)),
"Expected NULL pointer, got 0x%p\n", pNameInfo->ThreadName.Buffer);
/* NOTE: pNameInfo->ThreadName.Buffer[0] is invalid */
}
else
{
ok_ptr(pNameInfo->ThreadName.Buffer, (PVOID)(&pNameInfo->ThreadName + 1));
if (pNameInfo->ThreadName.Buffer == (PVOID)(&pNameInfo->ThreadName + 1))
ok_nwstr(pNameInfo->ThreadName.Buffer, TestName, ExpectedNameLength / sizeof(WCHAR));
else
skip("pNameInfo->ThreadName.Buffer is invalid (0x%p)\n", pNameInfo->ThreadName.Buffer);
}
RtlFreeHeap(RtlGetProcessHeap(), 0, pNameInfo);
}
else
{
skip("pNameInfo is NULL\n");
}
winetest_pop_context();
}
static
void
Test_ThreadNameInformation(void)
{
ULONG NTDDIVersion;
NTSTATUS Status;
HANDLE hWaitEvent = NULL, hThread = NULL;
OSVERSIONINFOEXW osVerInfo;
osVerInfo.dwOSVersionInfoSize = sizeof(osVerInfo);
GetVersionExW((LPOSVERSIONINFOW)&osVerInfo);
trace("osVerInfo: %lu.%lu.%lu ('%S')\n",
osVerInfo.dwMajorVersion,
osVerInfo.dwMinorVersion,
osVerInfo.dwBuildNumber,
osVerInfo.szCSDVersion);
NTDDIVersion = GetNTDDIVersion();
trace("NTDDIVersion: 0x%08lx\n", NTDDIVersion);
if (NTDDIVersion < NTDDI_WIN10_RS1)
{
trace("Running %s on NT %hu.%hu(.%hu), it may not work!\n",
__FUNCTION__,
(NTDDIVersion & 0xFF000000) >> 24,
(NTDDIVersion & 0x00FF0000) >> 16,
(NTDDIVersion & 0x000000FF));
}
Status = NtCreateEvent(&hWaitEvent,
EVENT_ALL_ACCESS,
NULL,
NotificationEvent,
FALSE);
ok_ntstatus(Status, STATUS_SUCCESS);
ok(hWaitEvent != NULL, "Expected not NULL, got NULL\n");
if (!NT_SUCCESS(Status))
{
skip("Could not create the stop event! (Status 0x%08lx)\n", Status);
goto Quit;
}
/* Create the dummy thread and wait for it to start up */
Status = RtlCreateUserThread(NtCurrentProcess(),
NULL,
FALSE,
0,
0,
0,
DummyThread,
(PVOID)hWaitEvent,
&hThread,
NULL);
ok_ntstatus(Status, STATUS_SUCCESS);
ok(hThread != NULL, "Expected not NULL, got NULL\n");
if (!NT_SUCCESS(Status))
{
skip("Failed to create the dummy thread (Status 0x%08lx)\n", Status);
goto Quit;
}
/* EXPECTATION: Initial thread name is empty. */
DoThreadNameTest(1, hThread, (PCWSTR)(ULONG_PTR)-1);
/* Set a non-empty thread name.
* EXPECTATION: Thread name is what we have set. */
DoThreadNameTest(2, hThread, L"Hello World!");
/* Set an empty-string thread name.
* EXPECTATION: Thread name is empty. */
DoThreadNameTest(3, hThread, L"");
/* Set a NULL-pointer empty-string thread name.
* EXPECTATION: Thread name is empty. */
DoThreadNameTest(4, hThread, NULL);
/* Send the kill signal and wait for the thread to terminate */
NtSetEvent(hWaitEvent, NULL);
NtWaitForSingleObject(hThread, FALSE, NULL);
/* Close the thread */
NtTerminateThread(hThread, STATUS_SUCCESS);
NtClose(hThread);
Quit:
/* Close the event */
if (hWaitEvent)
NtClose(hWaitEvent);
}
START_TEST(NtQueryInformationThread)
{
Test_ThreadBasicInformationClass();
Test_ThreadHideFromDebuggerClass();
Test_ThreadQueryAlignmentProbe();
Test_ThreadNameInformation();
}