Come comprendere l'isolamento delle sessioni di Windows — Session 0, RDP ed esecuzione simultanea di più utenti

· · Session 0, Isolamento delle sessioni, RDP, Desktop remoto, Servizi Windows, Multiutente, Windows, .NET, C#, Architettura, Consulenza tecnica

«Una finestra di dialogo sollevata da un servizio è invisibile a chiunque.» «Nel momento in cui qualcuno si è connesso via RDP, lo schermo della macchina fisica è passato alla schermata di accesso.» «La protezione dell’istanza singola doveva impedire gli avvii doppi, ma accedere come un altro utente ha allegramente avviato una seconda copia.» Gli incarichi di consulenza sulle applicazioni aziendali fanno emergere continuamente questi sintomi, e dietro tutti loro si nasconde la stessa lacuna: una comprensione incompleta del concetto di «sessione» di Windows. Le sessioni attraversano trasversalmente la progettazione dei servizi, la gestione del desktop remoto e la protezione dell’istanza singola per gli oggetti con nome, eppure è raro vedere l’argomento spiegato in modo sistematico in un unico posto.

Questo articolo organizza, da un punto di vista pratico, i motivi per cui è stato introdotto l’isolamento Session 0, il comportamento delle sessioni quando ci si connette via RDP, il modo in cui gli oggetti con nome vengono isolati per sessione, e gli errori di progettazione più comuni negli ambienti con PC condivisi e RDS (Remote Desktop Services).

1. Prima di tutto, l’essenziale

  • I servizi vengono eseguiti in una sessione dedicata chiamata Session 0, isolata dalla sessione interattiva di qualsiasi utente connesso. Un servizio non può mostrare direttamente una finestra di dialogo e, anche se ci prova, l’utente non la vede mai.1
  • La soluzione ufficialmente consigliata per un servizio che necessita di un’interfaccia utente consiste nel separare l’UI in un processo distinto (che vive nella sessione dell’utente) e comunicare con esso tramite IPC. Avviare un programma con interfaccia come utente interattivo tramite l’Utilità di pianificazione è un’altra opzione disponibile.2
  • Windows Server (RDS) è costruito attorno a più sessioni simultanee, ma i sistemi operativi client come Windows 10/11 Pro sono fondamentalmente a sessione singola. A parte l’edizione speciale «Enterprise multi-session» pensata per Azure Virtual Desktop, le normali installazioni del sistema operativo client non sono progettate per ospitare più sessioni interattive simultanee.3
  • Gli oggetti kernel con nome (mutex, eventi, semafori, ecc.) hanno ciascuno uno spazio dei nomi per sessione. Se si vuole ridurre un oggetto a una singola istanza sull’intera macchina e tra le sessioni, il nome deve essere esplicitamente preceduto dal prefisso Global\ — altrimenti la protezione dell’istanza singola non si comporterà come previsto. Le named pipe sono al di fuori di questo meccanismo: per impostazione predefinita usano uno spazio dei nomi separato, raggiungibile a livello dell’intera macchina (e da remoto, se il servizio server è in esecuzione); se serve distinguere per sessione o per utente, occorre incorporare qualcosa come l’ID di sessione nel nome della pipe stessa.45
  • Il tipo della sessione corrente si può determinare tramite la variabile d’ambiente SESSIONNAME o GetSystemMetrics(SM_REMOTESESSION). Tuttavia esistono casi — ad esempio quando si usa RemoteFX vGPU — in cui il rilevamento si discosta dal comportamento previsto, quindi non bisogna affidarvisi ciecamente.67
  • Se il comportamento su PC condivisi/RDS debba far parte dei requisiti è qualcosa da confermare fin dall’inizio della fase di progettazione. Le collisioni dei percorsi dei file temporanei, l’AppData confuso e l’accesso simultaneo a dispositivi/dongle sono gli incidenti classici che esplodono nel momento in cui un’app costruita per un singolo PC autonomo viene distribuita su RDS (si veda il capitolo 7).

2. Cos’è l’isolamento Session 0 — e perché è avvenuta la separazione dalle sessioni utente

Prima di Windows Vista / Windows Server 2008, un servizio e il primo utente ad accedere condividevano la stessa Session 0. I processi all’interno della stessa sessione possono scambiarsi messaggi di finestra indipendentemente dai rispettivi livelli di privilegio. Un processo con privilegi standard poteva inviare un messaggio appositamente costruito a una finestra di proprietà di un servizio in esecuzione come SYSTEM, dirottare la gestione dei messaggi del servizio ed elevare i propri privilegi — questa intera classe di elevazione dei privilegi, che sfrutta difetti nella gestione dei messaggi di finestra, è stata segnalata ripetutamente nel corso degli anni 2000, e di fatto sono state corrette nel tempo diverse vulnerabilità di elevazione dei privilegi che sfruttavano difetti nella gestione dei messaggi di finestra del kernel di Windows (win32k.sys).8

Questo quadro è cambiato a partire da Windows Vista. La Session 0 è diventata dedicata esclusivamente ai servizi e alle applicazioni non legate a una sessione utente interattiva, e al primo utente che accede viene ora assegnata la Session 1, al successivo la Session 2, e così via — ciascuno in una sessione diversa da quella del servizio. La Session 0 non supporta il funzionamento interattivo, e i servizi non possono inviare messaggi alle applicazioni, né le applicazioni possono inviarli ai servizi. Un servizio non può nemmeno mostrare direttamente elementi dell’interfaccia utente come una finestra di dialogo. Con questo cambiamento, il percorso stesso che permetteva a un processo con privilegi standard di inviare messaggi direttamente nella finestra di un servizio è stato chiuso a livello strutturale.1

In altre parole, l’isolamento Session 0 non è semplicemente un vincolo operativo del tipo «i servizi sono processi in background, quindi non hanno uno schermo» — è una scelta di sicurezza che blocca strutturalmente lo scambio di messaggi di finestra attraverso un confine di privilegi. L’unica via che rimane a un servizio per mostrare comunque qualcosa a schermo è la funzione WTSSendMessage, un’API che fa apparire un semplice message box in un’altra sessione, ma che non può essere usata per finestre di dialogo complesse o interazioni bidirezionali.1

3. I servizi non possono mostrare l’interfaccia utente — vincoli pratici e schemi di aggiramento

La conseguenza pratica dell’isolamento Session 0 è semplice. Se un servizio chiama MessageBox.Show o tenta di mostrare una form, sullo schermo dell’utente connesso non appare nulla. Peggio ancora, è una causa classica del blocco dell’intero processo, che resta in attesa per sempre di un pulsante OK che nessuno potrà mai cliccare. Quando si porta vecchio codice (qualcosa che una volta funzionava come «servizio interattivo» ai tempi di Windows XP) all’interno di un servizio, verificare sempre che non siano rimaste chiamate di visualizzazione dell’interfaccia.

Esistono due soluzioni ufficialmente documentate.2

  • Separare l’UI in un processo distinto (che vive nella sessione dell’utente interattivo) e comunicare tramite IPC. Il servizio esegue il lavoro e passa i risultati o le richieste di input al processo UI. Il processo UI li presenta all’utente come un’icona nell’area di notifica o come una finestra di dialogo, e restituisce al servizio il risultato dell’operazione. Se si usa un meccanismo IPC come le named pipe, occorre progettare le ACL in modo che nulla venga esposto involontariamente sulla rete. Negli ambienti in cui più utenti sono connessi contemporaneamente, la linea guida è distinguere le sessioni, ad esempio incorporando l’ID di sessione nel nome della pipe.2 Per la scelta del meccanismo IPC, si veda l’articolo gemello pubblicato lo stesso giorno, «Una tabella decisionale per la comunicazione tra processi in Windows». Come costruire il servizio stesso, insieme agli aspetti di progettazione operativa come tipo di avvio, account di esecuzione e opzioni di ripristino, è trattato in «Come creare un servizio Windows».
  • Avviare un programma come utente interattivo tramite l’Utilità di pianificazione. Invece di mantenerlo residente come servizio, questa configurazione avvia un programma con interfaccia con i privilegi e la sessione propri dell’utente connesso. Un punto a cui prestare attenzione qui: un’«attività registrata sotto SYSTEM» non deve mai essere quella che mostra l’interfaccia. L’account SYSTEM non ha diritti di accesso interattivo, e un utente non può vedere né interagire con un programma o un’attività in esecuzione con privilegi SYSTEM.9 Il modello di attività con privilegi elevati è pensato in modo che un’attività SYSTEM — il cui descrittore di sicurezza consente a un utente standard di avviarla — assuma solo le operazioni che richiedono privilegi di amministratore, mentre l’interfaccia utente stessa gira in un programma separato con i privilegi e la sessione propri dell’utente connesso (oppure come la stessa operazione registrata come attività sotto l’account dell’utente); questa è tutta la premessa di questa divisione dei ruoli.10 Mantenere l’attività SYSTEM rigorosamente come «broker senza interfaccia» e lasciare sempre che l’interfaccia visibile giri in un processo nella sessione propria dell’utente.

Qualunque sia l’approccio scelto, il punto chiave è costruire fin dal primo giorno, nella progettazione, il principio «il servizio resta dietro le quinte, l’interfaccia vive in un processo separato». Ciò che si osserva regolarmente in pratica è che cercare di aggiungerlo in seguito si scontra con codice già profondamente dipendente dall’interfaccia all’interno del servizio, facendo salire il costo della loro separazione.

4. Come si comportano le sessioni via RDP — la differenza tra RDS e un sistema operativo client

La confusione del tipo «è lo stesso RDP sul server e sul client, perché il comportamento è diverso?» deriva dalla presenza o assenza del ruolo Remote Desktop Services (RDS).

