Skip to content

Seed-backup screens clip words: 1.63% of 24-word seeds show a truncated or entirely missing word #519

Description

@BitHighlander

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 ?.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions