Comment comprendre l'isolation des sessions Windows — Session 0, RDP et exécution simultanée de plusieurs utilisateurs

· · Session 0, Isolation des sessions, RDP, Bureau à distance, Services Windows, Multi-utilisateur, Windows, .NET, C#, Architecture, Conseil technique

« Une boîte de dialogue levée depuis un service est invisible pour tout le monde. » « Dès qu’une personne s’est connectée via RDP, l’écran de la machine physique a basculé sur l’écran de connexion. » « La protection d’instance unique était censée empêcher les doubles lancements, mais se connecter sous un autre utilisateur a joyeusement démarré une seconde copie. » Les missions de conseil sur des applications métier font remonter sans cesse ces symptômes, et derrière chacun d’eux se cache la même lacune : une compréhension incomplète du concept de « session » Windows. Les sessions traversent aussi bien la conception des services, l’exploitation du bureau à distance, que la protection d’instance unique des objets nommés, et pourtant il est rare de voir ce sujet expliqué de façon systématique en un seul endroit.

Cet article organise, d’un point de vue pratique, les raisons de l’introduction de l’isolation Session 0, le comportement des sessions lors d’une connexion via RDP, la manière dont les objets nommés sont isolés par session, ainsi que les erreurs de conception courantes dans les environnements de PC partagés et RDS (Remote Desktop Services).

1. L’essentiel d’abord

  • Les services s’exécutent dans une session dédiée appelée Session 0, isolée de la session interactive de tout utilisateur connecté. Un service ne peut pas afficher directement de boîte de dialogue, et même s’il essaie, l’utilisateur ne la voit jamais.1
  • La solution officiellement recommandée pour un service ayant besoin d’une interface utilisateur consiste à scinder l’UI en un processus séparé (vivant dans la session de l’utilisateur) et à dialoguer avec lui via IPC. Lancer un programme d’interface utilisateur en tant qu’utilisateur interactif via le Planificateur de tâches est une autre option envisageable.2
  • Windows Server (RDS) est conçu autour de plusieurs sessions simultanées, mais les systèmes d’exploitation client comme Windows 10/11 Pro sont fondamentalement mono-session. À l’exception de l’édition spéciale « Enterprise multi-session » conçue pour Azure Virtual Desktop, les installations client ordinaires ne sont pas conçues pour héberger plusieurs sessions interactives simultanées.3
  • Les objets noyau nommés (mutex, événements, sémaphores, etc.) possèdent chacun un espace de noms propre à chaque session. Si vous voulez réduire un objet à une seule instance sur l’ensemble de la machine et à travers les sessions, vous devez explicitement préfixer le nom avec Global\ — sinon, la protection d’instance unique ne se comportera pas comme prévu. Les tubes nommés (named pipes) échappent à ce mécanisme : par défaut, ils utilisent un espace de noms distinct, accessible à l’échelle de la machine (et à distance, si le service serveur est actif) ; si vous devez distinguer par session ou par utilisateur, vous devez intégrer quelque chose comme l’ID de session dans le nom du tube lui-même.45
  • Vous pouvez déterminer le type de la session courante via la variable d’environnement SESSIONNAME ou GetSystemMetrics(SM_REMOTESESSION). Il existe toutefois des cas — par exemple lorsque RemoteFX vGPU est utilisé — où la détection s’écarte de son comportement voulu ; ne vous y fiez donc pas aveuglément.67
  • La question de savoir si le comportement PC partagé/RDS doit figurer dans vos exigences est à confirmer dès le tout début de la phase de conception. Les collisions de chemins de fichiers temporaires, l’AppData mélangé et l’accès simultané aux périphériques/dongles sont les incidents classiques qui éclatent dès qu’une application conçue pour un seul PC autonome est déployée sur RDS (voir le chapitre 7).

2. Ce qu’est l’isolation Session 0 — et pourquoi la séparation d’avec les sessions utilisateur a eu lieu

Avant Windows Vista / Windows Server 2008, un service et le premier utilisateur à se connecter partageaient la même Session 0. Les processus au sein d’une même session peuvent échanger des messages de fenêtre indépendamment de leurs niveaux de privilège respectifs. Un processus à privilèges standard pouvait envoyer un message forgé à une fenêtre appartenant à un service s’exécutant en tant que SYSTEM, détourner le traitement des messages du service, et élever ses privilèges — toute cette classe d’élévation de privilèges, qui exploite des failles dans le traitement des messages de fenêtre, a été signalée de façon répétée tout au long des années 2000, et de fait, plusieurs vulnérabilités d’élévation de privilèges exploitant des failles du traitement des messages de fenêtre du noyau Windows (win32k.sys) ont été corrigées au fil des ans.8