Windows Server con il ruolo RDS è progettato attorno a più utenti che accedono simultaneamente a un unico server, ciascuno operando in una sessione indipendente con il proprio desktop e le proprie applicazioni — una configurazione costruita fin dall’inizio su più sessioni simultanee.11 Sotto RDS, le normali connessioni RDP amministrative (per gli amministratori) supportano fino a due sessioni senza bisogno di una CAL, ma superare questo numero, o permettere a utenti comuni di connettersi contemporaneamente, richiede di distribuire il ruolo RD Session Host insieme a licenze RDS CAL (Client Access License) adeguate.12 Le CAL esistono in due modelli — per dispositivo e per utente — e i dettagli di licenza vanno confermati per ciascun ambiente.13

D’altra parte, un normale sistema operativo client come Windows 10/11 Pro o Enterprise non include il ruolo RDS ed è, in linea di principio, a sessione singola. Esiste un’edizione speciale di sistema client chiamata «Windows Enterprise multi-session» in grado di mantenere più sessioni interattive contemporaneamente, ma è offerta esclusivamente per Azure Virtual Desktop — le normali installazioni del sistema client, distribuite in locale, non sono progettate per supportare più sessioni utente simultanee.3 In pratica, coerentemente con l’esperienza familiare di «mi sono collegato via RDP al PC di un collega e lo schermo alla sua scrivania è passato alla schermata di blocco», è più sicuro assumere che sui sistemi client una sessione della console fisica (qualcuno che opera la macchina localmente) e una connessione remota non possano in linea di principio coesistere nello stesso momento.

Ecco un riepilogo delle differenze rilevanti per la progettazione di applicazioni aziendali.

Aspetto Windows Server + RDS Sistema operativo client (Windows 10/11 Pro, ecc.)
Sessioni simultanee Più utenti possono accedere contemporaneamente11 Fondamentalmente a sessione singola. Più sessioni interattive non sono supportate al di fuori dell’edizione multi-session riservata ad AVD3
Licenza Richiede RD Session Host + licenze RDS CAL (oltre alle due connessioni amministrative)12 La funzionalità Desktop remoto in sé è inclusa nel sistema operativo, ma i requisiti di licenza e le edizioni consentite vanno verificati separatamente
Uso tipico Esecuzione di applicazioni aziendali su terminali condivisi, ambienti thin client Manutenzione remota di un PC personale, accesso in smart working

Non tenere conto di questa differenza — assumendo che «funzionava sul sistema operativo client della mia macchina di sviluppo, quindi dovrebbe funzionare allo stesso modo nell’ambiente RDS del server condiviso» — farà trascurare i bug specifici della concorrenza multiutente trattati nel capitolo 7. Al contrario, se si prevede solo un funzionamento a sessione singola su un sistema client, non è necessario sostenere fin da subito il costo di progettazione del supporto per PC condivisi. Questa è una decisione da prendere presto, durante la definizione dei requisiti (capitolo 8).

5. Determinare la sessione corrente

Ci sono molte situazioni in cui si vuole sapere in quale sessione è attualmente in esecuzione il proprio processo — ad esempio per sopprimere un effetto di rendering pesante durante l’elaborazione in background, o per disabilitare una funzionalità che consuma molta banda su una sessione remota. Esistono diversi modi per determinarlo.

  • La variabile d’ambiente SESSIONNAME: impostata su Console per la sessione console, oppure su un nome che inizia con RDP-Tcp# per una connessione RDP. È la stessa informazione mostrata nella colonna SESSIONNAME elencata dal comando qwinsta.14 Utilizzabile per un controllo rapido, ma poiché non è un’API ufficiale è più sicuro non affidarvisi troppo.
  • GetSystemMetrics(SM_REMOTESESSION): un’API Win32 che indica se ci si trova in una sessione remota. Restituisce un valore diverso da zero se il processo chiamante è associato a una sessione client Terminal Services. C’è però un avvertimento: a partire da Windows 8/Server 2012, nelle sessioni remote che usano RemoteFX vGPU, questa funzione segnala erroneamente una sessione remota come locale. In tal caso, la soluzione alternativa documentata consiste nel confrontare il valore GlassSessionId del registro con l’ID della sessione corrente.67
  • SystemParameters.IsRemoteSession di .NET (WPF): espone lo stesso controllo eseguito da GetSystemMetrics(SM_REMOTESESSION), come proprietà leggibile da un’applicazione WPF.15 Da WinForms o da un’applicazione console, andrebbe chiamata direttamente tramite P/Invoke.
  • WTSGetActiveConsoleSessionId: un’API che recupera l’ID della sessione collegata alla console fisica. Confrontarlo con il proprio ID di sessione permette di determinare «sto girando sulla console fisica?». Notare che quando nessuno è collegato alla console fisica (cioè durante una transizione), viene restituito 0xFFFFFFFF.16

