Skip to content

Infrared: initialize timings_cnt on decoder alloc and fix its bounds check - #4429

Merged
turbospok merged 1 commit into
flipperdevices:devfrom
munzzyy:fix/infrared-timings-cnt-init
Jul 27, 2026
Merged

Infrared: initialize timings_cnt on decoder alloc and fix its bounds check#4429
turbospok merged 1 commit into
flipperdevices:devfrom
munzzyy:fix/infrared-timings-cnt-init

Conversation

@munzzyy

@munzzyy munzzyy commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

What's new

Two small issues around timings_cnt in the common IR decoder.

infrared_common_decoder_alloc() sets protocol and level but leaves timings_cnt uninitialized. Every caller goes through infrared_alloc_decoder(), which allocs then immediately resets. For RC5 (the one protocol with preamble_mark == 0), the reset path reads timings_cnt and passes it to consume_samples() as a length, and the reset function only zeroes timings_cnt a couple of lines later, after reset_state() has already used it. If the uninitialized value happens to be nonzero, consume_samples() shifts the 6-element timings[] array using that length, reading and writing past the end.

Separately, the defensive check in infrared_common_decode() is furi_check(decoder->timings_cnt <= sizeof(decoder->timings)). timings_cnt is an element count but sizeof(decoder->timings) is 24 (six uint32_t), so the check doesn't trip until 24 instead of 6. Should be COUNT_OF, which the file already uses elsewhere.

Both come down to timings_cnt not being trustworthy: initialize it at alloc so the first reset sees 0, and compare against the element count in the bounds check.

Verification

Traced the alloc → reset call path and confirmed timings_cnt is read in reset_state() before reset() zeroes it, and that RC5's preamble_mark is 0 so it takes that branch. Compiled infrared_common_decoder.c standalone against a spec mirroring RC5 and called it the way infrared_alloc_decoder() does; before the change AddressSanitizer catches an out-of-bounds access in consume_samples() from the uninitialized count, and it's clean after. clang-format is clean, and COUNT_OF is already included/used in this file.

Being upfront on severity: how often the uninitialized read actually bites depends on what byte the heap left in that slot, which I can't observe without flashing. On a reused/dirty heap block it's nonzero and fires; on a fresh-zeroed one it doesn't. The fix is correct either way, and the sizeof/COUNT_OF check is simply wrong regardless. On device this only touches allocation-time init and a bounds constant, so IR read/learn/send for NEC/RC5/RC6/Samsung/etc. should behave exactly as before.

Author checklist (Fill this out)

  • I've read the contribution guidelines and my PR follows them.
  • I own the code I'm submitting or have code owner's permission (or code license allows redistribution) to submit it.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.

AI usage disclosure (Fill this out):

  • Partially AI assisted (clarify below which code was AI assisted and briefly explain what it does).

I used an AI assistant to compile infrared_common_decoder.c standalone (outside this repo) with a synthetic RC5-like spec and confirm that timings_cnt is uninitialized after alloc and that ASan catches the resulting out-of-bounds access when reset runs the same way infrared_alloc_decoder() calls it. The two-line fix is hand-written.

…check

infrared_common_decoder_alloc() sets protocol and level but leaves
timings_cnt uninitialized. Every caller allocs then resets, and for RC5
(the one protocol with preamble_mark == 0) the reset path reads
timings_cnt and feeds it to consume_samples() as a length before the
reset function zeroes it a few lines later. With a nonzero garbage value
that shifts the 6-element timings[] array out of bounds, both read and
write.

Separately, the bounds check in infrared_common_decode() compares
timings_cnt (an element count) against sizeof(decoder->timings) (24
bytes for six uint32_t), so it doesn't fire until 24 instead of 6.
Should be COUNT_OF.

Both come down to timings_cnt not being trustworthy; init it at alloc and
use the element count in the check.

Signed-off-by: Cole Munz <colemunz@gmail.com>
@turbospok turbospok added Infrared Infrared app Bug Bugs, Bugfixes, all related to bugs Triage Issues under initial investigation AI-generated To mark changes made via AI tools labels Jul 27, 2026
@turbospok

Copy link
Copy Markdown
Collaborator

Hi @munzzyy
I see you posted 6 PRs in one day, that's a lot! Thanks!

I have two questions, just curious
1 - How exactly you were able to find all of those issues? They look legit and fixes look fine, I'm doing a review rn
2 - And why most of them were not tested on the hardware?
We will run unit tests and manual tests after, so that's not a big problem

@turbospok turbospok left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree on furi_check fix
However the statement about uninitialised timings_cnt is wrong

Flipper's malloc always returns zeroed memory. timings_cnt is 0 at alloc; the if(decoder->timings_cnt > 0) guard in reset_state never fires.

So setting it to zero at alloc is just a visual change, not affecting anything on hardware, that seems to be your testing issue

Ill keep it just for better readability

@turbospok turbospok removed the Triage Issues under initial investigation label Jul 27, 2026
@turbospok
turbospok merged commit 5dc7187 into flipperdevices:dev Jul 27, 2026
11 checks passed
@turbospok turbospok moved this to Testing in Flipper Zero Firmware Jul 27, 2026
@github-project-automation github-project-automation Bot moved this from Testing to Done in Flipper Zero Firmware Jul 27, 2026
@munzzyy

munzzyy commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

You're right, I checked after your comment. pvPortMalloc in furi/core/memmgr_heap.c ends with memset(pvReturn, 0, xToWipe), so timings_cnt is already 0 at alloc and that guard in reset_state was never the live path. My claim there was wrong. Thanks for catching it, and for keeping the init for readability.

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

Labels

AI-generated To mark changes made via AI tools Bug Bugs, Bugfixes, all related to bugs Infrared Infrared app

Projects

Development

Successfully merging this pull request may close these issues.

2 participants