Ce schéma a changé à partir de Windows Vista. La Session 0 est devenue dédiée exclusivement aux services et aux applications non liées à une session utilisateur interactive, et le premier utilisateur à se connecter se voit désormais attribuer la Session 1, le suivant la Session 2, et ainsi de suite — chacun dans une session différente de celle du service. La Session 0 ne prend pas en charge le fonctionnement interactif, et les services ne peuvent ni envoyer de messages aux applications, ni les applications en envoyer aux services. Un service ne peut pas non plus afficher directement des éléments d’UI tels qu’une boîte de dialogue. Avec ce changement, le chemin même qui permettait à un processus à privilèges standard d’envoyer des messages directement dans la fenêtre d’un service a été structurellement fermé.1

Autrement dit, l’isolation Session 0 n’est pas simplement une contrainte opérationnelle du type « les services sont des processus d’arrière-plan, donc ils n’ont pas d’écran » — c’est une conception de sécurité qui bloque structurellement l’échange de messages de fenêtre à travers une frontière de privilèges. La seule voie qui reste à un service pour afficher quoi que ce soit à l’écran est la fonction WTSSendMessage, une API qui fait apparaître une simple boîte de message dans une autre session, mais qui ne peut pas être utilisée pour des boîtes de dialogue complexes ou une interaction bidirectionnelle.1

3. Les services ne peuvent pas afficher d’UI — contraintes pratiques et schémas de contournement

La conséquence pratique de l’isolation Session 0 est simple. Si un service appelle MessageBox.Show ou tente d’afficher un formulaire, rien n’apparaît sur l’écran de l’utilisateur connecté. Pire, c’est une cause classique de blocage complet du processus, qui attend indéfiniment un bouton OK que personne ne pourra jamais cliquer. Lors du portage d’un ancien code (quelque chose qui fonctionnait autrefois comme un « service interactif » à l’époque de Windows XP) vers un service, vérifiez toujours qu’aucun appel d’affichage d’UI n’a été laissé en place.

Il existe deux solutions officiellement documentées.2

  • Scinder l’UI en un processus séparé (vivant dans la session de l’utilisateur interactif) et communiquer via IPC. Le service effectue le travail et transmet les résultats ou les demandes de saisie au processus d’UI. Le processus d’UI les présente à l’utilisateur sous forme d’icône dans la zone de notification ou de boîte de dialogue, et renvoie le résultat de l’opération au service. Si vous utilisez un mécanisme IPC tel que les tubes nommés, vous devez concevoir les listes de contrôle d’accès (ACL) de manière à ce que rien ne soit exposé involontairement sur le réseau. Dans les environnements où plusieurs utilisateurs sont connectés côte à côte, la recommandation est de distinguer les sessions en intégrant, par exemple, l’ID de session dans le nom du tube.2 Pour le choix du mécanisme IPC, voir l’article compagnon publié le même jour, « Un tableau de décision pour la communication inter-processus Windows ». La façon de construire le service lui-même, ainsi que les points de conception opérationnelle tels que le type de démarrage, le compte d’exécution et les options de récupération, sont couverts dans « Comment construire un service Windows ».
  • Lancer un programme en tant qu’utilisateur interactif via le Planificateur de tâches. Plutôt que de le maintenir résident comme service, cette configuration lance un programme doté d’une UI sous les privilèges et la session propres de l’utilisateur connecté. Un point de vigilance ici : une « tâche enregistrée sous SYSTEM » ne doit jamais être celle qui affiche l’UI. Le compte SYSTEM n’a aucun droit d’ouverture de session interactive, et un utilisateur ne peut ni voir ni interagir avec un programme ou une tâche s’exécutant sous les privilèges SYSTEM.9 Le modèle de tâche élevée est conçu pour qu’une tâche SYSTEM — dont le descripteur de sécurité permet à un utilisateur standard de la démarrer — ne prenne en charge que les opérations nécessitant des privilèges administrateur, tandis que l’UI elle-même s’exécute dans un programme séparé sous les privilèges et la session propres de l’utilisateur connecté (ou en tant que la même opération enregistrée comme tâche sous le compte de l’utilisateur) ; c’est toute la prémisse de cette répartition des rôles.10 Gardez la tâche SYSTEM strictement en tant que « courtier sans interface », et laissez toujours l’UI visible s’exécuter dans un processus de la propre session de l’utilisateur.

Quelle que soit l’approche retenue, l’essentiel est de construire dès le premier jour, dans la conception, le principe « le service reste en coulisses, l’UI vit dans un processus séparé ». Ce que l’on observe couramment en pratique, c’est que tenter de l’ajouter après coup se heurte à du code déjà profondément dépendant de l’UI dans le service, ce qui fait grimper le coût de leur séparation.

4. Comment se comportent les sessions via RDP — la différence entre RDS et un système client

La confusion du type « c’est le même RDP sur le serveur et sur le client, alors pourquoi le comportement diffère-t-il ? » provient de la présence ou non du rôle Remote Desktop Services (RDS).

