Renaming all API in io_ext to ioe_.*() + small io refactoring
What changed, and why it matters
This commit is mostly a renaming exercise: functions that started with 'io_' in the Ledger Bitcoin app now start with 'ioe_' to avoid clashing with the operating system's own 'io_' functions. Alongside the rename, one small logic fix was made in the response-buffering code. Previously, when adding response data that was too large, the code could pass a negative length to a recursive call, which is unsafe. The new code computes how much space remains, copies only that amount, and then marks the response as too long. This is a defensive improvement, not an active vulnerability fix for a known exploit.
Treat as a routine hardening/refactoring commit. Review the new ioe_add_to_response bounds logic in isolation to confirm it matches the intended truncation behavior, and verify that all call sites were renamed consistently. No urgent security response is warranted absent additional context.
Security signals we found
Buffer-length calculation hardened in APDU response builder
Recursive response-append path removed
Potential negative-length size_t wraparound eliminated
API renamed to avoid namespace collision with OS io_* symbols
Evidence from the diff
The diff renames the io_ext API from io_ to ioe_ across dispatcher.c, io_ext.c/h, main.c, sign_psbt.c, and display.c. The only behavioral change is in ioe_add_to_response(). The old implementation checked if G_output_len >= IO_APDU_BUFFER_SIZE - 2, then in an else-if could call io_add_to_response(rdata, IO_APDU_BUFFER_SIZE - 2 - rdata_len). Because IO_APDU_BUFFER_SIZE - 2 - rdata_len can be negative when rdata_len is large, the recursive call would receive a size_t that wraps to a very large value, causing a memmove of excessive length and potential buffer corruption. The new code computes remaining = (IO_APDU_BUFFER_SIZE - 2) - G_output_len, checks rdata_len > remaining, and copies only remaining bytes before finalizing with SW_WRONG_RESPONSE_LENGTH. This removes the recursion and the signed/unsigned length hazard.
Changed components
src/boilerplate/io_ext.csrc/boilerplate/io_ext.hsrc/boilerplate/dispatcher.csrc/main.csrc/handler/sign_psbt.csrc/ui/display.cInspect captured patch +60 / −61
diff --git a/src/boilerplate/dispatcher.c b/src/boilerplate/dispatcher.c
index 00194fe..f4d9b1d 100644
--- a/src/boilerplate/dispatcher.c
+++ b/src/boilerplate/dispatcher.c
@@ -43,16 +43,16 @@ struct {
} G_dispatcher_state;
static void add_to_response(const void *rdata, size_t rdata_len) {
- io_add_to_response(rdata, rdata_len);
+ ioe_add_to_response(rdata, rdata_len);
}
static void finalize_response(uint16_t sw) {
G_dispatcher_state.sw = sw;
- io_finalize_response(sw);
+ ioe_finalize_response(sw);
}
static void send_response() {
- io_send_response();
+ ioe_send_response();
}
static void set_ui_dirty() {
@@ -68,14 +68,14 @@ static int process_interruption(dispatcher_context_t *dc) {
// Reset structured APDU command
memset(&cmd, 0, sizeof(cmd));
- io_start_interruption_timeout();
+ ioe_start_interruption_timeout();
// Receive command bytes in G_io_apdu_buffer
if ((input_len = io_exchange(CHANNEL_APDU, G_output_len)) < 0) {
return -1;
}
- io_clear_interruption_timeout();
+ ioe_clear_interruption_timeout();
G_output_len = 0;
@@ -133,13 +133,13 @@ void apdu_dispatcher(command_descriptor_t const cmd_descriptors[],
G_dispatcher_context.read_buffer = buffer_create(cmd->data, cmd->lc);
if (cmd->p2 > CURRENT_PROTOCOL_VERSION) {
- io_send_sw(SW_WRONG_P1P2);
+ ioe_send_sw(SW_WRONG_P1P2);
return;
}
if (cmd->cla == CLA_FRAMEWORK && cmd->ins == INS_CONTINUE) {
PRINTF("Unexpected INS_CONTINUE.\n");
- io_send_sw(SW_BAD_STATE); // received INS_CONTINUE, but no command was interrupted.
+ ioe_send_sw(SW_BAD_STATE); // received INS_CONTINUE, but no command was interrupted.
return;
} else {
bool cla_found = false, ins_found = false;
@@ -155,14 +155,14 @@ void apdu_dispatcher(command_descriptor_t const cmd_descriptors[],
}
if (!cla_found) {
- io_send_sw(SW_CLA_NOT_SUPPORTED);
+ ioe_send_sw(SW_CLA_NOT_SUPPORTED);
return;
} else if (!ins_found) {
- io_send_sw(SW_INS_NOT_SUPPORTED);
+ ioe_send_sw(SW_INS_NOT_SUPPORTED);
return;
}
- io_start_processing_timeout();
+ ioe_start_processing_timeout();
handler(&G_dispatcher_context, cmd->p2);
}
@@ -170,7 +170,7 @@ void apdu_dispatcher(command_descriptor_t const cmd_descriptors[],
// Failure to do so indicates a bug in the last command processors.
if (G_dispatcher_state.sw == 0) {
PRINTF("No response before terminating\n");
- io_send_sw(SW_BAD_STATE);
+ ioe_send_sw(SW_BAD_STATE);
}
// We call the termination callback if given, but only if:
@@ -183,7 +183,7 @@ void apdu_dispatcher(command_descriptor_t const cmd_descriptors[],
G_was_processing_screen_shown = 0;
}
- io_clear_processing_timeout();
+ ioe_clear_processing_timeout();
/* Resetting loading information screen */
G_processing_screen_text = NULL;
diff --git a/src/boilerplate/io_ext.c b/src/boilerplate/io_ext.c
index d419c0e..537d6c7 100644
--- a/src/boilerplate/io_ext.c
+++ b/src/boilerplate/io_ext.c
@@ -53,31 +53,31 @@ bool G_was_processing_screen_shown;
uint16_t G_interruption_timeout_start_tick;
uint16_t G_processing_timeout_start_tick;
-void io_start_interruption_timeout() {
+void ioe_start_interruption_timeout() {
G_interruption_timeout_start_tick = G_ticks;
G_is_timeout_active.interruption = true;
}
-void io_clear_interruption_timeout() {
+void ioe_clear_interruption_timeout() {
G_is_timeout_active.interruption = false;
}
-void io_start_processing_timeout() {
+void ioe_start_processing_timeout() {
G_processing_timeout_start_tick = G_ticks;
G_is_timeout_active.processing = true;
}
-void io_clear_processing_timeout() {
+void ioe_clear_processing_timeout() {
G_is_timeout_active.processing = false;
}
-void io_reset_timeouts() {
- io_clear_interruption_timeout();
- io_clear_processing_timeout();
+void ioe_reset_timeouts() {
+ ioe_clear_interruption_timeout();
+ ioe_clear_processing_timeout();
G_was_processing_screen_shown = false;
}
-void io_show_processing_screen() {
+void ioe_show_processing_screen() {
if (!G_was_processing_screen_shown) {
G_was_processing_screen_shown = true;
if (!G_called_from_swap) {
@@ -92,14 +92,14 @@ void app_ticker_event_callback(void) {
if (G_is_timeout_active.processing &&
(uint16_t) (G_ticks - G_processing_timeout_start_tick) >= PROCESSING_TIMEOUT_TICKS) {
- io_clear_processing_timeout();
+ ioe_clear_processing_timeout();
- io_show_processing_screen();
+ ioe_show_processing_screen();
}
if (G_is_timeout_active.interruption &&
(uint16_t) (G_ticks - G_interruption_timeout_start_tick) >= INTERRUPTION_TIMEOUT_TICKS) {
- io_clear_interruption_timeout();
+ ioe_clear_interruption_timeout();
// TODO: It would be better to have the dispatcher be notified somehow.
// This would require some tampering with the io_exchange in
@@ -108,20 +108,21 @@ void app_ticker_event_callback(void) {
}
}
-void io_add_to_response(const void *rdata, size_t rdata_len) {
- if (G_output_len >= IO_APDU_BUFFER_SIZE - 2) {
+void ioe_add_to_response(const void *rdata, size_t rdata_len) {
+ size_t remaining = (IO_APDU_BUFFER_SIZE - 2) - G_output_len;
+
+ if (rdata_len > remaining) {
+ // Truncate: copy only what fits, then finalize with error SW
+ memmove(G_io_apdu_buffer + G_output_len, rdata, remaining);
G_output_len = IO_APDU_BUFFER_SIZE;
write_u16_be(G_io_apdu_buffer, IO_APDU_BUFFER_SIZE - 2, SW_WRONG_RESPONSE_LENGTH);
- } else if (G_output_len + rdata_len > IO_APDU_BUFFER_SIZE - 2) {
- io_add_to_response(rdata, IO_APDU_BUFFER_SIZE - 2 - rdata_len);
- io_finalize_response(SW_WRONG_RESPONSE_LENGTH);
} else {
memmove(G_io_apdu_buffer + G_output_len, rdata, rdata_len);
G_output_len += rdata_len;
}
}
-void io_finalize_response(uint16_t sw) {
+void ioe_finalize_response(uint16_t sw) {
if (G_output_len >= IO_APDU_BUFFER_SIZE - 2) {
G_output_len = IO_APDU_BUFFER_SIZE;
write_u16_be(G_io_apdu_buffer, IO_APDU_BUFFER_SIZE - 2, SW_WRONG_RESPONSE_LENGTH);
@@ -131,17 +132,15 @@ void io_finalize_response(uint16_t sw) {
}
}
-int io_send_response() {
- int ret;
-
- ret = io_exchange(CHANNEL_APDU | IO_RETURN_AFTER_TX, G_output_len);
+int ioe_send_response() {
+ int ret = io_exchange(CHANNEL_APDU | IO_RETURN_AFTER_TX, G_output_len);
G_output_len = 0;
return ret;
}
-int io_send_sw(uint16_t sw) {
+int ioe_send_sw(uint16_t sw) {
G_output_len = 0;
- io_finalize_response(sw);
- return io_send_response();
+ ioe_finalize_response(sw);
+ return ioe_send_response();
}
diff --git a/src/boilerplate/io_ext.h b/src/boilerplate/io_ext.h
index f0d14ec..799a191 100644
--- a/src/boilerplate/io_ext.h
+++ b/src/boilerplate/io_ext.h
@@ -21,46 +21,46 @@ extern uint16_t G_output_len;
* @return 1 if success, 0 otherwise.
*
*/
-uint8_t io_event(uint8_t channel);
+uint8_t ioe_event(uint8_t channel);
-uint16_t io_exchange_al(uint8_t channel, uint16_t tx_len);
+uint16_t ioe_exchange_al(uint8_t channel, uint16_t tx_len);
#define INTERRUPTION_TIMEOUT_TICKS 50
#define PROCESSING_TIMEOUT_TICKS 10
/**
* Instructs io_event to reset the app if INTERRUPTION_TIMEOUT_TICKS tick events are received before
- * io_clear_interruption_timeout is called. Used to cause an app reset if the client stop responding
- * while an APDU is being processed.
+ * ioe_clear_interruption_timeout is called. Used to cause an app reset if the client stop
+ * responding while an APDU is being processed.
*/
-void io_start_interruption_timeout();
+void ioe_start_interruption_timeout();
/**
- * Removes the timeout started from io_start_interruption_timeout.
+ * Removes the timeout started from ioe_start_interruption_timeout.
*/
-void io_clear_interruption_timeout();
+void ioe_clear_interruption_timeout();
/**
* Instructs io_event to show the "Processing..." screen if PROCESSING_TIMEOUT_TICKS tick events are
- * received before io_clear_interruption_timeout is called.
+ * received before ioe_clear_interruption_timeout is called.
*/
-void io_start_processing_timeout();
+void ioe_start_processing_timeout();
/**
* Removes the timeout started from io_start_processing_timeout.
*/
-void io_clear_processing_timeout();
+void ioe_clear_processing_timeout();
/**
* Clears both the interruption and processing timeouts, and sets G_was_processing_screen_shown to
* false.
*/
-void io_reset_timeouts();
+void ioe_reset_timeouts();
/**
* Shows the "Processing..." screen.
*/
-void io_show_processing_screen();
+void ioe_show_processing_screen();
/**
* Append data to the APDU response buffer (G_io_apdu_buffer).
@@ -70,25 +70,25 @@ void io_show_processing_screen();
* @param[in] rdata_len
* Length of data to append.
*/
-void io_add_to_response(const void *rdata, size_t rdata_len);
+void ioe_add_to_response(const void *rdata, size_t rdata_len);
/**
* Finalize the APDU response by appending the status word.
- * Must be called after all io_add_to_response() calls are done.
+ * Must be called after all ioe_add_to_response() calls are done.
*
* @param[in] sw
* Status word of APDU response.
*/
-void io_finalize_response(uint16_t sw);
+void ioe_finalize_response(uint16_t sw);
/**
* Send the previously prepared APDU response via io_exchange.
- * The response must have been built with io_add_to_response()/io_finalize_response()
+ * The response must have been built with ioe_add_to_response()/ioe_finalize_response()
* before calling this function.
*
* @return zero or positive integer if success, -1 otherwise.
*/
-int io_send_response(void);
+int ioe_send_response(void);
/**
* Send APDU response containing only a status word (no data).
@@ -98,4 +98,4 @@ int io_send_response(void);
*
* @return zero or positive integer if success, -1 otherwise.
*/
-int io_send_sw(uint16_t sw);
+int ioe_send_sw(uint16_t sw);
diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c
index 963a709..8c24c85 100644
--- a/src/handler/sign_psbt.c
+++ b/src/handler/sign_psbt.c
@@ -2184,7 +2184,7 @@ void handler_sign_psbt(dispatcher_context_t *dc, uint8_t protocol_version) {
}
// Signing always takes some time, so we rather not wait before showing the spinner
- io_show_processing_screen();
+ ioe_show_processing_screen();
/** SIGNING FLOW
*
diff --git a/src/main.c b/src/main.c
index 21b2e9c..0d6bf3e 100644
--- a/src/main.c
+++ b/src/main.c
@@ -85,7 +85,7 @@ const command_descriptor_t COMMAND_DESCRIPTORS[] = {
// clang-format on
static void initialize_app_globals() {
- io_reset_timeouts();
+ ioe_reset_timeouts();
// We only zero out should_exit field and not the entire G_swap_state, as
// we need the globals initialization to happen _after_ calling copy_transaction_parameters when
@@ -135,7 +135,7 @@ void app_main() {
// Parse APDU command from G_io_apdu_buffer
if (!apdu_parser(&cmd, G_io_apdu_buffer, input_len)) {
PRINTF("=> /!\\ BAD LENGTH: %.*H\n", input_len, G_io_apdu_buffer);
- io_send_sw(SW_WRONG_DATA_LENGTH);
+ ioe_send_sw(SW_WRONG_DATA_LENGTH);
continue;
}
@@ -150,7 +150,7 @@ void app_main() {
if (G_called_from_swap) {
if (cmd.cla != CLA_APP) {
- io_send_sw(SW_CLA_NOT_SUPPORTED);
+ ioe_send_sw(SW_CLA_NOT_SUPPORTED);
continue;
}
if (cmd.ins != GET_EXTENDED_PUBKEY && cmd.ins != GET_WALLET_ADDRESS &&
@@ -158,7 +158,7 @@ void app_main() {
PRINTF(
"Only GET_EXTENDED_PUBKEY, GET_WALLET_ADDRESS, SIGN_PSBT and "
"GET_MASTER_FINGERPRINT can be called during swap\n");
- io_send_sw(SW_INS_NOT_SUPPORTED);
+ ioe_send_sw(SW_INS_NOT_SUPPORTED);
continue;
}
}
diff --git a/src/ui/display.c b/src/ui/display.c
index 9feb7f5..601ef67 100644
--- a/src/ui/display.c
+++ b/src/ui/display.c
@@ -75,7 +75,7 @@ static bool io_ui_process(dispatcher_context_t *context) {
g_ux_flow_ended = false;
// We are not waiting for the client's input, nor we are doing computations on the device
- io_clear_processing_timeout();
+ ioe_clear_processing_timeout();
#ifdef REVAMPED_IO
do {
@@ -91,7 +91,7 @@ static bool io_ui_process(dispatcher_context_t *context) {
#endif // !REVAMPED_IO
// We're back at work, we want to show the "Processing..." screen when appropriate
- io_start_processing_timeout();
+ ioe_start_processing_timeout();
return g_ux_flow_response;
}
Why this scored 23/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.