fix(nestsclient): keep stream priority pack non-negative — Lite-03 priority bit-layout

Pre-fix, the M1 priority pack `((trackPriority and 0xFF) shl 24)`
would set bit 31 whenever `trackPriority >= 0x80`, producing a
negative `Int`. `QuicConnectionWriter.sortedByDescending { it.priority }`
sorts via signed `Int.compareTo`, so negative-signed values land
BELOW every positive priority — exactly inverting the intended
"higher trackPriority drains first" ordering.

The default `DEFAULT_TRACK_PRIORITY = 0x80` sits exactly on this
boundary, so this would have hit production the moment a hand-
tuned `trackPriority=0xFF` (highest intended) appeared in any
multi-track scenario.

Reshape the layout to keep bit 31 clear:

  bit 31      : 0 (reserved as the sign bit)
  bits 30..23 : trackPriority u8 (0..255)
  bits 22..0  : sequence low 23 bits  (~97 days at 1 grp/sec)

The trackPriority byte still occupies an 8-bit slot, just shifted
one bit lower. Sequence saturation moves from 24 bits (≈ 6 days)
to 23 bits (≈ 97 days) — still ample for the 9-minute JWT refresh
cycle.

Two regression tests:

  - The existing `publisher_packs_trackPriority_and_sequence_into_setPriority_value`
    is updated to assert the new bit layout AND that the encoded
    value is non-negative.
  - New `publisher_priority_pack_keeps_top_trackPriority_above_lower_trackPriority`
    is the direct guard against the bug: `trackPriority=0xFF`
    must encode HIGHER than `trackPriority=0x7F`. Pre-fix this
    test fails with `0xFF` encoding to a negative Int below the
    positive `0x7F` encoding; post-fix both are positive and
    correctly ordered.

Surfaced by the merge-prep security review of the moq-lite
Lite-03/04 audit branch — agent flagged it as "QoS/correctness
not security, excluded by DoS rule" but it's a real bug worth
fixing before merge.

Audit doc updated: bit layout in `nestsClient/plans/2026-05-09-moq-lite-rfc-compliance.md`
(M1 + L4 entries).

https://claude.ai/code/session_012TGfo99Ugz7fcCPv85a8Tq
This commit is contained in:
Claude
2026-05-09 15:21:28 +00:00
parent dfdf07269d
commit 064654512d
3 changed files with 131 additions and 32 deletions
@@ -1264,18 +1264,30 @@ class MoqLiteSession internal constructor(
// queue gets congested.
//
// Wire layout into `Int`:
// bits 31..24 trackPriority u8 (0..255) — top byte
// bits 23..0 sequence low 24 bits — wraps every
// ~16M groups within a single track
// bit 31 0 (always — keeps the value non-negative)
// bits 30..23 trackPriority u8 (0..255)
// bits 22..0 sequence low 23 bits (~8.4M groups)
//
// Bit 31 is reserved as the sign bit because
// [com.vitorpamplona.quic.connection.QuicConnectionWriter] sorts
// streams via signed `sortedByDescending { it.priority }`.
// Setting bit 31 (which would happen for `trackPriority >= 0x80`
// shifted by 24) produces a negative `Int` that sorts BELOW
// positive priorities, inverting the intent. The audit's
// priority-byte default `DEFAULT_TRACK_PRIORITY = 0x80` sits
// exactly on this boundary — without the 23-bit shift the
// default-vs-default case would be fine but a hand-tuned 0xFF
// (highest intended) would sort behind 0x7F (lower intended).
//
// Saturating cast on `sequence` guards a theoretical broadcast
// long enough to outgrow 24 bits (≈ 6 days at the production
// 1-group/sec cadence; the priority degrades to "all newer
// groups tie" past that, but they still beat older groups of
// any LOWER-priority track via the top byte). Defensive only;
// production sessions cycle on JWT refresh every 9 min.
val seq24 = sequence.coerceAtMost(0xFF_FFFFL).toInt() and 0x00FF_FFFF
val packedPriority = ((trackPriority and 0xFF) shl 24) or seq24
// long enough to outgrow 23 bits (≈ 97 days at the production
// 1-group/sec cadence). Past that the priority degrades to "all
// newer groups within a track tie", but they still beat older
// groups of any LOWER-priority track via the high byte.
// Production sessions cycle on JWT refresh every 9 min, so the
// wrap is defensive only.
val seq23 = sequence.coerceAtMost(0x7F_FFFFL).toInt() and 0x007F_FFFF
val packedPriority = ((trackPriority and 0xFF) shl 23) or seq23
uni.setPriority(packedPriority)
uni.write(Varint.encode(MoqLiteDataType.Group.code))
uni.write(MoqLiteCodec.encodeGroupHeader(MoqLiteGroupHeader(subscribeId, sequence)))