Windows Server avec le rôle RDS est conçu autour de plusieurs utilisateurs se connectant simultanément à un même serveur, chacun travaillant dans une session indépendante avec son propre bureau et ses propres applications — une configuration bâtie dès le départ sur plusieurs sessions simultanées.11 Sous RDS, les connexions d’administration RDP classiques (pour les administrateurs) prennent en charge jusqu’à deux sessions sans nécessiter de CAL, mais dépasser ce nombre, ou laisser des utilisateurs ordinaires se connecter simultanément, exige de déployer le rôle RD Session Host ainsi que des licences RDS CAL (Client Access License) appropriées.12 Les CAL existent selon deux modèles — par périphérique et par utilisateur — et les détails de licence doivent être confirmés pour chaque environnement.13

De son côté, un système d’exploitation client ordinaire tel que Windows 10/11 Pro ou Enterprise ne comporte pas le rôle RDS et est, en principe, mono-session. Il existe une édition spéciale de système client appelée « Windows Enterprise multi-session » capable de maintenir plusieurs sessions interactives à la fois, mais elle est proposée exclusivement pour Azure Virtual Desktop — les installations client ordinaires, distribuées en local, ne sont pas conçues pour prendre en charge plusieurs sessions utilisateur simultanées.3 En pratique, conformément à l’expérience familière « je me suis connecté en RDP au poste d’un collègue et l’écran de son bureau a basculé sur l’écran de verrouillage », il est plus sûr de considérer que sur les systèmes client, une session de console physique (quelqu’un opérant la machine localement) et une connexion à distance ne peuvent en principe pas coexister au même moment.

Voici un résumé des différences qui comptent pour la conception d’applications métier.

Aspect Windows Server + RDS Système client (Windows 10/11 Pro, etc.)
Sessions simultanées Plusieurs utilisateurs peuvent se connecter simultanément11 Fondamentalement mono-session. Plusieurs sessions interactives ne sont pas prises en charge en dehors de l’édition multi-session réservée à AVD3
Licence Nécessite RD Session Host + des licences RDS CAL (au-delà des deux connexions d’administration)12 La fonctionnalité Bureau à distance elle-même est intégrée à l’OS, mais les exigences de licence et les éditions autorisées doivent être vérifiées séparément
Usage typique Exécution d’applications métier sur des terminaux partagés, environnements client léger Maintenance à distance d’un PC personnel, accès en télétravail

Ne pas tenir compte de cette différence — en supposant que « ça fonctionnait sur le système client de ma machine de développement, donc ça devrait fonctionner de la même façon sur l’environnement RDS du serveur partagé » — vous fera passer à côté des bugs propres à la concurrence multi-utilisateur abordés au chapitre 7. Inversement, si vous ne prévoyez jamais qu’un fonctionnement mono-session sur un système client, il n’est pas nécessaire de payer d’emblée le coût de conception d’un support PC partagé. C’est une décision qui doit être prise tôt, lors de la définition des exigences (chapitre 8).

5. Déterminer la session courante

Il existe de nombreuses situations où l’on souhaite savoir dans quelle session s’exécute actuellement son processus — par exemple pour supprimer un effet de rendu coûteux pendant un traitement en arrière-plan, ou désactiver une fonctionnalité gourmande en bande passante sur une session distante. Il existe plusieurs façons de le déterminer.

  • La variable d’environnement SESSIONNAME : définie sur Console pour la session console, ou sur un nom commençant par RDP-Tcp# pour une connexion RDP. C’est la même information que celle affichée dans la colonne SESSIONNAME listée par la commande qwinsta.14 Utilisable pour une vérification rapide, mais comme ce n’est pas une API officielle, il est plus sûr de ne pas trop s’y fier.
  • GetSystemMetrics(SM_REMOTESESSION) : une API Win32 qui indique si l’on se trouve dans une session distante. Elle renvoie une valeur non nulle si le processus appelant est associé à une session client Terminal Services. Il y a toutefois une mise en garde : à partir de Windows 8/Server 2012, sur les sessions distantes utilisant RemoteFX vGPU, cette fonction signale à tort une session distante comme locale. Dans ce cas, la solution de contournement documentée consiste à comparer la valeur GlassSessionId du registre avec l’ID de session actuel.67
  • SystemParameters.IsRemoteSession de .NET (WPF) : expose le même contrôle que celui effectué par GetSystemMetrics(SM_REMOTESESSION), sous forme de propriété lisible depuis une application WPF.15 Depuis WinForms ou une application console, il faudrait l’appeler directement via P/Invoke.
  • WTSGetActiveConsoleSessionId : une API qui récupère l’ID de la session connectée à la console physique. La comparer à votre propre ID de session permet de déterminer « suis-je en train de m’exécuter sur la console physique ? ». Notez que lorsque personne n’est connecté à la console physique (c’est-à-dire pendant une transition), elle renvoie 0xFFFFFFFF.16

