fix(core): reject malformed protobuf field keys
What changed, and why it matters
This commit fixes the way a Trezor hardware wallet's Rust code reads protobuf message field keys. Previously, if reading a field key failed, the decoder silently stopped and treated the message as complete. Now it explicitly checks whether data remains and reports an error if a malformed or truncated field key is encountered. The change prevents an attacker from sneaking invalid or truncated protobuf data past the parser, which could lead to messages being accepted even though they contain garbage or hidden fields.
Treat this as a security hardening fix and include it in release notes. Review whether the old behavior could have allowed crafted messages to bypass authorization checks by truncating fields or hiding unexpected tags. Consider fuzzing the protobuf decoder with malformed keys and boundary-length uvarints.
Security signals we found
silent acceptance of malformed/truncated protobuf input
error-handling change from swallowing failures to propagating them
input-validation hardening for wire message parsing
potential bypass of expected message structure enforcement
Evidence from the diff
In core/embed/rust/src/protobuf/decode.rs, the message decoder loop changed from while let Ok(field_key) = stream.read_uvarint() to while stream.remaining() > 0 { let field_key = stream.read_uvarint()?; ... }. The old behavior swallowed malformed uvarint keys (e.g., overlong or truncated) by exiting the loop and returning Ok, effectively accepting a partially parsed message. The new behavior propagates read errors as decoding failures. A Python unit test was added to verify that overlong and truncated field keys raise OverflowError and EOFError respectively.
Changed components
core/embed/rust/src/protobuf/decode.rsTrezor Core protobuf wire decodermessage parsing path for host-to-device protobuf messagesInspect captured patch +9 / −3
### core/embed/rust/src/protobuf/decode.rs
@@ -103,9 +103,9 @@ impl Decoder {
depth: u8,
map: &mut Map,
) -> Result<(), Error> {
- // Loop, trying to read the field key that contains the tag and primitive value
- // type. If we fail to read the key, we are at the end of the stream.
- while let Ok(field_key) = stream.read_uvarint() {
+ // Decode field keys until the stream is exhausted
+ while stream.remaining() > 0 {
+ let field_key = stream.read_uvarint()?;
let field_tag = u8::try_from(field_key >> 3)?;
let prim_type = u8::try_from(field_key & 7)?;
### core/tests/test_trezor.protobuf.py
@@ -74,6 +74,12 @@ def test_load_uvarint(self):
self.assertEqual(load_uvarint(b"\xff\x01"), 0xFF)
self.assertEqual(load_uvarint(b"\xc0\xc4\x07"), 123456)
+ def test_reject_malformed_field_key(self):
+ with self.assertRaises(OverflowError):
+ load_message(ApplySettings, b"\x80" * 9 + b"\x02")
+ with self.assertRaises(EOFError):
+ load_message(ApplySettings, b"\x80")
+
def test_validate_enum(self):
# ok message:
msg = Failure(code=7)Why this scored 61/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.