Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[API Proposal]: New overloads for named wait handles that enable creating/opening user-specific synchronization primitives #102682

Open
kouvel opened this issue May 25, 2024 · 7 comments
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.Threading
Milestone

Comments

@kouvel
Copy link
Member

kouvel commented May 25, 2024

Background and motivation

  • Named wait handles (Mutex, Semaphore, and EventWaitHandle) are openable by any user (by default on Windows unless ACL APIs are used, and always on Unixes). In many cases, it would be sufficient to share a named wait handle restricted to the current user. It would be desirable to have a cross-platform way of specifying that a named wait handle should be restricted to the current user. Only named mutexes are supported on Unixes, but similar APIs are also proposed for other named wait handles.
  • It would also be desirable to have a conservative default, or perhaps better, no default at all to get an explicit choice. The current APIs have to be AllUsers for backward compatibility. A consideration may be to eventually deprecate the current APIs.
  • The name prefixes Local\ and Global\ refer to sessions but they are ambiguous and have been mistaken to refer to users

API Proposal

namespace System.Threading
{
    [Flags]
    public enum NamedWaitHandleOptions
    {
        /// <summary>
        /// The named wait handle is restricted to the current user. Unless excluded by other options, processes running as the
        /// current user can open the same named wait handle. Processes running as other users can't open the same named wait
        /// handle.
        /// </summary>
        CurrentUserOnly = 1 << 0,

        /// <summary>
        /// The named wait handle is not restricted to a user. Unless excluded by other options, processes running as any user
        /// can open the same named wait handle.
        /// </summary>
        AllUsers = 1 << 1,

        /// <summary>
        /// The named wait handle is restricted to the current session. Unless excluded by other options, processes running in
        /// the current session can open the same named wait handle. Processes running in other sessions can't open the same
        /// named wait handle.
        /// </summary>
        CurrentSessionOnly = 1 << 2,

        /// <summary>
        /// The named wait handle is not restricted to a session. Unless excluded by other options, processes running in any
        /// session can open the same named wait handle.
        /// </summary>
        AllSessions = 1 << 3
    }

    public sealed partial class Mutex : WaitHandle
    {
        //
        // Proposed
        //

        public Mutex(string? name, NamedWaitHandleOptions options) { }
        public Mutex(bool initiallyOwned, string? name, NamedWaitHandleOptions options) { }
        public Mutex(bool initiallyOwned, string? name, NamedWaitHandleOptions options, out bool createdNew) { }
        public static Mutex OpenExisting(string name, NamedWaitHandleOptions options) { }
        public static bool TryOpenExisting(string name, NamedWaitHandleOptions options, [NotNullWhen(true)] out Mutex? result) { }

        //
        // Existing
        //

        public Mutex(bool initiallyOwned, string? name) { }
        public Mutex(bool initiallyOwned, string? name, out bool createdNew) { }
        public static Mutex OpenExisting(string name) { }
        public static bool TryOpenExisting(string name, [NotNullWhen(true)] out Mutex? result) { }
    }

    public sealed partial class Semaphore : WaitHandle
    {
        //
        // Proposed
        //

        public Semaphore(int initialCount, int maximumCount, string? name, NamedWaitHandleOptions options) { }
        public Semaphore(int initialCount, int maximumCount, string? name, NamedWaitHandleOptions options, out bool createdNew) { }
        [SupportedOSPlatform("windows")]
        public static Semaphore OpenExisting(string name, NamedWaitHandleOptions options) { }
        [SupportedOSPlatform("windows")]
        public static bool TryOpenExisting(string name, NamedWaitHandleOptions options, [NotNullWhen(true)] out Semaphore? result) { }

        //
        // Existing
        //

        public Semaphore(int initialCount, int maximumCount, string? name) { }
        public Semaphore(int initialCount, int maximumCount, string? name, out bool createdNew) { }
        [SupportedOSPlatform("windows")]
        public static Semaphore OpenExisting(string name) { }
        [SupportedOSPlatform("windows")]
        public static bool TryOpenExisting(string name, [NotNullWhen(true)] out Semaphore? result) { }
    }