N’utilisez pas WTSGetActiveConsoleSessionId pour identifier des sessions dans un environnement RDS. Fidèle à son nom, tout ce qu’elle renvoie est « la session connectée à la console physique » — un utilisateur connecté via RDP n’est pas couvert.16 Si un service a besoin de savoir dans quelle session afficher son UI compagnon, cette API suffit lorsque vous ne prévoyez qu’un utilisateur connecté à la console, mais dans un environnement RDS où plusieurs utilisateurs RDP peuvent être connectés simultanément, vous devriez plutôt énumérer les sessions en cours d’exécution avec WTSEnumerateSessions17, ou simplement utiliser l’ID de session transmis par une notification de changement de session (WM_WTSSESSION_CHANGE). Cette confusion est une cause classique, difficile à repérer, de bugs où l’UI de notification n’apparaît tout simplement pas — ou apparaît dans la mauvaise session — spécifiquement pour les utilisateurs connectés via RDP. Gardez fermement cette distinction à l’esprit lorsque vous décidez dans quelle session lancer le processus d’UI pour la configuration IPC décrite au chapitre 3.

6. Isolation par session des objets nommés

Les objets noyau nommés — mutex, événements, sémaphores, minuteurs pouvant être attendus, objets de mappage de fichiers — possèdent chacun un espace de noms indépendant par session. Même si vous créez un mutex nommé MyAppMutex dans une session, tenter d’ouvrir un objet du même nom depuis une session différente crée un objet entièrement nouveau et distinct. Pour les processus qui s’exécutent principalement en Session 0, comme les services, ou pour les configurations client/serveur qui veulent partager un objet entre plusieurs sessions, vous pouvez le placer explicitement dans l’espace de noms global en préfixant le nom avec Global\. À l’inverse, le préfixer avec Local\ le place explicitement dans l’espace de noms de votre propre session.4

C’est là que cela compte vraiment en pratique : le mutex utilisé pour la protection d’instance unique.

  • Si tout ce que vous voulez est « empêcher le même utilisateur de lancer deux copies de l’application dans la même session », le nom de mutex par défaut (sans préfixe) suffit. L’espace de noms par session entre automatiquement en jeu, si bien que la session d’un utilisateur différent est traitée comme un mutex différent.
  • Si vous voulez « même avec plusieurs utilisateurs connectés simultanément dans un environnement RDS, limiter l’application à une seule instance à l’échelle de la machine », vous ne pouvez atteindre cet objectif qu’en utilisant explicitement Global\. Laissé par défaut, chaque utilisateur finit par pouvoir lancer sa propre copie — un bug qui se traduit par « la protection d’instance unique était censée fonctionner, mais plusieurs instances ont quand même été lancées ».

Notez également qu’un processus en dehors de la Session 0 a besoin du privilège SeCreateGlobalPrivilege pour créer un nouvel objet de mappage de fichiers ou un objet de lien symbolique dans l’espace de noms global (ouvrir simplement un objet existant ne le requiert pas). Cette vérification de privilège ne s’applique qu’aux mappages de fichiers et aux liens symboliques — elle n’est pas imposée à la création de mutex ou d’événements — mais elle est bonne à retenir pour toute conception utilisant de la mémoire partagée (un fichier mappé en mémoire) dans l’espace de noms global.4

Il existe aussi un principe général pour les applications client/serveur : la recommandation est de ne pas assimiler « une connexion depuis une machine » à « une session utilisateur ». Étant donné que plusieurs sessions (les connexions RDP de plusieurs utilisateurs) peuvent être établies simultanément depuis la même machine, il est recommandé côté serveur d’identifier les sessions à l’aide, par exemple, de ProcessIdToSessionId, et de provisionner un canal de communication séparé par session.18

7. Erreurs de conception courantes sur les PC partagés et les environnements RDS

