From 78bc6560b82f0eaecf0dce0b0c01a3ce1e2776f2 Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Thu, 6 Aug 2026 10:40:16 +0200 Subject: [PATCH] Fail fast when no console session exists and keep the SAS value type --- client/vnc/server/agent_windows.go | 19 +++++++++++++++++-- client/vnc/server/server_windows.go | 8 +++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/client/vnc/server/agent_windows.go b/client/vnc/server/agent_windows.go index 57fb983a7..865ebd4d7 100644 --- a/client/vnc/server/agent_windows.go +++ b/client/vnc/server/agent_windows.go @@ -88,6 +88,11 @@ const ( // 1. Active session with a user logged in (RDP user in session ≥2) // 2. Active session without a user (console at login screen) // 3. Console session ID +// +// noActiveSession is what the WTS APIs report when no session is attached to +// the console, which is the state a headless or freshly booted machine sits in. +const noActiveSession = uint32(0xFFFFFFFF) + func getActiveSessionID() uint32 { var sessionInfo uintptr var count uint32 @@ -402,7 +407,7 @@ const ( ) func newSessionManager() *sessionManager { - m := &sessionManager{sessionID: ^uint32(0), done: make(chan struct{})} + m := &sessionManager{sessionID: noActiveSession, done: make(chan struct{})} if h, err := createKillOnCloseJob(); err != nil { log.Warnf("create job object for vnc-agent (orphan agents possible after crash): %v", err) } else { @@ -498,6 +503,16 @@ func (m *sessionManager) Resolve(ctx context.Context) (string, string, uint32, e return socketPath, token, 0, nil } + // With no session on the console there is nothing to wait for: the + // manager will not spawn an agent, so say so now rather than after the + // deadline, and say it with the error the caller turns into a message + // about nobody being logged in. Queried live instead of read from the + // manager, whose sessionID starts out as the same sentinel and would + // make this fire during the first tick after startup. + if getActiveSessionID() == noActiveSession { + return "", "", 0, errNoConsoleUser + } + select { case <-ctx.Done(): return "", "", 0, ctx.Err() @@ -622,7 +637,7 @@ func (m *sessionManager) scheduleNextSpawn(exitCode uint32, lifetime time.Durati // window has elapsed. Returns false to permanently stop the manager when the // service lacks the privileges needed to spawn cross-session. func (m *sessionManager) maybeSpawnAgent(sid uint32) bool { - if m.agentProc != 0 || sid == 0xFFFFFFFF || !time.Now().After(m.nextSpawnAt) { + if m.agentProc != 0 || sid == noActiveSession || !time.Now().After(m.nextSpawnAt) { return true } diff --git a/client/vnc/server/server_windows.go b/client/vnc/server/server_windows.go index a6b04a3fc..84145376c 100644 --- a/client/vnc/server/server_windows.go +++ b/client/vnc/server/server_windows.go @@ -112,8 +112,14 @@ func enableSoftwareSAS() { defer sasStateMu.Unlock() if !savedSASState.captured { - prev, _, err := key.GetIntegerValue("SoftwareSASGeneration") + prev, valType, err := key.GetIntegerValue("SoftwareSASGeneration") switch { + case err == nil && valType != registry.DWORD: + // Restoring writes a DWORD, so anything else would come back with a + // different type than the administrator set, even when the number + // itself fits. + log.Warnf("SoftwareSASGeneration has registry type %d rather than DWORD, leaving it alone", valType) + return case err == nil && prev > math.MaxUint32: // A DWORD is what the policy takes, so a wider value is not ours to // narrow and restore.