Skip to content

fix: make the public headers self-contained, define two outputs, pin the third-party actions - #146

Merged
aido merged 3 commits into
aido:bip85from
buzzromain:fix/header-and-output-hygiene-bip85
Aug 4, 2026
Merged

aido merged 3 commits into
aido:bip85from
buzzromain:fix/header-and-output-hygiene-bip85

Conversation

@buzzromain

Copy link
Copy Markdown

Three small things, each one an output or a dependency that was left to whatever happened to be around it.

The public headers of src/common did not stand on their own

common_bip39.h, common_sskr.h and common.h use uint8_t, size_t and bool without including <stdint.h>, <stddef.h> or <stdbool.h>; sss/sss.h uses size_t without <stddef.h>. They compile anywhere their callers pull those in first, and nowhere else. It showed twice recently: the bool added to sskr.h and sss/sss.h for the randomness callback did not compile, and neither did a file whose first include was common_bip39.h.

Nothing already in the suite could hold this — every file that includes these headers also includes os.h, cx.h or the standard headers ahead of them, so the property stays green while unmet.

tests/unit/tests/header_self_contained.c is one translation unit per header: the same source compiled once for each with a different HEADER_UNDER_TEST. Compiling is the whole test.

Two earlier shapes of that check did not work, which is why it looks like this:

  • one file including every header in turn passes with the dependency unmet, because the second header is satisfied by whatever the first one included — stripping the includes back out of common_sskr.h left it compiling;
  • putting cx_errors.h in front of all of them has the same effect from another direction, since it includes <stdint.h> itself.

So cx_errors.h now precedes only common.h, which needs cx_err_t. That makes common.h the one header this cannot falsify: cx_errors.h brings <stdint.h> and, through ledger_assert.h, <stdbool.h>, so deleting its own includes still compiles. They are kept because they state what it uses, and the file says plainly that no test holds them. The other ten were each checked by deleting their includes and watching the build fail.

Two outputs left undefined

bolos_ux_bip39_to_sskr_convert() writes *share_count only inside the branch that runs when the mnemonic decodes. A mnemonic the decoder refuses skips the whole body and returns 1, leaving the caller's count at whatever it held — and both callers keep it in a struct that outlives the call, where it is the number of share screens the review then pages through. Not reachable today, since the phrase is checked before this is called, but it is the one output of this function that was undefined on a path out of it.

sss_create_digest() copies four bytes of an HMAC into the caller's buffer and left the other twenty-eight on the stack. They are not the secret — HMAC does not run backwards — but every other local in that file is erased, including the digest and the coordinate arrays a few lines below.

Third-party actions pinned

dawidd6/action-download-artifact and ncipollo/release-action were on major version tags, which are mutable, and both run inside the release job — the one holding contents: write that downloads a build artefact and publishes it. actions/checkout is pinned alongside them, at the two versions the workflows already used. Each SHA carries its tag as a comment so the version stays readable and Dependabot can still offer updates.

The reusable workflows under LedgerHQ/ledger-app-workflows stay on @v1: that is Ledger's convention for their applications, and pinning them here would freeze this repository against fixes made on their side.

Verification

  • six declared targets build, zero application-source warnings on each
  • clang-format clean over src/, actionlint reports nothing new
  • unit suite 78/78, functional suite unchanged on every device
  • ten of the eleven headers falsified individually; the eleventh is documented above

common_bip39.h, common_sskr.h and common.h use uint8_t, size_t and bool
without including <stdint.h>, <stddef.h> or <stdbool.h>; sss/sss.h uses size_t
without <stddef.h>. They compile anywhere their callers pull those in first,
and nowhere else. It showed twice: the bool added to sskr.h and sss/sss.h for
the randomness callback did not compile, and neither did a file whose first
include was common_bip39.h.

Nothing already in the suite could hold this. Every file that includes these
headers also includes os.h, cx.h or the standard headers ahead of them, so the
property stays green while unmet.

tests/unit/tests/header_self_contained.c is one translation unit per header,
the same source compiled once for each with a different HEADER_UNDER_TEST.
Compiling is the whole test; the executable only depends on the objects.

Two earlier shapes of that check did not work, which is why it looks like
this:

  - one file including every header in turn passes with the dependency unmet,
    because the second header is satisfied by whatever the first one included.
    Stripping the includes back out of common_sskr.h left it compiling;

  - putting cx_errors.h in front of all of them has the same effect from a
    different direction, since it includes <stdint.h> itself. With that in
    place, stripping the includes out of any header still compiled.

So cx_errors.h now precedes only common.h, which needs cx_err_t for
compare_recovery_phrase_finish(). That makes common.h the one header this
cannot falsify: cx_errors.h brings <stdint.h> and, through ledger_assert.h,
<stdbool.h>, so deleting its own includes still compiles. They are kept
because they state what it uses, and the file says that no test holds them.

The other ten headers were each checked by deleting their includes and
watching the build fail.
bolos_ux_bip39_to_sskr_convert() writes *share_count only inside the branch
that runs when the mnemonic decodes. A mnemonic the decoder refuses skips the
whole body and returns 1, leaving the caller's count at whatever it held --
and both callers keep it in a struct that outlives the call, where it is the
number of share screens the review then pages through. Not reachable today,
because the phrase is checked before this is called, but it is the one output
of this function that was undefined on a path out of it. Set at the top, like
sskr_generate_shards() already sets *shard_len.

sss_create_digest() copies four bytes of an HMAC into the caller's buffer and
left the other twenty-eight on the stack. They are not the secret -- HMAC does
not run backwards -- but every other local in that file is erased, including
the digest and the coordinate arrays a few lines below, and this one had no
reason to be the exception.
dawidd6/action-download-artifact and ncipollo/release-action were on major
version tags, which are mutable, and both run inside the release job -- the
one that holds contents: write, downloads a build artifact and publishes it.
Moving either tag changes what runs with write access to this repository.
actions/checkout is pinned alongside them for the same reason, at the two
versions the workflows already used.

Each SHA is the commit the tag pointed at when this was written, with the tag
kept beside it as a comment so the next reader can tell which version it is
and Dependabot can still offer an update.

The reusable workflows under LedgerHQ/ledger-app-workflows stay on @v1. That
is Ledger's own convention for their applications, and pinning them here would
freeze this repository against fixes made on their side; it is an accepted
risk rather than an oversight.
@aido
aido merged commit b2cb2cb into aido:bip85 Aug 4, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants