wire: make ble message handling use the same logic as other inputs
What changed, and why it matters
This commit refactors how Bluetooth Low Energy (BLE) messages are handled in the Blockstream Jade hardware wallet so that BLE uses the same message-parsing path as USB serial and TCP (QEMU). Previously, BLE had a special 'reject_incomplete' flag that could discard buffered data when the buffer was full. The change removes that special-case logic and makes all input sources feed data the same way. The commit message frames this as a cleanup that enables future work on recovering from message-stream errors, not as a security fix. There is no direct evidence in the diff of an exploitable vulnerability, but unifying input handling reduces the chance of subtle BLE-only bugs such as partial-message truncation or inconsistent state.
Treat as a defensive hardening/cleanup change rather than an urgent security patch. Reviewers should verify that the new BLE chunked-copy loop cannot cause `handle_data()` to be called with `new_data_len` that, combined with `ble_read`, exceeds `MAX_INPUT_MSG_SIZE`, and that the removal of `reject_incomplete` does not introduce a denial-of-service path via unbounded buffering. Consider requesting a security note from the vendor if this change was motivated by a reported issue.
Security signals we found
Refactor of untrusted input parsing path for BLE, serial, and TCP
Removal of BLE-specific `reject_incomplete` flush behavior that could drop buffered bytes
Unification of input handling across all transport sources
No explicit bounds-check changes; existing `MAX_INPUT_MSG_SIZE` checks remain
Commit message mentions future 're-sync message streams when an error occurs' but does not claim a current vulnerability
Evidence from the diff
The patch removes the reject_incomplete parameter from handle_data() in main/wire.c/main/wire.h and updates all callers (libjade/libjade.c, main/ble/ble.c, main/qemu/qemu_tcp.c, main/serial.c) to call the simplified signature. The BLE handler is rewritten to copy incoming mbuf data in chunks and pass each chunk through handle_data(), rather than using the previous logic that could call handle_data(..., reject_incomplete=true) to flush the buffer when ble_read + ble_msg_len >= MAX_INPUT_MSG_SIZE. The old behavior could discard valid-but-incomplete buffered data under buffer-pressure conditions; the new behavior keeps buffered data and lets the common parser manage it. The commit also adds documentation to wire.h clarifying buffer semantics.
Changed components
main/wire.cmain/wire.hmain/ble/ble.cmain/serial.cmain/qemu/qemu_tcp.clibjade/libjade.clibjade/libjade.hInspect captured patch +61 / −69
diff --git a/libjade/libjade.c b/libjade/libjade.c
index 127ae39..b67b063 100644
--- a/libjade/libjade.c
+++ b/libjade/libjade.c
@@ -475,36 +475,33 @@ static uint8_t _libjade_serial_data_in[MAX_INPUT_MSG_SIZE + 1] = { 0 };
static size_t _libjade_serial_read_ptr = 0;
static TickType_t _libjade_last_processing_time = 0;
-bool libjade_send(const uint8_t* data, size_t size)
+bool libjade_send(const uint8_t* data, size_t len)
{
// Pass messages as though they come from the serial interface
_libjade_serial_data_in[0] = SOURCE_SERIAL;
- while (size) {
+ while (len) {
const size_t remaining_bytes = MAX_INPUT_MSG_SIZE - _libjade_serial_read_ptr;
- const size_t to_write = size > remaining_bytes ? remaining_bytes : size;
-
- JADE_ASSERT(_libjade_serial_read_ptr + to_write <= MAX_INPUT_MSG_SIZE);
- memcpy(_libjade_serial_data_in + 1 + _libjade_serial_read_ptr, data, to_write);
- // Don't reject incomplete messages. If the buffer is full,
- // handle_data() will reject the entire buffer for us. Any
- // valid messages will be removed from the front of the buffer.
- const bool reject_incomplete = false;
- handle_data(_libjade_serial_data_in, &_libjade_serial_read_ptr, to_write, &_libjade_last_processing_time,
- reject_incomplete);
- data += to_write;
- size -= to_write;
+ const size_t copy_len = len > remaining_bytes ? remaining_bytes : len;
+
+ JADE_ASSERT(_libjade_serial_read_ptr + copy_len <= MAX_INPUT_MSG_SIZE);
+ memcpy(_libjade_serial_data_in + 1 + _libjade_serial_read_ptr, data, copy_len);
+
+ // Pass data through to the common handler
+ handle_data(_libjade_serial_data_in, &_libjade_serial_read_ptr, copy_len, &_libjade_last_processing_time);
+ data += copy_len;
+ len -= copy_len;
}
return true;
}
-uint8_t* libjade_receive(const unsigned int timeout, size_t* size_out)
+uint8_t* libjade_receive(const unsigned int timeout, size_t* len_out)
{
// timeout is in seconds, convert to milliseconds
const unsigned int ms = timeout * 1000;
- void* item = xRingbufferReceive(serial_out, size_out, ms / portTICK_PERIOD_MS);
+ void* item = xRingbufferReceive(serial_out, len_out, ms / portTICK_PERIOD_MS);
if (!item) {
// No message available
- *size_out = 0;
+ *len_out = 0;
}
return item;
}
diff --git a/libjade/libjade.h b/libjade/libjade.h
index 188ecf7..0462a9c 100644
--- a/libjade/libjade.h
+++ b/libjade/libjade.h
@@ -34,13 +34,13 @@ LIBJADE_API void libjade_stop(void);
/*
* Send a CBOR message to the global libjade instance.
*/
-LIBJADE_API bool libjade_send(const uint8_t* data, size_t size);
+LIBJADE_API bool libjade_send(const uint8_t* data, size_t len);
/*
* Receive a CBOR reply message from the global libjade instance.
* `libjade_release` must be used to free any returned message.
*/
-LIBJADE_API uint8_t* libjade_receive(unsigned int timeout, size_t* size_out);
+LIBJADE_API uint8_t* libjade_receive(unsigned int timeout, size_t* len_out);
/*
* Free a CBOR message returned from `libjade_receive`.
diff --git a/main/ble/ble.c b/main/ble/ble.c
index a902290..d80ca01 100644
--- a/main/ble/ble.c
+++ b/main/ble/ble.c
@@ -84,47 +84,41 @@ static int gatt_chr_event(
JADE_LOGI("Entering gatt_chr_event op: %d for attr: %d", ctxt->op, attr_handle);
if (attr_handle == rx_val_handle) {
- switch (ctxt->op) {
- case BLE_GATT_ACCESS_OP_WRITE_CHR:
- JADE_LOGI("Reading from ble device");
-
- const uint16_t ble_msg_len = OS_MBUF_PKTLEN(ctxt->om);
- JADE_LOGI("Reading %u bytes", ble_msg_len);
-
- if (ble_msg_len == 0) {
- return 0;
- }
-
- // Check we won't overrun the buffer
- if (ble_read + ble_msg_len >= MAX_INPUT_MSG_SIZE) {
- 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, reject_incomplete);
- JADE_ASSERT(ble_read == 0);
- }
+ if (ctxt->op != BLE_GATT_ACCESS_OP_WRITE_CHR) {
+ JADE_LOGW("Unexpected gatt access op: %u for rx chr, ignoring", ctxt->op);
+ return 0;
+ }
- uint16_t out_copy_len;
- uint8_t* const ble_data_in = full_ble_data_in + 1;
- const int rc = ble_hs_mbuf_to_flat(ctxt->om, ble_data_in + ble_read, ble_msg_len, &out_copy_len);
+ uint8_t* const ble_data_in = full_ble_data_in + 1;
+ uint16_t new_data_len = OS_MBUF_PKTLEN(ctxt->om);
+ JADE_LOGI("Reading %u bytes from ble device", new_data_len);
+
+ while (new_data_len) {
+ // Copy as many bytes as possible into the ble read buffer.
+ // The read buffer can never become full: if it fills up with
+ // unparsable data, then handle_data() rejects the whole buffer.
+ JADE_ASSERT(ble_read < MAX_INPUT_MSG_SIZE);
+ const size_t remaining_len = MAX_INPUT_MSG_SIZE - ble_read;
+ const size_t copy_len = new_data_len > remaining_len ? remaining_len : new_data_len;
+ // handle_data() requires size_t, but the ble functions use uint16_t:
+ // assert copy_len fits in uint16_t as a belt-n-braces sanity check.
+ JADE_ASSERT(copy_len <= 0xffff);
+
+ uint16_t out_len;
+ const int rc = ble_hs_mbuf_to_flat(ctxt->om, ble_data_in + ble_read, copy_len, &out_len);
JADE_ASSERT(rc == 0);
- JADE_ASSERT(out_copy_len == ble_msg_len);
+ JADE_ASSERT(out_len == copy_len);
- JADE_LOGD("Passing %u+%u bytes from ble device to common handler", ble_read, ble_msg_len);
- const bool reject_incomplete = false;
- handle_data(full_ble_data_in, &ble_read, ble_msg_len, &last_processing_time, reject_incomplete);
- return 0;
-
- default:
- JADE_LOGW("Unexpected gatt access op: %u for rx chr, ignoring", ctxt->op);
- return 0;
+ // Pass data through to the common handler
+ handle_data(full_ble_data_in, &ble_read, copy_len, &last_processing_time);
+ new_data_len -= copy_len;
}
} else if (attr_handle == tx_val_handle) {
JADE_LOGW("Received op %u for tx chr, ignoring", ctxt->op);
- return 0;
+ } else {
+ char buf[BLE_UUID_STR_LEN];
+ JADE_LOGW("Unexpected uuid, ignoring: %s", ble_uuid_to_str(ctxt->chr->uuid, buf));
}
-
- char buf[BLE_UUID_STR_LEN];
- JADE_LOGW("Unexpected uuid, ignoring: %s", ble_uuid_to_str(ctxt->chr->uuid, buf));
return 0;
}
diff --git a/main/qemu/qemu_tcp.c b/main/qemu/qemu_tcp.c
index d1ab642..4ebc00c 100644
--- a/main/qemu/qemu_tcp.c
+++ b/main/qemu/qemu_tcp.c
@@ -98,8 +98,7 @@ 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 reject_incomplete = false;
- handle_data(full_qemu_tcp_data_in, &read, len, &last_processing_time, reject_incomplete);
+ handle_data(full_qemu_tcp_data_in, &read, len, &last_processing_time);
}
}
diff --git a/main/serial.c b/main/serial.c
index 106183d..b010929 100644
--- a/main/serial.c
+++ b/main/serial.c
@@ -125,9 +125,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 reject_incomplete = false;
- handle_data(full_serial_data_in, &read, len, &last_processing_time, reject_incomplete);
+ // Pass data through to the common handler
+ handle_data(full_serial_data_in, &read, len, &last_processing_time);
}
serial_post_exit_event_and_await_death(&serial_reader_shutdown_done);
}
diff --git a/main/wire.c b/main/wire.c
index d48a2ce..93f0b8e 100644
--- a/main/wire.c
+++ b/main/wire.c
@@ -91,10 +91,7 @@ static bool handle_immediate_message(const cbor_msg_t* const ctx)
return false;
}
-// Handle bytes in receive buffer
-// NOTE: assumes sizes of input buffer - could be passed size 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,
- bool reject_incomplete)
+void handle_data(uint8_t* full_data_in, size_t* read_ptr, const size_t new_data_len, TickType_t* last_processing_time)
{
JADE_ASSERT(full_data_in);
JADE_ASSERT(read_ptr && *read_ptr <= MAX_INPUT_MSG_SIZE && *read_ptr + new_data_len <= MAX_INPUT_MSG_SIZE);
@@ -104,14 +101,14 @@ void handle_data(uint8_t* full_data_in, size_t* read_ptr, const size_t new_data_
const TickType_t now = xTaskGetTickCount();
JADE_ASSERT(now >= *last_processing_time);
- // Buffer is stale if we had bytes already and the timeout has expired
- bool have_stale = *read_ptr && now > *last_processing_time + TIMEOUT_TICKS;
- JADE_LOGI("%u new of %u total %sbytes at tick %lu (prev tick %lu)", new_data_len, *read_ptr + new_data_len,
- have_stale ? "stale " : "", now, *last_processing_time);
-
uint8_t* const data_in = full_data_in + 1;
cbor_msg_t ctx = { .source = full_data_in[0] };
+ // Buffer is stale if we had bytes already and the timeout has expired
+ bool have_stale = *read_ptr && now > *last_processing_time + TIMEOUT_TICKS;
+ JADE_LOGI("%u new of %u total %sbytes at tick %lu (prev %lu) from %d", new_data_len, *read_ptr + new_data_len,
+ have_stale ? "stale " : "", now, *last_processing_time, ctx.source);
+
while (true) {
// Try parsing an RPC message from the buffer plus the new data
const size_t parse_len = *read_ptr + new_data_len;
diff --git a/main/wire.h b/main/wire.h
index 2fac18f..93eca2f 100644
--- a/main/wire.h
+++ b/main/wire.h
@@ -5,7 +5,13 @@
#include <stddef.h>
#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 reject_incomplete);
+// Handle RPC message data from an external source.
+// full_data_in must be a buffer of length MAX_INPUT_MSG_SIZE + 1.
+// The first byte of full_data_in must be the input source (e.g. SOURCE_SERIAL).
+// read_ptr must be the end index of all data in the buffer including new data.
+// new_data_len is the size of new data written to the buffer before calling.
+// Any valid messages that can be extracted from the data will be processed
+// and removed from the buffer, leaving read_ptr at the new end index.
+void handle_data(uint8_t* full_data_in, size_t* read_ptr, size_t new_data_len, TickType_t* last_processing_time);
#endif /* WIRE_H_ */
Why this scored 48/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.