Reset Tight zlib stream when deflater is recreated mid-session

Also scrub brand-name references from comments.
This commit is contained in:
Viktor Liu
2026-05-18 07:54:21 +02:00
parent 785f94d13f
commit 7e7e056f3a
6 changed files with 36 additions and 17 deletions

View File

@@ -54,11 +54,11 @@ const (
// buildExtClipCaps emits the Caps payload. The flags word advertises every // buildExtClipCaps emits the Caps payload. The flags word advertises every
// action we support in the high byte (Caps + Request + Peek + Notify + // 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 // Provide) and every format we accept in the low 16 bits. Clients use
// action bits to decide whether to auto-Request on Notify; without // these action bits to decide whether to auto-Request on Notify; without
// Request in our Caps it silently drops our Notify messages. After the // Request in our Caps a conforming client silently drops our Notify
// flags word we emit one uint32 max size per format bit set, in ascending // messages. After the flags word we emit one uint32 max size per format
// bit order. // bit set, in ascending bit order.
func buildExtClipCaps() []byte { func buildExtClipCaps() []byte {
flags := extClipActionCaps | extClipActionRequest | extClipActionPeek | flags := extClipActionCaps | extClipActionRequest | extClipActionPeek |
extClipActionNotify | extClipActionProvide | extClipFormatText extClipActionNotify | extClipActionProvide | extClipFormatText

View File

@@ -632,10 +632,10 @@ var specialKeyMap = map[uint32]uint16{
0xffea: 0x3D, // Alt_R (Option) 0xffea: 0x3D, // Alt_R (Option)
0xffe7: 0x37, // Meta_L (Command) 0xffe7: 0x37, // Meta_L (Command)
0xffe8: 0x36, // Meta_R (Command) 0xffe8: 0x36, // Meta_R (Command)
0xffeb: 0x37, // Super_L (Command) - noVNC sends this 0xffeb: 0x37, // Super_L (Command)
0xffec: 0x36, // Super_R (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 0xff7e: 0x3A, // Mode_switch -> Option
0xfe03: 0x3D, // ISO_Level3_Shift -> Right Option 0xfe03: 0x3D, // ISO_Level3_Shift -> Right Option
@@ -674,7 +674,7 @@ var specialKeyMap = map[uint32]uint16{
0x002e: 0x2F, // period . 0x002e: 0x2F, // period .
0x002f: 0x2C, // slash / 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) 0x005f: 0x1B, // underscore _ (shift+minus)
0x002b: 0x18, // plus + (shift+equal) 0x002b: 0x18, // plus + (shift+equal)
0x007b: 0x21, // braceleft { (shift+[) 0x007b: 0x21, // braceleft { (shift+[)

View File

@@ -206,7 +206,7 @@ func (w *WindowsInputInjector) InjectKey(keysym uint32, down bool) {
// InjectKeyScancode queues a raw-scancode key event. PC AT Set 1 maps // InjectKeyScancode queues a raw-scancode key event. PC AT Set 1 maps
// directly onto what SendInput's KEYEVENTF_SCANCODE flag wants, so the // directly onto what SendInput's KEYEVENTF_SCANCODE flag wants, so the
// only translation is splitting the optional 0xE0 prefix off into 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. // reach for if the scancode is zero.
func (w *WindowsInputInjector) InjectKeyScancode(scancode uint32, keysym uint32, down bool) { func (w *WindowsInputInjector) InjectKeyScancode(scancode uint32, keysym uint32, down bool) {
if scancode == 0 { if scancode == 0 {

View File

@@ -69,10 +69,9 @@ const (
pseudoEncDesktopName = -307 pseudoEncDesktopName = -307
pseudoEncExtendedDesktopSize = -308 pseudoEncExtendedDesktopSize = -308
// Quality/Compression level pseudo-encodings (TightVNC extension). The // Quality/Compression level pseudo-encodings. The client picks one
// client picks one value from each range to tune JPEG quality and zlib // value from each range to tune JPEG quality and zlib effort. 0 is
// effort. 0 is lowest quality / fastest, 9 is highest quality / best // lowest quality / fastest, 9 is highest quality / best compression.
// compression.
pseudoEncQualityLevelMin = -32 pseudoEncQualityLevelMin = -32
pseudoEncQualityLevelMax = -23 pseudoEncQualityLevelMax = -23
pseudoEncCompressLevelMin = -256 pseudoEncCompressLevelMin = -256
@@ -445,6 +444,12 @@ type tightState struct {
// whether a SetEncodings refresh needs to recreate the tight state. // whether a SetEncodings refresh needs to recreate the tight state.
qualityLevel int qualityLevel int
compressLevel 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 { 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 // Sub-encoding byte: stream 0, basic encoding (top nibble = 0x40 =
// = 0x40 = explicit filter follows). // 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) subenc := byte(tightBasicFilter)
if t.pendingZlibReset {
subenc |= 0x01
t.pendingZlibReset = false
}
filter := byte(tightFilterCopy) filter := byte(tightFilterCopy)
if pixelStream < 12 { if pixelStream < 12 {

View File

@@ -32,7 +32,7 @@ const (
) )
// RFB security-failure reason codes sent to the client. These prefixes are // 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". // free text. Format: "CODE: human message".
const ( const (
RejectCodeJWTMissing = "AUTH_JWT_MISSING" RejectCodeJWTMissing = "AUTH_JWT_MISSING"

View File

@@ -77,7 +77,7 @@ type session struct {
// captureErrLast throttles "capture (transient)" logs while the // captureErrLast throttles "capture (transient)" logs while the
// capturer is in a sustained failure state (e.g. X server died but a // 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 captureErrLast time.Time
captureErrSeen bool captureErrSeen bool
@@ -290,7 +290,15 @@ func (s *session) handleSetEncodings() error {
if s.useTight && (s.tight == nil || if s.useTight && (s.tight == nil ||
s.tight.qualityLevel != s.clientJPEGQuality || s.tight.qualityLevel != s.clientJPEGQuality ||
s.tight.compressLevel != s.clientZlibLevel) { 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) s.tight = newTightStateWithLevels(s.clientJPEGQuality, s.clientZlibLevel)
if replacing {
s.tight.pendingZlibReset = true
}
} }
sendExtClipCaps := s.clientSupportsExtClipboard && !s.extClipCapsSent sendExtClipCaps := s.clientSupportsExtClipboard && !s.extClipCapsSent
if sendExtClipCaps { if sendExtClipCaps {