    public partial class EventWaitHandle : WaitHandle
    {
        //
        // Proposed
        //

        public EventWaitHandle(bool initialState, EventResetMode mode, string? name, NamedWaitHandleOptions options) { }
        public EventWaitHandle(bool initialState, EventResetMode mode, string? name, NamedWaitHandleOptions options, out bool createdNew) { }
        [SupportedOSPlatform("windows")]
        public static EventWaitHandle OpenExisting(string name, NamedWaitHandleOptions options) { }
        [SupportedOSPlatform("windows")]
        public static bool TryOpenExisting(string name, NamedWaitHandleOptions options, [NotNullWhen(true)] out EventWaitHandle? result) { }

        //
        // Existing
        //

        public EventWaitHandle(bool initialState, EventResetMode mode, string? name) { }
        public EventWaitHandle(bool initialState, EventResetMode mode, string? name, out bool createdNew) { }
        [SupportedOSPlatform("windows")]
        public static EventWaitHandle OpenExisting(string name) { }
        [SupportedOSPlatform("windows")]
        public static bool TryOpenExisting(string name, [NotNullWhen(true)] out EventWaitHandle? result) { }
    }
}

Notes about some behaviors

  • Namespaces
    • On all OSes, named wait handles use a different namespace between CurrentSessionOnly and AllSessions, for instance, there can be two different mutexes with the same name, one using CurrentSessionOnly and the other using AllSessions. In addition, when using CurrentSessionOnly, each session uses a different namespace, so for instance, there can be multiple different mutexes using CurrentSessionOnly in different sessions.
    • On Windows, named wait handles don't use a different namespace between CurrentUserOnly and AllUsers
    • On Unixes, named wait handles use a different namespace between CurrentUserOnly and AllUsers. In addition, when using CurrentUserOnly, each user uses a different namespace.
  • Exception conditions
    • ArgumentException
      • The options argument does not specify CurrentUserOnly or AllUsers
      • The name argument doesn't have a session prefix, and the options argument does not specify CurrentSessionOnly or AllSessions
      • The options argument specifies both CurrentUserOnly and AllUsers
      • The options argument specifies both CurrentSessionOnly and AllSessions
      • The name argument has a session prefix and the options argument specifies CurrentSessionOnly or AllSessions, but the two don't match
    • IOException
      • On Unixes, named mutexes use the file system for some shared memory. When using CurrentUserOnly, the relevant files would also be restricted to the current user. If for some reason that would not be possible, an IOException would be thrown.
  • Security
    • A named wait handle instance that is passed around may be operable by code that runs with lower privileges, so care should be taken when sharing such instances

API Usage

        var mutex = new Mutex("MyMutex", NamedWaitHandleOptions.CurrentUserOnly | NamedWaitHandleOptions.AllSessions);
        var mutex = new Mutex(@"Global\MyMutex", NamedWaitHandleOptions.CurrentUserOnly);
        var mutex = Mutex.OpenExisting("MyMutex", NamedWaitHandleOptions.CurrentUserOnly | NamedWaitHandleOptions.AllUsers);
        if (Mutex.TryOpenExisting("MyMutex", NamedWaitHandleOptions.CurrentUserOnly | NamedWaitHandleOptions.AllUsers, out Mutex mutex))
        {
        }

Similarly for other enum values and other named wait handles.

Alternative Designs

  • Using a new prefix instead of a new argument. For instance, a mutex name could be @"Global\User\MyMutex" to specify that it should be restricted to the current user and available to all sessions. Aside from what was covered in the background/motivation section regarding defaults and ambiguity, an argument is more clear and more purposed. This is also similar to the existing PipeOptions.CurrentUserOnly for named pipes.
  • Not including the session-scoping in the enum (covered in background/motivation)
  • Adding new APIs only for Mutex (covered in background/motivation)
  • Having separate enums for each type of named wait handle such as NamedMutexOptions. It seems unlikely that there would be new type-specific options for the different types, and if there are, they could be added in new arguments.

