bbqr: reject part sizes that would return unwritten bytes - #718
Open
drk1wi wants to merge 1 commit into
Open
Conversation
save_packet() sized the result from the declared geometry but write_pkt() only wrote len(data) bytes per part, so a series of short parts made final_size count bytes that were never written. For BBQrPsramStorage those come back out of PSRAM, which still holds whatever the previous scan left there. Enforce what collect() already documents: every part is blksize, except the final one.
Contributor
|
Looking good, add to |
scgbckbone
reviewed
Aug 13, 2026
scgbckbone
left a comment
Collaborator
There was a problem hiding this comment.
Thanks for the report and the original patch — the diagnosis and test scenarios are yours. #726 is a hardened successor; strictly better in three ways:
- No new DoS: the bare
asserts escape into flows that only catchQRDecodeExplained(seed entry, XPRV import, TAPSIGNER) and land indie_with_debug— one crafted QR series = fatal screen + logout. #726 raises aQRDecodeExplainedsubclass and recovers incollect()instead. - No conflict with repeated-frame handling: completeness is checked in
finalize(), not_finalize()— the frag flush stays callable mid-scan on incomplete series. - Also fixes the stale-frags bug your body disclosed and deferred (abandoned-series fragments flushed into the next decode), with a regression test.
Same geometry enforcement, same credit — you're thanked in the ChangeLog. Curious what you think.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
BBQrStorage.save_packet()(shared/bbqr.py:271-282) sizes the decoded result fromthe geometry the QR series declares:
but
write_pkt()only ever writeslen(data)bytes. Nothing requires a non-finalpart to be
blksizelong, norrunt_sizeto be<= blksize, andblksizeiswhatever the first part to arrive happens to carry (
:232-233). Any shortfall iscounted into
final_sizeand never written.BBQrPsramStorage.get_buffer()(:437-440) then returnsPSRAM.read_at(0, self.final_size), andpsram.py:16-18has no initialisationcheck. PSRAM[0..2MB) is the PSBT staging area and is not wiped at boot, so the
uncounted bytes are whatever the previous scan or export left there. A 16-part series
carrying 43 attacker bytes came back as 239, and
decode_qr_resultclassified theresult as text and routed it onward.
Fix: enforce in
save_packet()whatcollect()(:228) already documents as arequired assumption, that every part is
blksizeexcept the final one which may beshort, and refuse in
_finalize()to complete a series with a part missing.Two things in the same file are deliberately left alone.
BBQrStorage.get_buffer()returns the whole buffer rather than
buf[:final_size], which nothing in tree reachesbecause
scanner.py:251uses the PSRAM subclass and that overridesget_buffer()with a bounded read.
BBQrPsramStoragedoes not overridereset(), so afragsentryfrom an abandoned series survives a header change and gets flushed into the next
series by
_finalize(). That one is reachable and is a separate defect.The new checks raise
AssertionErrorout ofcollect().ux_q1.py:666,actions.py:1451andtapsigner.py:54do not catch it, where the neighbouring idiomfor this kind of rejection is
raise QRDecodeExplained(bbqr.py:198).Reported privately to security@coinkite.com and opened here as agreed.