fix(core): five sweep lows — seal-lane fallback, flush partial, codec echo, idle clamp, pair-name UTF-8

From the 2026-07-20 punktfunk-core quality sweep's adjudicated lows
(~/punktfunk-sweeps/punktfunk-core-2026-07-20.json), each with a
regression test:

- session: the two-lane seal's dead-worker fallback dropped the frame's
  back half and returned Ok with half an access unit; the corpse lane
  was also never respawned. The send-failure arm now reclaims the tail
  (single-lane seals the WHOLE frame) and both failure arms drop the
  lane so the next large frame respawns it. A recv-side death now
  surfaces as an error instead of silent truncation.
- reassemble: Reassembler::reset() left pending_partial parked, so a
  pre-flush stale partial survived Session::flush_backlog and was
  delivered as the first "frame" after a jump-to-live.
- caps: resolve_codec echoed a non-conformant multi-bit preferred byte
  verbatim (downstream from_wire folds it to HEVC — possibly outside
  the shared set). It now isolates one bit of the intersection.
- endpoint: stream_transport_idle only floored the value; an absurd
  operator-supplied idle timeout blew past QUIC's VarInt ms ceiling and
  panicked host startup through the expect. Clamped to 1s..1h.
- pairing: PairRequest::encode cut the device name at a raw byte-64
  boundary, splitting multi-byte UTF-8 (host showed U+FFFD forever);
  it now shares Hello's char-boundary truncate_to.