Non usare WTSGetActiveConsoleSessionId per identificare le sessioni in un ambiente RDS. Fedele al suo nome, tutto ciò che restituisce è «la sessione collegata alla console fisica» — un utente connesso via RDP non è coperto.16 Se un servizio ha bisogno di sapere in quale sessione mostrare la sua UI companion, questa API è sufficiente quando si prevede solo un utente connesso alla console, ma in un ambiente RDS dove più utenti RDP possono essere connessi simultaneamente, si dovrebbe invece enumerare le sessioni in esecuzione con WTSEnumerateSessions17, oppure usare semplicemente l’ID di sessione fornito da una notifica di cambio sessione (WM_WTSSESSION_CHANGE). Questo scambio è una causa classica, difficile da individuare, di bug in cui l’interfaccia di notifica semplicemente non appare — oppure appare nella sessione sbagliata — specificamente per gli utenti connessi via RDP. Tenere ben presente questa distinzione quando si decide in quale sessione avviare il processo UI per la configurazione IPC descritta nel capitolo 3.

6. Isolamento per sessione degli oggetti con nome

Gli oggetti kernel con nome — mutex, eventi, semafori, timer attendibili, oggetti di mapping dei file — hanno ciascuno uno spazio dei nomi indipendente per sessione. Anche creando in una sessione un mutex chiamato MyAppMutex, tentare di aprire un oggetto con lo stesso nome da una sessione diversa crea un oggetto completamente nuovo e distinto. Per i processi che girano principalmente in Session 0, come i servizi, o per le configurazioni client/server che vogliono condividere un oggetto tra più sessioni, è possibile posizionarlo esplicitamente nello spazio dei nomi globale anteponendo al nome il prefisso Global\. Al contrario, anteporre Local\ lo posiziona esplicitamente nello spazio dei nomi della propria sessione.4

È qui che questo conta davvero in pratica: il mutex usato per la protezione dell’istanza singola.

  • Se tutto ciò che serve è «impedire allo stesso utente di avviare due copie dell’app nella stessa sessione», il nome di mutex predefinito (senza prefisso) è sufficiente. Lo spazio dei nomi per sessione entra automaticamente in gioco, quindi la sessione di un utente diverso viene trattata come un mutex diverso.
  • Se si vuole «anche con più utenti connessi contemporaneamente in un ambiente RDS, limitare l’app a una singola istanza sull’intera macchina», questo obiettivo si può raggiungere solo usando esplicitamente Global\. Lasciato al valore predefinito, ogni utente finisce per poter avviare la propria copia — un bug che si manifesta come «la protezione dell’istanza singola doveva funzionare, ma sono comunque partite più istanze».

Da notare anche che un processo al di fuori della Session 0 richiede il privilegio SeCreateGlobalPrivilege per creare un nuovo oggetto di mapping dei file o un oggetto di collegamento simbolico nello spazio dei nomi globale (aprire semplicemente un oggetto esistente non lo richiede). Questo controllo dei privilegi si applica solo ai mapping dei file e ai collegamenti simbolici — non viene imposto alla creazione di mutex o eventi — ma vale la pena ricordarlo per qualsiasi progettazione che usi memoria condivisa (un file mappato in memoria) nello spazio dei nomi globale.4

Esiste anche un principio generale per le applicazioni client/server: la linea guida è non equiparare «una connessione da una macchina» a «una sessione utente». Dato che da una stessa macchina possono essere stabilite simultaneamente più sessioni (le connessioni RDP di più utenti), lato server si raccomanda di identificare le sessioni usando, ad esempio, ProcessIdToSessionId, e di predisporre un canale di comunicazione separato per ciascuna sessione.18

7. Errori di progettazione comuni su PC condivisi e ambienti RDS

