[NTOS:PS][NTDLL_APITESTS] Implement ThreadNameInformation for NtQuery/SetInformationThread() (#8484)

The `ThreadNameInformation` (#38) class is the Windows 10.1607+ way
of assigning a human-readable name (i.e. description) to a given
thread object, that is visible to debuggers and diagnostic tools
(e.g. WinDbg `!thread` command, Process Explorer ...), which is
useful for debugging scenarios.[^1]

Before this, the only way to assign a name to a thread for debugging
purposes was to raise a specific exception, that had to be caught and
interpreted by a supported debugger.[^2][^3]

When the thread object is being deleted (`kill.c!PspDeleteThread()`),
free the thread name if set (courtesy of Ahmed Arif, PR #8796).

References:
[^1]: https://learn.microsoft.com/en-us/visualstudio/debugger/tips-for-debugging-threads
[^2]: https://learn.microsoft.com/en-us/archive/blogs/stevejs/naming-threads-in-win32-and-net
[^3]: https://ofekshilon.com/2009/04/10/naming-threads/
This commit is contained in:
Hermès Bélusca-Maïto
2025-11-23 20:31:13 +01:00
parent 401f3a8a79
commit a4621bb280
6 changed files with 205 additions and 5 deletions

View File

@@ -56,7 +56,7 @@ QuerySetProcessValidator(
break;
}
/* This one works different from the others */
/* This one works differently from the others */
case ProcessUserModeIOPL:
{
if (ExpectedStatus == STATUS_INFO_LENGTH_MISMATCH)
@@ -159,7 +159,7 @@ QuerySetProcessValidator(
break;
}
/* This one works different from the others */
/* This one works differently from the others */
case ProcessUserModeIOPL:
{
if (ExpectedStatus == STATUS_INFO_LENGTH_MISMATCH)
@@ -295,6 +295,21 @@ QuerySetThreadValidator(
break;
}
/* ThreadNameInformation is Windows 10+, but
* ReactOS supports this class, so don't exclude it */
case ThreadNameInformation:
{
#ifndef __REACTOS__
if (GetNTVersion() < _WIN32_WINNT_WIN10)
SpecialStatus = STATUS_INVALID_INFO_CLASS;
#else
/* This one works differently from the others */
if (ExpectedStatus == STATUS_INFO_LENGTH_MISMATCH)
ExpectedStatus = STATUS_BUFFER_TOO_SMALL;
#endif
break;
}
default:
{
/* All of these classes only exist on Windows 7 and above */
@@ -381,6 +396,17 @@ QuerySetThreadValidator(
break;
}
/* ThreadNameInformation is Windows 10+, but
* ReactOS supports this class, so don't exclude it */
case ThreadNameInformation:
{
#ifndef __REACTOS__
if (GetNTVersion() < _WIN32_WINNT_WIN10)
SpecialStatus = STATUS_INVALID_INFO_CLASS;
#endif
break;
}
default:
{
/* All of these classes only exist on Windows 7 and above */

View File

@@ -593,8 +593,15 @@ static const INFORMATION_CLASS_INFO PsThreadInfoClass[] =
IQS_NONE,
/* ThreadContainerId */
IQS_NONE,
/* ThreadNameInformation */
IQS_NONE,
IQS_SAME
(
UNICODE_STRING,
ULONG_PTR,
ICIF_QUERY | ICIF_SET | ICIF_SIZE_VARIABLE
),
/* ThreadSelectedCpuSets */
IQS_NONE,
/* ThreadSystemThreadInformation */

View File

@@ -136,6 +136,7 @@
#define TAG_PS_APC 'pasP' /* Psap - Ps APC */
#define TAG_SHIM 'MIHS'
#define TAG_QUOTA_BLOCK 'bQsP'
#define TAG_THREAD_NAME 'mNhT'
/* Run-Time Library Tags */
#define TAG_HDTB 'BTDH'

View File

@@ -416,11 +416,19 @@ PspDeleteThread(IN PVOID ObjectBody)
}
}
/* Cleanup impersionation information */
/* Cleanup impersonation information */
PspDeleteThreadSecurity(Thread);
/* Free the thread name if set */
if (Thread->ThreadName)
{
ExFreePoolWithTag(Thread->ThreadName, TAG_THREAD_NAME);
Thread->ThreadName = NULL;
}
/* Make sure the thread was inserted, before continuing */
if (!Process) return;
if (!Process)
return;
/* Check if the thread list is valid */
if (Thread->ThreadListEntry.Flink)

View File

@@ -2879,6 +2879,92 @@ NtSetInformationThread(
break;
}
#if (NTDDI_VERSION >= NTDDI_WIN10_RS1) || defined(__REACTOS__)
case ThreadNameInformation:
{
UNICODE_STRING CapturedThreadName;
PUNICODE_STRING NewThreadName;
/* Check buffer length */
if (ThreadInformationLength != sizeof(UNICODE_STRING))
{
Status = STATUS_INFO_LENGTH_MISMATCH;
break;
}
/* Reference the thread.
* NOTE: Win10+ uses THREAD_SET_LIMITED_INFORMATION instead;
* however some tools misuse thread names to perform suspicious
* operations; therefore we try to mess with these by requiring
* a bit more of access rights. */
Status = ObReferenceObjectByHandle(ThreadHandle,
THREAD_SET_INFORMATION,
PsThreadType,
PreviousMode,
(PVOID*)&Thread,
NULL);
if (!NT_SUCCESS(Status))
break;
/* Probe and capture the thread name */
Status = ProbeAndCaptureUnicodeString(&CapturedThreadName,
PreviousMode,
(PUNICODE_STRING)ThreadInformation);
if (!NT_SUCCESS(Status))
{
ObDereferenceObject(Thread);
break;
}
/* Allocate a new buffer only if the thread name isn't empty
* (REMARK: We only consider Length instead of MaximumLength).
* If empty, just reset the thread name pointer to NULL instead
* of allocating an empty UNICODE_STRING. */
NewThreadName = NULL;
if (CapturedThreadName.Length > 0)
{
ULONG Length = sizeof(UNICODE_STRING) + CapturedThreadName.Length;
NewThreadName = ExAllocatePoolWithTag(NonPagedPool, // FIXME: NonPagedPoolNx
Length, TAG_THREAD_NAME);
if (!NewThreadName)
{
Status = STATUS_INSUFFICIENT_RESOURCES;
}
else
{
/* Copy the new thread name */
NewThreadName->Length =
NewThreadName->MaximumLength = CapturedThreadName.Length;
NewThreadName->Buffer = (PWCH)(NewThreadName + 1);
RtlCopyMemory(NewThreadName->Buffer,
CapturedThreadName.Buffer,
CapturedThreadName.Length);
}
}
/* Free the captured string */
ReleaseCapturedUnicodeString(&CapturedThreadName, PreviousMode);
/* Replace the original thread name with the new one */
if (NT_SUCCESS(Status))
{
PUNICODE_STRING OldThreadName;
PspLockThreadSecurityExclusive(Thread);
OldThreadName = Thread->ThreadName;
Thread->ThreadName = NewThreadName;
PspUnlockThreadSecurityExclusive(Thread);
/* Free the old thread name */
if (OldThreadName)
ExFreePoolWithTag(OldThreadName, TAG_THREAD_NAME);
}
/* Dereference the thread */
ObDereferenceObject(Thread);
break;
}
#endif /* (NTDDI_VERSION >= NTDDI_WIN10_RS1) || defined(__REACTOS__) */
/* Anything else */
default:
/* Not yet implemented */
@@ -3388,6 +3474,73 @@ NtQueryInformationThread(
break;
}
#if (NTDDI_VERSION >= NTDDI_WIN10_RS1) || defined(__REACTOS__)
case ThreadNameInformation:
{
PUNICODE_STRING ThreadName;
/* Reference the thread */
Status = ObReferenceObjectByHandle(ThreadHandle,
// FIXME: Use THREAD_QUERY_LIMITED_INFORMATION when implemented
THREAD_QUERY_INFORMATION,
PsThreadType,
PreviousMode,
(PVOID*)&Thread,
NULL);
if (!NT_SUCCESS(Status))
break;
PspLockThreadSecurityShared(Thread);
ThreadName = Thread->ThreadName;
/* Set the return length (REMARK: We only
* consider Length instead of MaximumLength) */
Length = sizeof(UNICODE_STRING);
Length += (ThreadName ? ThreadName->Length : 0);
if (ThreadInformationLength < Length)
{
PspUnlockThreadSecurityShared(Thread);
ObDereferenceObject(Thread);
Status = STATUS_BUFFER_TOO_SMALL;
/* As on Windows, and *not* STATUS_INFO_LENGTH_MISMATCH */
break;
}
/* Protect writes with SEH */
_SEH2_TRY
{
PTHREAD_NAME_INFORMATION NameInfo =
(PTHREAD_NAME_INFORMATION)ThreadInformation;
if (ThreadName && (ThreadName->Length > 0))
{
NameInfo->ThreadName.Length =
NameInfo->ThreadName.MaximumLength = ThreadName->Length;
NameInfo->ThreadName.Buffer = (PWCH)(&NameInfo->ThreadName + 1);
RtlCopyMemory(NameInfo->ThreadName.Buffer,
ThreadName->Buffer,
ThreadName->Length);
}
else
{
RtlInitEmptyUnicodeString(&NameInfo->ThreadName, NULL, 0);
}
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
/* Get exception code */
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
PspUnlockThreadSecurityShared(Thread);
/* Dereference the thread */
ObDereferenceObject(Thread);
break;
}
#endif /* (NTDDI_VERSION >= NTDDI_WIN10_RS1) || defined(__REACTOS__) */
/* Anything else */
default:
/* Not yet implemented */

View File

@@ -1340,6 +1340,11 @@ typedef struct _ETHREAD
LIST_ENTRY AlpcWaitListEntry;
KSEMAPHORE AlpcWaitSemaphore;
ULONG CacheManagerCount;
#endif
// TODO: Missing Vista+ members
#if (NTDDI_VERSION >= NTDDI_WIN10_RS1) || defined(__REACTOS__)
PUNICODE_STRING ThreadName;
// TODO: Missing Win10+ members
#endif
} ETHREAD;