fix: guard lock parsers against short payloads#499
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests.
🚀 New features to boost your workflow:
|
|
|
Rebase with requested adjustmentsBranch StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
37bf281 to
e409bb2
Compare
| if len(data) < min_len: | ||
| _LOGGER.debug( | ||
| "lock data too short for %s: got %d bytes, need %d", | ||
| model, | ||
| len(data), | ||
| min_len, | ||
| ) | ||
| return {} |
There was a problem hiding this comment.
This isn't great that it silently returns nothing in production. This should be rare, likely a bug in the firmware if it happens. probably error level log.
There was a problem hiding this comment.
Agreed — this matches the same concern raised on the device-level audit (PR #500). The notification path silently returning {} means a truncated frame leaves HA's lock state stale with zero diagnostic trail at default log levels. Will bump to _LOGGER.error here. Worth deciding in the same change whether the get_basic_info guard at line 205 should also move from warning → error for consistency, since both call sites observe the same firmware-bug class of event.
Rebase with requested adjustmentsBranch Changes applied
StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
The _lock_unlock battery/firmware refresh logged a too-short basic_data reply at warning level while get_basic_info() logged the identical firmware-bug event class at error. Per bdraco's review on sblibs#499, this class of event should surface at error so a truncated frame leaves a diagnostic trail at default log levels. Also format as hex to match the sibling call site.
|
Resolved the open question from the earlier reply: the |
|
|
The _lock_unlock battery/firmware refresh logged a too-short basic_data reply at warning level while get_basic_info() logged the identical firmware-bug event class at error. Per bdraco's review on sblibs#499, this class of event should surface at error so a truncated frame leaves a diagnostic trail at default log levels. Also format as hex to match the sibling call site.
Simple rebaseBranch StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
d808165 to
6edf4d1
Compare
There was a problem hiding this comment.
Pull request overview
This PR hardens the Lock device parsing path against truncated/short payloads to prevent IndexError crashes (especially from notification decrypts and get_basic_info), and adds tests to ensure short inputs degrade gracefully.
Changes:
- Add per-lock-model minimum-length guards in
SwitchbotLock._parse_lock_dataand return{}for undersized payloads. - Tighten
SwitchbotLock.get_basic_infoto returnNonewhenbasic_datais shorter than 3 bytes. - Add tests covering short payloads across lock models, notification path with empty decrypted payload, and short
basic_data.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
switchbot/devices/lock.py |
Adds per-model minimum-length guard to _parse_lock_data and strengthens get_basic_info handling for short basic_data. |
tests/test_lock.py |
Adds regression tests to ensure short lock payloads/basic info do not raise and return safe values. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| self._update_parsed_data(self._parse_basic_data(basic_data)) | ||
| else: | ||
| _LOGGER.warning("Invalid basic data received: %s", basic_data) | ||
| _LOGGER.error("Invalid basic data received: %s", basic_data.hex()) |
| "basic_data: %s, address: %s", basic_data.hex(), self._device.address | ||
| ) | ||
| if len(basic_data) < 3: | ||
| _LOGGER.error("Invalid basic data received: %s", basic_data.hex()) |
| _LOGGER.error( | ||
| "lock data too short for %s: got %d bytes, need %d", | ||
| model, | ||
| len(data), | ||
| min_len, | ||
| ) |
Rebase with requested adjustmentsBranch Changes applied
StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
The _lock_unlock battery/firmware refresh logged a too-short basic_data reply at warning level while get_basic_info() logged the identical firmware-bug event class at error. Per bdraco's review on sblibs#499, this class of event should surface at error so a truncated frame leaves a diagnostic trail at default log levels. Also format as hex to match the sibling call site.
6edf4d1 to
3b51d59
Compare
PR Review — fix: guard lock parsers against short payloadsSolid, well-tested defensive hardening of the lock parse path. Merge-ready. Strengths:
🟢 Suggestions
1. Min-length table must stay in sync with the branch dispatch
|
`_parse_lock_data` indexes `data[1]` for all lock variants and `data[5]` in the LOCK_PRO/LOCK_ULTRA/LOCK_PRO_WIFI branch, but is reachable from `_update_lock_status` (notification path, decrypted payload of unknown length) and `get_basic_info` (`lock_raw_data[1:]`) without any length check. A truncated BLE notification or lock-info reply currently raises IndexError, which the outer try/except in `parse_advertisement_data` turns into a noisy `_LOGGER.exception` and a dropped advertisement (same class of bug as sblibs#285). Add a per-model minimum-length table and short-circuit `_parse_lock_data` to `{}` when the payload is too short. Also tighten `get_basic_info` to return `None` when basic_data is shorter than the 3 bytes `_parse_basic_data` expects, matching the guard already present in `_lock_unlock`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The _lock_unlock battery/firmware refresh logged a too-short basic_data reply at warning level while get_basic_info() logged the identical firmware-bug event class at error. Per bdraco's review on sblibs#499, this class of event should surface at error so a truncated frame leaves a diagnostic trail at default log levels. Also format as hex to match the sibling call site.
Rebase with requested adjustmentsBranch Changes applied
StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
3b51d59 to
25a3f3a
Compare
What
Add per-model length guards to
SwitchbotLock._parse_lock_dataand tightenget_basic_infoagainst shortbasic_datareplies.Why
_parse_lock_dataindexesdata[1]for every variant anddata[5]in the LOCK_PRO/LOCK_ULTRA/LOCK_PRO_WIFI branch, but is reachable from two paths without any length check:_update_lock_status(notification path) — decrypted payload of arbitrary length.get_basic_info— passeslock_raw_data[1:]straight through.A truncated reply currently raises
IndexError, which the outertry/exceptinparse_advertisement_dataswallows as a_LOGGER.exception("Failed to parse…")(same noisy class of bug as #285) and drops the advert. Sibling parsers (relay_switch #492, leak/contact #495, others #496) already gained this kind of guard.How
_LOCK_DATA_MIN_LEN_BY_MODELtable: 2 bytes for LOCK/LOCK_LITE/LOCK_VISION/LOCK_VISION_PRO, 6 bytes for LOCK_PRO/LOCK_ULTRA/LOCK_PRO_WIFI._parse_lock_datashort-circuits to{}when below the threshold. Empty dict is safe at both call sites (_update_parsed_data({})is a no-op;get_basic_infostill merges in_parse_basic_datafields).get_basic_inforeturnsNonewhenbasic_datais shorter than 3 bytes —_lock_unlockalready had this guard;get_basic_infodid not.Testing
get_basic_infowith a 2-byte basic_data →None.pytest→ 1228 passed.Quality Report
Changes: 5 files changed, 105 insertions(+), 7 deletions(-)
Code scan: clean
Tests: passed (1228 passed)
Branch hygiene: clean
Generated by Kōan