From 62eb66a1663351f80ed81f835d9d21c6f2549443 Mon Sep 17 00:00:00 2001 From: Zebediah Figura Date: Mon, 2 Nov 2020 20:24:07 -0600 Subject: [PATCH] ntdll: Reimplement Win32 futexes on top of thread-ID alerts. Signed-off-by: Zebediah Figura --- dlls/ntdll/ntdll_misc.h | 2 + dlls/ntdll/sync.c | 185 ++++++++++++++++++++++++++++++++++++++- dlls/ntdll/thread.c | 2 + dlls/ntdll/unix/loader.c | 3 - dlls/ntdll/unix/sync.c | 162 ---------------------------------- dlls/ntdll/unixlib.h | 4 - 6 files changed, 186 insertions(+), 172 deletions(-) diff --git a/dlls/ntdll/ntdll_misc.h b/dlls/ntdll/ntdll_misc.h index e0d371e4c54..8fc5e54e4a4 100644 --- a/dlls/ntdll/ntdll_misc.h +++ b/dlls/ntdll/ntdll_misc.h @@ -90,6 +90,8 @@ extern void init_directories(void) DECLSPEC_HIDDEN; extern struct _KUSER_SHARED_DATA *user_shared_data DECLSPEC_HIDDEN; +extern void addr_wait_free_entry(void) DECLSPEC_HIDDEN; + /* locale */ extern LCID user_lcid, system_lcid; extern DWORD ntdll_umbstowcs( const char* src, DWORD srclen, WCHAR* dst, DWORD dstlen ) DECLSPEC_HIDDEN; diff --git a/dlls/ntdll/sync.c b/dlls/ntdll/sync.c index 8df7015df9f..a1c0b90b366 100644 --- a/dlls/ntdll/sync.c +++ b/dlls/ntdll/sync.c @@ -37,6 +37,13 @@ #include "wine/debug.h" #include "ntdll_misc.h" +WINE_DEFAULT_DEBUG_CHANNEL(sync); + +static const char *debugstr_timeout( const LARGE_INTEGER *timeout ) +{ + if (!timeout) return "(infinite)"; + return wine_dbgstr_longlong( timeout->QuadPart ); +} /****************************************************************** * RtlRunOnceInitialize (NTDLL.@) @@ -531,13 +538,143 @@ NTSTATUS WINAPI RtlSleepConditionVariableSRW( RTL_CONDITION_VARIABLE *variable, return status; } +/* The following functions define a lock-free array mapping thread IDs to + * values, which can be grown but not shrunk. We do this by allocating one slice + * of the array at a time, and storing a pointer to the next slice at the end. + * + * This is both for efficiency (we want this function to be as fast as possible) + * and because locking the TEB list is hard otherwise—we need to safely access + * the TEB list, but cannot do so using any of these synchronization primitives, + * and we may need to access the TEB list before being inserted into it (e.g. + * from heap locks, or the TEB list lock itself.) + */ + +struct addr_wait_entry +{ + void *addr; + HANDLE tid; +}; + +struct addr_wait_array +{ + struct addr_wait_entry entries[(0x1000 - sizeof(struct addr_wait_entry *)) / sizeof(struct addr_wait_entry)]; + struct addr_wait_array *next; +}; + +static struct addr_wait_array first_addr_wait_array; + +static struct addr_wait_entry *addr_wait_allocate_entry( HANDLE tid ) +{ + struct addr_wait_array *array = &first_addr_wait_array; + + for (;;) + { + struct addr_wait_array *new_array = NULL; + SIZE_T size = sizeof(*new_array); + unsigned int i; + + for (;;) + { + for (i = 0; i < ARRAY_SIZE(array->entries); ++i) + { + if (!array->entries[i].tid && !InterlockedCompareExchangePointer( &array->entries[i].tid, tid, NULL )) + return &array->entries[i]; + } + + if (!array->next) break; + array = array->next; + } + + if (NtAllocateVirtualMemory( NtCurrentProcess(), (void **)&new_array, 0, &size, MEM_COMMIT, PAGE_READWRITE )) + return NULL; + + if (InterlockedCompareExchangePointer( (void **)&array->next, new_array, NULL )) + { + /* another thread beat us to it */ + NtFreeVirtualMemory( NtCurrentProcess(), (void **)&new_array, &size, MEM_RELEASE ); + } + /* start searching again from the new array */ + array = array->next; + } +} + +void addr_wait_free_entry(void) +{ + struct addr_wait_entry *entry = NtCurrentTeb()->ReservedForPerf; + if (entry) + InterlockedExchangePointer( &entry->tid, NULL ); +} + +static BOOL compare_addr( const void *addr, const void *cmp, SIZE_T size ) +{ + switch (size) + { + case 1: + return (*(const UCHAR *)addr == *(const UCHAR *)cmp); + case 2: + return (*(const USHORT *)addr == *(const USHORT *)cmp); + case 4: + return (*(const ULONG *)addr == *(const ULONG *)cmp); + case 8: + return (*(const ULONG64 *)addr == *(const ULONG64 *)cmp); + } + + return FALSE; +} + /*********************************************************************** * RtlWaitOnAddress (NTDLL.@) */ NTSTATUS WINAPI RtlWaitOnAddress( const void *addr, const void *cmp, SIZE_T size, const LARGE_INTEGER *timeout ) { - return unix_funcs->RtlWaitOnAddress( addr, cmp, size, timeout ); + struct addr_wait_entry *entry = NtCurrentTeb()->ReservedForPerf; + NTSTATUS ret; + + TRACE("addr %p cmp %p size %#Ix timeout %s\n", addr, cmp, size, debugstr_timeout( timeout )); + + if (size != 1 && size != 2 && size != 4 && size != 8) + return STATUS_INVALID_PARAMETER; + + if (!entry) + { + if (!(entry = addr_wait_allocate_entry( NtCurrentTeb()->ClientId.UniqueThread ))) + return STATUS_NO_MEMORY; + NtCurrentTeb()->ReservedForPerf = entry; + } + + InterlockedExchangePointer( &entry->addr, (void *)addr ); + + /* Ensure that the compare-and-swap above is ordered before the comparison + * below. This barrier is paired with another in RtlWakeByAddress*(). + * + * In more detail, given the following sequence: + * + * Thread A Thread B + * ----------------------------------------------------------------- + * RtlWaitOnAddress( &val ); val = 1; + * queue thread RtlWakeByAddress( &val ); + * MemoryBarrier(); <---- paired with ----> MemoryBarrier(); + * compare_addr( &val ); if (thread is queued) + * + * We must ensure that the thread is queued [through the above + * InterlockedExchangePointer()] before reading "val", and that writes to + * "val" by the application happen before we check for queued threads. + * Otherwise, thread A can deadlock: "val" may appear unchanged, while + * thread B observed that thread A was not queued. + */ + MemoryBarrier(); + + if (!compare_addr( addr, cmp, size )) + { + InterlockedExchangePointer( &entry->addr, NULL ); + return STATUS_SUCCESS; + } + + ret = NtWaitForAlertByThreadId( NULL, timeout ); + InterlockedExchangePointer( &entry->addr, NULL ); + if (ret == STATUS_ALERTED) ret = STATUS_SUCCESS; + return ret; } /*********************************************************************** @@ -545,7 +682,26 @@ NTSTATUS WINAPI RtlWaitOnAddress( const void *addr, const void *cmp, SIZE_T size */ void WINAPI RtlWakeAddressAll( const void *addr ) { - return unix_funcs->RtlWakeAddressAll( addr ); + struct addr_wait_array *array; + unsigned int i; + + TRACE("%p\n", addr); + + if (!addr) return; + + /* Ensure that memory stores to "addr" are ordered before reading the + * array below. Paired with another barrier in RtlWaitOnAddress() [q.v.]. + */ + MemoryBarrier(); + + for (array = &first_addr_wait_array; array != NULL; array = array->next) + { + for (i = 0; i < ARRAY_SIZE(array->entries); ++i) + { + if (array->entries[i].addr == addr) + NtAlertThreadByThreadId( array->entries[i].tid ); + } + } } /*********************************************************************** @@ -553,5 +709,28 @@ void WINAPI RtlWakeAddressAll( const void *addr ) */ void WINAPI RtlWakeAddressSingle( const void *addr ) { - return unix_funcs->RtlWakeAddressSingle( addr ); + struct addr_wait_array *array; + unsigned int i; + + TRACE("%p\n", addr); + + if (!addr) return; + + /* Ensure that memory stores to "addr" are ordered before reading the + * array below. Paired with another barrier in RtlWaitOnAddress() [q.v.]. + */ + MemoryBarrier(); + + for (array = &first_addr_wait_array; array != NULL; array = array->next) + { + for (i = 0; i < ARRAY_SIZE(array->entries); ++i) + { + if (array->entries[i].addr == addr + && InterlockedCompareExchangePointer( &array->entries[i].addr, NULL, (void *)addr ) == addr) + { + NtAlertThreadByThreadId( array->entries[i].tid ); + return; + } + } + } } diff --git a/dlls/ntdll/thread.c b/dlls/ntdll/thread.c index 425e8770294..bc308e17bee 100644 --- a/dlls/ntdll/thread.c +++ b/dlls/ntdll/thread.c @@ -85,6 +85,8 @@ void WINAPI RtlExitUserThread( ULONG status ) NtQueryInformationThread( GetCurrentThread(), ThreadAmILastThread, &last, sizeof(last), NULL ); if (last) RtlExitUserProcess( status ); LdrShutdownThread(); + /* must be done last, in particular after any heap allocations */ + addr_wait_free_entry(); for (;;) NtTerminateThread( GetCurrentThread(), status ); } diff --git a/dlls/ntdll/unix/loader.c b/dlls/ntdll/unix/loader.c index 2ee67588df8..465ab427469 100644 --- a/dlls/ntdll/unix/loader.c +++ b/dlls/ntdll/unix/loader.c @@ -1584,9 +1584,6 @@ static struct unix_funcs unix_funcs = #endif DbgUiIssueRemoteBreakin, RtlGetSystemTimePrecise, - RtlWaitOnAddress, - RtlWakeAddressAll, - RtlWakeAddressSingle, fast_RtlpWaitForCriticalSection, fast_RtlpUnWaitCriticalSection, fast_RtlDeleteCriticalSection, diff --git a/dlls/ntdll/unix/sync.c b/dlls/ntdll/unix/sync.c index 86b9b3a4978..0ea8e28613c 100644 --- a/dlls/ntdll/unix/sync.c +++ b/dlls/ntdll/unix/sync.c @@ -78,10 +78,6 @@ WINE_DEFAULT_DEBUG_CHANNEL(sync); HANDLE keyed_event = 0; -static const LARGE_INTEGER zero_timeout; - -static pthread_mutex_t addr_mutex = PTHREAD_MUTEX_INITIALIZER; - static const char *debugstr_timeout( const LARGE_INTEGER *timeout ) { if (!timeout) return "(infinite)"; @@ -191,24 +187,6 @@ static void timespec_from_timeout( struct timespec *timespec, const LARGE_INTEGE #endif -static BOOL compare_addr( const void *addr, const void *cmp, SIZE_T size ) -{ - switch (size) - { - case 1: - return (*(const UCHAR *)addr == *(const UCHAR *)cmp); - case 2: - return (*(const USHORT *)addr == *(const USHORT *)cmp); - case 4: - return (*(const ULONG *)addr == *(const ULONG *)cmp); - case 8: - return (*(const ULONG64 *)addr == *(const ULONG64 *)cmp); - } - - return FALSE; -} - - /* create a struct security_descriptor and contained information in one contiguous piece of memory */ NTSTATUS alloc_object_attributes( const OBJECT_ATTRIBUTES *attr, struct object_attributes **ret, data_size_t *ret_len ) @@ -2895,71 +2873,6 @@ NTSTATUS CDECL fast_RtlWakeConditionVariable( RTL_CONDITION_VARIABLE *variable, return STATUS_SUCCESS; } - -/* We can't map addresses to futex directly, because an application can wait on - * 8 bytes, and we can't pass all 8 as the compare value to futex(). Instead we - * map all addresses to a small fixed table of futexes. This may result in - * spurious wakes, but the application is already expected to handle those. */ - -static int addr_futex_table[256]; - -static inline int *hash_addr( const void *addr ) -{ - ULONG_PTR val = (ULONG_PTR)addr; - - return &addr_futex_table[(val >> 2) & 255]; -} - -static inline NTSTATUS fast_wait_addr( const void *addr, const void *cmp, SIZE_T size, - const LARGE_INTEGER *timeout ) -{ - int *futex; - int val; - struct timespec timespec; - int ret; - - if (!use_futexes()) - return STATUS_NOT_IMPLEMENTED; - - futex = hash_addr( addr ); - - /* We must read the previous value of the futex before checking the value - * of the address being waited on. That way, if we receive a wake between - * now and waiting on the futex, we know that val will have changed. - * Use an atomic load so that memory accesses are ordered between this read - * and the increment below. */ - val = InterlockedCompareExchange( futex, 0, 0 ); - if (!compare_addr( addr, cmp, size )) - return STATUS_SUCCESS; - - if (timeout) - { - timespec_from_timeout( ×pec, timeout ); - ret = futex_wait( futex, val, ×pec ); - } - else - ret = futex_wait( futex, val, NULL ); - - if (ret == -1 && errno == ETIMEDOUT) - return STATUS_TIMEOUT; - return STATUS_SUCCESS; -} - -static inline NTSTATUS fast_wake_addr( const void *addr ) -{ - int *futex; - - if (!use_futexes()) - return STATUS_NOT_IMPLEMENTED; - - futex = hash_addr( addr ); - - InterlockedIncrement( futex ); - - futex_wake( futex, INT_MAX ); - return STATUS_SUCCESS; -} - #else NTSTATUS CDECL fast_RtlTryAcquireSRWLockExclusive( RTL_SRWLOCK *lock ) @@ -3002,79 +2915,4 @@ NTSTATUS CDECL fast_wait_cv( RTL_CONDITION_VARIABLE *variable, const void *value return STATUS_NOT_IMPLEMENTED; } -static inline NTSTATUS fast_wait_addr( const void *addr, const void *cmp, SIZE_T size, - const LARGE_INTEGER *timeout ) -{ - return STATUS_NOT_IMPLEMENTED; -} - -static inline NTSTATUS fast_wake_addr( const void *addr ) -{ - return STATUS_NOT_IMPLEMENTED; -} - #endif - - -/*********************************************************************** - * RtlWaitOnAddress (NTDLL.@) - */ -NTSTATUS WINAPI RtlWaitOnAddress( const void *addr, const void *cmp, SIZE_T size, - const LARGE_INTEGER *timeout ) -{ - select_op_t select_op; - NTSTATUS ret; - timeout_t abs_timeout = timeout ? timeout->QuadPart : TIMEOUT_INFINITE; - - if (size != 1 && size != 2 && size != 4 && size != 8) - return STATUS_INVALID_PARAMETER; - - if ((ret = fast_wait_addr( addr, cmp, size, timeout )) != STATUS_NOT_IMPLEMENTED) - return ret; - - mutex_lock( &addr_mutex ); - if (!compare_addr( addr, cmp, size )) - { - mutex_unlock( &addr_mutex ); - return STATUS_SUCCESS; - } - - if (abs_timeout < 0) - { - LARGE_INTEGER now; - - NtQueryPerformanceCounter( &now, NULL ); - abs_timeout -= now.QuadPart; - } - - select_op.keyed_event.op = SELECT_KEYED_EVENT_WAIT; - select_op.keyed_event.handle = wine_server_obj_handle( keyed_event ); - select_op.keyed_event.key = wine_server_client_ptr( addr ); - - return server_select( &select_op, sizeof(select_op.keyed_event), SELECT_INTERRUPTIBLE, - abs_timeout, NULL, &addr_mutex, NULL ); -} - -/*********************************************************************** - * RtlWakeAddressAll (NTDLL.@) - */ -void WINAPI RtlWakeAddressAll( const void *addr ) -{ - if (fast_wake_addr( addr ) != STATUS_NOT_IMPLEMENTED) return; - - mutex_lock( &addr_mutex ); - while (NtReleaseKeyedEvent( 0, addr, 0, &zero_timeout ) == STATUS_SUCCESS) {} - mutex_unlock( &addr_mutex ); -} - -/*********************************************************************** - * RtlWakeAddressSingle (NTDLL.@) - */ -void WINAPI RtlWakeAddressSingle( const void *addr ) -{ - if (fast_wake_addr( addr ) != STATUS_NOT_IMPLEMENTED) return; - - mutex_lock( &addr_mutex ); - NtReleaseKeyedEvent( 0, addr, 0, &zero_timeout ); - mutex_unlock( &addr_mutex ); -} diff --git a/dlls/ntdll/unixlib.h b/dlls/ntdll/unixlib.h index 8dc42320f18..20f9a19ff8d 100644 --- a/dlls/ntdll/unixlib.h +++ b/dlls/ntdll/unixlib.h @@ -39,10 +39,6 @@ struct unix_funcs /* other Win32 API functions */ NTSTATUS (WINAPI *DbgUiIssueRemoteBreakin)( HANDLE process ); LONGLONG (WINAPI *RtlGetSystemTimePrecise)(void); - NTSTATUS (WINAPI *RtlWaitOnAddress)( const void *addr, const void *cmp, SIZE_T size, - const LARGE_INTEGER *timeout ); - void (WINAPI *RtlWakeAddressAll)( const void *addr ); - void (WINAPI *RtlWakeAddressSingle)( const void *addr ); /* fast locks */ NTSTATUS (CDECL *fast_RtlpWaitForCriticalSection)( RTL_CRITICAL_SECTION *crit, int timeout ); -- 2.30.0