Finding
Severity: high · Dimension: confirm-ui-truth · Location: lib/firmware/reset.c:425
ResetDevice with strength=256 produces the 24-word seed "tonight consider congress announce dance frog intact lyrics piece quality there recipe general stock hurry quarter rabbit earn fit urge melt giant cousin excuse". reset.c packs page 1 as "1.tonight 2.consider\n 3.congress 4.announce\n 5.dance" (calc_str_line at width 225 == 3, so the check passes). confirm_constant_power draws it via layout_constant_power_notification at x=132 with wrap width 225. draw_string_walk never wraps (x_offset never reaches 225) so it runs off the canvas: draw_char_impl rejects the glyph at absolute x>256, have_space goes false, and the walk stops. Replaying the real renderer with the real font tables: line 2 measures 125 px against 124 px available, so the screen shows "1.tonight 2.consider / 3.congress 4.announc" and the entire third line "5.dance" is never drawn. No ellipsis, no warning, no page indicator — the user writes down 23 words and a 7-letter fragment. Measured over 100,000 random BIP-39 seeds: 1,629/100,000 (1.63%) of 24-word seeds and 46/100,000 (0.05%) of 12-word seeds produce at least one clipped backup screen; of the clipped screens 1,036 render a word with characters missing (e.g. "24.announ" for "announce", "20.busines" for "business") and 600 omit a whole word. Sample line widths vs. the 124 px available: "3.congress 4.announce"=125, "19.business 20.business"=127, "23.mystery 24.announce"=132. Result: an unrecoverable seed backup. The identical defect is in the BIP-85 child-seed display at fsm_msg_bip85.h:74.
Evidence
reset.c:422-427 (page packing — the ONLY fit check, and it measures width 225):
snprintf(mnemonic_display, FORMATTED_MNEMONIC_BUF, "%s %s",
formatted_mnemonic[page_count], formatted_word);
if (calc_str_line(get_body_font(), mnemonic_display, BODY_WIDTH) > 3) {
page_count++;
layout.c:389-392 (where that body is actually drawn):
sp.x = 128 + LEFT_MARGIN;
sp.color = BODY_COLOR;
draw_string(canvas, body_font, str2, &sp, BODY_WIDTH,
font_height(body_font) + BODY_FONT_LINE_PADDING);
keepkey_display.c:303-309 (proves only x>=128 is real screen area in constant-power mode):
if (constant_power) {
for (int y = 0; y < 64; y++) {
for (int x = 0; x < 128; x++) {
canvas.buffer[y * 256 + x] = 255 - canvas.buffer[y * 256 + x + 128];
confirm_sm.c:508-510 (why nothing catches it later):
const bool render_incomplete =
(layout_notification_func == &layout_standard_notification) &&
!confirm_body_fits(request_body, body_width);
layout.h:55 `#define BODY_WIDTH 225` — but 256 - (128+4) = 124 px are actually available.
Suggested fix
The packing loop never checks row WIDTH at all — because every word pair is explicitly '\n'-terminated, calc_str_line at width 225 only ever counts newlines. Either (a) pass the true wrap width to both sides: measure with calc_str_line(get_body_font(), display, 124) and change layout_constant_power_notification's draw_string width argument from BODY_WIDTH to (KEEPKEY_DISPLAY_WIDTH - (128 + LEFT_MARGIN)); or (b) better, extend confirm_helper's render check to cover layout_constant_power_notification by making confirm_body_fits take the layout's real x-origin and width instead of hard-coding the standard-notification geometry, so a clipped seed page fails closed like every other clipped body. Fix fsm_msg_bip85.h:74 the same way.
Verification
(not independently verified)
Found by an adversarial audit of release/7.15 (628b09257). Each finding was independently re-checked by a separate reviewer instructed to refute it by default; this one survived at confidence ?.
Finding
Severity: high · Dimension: confirm-ui-truth · Location:
lib/firmware/reset.c:425ResetDevice with strength=256 produces the 24-word seed "tonight consider congress announce dance frog intact lyrics piece quality there recipe general stock hurry quarter rabbit earn fit urge melt giant cousin excuse". reset.c packs page 1 as "1.tonight 2.consider\n 3.congress 4.announce\n 5.dance" (calc_str_line at width 225 == 3, so the check passes). confirm_constant_power draws it via layout_constant_power_notification at x=132 with wrap width 225. draw_string_walk never wraps (x_offset never reaches 225) so it runs off the canvas: draw_char_impl rejects the glyph at absolute x>256, have_space goes false, and the walk stops. Replaying the real renderer with the real font tables: line 2 measures 125 px against 124 px available, so the screen shows "1.tonight 2.consider / 3.congress 4.announc" and the entire third line "5.dance" is never drawn. No ellipsis, no warning, no page indicator — the user writes down 23 words and a 7-letter fragment. Measured over 100,000 random BIP-39 seeds: 1,629/100,000 (1.63%) of 24-word seeds and 46/100,000 (0.05%) of 12-word seeds produce at least one clipped backup screen; of the clipped screens 1,036 render a word with characters missing (e.g. "24.announ" for "announce", "20.busines" for "business") and 600 omit a whole word. Sample line widths vs. the 124 px available: "3.congress 4.announce"=125, "19.business 20.business"=127, "23.mystery 24.announce"=132. Result: an unrecoverable seed backup. The identical defect is in the BIP-85 child-seed display at fsm_msg_bip85.h:74.
Evidence
Suggested fix
The packing loop never checks row WIDTH at all — because every word pair is explicitly '\n'-terminated, calc_str_line at width 225 only ever counts newlines. Either (a) pass the true wrap width to both sides: measure with calc_str_line(get_body_font(), display, 124) and change layout_constant_power_notification's draw_string width argument from BODY_WIDTH to (KEEPKEY_DISPLAY_WIDTH - (128 + LEFT_MARGIN)); or (b) better, extend confirm_helper's render check to cover layout_constant_power_notification by making confirm_body_fits take the layout's real x-origin and width instead of hard-coding the standard-notification geometry, so a clipped seed page fails closed like every other clipped body. Fix fsm_msg_bip85.h:74 the same way.
Verification
(not independently verified)
Found by an adversarial audit of release/7.15 (
628b09257). Each finding was independently re-checked by a separate reviewer instructed to refute it by default; this one survived at confidence?.