Risks

  • In some (maybe odd?) setups on Unixes, /tmp may not have the sticky bit, or its file system may not support some things (such as if it's a mounted share), or something like that, in which case CurrentUserOnly may not work
@kouvel kouvel added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label May 25, 2024
@kouvel kouvel self-assigned this May 25, 2024
@kouvel kouvel added this to the 9.0.0 milestone May 25, 2024
Copy link
Contributor

Tagging subscribers to this area: @mangod9
See info in area-owners.md if you want to be subscribed.

@jkotas
Copy link
Member

jkotas commented May 25, 2024

Do we have any customers asking for emulation of named Windows synchronization APIs beyond Mutexes on non-Windows platforms?

Only named mutexes are supported on Unixes, but similar APIs are also proposed for other named wait handles.

Are the reliability guarantees of the Unix emulation of the other named wait handles going to match Windows in case of abnormal process termination?

@kouvel
Copy link
Member Author

kouvel commented May 28, 2024

Do we have any customers asking for emulation of named Windows synchronization APIs beyond Mutexes on non-Windows platforms?

Not that I've heard of.

Are the reliability guarantees of the Unix emulation of the other named wait handles going to match Windows in case of abnormal process termination?

It might be feasible on Linux where pthread process-shared robust mutexes and process-shared conditions are available. On OSX/BSD it may be more challenging or may need the use of some other primitive.

@jkotas
Copy link
Member

jkotas commented May 28, 2024

Not that I've heard of.

I do not think we should be expanding the set of emulated named synchronization primitives outside Windows then. It is very niche functionality and fixing the security and reliability problems in the existing named mutex costed us a lot over time.

@kouvel
Copy link
Member Author

kouvel commented May 28, 2024

I do not think we should be expanding the set of emulated named synchronization primitives outside Windows then.

I'm not suggesting expanding the implementations of named events and semaphores into Unixes, I'm only suggesting expanding their APIs similarly, mostly for Windows, as even there it could still be beneficial to have an easy and explicit way of specifying the user scope of a named wait handle.

@kouvel kouvel added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation labels May 29, 2024
@terrajobst terrajobst added the blocking Marks issues that we want to fast track in order to unblock other important work label Jun 11, 2024
@bartonjs
Copy link
Member

bartonjs commented Jun 18, 2024

Video

  • We changed the flags enum to a struct
  • The default value for the struct will be current-user in current-scope. The property names that were chosen mean they have to default to true.
  • We've marked all the existing constructors / OpenExisting methods as [Obsolete]. Right now SYSLIB0057 is the next available diagnostic ID, but use whatever is next in Obsoletions.cs when creating the PR.
  • It's felt that current-user+current-scope is the best default going forward, but that we shouldn't change the behavior for the existing members for .NET 9. We may revise this for .NET 10, since there will have been an intervening release with the obsoletion.
namespace System.Threading
{
    public struct NamedWaitHandleOptions
    {
        private bool _allUsers;
        private bool _allSessions;

        // Note that for default(NamedWaitHandleOptions), both of these properties will return `true`.

        public bool CurrentUserOnly { get => !_allUsers; set => _allUsers = !value; }
        public bool CurrentSessionOnly { get => !_allSessions; set => _allSessions = !value; }
    }

    public sealed partial class Mutex : WaitHandle
    {
        //
        // Proposed
        //

        public Mutex(string? name, NamedWaitHandleOptions options) { }
        public Mutex(bool initiallyOwned, string? name, NamedWaitHandleOptions options) { }
        public Mutex(bool initiallyOwned, string? name, NamedWaitHandleOptions options, out bool createdNew) { }
        public static Mutex OpenExisting(string name, NamedWaitHandleOptions options) { }
        public static bool TryOpenExisting(string name, NamedWaitHandleOptions options, [NotNullWhen(true)] out Mutex? result) { }

        //
        // Existing
        //
        [Obsolete("some message", DiagnosticId = "SYSLIB0057")]
        public Mutex(bool initiallyOwned, string? name) { }
        [Obsolete("some message", DiagnosticId = "SYSLIB0057")]
        public Mutex(bool initiallyOwned, string? name, out bool createdNew) { }
        [Obsolete("some message", DiagnosticId = "SYSLIB0057")]
        public static Mutex OpenExisting(string name) { }
        [Obsolete("some message", DiagnosticId = "SYSLIB0057")]
        public static bool TryOpenExisting(string name, [NotNullWhen(true)] out Mutex? result) { }
    }

    public sealed partial class Semaphore : WaitHandle
    {
        //
        // Proposed
        //

        public Semaphore(int initialCount, int maximumCount, string? name, NamedWaitHandleOptions options) { }
        public Semaphore(int initialCount, int maximumCount, string? name, NamedWaitHandleOptions options, out bool createdNew) { }
        [SupportedOSPlatform("windows")]
        public static Semaphore OpenExisting(string name, NamedWaitHandleOptions options) { }
        [SupportedOSPlatform("windows")]
        public static bool TryOpenExisting(string name, NamedWaitHandleOptions options, [NotNullWhen(true)] out Semaphore? result) { }

        //
        // Existing
        //

        [Obsolete("some message", DiagnosticId = "SYSLIB0057")]
        public Semaphore(int initialCount, int maximumCount, string? name) { }
        [Obsolete("some message", DiagnosticId = "SYSLIB0057")]
        public Semaphore(int initialCount, int maximumCount, string? name, out bool createdNew) { }
        [SupportedOSPlatform("windows")]
        [Obsolete("some message", DiagnosticId = "SYSLIB0057")]
        public static Semaphore OpenExisting(string name) { }
        [SupportedOSPlatform("windows")]
        [Obsolete("some message", DiagnosticId = "SYSLIB0057")]
        public static bool TryOpenExisting(string name, [NotNullWhen(true)] out Semaphore? result) { }
    }

    public partial class EventWaitHandle : WaitHandle
    {
        //
        // Proposed
        //

        public EventWaitHandle(bool initialState, EventResetMode mode, string? name, NamedWaitHandleOptions options) { }
        public EventWaitHandle(bool initialState, EventResetMode mode, string? name, NamedWaitHandleOptions options, out bool createdNew) { }
        [SupportedOSPlatform("windows")]
        public static EventWaitHandle OpenExisting(string name, NamedWaitHandleOptions options) { }
        [SupportedOSPlatform("windows")]
        public static bool TryOpenExisting(string name, NamedWaitHandleOptions options, [NotNullWhen(true)] out EventWaitHandle? result) { }

        //
        // Existing
        //

        [Obsolete("some message", DiagnosticId = "SYSLIB0057")]
        public EventWaitHandle(bool initialState, EventResetMode mode, string? name) { }
        [Obsolete("some message", DiagnosticId = "SYSLIB0057")]
        public EventWaitHandle(bool initialState, EventResetMode mode, string? name, out bool createdNew) { }
        [SupportedOSPlatform("windows")]
        [Obsolete("some message", DiagnosticId = "SYSLIB0057")]
        public static EventWaitHandle OpenExisting(string name) { }
        [SupportedOSPlatform("windows")]
        [Obsolete("some message", DiagnosticId = "SYSLIB0057")]
        public static bool TryOpenExisting(string name, [NotNullWhen(true)] out EventWaitHandle? result) { }
    }
}

@bartonjs bartonjs added api-approved API was approved in API review, it can be implemented and removed blocking Marks issues that we want to fast track in order to unblock other important work api-ready-for-review API is ready for review, it is NOT ready for implementation labels Jun 18, 2024
@KalleOlaviNiemitalo

This comment was marked as resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-approved API was approved in API review, it can be implemented area-System.Threading
Projects
None yet
Development

No branches or pull requests

5 participants