fmt: fix and prevent future use of unsupported format spec "zu"
What changed, and why it matters
This commit fixes the use of the "%zu" printf format specifier, which is not supported by the ESP32 toolchain used in this project. The unsupported format could cause log messages and user-facing strings to display incorrect or garbage values. The commit also adds a CI check to prevent future use of "%zu". There is no direct evidence in the commit that this caused a security vulnerability such as information disclosure or memory corruption, but it is a code-quality fix that removes undefined/unsupported formatting behavior.
Treat as a low-risk maintainability/portability fix. Review whether any %zu usage elsewhere in the codebase (outside main/ and libjade/) remains unsupported. No urgent security response is indicated by the diff alone.
Security signals we found
Use of unsupported printf format specifier %zu on embedded target
Potential for malformed log/UI output due to undefined format behavior
Addition of CI guard to prevent recurrence
No direct evidence of memory corruption, buffer overflow, or information leak in diff
Evidence from the diff
The commit replaces all uses of the C99 %zu format specifier (for size_t) with %u and explicit unsigned int casts, because the ESP-IDF/newlib printf implementation used by Blockstream Jade does not support %zu. It also adds a git grep check in gitlab/test.yml to fail CI if %zu appears in main/ or libjade/. The affected code is mostly logging (JADE_LOGD/LOGI/LOGW/LOGE) and snprintf titles in UI flows. The change is defensive and improves portability/reliability of formatted output, but the diff itself does not demonstrate an exploitable bug.
Changed components
libjade/selfcheck/cbor.cmain/otpauth.cmain/process/mnemonic.cmain/process/sign_utils.cgitlab/test.yml (CI format check)Inspect captured patch +21 / −14
### gitlab/test.yml
@@ -17,6 +17,7 @@ test_format:
dependencies: []
script:
- pushd /opt/esp/idf && . ./export.sh && popd
+ - if git grep -I '%zu' main/ libjade/; then echo 'unsupported format spec "zu"'; echo exit 1; fi
- ./format.sh
- idf.py reconfigure
- git diff --exit-code
### libjade/selfcheck/cbor.c
@@ -50,7 +50,7 @@ static bool test_parser_recursion_limit(void)
enum { MIN_DEPTH = CBOR_PARSER_MAX_RECURSIONS - 1, MAX_DEPTH = CBOR_PARSER_MAX_RECURSIONS + 1 };
uint8_t cbor[MAX_DEPTH + 1];
- for (size_t depth = MIN_DEPTH; depth <= MAX_DEPTH; ++depth) {
+ for (unsigned int depth = MIN_DEPTH; depth <= MAX_DEPTH; ++depth) {
// A single-item array at each level, terminated by a null leaf:
// [[[...[null]...]]]
memset(cbor, CborArrayType | 1, depth); // one element arrays
@@ -65,9 +65,9 @@ static bool test_parser_recursion_limit(void)
const CborError expected_err = depth <= CBOR_PARSER_MAX_RECURSIONS ? CborNoError : CborErrorNestingTooDeep;
const CborError err = cbor_value_validate_basic(&root);
const char* err_text = err == CborNoError ? "ok" : cbor_error_string(err);
- JADE_LOGD("Validating CBOR with nesting depth %zu: %s", depth, err_text);
+ JADE_LOGD("Validating CBOR with nesting depth %u: %s", depth, err_text);
if (err != expected_err) {
- JADE_LOGE("Failed CBOR validation case with nesting depth %zu", depth);
+ JADE_LOGE("Failed CBOR validation case with nesting depth %u", depth);
return false;
}
}
### main/otpauth.c
@@ -411,7 +411,7 @@ static bool decode_otp_parameters_fn(pb_istream_t* stream, const pb_field_t* fie
}
}
if (i == ctx->uris_out_len) {
- JADE_LOGE("Too many OTP records in migration data, max supported is %zu", ctx->uris_out_len);
+ JADE_LOGE("Too many OTP records in migration data, max supported is %u", (unsigned int)ctx->uris_out_len);
return false;
}
### main/process/mnemonic.c
@@ -324,7 +324,7 @@ static bool display_confirm_mnemonic(const size_t nwords, char* mnemonic, const
gui_view_node_t* textbox = NULL;
gui_activity_t* const confirm_act
= make_confirm_mnemonic_word_activity(&textbox, i, offset_word_to_confirm, mnemonic, word_offs, nwords);
- JADE_LOGD("selected = %zu", selected);
+ JADE_LOGD("selected = %u", (unsigned int)selected);
// Pick some other words from the mnemonic as options, but avoid
// the words currently displayed on screen (neighbouring words).
@@ -435,7 +435,7 @@ static void enable_relevant_chars(const bool is_mnemonic, const char* word, cons
JADE_ASSERT(act && backspace && enter && btns && btns_len == 26);
JADE_ASSERT(backspace->activity == act && enter->activity == act);
- JADE_LOGD("word = %s, word_len = %zu", word, word_len);
+ JADE_LOGD("word = %s, word_len = %u", word, (unsigned int)word_len);
// Enable enter if a) not entering a mnemonic, and b) not part-way through entering a word
// Enable backspace in all cases.
@@ -711,7 +711,8 @@ static wordlist_word_result_t select_wordlist_word(const bool is_mnemonic, const
if (possible_words <= NUM_WORDS_SELECT) {
// 'Small' number of words - allow user to select from these words
char choose_word_title[16]; // sufficient
- const int ret = snprintf(choose_word_title, sizeof(choose_word_title), "Select word %zu", word_index + 1);
+ const int ret = snprintf(
+ choose_word_title, sizeof(choose_word_title), "Select word %u", (unsigned int)(word_index + 1));
JADE_ASSERT(ret > 0 && ret < sizeof(choose_word_title));
gui_update_text(ui->label, choose_word_title);
@@ -853,7 +854,8 @@ static wordlist_word_result_t select_resolved_word_number(const size_t word_inde
JADE_ASSERT(word && choose_word_activity && label && text_selection);
char confirm_word_title[16]; // sufficient
- const int ret = snprintf(confirm_word_title, sizeof(confirm_word_title), "Confirm word %zu", word_index + 1);
+ const int ret
+ = snprintf(confirm_word_title, sizeof(confirm_word_title), "Confirm word %u", (unsigned int)(word_index + 1));
JADE_ASSERT(ret > 0 && ret < sizeof(confirm_word_title));
gui_update_text(label, confirm_word_title);
@@ -939,7 +941,8 @@ static size_t get_wordlist_words(
// Reset default title for next word when entering mnemonic phrase
if (is_mnemonic) {
char enter_word_title[16];
- const int ret = snprintf(enter_word_title, sizeof(enter_word_title), "Insert word %zu", word_index + 1);
+ const int ret = snprintf(
+ enter_word_title, sizeof(enter_word_title), "Insert word %u", (unsigned int)(word_index + 1));
JADE_ASSERT(ret > 0 && ret < sizeof(enter_word_title));
gui_update_text(ui.titletext, enter_word_title);
}
@@ -1039,7 +1042,8 @@ static size_t get_word_number_words(
JADE_ASSERT(!wordlist_words[word_index]);
char title[24];
- const int ret = snprintf(title, sizeof(title), "Word %zu/%zu", word_index + 1, nwords);
+ const int ret
+ = snprintf(title, sizeof(title), "Word %u/%u", (unsigned int)(word_index + 1), (unsigned int)nwords);
JADE_ASSERT(ret > 0 && ret < sizeof(title));
const char* word = NULL;
@@ -1053,7 +1057,8 @@ static size_t get_word_number_words(
= calculate_valid_final_words(wordlist_words, word_index, nwords, final_words);
char enter_word_title[16];
- const int ret = snprintf(enter_word_title, sizeof(enter_word_title), "Insert word %zu", word_index + 1);
+ const int ret = snprintf(
+ enter_word_title, sizeof(enter_word_title), "Insert word %u", (unsigned int)(word_index + 1));
JADE_ASSERT(ret > 0 && ret < sizeof(enter_word_title));
gui_update_text(calc_ui.titletext, enter_word_title);
@@ -1391,7 +1396,7 @@ static bool mnemonic_qr(char* mnemonic, const size_t mnemonic_len)
}
if (qr_data.len >= mnemonic_len) {
- JADE_LOGW("String data from qr unexpectedly long - ignored: %zu", qr_data.len);
+ JADE_LOGW("String data from qr unexpectedly long - ignored: %u", (unsigned int)qr_data.len);
goto cleanup;
}
@@ -1458,7 +1463,7 @@ void get_passphrase(char* passphrase, const size_t passphrase_len)
// Passphrase made up only of bip39 wordlist words
const size_t nwords
= get_wordlist_words(WORDLIST_PASSPHRASE, WORDLIST_PASSPHRASE_MAX_WORDS, passphrase, passphrase_len);
- JADE_LOGI("%zu wordlist words used for passphrase", nwords);
+ JADE_LOGI("%u wordlist words used for passphrase", (unsigned int)nwords);
} else {
// Free-text passphrase
get_freetext_passphrase(passphrase, passphrase_len);
### main/process/sign_utils.c
@@ -137,7 +137,8 @@ static bool rpc_get_asset_summary(jade_process_t* process, const char* field, co
}
if (num_array_items > max_items) {
- JADE_LOGE("Too many asset summary records in message: %zu (max %zu)", num_array_items, max_items);
+ JADE_LOGE("Too many asset summary records in message: %u (max %u)", (unsigned int)num_array_items,
+ (unsigned int)max_items);
return false;
}
Why this scored 20/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.