wire: fix not processing all received data when wire.c timeout hit
What changed, and why it matters
This commit fixes a bug in how Blockstream Jade processes incoming messages when a communication timeout occurs. Previously, if old, partial data was sitting in the buffer when new data arrived after a timeout, the code would discard the old bytes and move only the new bytes to the start of the buffer, potentially ignoring any complete message that could be formed by combining the old and new data. The fix changes the logic so that, after a timeout, the code still tries to process all bytes in the buffer together, only rejecting incomplete data if no complete message can be parsed. The commit also renames a parameter for clarity. There is no explicit vendor statement that this is a security fix, and no CVE or researcher attribution is present in the materials.
Treat as a reliability/robustness fix. Review whether the timeout path could be triggered by an attacker sending fragmented messages to induce desynchronization or denial of service. If such a scenario is confirmed, consider a security advisory and CVE assignment. Otherwise, include in regular release notes.
Security signals we found
Logic change in message-boundary parsing after timeout
Potential message loss/desynchronization due to incorrect buffer handling
No explicit security framing by vendor
No CVE or advisory referenced in commit
Evidence from the diff
The change is in main/wire.c and its callers. The handle_data() function previously, upon detecting a timeout, called handle_data_impl() with reject_if_no_msg=true on the existing buffer, asserted the buffer was empty, and then memmoved only the new_data_len bytes to the start of the buffer. This meant any complete CBOR message spanning stale and newly received bytes would be split: the stale prefix was discarded, and only the new suffix was kept, likely leaving an incomplete message. The new code sets reject_incomplete=true but appends the new data first, then calls handle_data_impl() on the combined buffer. handle_data_impl() will now process any complete messages from the combined buffer and only discard the remainder if it is incomplete and reject_incomplete is true. After a complete message is read, reject_incomplete is reset to false so trailing incomplete data is preserved for future completion. The parameter rename from force_reject_if_no_msg to reject_incomplete is cosmetic. The bug could cause message loss or desynchronization after a timeout, but the commit does not describe a specific security impact or attack scenario.
Changed components
main/wire.cmain/wire.hmain/serial.cmain/ble/ble.cmain/qemu/qemu_tcp.clibjade/libjade.cInspect captured patch +26 / −31
diff --git a/libjade/libjade.c b/libjade/libjade.c
index 00893c4..860e8a4 100644
--- a/libjade/libjade.c
+++ b/libjade/libjade.c
@@ -485,9 +485,9 @@ bool libjade_send(const uint8_t* data, const size_t size)
uint8_t* data_with_source = _libjade_serial_data_in;
data_with_source[0] = SOURCE_SERIAL;
memcpy(data_with_source + 1 + _libjade_serial_read_ptr, data, size);
- const bool force_reject_if_no_msg = false;
- handle_data(data_with_source, &_libjade_serial_read_ptr, size, &_libjade_last_processing_time,
- force_reject_if_no_msg, _libjade_serial_data_out);
+ const bool reject_incomplete = false;
+ handle_data(data_with_source, &_libjade_serial_read_ptr, size, &_libjade_last_processing_time, reject_incomplete,
+ _libjade_serial_data_out);
return true;
}
diff --git a/main/ble/ble.c b/main/ble/ble.c
index 73b45fd..77bc5cb 100644
--- a/main/ble/ble.c
+++ b/main/ble/ble.c
@@ -98,10 +98,10 @@ static int gatt_chr_event(
// Check we won't overrun the buffer
if (ble_read + ble_msg_len >= MAX_INPUT_MSG_SIZE) {
- const bool force_reject_if_no_msg = true; // reject what we have in the buffer
+ const bool reject_incomplete = true; // Reject current buffer if incomplete
const size_t new_data = 0;
handle_data(
- full_ble_data_in, &ble_read, new_data, &last_processing_time, force_reject_if_no_msg, ble_data_out);
+ full_ble_data_in, &ble_read, new_data, &last_processing_time, reject_incomplete, ble_data_out);
JADE_ASSERT(ble_read == 0);
}
@@ -112,9 +112,9 @@ static int gatt_chr_event(
JADE_ASSERT(out_copy_len == ble_msg_len);
JADE_LOGD("Passing %u+%u bytes from ble device to common handler", ble_read, ble_msg_len);
- const bool force_reject_if_no_msg = false;
+ const bool reject_incomplete = false;
handle_data(
- full_ble_data_in, &ble_read, ble_msg_len, &last_processing_time, force_reject_if_no_msg, ble_data_out);
+ full_ble_data_in, &ble_read, ble_msg_len, &last_processing_time, reject_incomplete, ble_data_out);
return 0;
default:
diff --git a/main/qemu/qemu_tcp.c b/main/qemu/qemu_tcp.c
index 3c74d3f..6275640 100644
--- a/main/qemu/qemu_tcp.c
+++ b/main/qemu/qemu_tcp.c
@@ -99,9 +99,8 @@ static void qemu_tcp_reader(void* ignore)
// Pass to common handler
JADE_LOGD("Passing %u+%u bytes from tcp stream to common handler", read, len);
- const bool force_reject_if_no_msg = false;
- handle_data(
- full_qemu_tcp_data_in, &read, len, &last_processing_time, force_reject_if_no_msg, qemu_tcp_data_out);
+ const bool reject_incomplete = false;
+ handle_data(full_qemu_tcp_data_in, &read, len, &last_processing_time, reject_incomplete, qemu_tcp_data_out);
}
}
diff --git a/main/serial.c b/main/serial.c
index f723e4c..6f8509b 100644
--- a/main/serial.c
+++ b/main/serial.c
@@ -127,8 +127,8 @@ static void serial_reader(void* ignore)
#endif // CONFIG_IDF_TARGET_ESP32S3
JADE_LOGD("Passing %u+%u bytes from serial device to common handler", read, len);
- const bool force_reject_if_no_msg = false;
- handle_data(full_serial_data_in, &read, len, &last_processing_time, force_reject_if_no_msg, serial_data_out);
+ const bool reject_incomplete = false;
+ handle_data(full_serial_data_in, &read, len, &last_processing_time, reject_incomplete, serial_data_out);
}
serial_post_exit_event_and_await_death(&serial_reader_shutdown_done);
}
diff --git a/main/wire.c b/main/wire.c
index d0ecc24..42c1f2d 100644
--- a/main/wire.c
+++ b/main/wire.c
@@ -77,7 +77,7 @@ static bool handleImmediateMessage(cbor_msg_t* ctx)
// Handle bytes in receive buffer
// NOTE: assumes sizes of input and output buffers - could be passed sizes if preferred
-static void handle_data_impl(uint8_t* full_data_in, size_t* read_ptr, bool reject_if_no_msg, uint8_t* data_out)
+static void handle_data_impl(uint8_t* full_data_in, size_t* read_ptr, bool reject_incomplete, uint8_t* data_out)
{
JADE_ASSERT(full_data_in);
JADE_ASSERT(read_ptr);
@@ -105,7 +105,7 @@ static void handle_data_impl(uint8_t* full_data_in, size_t* read_ptr, bool rejec
// If we could not fetch a message from the buffer..
if (msg_len == 0) {
- if (!reject_if_no_msg) {
+ if (!reject_incomplete) {
// Not a complete cbor message, but we are allowed to await more data to complete the message
JADE_LOGD("Got incomplete CBOR message, length %u - awaiting more data...", read);
return;
@@ -143,10 +143,10 @@ static void handle_data_impl(uint8_t* full_data_in, size_t* read_ptr, bool rejec
// Otherwise we have some data left in the buffer - move the unhandled data down to the start of the buffer
// (overwriting what we've handled)
- // Also set 'reject_if_no_msg' to false, as we have now read a message.
+ // Also set 'reject_incomplete' to false, as we have now read a message.
memmove(data_in, data_in + msg_len, read - msg_len);
*read_ptr -= msg_len;
- reject_if_no_msg = false;
+ reject_incomplete = false;
}
// Discard the entire buffer by resetting the read-ptr
@@ -156,7 +156,7 @@ static void handle_data_impl(uint8_t* full_data_in, size_t* read_ptr, bool rejec
// Handle new bytes received
// NOTE: assumes sizes of input and output buffers - could be passed sizes if preferred
void handle_data(uint8_t* full_data_in, size_t* read_ptr, const size_t new_data_len, TickType_t* last_processing_time,
- const bool force_reject_if_no_msg, uint8_t* data_out)
+ bool reject_incomplete, uint8_t* data_out)
{
JADE_ASSERT(full_data_in);
JADE_ASSERT(read_ptr);
@@ -168,25 +168,21 @@ void handle_data(uint8_t* full_data_in, size_t* read_ptr, const size_t new_data_
const TickType_t time_now = xTaskGetTickCount();
JADE_ASSERT(time_now >= *last_processing_time);
- // Handle any stale bytes in the buffer
+ JADE_LOGI("Received %u new bytes, total in buffer is now %u, time is %lu ticks (time since last %lu)", new_data_len,
+ *read_ptr + new_data_len, time_now, time_now - *last_processing_time);
+
if (*read_ptr > 0 && time_now > *last_processing_time + TIMEOUT_TICKS) {
- // Have stale bytes resting in buffer - reject them
- const bool reject_if_no_msg = true;
- const size_t initial_offset = *read_ptr;
- JADE_LOGW("Timing out %u bytes in buffer", *read_ptr);
- handle_data_impl(full_data_in, read_ptr, reject_if_no_msg, data_out);
- JADE_ASSERT(*read_ptr == 0);
-
- // Copy newly recevied bytes down to start of buffer
- uint8_t* const data_in = full_data_in + 1;
- memmove(data_in, data_in + initial_offset, new_data_len);
+ // Have stale bytes resting in buffer - reject if no complete message found
+ JADE_LOGW("Timing out %u bytes in buffer (time_now: %lu, last_processing_time: %lu, TIMEOUT_TICKS: %lu)",
+ *read_ptr, time_now, *last_processing_time, TIMEOUT_TICKS);
+ reject_incomplete = true;
}
// Append new bytes, and try to parse
*read_ptr += new_data_len;
JADE_LOGD("Passing %u bytes to common handler", *read_ptr);
- const bool reject_if_no_msg = force_reject_if_no_msg || (*read_ptr == MAX_INPUT_MSG_SIZE);
- handle_data_impl(full_data_in, read_ptr, reject_if_no_msg, data_out);
+ reject_incomplete |= (*read_ptr == MAX_INPUT_MSG_SIZE);
+ handle_data_impl(full_data_in, read_ptr, reject_incomplete, data_out);
// Update caller's 'last processing time'
*last_processing_time = time_now;
diff --git a/main/wire.h b/main/wire.h
index 1ae8375..af5e58a 100644
--- a/main/wire.h
+++ b/main/wire.h
@@ -6,6 +6,6 @@
#include <stdint.h>
void handle_data(uint8_t* full_data_in, size_t* read_ptr, size_t new_data_len, TickType_t* last_processing_time,
- bool force_reject_if_no_msg, uint8_t* data_out);
+ bool reject_incomplete, uint8_t* data_out);
#endif /* WIRE_H_ */
Why this scored 35/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.