Esiste un pugno di schemi di fallimento ricorrenti che esplodono nel momento in cui un’app costruita pensando solo a un uso su singolo PC, solo console, viene distribuita su un PC condiviso o un ambiente RDS.

  • Collisioni dei percorsi dei file temporanei. Per impostazione predefinita, RDS crea una cartella temporanea separata per ogni sessione (sotto il profilo dell’utente, con l’ID di sessione incorporato nel nome).19 In altre parole, se l’app usa semplicemente la variabile d’ambiente %TEMP%, ottiene gratuitamente il vantaggio di questo isolamento. I problemi iniziano quando l’app codifica un percorso fisso come C:\Work\temp invece di usare %TEMP%. Se più sessioni dello stesso account utente, o più utenti, scrivono simultaneamente sullo stesso percorso fisso, ovviamente si contenderanno il file.
  • AppData confuso. Dove salvare le impostazioni utente, le cache e i log va deciso con una solida comprensione del meccanismo dei profili utente di Windows — locale contro roaming, e quando usare %LOCALAPPDATA% piuttosto che %APPDATA%. Su un PC condiviso, una progettazione trascurata su questo punto tende a manifestarsi come «il mio schermo è cambiato a causa delle impostazioni di qualcun altro» o «il log è stato sovrascritto». Si veda «Un’introduzione ai profili utente di Windows» per i dettagli.
  • Assumere l’accesso simultaneo a dispositivi, dongle e porte seriali. Accedere a un dispositivo locale lato client o a una porta seriale da una sessione RDS passa attraverso un meccanismo di reindirizzamento. Se si cerca di far usare contemporaneamente a più sessioni un’app che dipende da un dongle USB fisico o da un dispositivo di comunicazione seriale, si incontrano problemi in cui l’hardware stesso potrebbe non supportare l’accesso concorrente fin dall’inizio, oppure la visibilità dipende dalla configurazione del reindirizzamento e può essere presente o assente. Il modo in cui un dispositivo collegato lato client appare da una sessione lato server dipende dalla configurazione del reindirizzamento, quindi va confermato in fase di progettazione.20
  • Stampanti e unità codificate in modo fisso. Codificare in modo fisso all’interno di un’app un nome di stampante predefinita o una lettera di unità si rompe facilmente in un ambiente PC condiviso dove la stampante predefinita reindirizzata o l’unità mappata differiscono per ogni utente. È più sicuro recuperare il nome della stampante a runtime e, dove possibile, trattare le unità come percorsi UNC.
  • Lettura errata di HKCU / del registro per utente. Vale anche la pena notare che HKEY_CURRENT_USER è legato all’utente connesso, non alla sessione. In una configurazione in cui lo stesso utente ha più sessioni (ad esempio condividendo lo stesso account su RDS), i valori di HKCU sono condivisi tra tutte le sessioni di quell’utente — quindi memorizzare in HKCU uno stato di runtime che in realtà si vuole isolato per sessione filtrerà involontariamente nelle altre sessioni. Le informazioni che richiedono un isolamento per sessione dovrebbero invece usare gli spazi dei nomi per sessione del capitolo 6, o un percorso di file che incorpora l’ID di sessione.

Tutti questi sono il tipo di bug che «non si noterebbe mai su un singolo PC autonomo» e che «emerge solo quando più utenti usano davvero l’app contemporaneamente» — quindi lo schema tipico è che un ambiente di test costituito solo da un’unica macchina di sviluppo autonoma non lo riproduce mai, e appare per la prima volta solo su un PC condiviso o un ambiente RDS di produzione.

8. Tabella decisionale — il supporto «PC condiviso/RDS» dovrebbe far parte dei requisiti?

Se il comportamento su PC condiviso/RDS debba far parte dei requisiti è qualcosa da confermare durante la definizione dei requisiti, prima che l’implementazione inizi. Una volta deciso di includerlo, usare i punti seguenti come lista di controllo.

Aspetto Va bene assumere solo PC autonomo/console Serve il supporto PC condiviso/RDS (multiutente simultaneo)
File temporanei / impostazioni Un percorso fisso causa poco danno reale Rendere obbligatori percorsi per utente/per sessione come %TEMP% / %LOCALAPPDATA% (capitolo 7)
Oggetti con nome Va bene lasciare il valore predefinito (spazio dei nomi di sessione) Se serve «un’istanza sull’intera macchina», usare esplicitamente Global\. Se basta «una per sessione», lasciare il valore predefinito così com’è. Se si vuole «una per utente, che copra le sessioni di quell’utente», combinare Global\ con il SID dell’utente (capitolo 6; si veda anche l’articolo sulla protezione dell’istanza singola)
Dispositivi / dongle Connessione locale diretta e semplice Progettare tenendo conto del fatto che l’accesso passa attraverso il reindirizzamento e l’accesso concorrente potrebbe non essere possibile
UI e servizi Facile tenere tutto su un unico desktop Separare in un processo distinto + IPC, alla luce dell’isolamento Session 0 (capitolo 3)
Licenza / fatturazione Semplice da contare per numero di macchine Confermare il concetto del numero di sessioni simultanee e se sono richieste licenze RDS CAL, durante la definizione dei requisiti13

L’asse della decisione è semplice: concordare presto con il cliente, durante lo sviluppo, se «questa app deve semplicemente funzionare in modo autonomo sul PC di una singola persona» oppure se «potrebbe essere usata da più persone contemporaneamente su un terminale condiviso o un ambiente RDS». Lasciare questo punto ambiguo mentre l’implementazione procede porta a dover ricostruire da zero ciascuno dei punti sopra elencati.

9. Esempio di implementazione — usare in modo appropriato il rilevamento della sessione e gli spazi dei nomi

Ecco come i metodi di rilevamento e le scelte di denominazione del mutex dei capitoli 5 e 6 si combinano nel codice .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;

    // Per un'app WPF, System.Windows.SystemParameters.IsRemoteSession restituisce lo stesso risultato
    public static bool IsRemoteSession() => GetSystemMetrics(SM_REMOTESESSION) != 0;

    // Controllo rapido e approssimativo. Non è un'API ufficiale, quindi va riservato
    // a log/diagnostica piuttosto che a decisioni importanti
    public static string? SessionName() =>
        Environment.GetEnvironmentVariable("SESSIONNAME");
}

