Refuse an ambiguous X display and settle the approval timeout race under one claim

Claude-Session: https://claude.ai/code/session_01QKDYfH4WKLbpNQHccpVo3P
This commit is contained in:
Viktor Liu
2026-08-29 17:25:02 +02:00
parent fecd7cfff5
commit 12040b1be0
8 changed files with 307 additions and 94 deletions
+68 -22
View File
@@ -65,15 +65,32 @@ func detectX11Display() {
return
}
// Try /proc first (Linux), then ps fallback (FreeBSD and others).
if detectX11FromProc() {
// Try /proc first (Linux), then the socket scan (FreeBSD and others).
switch detectX11FromProc() {
case x11Detected:
return
}
if detectX11FromSockets() {
case x11Ambiguous:
// /proc listed X servers and none of them could be shown to be the one
// on the console. Falling through to the socket scan would only make
// the same guess with less information, so stop here: no capture at all
// beats capturing a session that may not be the caller's.
return
case x11NotFound:
}
detectX11FromSockets()
}
// x11Detection is the outcome of one display-detection pass. Ambiguous is
// deliberately distinct from NotFound: it means candidates existed and were
// refused, which must not be retried by a less discriminating fallback.
type x11Detection int
const (
x11NotFound x11Detection = iota
x11Detected
x11Ambiguous
)
// xorgCandidate is one X server found in /proc, with the pieces that decide
// whether it is the one a remote user should be attached to.
type xorgCandidate struct {
@@ -94,10 +111,10 @@ type xorgCandidate struct {
// delivering the remote user's input to it. /sys/class/tty/tty0/active names the
// session actually on the console and X records its own VT in argv, so the two
// can be matched.
func detectX11FromProc() bool {
func detectX11FromProc() x11Detection {
entries, err := os.ReadDir("/proc")
if err != nil {
return false
return x11NotFound
}
var candidates []xorgCandidate
@@ -117,40 +134,69 @@ func detectX11FromProc() bool {
candidates = append(candidates, xorgCandidate{display: display, auth: auth, vt: parseXorgVT(args)})
}
best, ok := pickXorgCandidate(candidates, activeVT())
if !ok {
return false
best, outcome := pickXorgCandidate(candidates, activeVT())
if outcome != x11Detected {
return outcome
}
setDisplayEnv(best.display, best.auth)
return true
return x11Detected
}
// pickXorgCandidate chooses which X server to attach to: the one on the active
// VT when that is known, and otherwise the lowest display number, so the choice
// is at least stable across runs instead of following readdir order.
func pickXorgCandidate(candidates []xorgCandidate, activeVT int) (xorgCandidate, bool) {
// pickXorgCandidate chooses which X server to attach to.
//
// The one on the active VT wins outright. Failing that the choice has to be
// unambiguous, because guessing wrong means handing a remote user another local
// user's screen and keyboard: a single candidate is taken, and anything else is
// refused. The lowest display number only breaks ties among servers that all
// record no VT, where there is no active-session signal to go on at all.
func pickXorgCandidate(candidates []xorgCandidate, activeVT int) (xorgCandidate, x11Detection) {
if len(candidates) == 0 {
return xorgCandidate{}, false
return xorgCandidate{}, x11NotFound
}
if activeVT > 0 {
for _, c := range candidates {
if c.vt == activeVT {
return c, true
return c, x11Detected
}
}
}
if len(candidates) == 1 {
return candidates[0], x11Detected
}
best := candidates[0]
for _, c := range candidates[1:] {
// Several servers and no way to tell which is on the console. One that
// records a VT is on some seat other than the active one, so it is a
// session this connection has no business showing.
var vtless []xorgCandidate
for _, c := range candidates {
if c.vt < 0 {
vtless = append(vtless, c)
}
}
if len(vtless) == 0 {
log.Warnf("found %d X servers, all on virtual terminals and none of them the active one (%s); not attaching to any",
len(candidates), describeActiveVT(activeVT))
return xorgCandidate{}, x11Ambiguous
}
best := vtless[0]
for _, c := range vtless[1:] {
if displayNumber(c.display) < displayNumber(best.display) {
best = c
}
}
if len(candidates) > 1 {
log.Warnf("found %d X servers and none on the active VT (%d); attaching to DISPLAY=%s",
len(candidates), activeVT, best.display)
log.Warnf("found %d X servers and none on the active VT (%s); attaching to the lowest VT-less display %s",
len(candidates), describeActiveVT(activeVT), best.display)
return best, x11Detected
}
// describeActiveVT renders the active VT for a log line, keeping "we could not
// read it" distinct from a VT number that simply matched nothing.
func describeActiveVT(vt int) string {
if vt <= 0 {
return "unknown"
}
return best, true
return "tty" + strconv.Itoa(vt)
}
// activeVT reports the virtual terminal currently on the console, or -1 when
+58 -30
View File
@@ -11,25 +11,26 @@ import (
// TestPickXorgCandidate covers which X server a service-mode capturer attaches
// to when several are running. Picking the wrong one shows a remote user
// another local user's screen, so the active VT wins whenever it is known.
// another local user's screen and delivers their input to it, so the active VT
// wins whenever it is known and an unresolvable choice is refused outright.
func TestPickXorgCandidate(t *testing.T) {
tests := []struct {
name string
candidates []xorgCandidate
activeVT int
want string
wantOK bool
name string
candidates []xorgCandidate
activeVT int
want string
wantOutcome x11Detection
}{
{
name: "no X server found",
wantOK: false,
name: "no X server found",
wantOutcome: x11NotFound,
},
{
name: "single server is used whatever its VT",
candidates: []xorgCandidate{{display: ":3", vt: 9}},
activeVT: 2,
want: ":3",
wantOK: true,
name: "single server is used whatever its VT",
candidates: []xorgCandidate{{display: ":3", vt: 9}},
activeVT: 2,
want: ":3",
wantOutcome: x11Detected,
},
{
name: "active VT wins over the lower display number",
@@ -37,19 +38,37 @@ func TestPickXorgCandidate(t *testing.T) {
{display: ":0", vt: 2},
{display: ":1", vt: 7},
},
activeVT: 7,
want: ":1",
wantOK: true,
activeVT: 7,
want: ":1",
wantOutcome: x11Detected,
},
{
name: "unknown active VT falls back to the lowest display",
name: "several servers on other VTs are refused, not guessed at",
candidates: []xorgCandidate{
{display: ":1", vt: 7},
{display: ":0", vt: 2},
{display: ":1", vt: 3},
},
activeVT: -1,
want: ":0",
wantOK: true,
activeVT: 7,
wantOutcome: x11Ambiguous,
},
{
name: "unknown active VT does not license a guess between seats",
candidates: []xorgCandidate{
{display: ":0", vt: 2},
{display: ":1", vt: 3},
},
activeVT: -1,
wantOutcome: x11Ambiguous,
},
{
name: "a VT-less server is preferred over one on an inactive VT",
candidates: []xorgCandidate{
{display: ":0", vt: 2},
{display: ":99", vt: -1},
},
activeVT: 7,
want: ":99",
wantOutcome: x11Detected,
},
{
name: "display numbers compare numerically, not lexically",
@@ -57,9 +76,9 @@ func TestPickXorgCandidate(t *testing.T) {
{display: ":10", vt: -1},
{display: ":2", vt: -1},
},
activeVT: -1,
want: ":2",
wantOK: true,
activeVT: -1,
want: ":2",
wantOutcome: x11Detected,
},
{
name: "an unparseable display is only picked when it is alone",
@@ -67,17 +86,18 @@ func TestPickXorgCandidate(t *testing.T) {
{display: ":bogus", vt: -1},
{display: ":4", vt: -1},
},
activeVT: -1,
want: ":4",
wantOK: true,
activeVT: -1,
want: ":4",
wantOutcome: x11Detected,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, ok := pickXorgCandidate(tc.candidates, tc.activeVT)
require.Equal(t, tc.wantOK, ok)
if !tc.wantOK {
got, outcome := pickXorgCandidate(tc.candidates, tc.activeVT)
require.Equal(t, tc.wantOutcome, outcome)
if tc.wantOutcome != x11Detected {
assert.Empty(t, got.display, "a refused choice must not leak a display")
return
}
assert.Equal(t, tc.want, got.display)
@@ -92,3 +112,11 @@ func TestParseXorgVT(t *testing.T) {
assert.Equal(t, -1, parseXorgVT([]string{"/usr/bin/Xvfb", ":99", "-screen", "0", "1920x1080x24"}))
assert.Equal(t, -1, parseXorgVT([]string{"/usr/lib/Xorg", ":0", "vtconsole"}))
}
// TestDescribeActiveVT keeps "we could not read the active VT" legible in the
// log line, rather than printing a -1 that reads like a real terminal.
func TestDescribeActiveVT(t *testing.T) {
assert.Equal(t, "unknown", describeActiveVT(-1))
assert.Equal(t, "unknown", describeActiveVT(0))
assert.Equal(t, "tty7", describeActiveVT(7))
}
+24 -14
View File
@@ -30,13 +30,17 @@ func (s *session) pendingCursorRect(pf clientPixelFormat) []byte {
// cannot produce one, or the sprite failed to encode. Say which, once per
// session, so the next report of a missing cursor names its own cause.
if !supported || failed || composite {
s.logCursorSkip("no cursor rect: client_requested=%v source_failed=%v compositing=%v",
// The three flags form the key so a client that negotiates the encoding
// mid-session, or a source that starts failing, still gets a line: eight
// combinations at most.
s.logCursorSkip(fmt.Sprintf("state:%t%t%t", supported, failed, composite),
"no cursor rect: client_requested=%v source_failed=%v compositing=%v",
supported, failed, composite)
return nil
}
src, ok := s.capturer.(cursorSource)
if !ok {
s.logCursorSkip("no cursor rect: capturer %T reports no cursor source", s.capturer)
s.logCursorSkip("no-source", "no cursor rect: capturer %T reports no cursor source", s.capturer)
return nil
}
img, hotX, hotY, serial, err := src.Cursor()
@@ -48,7 +52,7 @@ func (s *session) pendingCursorRect(pf clientPixelFormat) []byte {
return nil
}
if img == nil {
s.logCursorSkip("no cursor rect: capturer returned no sprite")
s.logCursorSkip("no-sprite", "no cursor rect: capturer returned no sprite")
return nil
}
if serial == lastSerial {
@@ -57,7 +61,10 @@ func (s *session) pendingCursorRect(pf clientPixelFormat) []byte {
buf := encodeCursorPseudoRect(img, hotX, hotY, pf)
if buf == nil {
b := img.Bounds()
s.logCursorSkip("no cursor rect: sprite %dx%d stride=%d pix=%d could not be encoded",
// Fixed key, varying detail: a source returning junk dimensions must not
// be able to mint a new throttle entry per frame.
s.logCursorSkip("encode-failed",
"no cursor rect: sprite %dx%d stride=%d pix=%d could not be encoded",
b.Dx(), b.Dy(), img.Stride, len(img.Pix))
return nil
}
@@ -83,25 +90,28 @@ func (s *session) pendingCursorRect(pf clientPixelFormat) []byte {
}
// logCursorSkip reports why this update carries no cursor rect, once per
// distinct line. pendingCursorRect runs per framebuffer update, so an
// unthrottled log would flood; throttling once per session instead would let
// whichever reason came first hide every later one, and the reasons do change
// as the client negotiates encodings and the cursor source starts failing.
func (s *session) logCursorSkip(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
// reason. pendingCursorRect runs per framebuffer update, so an unthrottled log
// would flood; throttling once per session instead would let whichever reason
// came first hide every later one, and the reasons do change as the client
// negotiates encodings and the cursor source starts failing.
//
// reason keys the throttle and must come from a fixed set, never from the
// message: a cursor source handing back varying junk dimensions would otherwise
// mint a new key per frame, growing the map for the life of the session and
// logging every one of them.
func (s *session) logCursorSkip(reason, format string, args ...any) {
s.cursorSkipMu.Lock()
if s.cursorSkipSeen == nil {
s.cursorSkipSeen = make(map[string]struct{})
}
_, seen := s.cursorSkipSeen[msg]
s.cursorSkipSeen[msg] = struct{}{}
_, seen := s.cursorSkipSeen[reason]
s.cursorSkipSeen[reason] = struct{}{}
s.cursorSkipMu.Unlock()
if seen {
return
}
s.log.Debug(msg)
s.log.Debugf(format, args...)
}
// maxCursorDim caps the cursor sprite size we'll encode. Real platform