Skip to content

fix(HardwareTimer): guard HERTZ_FORMAT get* against register==0#3020

Open
94xhn wants to merge 1 commit into
stm32duino:mainfrom
94xhn:fix/hardwaretimer-hertz-format-divzero
Open

fix(HardwareTimer): guard HERTZ_FORMAT get* against register==0#3020
94xhn wants to merge 1 commit into
stm32duino:mainfrom
94xhn:fix/hardwaretimer-hertz-format-divzero

Conversation

@94xhn

@94xhn 94xhn commented Jul 11, 2026

Copy link
Copy Markdown

Pull Request template

Summary

HardwareTimer::getCount(HERTZ_FORMAT) and HardwareTimer::getCaptureCompare(channel, HERTZ_COMPARE_FORMAT) both convert a hardware register value to a frequency by dividing getTimerClkFreq() by that register value. Neither has a zero-guard, and both registers legitimately read 0 during normal operation (not because of a caller mistake), so both are reachable divide-by-zero bugs.

This PR fixes/implements the following bugs/features

  • Bug: getCount(HERTZ_FORMAT) divides by the live CNT (counter) register, which is 0 right at/after every timer reset or overflow, purely as a side effect of the timer running.
  • Bug: getCaptureCompare(channel, HERTZ_COMPARE_FORMAT) divides by the live CCR (capture/compare) register, which is 0 on reset before the first setCaptureCompare() call, and in Input Capture mode whenever no edge has been captured yet or the last captured edge landed exactly at CNT == 0.

Motivation

In libraries/SrcWrapper/src/HardwareTimer.cpp:

uint32_t HardwareTimer::getCount(TimerFormat_t format)
{
  uint32_t CNT_RegisterValue = LL_TIM_GetCounter(_timerObj.instance);
  ...
    case HERTZ_FORMAT:
      return_value = (uint32_t)(getTimerClkFreq() / (CNT_RegisterValue  * Prescalerfactor));
      break;
uint32_t HardwareTimer::getCaptureCompare(uint32_t channel,  TimerCompareFormat_t format)
{
  ...
    case HERTZ_COMPARE_FORMAT:
      return_value = (uint32_t)(getTimerClkFreq() / (CCR_RegisterValue  * Prescalerfactor));
      break;

Neither CNT_RegisterValue nor CCR_RegisterValue is validated to be non-zero before the division, and 0 is not an edge case a caller passes in — it's a value the hardware itself produces during ordinary operation:

  • CNT (the running counter) counts 0, 1, 2, ..., ARR, 0, 1, 2, ... — it reads 0 once every single timer period. Note getOverflow()/setOverflow() already handle the analogous "register can be one less than the real period" situation via the documented ARR + 1 offset; getCount() has no equivalent protection.
  • CCR (capture/compare register) is 0 immediately after begin()/setMode() before any setCaptureCompare() call, and in Input Capture mode it holds the last captured counter value, which is 0 before the first edge is captured or whenever an edge happens to land exactly when CNT == 0.

Integer division by zero is undefined behavior in C/C++. On this project's usual desktop/CI toolchains it reliably crashes; on Cortex-M targets, UDIV/SDIV by 0 is architecturally defined to return 0 when DIV_0_TRP is not set in SCB->CCR (the default, and not touched anywhere in this codebase), so on-target the bug doesn't crash — it silently returns 0 Hz/no fault, which is arguably worse since it looks like a valid (if surprising) reading instead of an obvious error.

This is the same general bug shape as the getCaptureCompare() PERCENT_COMPARE_FORMAT/RESOLUTION_xB_COMPARE_FORMAT divide-by-zero I reported separately in #3019 (dividing by a hardware register that can be 0), found while re-reading the same conversion functions for other HERTZ_FORMAT/HERTZ_COMPARE_FORMAT siblings — this is an independent code path (different case branches, different registers) so I've kept it as its own PR.

Fix

Add an explicit == 0 guard to both HERTZ_FORMAT branches and return 0 (no meaningful frequency available) instead of dividing, following the same "do not underflow, default to a safe value" style setOverflow() already uses a few lines above in this file. No public API/signature changes.

Validation

  • Ensure CI build is passed.

  • This is pure integer arithmetic on values that are otherwise plain uint32_ts once read through the existing LL_TIM_* abstraction, so I extracted both formulas verbatim into a standalone host-side C program and ran it on a desktop toolchain (independent of any STM32 board):

    === getCount(HERTZ_FORMAT): CNT_RegisterValue sweep, Prescalerfactor=1, clk=84MHz ===
    CNT_RegisterValue  got (before fix)       got (after fix)
    0                  *** DIV BY ZERO ***    0                       <- BEFORE FIX: integer division by zero
    1                  84000000               84000000
    2                  42000000               42000000
    100                840000                 840000
    65535              1281                   1281
    
    === getCaptureCompare(HERTZ_COMPARE_FORMAT): CCR_RegisterValue sweep, Prescalerfactor=1, clk=84MHz ===
    CCR_RegisterValue  got (before fix)       got (after fix)
    0                  *** DIV BY ZERO ***    0                       <- BEFORE FIX: integer division by zero
    1                  84000000               84000000
    2                  42000000               42000000
    100                840000                 840000
    65535              1281                   1281
    

    Running the buggy formula directly (getCount_HERTZ_BUGGY(0, 1)) crashes the host process immediately (process exits before the next printf, no SIGFPE handler installed) — confirming the divide-by-zero is real, not just theoretical, for both formulas. The fixed formula returns a defined 0 in the same case and is unchanged for every non-zero register value tested.

  • I do not own STM32 hardware set up for this project's test rig, so I have not run this on-target; the change only adds a zero-guard around a pure integer division that is otherwise structurally identical to the surrounding, already-shipped code, so on-target risk is limited to the two == 0 branches added.

Code formatting

  • Ensure AStyle check is passed thanks CI

Closing issues

N/A – found while reading the timer tick/frequency conversion code, not tied to an existing issue.


Disclosure: I used an AI coding assistant (Claude) to help read through the codebase, extract the reproduction, and draft this PR. All findings were verified by me by compiling and running the standalone reproduction shown above.

getCount(HERTZ_FORMAT) and getCaptureCompare(..., HERTZ_COMPARE_FORMAT)
both compute getTimerClkFreq() / (register * Prescalerfactor), dividing
directly by the live CNT (counter) or CCR (capture/compare) hardware
register with no zero-guard.

Both registers legitimately read 0 with no caller error involved:
- CNT is 0 right at/after every timer reset or overflow, simply from
  the timer running (getOverflow()/setOverflow() already special-case
  this exact situation via the ARR+1 "period" offset; getCount() and
  getCaptureCompare() do not).
- CCR is 0 on reset before the first setCaptureCompare() call, and in
  Input Capture mode holds whatever was last captured, which is 0
  before any edge has been captured or if an edge lands exactly at
  CNT == 0.

Hitting either case triggers an integer divide by zero: undefined
behavior in C/C++, verified to crash (SIGFPE-equivalent) on a desktop
toolchain. On Cortex-M without DIV_0_TRP configured (the default),
UDIV silently returns 0, so on-target this instead silently reports
"0 Hz" with no indication anything went wrong.

Add an explicit zero-check to both HERTZ_FORMAT branches, returning 0
(no meaningful frequency available) instead of dividing, matching the
existing "do not underflow" zero-guard style already used by
setOverflow() in this same file. No public API/signature changes.

Signed-off-by: 94xhn <87560781+94xhn@users.noreply.github.com>
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.

1 participant