Il mutex usato per la protezione dell’istanza singola deve distinguere tre casi: «una sull’intera macchina», «una per sessione» e «una per utente, che copra le sessioni di quell’utente». Il terzo caso (per utente, tra le sessioni) non può essere ottenuto con il solo Global\ predefinito — il nome deve includere anche il SID dell’utente. Questo è trattato in dettaglio in «Impedire gli avvii doppi nelle applicazioni Windows».

// Richiede: using System.Security.AccessControl; e using System.Security.Principal;
// (richiede anche il pacchetto NuGet System.Threading.AccessControl)

// Intento: anche con più utenti connessi contemporaneamente su RDS/un PC condiviso,
// mantenere esattamente un'istanza sull'intera macchina.
// Mutex/MutexAcl.Create di .NET richiede internamente SYNCHRONIZE e
// MUTEX_MODIFY_STATE, oltre a DELETE/READ_CONTROL/WRITE_DAC/WRITE_OWNER, anche solo
// per aprire un mutex esistente. Questo significa che, a meno che a «Utenti
// autenticati» non venga concesso l'equivalente di FullControl, un utente
// legittimo diverso dal creatore otterrà una UnauthorizedAccessException nel
// momento in cui si verifica createdNew.
// (Il compromesso: questo lascia anche l'ACL modificabile in seguito da qualsiasi
// utente autenticato. Se ciò è inaccettabile, abbandonare la classe Mutex e
// chiamare CreateMutexEx direttamente tramite P/Invoke, richiedendo solo i diritti
// di accesso minimi necessari)
//
// Attenzione: questa ACL si applica solo se si è quelli che sono riusciti a creare
// per primi il mutex. Un mutex Global\ con nome fisso rischia di essere pre-creato
// da un altro utente (name squatting), nel qual caso createdGlobal torna false,
// oppure si ottiene una UnauthorizedAccessException a seconda dell'ACL dell'altra
// parte. Se serve davvero escludere gli altri, combinare un nome difficile da
// indovinare con un altro meccanismo di esclusione (per i dettagli si veda il
// capitolo 5 dell'articolo gemello)
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);

// Intento: basta un'istanza per sessione (più sessioni dello stesso utente
// possono benissimo essere trattate separatamente).
// Lasciare nello spazio dei nomi di sessione predefinito così com'è
using var perSessionMutex = new Mutex(
    initiallyOwned: false,
    name: "KomuraSoft.MyApp.SingleInstance",
    createdNew: out bool createdPerSession);

if (!createdGlobal)
{
    // Già in esecuzione altrove sulla macchina, incluse altre sessioni
    return;
}

Creare un mutex con prefisso Global\ non richiede di per sé alcun privilegio speciale (come detto in precedenza, il controllo dei privilegi si applica solo alla creazione di nuovi oggetti di mapping dei file e collegamenti simbolici).4 È un errore lasciare il mutex nello spazio dei nomi di sessione predefinito se l’intento è «uno per utente». Non è raro che lo stesso utente detenga contemporaneamente una sessione RDP disconnessa e una nuova sessione console/RDP, e in tal caso ogni sessione viene trattata come un mutex separato — quindi, lasciando il valore predefinito così com’è, lo stesso utente può finire per avviare una seconda istanza. Quale tra «una sull’intera macchina», «una per sessione» o «una per utente tra le sessioni» sia il requisito corretto dipende dalla natura dell’app, quindi confermare il requisito prima di fissare uno schema di denominazione.

Una «sessione» di Windows è un concetto che continua a riemergere in argomenti che a prima vista sembrano scollegati tra loro — la progettazione dei servizi, la gestione del desktop remoto, la protezione dell’istanza singola per gli oggetti con nome. La struttura centrale si riduce a tre punti. Un servizio gira in Session 0, una sessione speciale e isolata, e non può mostrare direttamente l’interfaccia. RDS è costruito attorno a più sessioni simultanee, mentre un normale sistema operativo client è fondamentalmente a sessione singola. E gli oggetti con nome hanno uno spazio dei nomi per sessione, il che richiede di valutare, in base all’intento, se Global\ sia necessario.

Se il comportamento su PC condiviso/RDS debba far parte dei requisiti è un argomento in cui accorgersene tardi durante l’implementazione causa molto lavoro di rifacimento. Raccomandiamo vivamente di confermare, durante la definizione dei requisiti, «chi userà quest’app, su quali macchine, e in quanti contemporaneamente». Se si pianifica di distribuire un’app esistente su un ambiente PC condiviso o RDS, oppure occorre indagare un bug in cui «le cose vanno male quando più utenti la usano», di solito è più veloce diagnosticare osservando la configurazione reale — non esitate a contattarci.