Il existe une poignée de schémas d’échec récurrents qui éclatent dès qu’une application conçue en pensant uniquement à un usage mono-PC, en console uniquement, est déployée sur un PC partagé ou un environnement RDS.

  • Collisions de chemins de fichiers temporaires. Par défaut, RDS crée un dossier temporaire séparé par session (sous le profil de l’utilisateur, avec l’ID de session intégré dans le nom).19 Autrement dit, si votre application utilise simplement la variable d’environnement %TEMP%, elle bénéficie gratuitement de cette isolation. Les ennuis commencent lorsque l’application code en dur un chemin fixe tel que C:\Work\temp au lieu d’utiliser %TEMP%. Si plusieurs sessions du même compte utilisateur, ou plusieurs utilisateurs, écrivent simultanément dans le même chemin fixe, ils se disputeront bien sûr le fichier.
  • AppData mélangé. L’endroit où enregistrer les paramètres utilisateur, les caches et les journaux doit être décidé avec une bonne compréhension du mécanisme de profil utilisateur de Windows — local contre itinérant, et quand utiliser %LOCALAPPDATA% plutôt que %APPDATA%. Sur un PC partagé, une conception bâclée sur ce point a tendance à se manifester par « mon écran a changé à cause des paramètres de quelqu’un d’autre » ou « le journal a été écrasé ». Voir « Une introduction aux profils utilisateur Windows » pour plus de détails.
  • Supposer un accès simultané aux périphériques, dongles et ports série. Accéder à un périphérique local côté client ou à un port série depuis une session RDS passe par un mécanisme de redirection. Si vous essayez de laisser plusieurs sessions utiliser simultanément une application qui dépend d’un dongle USB physique ou d’un dispositif de communication série, vous rencontrerez des problèmes où le matériel lui-même peut ne pas prendre en charge l’accès concurrent en premier lieu, ou où la visibilité dépend de la configuration de redirection et peut être présente ou non. La façon dont un périphérique attaché côté client apparaît depuis une session côté serveur dépend de la configuration de redirection, ce qui doit donc être confirmé au stade de la conception.20
  • Imprimantes et lecteurs codés en dur. Coder en dur un nom d’imprimante par défaut ou une lettre de lecteur dans une application se casse facilement dans un environnement PC partagé où l’imprimante par défaut redirigée ou le lecteur mappé diffère selon l’utilisateur. Il est plus sûr de rechercher le nom de l’imprimante à l’exécution et, dans la mesure du possible, de traiter les lecteurs comme des chemins UNC.
  • Mauvaise lecture de HKCU / du registre par utilisateur. Il convient également de noter que HKEY_CURRENT_USER est lié à l’utilisateur connecté, pas à la session. Dans une configuration où le même utilisateur possède plusieurs sessions (par exemple, en partageant le même compte sur RDS), les valeurs HKCU sont partagées entre toutes les sessions de cet utilisateur — stocker dans HKCU un état d’exécution que l’on souhaite en réalité isolé par session provoquera donc une fuite involontaire vers les autres sessions. Les informations nécessitant une isolation par session devraient plutôt utiliser les espaces de noms par session du chapitre 6, ou un chemin de fichier intégrant l’ID de session.

Tous ces cas sont le genre de bug qu’« on ne remarquerait jamais sur un seul PC autonome » et qui « ne se manifeste que lorsque plusieurs utilisateurs utilisent réellement l’application en même temps » — le scénario typique est donc qu’un environnement de test constitué uniquement d’une seule machine de développement autonome ne le reproduit jamais, et qu’il n’apparaît pour la première fois que sur un PC partagé ou un environnement RDS de production.

8. Tableau de décision — le support « PC partagé/RDS » doit-il faire partie de vos exigences ?

La question de savoir si le comportement PC partagé/RDS fait partie de vos exigences est à confirmer lors de la définition des exigences, avant le début de l’implémentation. Une fois que vous avez décidé de l’inclure, utilisez les points suivants comme liste de contrôle.

Aspect Correct de supposer un PC autonome / console uniquement Nécessite un support PC partagé/RDS (multi-utilisateur simultané)
Fichiers temporaires / paramètres Un chemin fixe cause peu de tort réel Rendre obligatoires des chemins par utilisateur/par session tels que %TEMP% / %LOCALAPPDATA% (chapitre 7)
Objets nommés Correct de laisser la valeur par défaut (espace de noms de session) Si vous avez besoin d’« une instance à l’échelle de la machine », utilisez explicitement Global\. Si « une par session » suffit, laissez la valeur par défaut telle quelle. Si vous voulez « une par utilisateur, couvrant les sessions de cet utilisateur », combinez Global\ avec le SID de l’utilisateur (chapitre 6 ; voir aussi l’article sur la protection d’instance unique)
Périphériques / dongles Connexion locale directe et simple Concevoir en tenant compte du fait que l’accès passe par la redirection et que l’accès concurrent peut ne pas être possible
UI et services Facile de tout garder sur un seul bureau Scinder en un processus séparé + IPC, à la lumière de l’isolation Session 0 (chapitre 3)
Licence / facturation Simple à compter par nombre de machines Confirmer le concept du nombre de sessions simultanées et si des licences RDS CAL sont requises, lors de la définition des exigences13

L’axe de la décision est simple : mettez-vous d’accord avec le client tôt dans le développement pour savoir si « cette application doit simplement fonctionner de façon autonome sur le PC d’une seule personne » ou si « elle pourrait être utilisée par plusieurs personnes à la fois sur un terminal partagé ou un environnement RDS ». Laissez ce point ambigu pendant que l’implémentation avance, et vous finirez par devoir reconstruire chacun des points ci-dessus depuis zéro.

9. Exemple d’implémentation — utiliser judicieusement la détection de session et les espaces de noms

Voici comment les méthodes de détection et les choix de nommage de mutex des chapitres 5 et 6 se combinent dans du code .NET.

using System.Runtime.InteropServices;

internal static class SessionInfo
{
    [DllImport("user32.dll")]
    private static extern int GetSystemMetrics(int nIndex);

    private const int SM_REMOTESESSION = 0x1000;

