From 7e7e056f3a7f4de3e8ade1f41e5b0262545b26ea Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Mon, 18 May 2026 07:54:21 +0200 Subject: [PATCH] Reset Tight zlib stream when deflater is recreated mid-session Also scrub brand-name references from comments. --- client/vnc/server/extclipboard.go | 10 +++++----- client/vnc/server/input_darwin.go | 6 +++--- client/vnc/server/input_windows.go | 2 +- client/vnc/server/rfb.go | 23 +++++++++++++++++------ client/vnc/server/server.go | 2 +- client/vnc/server/session.go | 10 +++++++++- 6 files changed, 36 insertions(+), 17 deletions(-) diff --git a/client/vnc/server/extclipboard.go b/client/vnc/server/extclipboard.go index 86ab6d554..38234b007 100644 --- a/client/vnc/server/extclipboard.go +++ b/client/vnc/server/extclipboard.go @@ -54,11 +54,11 @@ const ( // buildExtClipCaps emits the Caps payload. The flags word advertises every // action we support in the high byte (Caps + Request + Peek + Notify + -// Provide) and every format we accept in the low 16 bits. noVNC uses these -// action bits to decide whether to auto-Request on Notify; without -// Request in our Caps it silently drops our Notify messages. After the -// flags word we emit one uint32 max size per format bit set, in ascending -// bit order. +// Provide) and every format we accept in the low 16 bits. Clients use +// these action bits to decide whether to auto-Request on Notify; without +// Request in our Caps a conforming client silently drops our Notify +// messages. After the flags word we emit one uint32 max size per format +// bit set, in ascending bit order. func buildExtClipCaps() []byte { flags := extClipActionCaps | extClipActionRequest | extClipActionPeek | extClipActionNotify | extClipActionProvide | extClipFormatText diff --git a/client/vnc/server/input_darwin.go b/client/vnc/server/input_darwin.go index 595219745..1bda2285c 100644 --- a/client/vnc/server/input_darwin.go +++ b/client/vnc/server/input_darwin.go @@ -632,10 +632,10 @@ var specialKeyMap = map[uint32]uint16{ 0xffea: 0x3D, // Alt_R (Option) 0xffe7: 0x37, // Meta_L (Command) 0xffe8: 0x36, // Meta_R (Command) - 0xffeb: 0x37, // Super_L (Command) - noVNC sends this + 0xffeb: 0x37, // Super_L (Command) 0xffec: 0x36, // Super_R (Command) - // Mode_switch / ISO_Level3_Shift (sent by noVNC for macOS Option remap) + // Mode_switch / ISO_Level3_Shift (for macOS Option remap on layouts) 0xff7e: 0x3A, // Mode_switch -> Option 0xfe03: 0x3D, // ISO_Level3_Shift -> Right Option @@ -674,7 +674,7 @@ var specialKeyMap = map[uint32]uint16{ 0x002e: 0x2F, // period . 0x002f: 0x2C, // slash / - // Shifted punctuation (noVNC sends these as separate keysyms) + // Shifted punctuation (clients sometimes send these as separate keysyms) 0x005f: 0x1B, // underscore _ (shift+minus) 0x002b: 0x18, // plus + (shift+equal) 0x007b: 0x21, // braceleft { (shift+[) diff --git a/client/vnc/server/input_windows.go b/client/vnc/server/input_windows.go index 9f6af4089..aeeb35be6 100644 --- a/client/vnc/server/input_windows.go +++ b/client/vnc/server/input_windows.go @@ -206,7 +206,7 @@ func (w *WindowsInputInjector) InjectKey(keysym uint32, down bool) { // InjectKeyScancode queues a raw-scancode key event. PC AT Set 1 maps // directly onto what SendInput's KEYEVENTF_SCANCODE flag wants, so the // only translation is splitting the optional 0xE0 prefix off into the -// KEYEVENTF_EXTENDEDKEY flag. keysym is the noVNC-provided fallback we +// KEYEVENTF_EXTENDEDKEY flag. keysym is the client-provided fallback we // reach for if the scancode is zero. func (w *WindowsInputInjector) InjectKeyScancode(scancode uint32, keysym uint32, down bool) { if scancode == 0 { diff --git a/client/vnc/server/rfb.go b/client/vnc/server/rfb.go index 6c8b200be..93e71d84f 100644 --- a/client/vnc/server/rfb.go +++ b/client/vnc/server/rfb.go @@ -69,10 +69,9 @@ const ( pseudoEncDesktopName = -307 pseudoEncExtendedDesktopSize = -308 - // Quality/Compression level pseudo-encodings (TightVNC extension). The - // client picks one value from each range to tune JPEG quality and zlib - // effort. 0 is lowest quality / fastest, 9 is highest quality / best - // compression. + // Quality/Compression level pseudo-encodings. The client picks one + // value from each range to tune JPEG quality and zlib effort. 0 is + // lowest quality / fastest, 9 is highest quality / best compression. pseudoEncQualityLevelMin = -32 pseudoEncQualityLevelMax = -23 pseudoEncCompressLevelMin = -256 @@ -445,6 +444,12 @@ type tightState struct { // whether a SetEncodings refresh needs to recreate the tight state. qualityLevel int compressLevel int + // pendingZlibReset becomes true when this tightState replaces an + // in-use one (e.g. CompressLevel change mid-session). The next Basic + // rect we emit ORs the stream-0 reset bit into its sub-encoding byte + // so the client's inflater drops its now-stale dictionary; cleared + // after one emission. + pendingZlibReset bool } func newTightState() *tightState { @@ -618,9 +623,15 @@ func encodeTightBasic(img *image.RGBA, x, y, w, h int, t *tightState) ([]byte, b } } - // Sub-encoding byte: stream 0, no resets, basic encoding (top nibble - // = 0x40 = explicit filter follows). + // Sub-encoding byte: stream 0, basic encoding (top nibble = 0x40 = + // explicit filter follows). The low nibble carries per-stream reset + // flags; bit 0 here tells the client to reset its stream-0 inflater + // when our deflater was just recreated. subenc := byte(tightBasicFilter) + if t.pendingZlibReset { + subenc |= 0x01 + t.pendingZlibReset = false + } filter := byte(tightFilterCopy) if pixelStream < 12 { diff --git a/client/vnc/server/server.go b/client/vnc/server/server.go index 1bda413e8..cdcea9570 100644 --- a/client/vnc/server/server.go +++ b/client/vnc/server/server.go @@ -32,7 +32,7 @@ const ( ) // RFB security-failure reason codes sent to the client. These prefixes are -// stable so dashboard/noVNC integrations can branch on them without parsing +// stable so dashboard integrations can branch on them without parsing // free text. Format: "CODE: human message". const ( RejectCodeJWTMissing = "AUTH_JWT_MISSING" diff --git a/client/vnc/server/session.go b/client/vnc/server/session.go index 697fabe76..6abdef294 100644 --- a/client/vnc/server/session.go +++ b/client/vnc/server/session.go @@ -77,7 +77,7 @@ type session struct { // captureErrLast throttles "capture (transient)" logs while the // capturer is in a sustained failure state (e.g. X server died but a - // noVNC tab is still open). Owned by the encoder goroutine. + // client is still connected). Owned by the encoder goroutine. captureErrLast time.Time captureErrSeen bool @@ -290,7 +290,15 @@ func (s *session) handleSetEncodings() error { if s.useTight && (s.tight == nil || s.tight.qualityLevel != s.clientJPEGQuality || s.tight.compressLevel != s.clientZlibLevel) { + // When we replace an in-use tightState the client's stream-0 + // inflater carries dictionary state from the old deflater. Carry + // the pending-reset flag so the next Basic rect tells the client + // to reset its inflater before decoding. + replacing := s.tight != nil s.tight = newTightStateWithLevels(s.clientJPEGQuality, s.clientZlibLevel) + if replacing { + s.tight.pendingZlibReset = true + } } sendExtClipCaps := s.clientSupportsExtClipboard && !s.extClipCapsSent if sendExtClipCaps {