Skip to content

Commit 3eb245d

Browse files
authoredMar 12, 2025··
Add the CurrentUserOnly and CurrentSessionOnly options for named Mutex, Semaphore, and EventWaitHandle (#112213)
- In `Mutex`, `Semaphore`, and `EventWaitHandle`, added overloads for methods with a `name` argument to also include a `NamedWaitHandleOptions options` argument. The `CurrentUserOnly` option indicates whether the object should be limited in access to the current user. The `CurrentSessionOnly` option indicates whether the object object is intended to be used only within the current session (an alternative to the name prefixes, such as `Global\`). The default value for `NamedWaitHandleOptions` has `CurrentUserOnly=true` and `CurrentSessionOnly=true`. - On Unixes, named semaphores and events are not supported. Named mutexes are have a functional implementation on Unixes in CoreCLR. NativeAOT and Mono on Unixes use an incomplete/temporary implementation where named mutexes are just process-local mutexes stored in a dictionary keyed by the name and can't be used to synchronize between processes. This change only briefly extends the NativeAOT/Mono implementation to add another prefix to the name when `CurrentUserOnly=true`. Sharing the CoreCLR Unix named mutex implementation with NativeAOT/Mono is left to a separate change. Most of what is described below when `CurrentUserOnly=true` on Unixes refers to the CoreCLR implementation. - The plan is to also deprecate the APIs with a `name` argument but without an `options` argument in a separate change - API review: #102682 (comment) ### Windows with `CurrentUserOnly=true` When creating a named mutex, a security descriptor is created and assigned to the object. - The owner and group are set to the current user - The DACL has two ACEs: - One ACE allows the current user the relevant access rights - Another ACE allows the `BUILTIN\Administrators` group the `READ_CONTROL` access right to enable reading the security info for diagnostic purposes - The SACL has a mandatory label ACE - The access policy prevents principals with an integrity level lower than the object from opening the object - The integrity level assigned to the object is the same as what would be assigned by the system by default - When opening a named mutex, the owner and DACL are verified to see if they are as expected, and if not, a `WaitHandleCannotBeOpenedException` is thrown - Access controls are set when creating an object and checked when opening an object. Once a handle to the object is obtained, typical synchronization operations may not do further access checks. It's up to the caller to be careful about how the handle to the object is passed around. ### Unixes with `CurrentUserOnly=true` - Files relevant to named mutexes (shared memory and lock files) go under `/tmp/.dotnet-uidN/` where `N` is the effective user ID - `/tmp/` (or equivalent) was chosen mainly for the automatic cleanup, as the number of files can add up in cases where mutexes are not disposed, particularly when randomly generated names are used upon each invocation of a process. Due to the use of a global location `/tmp/` or equivalent, it would be possible for a user to deny access to the `.dotnet-uidN` directory of a different user if the directory hadn't been created yet. An alternative considered was to use the home directory and introduce some kind of periodic cleanup strategy, but there are also other tradeoffs. - `/tmp/` or equivalent must either have the sticky bit, or it must be owned by the current user and without write access for any other user. Otherwise, an exception is thrown. - Permissions of the `/tmp/.dotnet-uidN/` directory and files/directories under it are limited in access to the current user - When opening a named mutex, the owner and permissions of relevant files/directories are verified to see if they are as expected, and if not, an exception is thrown - Access controls are set when creating an object and checked when opening an object. Once a handle to the object is obtained, typical synchronization operations may not do further access checks. It's up to the caller to be careful about how the handle to the object is passed around. ### Namespaces - On Windows, there is no namespace for kernel objects for each user. `CurrentSessionOnly=true` is close, but it is possible for multiple users to be running code simultaneously in the same session. There is a global namespace, and a namespace per session. When `CurrentUserOnly=true`, callers may need to ensure that the name used is distinguished for different users. - On Unixes, a different directory tree is used when `CurrentUserOnly=true`, so each user has a separate namespace for objects, including for session-scoped and session-unscoped objects.
1 parent 82c916f commit 3eb245d

39 files changed

+3154
-638
lines changed
 

‎src/coreclr/System.Private.CoreLib/src/System/Threading/Mutex.CoreCLR.Unix.cs

+52-9
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,40 @@ namespace System.Threading
1414
/// </summary>
1515
public sealed partial class Mutex : WaitHandle
1616
{
17-
private void CreateMutexCore(bool initiallyOwned, string? name, out bool createdNew)
17+
private unsafe void CreateMutexCore(bool initiallyOwned)
1818
{
19-
SafeWaitHandle mutexHandle = CreateMutexCore(initiallyOwned, name, out int errorCode, out string? errorDetails);
19+
SafeWaitHandle handle =
20+
CreateMutex(
21+
initiallyOwned,
22+
name: null,
23+
currentUserOnly: false,
24+
systemCallErrors: null,
25+
systemCallErrorsBufferSize: 0);
26+
if (handle.IsInvalid)
27+
{
28+
int errorCode = Marshal.GetLastPInvokeError();
29+
handle.SetHandleAsInvalid();
30+
throw Win32Marshal.GetExceptionForWin32Error(errorCode);
31+
}
32+
33+
SafeWaitHandle = handle;
34+
}
35+
36+
private void CreateMutexCore(
37+
bool initiallyOwned,
38+
string? name,
39+
NamedWaitHandleOptionsInternal options,
40+
out bool createdNew)
41+
{
42+
bool currentUserOnly = false;
43+
if (!string.IsNullOrEmpty(name) && options.WasSpecified)
44+
{
45+
name = options.GetNameWithSessionPrefix(name);
46+
currentUserOnly = options.CurrentUserOnly;
47+
}
48+
49+
SafeWaitHandle mutexHandle =
50+
CreateMutexCore(initiallyOwned, name, currentUserOnly, out int errorCode, out string? errorDetails);
2051
if (mutexHandle.IsInvalid)
2152
{
2253
mutexHandle.SetHandleAsInvalid();
@@ -33,16 +64,26 @@ private void CreateMutexCore(bool initiallyOwned, string? name, out bool created
3364
SafeWaitHandle = mutexHandle;
3465
}
3566

36-
private static OpenExistingResult OpenExistingWorker(string name, out Mutex? result)
67+
private static OpenExistingResult OpenExistingWorker(
68+
string name,
69+
NamedWaitHandleOptionsInternal options,
70+
out Mutex? result)
3771
{
3872
ArgumentException.ThrowIfNullOrEmpty(name);
3973

74+
bool currentUserOnly = false;
75+
if (options.WasSpecified)
76+
{
77+
name = options.GetNameWithSessionPrefix(name);
78+
currentUserOnly = options.CurrentUserOnly;
79+
}
80+
4081
result = null;
4182
// To allow users to view & edit the ACL's, call OpenMutex
4283
// with parameters to allow us to view & edit the ACL. This will
4384
// fail if we don't have permission to view or edit the ACL's.
4485
// If that happens, ask for less permissions.
45-
SafeWaitHandle myHandle = OpenMutexCore(name, out int errorCode, out string? errorDetails);
86+
SafeWaitHandle myHandle = OpenMutexCore(name, currentUserOnly, out int errorCode, out string? errorDetails);
4687

4788
if (myHandle.IsInvalid)
4889
{
@@ -86,11 +127,13 @@ public void ReleaseMutex()
86127
private static unsafe SafeWaitHandle CreateMutexCore(
87128
bool initialOwner,
88129
string? name,
130+
bool currentUserOnly,
89131
out int errorCode,
90132
out string? errorDetails)
91133
{
92134
byte* systemCallErrors = stackalloc byte[SystemCallErrorsBufferSize];
93-
SafeWaitHandle mutexHandle = CreateMutex(initialOwner, name, systemCallErrors, SystemCallErrorsBufferSize);
135+
SafeWaitHandle mutexHandle =
136+
CreateMutex(initialOwner, name, currentUserOnly, systemCallErrors, SystemCallErrorsBufferSize);
94137

95138
// Get the error code even if the handle is valid, as it could be ERROR_ALREADY_EXISTS, indicating that the mutex
96139
// already exists and was opened
@@ -100,10 +143,10 @@ private static unsafe SafeWaitHandle CreateMutexCore(
100143
return mutexHandle;
101144
}
102145

103-
private static unsafe SafeWaitHandle OpenMutexCore(string name, out int errorCode, out string? errorDetails)
146+
private static unsafe SafeWaitHandle OpenMutexCore(string name, bool currentUserOnly, out int errorCode, out string? errorDetails)
104147
{
105148
byte* systemCallErrors = stackalloc byte[SystemCallErrorsBufferSize];
106-
SafeWaitHandle mutexHandle = OpenMutex(name, systemCallErrors, SystemCallErrorsBufferSize);
149+
SafeWaitHandle mutexHandle = OpenMutex(name, currentUserOnly, systemCallErrors, SystemCallErrorsBufferSize);
107150
errorCode = mutexHandle.IsInvalid ? Marshal.GetLastPInvokeError() : Interop.Errors.ERROR_SUCCESS;
108151
errorDetails = mutexHandle.IsInvalid ? GetErrorDetails(systemCallErrors) : null;
109152
return mutexHandle;
@@ -127,9 +170,9 @@ private static unsafe SafeWaitHandle OpenMutexCore(string name, out int errorCod
127170
}
128171

129172
[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "PAL_CreateMutexW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
130-
private static unsafe partial SafeWaitHandle CreateMutex([MarshalAs(UnmanagedType.Bool)] bool initialOwner, string? name, byte* systemCallErrors, uint systemCallErrorsBufferSize);
173+
private static unsafe partial SafeWaitHandle CreateMutex([MarshalAs(UnmanagedType.Bool)] bool initialOwner, string? name, [MarshalAs(UnmanagedType.Bool)] bool currentUserOnly, byte* systemCallErrors, uint systemCallErrorsBufferSize);
131174

132175
[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "PAL_OpenMutexW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
133-
private static unsafe partial SafeWaitHandle OpenMutex(string name, byte* systemCallErrors, uint systemCallErrorsBufferSize);
176+
private static unsafe partial SafeWaitHandle OpenMutex(string name, [MarshalAs(UnmanagedType.Bool)] bool currentUserOnly, byte* systemCallErrors, uint systemCallErrorsBufferSize);
134177
}
135178
}

‎src/coreclr/pal/inc/pal.h

+2
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,7 @@ PALAPI
854854
PAL_CreateMutexW(
855855
IN BOOL bInitialOwner,
856856
IN LPCWSTR lpName,
857+
IN BOOL bCurrentUserOnly,
857858
IN LPSTR lpSystemCallErrors,
858859
IN DWORD dwSystemCallErrorsBufferSize);
859860

@@ -875,6 +876,7 @@ HANDLE
875876
PALAPI
876877
PAL_OpenMutexW(
877878
IN LPCWSTR lpName,
879+
IN BOOL bCurrentUserOnly,
878880
IN LPSTR lpSystemCallErrors,
879881
IN DWORD dwSystemCallErrorsBufferSize);
880882

‎src/coreclr/pal/inc/pal_error.h

-1
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,6 @@
147147
#define ERROR_PALINIT_TLS 65295L
148148
#define ERROR_PALINIT_ENV 65296L
149149
#define ERROR_PALINIT_DBG_CHANNELS 65297L
150-
#define ERROR_PALINIT_SHARED_MEMORY_MANAGER 65298L
151150
#define ERROR_PALINIT_SHM 65299L
152151
#define ERROR_PALINIT_MODULE_MANAGER 65300L
153152

‎src/coreclr/pal/src/include/pal/mutex.hpp

+5-3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ namespace CorUnix
3737
LPSECURITY_ATTRIBUTES lpMutexAttributes,
3838
BOOL bInitialOwner,
3939
LPCSTR lpName,
40+
BOOL bCurrentUserOnly,
4041
HANDLE *phMutex
4142
);
4243

@@ -51,6 +52,7 @@ namespace CorUnix
5152
SharedMemorySystemCallErrors *errors,
5253
CPalThread *pThread,
5354
LPCSTR lpName,
55+
BOOL bCurrentUserOnly,
5456
HANDLE *phMutex
5557
);
5658

@@ -216,10 +218,10 @@ class NamedMutexProcessData : public SharedMemoryProcessDataBase
216218
bool m_hasRefFromLockOwnerThread;
217219

218220
public:
219-
static SharedMemoryProcessDataHeader *CreateOrOpen(SharedMemorySystemCallErrors *errors, LPCSTR name, bool acquireLockIfCreated, bool *createdRef);
220-
static SharedMemoryProcessDataHeader *Open(SharedMemorySystemCallErrors *errors, LPCSTR name);
221+
static SharedMemoryProcessDataHeader *CreateOrOpen(SharedMemorySystemCallErrors *errors, LPCSTR name, bool isUserScope, bool acquireLockIfCreated, bool *createdRef);
222+
static SharedMemoryProcessDataHeader *Open(SharedMemorySystemCallErrors *errors, LPCSTR name, bool isUserScope);
221223
private:
222-
static SharedMemoryProcessDataHeader *CreateOrOpen(SharedMemorySystemCallErrors *errors, LPCSTR name, bool createIfNotExist, bool acquireLockIfCreated, bool *createdRef);
224+
static SharedMemoryProcessDataHeader *CreateOrOpen(SharedMemorySystemCallErrors *errors, LPCSTR name, bool isUserScope, bool createIfNotExist, bool acquireLockIfCreated, bool *createdRef);
223225

224226
public:
225227
NamedMutexProcessData(

‎src/coreclr/pal/src/include/pal/sharedmemory.h

+53-32
Original file line numberDiff line numberDiff line change
@@ -27,27 +27,26 @@
2727
#define SHARED_MEMORY_MAX_FILE_NAME_CHAR_COUNT (_MAX_FNAME - 1)
2828
#define SHARED_MEMORY_MAX_NAME_CHAR_COUNT (STRING_LENGTH("Global\\") + SHARED_MEMORY_MAX_FILE_NAME_CHAR_COUNT)
2929

30-
#define SHARED_MEMORY_RUNTIME_TEMP_DIRECTORY_NAME ".dotnet"
31-
#define SHARED_MEMORY_SHARED_MEMORY_DIRECTORY_NAME ".dotnet/shm"
32-
#define SHARED_MEMORY_LOCK_FILES_DIRECTORY_NAME ".dotnet/lockfiles"
33-
static_assert_no_msg(ARRAY_SIZE(SHARED_MEMORY_LOCK_FILES_DIRECTORY_NAME) >= ARRAY_SIZE(SHARED_MEMORY_SHARED_MEMORY_DIRECTORY_NAME));
30+
#define SHARED_MEMORY_USER_UNSCOPED_RUNTIME_TEMP_DIRECTORY_NAME ".dotnet"
31+
#define SHARED_MEMORY_USER_SCOPED_RUNTIME_TEMP_DIRECTORY_NAME_PREFIX ".dotnet-uid"
32+
#define SHARED_MEMORY_SHARED_MEMORY_DIRECTORY_NAME "shm"
33+
#define SHARED_MEMORY_LOCK_FILES_DIRECTORY_NAME "lockfiles"
34+
static_assert_no_msg(STRING_LENGTH(SHARED_MEMORY_LOCK_FILES_DIRECTORY_NAME) >= STRING_LENGTH(SHARED_MEMORY_SHARED_MEMORY_DIRECTORY_NAME));
3435

3536
#define SHARED_MEMORY_GLOBAL_DIRECTORY_NAME "global"
3637
#define SHARED_MEMORY_SESSION_DIRECTORY_NAME_PREFIX "session"
37-
static_assert_no_msg(ARRAY_SIZE(SHARED_MEMORY_SESSION_DIRECTORY_NAME_PREFIX) >= ARRAY_SIZE(SHARED_MEMORY_GLOBAL_DIRECTORY_NAME));
3838

39-
#define SHARED_MEMORY_UNIQUE_TEMP_NAME_TEMPLATE ".coreclr.XXXXXX"
40-
41-
#define SHARED_MEMORY_MAX_SESSION_ID_CHAR_COUNT (10)
39+
#define SHARED_MEMORY_UNIQUE_TEMP_NAME_TEMPLATE ".dotnet.XXXXXX"
4240

4341
// Note that this Max size does not include the prefix folder path size which is unknown (in the case of sandbox) until runtime
4442
#define SHARED_MEMORY_MAX_FILE_PATH_CHAR_COUNT \
4543
( \
44+
STRING_LENGTH(SHARED_MEMORY_USER_SCOPED_RUNTIME_TEMP_DIRECTORY_NAME_PREFIX) + \
45+
11 /* user ID, path separator */ + \
4646
STRING_LENGTH(SHARED_MEMORY_LOCK_FILES_DIRECTORY_NAME) + \
4747
1 /* path separator */ + \
4848
STRING_LENGTH(SHARED_MEMORY_SESSION_DIRECTORY_NAME_PREFIX) + \
49-
SHARED_MEMORY_MAX_SESSION_ID_CHAR_COUNT + \
50-
1 /* path separator */ + \
49+
11 /* session ID, path separator */ + \
5150
SHARED_MEMORY_MAX_FILE_NAME_CHAR_COUNT \
5251
)
5352

@@ -98,12 +97,17 @@ class SharedMemorySystemCallErrors
9897
void Append(LPCSTR format, ...);
9998
};
10099

100+
class SharedMemoryId;
101+
101102
class SharedMemoryHelpers
102103
{
103104
private:
104-
static const mode_t PermissionsMask_CurrentUser_ReadWriteExecute;
105+
static const mode_t PermissionsMask_OwnerUser_ReadWrite;
106+
static const mode_t PermissionsMask_OwnerUser_ReadWriteExecute;
107+
static const mode_t PermissionsMask_NonOwnerUsers_Write;
105108
static const mode_t PermissionsMask_AllUsers_ReadWrite;
106109
static const mode_t PermissionsMask_AllUsers_ReadWriteExecute;
110+
static const mode_t PermissionsMask_Sticky;
107111
public:
108112
static const UINT32 InvalidProcessId;
109113
static const SIZE_T InvalidThreadId;
@@ -114,17 +118,14 @@ class SharedMemoryHelpers
114118
static SIZE_T AlignUp(SIZE_T value, SIZE_T alignment);
115119

116120
static void *Alloc(SIZE_T byteCount);
117-
118-
template<SIZE_T SuffixByteCount> static void BuildSharedFilesPath(PathCharString& destination, const char (&suffix)[SuffixByteCount]);
119-
static void BuildSharedFilesPath(PathCharString& destination, const char *suffix, int suffixByteCount);
120121
static bool AppendUInt32String(PathCharString& destination, UINT32 value);
121122

122-
static bool EnsureDirectoryExists(SharedMemorySystemCallErrors *errors, const char *path, bool isGlobalLockAcquired, bool createIfNotExist = true, bool isSystemDirectory = false);
123+
static bool EnsureDirectoryExists(SharedMemorySystemCallErrors *errors, const char *path, const SharedMemoryId *id, bool isGlobalLockAcquired, bool createIfNotExist = true, bool isSystemDirectory = false);
123124
private:
124125
static int Open(SharedMemorySystemCallErrors *errors, LPCSTR path, int flags, mode_t mode = static_cast<mode_t>(0));
125126
public:
126127
static int OpenDirectory(SharedMemorySystemCallErrors *errors, LPCSTR path);
127-
static int CreateOrOpenFile(SharedMemorySystemCallErrors *errors, LPCSTR path, bool createIfNotExist = true, bool *createdRef = nullptr);
128+
static int CreateOrOpenFile(SharedMemorySystemCallErrors *errors, LPCSTR path, const SharedMemoryId *id, bool createIfNotExist = true, bool *createdRef = nullptr);
128129
static void CloseFile(int fileDescriptor);
129130

130131
static int ChangeMode(LPCSTR path, mode_t mode);
@@ -150,19 +151,24 @@ class SharedMemoryId
150151
LPCSTR m_name;
151152
SIZE_T m_nameCharCount;
152153
bool m_isSessionScope; // false indicates global scope
154+
bool m_isUserScope;
155+
uid_t m_userScopeUid;
153156

154157
public:
155158
SharedMemoryId();
156-
SharedMemoryId(LPCSTR name, SIZE_T nameCharCount, bool isSessionScope);
157-
SharedMemoryId(LPCSTR name);
159+
SharedMemoryId(LPCSTR name, bool isUserScope);
158160

159161
public:
160162
LPCSTR GetName() const;
161163
SIZE_T GetNameCharCount() const;
164+
void ReplaceNamePtr(LPCSTR name);
162165
bool IsSessionScope() const;
163-
bool Equals(SharedMemoryId *other) const;
166+
bool IsUserScope() const;
167+
uid_t GetUserScopeUid() const;
168+
bool Equals(const SharedMemoryId *other) const;
164169

165170
public:
171+
bool AppendRuntimeTempDirectoryName(PathCharString& path) const;
166172
bool AppendSessionDirectoryName(PathCharString& path) const;
167173
};
168174

@@ -222,22 +228,22 @@ class SharedMemoryProcessDataHeader
222228
SharedMemoryProcessDataHeader *m_nextInProcessDataHeaderList;
223229

224230
public:
225-
static SharedMemoryProcessDataHeader *CreateOrOpen(SharedMemorySystemCallErrors *errors, LPCSTR name, SharedMemorySharedDataHeader requiredSharedDataHeader, SIZE_T sharedDataByteCount, bool createIfNotExist, bool *createdRef);
231+
static SharedMemoryProcessDataHeader *CreateOrOpen(SharedMemorySystemCallErrors *errors, LPCSTR name, bool isUserScope, SharedMemorySharedDataHeader requiredSharedDataHeader, SIZE_T sharedDataByteCount, bool createIfNotExist, bool *createdRef);
226232

227233
public:
228234
static SharedMemoryProcessDataHeader *PalObject_GetProcessDataHeader(CorUnix::IPalObject *object);
229235
static void PalObject_SetProcessDataHeader(CorUnix::IPalObject *object, SharedMemoryProcessDataHeader *processDataHeader);
230236
static void PalObject_Close(CorUnix::CPalThread *thread, CorUnix::IPalObject *object, bool isShuttingDown);
231237

232238
private:
233-
SharedMemoryProcessDataHeader(SharedMemoryId *id, int fileDescriptor, SharedMemorySharedDataHeader *sharedDataHeader, SIZE_T sharedDataTotalByteCount);
239+
SharedMemoryProcessDataHeader(const SharedMemoryId *id, int fileDescriptor, SharedMemorySharedDataHeader *sharedDataHeader, SIZE_T sharedDataTotalByteCount);
234240
public:
235-
static SharedMemoryProcessDataHeader *New(SharedMemoryId *id, int fileDescriptor, SharedMemorySharedDataHeader *sharedDataHeader, SIZE_T sharedDataTotalByteCount);
241+
static SharedMemoryProcessDataHeader *New(const SharedMemoryId *id, int fileDescriptor, SharedMemorySharedDataHeader *sharedDataHeader, SIZE_T sharedDataTotalByteCount);
236242
~SharedMemoryProcessDataHeader();
237243
void Close();
238244

239245
public:
240-
SharedMemoryId *GetId();
246+
const SharedMemoryId *GetId() const;
241247
SharedMemoryProcessDataBase *GetData() const;
242248
void SetData(SharedMemoryProcessDataBase *data);
243249
SharedMemorySharedDataHeader *GetSharedDataHeader() const;
@@ -256,8 +262,24 @@ class SharedMemoryManager
256262
static CRITICAL_SECTION s_creationDeletionProcessLock;
257263
static int s_creationDeletionLockFileDescriptor;
258264

259-
static PathCharString* s_runtimeTempDirectoryPath;
260-
static PathCharString* s_sharedMemoryDirectoryPath;
265+
struct UserScopeUidAndFileDescriptor
266+
{
267+
uid_t userScopeUid;
268+
int fileDescriptor;
269+
270+
UserScopeUidAndFileDescriptor() : userScopeUid((uid_t)0), fileDescriptor(-1)
271+
{
272+
}
273+
274+
UserScopeUidAndFileDescriptor(uid_t userScopeUid, int fileDescriptor)
275+
: userScopeUid(userScopeUid), fileDescriptor(fileDescriptor)
276+
{
277+
}
278+
};
279+
280+
static UserScopeUidAndFileDescriptor *s_userScopeUidToCreationDeletionLockFDs;
281+
static int s_userScopeUidToCreationDeletionLockFDsCount;
282+
static int s_userScopeUidToCreationDeletionLockFDsCapacity;
261283

262284
private:
263285
static SharedMemoryProcessDataHeader *s_processDataHeaderListHead;
@@ -269,17 +291,16 @@ class SharedMemoryManager
269291
#endif // _DEBUG
270292

271293
public:
272-
static bool StaticInitialize();
294+
static void StaticInitialize();
273295
static void StaticClose();
274296

275297
public:
276298
static void AcquireCreationDeletionProcessLock();
277299
static void ReleaseCreationDeletionProcessLock();
278-
static void AcquireCreationDeletionFileLock(SharedMemorySystemCallErrors *errors);
279-
static void ReleaseCreationDeletionFileLock();
280-
281-
public:
282-
static bool CopySharedMemoryBasePath(PathCharString& destination);
300+
static void AcquireCreationDeletionFileLock(SharedMemorySystemCallErrors *errors, const SharedMemoryId *id);
301+
static void ReleaseCreationDeletionFileLock(const SharedMemoryId *id);
302+
static void AddUserScopeUidCreationDeletionLockFD(uid_t userScopeUid, int creationDeletionLockFD);
303+
static int FindUserScopeCreationDeletionLockFD(uid_t userScopeUid);
283304

284305
#ifdef _DEBUG
285306
public:
@@ -290,7 +311,7 @@ class SharedMemoryManager
290311
public:
291312
static void AddProcessDataHeader(SharedMemoryProcessDataHeader *processDataHeader);
292313
static void RemoveProcessDataHeader(SharedMemoryProcessDataHeader *processDataHeader);
293-
static SharedMemoryProcessDataHeader *FindProcessDataHeader(SharedMemoryId *id);
314+
static SharedMemoryProcessDataHeader *FindProcessDataHeader(const SharedMemoryId *id);
294315
};
295316

296317
#endif // !_PAL_SHARED_MEMORY_H_

‎src/coreclr/pal/src/include/pal/sharedmemory.inl

-21
This file was deleted.

‎src/coreclr/pal/src/init/pal.cpp

+1-6
Original file line numberDiff line numberDiff line change
@@ -410,12 +410,7 @@ Initialize(
410410
// we use large numbers of threads or have many open files.
411411
}
412412

413-
if (!SharedMemoryManager::StaticInitialize())
414-
{
415-
ERROR("Shared memory static initialization failed!\n");
416-
palError = ERROR_PALINIT_SHARED_MEMORY_MANAGER;
417-
goto CLEANUP1;
418-
}
413+
SharedMemoryManager::StaticInitialize();
419414

420415
//
421416
// Initialize global process data

‎src/coreclr/pal/src/sharedmemory/sharedmemory.cpp

+323-131
Large diffs are not rendered by default.

‎src/coreclr/pal/src/synchobj/mutex.cpp

+53-32
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@ SET_DEFAULT_DEBUG_CHANNEL(SYNC); // some headers have code with asserts, so do t
3434
#include <time.h>
3535
#include <unistd.h>
3636

37-
#include "pal/sharedmemory.inl"
38-
3937
using namespace CorUnix;
4038

4139
/* ------------------- Definitions ------------------------------*/
@@ -89,7 +87,7 @@ CreateMutexW(
8987
IN BOOL bInitialOwner,
9088
IN LPCWSTR lpName)
9189
{
92-
return PAL_CreateMutexW(bInitialOwner, lpName, nullptr, 0);
90+
return PAL_CreateMutexW(bInitialOwner, lpName, false /* bCurrentUserOnly */, nullptr, 0);
9391
}
9492

9593
/*++
@@ -113,6 +111,7 @@ PALAPI
113111
PAL_CreateMutexW(
114112
IN BOOL bInitialOwner,
115113
IN LPCWSTR lpName,
114+
IN BOOL bCurrentUserOnly,
116115
IN LPSTR lpSystemCallErrors,
117116
IN DWORD dwSystemCallErrorsBufferSize)
118117
{
@@ -171,6 +170,7 @@ PAL_CreateMutexW(
171170
nullptr,
172171
bInitialOwner,
173172
lpName == nullptr ? nullptr : utf8Name,
173+
bCurrentUserOnly,
174174
&hMutex
175175
);
176176
}
@@ -238,6 +238,7 @@ CorUnix::InternalCreateMutex(
238238
LPSECURITY_ATTRIBUTES lpMutexAttributes,
239239
BOOL bInitialOwner,
240240
LPCSTR lpName,
241+
BOOL bCurrentUserOnly,
241242
HANDLE *phMutex
242243
)
243244
{
@@ -317,7 +318,8 @@ CorUnix::InternalCreateMutex(
317318
SharedMemoryProcessDataHeader *processDataHeader;
318319
try
319320
{
320-
processDataHeader = NamedMutexProcessData::CreateOrOpen(errors, lpName, !!bInitialOwner, &createdNamedMutex);
321+
processDataHeader =
322+
NamedMutexProcessData::CreateOrOpen(errors, lpName, !!bCurrentUserOnly, !!bInitialOwner, &createdNamedMutex);
321323
}
322324
catch (SharedMemoryException ex)
323325
{
@@ -543,7 +545,7 @@ OpenMutexA (
543545
goto OpenMutexAExit;
544546
}
545547

546-
palError = InternalOpenMutex(nullptr, pthr, lpName, &hMutex);
548+
palError = InternalOpenMutex(nullptr, pthr, lpName, false /* bCurrentUserOnly */, &hMutex);
547549

548550
OpenMutexAExit:
549551
if (NO_ERROR != palError)
@@ -571,7 +573,7 @@ OpenMutexW(
571573
IN BOOL bInheritHandle,
572574
IN LPCWSTR lpName)
573575
{
574-
return PAL_OpenMutexW(lpName, nullptr, 0);
576+
return PAL_OpenMutexW(lpName, false /* bCurrentUserOnly */, nullptr, 0);
575577
}
576578

577579
/*++
@@ -593,6 +595,7 @@ HANDLE
593595
PALAPI
594596
PAL_OpenMutexW(
595597
IN LPCWSTR lpName,
598+
IN BOOL bCurrentUserOnly,
596599
IN LPSTR lpSystemCallErrors,
597600
IN DWORD dwSystemCallErrorsBufferSize)
598601
{
@@ -612,10 +615,11 @@ PAL_OpenMutexW(
612615

613616
/* validate parameters */
614617
if (lpName == nullptr ||
618+
lpName[0] == W('\0') ||
615619
(int)dwSystemCallErrorsBufferSize < 0 ||
616620
(lpSystemCallErrors == nullptr) != (dwSystemCallErrorsBufferSize == 0))
617621
{
618-
ERROR("name is NULL or other parameters are invalid\n");
622+
ERROR("One or more parameters are invalid\n");
619623
palError = ERROR_INVALID_PARAMETER;
620624
goto OpenMutexWExit;
621625
}
@@ -643,7 +647,7 @@ PAL_OpenMutexW(
643647
}
644648

645649
SharedMemorySystemCallErrors errors(lpSystemCallErrors, (int)dwSystemCallErrorsBufferSize);
646-
palError = InternalOpenMutex(&errors, pthr, lpName == nullptr ? nullptr : utf8Name, &hMutex);
650+
palError = InternalOpenMutex(&errors, pthr, lpName == nullptr ? nullptr : utf8Name, bCurrentUserOnly, &hMutex);
647651
}
648652

649653
OpenMutexWExit:
@@ -675,6 +679,7 @@ CorUnix::InternalOpenMutex(
675679
SharedMemorySystemCallErrors *errors,
676680
CPalThread *pthr,
677681
LPCSTR lpName,
682+
BOOL bCurrentUserOnly,
678683
HANDLE *phMutex
679684
)
680685
{
@@ -711,7 +716,7 @@ CorUnix::InternalOpenMutex(
711716
SharedMemoryProcessDataHeader *processDataHeader;
712717
try
713718
{
714-
processDataHeader = NamedMutexProcessData::Open(errors, lpName);
719+
processDataHeader = NamedMutexProcessData::Open(errors, lpName, bCurrentUserOnly);
715720
}
716721
catch (SharedMemoryException ex)
717722
{
@@ -1074,20 +1079,29 @@ const DWORD NamedMutexProcessData::PollLoopMaximumSleepMilliseconds = 100;
10741079
SharedMemoryProcessDataHeader *NamedMutexProcessData::CreateOrOpen(
10751080
SharedMemorySystemCallErrors *errors,
10761081
LPCSTR name,
1082+
bool isUserScope,
10771083
bool acquireLockIfCreated,
10781084
bool *createdRef)
10791085
{
1080-
return CreateOrOpen(errors, name, true /* createIfNotExist */, acquireLockIfCreated, createdRef);
1086+
return CreateOrOpen(errors, name, isUserScope, true /* createIfNotExist */, acquireLockIfCreated, createdRef);
10811087
}
10821088

1083-
SharedMemoryProcessDataHeader *NamedMutexProcessData::Open(SharedMemorySystemCallErrors *errors, LPCSTR name)
1089+
SharedMemoryProcessDataHeader *NamedMutexProcessData::Open(SharedMemorySystemCallErrors *errors, LPCSTR name, bool isUserScope)
10841090
{
1085-
return CreateOrOpen(errors, name, false /* createIfNotExist */, false /* acquireLockIfCreated */, nullptr /* createdRef */);
1091+
return
1092+
CreateOrOpen(
1093+
errors,
1094+
name,
1095+
isUserScope,
1096+
false /* createIfNotExist */,
1097+
false /* acquireLockIfCreated */,
1098+
nullptr /* createdRef */);
10861099
}
10871100

10881101
SharedMemoryProcessDataHeader *NamedMutexProcessData::CreateOrOpen(
10891102
SharedMemorySystemCallErrors *errors,
10901103
LPCSTR name,
1104+
bool isUserScope,
10911105
bool createIfNotExist,
10921106
bool acquireLockIfCreated,
10931107
bool *createdRef)
@@ -1153,7 +1167,8 @@ SharedMemoryProcessDataHeader *NamedMutexProcessData::CreateOrOpen(
11531167

11541168
if (m_acquiredCreationDeletionFileLock)
11551169
{
1156-
SharedMemoryManager::ReleaseCreationDeletionFileLock();
1170+
_ASSERTE(m_processDataHeader != nullptr);
1171+
SharedMemoryManager::ReleaseCreationDeletionFileLock(m_processDataHeader->GetId());
11571172
}
11581173

11591174
if (!m_cancel && m_processDataHeader != nullptr)
@@ -1178,6 +1193,7 @@ SharedMemoryProcessDataHeader *NamedMutexProcessData::CreateOrOpen(
11781193
SharedMemoryProcessDataHeader::CreateOrOpen(
11791194
errors,
11801195
name,
1196+
isUserScope,
11811197
SharedMemorySharedDataHeader(SharedMemoryType::Mutex, SyncSystemVersion),
11821198
sizeof(NamedMutexSharedData),
11831199
createIfNotExist,
@@ -1186,18 +1202,19 @@ SharedMemoryProcessDataHeader *NamedMutexProcessData::CreateOrOpen(
11861202
{
11871203
*createdRef = created;
11881204
}
1205+
if (processDataHeader == nullptr)
1206+
{
1207+
_ASSERTE(!created);
1208+
_ASSERTE(!createIfNotExist);
1209+
return nullptr;
1210+
}
11891211
if (created)
11901212
{
11911213
// If the shared memory file was created, the creation/deletion file lock would have been acquired so that we can
11921214
// initialize the shared data
11931215
_ASSERTE(SharedMemoryManager::IsCreationDeletionFileLockAcquired());
11941216
autoCleanup.m_acquiredCreationDeletionFileLock = true;
11951217
}
1196-
if (processDataHeader == nullptr)
1197-
{
1198-
_ASSERTE(!createIfNotExist);
1199-
return nullptr;
1200-
}
12011218
autoCleanup.m_processDataHeader = processDataHeader;
12021219

12031220
if (created)
@@ -1210,27 +1227,29 @@ SharedMemoryProcessDataHeader *NamedMutexProcessData::CreateOrOpen(
12101227
{
12111228
#if !NAMED_MUTEX_USE_PTHREAD_MUTEX
12121229
// Create the lock files directory
1213-
SharedMemoryHelpers::BuildSharedFilesPath(lockFilePath, SHARED_MEMORY_LOCK_FILES_DIRECTORY_NAME);
1230+
const SharedMemoryId *id = processDataHeader->GetId();
1231+
SharedMemoryHelpers::VerifyStringOperation(
1232+
lockFilePath.Set(*gSharedFilesPath) &&
1233+
id->AppendRuntimeTempDirectoryName(lockFilePath) &&
1234+
lockFilePath.Append('/') && lockFilePath.Append(SHARED_MEMORY_LOCK_FILES_DIRECTORY_NAME));
12141235
if (created)
12151236
{
1216-
SharedMemoryHelpers::EnsureDirectoryExists(errors, lockFilePath, true /* isGlobalLockAcquired */);
1237+
SharedMemoryHelpers::EnsureDirectoryExists(errors, lockFilePath, id, true /* isGlobalLockAcquired */);
12171238
}
12181239

12191240
// Create the session directory
1220-
SharedMemoryId *id = processDataHeader->GetId();
1221-
SharedMemoryHelpers::VerifyStringOperation(lockFilePath.Append('/'));
1222-
SharedMemoryHelpers::VerifyStringOperation(id->AppendSessionDirectoryName(lockFilePath));
1241+
SharedMemoryHelpers::VerifyStringOperation(lockFilePath.Append('/') && id->AppendSessionDirectoryName(lockFilePath));
12231242
if (created)
12241243
{
1225-
SharedMemoryHelpers::EnsureDirectoryExists(errors, lockFilePath, true /* isGlobalLockAcquired */);
1244+
SharedMemoryHelpers::EnsureDirectoryExists(errors, lockFilePath, id, true /* isGlobalLockAcquired */);
12261245
autoCleanup.m_lockFilePath = &lockFilePath;
12271246
autoCleanup.m_sessionDirectoryPathCharCount = lockFilePath.GetCount();
12281247
}
12291248

12301249
// Create or open the lock file
1231-
SharedMemoryHelpers::VerifyStringOperation(lockFilePath.Append('/'));
1232-
SharedMemoryHelpers::VerifyStringOperation(lockFilePath.Append(id->GetName(), id->GetNameCharCount()));
1233-
int lockFileDescriptor = SharedMemoryHelpers::CreateOrOpenFile(errors, lockFilePath, created);
1250+
SharedMemoryHelpers::VerifyStringOperation(
1251+
lockFilePath.Append('/') && lockFilePath.Append(id->GetName(), id->GetNameCharCount()));
1252+
int lockFileDescriptor = SharedMemoryHelpers::CreateOrOpenFile(errors, lockFilePath, id, created);
12341253
if (lockFileDescriptor == -1)
12351254
{
12361255
_ASSERTE(!created);
@@ -1394,11 +1413,13 @@ void NamedMutexProcessData::Close(bool isAbruptShutdown, bool releaseSharedData)
13941413
{
13951414
// Delete the lock file, and the session directory if it's not empty
13961415
PathCharString path;
1397-
SharedMemoryHelpers::BuildSharedFilesPath(path, SHARED_MEMORY_LOCK_FILES_DIRECTORY_NAME);
1398-
SharedMemoryId *id = m_processDataHeader->GetId();
1399-
SharedMemoryHelpers::VerifyStringOperation(path.Append('/'));
1400-
SharedMemoryHelpers::VerifyStringOperation(id->AppendSessionDirectoryName(path));
1401-
SharedMemoryHelpers::VerifyStringOperation(path.Append('/'));
1416+
const SharedMemoryId *id = m_processDataHeader->GetId();
1417+
SharedMemoryHelpers::VerifyStringOperation(
1418+
path.Set(*gSharedFilesPath) &&
1419+
id->AppendRuntimeTempDirectoryName(path) &&
1420+
path.Append('/') && path.Append(SHARED_MEMORY_LOCK_FILES_DIRECTORY_NAME) &&
1421+
path.Append('/') && id->AppendSessionDirectoryName(path) &&
1422+
path.Append('/'));
14021423
SIZE_T sessionDirectoryPathCharCount = path.GetCount();
14031424
SharedMemoryHelpers::VerifyStringOperation(path.Append(id->GetName(), id->GetNameCharCount()));
14041425
unlink(path);

‎src/coreclr/pal/tests/palsuite/threading/NamedMutex/test1/namedmutex.cpp

+209-137
Large diffs are not rendered by default.

‎src/coreclr/pal/tests/palsuite/threading/NamedMutex/test1/nopal.cpp

+25-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <sys/stat.h>
88
#include <sys/types.h>
99

10+
#include <errno.h>
1011
#include <fcntl.h>
1112
#include <signal.h>
1213
#include <stdio.h>
@@ -27,6 +28,16 @@ unsigned int test_getpid()
2728
return getpid();
2829
}
2930

31+
unsigned int test_getsid()
32+
{
33+
return getsid(0);
34+
}
35+
36+
unsigned int test_geteuid()
37+
{
38+
return geteuid();
39+
}
40+
3041
int test_kill(unsigned int pid)
3142
{
3243
return kill(pid, SIGKILL);
@@ -41,11 +52,24 @@ bool TestFileExists(const char *path)
4152
return true;
4253
}
4354

44-
bool WriteHeaderInfo(const char *path, char sharedMemoryType, char version, int *fdRef)
55+
bool WriteHeaderInfo(const char *path, bool currentUserOnly, char sharedMemoryType, char version, int *fdRef)
4556
{
4657
int fd = open(path, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
4758
if (fd == -1)
4859
return false;
60+
61+
if (currentUserOnly)
62+
{
63+
int chmodResult;
64+
do
65+
{
66+
chmodResult = chmod(path, S_IRUSR | S_IWUSR);
67+
} while (chmodResult != 0 && errno == EINTR);
68+
69+
if (chmodResult != 0)
70+
return false;
71+
}
72+
4973
*fdRef = fd;
5074
if (ftruncate(fd, getpagesize()) != 0)
5175
return false;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Runtime.InteropServices;
5+
using Microsoft.Win32.SafeHandles;
6+
7+
internal static partial class Interop
8+
{
9+
internal static partial class Advapi32
10+
{
11+
[LibraryImport(Libraries.Advapi32, SetLastError = true)]
12+
[return: MarshalAs(UnmanagedType.Bool)]
13+
internal static partial bool InitializeAcl(nint pAcl, int nAclLength, int dwAclRevision);
14+
15+
[LibraryImport(Libraries.Advapi32, EntryPoint = "SetEntriesInAclW", SetLastError = true)]
16+
internal static unsafe partial int SetEntriesInAcl(
17+
int cCountOfExplicitEntries,
18+
EXPLICIT_ACCESS* pListOfExplicitEntries,
19+
nint OldAcl,
20+
out SafeLocalAllocHandle NewAcl);
21+
22+
[LibraryImport(Libraries.Advapi32, SetLastError = true)]
23+
[return: MarshalAs(UnmanagedType.Bool)]
24+
internal static unsafe partial bool GetAce(ACL* pAcl, int dwAceIndex, out ACE* pAce);
25+
26+
[LibraryImport(Libraries.Advapi32, SetLastError = true)]
27+
[return: MarshalAs(UnmanagedType.Bool)]
28+
internal static partial bool AddMandatoryAce(
29+
nint pAcl,
30+
int dwAceRevision,
31+
int AceFlags,
32+
int MandatoryPolicy,
33+
nint pLabelSid);
34+
35+
internal const int ACL_REVISION = 2;
36+
37+
// Values for ACE_HEADER.AceType
38+
internal const byte ACCESS_ALLOWED_ACE_TYPE = 0x0;
39+
40+
// Values for MandatoryPolicy in AddMandatoryAce()
41+
internal const int SYSTEM_MANDATORY_LABEL_NO_WRITE_UP = 0x1;
42+
internal const int SYSTEM_MANDATORY_LABEL_NO_READ_UP = 0x2;
43+
44+
// Values for the RID portion of a mandatory label SID, which indicates the integrity level
45+
internal const uint SECURITY_MANDATORY_MEDIUM_RID = 0x2000;
46+
47+
[StructLayout(LayoutKind.Sequential)]
48+
internal struct ACL
49+
{
50+
public byte AclRevision;
51+
public byte Sbz1;
52+
public ushort AclSize;
53+
public ushort AceCount;
54+
public ushort Sbz2;
55+
}
56+
57+
[StructLayout(LayoutKind.Sequential)]
58+
internal struct ACE
59+
{
60+
public ACE_HEADER Header;
61+
public uint Mask;
62+
public uint SidStart;
63+
64+
public static int SizeOfSidPortionInAce => sizeof(uint);
65+
}
66+
67+
[StructLayout(LayoutKind.Sequential)]
68+
internal struct ACE_HEADER
69+
{
70+
public byte AceType;
71+
public byte AceFlags;
72+
public ushort AceSize;
73+
}
74+
75+
[StructLayout(LayoutKind.Sequential)]
76+
internal struct EXPLICIT_ACCESS
77+
{
78+
public int grfAccessPermissions;
79+
public ACCESS_MODE grfAccessMode;
80+
public int grfInheritance;
81+
public TRUSTEE Trustee;
82+
}
83+
84+
[StructLayout(LayoutKind.Sequential)]
85+
internal struct TRUSTEE
86+
{
87+
public nint pMultipleTrustee;
88+
public int MultipleTrusteeOperation;
89+
public TRUSTEE_FORM TrusteeForm;
90+
public TRUSTEE_TYPE TrusteeType;
91+
public nint ptstrName;
92+
}
93+
94+
internal enum ACCESS_MODE
95+
{
96+
NOT_USED_ACCESS,
97+
GRANT_ACCESS,
98+
SET_ACCESS,
99+
DENY_ACCESS,
100+
REVOKE_ACCESS,
101+
SET_AUDIT_SUCCESS,
102+
SET_AUDIT_FAILURE
103+
}
104+
105+
// Constants for EXPLICIT_ACCESS.grfInheritance
106+
internal const int EXPLICIT_ACCESS_NO_INHERITANCE = 0;
107+
108+
internal enum TRUSTEE_FORM
109+
{
110+
TRUSTEE_IS_SID,
111+
TRUSTEE_IS_NAME,
112+
TRUSTEE_BAD_FORM,
113+
TRUSTEE_IS_OBJECTS_AND_SID,
114+
TRUSTEE_IS_OBJECTS_AND_NAME
115+
}
116+
117+
internal enum TRUSTEE_TYPE
118+
{
119+
TRUSTEE_IS_UNKNOWN,
120+
TRUSTEE_IS_USER,
121+
TRUSTEE_IS_GROUP,
122+
TRUSTEE_IS_DOMAIN,
123+
TRUSTEE_IS_ALIAS,
124+
TRUSTEE_IS_WELL_KNOWN_GROUP,
125+
TRUSTEE_IS_DELETED,
126+
TRUSTEE_IS_INVALID,
127+
TRUSTEE_IS_COMPUTER
128+
}
129+
}
130+
}

‎src/libraries/Common/src/Interop/Windows/Advapi32/Interop.CreateWellKnownSid.cs

+19-1
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,29 @@ internal static partial class Interop
88
{
99
internal static partial class Advapi32
1010
{
11-
[LibraryImport(Interop.Libraries.Advapi32, EntryPoint = "CreateWellKnownSid", SetLastError = true)]
11+
private const int SID_MAX_SUB_AUTHORITIES = 15;
12+
internal const int SECURITY_MAX_SID_SIZE =
13+
12 /* sizeof(SID) */ - 4 /* sizeof(DWORD) */ + SID_MAX_SUB_AUTHORITIES * 4 /* sizeof(DWORD) */;
14+
15+
[LibraryImport(Libraries.Advapi32, SetLastError = true)]
1216
internal static partial int CreateWellKnownSid(
1317
int sidType,
1418
byte[]? domainSid,
1519
byte[] resultSid,
1620
ref uint resultSidLength);
21+
22+
[LibraryImport(Libraries.Advapi32, SetLastError = true)]
23+
[return: MarshalAs(UnmanagedType.Bool)]
24+
internal static partial bool CreateWellKnownSid(
25+
int sidType,
26+
nint domainSid,
27+
nint resultSid,
28+
ref uint resultSidLength);
29+
30+
internal enum WELL_KNOWN_SID_TYPE
31+
{
32+
WinBuiltinAdministratorsSid = 26,
33+
WinMediumLabelSid = 67
34+
}
1735
}
1836
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Runtime.InteropServices;
6+
7+
internal static partial class Interop
8+
{
9+
internal static partial class Advapi32
10+
{
11+
[LibraryImport(Interop.Libraries.Advapi32, SetLastError = true)]
12+
[return: MarshalAs(UnmanagedType.Bool)]
13+
internal static partial bool EqualSid(nint pSid1, nint pSid2);
14+
}
15+
}

‎src/libraries/Common/src/Interop/Windows/Advapi32/Interop.GetSecurityInfoByHandle.cs

+23
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,28 @@ internal static unsafe partial uint GetSecurityInfoByHandle(
1818
IntPtr* dacl,
1919
IntPtr* sacl,
2020
IntPtr* securityDescriptor);
21+
22+
// Values for objectType
23+
internal enum SE_OBJECT_TYPE
24+
{
25+
SE_UNKNOWN_OBJECT_TYPE,
26+
SE_FILE_OBJECT,
27+
SE_SERVICE,
28+
SE_PRINTER,
29+
SE_REGISTRY_KEY,
30+
SE_LMSHARE,
31+
SE_KERNEL_OBJECT,
32+
SE_WINDOW_OBJECT,
33+
SE_DS_OBJECT,
34+
SE_DS_OBJECT_ALL,
35+
SE_PROVIDER_DEFINED_OBJECT,
36+
SE_WMIGUID_OBJECT,
37+
SE_REGISTRY_WOW64_32KEY,
38+
SE_REGISTRY_WOW64_64KEY
39+
}
40+
41+
// Values for securityInformation
42+
internal const uint OWNER_SECURITY_INFORMATION = 0x1;
43+
internal const uint DACL_SECURITY_INFORMATION = 0x4;
2144
}
2245
}
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,19 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
54
using System.Runtime.InteropServices;
6-
using System.Security.Principal;
75
using Microsoft.Win32.SafeHandles;
86

97
internal static partial class Interop
108
{
119
internal static partial class Advapi32
1210
{
13-
[LibraryImport(Interop.Libraries.Advapi32, SetLastError = true)]
11+
[LibraryImport(Libraries.Advapi32, SetLastError = true)]
1412
[return: MarshalAs(UnmanagedType.Bool)]
15-
private static partial bool OpenThreadToken(
16-
IntPtr ThreadHandle,
17-
TokenAccessLevels dwDesiredAccess,
18-
[MarshalAs(UnmanagedType.Bool)] bool bOpenAsSelf,
19-
out SafeAccessTokenHandle phThreadToken);
20-
21-
internal static bool OpenThreadToken(TokenAccessLevels desiredAccess, WinSecurityContext openAs, out SafeAccessTokenHandle tokenHandle)
22-
{
23-
bool openAsSelf = true;
24-
if (openAs == WinSecurityContext.Thread)
25-
openAsSelf = false;
26-
27-
if (OpenThreadToken(Kernel32.GetCurrentThread(), desiredAccess, openAsSelf, out tokenHandle))
28-
return true;
29-
30-
if (openAs == WinSecurityContext.Both)
31-
{
32-
openAsSelf = false;
33-
tokenHandle.Dispose();
34-
if (OpenThreadToken(Kernel32.GetCurrentThread(), desiredAccess, openAsSelf, out tokenHandle))
35-
return true;
36-
}
37-
38-
return false;
39-
}
13+
internal static partial bool OpenThreadToken(
14+
nint ThreadHandle,
15+
int DesiredAccess,
16+
[MarshalAs(UnmanagedType.Bool)] bool OpenAsSelf,
17+
out SafeTokenHandle TokenHandle);
4018
}
4119
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Runtime.InteropServices;
6+
using System.Security.Principal;
7+
using Microsoft.Win32.SafeHandles;
8+
9+
internal static partial class Interop
10+
{
11+
internal static partial class Advapi32
12+
{
13+
[LibraryImport(Interop.Libraries.Advapi32, SetLastError = true)]
14+
[return: MarshalAs(UnmanagedType.Bool)]
15+
private static partial bool OpenThreadToken(
16+
IntPtr ThreadHandle,
17+
TokenAccessLevels dwDesiredAccess,
18+
[MarshalAs(UnmanagedType.Bool)] bool bOpenAsSelf,
19+
out SafeAccessTokenHandle phThreadToken);
20+
21+
internal static bool OpenThreadToken(TokenAccessLevels desiredAccess, WinSecurityContext openAs, out SafeAccessTokenHandle tokenHandle)
22+
{
23+
bool openAsSelf = true;
24+
if (openAs == WinSecurityContext.Thread)
25+
openAsSelf = false;
26+
27+
if (OpenThreadToken(Kernel32.GetCurrentThread(), desiredAccess, openAsSelf, out tokenHandle))
28+
return true;
29+
30+
if (openAs == WinSecurityContext.Both)
31+
{
32+
openAsSelf = false;
33+
tokenHandle.Dispose();
34+
if (OpenThreadToken(Kernel32.GetCurrentThread(), desiredAccess, openAsSelf, out tokenHandle))
35+
return true;
36+
}
37+
38+
return false;
39+
}
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Runtime.InteropServices;
5+
6+
internal static partial class Interop
7+
{
8+
internal static partial class Advapi32
9+
{
10+
internal const int SECURITY_DESCRIPTOR_MIN_LENGTH = // AlignUp(4, sizeof(nint)) + 4 * sizeof(nint)
11+
#if TARGET_64BIT
12+
40;
13+
#else
14+
20;
15+
#endif
16+
17+
internal const int SECURITY_DESCRIPTOR_REVISION = 1;
18+
19+
[LibraryImport(Libraries.Advapi32, SetLastError = true)]
20+
[return: MarshalAs(UnmanagedType.Bool)]
21+
internal static partial bool InitializeSecurityDescriptor(nint pSecurityDescriptor, int dwRevision);
22+
23+
[LibraryImport(Libraries.Advapi32, SetLastError = true)]
24+
[return: MarshalAs(UnmanagedType.Bool)]
25+
internal static partial bool SetSecurityDescriptorOwner(
26+
nint pSecurityDescriptor,
27+
nint pOwner,
28+
[MarshalAs(UnmanagedType.Bool)] bool bOwnerDefaulted);
29+
30+
[LibraryImport(Libraries.Advapi32, SetLastError = true)]
31+
[return: MarshalAs(UnmanagedType.Bool)]
32+
internal static partial bool SetSecurityDescriptorGroup(
33+
nint pSecurityDescriptor,
34+
nint pGroup,
35+
[MarshalAs(UnmanagedType.Bool)] bool bGroupDefaulted);
36+
37+
[LibraryImport(Libraries.Advapi32, SetLastError = true)]
38+
[return: MarshalAs(UnmanagedType.Bool)]
39+
internal static partial bool SetSecurityDescriptorDacl(
40+
nint pSecurityDescriptor,
41+
[MarshalAs(UnmanagedType.Bool)] bool bDaclPresent,
42+
nint pDacl,
43+
[MarshalAs(UnmanagedType.Bool)] bool bDaclDefaulted);
44+
45+
[LibraryImport(Libraries.Advapi32, SetLastError = true)]
46+
[return: MarshalAs(UnmanagedType.Bool)]
47+
internal static partial bool SetSecurityDescriptorSacl(
48+
nint pSecurityDescriptor,
49+
[MarshalAs(UnmanagedType.Bool)] bool bSaclPresent,
50+
nint pSacl,
51+
[MarshalAs(UnmanagedType.Bool)] bool bSaclDefaulted);
52+
}
53+
}

‎src/libraries/Common/src/Interop/Windows/Interop.Errors.cs

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ internal static partial class Errors
3838
internal const int ERROR_INVALID_NAME = 0x7B;
3939
internal const int ERROR_INVALID_LEVEL = 0x7C;
4040
internal const int ERROR_MOD_NOT_FOUND = 0x7E;
41+
internal const int ERROR_PROC_NOT_FOUND = 0x7F;
4142
internal const int ERROR_NEGATIVE_SEEK = 0x83;
4243
internal const int ERROR_DIR_NOT_EMPTY = 0x91;
4344
internal const int ERROR_BAD_PATHNAME = 0xA1;

‎src/libraries/Common/src/Interop/Windows/Kernel32/Interop.Constants.cs

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ internal static partial class Kernel32
1010
internal const int WAIT_ABANDONED = 0x00000080;
1111

1212
internal const int MAXIMUM_ALLOWED = 0x02000000;
13+
internal const int STANDARD_RIGHTS_REQUIRED = 0x000F0000;
14+
internal const int READ_CONTROL = 0x00020000;
15+
internal const int WRITE_DAC = 0x00040000;
16+
internal const int WRITE_OWNER = 0x00080000;
1317
internal const int SYNCHRONIZE = 0x00100000;
1418
internal const int MUTEX_MODIFY_STATE = 0x00000001;
1519
internal const int SEMAPHORE_MODIFY_STATE = 0x00000002;

‎src/libraries/Common/src/System/Threading/OpenExistingResult.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ internal enum OpenExistingResult
1212
Success,
1313
NameNotFound,
1414
PathNotFound,
15-
NameInvalid
15+
NameInvalid,
16+
ObjectIncompatibleWithCurrentUserOnly
1617
}
1718
}

‎src/libraries/System.Private.CoreLib/src/Resources/Strings.resx

+6
Original file line numberDiff line numberDiff line change
@@ -3476,6 +3476,12 @@
34763476
<data name="Lock_Exit_SynchronizationLockException" xml:space="preserve">
34773477
<value>The calling thread does not hold the lock.</value>
34783478
</data>
3479+
<data name="NamedWaitHandles_IncompatibleNamePrefix" xml:space="preserve">
3480+
<value>The specified name '{0}' has a prefix that is not compatible with the specified option 'CurrentSessionOnly'.</value>
3481+
</data>
3482+
<data name="NamedWaitHandles_ExistingObjectIncompatibleWithCurrentUserOnly" xml:space="preserve">
3483+
<value>An object with the specified name '{0}' exists and can't be opened because it's not compatible with the specified option 'CurrentUserOnly = true'.</value>
3484+
</data>
34793485
<data name="SpinLock_IsHeldByCurrentThread" xml:space="preserve">
34803486
<value>Thread tracking is disabled.</value>
34813487
</data>

‎src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems

+31
Original file line numberDiff line numberDiff line change
@@ -1241,6 +1241,7 @@
12411241
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\ManualResetEventSlim.cs" />
12421242
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\Monitor.cs" />
12431243
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\Mutex.cs" />
1244+
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\NamedWaitHandleOptions.cs" />
12441245
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\NativeOverlapped.cs" />
12451246
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\Overlapped.cs" />
12461247
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\ParameterizedThreadStart.cs" />
@@ -1604,9 +1605,18 @@
16041605
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\CompilerServices\QCallHandles.cs" Condition="'$(FeatureNativeAot)' != 'true'" />
16051606
</ItemGroup>
16061607
<ItemGroup Condition="'$(TargetsWindows)' == 'true'">
1608+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.AccessControlLists.cs">
1609+
<Link>Common\Interop\Windows\Advapi32\Interop.AccessControlLists.cs</Link>
1610+
</Compile>
1611+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.CreateWellKnownSid.cs">
1612+
<Link>Common\Interop\Windows\Advapi32\Interop.CreateWellKnownSid.cs</Link>
1613+
</Compile>
16071614
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.EncryptDecrypt.cs">
16081615
<Link>Common\Interop\Windows\Advapi32\Interop.EncryptDecrypt.cs</Link>
16091616
</Compile>
1617+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.EqualSid.cs">
1618+
<Link>Interop\Windows\Advapi32\Interop.EqualSid.cs</Link>
1619+
</Compile>
16101620
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.EventActivityIdControl.cs">
16111621
<Link>Common\Interop\Windows\Advapi32\Interop.EventActivityIdControl.cs</Link>
16121622
</Compile>
@@ -1628,6 +1638,18 @@
16281638
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.EventWriteTransfer.cs">
16291639
<Link>Common\Interop\Windows\Advapi32\Interop.EventWriteTransfer.cs</Link>
16301640
</Compile>
1641+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.GetLengthSid.cs">
1642+
<Link>Common\Interop\Windows\Advapi32\Interop.GetLengthSid.cs</Link>
1643+
</Compile>
1644+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.GetSecurityInfoByHandle.cs">
1645+
<Link>Interop\Windows\Advapi32\Interop.GetSecurityInfoByHandle.cs</Link>
1646+
</Compile>
1647+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.GetSidIdentifierAuthority.cs">
1648+
<Link>Interop\Windows\Advapi32\Interop.GetSidIdentifierAuthority.cs</Link>
1649+
</Compile>
1650+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.GetTokenInformation.cs">
1651+
<Link>Interop\Windows\Advapi32\Interop.GetTokenInformation.cs</Link>
1652+
</Compile>
16311653
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.GetTokenInformation_void.cs">
16321654
<Link>Common\Interop\Windows\Advapi32\Interop.GetTokenInformation_void.cs</Link>
16331655
</Compile>
@@ -1637,6 +1659,9 @@
16371659
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.OpenProcessToken.cs">
16381660
<Link>Common\Interop\Windows\Advapi32\Interop.OpenProcessToken.cs</Link>
16391661
</Compile>
1662+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.OpenThreadToken.cs">
1663+
<Link>Common\Interop\Windows\Advapi32\Interop.OpenThreadToken.cs</Link>
1664+
</Compile>
16401665
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.RegCloseKey.cs">
16411666
<Link>Common\Interop\Windows\Advapi32\Interop.RegCloseKey.cs</Link>
16421667
</Compile>
@@ -1670,6 +1695,9 @@
16701695
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.RegSetValueEx.cs">
16711696
<Link>Common\Interop\Windows\Advapi32\Interop.RegSetValueEx.cs</Link>
16721697
</Compile>
1698+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.SecurityDescriptors.cs">
1699+
<Link>Common\Interop\Windows\Advapi32\Interop.SecurityDescriptors.cs</Link>
1700+
</Compile>
16731701
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.TOKEN_ACCESS_LEVELS.cs">
16741702
<Link>Common\Interop\Windows\Advapi32\Interop.TOKEN_ACCESS_LEVELS.cs</Link>
16751703
</Compile>
@@ -2144,6 +2172,9 @@
21442172
<Compile Include="$(CommonPath)\Interop\Windows\Kernel32\Interop.Sleep.cs">
21452173
<Link>Common\Interop\Windows\Kernel32\Interop.Sleep.cs</Link>
21462174
</Compile>
2175+
<Compile Include="$(CommonPath)Microsoft\Win32\SafeHandles\SafeLocalAllocHandle.cs">
2176+
<Link>Common\Microsoft\Win32\SafeHandles\SafeLocalAllocHandle.cs</Link>
2177+
</Compile>
21472178
<Compile Include="$(CommonPath)Microsoft\Win32\SafeHandles\SafeTokenHandle.cs">
21482179
<Link>Common\Microsoft\Win32\SafeHandles\SafeTokenHandle.cs</Link>
21492180
</Compile>

‎src/libraries/System.Private.CoreLib/src/System/Threading/EventWaitHandle.Unix.cs

+22-2
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,36 @@ namespace System.Threading
1010
{
1111
public partial class EventWaitHandle
1212
{
13-
private void CreateEventCore(bool initialState, EventResetMode mode, string? name, out bool createdNew)
13+
private void CreateEventCore(bool initialState, EventResetMode mode)
1414
{
15+
ValidateMode(mode);
16+
SafeWaitHandle = WaitSubsystem.NewEvent(initialState, mode);
17+
}
18+
19+
#pragma warning disable IDE0060 // Unused parameter
20+
private void CreateEventCore(
21+
bool initialState,
22+
EventResetMode mode,
23+
string? name,
24+
NamedWaitHandleOptionsInternal options,
25+
out bool createdNew)
26+
{
27+
ValidateMode(mode);
28+
1529
if (name != null)
30+
{
1631
throw new PlatformNotSupportedException(SR.PlatformNotSupported_NamedSynchronizationPrimitives);
32+
}
1733

1834
SafeWaitHandle = WaitSubsystem.NewEvent(initialState, mode);
1935
createdNew = true;
2036
}
37+
#pragma warning restore IDE0060
2138

22-
private static OpenExistingResult OpenExistingWorker(string name, out EventWaitHandle? result)
39+
private static OpenExistingResult OpenExistingWorker(
40+
string name,
41+
NamedWaitHandleOptionsInternal options,
42+
out EventWaitHandle? result)
2343
{
2444
throw new PlatformNotSupportedException(SR.PlatformNotSupported_NamedSynchronizationPrimitives);
2545
}

‎src/libraries/System.Private.CoreLib/src/System/Threading/EventWaitHandle.Windows.cs

+120-14
Original file line numberDiff line numberDiff line change
@@ -11,46 +11,131 @@ public partial class EventWaitHandle
1111
{
1212
private const uint AccessRights = (uint)Interop.Kernel32.MAXIMUM_ALLOWED | Interop.Kernel32.SYNCHRONIZE | Interop.Kernel32.EVENT_MODIFY_STATE;
1313

14+
#if TARGET_WINDOWS
15+
// Can't use MAXIMUM_ALLOWED in an access control entry (ACE)
16+
private const int CurrentUserOnlyAceRights =
17+
Interop.Kernel32.STANDARD_RIGHTS_REQUIRED | Interop.Kernel32.SYNCHRONIZE | Interop.Kernel32.EVENT_MODIFY_STATE;
18+
#endif
19+
1420
private EventWaitHandle(SafeWaitHandle handle)
1521
{
1622
SafeWaitHandle = handle;
1723
}
1824

19-
private void CreateEventCore(bool initialState, EventResetMode mode, string? name, out bool createdNew)
25+
private unsafe void CreateEventCore(bool initialState, EventResetMode mode)
26+
{
27+
ValidateMode(mode);
28+
29+
uint flags = initialState ? Interop.Kernel32.CREATE_EVENT_INITIAL_SET : 0;
30+
if (mode == EventResetMode.ManualReset)
31+
flags |= Interop.Kernel32.CREATE_EVENT_MANUAL_RESET;
32+
SafeWaitHandle handle = Interop.Kernel32.CreateEventEx(lpSecurityAttributes: 0, name: null, flags, AccessRights);
33+
if (handle.IsInvalid)
34+
{
35+
int errorCode = Marshal.GetLastPInvokeError();
36+
handle.SetHandleAsInvalid();
37+
throw Win32Marshal.GetExceptionForWin32Error(errorCode);
38+
}
39+
40+
SafeWaitHandle = handle;
41+
}
42+
43+
private unsafe void CreateEventCore(
44+
bool initialState,
45+
EventResetMode mode,
46+
string? name,
47+
NamedWaitHandleOptionsInternal options,
48+
out bool createdNew)
2049
{
21-
#if TARGET_UNIX || TARGET_BROWSER || TARGET_WASI
50+
ValidateMode(mode);
51+
52+
#if !TARGET_WINDOWS
2253
if (name != null)
54+
{
2355
throw new PlatformNotSupportedException(SR.PlatformNotSupported_NamedSynchronizationPrimitives);
56+
}
2457
#endif
25-
uint eventFlags = initialState ? Interop.Kernel32.CREATE_EVENT_INITIAL_SET : 0;
26-
if (mode == EventResetMode.ManualReset)
27-
eventFlags |= (uint)Interop.Kernel32.CREATE_EVENT_MANUAL_RESET;
2858

29-
SafeWaitHandle handle = Interop.Kernel32.CreateEventEx(IntPtr.Zero, name, eventFlags, AccessRights);
59+
void* securityAttributesPtr = null;
60+
SafeWaitHandle handle;
61+
int errorCode;
62+
#if TARGET_WINDOWS
63+
Thread.CurrentUserSecurityDescriptorInfo securityDescriptorInfo = default;
64+
Interop.Kernel32.SECURITY_ATTRIBUTES securityAttributes = default;
65+
if (!string.IsNullOrEmpty(name) && options.WasSpecified)
66+
{
67+
name = options.GetNameWithSessionPrefix(name);
68+
if (options.CurrentUserOnly)
69+
{
70+
securityDescriptorInfo = new(CurrentUserOnlyAceRights);
71+
securityAttributes.nLength = (uint)sizeof(Interop.Kernel32.SECURITY_ATTRIBUTES);
72+
securityAttributes.lpSecurityDescriptor = (void*)securityDescriptorInfo.SecurityDescriptor;
73+
securityAttributesPtr = &securityAttributes;
74+
}
75+
}
3076

31-
int errorCode = Marshal.GetLastPInvokeError();
32-
if (handle.IsInvalid)
77+
using (securityDescriptorInfo)
3378
{
34-
handle.SetHandleAsInvalid();
35-
if (!string.IsNullOrEmpty(name) && errorCode == Interop.Errors.ERROR_INVALID_HANDLE)
36-
throw new WaitHandleCannotBeOpenedException(SR.Format(SR.Threading_WaitHandleCannotBeOpenedException_InvalidHandle, name));
79+
#endif
80+
uint eventFlags = initialState ? Interop.Kernel32.CREATE_EVENT_INITIAL_SET : 0;
81+
if (mode == EventResetMode.ManualReset)
82+
eventFlags |= Interop.Kernel32.CREATE_EVENT_MANUAL_RESET;
83+
handle = Interop.Kernel32.CreateEventEx((nint)securityAttributesPtr, name, eventFlags, AccessRights);
84+
errorCode = Marshal.GetLastPInvokeError();
85+
86+
if (handle.IsInvalid)
87+
{
88+
handle.SetHandleAsInvalid();
89+
if (!string.IsNullOrEmpty(name) && errorCode == Interop.Errors.ERROR_INVALID_HANDLE)
90+
throw new WaitHandleCannotBeOpenedException(SR.Format(SR.Threading_WaitHandleCannotBeOpenedException_InvalidHandle, name));
91+
92+
throw Win32Marshal.GetExceptionForWin32Error(errorCode, name);
93+
}
94+
#if TARGET_WINDOWS
3795

38-
throw Win32Marshal.GetExceptionForWin32Error(errorCode, name);
96+
if (errorCode == Interop.Errors.ERROR_ALREADY_EXISTS && securityAttributesPtr != null)
97+
{
98+
try
99+
{
100+
if (!Thread.CurrentUserSecurityDescriptorInfo.IsSecurityDescriptorCompatible(
101+
securityDescriptorInfo.TokenUser,
102+
handle,
103+
Interop.Kernel32.EVENT_MODIFY_STATE))
104+
{
105+
throw new WaitHandleCannotBeOpenedException(SR.Format(SR.NamedWaitHandles_ExistingObjectIncompatibleWithCurrentUserOnly, name));
106+
}
107+
}
108+
catch
109+
{
110+
handle.Dispose();
111+
throw;
112+
}
113+
}
39114
}
115+
#endif
116+
40117
createdNew = errorCode != Interop.Errors.ERROR_ALREADY_EXISTS;
41118
SafeWaitHandle = handle;
42119
}
43120

44-
private static OpenExistingResult OpenExistingWorker(string name, out EventWaitHandle? result)
121+
private static OpenExistingResult OpenExistingWorker(
122+
string name,
123+
NamedWaitHandleOptionsInternal options,
124+
out EventWaitHandle? result)
45125
{
46126
#if TARGET_WINDOWS
47127
ArgumentException.ThrowIfNullOrEmpty(name);
48128

49-
result = null;
129+
if (options.WasSpecified)
130+
{
131+
name = options.GetNameWithSessionPrefix(name);
132+
}
133+
50134
SafeWaitHandle myHandle = Interop.Kernel32.OpenEvent(AccessRights, false, name);
51135

52136
if (myHandle.IsInvalid)
53137
{
138+
result = null;
54139
int errorCode = Marshal.GetLastPInvokeError();
55140

56141
myHandle.Dispose();
@@ -64,6 +149,27 @@ private static OpenExistingResult OpenExistingWorker(string name, out EventWaitH
64149

65150
throw Win32Marshal.GetExceptionForWin32Error(errorCode, name);
66151
}
152+
153+
if (options.WasSpecified && options.CurrentUserOnly)
154+
{
155+
try
156+
{
157+
if (!Thread.CurrentUserSecurityDescriptorInfo.IsValidSecurityDescriptor(
158+
myHandle,
159+
Interop.Kernel32.EVENT_MODIFY_STATE))
160+
{
161+
myHandle.Dispose();
162+
result = null;
163+
return OpenExistingResult.ObjectIncompatibleWithCurrentUserOnly;
164+
}
165+
}
166+
catch
167+
{
168+
myHandle.Dispose();
169+
throw;
170+
}
171+
}
172+
67173
result = new EventWaitHandle(myHandle);
68174
return OpenExistingResult.Success;
69175
#else

‎src/libraries/System.Private.CoreLib/src/System/Threading/EventWaitHandle.cs

+108-9
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,110 @@ namespace System.Threading
1010
{
1111
public partial class EventWaitHandle : WaitHandle
1212
{
13-
public EventWaitHandle(bool initialState, EventResetMode mode) :
14-
this(initialState, mode, null, out _)
13+
public EventWaitHandle(bool initialState, EventResetMode mode)
1514
{
15+
CreateEventCore(initialState, mode);
1616
}
1717

18-
public EventWaitHandle(bool initialState, EventResetMode mode, string? name) :
19-
this(initialState, mode, name, out _)
18+
/// <summary>
19+
/// Creates a named or unnamed event, or opens a named event if a event with the name already exists.
20+
/// </summary>
21+
/// <param name="initialState">True to initially set the event to a signaled state; false otherwise.</param>
22+
/// <param name="mode">Indicates whether the event resets automatically or manually.</param>
23+
/// <param name="name">
24+
/// The name, if the event is to be shared with other processes; otherwise, null or an empty string.
25+
/// </param>
26+
/// <param name="options">
27+
/// Options for the named event. Defaulted options, such as when passing 'options: default' in C#, are
28+
/// 'CurrentUserOnly = true' and 'CurrentSessionOnly = true'. For more information, see 'NamedWaitHandleOptions'. The
29+
/// specified options may affect the namespace for the name, and access to the underlying event object.
30+
/// </param>
31+
public EventWaitHandle(bool initialState, EventResetMode mode, string? name, NamedWaitHandleOptions options)
2032
{
33+
CreateEventCore(initialState, mode, name, new(options), out _);
34+
}
35+
36+
public EventWaitHandle(bool initialState, EventResetMode mode, string? name)
37+
{
38+
CreateEventCore(initialState, mode, name, options: default, out _);
39+
}
40+
41+
/// <summary>
42+
/// Creates a named or unnamed event, or opens a named event if a event with the name already exists.
43+
/// </summary>
44+
/// <param name="initialState">True to initially set the event to a signaled state; false otherwise.</param>
45+
/// <param name="mode">Indicates whether the event resets automatically or manually.</param>
46+
/// <param name="name">
47+
/// The name, if the event is to be shared with other processes; otherwise, null or an empty string.
48+
/// </param>
49+
/// <param name="options">
50+
/// Options for the named event. Defaulted options, such as when passing 'options: default' in C#, are
51+
/// 'CurrentUserOnly = true' and 'CurrentSessionOnly = true'. For more information, see 'NamedWaitHandleOptions'. The
52+
/// specified options may affect the namespace for the name, and access to the underlying event object.
53+
/// </param>
54+
/// <param name="createdNew">
55+
/// True if the event was created; false if an existing named event was opened.
56+
/// </param>
57+
public EventWaitHandle(bool initialState, EventResetMode mode, string? name, NamedWaitHandleOptions options, out bool createdNew)
58+
{
59+
CreateEventCore(initialState, mode, name, new(options), out createdNew);
2160
}
2261

2362
public EventWaitHandle(bool initialState, EventResetMode mode, string? name, out bool createdNew)
63+
{
64+
CreateEventCore(initialState, mode, name, options: default, out createdNew);
65+
}
66+
67+
private static void ValidateMode(EventResetMode mode)
2468
{
2569
if (mode != EventResetMode.AutoReset && mode != EventResetMode.ManualReset)
70+
{
2671
throw new ArgumentException(SR.Argument_InvalidFlag, nameof(mode));
72+
}
73+
}
74+
75+
/// <summary>
76+
/// Opens an existing named event.
77+
/// </summary>
78+
/// <param name="name">The name of the event to be shared with other processes.</param>
79+
/// <param name="options">
80+
/// Options for the named event. Defaulted options, such as when passing 'options: default' in C#, are
81+
/// 'CurrentUserOnly = true' and 'CurrentSessionOnly = true'. For more information, see 'NamedWaitHandleOptions'. The
82+
/// specified options may affect the namespace for the name, and access to the underlying event object.
83+
/// </param>
84+
/// <returns>An object that represents the named event.</returns>
85+
[SupportedOSPlatform("windows")]
86+
public static EventWaitHandle OpenExisting(string name, NamedWaitHandleOptions options)
87+
{
88+
OpenExistingResult openExistingResult = OpenExistingWorker(name, new(options), out EventWaitHandle? result);
89+
if (openExistingResult != OpenExistingResult.Success)
90+
{
91+
ThrowForOpenExistingFailure(openExistingResult, name);
92+
}
2793

28-
CreateEventCore(initialState, mode, name, out createdNew);
94+
Debug.Assert(result != null, "result should be non-null on success");
95+
return result;
2996
}
3097

3198
[SupportedOSPlatform("windows")]
3299
public static EventWaitHandle OpenExisting(string name)
33100
{
34-
switch (OpenExistingWorker(name, out EventWaitHandle? result))
101+
OpenExistingResult openExistingResult = OpenExistingWorker(name, options: default, out EventWaitHandle? result);
102+
if (openExistingResult != OpenExistingResult.Success)
103+
{
104+
ThrowForOpenExistingFailure(openExistingResult, name);
105+
}
106+
107+
Debug.Assert(result != null, "result should be non-null on success");
108+
return result;
109+
}
110+
111+
[DoesNotReturn]
112+
private static void ThrowForOpenExistingFailure(OpenExistingResult openExistingResult, string name)
113+
{
114+
Debug.Assert(openExistingResult != OpenExistingResult.Success);
115+
116+
switch (openExistingResult)
35117
{
36118
case OpenExistingResult.NameNotFound:
37119
throw new WaitHandleCannotBeOpenedException();
@@ -40,13 +122,30 @@ public static EventWaitHandle OpenExisting(string name)
40122
case OpenExistingResult.PathNotFound:
41123
throw new DirectoryNotFoundException(SR.Format(SR.IO_PathNotFound_Path, name));
42124
default:
43-
Debug.Assert(result != null, "result should be non-null on success");
44-
return result;
125+
Debug.Assert(openExistingResult == OpenExistingResult.ObjectIncompatibleWithCurrentUserOnly);
126+
throw new WaitHandleCannotBeOpenedException(SR.Format(SR.NamedWaitHandles_ExistingObjectIncompatibleWithCurrentUserOnly, name));
45127
}
46128
}
47129

130+
/// <summary>
131+
/// Tries to open an existing named event and returns a value indicating whether it was successful.
132+
/// </summary>
133+
/// <param name="name">The name of the event to be shared with other processes.</param>
134+
/// <param name="options">
135+
/// Options for the named event. Defaulted options, such as when passing 'options: default' in C#, are
136+
/// 'CurrentUserOnly = true' and 'CurrentSessionOnly = true'. For more information, see 'NamedWaitHandleOptions'. The
137+
/// specified options may affect the namespace for the name, and access to the underlying event object.
138+
/// </param>
139+
/// <param name="result">
140+
/// An object that represents the named event if the method returns true; otherwise, null.
141+
/// </param>
142+
/// <returns>True if the named event was opened successfully; otherwise, false.</returns>
143+
[SupportedOSPlatform("windows")]
144+
public static bool TryOpenExisting(string name, NamedWaitHandleOptions options, [NotNullWhen(true)] out EventWaitHandle? result) =>
145+
OpenExistingWorker(name, new(options), out result!) == OpenExistingResult.Success;
146+
48147
[SupportedOSPlatform("windows")]
49148
public static bool TryOpenExisting(string name, [NotNullWhen(true)] out EventWaitHandle? result) =>
50-
OpenExistingWorker(name, out result!) == OpenExistingResult.Success;
149+
OpenExistingWorker(name, options: default, out result!) == OpenExistingResult.Success;
51150
}
52151
}

‎src/libraries/System.Private.CoreLib/src/System/Threading/Mutex.Unix.cs

+37-3
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,18 @@ namespace System.Threading
1010
{
1111
public sealed partial class Mutex
1212
{
13-
private void CreateMutexCore(bool initiallyOwned, string? name, out bool createdNew)
13+
private void CreateMutexCore(bool initiallyOwned) => SafeWaitHandle = WaitSubsystem.NewMutex(initiallyOwned);
14+
15+
private void CreateMutexCore(
16+
bool initiallyOwned,
17+
string? name,
18+
NamedWaitHandleOptionsInternal options,
19+
out bool createdNew)
1420
{
15-
if (name != null)
21+
if (!string.IsNullOrEmpty(name))
1622
{
23+
name = BuildNameForOptions(name, options);
24+
1725
SafeWaitHandle? safeWaitHandle = WaitSubsystem.CreateNamedMutex(initiallyOwned, name, out createdNew);
1826
if (safeWaitHandle == null)
1927
{
@@ -27,15 +35,41 @@ private void CreateMutexCore(bool initiallyOwned, string? name, out bool created
2735
createdNew = true;
2836
}
2937

30-
private static OpenExistingResult OpenExistingWorker(string name, out Mutex? result)
38+
private static OpenExistingResult OpenExistingWorker(
39+
string name,
40+
NamedWaitHandleOptionsInternal options,
41+
out Mutex? result)
3142
{
3243
ArgumentException.ThrowIfNullOrEmpty(name);
3344

45+
name = BuildNameForOptions(name, options);
46+
3447
OpenExistingResult status = WaitSubsystem.OpenNamedMutex(name, out SafeWaitHandle? safeWaitHandle);
3548
result = status == OpenExistingResult.Success ? new Mutex(safeWaitHandle!) : null;
3649
return status;
3750
}
3851

52+
private static string BuildNameForOptions(string name, NamedWaitHandleOptionsInternal options)
53+
{
54+
if (options.WasSpecified)
55+
{
56+
name = options.GetNameWithSessionPrefix(name);
57+
}
58+
59+
if (name.StartsWith(NamedWaitHandleOptionsInternal.CurrentSessionPrefix) &&
60+
name.Length > NamedWaitHandleOptionsInternal.CurrentSessionPrefix.Length)
61+
{
62+
name = name.Substring(NamedWaitHandleOptionsInternal.CurrentSessionPrefix.Length);
63+
}
64+
65+
if (options.WasSpecified && options.CurrentUserOnly)
66+
{
67+
name = @"User\" + name;
68+
}
69+
70+
return name;
71+
}
72+
3973
public void ReleaseMutex()
4074
{
4175
// The field value is modifiable via the public <see cref="WaitHandle.SafeWaitHandle"/> property, save it locally

‎src/libraries/System.Private.CoreLib/src/System/Threading/Mutex.Windows.cs

+100-11
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,98 @@ public sealed partial class Mutex : WaitHandle
1515
private const uint AccessRights =
1616
(uint)Interop.Kernel32.MAXIMUM_ALLOWED | Interop.Kernel32.SYNCHRONIZE | Interop.Kernel32.MUTEX_MODIFY_STATE;
1717

18-
private void CreateMutexCore(bool initiallyOwned, string? name, out bool createdNew)
18+
// Can't use MAXIMUM_ALLOWED in an access control entry (ACE)
19+
private const int CurrentUserOnlyAceRights =
20+
Interop.Kernel32.STANDARD_RIGHTS_REQUIRED | Interop.Kernel32.SYNCHRONIZE | Interop.Kernel32.MUTEX_MODIFY_STATE;
21+
22+
private void CreateMutexCore(bool initiallyOwned)
1923
{
20-
uint mutexFlags = initiallyOwned ? Interop.Kernel32.CREATE_MUTEX_INITIAL_OWNER : 0;
21-
SafeWaitHandle mutexHandle = Interop.Kernel32.CreateMutexEx(IntPtr.Zero, name, mutexFlags, AccessRights);
22-
int errorCode = Marshal.GetLastPInvokeError();
24+
uint flags = initiallyOwned ? Interop.Kernel32.CREATE_MUTEX_INITIAL_OWNER : 0;
25+
SafeWaitHandle handle = Interop.Kernel32.CreateMutexEx(lpMutexAttributes: 0, name: null, flags, AccessRights);
26+
if (handle.IsInvalid)
27+
{
28+
int errorCode = Marshal.GetLastPInvokeError();
29+
handle.SetHandleAsInvalid();
30+
throw Win32Marshal.GetExceptionForWin32Error(errorCode);
31+
}
32+
33+
SafeWaitHandle = handle;
34+
}
2335

24-
if (mutexHandle.IsInvalid)
36+
private unsafe void CreateMutexCore(
37+
bool initiallyOwned,
38+
string? name,
39+
NamedWaitHandleOptionsInternal options,
40+
out bool createdNew)
41+
{
42+
Thread.CurrentUserSecurityDescriptorInfo securityDescriptorInfo = default;
43+
Interop.Kernel32.SECURITY_ATTRIBUTES securityAttributes = default;
44+
Interop.Kernel32.SECURITY_ATTRIBUTES* securityAttributesPtr = null;
45+
if (!string.IsNullOrEmpty(name) && options.WasSpecified)
2546
{
26-
mutexHandle.SetHandleAsInvalid();
27-
if (errorCode == Interop.Errors.ERROR_INVALID_HANDLE)
28-
throw new WaitHandleCannotBeOpenedException(SR.Format(SR.Threading_WaitHandleCannotBeOpenedException_InvalidHandle, name));
47+
name = options.GetNameWithSessionPrefix(name);
48+
if (options.CurrentUserOnly)
49+
{
50+
securityDescriptorInfo = new(CurrentUserOnlyAceRights);
51+
securityAttributes.nLength = (uint)sizeof(Interop.Kernel32.SECURITY_ATTRIBUTES);
52+
securityAttributes.lpSecurityDescriptor = (void*)securityDescriptorInfo.SecurityDescriptor;
53+
securityAttributesPtr = &securityAttributes;
54+
}
55+
}
2956

30-
throw Win32Marshal.GetExceptionForWin32Error(errorCode, name);
57+
SafeWaitHandle mutexHandle;
58+
int errorCode;
59+
using (securityDescriptorInfo)
60+
{
61+
uint mutexFlags = initiallyOwned ? Interop.Kernel32.CREATE_MUTEX_INITIAL_OWNER : 0;
62+
mutexHandle = Interop.Kernel32.CreateMutexEx((nint)securityAttributesPtr, name, mutexFlags, AccessRights);
63+
errorCode = Marshal.GetLastPInvokeError();
64+
65+
if (mutexHandle.IsInvalid)
66+
{
67+
mutexHandle.SetHandleAsInvalid();
68+
if (errorCode == Interop.Errors.ERROR_INVALID_HANDLE)
69+
throw new WaitHandleCannotBeOpenedException(SR.Format(SR.Threading_WaitHandleCannotBeOpenedException_InvalidHandle, name));
70+
71+
throw Win32Marshal.GetExceptionForWin32Error(errorCode, name);
72+
}
73+
74+
if (errorCode == Interop.Errors.ERROR_ALREADY_EXISTS && securityAttributesPtr != null)
75+
{
76+
try
77+
{
78+
if (!Thread.CurrentUserSecurityDescriptorInfo.IsSecurityDescriptorCompatible(
79+
securityDescriptorInfo.TokenUser,
80+
mutexHandle,
81+
Interop.Kernel32.MUTEX_MODIFY_STATE))
82+
{
83+
throw new WaitHandleCannotBeOpenedException(SR.Format(SR.NamedWaitHandles_ExistingObjectIncompatibleWithCurrentUserOnly, name));
84+
}
85+
}
86+
catch
87+
{
88+
mutexHandle.Dispose();
89+
throw;
90+
}
91+
}
3192
}
3293

3394
createdNew = errorCode != Interop.Errors.ERROR_ALREADY_EXISTS;
3495
SafeWaitHandle = mutexHandle;
3596
}
3697

37-
private static OpenExistingResult OpenExistingWorker(string name, out Mutex? result)
98+
private static OpenExistingResult OpenExistingWorker(
99+
string name,
100+
NamedWaitHandleOptionsInternal options,
101+
out Mutex? result)
38102
{
39103
ArgumentException.ThrowIfNullOrEmpty(name);
40104

41-
result = null;
105+
if (options.WasSpecified)
106+
{
107+
name = options.GetNameWithSessionPrefix(name);
108+
}
109+
42110
// To allow users to view & edit the ACL's, call OpenMutex
43111
// with parameters to allow us to view & edit the ACL. This will
44112
// fail if we don't have permission to view or edit the ACL's.
@@ -47,6 +115,7 @@ private static OpenExistingResult OpenExistingWorker(string name, out Mutex? res
47115

48116
if (myHandle.IsInvalid)
49117
{
118+
result = null;
50119
int errorCode = Marshal.GetLastPInvokeError();
51120

52121
myHandle.Dispose();
@@ -61,6 +130,26 @@ private static OpenExistingResult OpenExistingWorker(string name, out Mutex? res
61130
throw Win32Marshal.GetExceptionForWin32Error(errorCode, name);
62131
}
63132

133+
if (options.WasSpecified && options.CurrentUserOnly)
134+
{
135+
try
136+
{
137+
if (!Thread.CurrentUserSecurityDescriptorInfo.IsValidSecurityDescriptor(
138+
myHandle,
139+
Interop.Kernel32.MUTEX_MODIFY_STATE))
140+
{
141+
myHandle.Dispose();
142+
result = null;
143+
return OpenExistingResult.ObjectIncompatibleWithCurrentUserOnly;
144+
}
145+
}
146+
catch
147+
{
148+
myHandle.Dispose();
149+
throw;
150+
}
151+
}
152+
64153
result = new Mutex(myHandle);
65154
return OpenExistingResult.Success;
66155
}

‎src/libraries/System.Private.CoreLib/src/System/Threading/Mutex.cs

+118-9
Original file line numberDiff line numberDiff line change
@@ -13,49 +13,158 @@ namespace System.Threading
1313
/// </summary>
1414
public sealed partial class Mutex : WaitHandle
1515
{
16+
/// <summary>
17+
/// Creates a named or unnamed mutex, or opens a named mutex if a mutex with the name already exists.
18+
/// </summary>
19+
/// <param name="initiallyOwned">
20+
/// True to acquire the mutex on the calling thread if it's created; otherwise, false.
21+
/// </param>
22+
/// <param name="name">
23+
/// The name, if the mutex is to be shared with other processes; otherwise, null or an empty string.
24+
/// </param>
25+
/// <param name="options">
26+
/// Options for the named mutex. Defaulted options, such as when passing 'options: default' in C#, are
27+
/// 'CurrentUserOnly = true' and 'CurrentSessionOnly = true'. For more information, see 'NamedWaitHandleOptions'. The
28+
/// specified options may affect the namespace for the name, and access to the underlying mutex object.
29+
/// </param>
30+
/// <param name="createdNew">
31+
/// True if the mutex was created; false if an existing named mutex was opened.
32+
/// </param>
33+
public Mutex(bool initiallyOwned, string? name, NamedWaitHandleOptions options, out bool createdNew)
34+
{
35+
CreateMutexCore(initiallyOwned, name, new(options), out createdNew);
36+
}
37+
1638
public Mutex(bool initiallyOwned, string? name, out bool createdNew)
1739
{
18-
CreateMutexCore(initiallyOwned, name, out createdNew);
40+
CreateMutexCore(initiallyOwned, name, options: default, out createdNew);
41+
}
42+
43+
/// <summary>
44+
/// Creates a named or unnamed mutex, or opens a named mutex if a mutex with the name already exists.
45+
/// </summary>
46+
/// <param name="initiallyOwned">
47+
/// True to acquire the mutex on the calling thread if it's created; otherwise, false.
48+
/// </param>
49+
/// <param name="name">
50+
/// The name, if the mutex is to be shared with other processes; otherwise, null or an empty string.
51+
/// </param>
52+
/// <param name="options">
53+
/// Options for the named mutex. Defaulted options, such as when passing 'options: default' in C#, are
54+
/// 'CurrentUserOnly = true' and 'CurrentSessionOnly = true'. For more information, see 'NamedWaitHandleOptions'. The
55+
/// specified options may affect the namespace for the name, and access to the underlying mutex object.
56+
/// </param>
57+
public Mutex(bool initiallyOwned, string? name, NamedWaitHandleOptions options)
58+
{
59+
CreateMutexCore(initiallyOwned, name, new(options), createdNew: out _);
1960
}
2061

2162
public Mutex(bool initiallyOwned, string? name)
2263
{
23-
CreateMutexCore(initiallyOwned, name, out _);
64+
CreateMutexCore(initiallyOwned, name, options: default, createdNew: out _);
65+
}
66+
67+
/// <summary>
68+
/// Creates a named or unnamed mutex, or opens a named mutex if a mutex with the name already exists.
69+
/// </summary>
70+
/// <param name="name">
71+
/// The name, if the mutex is to be shared with other processes; otherwise, null or an empty string.
72+
/// </param>
73+
/// <param name="options">
74+
/// Options for the named mutex. Defaulted options, such as when passing 'options: default' in C#, are
75+
/// 'CurrentUserOnly = true' and 'CurrentSessionOnly = true'. For more information, see 'NamedWaitHandleOptions'. The
76+
/// specified options may affect the namespace for the name, and access to the underlying mutex object.
77+
/// </param>
78+
public Mutex(string? name, NamedWaitHandleOptions options)
79+
{
80+
CreateMutexCore(initiallyOwned: false, name, new(options), createdNew: out _);
2481
}
2582

2683
public Mutex(bool initiallyOwned)
2784
{
28-
CreateMutexCore(initiallyOwned, null, out _);
85+
CreateMutexCore(initiallyOwned);
2986
}
3087

3188
public Mutex()
3289
{
33-
CreateMutexCore(false, null, out _);
90+
CreateMutexCore(initiallyOwned: false);
3491
}
3592

3693
private Mutex(SafeWaitHandle handle)
3794
{
3895
SafeWaitHandle = handle;
3996
}
4097

98+
/// <summary>
99+
/// Opens an existing named mutex.
100+
/// </summary>
101+
/// <param name="name">The name of the mutex to be shared with other processes.</param>
102+
/// <param name="options">
103+
/// Options for the named mutex. Defaulted options, such as when passing 'options: default' in C#, are
104+
/// 'CurrentUserOnly = true' and 'CurrentSessionOnly = true'. For more information, see 'NamedWaitHandleOptions'. The
105+
/// specified options may affect the namespace for the name, and access to the underlying mutex object.
106+
/// </param>
107+
/// <returns>An object that represents the named mutex.</returns>
108+
public static Mutex OpenExisting(string name, NamedWaitHandleOptions options)
109+
{
110+
OpenExistingResult openExistingResult = OpenExistingWorker(name, new(options), out Mutex? result);
111+
if (openExistingResult != OpenExistingResult.Success)
112+
{
113+
ThrowForOpenExistingFailure(openExistingResult, name);
114+
}
115+
116+
Debug.Assert(result != null, "result should be non-null on success");
117+
return result;
118+
}
119+
41120
public static Mutex OpenExisting(string name)
42121
{
43-
switch (OpenExistingWorker(name, out Mutex? result))
122+
OpenExistingResult openExistingResult = OpenExistingWorker(name, options: default, out Mutex? result);
123+
if (openExistingResult != OpenExistingResult.Success)
124+
{
125+
ThrowForOpenExistingFailure(openExistingResult, name);
126+
}
127+
128+
Debug.Assert(result != null, "result should be non-null on success");
129+
return result;
130+
}
131+
132+
[DoesNotReturn]
133+
private static void ThrowForOpenExistingFailure(OpenExistingResult openExistingResult, string name)
134+
{
135+
Debug.Assert(openExistingResult != OpenExistingResult.Success);
136+
137+
switch (openExistingResult)
44138
{
45139
case OpenExistingResult.NameNotFound:
46140
throw new WaitHandleCannotBeOpenedException();
47141
case OpenExistingResult.NameInvalid:
48142
throw new WaitHandleCannotBeOpenedException(SR.Format(SR.Threading_WaitHandleCannotBeOpenedException_InvalidHandle, name));
49143
case OpenExistingResult.PathNotFound:
50144
throw new DirectoryNotFoundException(SR.Format(SR.IO_PathNotFound_Path, name));
51-
52145
default:
53-
Debug.Assert(result != null, "result should be non-null on success");
54-
return result;
146+
Debug.Assert(openExistingResult == OpenExistingResult.ObjectIncompatibleWithCurrentUserOnly);
147+
throw new WaitHandleCannotBeOpenedException(SR.Format(SR.NamedWaitHandles_ExistingObjectIncompatibleWithCurrentUserOnly, name));
55148
}
56149
}
57150

151+
/// <summary>
152+
/// Tries to open an existing named mutex and returns a value indicating whether it was successful.
153+
/// </summary>
154+
/// <param name="name">The name of the mutex to be shared with other processes.</param>
155+
/// <param name="options">
156+
/// Options for the named mutex. Defaulted options, such as when passing 'options: default' in C#, are
157+
/// 'CurrentUserOnly = true' and 'CurrentSessionOnly = true'. For more information, see 'NamedWaitHandleOptions'. The
158+
/// specified options may affect the namespace for the name, and access to the underlying mutex object.
159+
/// </param>
160+
/// <param name="result">
161+
/// An object that represents the named mutex if the method returns true; otherwise, null.
162+
/// </param>
163+
/// <returns>True if the named mutex was opened successfully; otherwise, false.</returns>
164+
public static bool TryOpenExisting(string name, NamedWaitHandleOptions options, [NotNullWhen(true)] out Mutex? result) =>
165+
OpenExistingWorker(name, new(options), out result!) == OpenExistingResult.Success;
166+
58167
public static bool TryOpenExisting(string name, [NotNullWhen(true)] out Mutex? result) =>
59-
OpenExistingWorker(name, out result!) == OpenExistingResult.Success;
168+
OpenExistingWorker(name, options: default, out result!) == OpenExistingResult.Success;
60169
}
61170
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
6+
namespace System.Threading
7+
{
8+
/// <summary>
9+
/// Represents a set of options for named synchronization objects that are wait handles and can be shared between processes,
10+
/// such as 'Mutex', 'Semaphore', and 'EventWaitHandle'.
11+
/// </summary>
12+
public struct NamedWaitHandleOptions
13+
{
14+
private bool _notCurrentUserOnly;
15+
private bool _notCurrentSessionOnly;
16+
17+
/// <summary>
18+
/// Indicates whether the named synchronization object should be limited in access to the current user.
19+
/// </summary>
20+
/// <remarks>
21+
/// The default value is true.
22+
///
23+
/// If the option is true when creating a named synchronization object, the object is limited in access to the calling
24+
/// user. If the option is true when opening an existing named synchronization object, the object's access controls are
25+
/// verified for the calling user.
26+
///
27+
/// If the option is false when creating a named synchronization object, the object is not limited in access to a user.
28+
///
29+
/// On Unix-like operating systems, each user has namespaces for the object's name that are used when the option is
30+
/// true. These user-scoped namespaces are distinct from user-scoped namespaces for other users, and also distinct from
31+
/// namespaces used when the option is false.
32+
/// </remarks>
33+
public bool CurrentUserOnly
34+
{
35+
get => !_notCurrentUserOnly;
36+
set => _notCurrentUserOnly = !value;
37+
}
38+
39+
/// <summary>
40+
/// Indicates whether the named synchronization object is intended to be used only within the current session.
41+
/// </summary>
42+
/// <remarks>
43+
/// The default value is true.
44+
///
45+
/// Each session has namespaces for the object's name that are used when the option is true. These session-scoped
46+
/// namespaces are distinct from session-scoped namespaces for other sessions, and also distinct from namespaces used
47+
/// when the option is false.
48+
///
49+
/// If the option is true when creating a named synchronization object, the object is limited in scope to the current
50+
/// session, and can't be opened by processes running in different sessions.
51+
///
52+
/// On Windows, a session is a Terminal Services session. On Unix-like operating systems, a session is typically a shell
53+
/// session, where each shell gets its own session in which processes started from the shell run.
54+
/// </remarks>
55+
public bool CurrentSessionOnly
56+
{
57+
get => !_notCurrentSessionOnly;
58+
set => _notCurrentSessionOnly = !value;
59+
}
60+
}
61+
62+
// This is an internal struct used by named wait handle helpers to also track whether the options were specified in APIs
63+
// that were used. Using the constructor indicates WasSpecified=true, and 'default' indicates WasSpecified=false.
64+
internal readonly struct NamedWaitHandleOptionsInternal
65+
{
66+
public const string CurrentSessionPrefix = @"Local\";
67+
public const string AllSessionsPrefix = @"Global\";
68+
69+
private readonly NamedWaitHandleOptions _options;
70+
private readonly bool _wasSpecified;
71+
72+
public NamedWaitHandleOptionsInternal(NamedWaitHandleOptions options)
73+
{
74+
_options = options;
75+
_wasSpecified = true;
76+
}
77+
78+
public bool CurrentUserOnly
79+
{
80+
get
81+
{
82+
Debug.Assert(WasSpecified);
83+
return _options.CurrentUserOnly;
84+
}
85+
}
86+
87+
public bool CurrentSessionOnly
88+
{
89+
get
90+
{
91+
Debug.Assert(WasSpecified);
92+
return _options.CurrentSessionOnly;
93+
}
94+
}
95+
96+
public bool WasSpecified => _wasSpecified;
97+
98+
public string GetNameWithSessionPrefix(string name)
99+
{
100+
Debug.Assert(!string.IsNullOrEmpty(name));
101+
Debug.Assert(WasSpecified);
102+
103+
bool hasPrefix = name.Contains('\\');
104+
if (!hasPrefix)
105+
{
106+
if (CurrentSessionOnly)
107+
{
108+
#if TARGET_WINDOWS
109+
// Services use the global namespace by default, so always include a prefix on Windows
110+
name = CurrentSessionPrefix + name;
111+
#endif
112+
}
113+
else
114+
{
115+
name = AllSessionsPrefix + name;
116+
}
117+
118+
return name;
119+
}
120+
121+
// Verify that the prefix is compatible with the CurrentSessionOnly option. On Windows, when CurrentSessionOnly is
122+
// false, any other prefix is permitted here, as a custom namespace can be created and used with a prefix.
123+
124+
bool incompatible;
125+
if (CurrentSessionOnly)
126+
{
127+
incompatible = !name.StartsWith(CurrentSessionPrefix, StringComparison.Ordinal);
128+
}
129+
else
130+
{
131+
#if TARGET_WINDOWS
132+
incompatible = name.StartsWith(CurrentSessionPrefix, StringComparison.Ordinal);
133+
#else
134+
incompatible = !name.StartsWith(AllSessionsPrefix, StringComparison.Ordinal);
135+
#endif
136+
}
137+
138+
if (incompatible)
139+
{
140+
throw new ArgumentException(SR.Format(SR.NamedWaitHandles_IncompatibleNamePrefix, name), nameof(name));
141+
}
142+
143+
return name;
144+
}
145+
}
146+
}

‎src/libraries/System.Private.CoreLib/src/System/Threading/Semaphore.Unix.cs

+20-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,22 @@ namespace System.Threading
1010
{
1111
public sealed partial class Semaphore
1212
{
13-
private void CreateSemaphoreCore(int initialCount, int maximumCount, string? name, out bool createdNew)
13+
private void CreateSemaphoreCore(int initialCount, int maximumCount)
1414
{
15+
ValidateArguments(initialCount, maximumCount);
16+
SafeWaitHandle = WaitSubsystem.NewSemaphore(initialCount, maximumCount);
17+
}
18+
19+
#pragma warning disable IDE0060 // Unused parameter
20+
private void CreateSemaphoreCore(
21+
int initialCount,
22+
int maximumCount,
23+
string? name,
24+
NamedWaitHandleOptionsInternal options,
25+
out bool createdNew)
26+
{
27+
ValidateArguments(initialCount, maximumCount);
28+
1529
if (name != null)
1630
{
1731
throw new PlatformNotSupportedException(SR.PlatformNotSupported_NamedSynchronizationPrimitives);
@@ -20,8 +34,12 @@ private void CreateSemaphoreCore(int initialCount, int maximumCount, string? nam
2034
SafeWaitHandle = WaitSubsystem.NewSemaphore(initialCount, maximumCount);
2135
createdNew = true;
2236
}
37+
#pragma warning restore IDE0060
2338

24-
private static OpenExistingResult OpenExistingWorker(string name, out Semaphore? result)
39+
private static OpenExistingResult OpenExistingWorker(
40+
string name,
41+
NamedWaitHandleOptionsInternal options,
42+
out Semaphore? result)
2543
{
2644
throw new PlatformNotSupportedException(SR.PlatformNotSupported_NamedSynchronizationPrimitives);
2745
}

‎src/libraries/System.Private.CoreLib/src/System/Threading/Semaphore.Windows.cs

+130-15
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Diagnostics;
54
using System.IO;
65
using System.Runtime.InteropServices;
76
using Microsoft.Win32.SafeHandles;
@@ -12,43 +11,138 @@ public sealed partial class Semaphore
1211
{
1312
private const uint AccessRights = (uint)Interop.Kernel32.MAXIMUM_ALLOWED | Interop.Kernel32.SYNCHRONIZE | Interop.Kernel32.SEMAPHORE_MODIFY_STATE;
1413

14+
#if TARGET_WINDOWS
15+
// Can't use MAXIMUM_ALLOWED in an access control entry (ACE)
16+
private const int CurrentUserOnlyAceRights =
17+
Interop.Kernel32.STANDARD_RIGHTS_REQUIRED | Interop.Kernel32.SYNCHRONIZE | Interop.Kernel32.SEMAPHORE_MODIFY_STATE;
18+
#endif
19+
1520
private Semaphore(SafeWaitHandle handle)
1621
{
1722
SafeWaitHandle = handle;
1823
}
1924

20-
private void CreateSemaphoreCore(int initialCount, int maximumCount, string? name, out bool createdNew)
25+
private void CreateSemaphoreCore(int initialCount, int maximumCount)
2126
{
22-
Debug.Assert(initialCount >= 0);
23-
Debug.Assert(maximumCount >= 1);
24-
Debug.Assert(initialCount <= maximumCount);
27+
ValidateArguments(initialCount, maximumCount);
28+
29+
SafeWaitHandle handle =
30+
Interop.Kernel32.CreateSemaphoreEx(
31+
lpSecurityAttributes: 0,
32+
initialCount,
33+
maximumCount,
34+
name: null,
35+
flags: 0,
36+
AccessRights);
37+
if (handle.IsInvalid)
38+
{
39+
int errorCode = Marshal.GetLastPInvokeError();
40+
handle.SetHandleAsInvalid();
41+
throw Win32Marshal.GetExceptionForWin32Error(errorCode);
42+
}
2543

26-
#if TARGET_UNIX || TARGET_BROWSER || TARGET_WASI
44+
SafeWaitHandle = handle;
45+
}
46+
47+
private unsafe void CreateSemaphoreCore(
48+
int initialCount,
49+
int maximumCount,
50+
string? name,
51+
NamedWaitHandleOptionsInternal options,
52+
out bool createdNew)
53+
{
54+
ValidateArguments(initialCount, maximumCount);
55+
56+
#if !TARGET_WINDOWS
2757
if (name != null)
58+
{
2859
throw new PlatformNotSupportedException(SR.PlatformNotSupported_NamedSynchronizationPrimitives);
60+
}
2961
#endif
30-
SafeWaitHandle myHandle = Interop.Kernel32.CreateSemaphoreEx(IntPtr.Zero, initialCount, maximumCount, name, 0, AccessRights);
3162

32-
int errorCode = Marshal.GetLastPInvokeError();
33-
if (myHandle.IsInvalid)
63+
void* securityAttributesPtr = null;
64+
SafeWaitHandle myHandle;
65+
int errorCode;
66+
#if TARGET_WINDOWS
67+
Thread.CurrentUserSecurityDescriptorInfo securityDescriptorInfo = default;
68+
Interop.Kernel32.SECURITY_ATTRIBUTES securityAttributes = default;
69+
if (!string.IsNullOrEmpty(name) && options.WasSpecified)
3470
{
35-
myHandle.Dispose();
71+
name = options.GetNameWithSessionPrefix(name);
72+
if (options.CurrentUserOnly)
73+
{
74+
securityDescriptorInfo = new(CurrentUserOnlyAceRights);
75+
securityAttributes.nLength = (uint)sizeof(Interop.Kernel32.SECURITY_ATTRIBUTES);
76+
securityAttributes.lpSecurityDescriptor = (void*)securityDescriptorInfo.SecurityDescriptor;
77+
securityAttributesPtr = &securityAttributes;
78+
}
79+
}
3680

37-
if (!string.IsNullOrEmpty(name) && errorCode == Interop.Errors.ERROR_INVALID_HANDLE)
38-
throw new WaitHandleCannotBeOpenedException(
39-
SR.Format(SR.Threading_WaitHandleCannotBeOpenedException_InvalidHandle, name));
81+
using (securityDescriptorInfo)
82+
{
83+
#endif
84+
myHandle =
85+
Interop.Kernel32.CreateSemaphoreEx(
86+
(nint)securityAttributesPtr,
87+
initialCount,
88+
maximumCount,
89+
name,
90+
flags: 0,
91+
AccessRights);
92+
errorCode = Marshal.GetLastPInvokeError();
93+
94+
if (myHandle.IsInvalid)
95+
{
96+
myHandle.SetHandleAsInvalid();
97+
98+
if (!string.IsNullOrEmpty(name) && errorCode == Interop.Errors.ERROR_INVALID_HANDLE)
99+
{
100+
throw new WaitHandleCannotBeOpenedException(
101+
SR.Format(SR.Threading_WaitHandleCannotBeOpenedException_InvalidHandle, name));
102+
}
103+
104+
throw Win32Marshal.GetExceptionForWin32Error(errorCode, name);
105+
}
106+
#if TARGET_WINDOWS
40107

41-
throw Win32Marshal.GetExceptionForLastWin32Error();
108+
if (errorCode == Interop.Errors.ERROR_ALREADY_EXISTS && securityAttributesPtr != null)
109+
{
110+
try
111+
{
112+
if (!Thread.CurrentUserSecurityDescriptorInfo.IsSecurityDescriptorCompatible(
113+
securityDescriptorInfo.TokenUser,
114+
myHandle,
115+
Interop.Kernel32.SEMAPHORE_MODIFY_STATE))
116+
{
117+
throw new WaitHandleCannotBeOpenedException(SR.Format(SR.NamedWaitHandles_ExistingObjectIncompatibleWithCurrentUserOnly, name));
118+
}
119+
}
120+
catch
121+
{
122+
myHandle.Dispose();
123+
throw;
124+
}
125+
}
42126
}
127+
#endif
128+
43129
createdNew = errorCode != Interop.Errors.ERROR_ALREADY_EXISTS;
44130
this.SafeWaitHandle = myHandle;
45131
}
46132

47-
private static OpenExistingResult OpenExistingWorker(string name, out Semaphore? result)
133+
private static OpenExistingResult OpenExistingWorker(
134+
string name,
135+
NamedWaitHandleOptionsInternal options,
136+
out Semaphore? result)
48137
{
49138
#if TARGET_WINDOWS
50139
ArgumentException.ThrowIfNullOrEmpty(name);
51140

141+
if (options.WasSpecified)
142+
{
143+
name = options.GetNameWithSessionPrefix(name);
144+
}
145+
52146
// Pass false to OpenSemaphore to prevent inheritedHandles
53147
SafeWaitHandle myHandle = Interop.Kernel32.OpenSemaphore(AccessRights, false, name);
54148

@@ -65,10 +159,31 @@ private static OpenExistingResult OpenExistingWorker(string name, out Semaphore?
65159
return OpenExistingResult.PathNotFound;
66160
if (errorCode == Interop.Errors.ERROR_INVALID_HANDLE)
67161
return OpenExistingResult.NameInvalid;
162+
68163
// this is for passed through NativeMethods Errors
69164
throw Win32Marshal.GetExceptionForLastWin32Error();
70165
}
71166

167+
if (options.WasSpecified && options.CurrentUserOnly)
168+
{
169+
try
170+
{
171+
if (!Thread.CurrentUserSecurityDescriptorInfo.IsValidSecurityDescriptor(
172+
myHandle,
173+
Interop.Kernel32.SEMAPHORE_MODIFY_STATE))
174+
{
175+
myHandle.Dispose();
176+
result = null;
177+
return OpenExistingResult.ObjectIncompatibleWithCurrentUserOnly;
178+
}
179+
}
180+
catch
181+
{
182+
myHandle.Dispose();
183+
throw;
184+
}
185+
}
186+
72187
result = new Semaphore(myHandle);
73188
return OpenExistingResult.Success;
74189
#else

‎src/libraries/System.Private.CoreLib/src/System/Threading/Semaphore.cs

+118-8
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,121 @@ public sealed partial class Semaphore : WaitHandle
1212
{
1313
// creates a nameless semaphore object
1414
// Win32 only takes maximum count of int.MaxValue
15-
public Semaphore(int initialCount, int maximumCount) : this(initialCount, maximumCount, null) { }
15+
public Semaphore(int initialCount, int maximumCount)
16+
{
17+
CreateSemaphoreCore(initialCount, maximumCount);
18+
}
19+
20+
/// <summary>
21+
/// Creates a named or unnamed semaphore, or opens a named semaphore if a semaphore with the name already exists.
22+
/// </summary>
23+
/// <param name="initialCount">
24+
/// The initial number of requests for the semaphore that can be satisfied concurrently.
25+
/// </param>
26+
/// <param name="maximumCount">
27+
/// The maximum number of requests for the semaphore that can be satisfied concurrently.
28+
/// </param>
29+
/// <param name="name">
30+
/// The name, if the semaphore is to be shared with other processes; otherwise, null or an empty string.
31+
/// </param>
32+
/// <param name="options">
33+
/// Options for the named semaphore. Defaulted options, such as when passing 'options: default' in C#, are
34+
/// 'CurrentUserOnly = true' and 'CurrentSessionOnly = true'. For more information, see 'NamedWaitHandleOptions'. The
35+
/// specified options may affect the namespace for the name, and access to the underlying semaphore object.
36+
/// </param>
37+
public Semaphore(int initialCount, int maximumCount, string? name, NamedWaitHandleOptions options)
38+
{
39+
CreateSemaphoreCore(initialCount, maximumCount, name, new(options), out _);
40+
}
41+
42+
public Semaphore(int initialCount, int maximumCount, string? name)
43+
{
44+
CreateSemaphoreCore(initialCount, maximumCount, name, options: default, out _);
45+
}
1646

17-
public Semaphore(int initialCount, int maximumCount, string? name) :
18-
this(initialCount, maximumCount, name, out _)
47+
/// <summary>
48+
/// Creates a named or unnamed semaphore, or opens a named semaphore if a semaphore with the name already exists.
49+
/// </summary>
50+
/// <param name="initialCount">
51+
/// The initial number of requests for the semaphore that can be satisfied concurrently.
52+
/// </param>
53+
/// <param name="maximumCount">
54+
/// The maximum number of requests for the semaphore that can be satisfied concurrently.
55+
/// </param>
56+
/// <param name="name">
57+
/// The name, if the semaphore is to be shared with other processes; otherwise, null or an empty string.
58+
/// </param>
59+
/// <param name="options">
60+
/// Options for the named semaphore. Defaulted options, such as when passing 'options: default' in C#, are
61+
/// 'CurrentUserOnly = true' and 'CurrentSessionOnly = true'. For more information, see 'NamedWaitHandleOptions'. The
62+
/// specified options may affect the namespace for the name, and access to the underlying semaphore object.
63+
/// </param>
64+
/// <param name="createdNew">
65+
/// True if the semaphore was created; false if an existing named semaphore was opened.
66+
/// </param>
67+
public Semaphore(int initialCount, int maximumCount, string? name, NamedWaitHandleOptions options, out bool createdNew)
1968
{
69+
CreateSemaphoreCore(initialCount, maximumCount, name, new(options), out createdNew);
2070
}
2171

2272
public Semaphore(int initialCount, int maximumCount, string? name, out bool createdNew)
73+
{
74+
CreateSemaphoreCore(initialCount, maximumCount, name, options: default, out createdNew);
75+
}
76+
77+
private static void ValidateArguments(int initialCount, int maximumCount)
2378
{
2479
ArgumentOutOfRangeException.ThrowIfNegative(initialCount);
2580
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maximumCount);
2681

2782
if (initialCount > maximumCount)
83+
{
2884
throw new ArgumentException(SR.Argument_SemaphoreInitialMaximum);
85+
}
86+
}
2987

30-
CreateSemaphoreCore(initialCount, maximumCount, name, out createdNew);
88+
/// <summary>
89+
/// Opens an existing named semaphore.
90+
/// </summary>
91+
/// <param name="name">The name of the semaphore to be shared with other processes.</param>
92+
/// <param name="options">
93+
/// Options for the named semaphore. Defaulted options, such as when passing 'options: default' in C#, are
94+
/// 'CurrentUserOnly = true' and 'CurrentSessionOnly = true'. For more information, see 'NamedWaitHandleOptions'. The
95+
/// specified options may affect the namespace for the name, and access to the underlying semaphore object.
96+
/// </param>
97+
/// <returns>An object that represents the named semaphore.</returns>
98+
[SupportedOSPlatform("windows")]
99+
public static Semaphore OpenExisting(string name, NamedWaitHandleOptions options)
100+
{
101+
OpenExistingResult openExistingResult = OpenExistingWorker(name, new(options), out Semaphore? result);
102+
if (openExistingResult != OpenExistingResult.Success)
103+
{
104+
ThrowForOpenExistingFailure(openExistingResult, name);
105+
}
106+
107+
Debug.Assert(result != null, "result should be non-null on success");
108+
return result;
31109
}
32110

33111
[SupportedOSPlatform("windows")]
34112
public static Semaphore OpenExisting(string name)
35113
{
36-
switch (OpenExistingWorker(name, out Semaphore? result))
114+
OpenExistingResult openExistingResult = OpenExistingWorker(name, options: default, out Semaphore? result);
115+
if (openExistingResult != OpenExistingResult.Success)
116+
{
117+
ThrowForOpenExistingFailure(openExistingResult, name);
118+
}
119+
120+
Debug.Assert(result != null, "result should be non-null on success");
121+
return result;
122+
}
123+
124+
[DoesNotReturn]
125+
private static void ThrowForOpenExistingFailure(OpenExistingResult openExistingResult, string name)
126+
{
127+
Debug.Assert(openExistingResult != OpenExistingResult.Success);
128+
129+
switch (openExistingResult)
37130
{
38131
case OpenExistingResult.NameNotFound:
39132
throw new WaitHandleCannotBeOpenedException();
@@ -42,14 +135,31 @@ public static Semaphore OpenExisting(string name)
42135
case OpenExistingResult.PathNotFound:
43136
throw new IOException(SR.Format(SR.IO_PathNotFound_Path, name));
44137
default:
45-
Debug.Assert(result != null, "result should be non-null on success");
46-
return result;
138+
Debug.Assert(openExistingResult == OpenExistingResult.ObjectIncompatibleWithCurrentUserOnly);
139+
throw new WaitHandleCannotBeOpenedException(SR.Format(SR.NamedWaitHandles_ExistingObjectIncompatibleWithCurrentUserOnly, name));
47140
}
48141
}
49142

143+
/// <summary>
144+
/// Tries to open an existing named semaphore and returns a value indicating whether it was successful.
145+
/// </summary>
146+
/// <param name="name">The name of the semaphore to be shared with other processes.</param>
147+
/// <param name="options">
148+
/// Options for the named semaphore. Defaulted options, such as when passing 'options: default' in C#, are
149+
/// 'CurrentUserOnly = true' and 'CurrentSessionOnly = true'. For more information, see 'NamedWaitHandleOptions'. The
150+
/// specified options may affect the namespace for the name, and access to the underlying semaphore object.
151+
/// </param>
152+
/// <param name="result">
153+
/// An object that represents the named semaphore if the method returns true; otherwise, null.
154+
/// </param>
155+
/// <returns>True if the named semaphore was opened successfully; otherwise, false.</returns>
156+
[SupportedOSPlatform("windows")]
157+
public static bool TryOpenExisting(string name, NamedWaitHandleOptions options, [NotNullWhen(true)] out Semaphore? result) =>
158+
OpenExistingWorker(name, new(options), out result!) == OpenExistingResult.Success;
159+
50160
[SupportedOSPlatform("windows")]
51161
public static bool TryOpenExisting(string name, [NotNullWhen(true)] out Semaphore? result) =>
52-
OpenExistingWorker(name, out result!) == OpenExistingResult.Success;
162+
OpenExistingWorker(name, options: default, out result!) == OpenExistingResult.Success;
53163

54164
public int Release() => ReleaseCore(1);
55165

0 commit comments

Comments
 (0)
Please sign in to comment.