    // Pour une application WPF, System.Windows.SystemParameters.IsRemoteSession renvoie le même résultat
    public static bool IsRemoteSession() => GetSystemMetrics(SM_REMOTESESSION) != 0;

    // Vérification rapide et sommaire. Ce n'est pas une API officielle, à réserver
    // aux journaux/diagnostics plutôt qu'à un branchement important
    public static string? SessionName() =>
        Environment.GetEnvironmentVariable("SESSIONNAME");
}

Le mutex utilisé pour la protection d’instance unique doit distinguer trois cas : « une à l’échelle de la machine », « une par session », et « une par utilisateur, couvrant les sessions de cet utilisateur ». Le troisième cas (par utilisateur, à travers les sessions) ne peut pas être atteint avec le seul Global\ par défaut — le nom doit aussi inclure le SID de l’utilisateur. C’est traité en détail dans « Empêcher les doubles lancements dans les applications Windows ».

// Nécessite : using System.Security.AccessControl; et using System.Security.Principal;
// (nécessite aussi le paquet NuGet System.Threading.AccessControl)

// Intention : même avec plusieurs utilisateurs connectés simultanément sur RDS/un PC partagé,
// conserver exactement une instance à l'échelle de la machine.
// Mutex/MutexAcl.Create de .NET demande en interne SYNCHRONIZE et MUTEX_MODIFY_STATE,
// ainsi que DELETE/READ_CONTROL/WRITE_DAC/WRITE_OWNER, même pour simplement ouvrir
// un mutex existant. Cela signifie que, sauf si « Utilisateurs authentifiés » se voit
// accorder l'équivalent de FullControl, un utilisateur légitime autre que le créateur
// obtiendra une UnauthorizedAccessException au moment de vérifier createdNew.
// (En contrepartie : cela laisse aussi l'ACL modifiable ultérieurement par tout
// utilisateur authentifié. Si c'est inacceptable, abandonnez la classe Mutex et
// appelez CreateMutexEx directement via P/Invoke, en ne demandant que les droits
// d'accès minimaux nécessaires)
//
// Attention : cette ACL ne s'applique que si vous êtes celui qui a réussi à créer
// le mutex en premier. Un mutex Global\ à nom fixe risque d'être pré-créé par un
// autre utilisateur (squattage de nom), auquel cas createdGlobal revient à false,
// ou vous obtenez une UnauthorizedAccessException selon l'ACL de cette autre partie.
// Si vous devez réellement exclure les autres, combinez un nom difficile à deviner
// avec un autre mécanisme d'exclusion (voir le chapitre 5 de l'article compagnon
// pour les détails)
var globalMutexSecurity = new MutexSecurity();
globalMutexSecurity.AddAccessRule(new MutexAccessRule(
    new SecurityIdentifier(WellKnownSidType.AuthenticatedUserSid, domainSid: null),
    MutexRights.FullControl,
    AccessControlType.Allow));

using var globalMutex = MutexAcl.Create(
    initiallyOwned: false,
    name: @"Global\KomuraSoft.MyApp.SingleInstance",
    createdNew: out bool createdGlobal,
    mutexSecurity: globalMutexSecurity);

// Intention : une instance suffit par session (plusieurs sessions du même
// utilisateur peuvent très bien être traitées séparément).
// Laisser dans l'espace de noms de session par défaut tel quel
using var perSessionMutex = new Mutex(
    initiallyOwned: false,
    name: "KomuraSoft.MyApp.SingleInstance",
    createdNew: out bool createdPerSession);

if (!createdGlobal)
{
    // Déjà en cours d'exécution ailleurs sur la machine, y compris dans d'autres sessions
    return;
}

Créer un mutex préfixé par Global\ ne nécessite en soi aucun privilège spécial (comme indiqué précédemment, la vérification de privilège ne s’applique qu’à la création de nouveaux objets de mappage de fichiers et de liens symboliques).4 C’est une erreur de laisser le mutex dans l’espace de noms de session par défaut si votre intention est « une par utilisateur ». Il n’est pas rare que le même utilisateur détienne simultanément une session RDP déconnectée et une nouvelle session console/RDP, et dans ce cas chaque session est traitée comme un mutex séparé — donc, en laissant la valeur par défaut telle quelle, le même utilisateur peut finir par lancer une seconde instance. Lequel de « une à l’échelle de la machine », « une par session », ou « une par utilisateur à travers les sessions » constitue la bonne exigence dépend de la nature de l’application ; confirmez donc l’exigence avant d’arrêter un schéma de nommage.

10. Résumé

Une « session » Windows est un concept qui ne cesse de resurgir dans des sujets qui, à première vue, semblent sans rapport entre eux — la conception des services, l’exploitation du bureau à distance, la protection d’instance unique des objets nommés. La structure centrale se résume à trois points. Un service s’exécute en Session 0, une session spéciale et isolée, et ne peut pas afficher directement d’UI. RDS est conçu autour de plusieurs sessions simultanées, tandis qu’un système d’exploitation client ordinaire est fondamentalement mono-session. Et les objets nommés possèdent un espace de noms par session, ce qui vous oblige à juger, selon l’intention, si Global\ est requis.