Articoli correlati

Aree di consulenza correlate

KomuraSoft LLC (合同会社小村ソフト) si occupa di revisioni progettuali di applicazioni Windows pensate per un’eventuale distribuzione su PC condivisi o ambienti RDS, della progettazione della separazione servizio/IPC, e dell’indagine sulle cause profonde di bug specifici dell’uso simultaneo da parte di più utenti.

Riferimenti

  1. Microsoft Learn, Service Changes for Windows Vista. Sul contesto dell’introduzione dell’isolamento Session 0, sull’impossibilità che ne deriva per servizi e applicazioni di scambiarsi messaggi di finestra, e sulla funzione WTSSendMessage come alternativa.  2 3

  2. Microsoft Learn, Interactive Services. Sui motivi per cui un servizio non può interagire direttamente con un utente, sulla progettazione che usa CreateProcessAsUser per avviare un processo GUI separato e coordinarsi tramite IPC, e sulla linea guida di distinguere i nomi delle pipe in base all’ID di sessione.  2 3

  3. Microsoft Learn, Windows Enterprise multi-session FAQ. Sulla capacità multi-sessione — mantenere più sessioni interattive contemporaneamente — precedentemente possibile solo su Windows Server, ora offerta esclusivamente per Azure Virtual Desktop.  2 3

  4. Microsoft Learn, Kernel object namespaces. Sullo spazio dei nomi per sessione degli oggetti kernel con nome, sui prefissi Global\ / Local\, e sul fatto che per creare un nuovo oggetto di mapping dei file o di collegamento simbolico nello spazio dei nomi globale sia richiesto il privilegio SeCreateGlobalPrivilege 2 3 4

  5. Microsoft Learn, Named Pipes. Sul fatto che le named pipe sono accessibili da qualsiasi processo entro i limiti dei controlli di sicurezza, e raggiungibili da remoto se il servizio server è in esecuzione (la base del perché si tratti di un meccanismo diverso dagli spazi dei nomi di sessione Global\/Local\ usati da mutex, ecc.). 

  6. Microsoft Learn, GetSystemMetrics function (winuser.h). Sulla specifica del rilevamento di una sessione remota tramite SM_REMOTESESSION 2

  7. Microsoft Learn, Detecting the Remote Desktop Services environment. Su un esempio di codice per GetSystemMetrics(SM_REMOTESESSION), sul vincolo per cui questa funzione rileva erroneamente una sessione remota come locale quando si usa RemoteFX vGPU, e sul rilevamento alternativo tramite la chiave di registro GlassSessionId 2

  8. Microsoft Security Bulletin, MS12-034 - Windows and Messages Vulnerability (CVE-2012-0180). Su una vulnerabilità di elevazione dei privilegi che sfrutta un difetto nella gestione dei messaggi di finestra del driver in modalità kernel di Windows (win32k.sys) (un esempio di questa classe di attacco tramite messaggi di finestra). 

  9. Microsoft Learn, schtasks create. Sul fatto che l’account SYSTEM non ha diritti di accesso interattivo, e un utente non può vedere né interagire con un programma o un’attività in esecuzione con privilegi SYSTEM. 

  10. Microsoft Learn, Elevated Task Model. Sulla configurazione in cui un’applicazione utente standard esegue operazioni che richiedono privilegi di amministratore tramite un’attività registrata come SYSTEM ma con un descrittore di sicurezza configurato in modo che un utente standard possa comunque avviarla. 

  11. Microsoft Learn, Remote Desktop Services overview in Windows Server. Sul fatto che RDS è la base che fornisce desktop multi-sessione, basati su sessioni.  2

  12. Microsoft Learn, Troubleshoot Remote desktop disconnected errors. Sul fatto che fino a due connessioni remote simultanee sono possibili per scopi amministrativi senza una licenza RDS CAL, e che oltre questo numero sono richiesti il ruolo RD Session Host e licenze RDS CAL adeguate.  2

  13. Microsoft Learn, License Remote Desktop Services with Client Access Licenses (CALs). Sulla differenza tra i modelli di licenza RDS CAL per dispositivo e per utente, e sul meccanismo di emissione/tracciamento del server di licenza.  2

  14. Microsoft Learn, qwinsta. Sul fatto che la colonna SESSIONNAME mostra il nome assegnato a una sessione (console per la console, o un nome che inizia con rdp-tcp# per una connessione RDP). 

  15. Microsoft Learn, SystemParameters.IsRemoteSession Property. Sulla proprietà che espone da WPF il controllo equivalente a SM_REMOTESESSION

  16. Microsoft Learn, WTSGetActiveConsoleSessionId function (winbase.h). Sul recupero dell’ID della sessione collegata alla console fisica, e sulla restituzione di 0xFFFFFFFF durante una transizione.  2

  17. Microsoft Learn, WTSEnumerateSessions function (wtsapi32.h). Sulla possibilità di enumerare tutte le sessioni su un server RD Session Host. 

  18. Microsoft Learn, Client/Server application guidelines. Sul perché «una connessione da una macchina» non debba essere equiparata a «una sessione utente», e sull’identificazione delle sessioni tramite ProcessIdToSessionId

  19. Microsoft Learn, Policy CSP - ADMX_TerminalServer (TS_TEMP_PER_SESSION). Sul fatto che Remote Desktop Services crea per impostazione predefinita una cartella temporanea separata per ogni sessione (sotto il profilo utente, con l’ID di sessione nel nome). 

  20. Microsoft Learn, Peripheral hardware guidelines. Su come le unità e le stampanti lato client appaiono da una sessione su un server RD Session Host a seconda della configurazione del reindirizzamento, e sul meccanismo che permette a un driver di dispositivo di disabilitare il reindirizzamento. 

Articoli recenti con gli stessi tag per approfondire argomenti vicini.

Queste pagine collocano l’argomento in un contesto più ampio di servizi e decisioni.

L’articolo è direttamente collegato ai servizi seguenti.

Domande frequenti

Domande che ricorrono nelle consulenze sull’argomento dell’articolo.

Perché un servizio Windows non può mostrare una finestra di dialogo?
A partire da Windows Vista, i servizi vengono eseguiti in una Session 0 dedicata, isolata dalla sessione interattiva di qualsiasi utente connesso: l'interfaccia sollevata da un servizio è quindi invisibile a chiunque, e una chiamata a MessageBox può bloccare il processo per sempre in attesa di un pulsante OK che nessuno può cliccare. Questo isolamento è una scelta di sicurezza che blocca strutturalmente lo scambio di messaggi di finestra attraverso un confine di privilegi, chiudendo così un'intera classe di attacchi di elevazione dei privilegi. La soluzione ufficialmente consigliata consiste nel separare l'interfaccia utente in un processo distinto che vive nella sessione dell'utente e nel comunicare con il servizio tramite IPC, ad esempio le named pipe; avviare un programma con interfaccia come utente interattivo tramite l'Utilità di pianificazione è un'altra opzione documentata.
Più utenti possono connettersi contemporaneamente a Windows 10 o 11 via RDP?
No: i normali sistemi operativi client come Windows 10/11 Pro o Enterprise sono fondamentalmente a sessione singola, quindi una sessione della console fisica e una connessione remota non possono in linea di principio coesistere; connettersi via RDP fa passare lo schermo locale alla schermata di blocco. Più sessioni interattive contemporanee richiedono Windows Server con il ruolo Remote Desktop Services, che necessita del ruolo RD Session Host e di licenze RDS CAL oltre alle due connessioni amministrative. L'unica edizione client multi-sessione, Windows Enterprise multi-session, è offerta esclusivamente per Azure Virtual Desktop.
Perché il mio mutex per l'istanza singola non riesce a impedire gli avvii dalle sessioni di altri utenti?
Gli oggetti kernel con nome, come mutex, eventi e semafori, hanno ciascuno uno spazio dei nomi per sessione. Un mutex creato senza prefisso finisce nello spazio dei nomi della sessione corrente, quindi la sessione di un altro utente (o persino una seconda sessione dello stesso utente) viene trattata come un oggetto completamente nuovo e distinto, e la protezione dell'istanza singola fallisce silenziosamente. Se si desidera un'unica istanza a livello dell'intera macchina e tra tutte le sessioni, il nome deve essere esplicitamente preceduto dal prefisso Global\. Se si desidera un'istanza per utente che copra tutte le sessioni di quell'utente, si deve combinare Global\ con il SID dell'utente incorporato nel nome.
Come può la mia applicazione rilevare se è in esecuzione in una sessione desktop remoto?
Il controllo standard è l'API Win32 GetSystemMetrics(SM_REMOTESESSION), che restituisce un valore diverso da zero per una sessione remota; WPF espone lo stesso controllo tramite la proprietà SystemParameters.IsRemoteSession. Attenzione: nelle sessioni remote che usano RemoteFX vGPU, questa funzione segnala erroneamente una sessione remota come locale, e la soluzione alternativa documentata consiste nel confrontare il valore GlassSessionId del registro con l'ID della sessione corrente. La variabile d'ambiente SESSIONNAME (Console rispetto a RDP-Tcp#...) è utile per una diagnosi rapida ma non è un'API ufficiale. Non usare WTSGetActiveConsoleSessionId per identificare le sessioni RDP: restituisce solo la sessione collegata alla console fisica.

Profilo dell’autore

Pagina di presentazione dell’autore dell’articolo.

Go Komura

Rappresentante di KomuraSoft LLC

Specializzato nello sviluppo di software Windows, nella consulenza tecnica e nell’analisi dei malfunzionamenti, soprattutto nei progetti con sistemi esistenti e guasti difficili da riprodurre.

Torna al blog