Cómo entender el aislamiento de sesiones de Windows — Session 0, RDP y la ejecución simultánea de varios usuarios
· Go Komura · Session 0, Aislamiento de sesiones, RDP, Escritorio remoto, Servicios de Windows, Multiusuario, Windows, .NET, C#, Arquitectura, Consultoría técnica
«Un cuadro de diálogo generado por un servicio es invisible para todos.» «En el momento en que alguien se conectó por RDP, la pantalla de la máquina física pasó a la pantalla de inicio de sesión.» «La protección de instancia única debía impedir los inicios dobles, pero iniciar sesión como otro usuario alegremente arrancó una segunda copia.» Los encargos de consultoría sobre aplicaciones de negocio hacen aflorar una y otra vez estos síntomas, y detrás de todos ellos hay la misma carencia: una comprensión incompleta del concepto de «sesión» de Windows. Las sesiones atraviesan tanto el diseño de servicios como la operación del escritorio remoto y la protección de instancia única de objetos con nombre, y sin embargo es raro ver este tema explicado de forma sistemática en un solo lugar.
Este artículo organiza, desde un punto de vista práctico, por qué se introdujo el aislamiento Session 0, cómo se comportan las sesiones al conectarse por RDP, cómo se aíslan por sesión los objetos con nombre, y los errores de diseño habituales que aparecen en entornos de PC compartidos y RDS (Servicios de Escritorio remoto).
1. Lo esencial primero
- Los servicios se ejecutan en una sesión dedicada llamada Session 0, aislada de la sesión interactiva de cualquier usuario que haya iniciado sesión. Un servicio no puede mostrar directamente un cuadro de diálogo, y aunque lo intente, el usuario nunca lo verá.1
- La solución oficialmente recomendada para un servicio que necesita interfaz de usuario consiste en separar la UI en un proceso distinto (que reside en la sesión del usuario) y comunicarse con él mediante IPC. Lanzar un programa con interfaz como usuario interactivo mediante el Programador de tareas es otra opción disponible.2
- Windows Server (RDS) está construido en torno a varias sesiones simultáneas, pero los sistemas operativos cliente como Windows 10/11 Pro son fundamentalmente de sesión única. Aparte de la edición especial «Enterprise multisession» pensada para Azure Virtual Desktop, las instalaciones cliente habituales no están diseñadas para albergar varias sesiones interactivas simultáneas.3
- Los objetos con nombre del kernel (mutex, eventos, semáforos, etc.) tienen cada uno un espacio de nombres propio por sesión. Si desea reducir un objeto a una única instancia en toda la máquina y a través de las sesiones, debe anteponer explícitamente al nombre el prefijo
Global\; de lo contrario, la protección de instancia única no se comportará como se espera. Las canalizaciones con nombre quedan fuera de este mecanismo: de forma predeterminada usan un espacio de nombres distinto, accesible en toda la máquina (y de forma remota, si el servicio de servidor está en ejecución); si necesita distinguir por sesión o por usuario, debe incrustar algo como el ID de sesión en el propio nombre de la canalización.45 - Puede determinar el tipo de la sesión actual mediante la variable de entorno
SESSIONNAMEoGetSystemMetrics(SM_REMOTESESSION). Sin embargo, hay casos —por ejemplo, cuando se usa RemoteFX vGPU— en los que la detección se desvía del comportamiento previsto, así que no confíe en ella ciegamente.67 - Si el comportamiento en PC compartidos/RDS debe formar parte de sus requisitos es algo que hay que confirmar justo al principio de la fase de diseño. Las colisiones de rutas de archivos temporales, la confusión de AppData y el acceso simultáneo a dispositivos/dongles son los incidentes clásicos que estallan en el momento en que una aplicación construida para un único PC independiente se despliega en RDS (véase el capítulo 7).
2. Qué es el aislamiento Session 0 — y por qué se produjo la separación de las sesiones de usuario
Antes de Windows Vista / Windows Server 2008, un servicio y el primer usuario que iniciaba sesión compartían la misma Session 0. Los procesos dentro de la misma sesión pueden intercambiar mensajes de ventana independientemente de su nivel de privilegios respectivo. Un proceso con privilegios estándar podía enviar un mensaje manipulado a una ventana propiedad de un servicio que se ejecutaba como SYSTEM, secuestrar el procesamiento de mensajes del servicio y escalar sus privilegios; toda esta clase de escalada de privilegios, que explota fallos en el procesamiento de mensajes de ventana, se reportó repetidamente a lo largo de la década de 2000, y de hecho se han corregido a lo largo de los años varias vulnerabilidades de escalada de privilegios que explotaban fallos en el procesamiento de mensajes de ventana del kernel de Windows (win32k.sys).8
Este panorama cambió a partir de Windows Vista. Session 0 pasó a dedicarse exclusivamente a los servicios y a las aplicaciones no vinculadas a una sesión de usuario interactiva, y al primer usuario que inicia sesión se le asigna ahora la Session 1, al siguiente la Session 2, y así sucesivamente, cada uno en una sesión distinta de la del servicio. Session 0 no admite el funcionamiento interactivo, y los servicios no pueden enviar mensajes a las aplicaciones, ni las aplicaciones a los servicios. Un servicio tampoco puede mostrar directamente elementos de interfaz de usuario como un cuadro de diálogo. Con este cambio, se cerró estructuralmente precisamente la vía que permitía a un proceso con privilegios estándar enviar mensajes directamente a la ventana de un servicio.1
Dicho de otro modo, el aislamiento Session 0 no es simplemente una restricción operativa del tipo «los servicios son procesos en segundo plano, así que no tienen pantalla»; es una decisión de seguridad que bloquea estructuralmente el intercambio de mensajes de ventana a través de un límite de privilegios. La única vía que le queda a un servicio para mostrar algo en pantalla es la función WTSSendMessage, una API que muestra un simple cuadro de mensaje en otra sesión, pero que no puede usarse para cuadros de diálogo complejos ni para interacción bidireccional.1
3. Los servicios no pueden mostrar interfaz de usuario — restricciones prácticas y patrones para sortearlas
La consecuencia práctica del aislamiento Session 0 es simple. Si un servicio llama a MessageBox.Show o intenta mostrar un formulario, no aparece nada en la pantalla del usuario con sesión iniciada. Peor aún, es una causa clásica de que todo el proceso se cuelgue, esperando indefinidamente un botón OK que nadie podrá pulsar jamás. Al migrar código antiguo (algo que en su día funcionaba como «servicio interactivo» en la época de Windows XP) a un servicio, compruebe siempre que no hayan quedado llamadas de visualización de interfaz.
Existen dos soluciones oficialmente documentadas.2
- Separar la UI en un proceso distinto (que reside en la sesión del usuario interactivo) y comunicarse mediante IPC. El servicio realiza el trabajo y pasa los resultados o las solicitudes de entrada al proceso de UI. El proceso de UI los presenta al usuario como un icono en el área de notificación o como un cuadro de diálogo, y devuelve al servicio el resultado de la operación. Si se usa IPC como las canalizaciones con nombre, hay que diseñar las ACL de forma que nada quede expuesto involuntariamente en la red. En entornos donde varios usuarios tienen sesión iniciada a la vez, la recomendación es distinguir las sesiones, por ejemplo incrustando el ID de sesión en el nombre de la canalización.2 Para elegir el mecanismo de IPC, véase el artículo complementario publicado el mismo día, «Una tabla de decisión para la comunicación entre procesos en Windows». Cómo construir el propio servicio, junto con aspectos de diseño operativo como el tipo de inicio, la cuenta de ejecución y las opciones de recuperación, se trata en «Cómo crear un servicio de Windows».
- Lanzar un programa como usuario interactivo mediante el Programador de tareas. En lugar de mantenerlo residente como servicio, esta configuración lanza un programa con interfaz bajo los privilegios y la sesión propios del usuario con sesión iniciada. Aquí hay un punto al que prestar atención: una «tarea registrada bajo SYSTEM» nunca debe ser la que muestre la interfaz. La cuenta SYSTEM no tiene derechos de inicio de sesión interactivo, y un usuario no puede ver ni interactuar con un programa o una tarea que se ejecute con privilegios SYSTEM.9 El modelo de tarea elevada está pensado para que una tarea SYSTEM —cuyo descriptor de seguridad permite que un usuario estándar la inicie— asuma únicamente las operaciones que requieren privilegios de administrador, mientras que la interfaz de usuario en sí se ejecuta en un programa aparte bajo los privilegios y la sesión propios del usuario con sesión iniciada (o como esa misma operación registrada como tarea bajo la cuenta del usuario); esa es toda la premisa de este reparto de roles.10 Mantenga la tarea SYSTEM estrictamente como un «intermediario sin interfaz» y deje que la interfaz visible se ejecute siempre en un proceso dentro de la propia sesión del usuario.
Sea cual sea el enfoque elegido, lo esencial es incorporar desde el primer día en el diseño el principio de que «el servicio permanece entre bastidores, la interfaz vive en un proceso separado». Lo que se observa habitualmente en la práctica es que intentar añadir esto más tarde choca con código ya profundamente dependiente de la interfaz dentro del servicio, lo que dispara el coste de separarlos.
4. Cómo se comportan las sesiones a través de RDP — la diferencia entre RDS y un sistema operativo cliente
La confusión de «es el mismo RDP en el servidor y en el cliente, ¿por qué se comporta de forma distinta?» proviene de si está presente o no el rol Servicios de Escritorio remoto (RDS).
Windows Server con el rol RDS está diseñado en torno a varios usuarios que inician sesión simultáneamente en un mismo servidor, cada uno trabajando en una sesión independiente con su propio escritorio y sus propias aplicaciones: una configuración construida desde el principio sobre varias sesiones simultáneas.11 Bajo RDS, las conexiones administrativas RDP habituales (para administradores) admiten hasta dos sesiones sin necesitar una CAL, pero superar ese número, o permitir que usuarios normales se conecten simultáneamente, exige desplegar el rol RD Session Host junto con las licencias RDS CAL (licencia de acceso de cliente) adecuadas.12 Las CAL existen en dos modelos —por dispositivo y por usuario— y los detalles de licencia deben confirmarse para cada entorno.13
Por otro lado, un sistema operativo cliente habitual como Windows 10/11 Pro o Enterprise no incluye el rol RDS y es, en principio, de sesión única. Existe una edición especial de sistema cliente llamada «Windows Enterprise multisession» capaz de mantener varias sesiones interactivas a la vez, pero se ofrece exclusivamente para Azure Virtual Desktop; las instalaciones cliente habituales, distribuidas en local, no están diseñadas para admitir varias sesiones de usuario simultáneas.3 En la práctica, en línea con la experiencia habitual de «me conecté por RDP al PC de un compañero y la pantalla de su puesto pasó a la pantalla de bloqueo», es más seguro asumir que en los sistemas cliente una sesión de consola física (alguien operando la máquina localmente) y una conexión remota en principio no pueden coexistir al mismo tiempo.
He aquí un resumen de las diferencias que importan para el diseño de aplicaciones de negocio.
| Aspecto | Windows Server + RDS | Sistema operativo cliente (Windows 10/11 Pro, etc.) |
|---|---|---|
| Sesiones simultáneas | Varios usuarios pueden iniciar sesión simultáneamente11 | Fundamentalmente de sesión única. No se admiten varias sesiones interactivas fuera de la edición multisesión exclusiva de AVD3 |
| Licencia | Requiere RD Session Host + licencias RDS CAL (más allá de las dos conexiones administrativas)12 | La propia función de Escritorio remoto viene incluida en el sistema operativo, pero los requisitos de licencia y las ediciones permitidas deben confirmarse por separado |
| Uso típico | Ejecución de aplicaciones de negocio en terminales compartidos, entornos de cliente ligero | Mantenimiento remoto de un PC personal, acceso en teletrabajo |
No tener en cuenta esta diferencia —asumiendo que «funcionaba en el sistema operativo cliente de mi máquina de desarrollo, así que debería funcionar igual en el entorno RDS del servidor compartido»— hará que pase por alto los errores específicos de la concurrencia multiusuario tratados en el capítulo 7. A la inversa, si solo se prevé un funcionamiento de sesión única en un sistema cliente, no es necesario asumir de antemano el coste de diseño del soporte de PC compartidos. Esta es una decisión que debe tomarse pronto, durante la definición de requisitos (capítulo 8).
5. Determinar la sesión actual
Hay muchas situaciones en las que se desea saber en qué sesión se está ejecutando actualmente el propio proceso, por ejemplo para suprimir un efecto de renderizado costoso durante un procesamiento en segundo plano, o para desactivar una función que consume mucho ancho de banda en una sesión remota. Existen varias formas de determinarlo.
- La variable de entorno
SESSIONNAME: se establece enConsolepara la sesión de consola, o en un nombre que empieza porRDP-Tcp#para una conexión RDP. Es la misma información que se muestra en la columnaSESSIONNAMEque lista el comandoqwinsta.14 Sirve para una comprobación rápida, pero, como no es una API oficial, es más seguro no confiar en ella demasiado. GetSystemMetrics(SM_REMOTESESSION): una API de Win32 que indica si se está en una sesión remota. Devuelve un valor distinto de cero si el proceso que la llama está asociado a una sesión de cliente de Terminal Services. Sin embargo, hay una advertencia: a partir de Windows 8/Server 2012, en las sesiones remotas que usan RemoteFX vGPU, esta función informa erróneamente de una sesión remota como local. En ese caso, la solución documentada consiste en comparar el valorGlassSessionIddel registro con el ID de sesión actual.67SystemParameters.IsRemoteSessionde .NET (WPF): expone la misma comprobación que realizaGetSystemMetrics(SM_REMOTESESSION), como una propiedad legible desde una aplicación WPF.15 Desde WinForms o una aplicación de consola, habría que llamarla directamente mediante P/Invoke.WTSGetActiveConsoleSessionId: una API que obtiene el ID de la sesión conectada a la consola física. Compararlo con el propio ID de sesión permite determinar «¿estoy ejecutándome en la consola física?». Tenga en cuenta que cuando nadie está conectado a la consola física (es decir, durante una transición), devuelve0xFFFFFFFF.16
No use WTSGetActiveConsoleSessionId para identificar sesiones en un entorno RDS. Fiel a su nombre, todo lo que devuelve es «la sesión conectada a la consola física»; un usuario conectado por RDP no queda cubierto.16 Si un servicio necesita saber en qué sesión mostrar su UI complementaria, esta API es suficiente cuando solo se espera un usuario conectado a la consola, pero en un entorno RDS donde varios usuarios RDP pueden tener sesión iniciada simultáneamente, en su lugar se deberían enumerar las sesiones en ejecución con WTSEnumerateSessions17, o simplemente usar el ID de sesión que proporciona una notificación de cambio de sesión (WM_WTSSESSION_CHANGE). Esta confusión es una causa clásica y difícil de detectar de errores en los que la interfaz de notificación simplemente no aparece —o aparece en la sesión equivocada— específicamente para los usuarios conectados por RDP. Tenga muy presente esta distinción al decidir en qué sesión lanzar el proceso de UI para la configuración de IPC descrita en el capítulo 3.
6. Aislamiento por sesión de los objetos con nombre
Los objetos con nombre del kernel —mutex, eventos, semáforos, temporizadores esperables, objetos de asignación de archivos— tienen cada uno un espacio de nombres independiente por sesión. Aunque se cree en una sesión un mutex llamado MyAppMutex, el intento de abrir un objeto con el mismo nombre desde una sesión distinta crea un objeto completamente nuevo y diferente. Para los procesos que se ejecutan principalmente en Session 0, como los servicios, o para configuraciones cliente/servidor que quieren compartir un objeto entre varias sesiones, se puede colocar explícitamente en el espacio de nombres global anteponiendo al nombre el prefijo Global\. A la inversa, anteponer Local\ lo coloca explícitamente en el espacio de nombres de la propia sesión.4
Aquí es donde esto realmente importa en la práctica: el mutex utilizado para la protección de instancia única.
- Si todo lo que se desea es «impedir que el mismo usuario lance dos copias de la aplicación dentro de la misma sesión», basta con el nombre de mutex predeterminado (sin prefijo). El espacio de nombres por sesión entra en juego automáticamente, de modo que la sesión de un usuario distinto se trata como un mutex diferente.
- Si se desea que «incluso con varios usuarios con sesión iniciada simultáneamente en un entorno RDS, la aplicación se limite a una única instancia en toda la máquina», ese objetivo solo puede lograrse usando explícitamente
Global\. Dejado en el valor predeterminado, cada usuario acaba pudiendo lanzar su propia copia, un error que se manifiesta como «la protección de instancia única debía funcionar, pero de todos modos se lanzaron varias instancias».
Tenga en cuenta también que un proceso fuera de Session 0 necesita el privilegio SeCreateGlobalPrivilege para crear un nuevo objeto de asignación de archivos o un objeto de vínculo simbólico en el espacio de nombres global (simplemente abrir un objeto existente no lo requiere). Esta comprobación de privilegios se aplica solo a las asignaciones de archivos y a los vínculos simbólicos —no se impone a la creación de mutex o eventos—, pero conviene recordarla para cualquier diseño que use memoria compartida (un archivo asignado en memoria) en el espacio de nombres global.4
Existe también un principio general para las aplicaciones cliente/servidor: la recomendación es no equiparar «una conexión desde una máquina» a «una sesión de usuario». Dado que desde la misma máquina se pueden establecer simultáneamente varias sesiones (las conexiones RDP de varios usuarios), se recomienda que el lado del servidor identifique las sesiones, por ejemplo, con ProcessIdToSessionId, y que aprovisione un canal de comunicación distinto por sesión.18
7. Errores de diseño habituales en PC compartidos y entornos RDS
Hay un puñado de patrones de fallo recurrentes que estallan en el momento en que una aplicación diseñada pensando solo en un uso de PC único y solo con consola se despliega en un PC compartido o un entorno RDS.
- Colisiones de rutas de archivos temporales. De forma predeterminada, RDS crea una carpeta temporal separada por sesión (bajo el perfil del usuario, con el ID de sesión incrustado en el nombre).19 Dicho de otro modo, si la aplicación simplemente usa la variable de entorno
%TEMP%, obtiene gratis el beneficio de este aislamiento. Los problemas empiezan cuando la aplicación codifica de forma fija una ruta comoC:\Work\tempen lugar de usar%TEMP%. Si varias sesiones de la misma cuenta de usuario, o varios usuarios, escriben simultáneamente en la misma ruta fija, por supuesto se disputarán el archivo. - Confusión de AppData. Dónde guardar la configuración de usuario, las cachés y los registros debe decidirse con una buena comprensión del mecanismo de perfiles de usuario de Windows: local frente a itinerante, y cuándo usar
%LOCALAPPDATA%en lugar de%APPDATA%. En un PC compartido, un diseño descuidado en este punto tiende a manifestarse como «mi pantalla cambió por la configuración de otra persona» o «el registro se sobrescribió». Véase «Una introducción a los perfiles de usuario de Windows» para más detalles. - Suponer acceso simultáneo a dispositivos, dongles y puertos serie. El acceso a un dispositivo local del lado del cliente o a un puerto serie desde una sesión RDS pasa por un mecanismo de redirección. Si se intenta que varias sesiones usen simultáneamente una aplicación que depende de un dongle USB físico o de un dispositivo de comunicación serie, surgen problemas en los que, o bien el propio hardware puede no admitir el acceso concurrente de entrada, o bien la visibilidad depende de la configuración de redirección y puede estar presente o no. Cómo aparece un dispositivo conectado del lado del cliente desde una sesión del lado del servidor depende de la configuración de redirección, por lo que debe confirmarse en la fase de diseño.20
- Impresoras y unidades codificadas de forma fija. Codificar de forma fija un nombre de impresora predeterminada o una letra de unidad dentro de una aplicación se rompe fácilmente en un entorno de PC compartido donde la impresora predeterminada redirigida o la unidad asignada difieren según el usuario. Es más seguro obtener el nombre de la impresora en tiempo de ejecución y, siempre que sea posible, tratar las unidades como rutas UNC.
- Interpretación errónea de HKCU / del registro por usuario. También conviene señalar que
HKEY_CURRENT_USERestá vinculado al usuario con sesión iniciada, no a la sesión. En una configuración donde el mismo usuario tiene varias sesiones (por ejemplo, al compartir la misma cuenta en RDS), los valores de HKCU se comparten entre todas las sesiones de ese usuario, por lo que almacenar en HKCU un estado de tiempo de ejecución que en realidad se desea aislado por sesión hará que se filtre de forma involuntaria a otras sesiones. La información que requiere aislamiento por sesión debería usar en su lugar los espacios de nombres por sesión del capítulo 6, o una ruta de archivo que incorpore el ID de sesión.
Todos estos son el tipo de error que «nunca se notaría en un único PC independiente» y que «solo se manifiesta cuando varios usuarios realmente usan la aplicación al mismo tiempo», por lo que el desarrollo típico es que un entorno de pruebas formado solo por una única máquina de desarrollo independiente nunca lo reproduce, y aparece por primera vez en un PC compartido o un entorno RDS de producción.
8. Tabla de decisión — ¿debe el soporte de «PC compartido/RDS» formar parte de los requisitos?
Si el comportamiento en PC compartido/RDS forma parte de sus requisitos es algo que debe confirmarse durante la definición de requisitos, antes de que comience la implementación. Una vez decidido incluirlo, use los siguientes puntos como lista de comprobación.
| Aspecto | Está bien asumir solo PC independiente/consola | Se necesita soporte de PC compartido/RDS (multiusuario simultáneo) |
|---|---|---|
| Archivos temporales / configuración | Una ruta fija causa poco daño real | Exigir rutas por usuario/por sesión como %TEMP% / %LOCALAPPDATA% (capítulo 7) |
| Objetos con nombre | Está bien dejar el valor predeterminado (espacio de nombres de sesión) | Si se necesita «una instancia en toda la máquina», usar explícitamente Global\. Si basta con «una por sesión», dejar el valor predeterminado tal cual. Si se desea «una por usuario, que abarque las sesiones de ese usuario», combinar Global\ con el SID del usuario (capítulo 6; véase también el artículo sobre protección de instancia única) |
| Dispositivos / dongles | Conexión local directa y simple | Diseñar teniendo en cuenta que el acceso pasa por redirección y que el acceso concurrente puede no ser posible |
| UI y servicios | Fácil mantener todo en un solo escritorio | Separar en un proceso distinto + IPC, a la luz del aislamiento Session 0 (capítulo 3) |
| Licencia / facturación | Fácil de contar por número de máquinas | Confirmar el concepto de número de sesiones simultáneas y si se requieren licencias RDS CAL, durante la definición de requisitos13 |
El eje de la decisión es simple: acuerde con el cliente al principio del desarrollo si «esta aplicación solo debe funcionar de forma autónoma en el PC de una sola persona» o si «puede que la usen varias personas a la vez en un terminal compartido o un entorno RDS». Deje este punto ambiguo mientras avanza la implementación y acabará teniendo que reconstruir desde cero cada uno de los puntos anteriores.
9. Ejemplo de implementación — usar con criterio la detección de sesión y los espacios de nombres
He aquí cómo se combinan en código .NET los métodos de detección y las decisiones de nomenclatura de mutex de los capítulos 5 y 6.
using System.Runtime.InteropServices;
internal static class SessionInfo
{
[DllImport("user32.dll")]
private static extern int GetSystemMetrics(int nIndex);
private const int SM_REMOTESESSION = 0x1000;
// Para una app WPF, System.Windows.SystemParameters.IsRemoteSession devuelve el mismo resultado
public static bool IsRemoteSession() => GetSystemMetrics(SM_REMOTESESSION) != 0;
// Comprobación rápida y aproximada. No es una API oficial, así que resérvela
// para registro/diagnóstico y no para decisiones importantes
public static string? SessionName() =>
Environment.GetEnvironmentVariable("SESSIONNAME");
}
El mutex utilizado para la protección de instancia única debe distinguir tres casos: «uno en toda la máquina», «uno por sesión» y «uno por usuario, que abarque las sesiones de ese usuario». El tercer caso (por usuario, a través de las sesiones) no se puede lograr solo con el Global\ predeterminado: el nombre también debe incluir el SID del usuario. Esto se trata en detalle en «Impedir los inicios dobles en aplicaciones Windows».
// Requiere: using System.Security.AccessControl; y using System.Security.Principal;
// (también requiere el paquete NuGet System.Threading.AccessControl)
// Intención: incluso con varios usuarios con sesión iniciada simultáneamente en
// RDS/un PC compartido, mantener exactamente una instancia en toda la máquina.
// Mutex/MutexAcl.Create de .NET solicita internamente SYNCHRONIZE y
// MUTEX_MODIFY_STATE, además de DELETE/READ_CONTROL/WRITE_DAC/WRITE_OWNER,
// incluso solo para abrir un mutex existente. Esto significa que, a menos que se
// conceda a «Usuarios autenticados» el equivalente de FullControl, un usuario
// legítimo distinto del creador obtendrá una UnauthorizedAccessException en el
// momento de comprobar createdNew.
// (La contrapartida: esto también deja la ACL modificable posteriormente por
// cualquier usuario autenticado. Si eso es inaceptable, prescinda de la clase
// Mutex y llame a CreateMutexEx directamente mediante P/Invoke, solicitando solo
// los derechos de acceso mínimos necesarios)
//
// Precaución: esta ACL solo se aplica si usted es quien logró crear el mutex
// primero. Un mutex Global\ con nombre fijo corre el riesgo de que otro usuario
// lo cree de antemano (ocupación de nombre), en cuyo caso createdGlobal devuelve
// false, o se obtiene una UnauthorizedAccessException según la ACL de esa otra
// parte. Si realmente necesita excluir a los demás, combine un nombre difícil de
// adivinar con otro mecanismo de exclusión (véase el capítulo 5 del artículo
// complementario para más detalles)
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);
// Intención: basta con una instancia por sesión (varias sesiones del mismo
// usuario pueden tratarse perfectamente por separado).
// Dejarlo en el espacio de nombres de sesión predeterminado tal cual
using var perSessionMutex = new Mutex(
initiallyOwned: false,
name: "KomuraSoft.MyApp.SingleInstance",
createdNew: out bool createdPerSession);
if (!createdGlobal)
{
// Ya se está ejecutando en otro lugar de la máquina, incluidas otras sesiones
return;
}
Crear un mutex con el prefijo Global\ no requiere en sí mismo ningún privilegio especial (como se mencionó antes, la comprobación de privilegios solo se aplica a la creación de nuevos objetos de asignación de archivos y vínculos simbólicos).4 Es un error dejar el mutex en el espacio de nombres de sesión predeterminado si la intención es «uno por usuario». No es raro que el mismo usuario mantenga simultáneamente una sesión RDP desconectada y una nueva sesión de consola/RDP, y en ese caso cada sesión se trata como un mutex separado, de modo que, dejando el valor predeterminado tal cual, el mismo usuario puede acabar lanzando una segunda instancia. Cuál de «uno en toda la máquina», «uno por sesión» o «uno por usuario a través de las sesiones» es el requisito correcto depende de la naturaleza de la aplicación, así que confirme el requisito antes de decidirse por un esquema de nomenclatura.
10. Resumen
Una «sesión» de Windows es un concepto que sigue reapareciendo en temas que a primera vista parecen no estar relacionados entre sí: el diseño de servicios, la operación del escritorio remoto, la protección de instancia única de objetos con nombre. La estructura central se reduce a tres puntos. Un servicio se ejecuta en Session 0, una sesión especial y aislada, y no puede mostrar directamente interfaz de usuario. RDS está construido en torno a varias sesiones simultáneas, mientras que un sistema operativo cliente habitual es fundamentalmente de sesión única. Y los objetos con nombre tienen un espacio de nombres por sesión, por lo que hay que juzgar, según la intención, si se necesita Global\.
Si el comportamiento en PC compartido/RDS forma parte de sus requisitos es un tema en el que notarlo tarde durante la implementación provoca una gran cantidad de retrabajo. Recomendamos encarecidamente confirmar, durante la definición de requisitos, «quién usará esta aplicación, en qué máquinas y cuántas personas a la vez». Si planea desplegar una aplicación existente en un entorno de PC compartido o RDS, o necesita investigar un error en el que «las cosas van mal cuando varios usuarios la usan», suele ser más rápido diagnosticar observando la configuración real; no dude en contactarnos.
Artículos relacionados
- Cómo crear un servicio de Windows — una guía de Worker Service
- Una tabla de decisión para la comunicación entre procesos en Windows
- Impedir los inicios dobles en aplicaciones Windows — mutex con nombre y activación de una instancia existente
- Una introducción a los perfiles de usuario de Windows — AppData y NTUSER.DAT
- Una forma concreta de aislar «solo las operaciones que requieren derechos de administrador» en una aplicación Windows
Áreas de consultoría relacionadas
KomuraSoft LLC (合同会社小村ソフト) se encarga de revisiones de diseño de aplicaciones Windows pensadas para un posible despliegue en PC compartidos o entornos RDS, del diseño de la separación servicio/IPC, y de la investigación de causas raíz de errores específicos del uso simultáneo por varios usuarios.
- Consultoría técnica / Revisión de diseño
- Desarrollo de aplicaciones Windows
- Modernización y mantenimiento de software Windows existente
- Contacto
Referencias
-
Microsoft Learn, Service Changes for Windows Vista. Sobre los antecedentes de la introducción del aislamiento Session 0, la consiguiente imposibilidad de que servicios y aplicaciones intercambien mensajes de ventana, y la función
WTSSendMessagecomo alternativa. ↩ ↩2 ↩3 -
Microsoft Learn, Interactive Services. Sobre por qué un servicio no puede interactuar directamente con un usuario, el diseño que usa
CreateProcessAsUserpara lanzar un proceso GUI aparte y coordinar mediante IPC, y la recomendación de distinguir los nombres de canalización por ID de sesión. ↩ ↩2 ↩3 -
Microsoft Learn, Windows Enterprise multi-session FAQ. Sobre la capacidad multisesión —mantener varias sesiones interactivas a la vez—, antes posible solo en Windows Server y ahora ofrecida exclusivamente para Azure Virtual Desktop. ↩ ↩2 ↩3
-
Microsoft Learn, Kernel object namespaces. Sobre el espacio de nombres por sesión de los objetos con nombre del kernel, los prefijos
Global\/Local\, y el requisito del privilegioSeCreateGlobalPrivilegepara crear un nuevo objeto de asignación de archivos o vínculo simbólico en el espacio de nombres global. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, Named Pipes. Sobre el hecho de que las canalizaciones con nombre son accesibles desde cualquier proceso dentro de los límites de las comprobaciones de seguridad, y alcanzables de forma remota si el servicio de servidor está en ejecución (la base de por qué se trata de un mecanismo distinto de los espacios de nombres de sesión
Global\/Local\que usan los mutex, etc.). ↩ -
Microsoft Learn, GetSystemMetrics function (winuser.h). Sobre la especificación de la detección de una sesión remota mediante
SM_REMOTESESSION. ↩ ↩2 -
Microsoft Learn, Detecting the Remote Desktop Services environment. Sobre un ejemplo de código para
GetSystemMetrics(SM_REMOTESESSION), la limitación por la cual esta función detecta erróneamente una sesión remota como local al usar RemoteFX vGPU, y la detección alternativa mediante la clave de registroGlassSessionId. ↩ ↩2 -
Microsoft Security Bulletin, MS12-034 - Windows and Messages Vulnerability (CVE-2012-0180). Sobre una vulnerabilidad de escalada de privilegios que explota un fallo en el procesamiento de mensajes de ventana del controlador en modo kernel de Windows (win32k.sys) (un ejemplo de esta clase de ataque mediante mensajes de ventana). ↩
-
Microsoft Learn, schtasks create. Sobre el hecho de que la cuenta SYSTEM no tiene derechos de inicio de sesión interactivo, y que un usuario no puede ver ni interactuar con un programa o una tarea que se ejecute con privilegios SYSTEM. ↩
-
Microsoft Learn, Elevated Task Model. Sobre la configuración en la que una aplicación de usuario estándar realiza operaciones que requieren privilegios de administrador mediante una tarea registrada como SYSTEM pero con un descriptor de seguridad configurado para que un usuario estándar pueda iniciarla de todos modos. ↩
-
Microsoft Learn, Remote Desktop Services overview in Windows Server. Sobre el hecho de que RDS es la base que proporciona escritorios multisesión basados en sesiones. ↩ ↩2
-
Microsoft Learn, Troubleshoot Remote desktop disconnected errors. Sobre el hecho de que hasta dos conexiones remotas simultáneas son posibles con fines administrativos sin una licencia RDS CAL, y que más allá de eso se requieren el rol RD Session Host y las licencias RDS CAL adecuadas. ↩ ↩2
-
Microsoft Learn, License Remote Desktop Services with Client Access Licenses (CALs). Sobre la diferencia entre los modelos de licencia RDS CAL por dispositivo y por usuario, y el mecanismo de emisión/seguimiento del servidor de licencias. ↩ ↩2
-
Microsoft Learn, qwinsta. Sobre el hecho de que la columna
SESSIONNAMEmuestra el nombre asignado a una sesión (consolepara la consola, o un nombre que empieza porrdp-tcp#para una conexión RDP). ↩ -
Microsoft Learn, SystemParameters.IsRemoteSession Property. Sobre la propiedad que expone desde WPF la comprobación equivalente a
SM_REMOTESESSION. ↩ -
Microsoft Learn, WTSGetActiveConsoleSessionId function (winbase.h). Sobre la obtención del ID de sesión conectado a la consola física, y la devolución de
0xFFFFFFFFdurante una transición. ↩ ↩2 -
Microsoft Learn, WTSEnumerateSessions function (wtsapi32.h). Sobre la posibilidad de enumerar todas las sesiones en un servidor RD Session Host. ↩
-
Microsoft Learn, Client/Server application guidelines. Sobre por qué «una conexión desde una máquina» no debe equipararse a «una sesión de usuario», y la identificación de sesiones mediante
ProcessIdToSessionId. ↩ -
Microsoft Learn, Policy CSP - ADMX_TerminalServer (TS_TEMP_PER_SESSION). Sobre el hecho de que Servicios de Escritorio remoto crea de forma predeterminada una carpeta temporal separada por sesión (bajo el perfil de usuario, con el ID de sesión en el nombre). ↩
-
Microsoft Learn, Peripheral hardware guidelines. Sobre cómo aparecen las unidades e impresoras del lado del cliente desde una sesión en un servidor RD Session Host según la configuración de redirección, y el mecanismo que permite a un controlador de dispositivo deshabilitar la redirección. ↩
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Iconos de la bandeja del sistema y notificaciones toast en aplicaciones Windows — los escollos de NotifyIcon y cómo elegir el AppNotification adecuado
Una guía práctica para mantener una aplicación Windows empresarial residente en la bandeja del sistema (área de notificación) y avisar al...
¿Hasta cuándo seguirán funcionando las aplicaciones VB6? — Estado del soporte del runtime y un camino práctico hacia la migración a .NET
¿Hasta cuándo seguirán funcionando las aplicaciones VB6? Este artículo aclara la asimetría entre la política de soporte del runtime de VB...
Era japonesa, festivos y fechas de cierre en aplicaciones empresariales: diseño resiliente a los cambios de era, JapaneseCalendar y cálculo de días hábiles en la práctica
Mostrar «Reiwa 8» en un informe, calcular días hábiles excluyendo festivos, pagar al final del mes siguiente tras un cierre el día 20: el...
Entender de verdad el modelo OSI — diseccionar una única petición HTTP en sus siete capas
Entendemos el modelo OSI a través de la práctica real en lugar de la memorización. Ensamblamos y diseccionamos en C# la trama Ethernet qu...
Internacionalización de aplicaciones WinForms/WPF: la práctica de resx, ensamblados satélite y cambio de cultura
Organizamos desde una perspectiva práctica la internacionalización de aplicaciones de escritorio Windows: la diferencia entre CurrentCult...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
Aplicaciones empresariales, integración de dispositivos y herramientas de comunicación, de los requisitos al desarrollo.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- ¿Por qué un servicio de Windows no puede mostrar un cuadro de diálogo?
- Desde Windows Vista, los servicios se ejecutan en una Session 0 dedicada, aislada de la sesión interactiva de cualquier usuario que haya iniciado sesión, por lo que una interfaz de usuario generada por un servicio es invisible para todos, y una llamada a MessageBox puede colgar el proceso indefinidamente esperando un botón OK que nadie puede pulsar. Este aislamiento es una decisión de seguridad que bloquea estructuralmente el intercambio de mensajes de ventana a través de un límite de privilegios, cerrando así toda una clase de ataques de escalada de privilegios. La solución oficialmente recomendada consiste en separar la interfaz de usuario en un proceso distinto que reside en la sesión del usuario y comunicarse con el servicio mediante IPC, como las canalizaciones con nombre; lanzar un programa con interfaz como usuario interactivo mediante el Programador de tareas es otra opción documentada.
- ¿Pueden varios usuarios conectarse a Windows 10 u 11 por RDP al mismo tiempo?
- No: los sistemas operativos cliente habituales, como Windows 10/11 Pro o Enterprise, son fundamentalmente de sesión única, por lo que una sesión de consola física y una conexión remota en principio no pueden coexistir; conectarse por RDP hace que la pantalla local pase a la pantalla de bloqueo. Varias sesiones interactivas simultáneas requieren Windows Server con el rol Servicios de Escritorio remoto, que necesita el rol RD Session Host y licencias RDS CAL además de las dos conexiones administrativas. La única edición cliente con soporte multisesión, Windows Enterprise multisession, se ofrece exclusivamente para Azure Virtual Desktop.
- ¿Por qué mi mutex de instancia única no impide los inicios desde sesiones de otros usuarios?
- Los objetos con nombre del kernel, como los mutex, los eventos y los semáforos, tienen cada uno un espacio de nombres propio por sesión. Un mutex creado sin prefijo termina en el espacio de nombres de la sesión actual, por lo que la sesión de otro usuario (o incluso una segunda sesión del mismo usuario) se trata como un objeto completamente nuevo y distinto, y la protección de instancia única falla silenciosamente. Si desea una única instancia en toda la máquina y en todas las sesiones, debe anteponer explícitamente al nombre el prefijo Global\. Si desea una instancia por usuario que abarque todas las sesiones de ese usuario, combine Global\ con el SID del usuario incrustado en el nombre.
- ¿Cómo puede mi aplicación detectar si se está ejecutando en una sesión de escritorio remoto?
- La comprobación estándar es la API de Win32 GetSystemMetrics(SM_REMOTESESSION), que devuelve un valor distinto de cero para una sesión remota; WPF expone la misma comprobación mediante la propiedad SystemParameters.IsRemoteSession. Tenga en cuenta que en las sesiones remotas que usan RemoteFX vGPU esta función informa erróneamente de una sesión remota como local, y la solución documentada consiste en comparar el valor GlassSessionId del registro con el ID de sesión actual. La variable de entorno SESSIONNAME (Console frente a RDP-Tcp#...) sirve para un diagnóstico rápido, pero no es una API oficial. No use WTSGetActiveConsoleSessionId para identificar sesiones RDP: solo devuelve la sesión conectada a la consola física.
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.
Enlaces públicos