La question de savoir si le comportement PC partagé/RDS fait partie de vos exigences est un sujet où le remarquer tard dans l’implémentation entraîne beaucoup de reprise de travail. Nous recommandons fortement de confirmer, lors de la définition des exigences, « qui utilisera cette application, sur quelles machines, et combien de personnes à la fois ». Si vous prévoyez de déployer une application existante sur un environnement PC partagé ou RDS, ou si vous devez enquêter sur un bug où « les choses tournent mal lorsque plusieurs utilisateurs l’utilisent », il est généralement plus rapide de diagnostiquer en observant la configuration réelle — n’hésitez pas à nous contacter.

Articles connexes

Domaines de conseil associés

KomuraSoft LLC (合同会社小村ソフト) prend en charge les revues de conception d’applications Windows destinées à un déploiement éventuel sur des PC partagés ou des environnements RDS, la conception de la séparation service/IPC, et l’investigation des causes racines de bugs propres à un usage multi-utilisateur simultané.

Références

  1. Microsoft Learn, Service Changes for Windows Vista. Sur le contexte de l’introduction de l’isolation Session 0, l’impossibilité qui en résulte pour les services et les applications d’échanger des messages de fenêtre, et la fonction WTSSendMessage comme alternative.  2 3

  2. Microsoft Learn, Interactive Services. Sur les raisons pour lesquelles un service ne peut pas interagir directement avec un utilisateur, la conception consistant à utiliser CreateProcessAsUser pour lancer un processus GUI séparé et coordonner via IPC, et la recommandation de distinguer les noms de tubes par ID de session.  2 3

  3. Microsoft Learn, Windows Enterprise multi-session FAQ. Sur la capacité multi-session — maintenir plusieurs sessions interactives à la fois — auparavant possible uniquement sous Windows Server, et désormais proposée exclusivement pour Azure Virtual Desktop.  2 3

  4. Microsoft Learn, Kernel object namespaces. Sur l’espace de noms par session des objets noyau nommés, les préfixes Global\ / Local\, et l’exigence du privilège SeCreateGlobalPrivilege pour créer un nouvel objet de mappage de fichiers ou de lien symbolique dans l’espace de noms global.  2 3 4

  5. Microsoft Learn, Named Pipes. Sur le fait que les tubes nommés sont accessibles depuis n’importe quel processus dans les limites des vérifications de sécurité, et joignables à distance si le service serveur est en cours d’exécution (la base expliquant pourquoi il s’agit d’un mécanisme différent des espaces de noms de session Global\/Local\ utilisés par les mutex, etc.). 

  6. Microsoft Learn, GetSystemMetrics function (winuser.h). Sur la spécification de la détection d’une session distante via SM_REMOTESESSION 2

  7. Microsoft Learn, Detecting the Remote Desktop Services environment. Sur un exemple de code pour GetSystemMetrics(SM_REMOTESESSION), la contrainte selon laquelle cette fonction détecte à tort une session distante comme locale lorsque RemoteFX vGPU est utilisé, et la détection alternative via la clé de registre GlassSessionId 2

  8. Microsoft Security Bulletin, MS12-034 - Windows and Messages Vulnerability (CVE-2012-0180). Sur une vulnérabilité d’élévation de privilèges exploitant une faille dans le traitement des messages de fenêtre du pilote en mode noyau de Windows (win32k.sys) (un exemple de cette classe d’attaque via les messages de fenêtre). 

  9. Microsoft Learn, schtasks create. Sur le fait que le compte SYSTEM n’a aucun droit d’ouverture de session interactive, et qu’un utilisateur ne peut ni voir ni interagir avec un programme ou une tâche s’exécutant sous les privilèges SYSTEM. 

  10. Microsoft Learn, Elevated Task Model. Sur la configuration où une application utilisateur standard effectue des opérations nécessitant des privilèges administrateur via une tâche enregistrée en tant que SYSTEM mais dont le descripteur de sécurité est configuré pour permettre à un utilisateur standard de la démarrer malgré tout. 

  11. Microsoft Learn, Remote Desktop Services overview in Windows Server. Sur le fait que RDS constitue le socle fournissant des bureaux multi-session, basés sur des sessions.  2

  12. Microsoft Learn, Troubleshoot Remote desktop disconnected errors. Sur le fait que jusqu’à deux connexions distantes simultanées sont possibles à des fins administratives sans licence RDS CAL, et que le rôle RD Session Host ainsi que des licences RDS CAL appropriées sont requis au-delà.  2

  13. Microsoft Learn, License Remote Desktop Services with Client Access Licenses (CALs). Sur la différence entre les modèles de licence RDS CAL par périphérique et par utilisateur, et le mécanisme d’émission/suivi du serveur de licences.  2

  14. Microsoft Learn, qwinsta. Sur le fait que la colonne SESSIONNAME affiche le nom attribué à une session (console pour la console, ou un nom commençant par rdp-tcp# pour une connexion RDP). 

  15. Microsoft Learn, SystemParameters.IsRemoteSession Property. Sur la propriété qui expose depuis WPF le contrôle équivalent à SM_REMOTESESSION

  16. Microsoft Learn, WTSGetActiveConsoleSessionId function (winbase.h). Sur la récupération de l’ID de session connectée à la console physique, et le renvoi de 0xFFFFFFFF pendant une transition.  2

  17. Microsoft Learn, WTSEnumerateSessions function (wtsapi32.h). Sur la possibilité d’énumérer toutes les sessions d’un serveur RD Session Host. 

  18. Microsoft Learn, Client/Server application guidelines. Sur les raisons pour lesquelles « une connexion depuis une machine » ne doit pas être assimilée à « une session utilisateur », et l’identification des sessions via ProcessIdToSessionId

  19. Microsoft Learn, Policy CSP - ADMX_TerminalServer (TS_TEMP_PER_SESSION). Sur le fait que Remote Desktop Services crée par défaut un dossier temporaire séparé par session (sous le profil utilisateur, avec l’ID de session dans le nom). 

  20. Microsoft Learn, Peripheral hardware guidelines. Sur la manière dont les lecteurs et imprimantes côté client apparaissent depuis un serveur RD Session Host selon la configuration de redirection, et le mécanisme permettant à un pilote de périphérique de désactiver la redirection. 

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

Pourquoi un service Windows ne peut-il pas afficher de boîte de dialogue ?
Depuis Windows Vista, les services s'exécutent dans une Session 0 dédiée, isolée de la session interactive de tout utilisateur connecté : une UI levée depuis un service est donc invisible pour tout le monde — et un appel à MessageBox peut bloquer indéfiniment le processus, en attente d'un bouton OK que personne ne peut cliquer. Cette isolation est une conception de sécurité qui bloque structurellement l'échange de messages de fenêtre à travers une frontière de privilèges, fermant ainsi toute une classe d'attaques par élévation de privilèges. La solution officiellement recommandée consiste à scinder l'interface utilisateur en un processus séparé qui vit dans la session de l'utilisateur et à communiquer avec le service via IPC, par exemple des tubes nommés (named pipes) ; lancer un programme d'interface utilisateur en tant qu'utilisateur interactif via le Planificateur de tâches est une autre option documentée.
Plusieurs utilisateurs peuvent-ils se connecter à Windows 10 ou 11 via RDP en même temps ?
Non — les systèmes d'exploitation client classiques comme Windows 10/11 Pro ou Enterprise sont fondamentalement mono-session, si bien qu'une session de console physique et une connexion à distance ne peuvent en principe pas coexister ; se connecter via RDP fait basculer l'écran local sur l'écran de verrouillage. Plusieurs sessions interactives simultanées nécessitent Windows Server avec le rôle Remote Desktop Services, qui exige le rôle RD Session Host et des licences RDS CAL au-delà des deux connexions d'administration. La seule édition client multi-session, Windows Enterprise multi-session, est proposée exclusivement pour Azure Virtual Desktop.
Pourquoi mon mutex d'instance unique ne parvient-il pas à empêcher les lancements depuis les sessions d'autres utilisateurs ?
Les objets noyau nommés tels que les mutex, les événements et les sémaphores possèdent chacun un espace de noms propre à chaque session. Un mutex créé sans préfixe atterrit dans l'espace de noms de la session courante ; la session d'un autre utilisateur (ou même une seconde session du même utilisateur) est donc traitée comme un objet entièrement nouveau et distinct, et la protection d'instance unique échoue silencieusement. Si vous voulez une seule instance à l'échelle de toute la machine et de toutes les sessions, vous devez explicitement préfixer le nom avec Global\. Si vous voulez une instance par utilisateur, couvrant toutes les sessions de cet utilisateur, combinez Global\ avec le SID de l'utilisateur intégré dans le nom.
Comment mon application peut-elle détecter si elle s'exécute dans une session de bureau à distance ?
Le contrôle standard est l'API Win32 GetSystemMetrics(SM_REMOTESESSION), qui renvoie une valeur non nulle pour une session distante ; WPF expose le même contrôle via la propriété SystemParameters.IsRemoteSession. Attention : sur les sessions distantes utilisant RemoteFX vGPU, cette fonction signale à tort une session distante comme locale, et la solution de contournement documentée consiste à comparer la valeur GlassSessionId du registre avec l'ID de session actuel. La variable d'environnement SESSIONNAME (Console contre RDP-Tcp#...) permet un diagnostic rapide mais n'est pas une API officielle. N'utilisez pas WTSGetActiveConsoleSessionId pour identifier les sessions RDP — elle ne renvoie que la session attachée à la console physique.

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.

Retour au blog