The frozen-FEC-ceiling finding (reassemble.rs:109) was already fixed on
main by the sweep's high-severity commit — skipped as stale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 18:16:02 +02:00
parent f4ec18d528
commit 76ac3cf867
6 changed files with 174 additions and 27 deletions
+26 -1
View File
@@ -91,8 +91,13 @@ pub fn resolve_codec(client_codecs: u8, host_capable: u8, preferred: u8) -> Opti
return None;
}
// Honor the client's preference when the host can also emit it; else fall back to precedence.
// `preferred` is a single-bit field by contract but arrives as a raw wire byte — isolate ONE
// bit of the intersection instead of echoing the request, so a non-conformant multi-bit
// value can never escape as a codec id (downstream `from_wire` folds unknown values to HEVC,
// which may not even be in the shared set).
if preferred != 0 && shared & preferred != 0 {
return Some(preferred);
let want = shared & preferred;
return Some(want & want.wrapping_neg());
}
// Precedence: HEVC > AV1 > H.264.
[CODEC_HEVC, CODEC_AV1, CODEC_H264]
@@ -222,4 +227,24 @@ mod tests {
0
);
}
#[test]
fn resolve_codec_canonicalizes_a_multi_bit_preference() {
// A non-conformant peer may stuff its capability MASK into `preferred` — the result
// must still be a single bit of the shared set, never the raw multi-bit echo (which
// folds to HEVC downstream and can select a codec the client can't decode).
assert_eq!(
resolve_codec(CODEC_H264, CODEC_H264 | CODEC_AV1, CODEC_H264 | CODEC_AV1),
Some(CODEC_H264)
);
// Several shared preferred bits: still exactly one bit, and one of the preferred ones.
let got = resolve_codec(
CODEC_H264 | CODEC_HEVC | CODEC_AV1,
CODEC_H264 | CODEC_HEVC | CODEC_AV1,
CODEC_AV1 | CODEC_HEVC,
)
.unwrap();
assert_eq!(got.count_ones(), 1);
assert_ne!(got & (CODEC_AV1 | CODEC_HEVC), 0);
}
}
+13 -2
View File
@@ -26,10 +26,12 @@ fn stream_transport() -> Arc<quinn::TransportConfig> {
/// path is PINGed at least twice per window and a single lost PING (wifi roam / brief blip) won't
/// false-close. `idle` is clamped to a ≥1s floor so a misconfigured tiny value can't tear live
/// sessions down. Active sessions are unaffected either way: video keeps the connection live and
/// the keep-alive holds it open through quiet control periods.
/// the keep-alive holds it open through quiet control periods. Clamped to a 1 s..1 h window:
/// the ceiling keeps an absurd operator-supplied value inside QUIC's VarInt millisecond range,
/// so the conversion below genuinely cannot fail (it used to panic host startup instead).
fn stream_transport_idle(idle: std::time::Duration) -> Arc<quinn::TransportConfig> {
use std::time::Duration;
let idle = idle.max(Duration::from_secs(1));
let idle = idle.clamp(Duration::from_secs(1), Duration::from_secs(3600));
let keep_alive = (idle / 2).min(Duration::from_secs(4));
let mut t = quinn::TransportConfig::default();
t.max_idle_timeout(Some(
@@ -305,4 +307,13 @@ mod tests {
assert_eq!(a, endpoint::cert_fingerprint(b"cert-a"));
assert_ne!(a, endpoint::cert_fingerprint(b"cert-b"));
}
#[test]
fn absurd_idle_timeout_is_clamped_not_a_panic() {
// The conversion to quinn's IdleTimeout fails past the QUIC VarInt millisecond
// ceiling — an operator-supplied huge PUNKTFUNK_IDLE_TIMEOUT_MS used to panic host
// startup through the `expect`. Both extremes must construct.
let _ = super::stream_transport_idle(std::time::Duration::MAX);
let _ = super::stream_transport_idle(std::time::Duration::ZERO);
}
}
+3 -2
View File
@@ -182,8 +182,9 @@ pub struct Start {
}
/// Truncate `s` to at most `max` bytes on a UTF-8 char boundary (so a multi-byte char straddling
/// the cap is dropped whole, never split). Shared by Hello's length-prefixed name/launch fields.
fn truncate_to(s: &str, max: usize) -> &str {
/// the cap is dropped whole, never split). Shared by Hello's length-prefixed name/launch fields
/// and [`PairRequest`](super::PairRequest)'s copy of the same name cap.
pub(super) fn truncate_to(s: &str, max: usize) -> &str {
if s.len() <= max {
return s;
}
+22 -3
View File
@@ -78,13 +78,16 @@ fn get_bytes(b: &[u8], off: usize) -> Result<(&[u8], usize)> {
impl PairRequest {
pub fn encode(&self) -> Vec<u8> {
let name = self.name.as_bytes();
let n = name.len().min(64);
// Same cap, same rule as Hello's copy of this field: truncate on a char boundary —
// a raw byte cut mid-sequence put invalid UTF-8 on the wire, and the host showed the
// name with a permanent replacement char in its paired-clients list.
let name = super::handshake::truncate_to(&self.name, HELLO_NAME_MAX).as_bytes();
let n = name.len();
let mut b = Vec::with_capacity(8 + n + self.spake_a.len());
b.extend_from_slice(CTL_MAGIC);
b.push(MSG_PAIR_REQUEST);
b.push(n as u8);
b.extend_from_slice(&name[..n]);
b.extend_from_slice(name);
put_bytes(&mut b, &self.spake_a);
b
}
@@ -201,4 +204,20 @@ mod tests {
bad.push(0);
assert!(PairProof::decode(&bad).is_err());
}
#[test]
fn pair_request_name_cap_respects_char_boundaries() {
// A multi-byte char straddling the 64-byte cap must be dropped whole (Hello's rule),
// not split mid-sequence into invalid UTF-8 the host then renders as U+FFFD forever.
let pr = PairRequest {
name: format!("{}\u{00fc}", "x".repeat(HELLO_NAME_MAX - 1)),
spake_a: vec![1, 2, 3],
};
let dec = PairRequest::decode(&pr.encode()).unwrap();
assert!(dec.name.len() <= HELLO_NAME_MAX && dec.name.starts_with('x'));
assert!(
!dec.name.contains('\u{FFFD}'),
"name must never be split mid-char on the wire"
);
}
}