From 94fdfd82a428808bcfc877ad5bbb8efb8c218a43 Mon Sep 17 00:00:00 2001 From: Maxim Raznatovski Date: Tue, 20 Oct 2026 17:12:55 +0200 Subject: [PATCH 1/3] fix: close the connection on an empty binary message --- README.md | 2 +- messaging.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ffeac9a..da3919a 100644 --- a/README.md +++ b/README.md @@ -386,7 +386,7 @@ Receivers that discard a binary role payload MUST still process Noise messages, ### Binary Message ID Structure -The first byte of every decrypted binary message is its message ID. IDs are assigned from the table below; each role's binary message definitions name the exact IDs it uses. +The first byte of every decrypted binary message is its message ID. A binary message whose AEAD plaintext is empty is a protocol error: the receiver MUST close the connection. IDs are assigned from the table below; each role's binary message definitions name the exact IDs it uses. | IDs | Assignment | |---|---| diff --git a/messaging.md b/messaging.md index a864cea..d919224 100644 --- a/messaging.md +++ b/messaging.md @@ -68,7 +68,7 @@ Receivers that discard a binary role payload MUST still process Noise messages, ### Binary Message ID Structure -The first byte of every decrypted binary message is its message ID. IDs are assigned from the table below; each role's binary message definitions name the exact IDs it uses. +The first byte of every decrypted binary message is its message ID. A binary message whose AEAD plaintext is empty is a protocol error: the receiver MUST close the connection. IDs are assigned from the table below; each role's binary message definitions name the exact IDs it uses. | IDs | Assignment | |---|---| From 23defae46997fe6ce211e3a1b80f20cf7e4db0fb Mon Sep 17 00:00:00 2001 From: Maxim Raznatovski Date: Tue, 20 Oct 2026 17:13:05 +0200 Subject: [PATCH 2/3] fix: close the connection on binary messages shorter than their header --- README.md | 6 ++++++ roles/player/v1.md | 2 ++ roles/source/v1.md | 2 ++ roles/visualizer/v1.md | 2 ++ 4 files changed, 12 insertions(+) diff --git a/README.md b/README.md index da3919a..8f1231f 100644 --- a/README.md +++ b/README.md @@ -1323,6 +1323,8 @@ During an active stream, unavailable clients SHOULD discard otherwise valid audi - Bytes 9-12: send_ahead (big-endian uint32) - microseconds from the server's transmission of this message to `timestamp` - Rest of bytes: encoded audio frame +An audio message shorter than 13 bytes is a protocol error: the client MUST close the connection. + The timestamp indicates when the first audio sample in this chunk should be output. Clients MUST translate this server timestamp to their local clock using the [time filter](#clock-synchronization), subtracting their [`output_delay_ms`](#client--server-clientstate-player-object) from the timestamp. Clients SHOULD compensate for any known processing delays (e.g., DAC latency, audio buffer delays) by accounting for these delays when submitting audio to the hardware. `send_ahead` reports the lead the server had in hand when it sent the chunk: its transmit time is `timestamp - send_ahead` in the server's clock, taken as described in [Transmit timestamps](#transmit-timestamps). The field saturates rather than wrapping: a server MUST send `0` when it transmits at or after `timestamp`, and `4294967295` when the true lead exceeds what the field can represent (about 71 minutes). Both saturation values report that no lead was measured, not a lead of that length, so a player MUST NOT use a chunk carrying either as a delay sample. @@ -1509,6 +1511,8 @@ Clients MUST send `client-stream/start` before the first audio chunk. Sending `c - Bytes 1-8: timestamp (big-endian int64) - server clock time in microseconds when the first sample was captured - Rest of bytes: encoded audio frame +A source audio message shorter than 9 bytes is a protocol error: the server MUST close the connection. + The timestamp indicates when the first audio sample in this chunk was captured (in server time domain). It is the best-effort time the audio reached the input (ADC). For codecs with encoder delay, it refers to the capture time of the first sample the decoder will emit for the chunk. The server MAY resample/transcode and then distribute the audio to players with its normal buffering and synchronization strategy. A source MUST NOT send a chunk longer than 150 ms, and SHOULD NOT send one shorter than 5 ms (the final chunk before a `client-stream/end` MAY be shorter). After a network stall, clients SHOULD drop buffered backlog beyond a small bound and resume from live capture rather than burst stale audio. @@ -1808,6 +1812,8 @@ Each visualization `type` has its own binary message type. Every message carries - Bytes 1-8: timestamp (big-endian int64) - server clock time in microseconds when this data should be displayed. Clients MUST translate this server timestamp to their local clock using the [time filter](#clock-synchronization) - Remaining bytes: data, layout per type below; all `uint16` fields are big-endian +A visualization message shorter than 9 bytes is a protocol error: the client MUST close the connection. + Data whose timestamp is already in the past on arrival is dropped; stale visualization frames are never rendered. `loudness`, `spectrum` bins, and the `f_peak` amplitude use the full `uint16` range 0-65535, where 0 = silence and 65535 = full scale. Values are A-weighted and dB-scaled: -60 dB → 0, 0 dB → 65535, mapped linearly across that range. diff --git a/roles/player/v1.md b/roles/player/v1.md index 384a831..47c228b 100644 --- a/roles/player/v1.md +++ b/roles/player/v1.md @@ -112,6 +112,8 @@ During an active stream, unavailable clients SHOULD discard otherwise valid audi - Bytes 9-12: send_ahead (big-endian uint32) - microseconds from the server's transmission of this message to `timestamp` - Rest of bytes: encoded audio frame +An audio message shorter than 13 bytes is a protocol error: the client MUST close the connection. + The timestamp indicates when the first audio sample in this chunk should be output. Clients MUST translate this server timestamp to their local clock using the [time filter](../../messaging.md#clock-synchronization), subtracting their [`output_delay_ms`](#client--server-clientstate-player-object) from the timestamp. Clients SHOULD compensate for any known processing delays (e.g., DAC latency, audio buffer delays) by accounting for these delays when submitting audio to the hardware. `send_ahead` reports the lead the server had in hand when it sent the chunk: its transmit time is `timestamp - send_ahead` in the server's clock, taken as described in [Transmit timestamps](../../messaging.md#transmit-timestamps). The field saturates rather than wrapping: a server MUST send `0` when it transmits at or after `timestamp`, and `4294967295` when the true lead exceeds what the field can represent (about 71 minutes). Both saturation values report that no lead was measured, not a lead of that length, so a player MUST NOT use a chunk carrying either as a delay sample. diff --git a/roles/source/v1.md b/roles/source/v1.md index cc3829d..0f9716b 100644 --- a/roles/source/v1.md +++ b/roles/source/v1.md @@ -109,6 +109,8 @@ Clients MUST send `client-stream/start` before the first audio chunk. Sending `c - Bytes 1-8: timestamp (big-endian int64) - server clock time in microseconds when the first sample was captured - Rest of bytes: encoded audio frame +A source audio message shorter than 9 bytes is a protocol error: the server MUST close the connection. + The timestamp indicates when the first audio sample in this chunk was captured (in server time domain). It is the best-effort time the audio reached the input (ADC). For codecs with encoder delay, it refers to the capture time of the first sample the decoder will emit for the chunk. The server MAY resample/transcode and then distribute the audio to players with its normal buffering and synchronization strategy. A source MUST NOT send a chunk longer than 150 ms, and SHOULD NOT send one shorter than 5 ms (the final chunk before a `client-stream/end` MAY be shorter). After a network stall, clients SHOULD drop buffered backlog beyond a small bound and resume from live capture rather than burst stale audio. diff --git a/roles/visualizer/v1.md b/roles/visualizer/v1.md index e0b7a8f..b917f42 100644 --- a/roles/visualizer/v1.md +++ b/roles/visualizer/v1.md @@ -67,6 +67,8 @@ Each visualization `type` has its own binary message type. Every message carries - Bytes 1-8: timestamp (big-endian int64) - server clock time in microseconds when this data should be displayed. Clients MUST translate this server timestamp to their local clock using the [time filter](../../messaging.md#clock-synchronization) - Remaining bytes: data, layout per type below; all `uint16` fields are big-endian +A visualization message shorter than 9 bytes is a protocol error: the client MUST close the connection. + Data whose timestamp is already in the past on arrival is dropped; stale visualization frames are never rendered. `loudness`, `spectrum` bins, and the `f_peak` amplitude use the full `uint16` range 0-65535, where 0 = silence and 65535 = full scale. Values are A-weighted and dB-scaled: -60 dB → 0, 0 dB → 65535, mapped linearly across that range. From 6b1f52c6d085abf7c297fd857538466a65511894 Mon Sep 17 00:00:00 2001 From: Maxim Raznatovski Date: Tue, 20 Oct 2026 17:23:27 +0200 Subject: [PATCH 3/3] fix: close the connection on a JSON message that is not valid JSON --- README.md | 2 +- messaging.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8f1231f..1cf4087 100644 --- a/README.md +++ b/README.md @@ -386,7 +386,7 @@ Receivers that discard a binary role payload MUST still process Noise messages, ### Binary Message ID Structure -The first byte of every decrypted binary message is its message ID. A binary message whose AEAD plaintext is empty is a protocol error: the receiver MUST close the connection. IDs are assigned from the table below; each role's binary message definitions name the exact IDs it uses. +The first byte of every decrypted binary message is its message ID. A binary message whose AEAD plaintext is empty is a protocol error: the receiver MUST close the connection. So is a message with ID `0` whose body is not a valid UTF-8 JSON object with a `type` string and a `payload` object. IDs are assigned from the table below; each role's binary message definitions name the exact IDs it uses. | IDs | Assignment | |---|---| diff --git a/messaging.md b/messaging.md index d919224..d6db34b 100644 --- a/messaging.md +++ b/messaging.md @@ -68,7 +68,7 @@ Receivers that discard a binary role payload MUST still process Noise messages, ### Binary Message ID Structure -The first byte of every decrypted binary message is its message ID. A binary message whose AEAD plaintext is empty is a protocol error: the receiver MUST close the connection. IDs are assigned from the table below; each role's binary message definitions name the exact IDs it uses. +The first byte of every decrypted binary message is its message ID. A binary message whose AEAD plaintext is empty is a protocol error: the receiver MUST close the connection. So is a message with ID `0` whose body is not a valid UTF-8 JSON object with a `type` string and a `payload` object. IDs are assigned from the table below; each role's binary message definitions name the exact IDs it uses. | IDs | Assignment | |---|---|