Skip to content

drivers/timers: avoid 32-bit overflow in arch_timer current_usec#18848

Open
maxikrie wants to merge 1 commit intoapache:masterfrom
maxikrie:master
Open

drivers/timers: avoid 32-bit overflow in arch_timer current_usec#18848
maxikrie wants to merge 1 commit intoapache:masterfrom
maxikrie:master

Conversation

@maxikrie
Copy link
Copy Markdown
Contributor

@maxikrie maxikrie commented May 4, 2026

Summary

current_usec() in drivers/timers/arch_timer.c computes elapsed
microseconds as:

return TICK2USEC(timebase) + (status.timeout - status.timeleft);

TICK2USEC(timebase) expands to (timebase) * USEC_PER_TICK. When
timebase is clock_t (uint32_t on 32-bit targets) and
USEC_PER_TICK is 10000, the multiplication overflows uint32_t at
tick 429,497 (~71 minutes at 100 Hz). The result wraps to near-zero.

Because clock_systime_ticks() calls up_timer_gettick()
current_usec() / USEC_PER_TICK when CONFIG_TIMER_ARCH=1, the
scheduler's tick counter suddenly reads ~0 instead of ~429,497. Any
watchdog armed just before the overflow (e.g. for usleep /
nanosleep) has an expiry tick that now appears to be far in the
future; wd_expiration() never fires it, and the sleeping task
blocks permanently.

Fix: cast timebase to uint64_t before the multiply so the
computation is done in 64-bit arithmetic.

Impact

  • New or modified features: No
  • User-facing changes required: No
  • Build process modifications: No
  • Architecture/board/driver implications: Affects all targets using
    CONFIG_TIMER_ARCH=1 with a 32-bit clock_t
  • Documentation updates: No
  • Security considerations: No
  • Backward/forward compatibility: No

Testing

I confirm that changes are verified on local setup and works as
intended.

Build environment:

  • OS: Linux
  • Target: ARM Cortex-M4 (nRF52840)
  • Compiler: arm-none-eabi-gcc

Target:

  • Architecture: arm
  • Board: nrf52840-dk
  • Config: sdc_nimble_bleprph

Observed before fix: With CONFIG_TIMER_ARCH=1 and a 100 Hz
system tick, usleep() hangs permanently after approximately 71
minutes of uptime. The board remains alive (BLE connectable) but the
sleeping task never wakes. Replacing usleep with a busy-loop never
fails, confirming the watchdog firing path is the issue.

After fix: Board runs the sdc_nimble_bleprph example
continuously past the 71-minute mark without any hang.

PR Verification Self-Check

  • Single functional change
  • All description fields completed
  • Follows NuttX coding standards
  • Not a work in progress
  • Ready for review and merge

@github-actions github-actions Bot added Area: Drivers Drivers issues Size: XS The size of the change in this PR is very small labels May 4, 2026
@linguini1
Copy link
Copy Markdown
Contributor

Could you please format according to the PR template and include some tests?

@maxikrie
Copy link
Copy Markdown
Contributor Author

maxikrie commented May 5, 2026

@acassis I am not sure why the build failed?
@linguini1 Please check again.

@linguini1
Copy link
Copy Markdown
Contributor

Could you please include the logs from your testing?

I think the building is failing due to an earlier issue. Please rebase onto master, and that should fix it

@maxikrie
Copy link
Copy Markdown
Contributor Author

maxikrie commented May 5, 2026

@linguini1 The root cause is

current_usec() overflowed near 71.6 min
-> up_timer_gettick() returned ticks near 0
-> clock_systime_ticks() jumped backwards
-> usleep watchdog was waiting for an absolute tick around 429500
-> current tick became 3
-> timeout would not expire until time caught up again

Here is a log for when it failed:
sdc/hci systick count=429490(+5) ticks=429490(+5) ctrl=00000007 reload=0009c3ff current=0004ef5b intctrl=00000000
sdc/low systick count=429500(+10) ticks=3(+-429487) ctrl=00000007 reload=0009c3ff current=00067373 intctrl=00000000

Within nrf52_sdc.c I was calling this debug function:
/****************************************************************************

  • Name: nrf52_sdc_debug_systick
    ****************************************************************************/

static void nrf52_sdc_debug_systick(FAR const char *tag)
{
#ifdef CONFIG_ARMV7M_SYSTICK
static uint32_t last_systick_count;
static clock_t last_sched_ticks;
static uint32_t log_count;

uint32_t systick_count = systick_debug_count();
clock_t sched_ticks = clock_systime_ticks();
uint32_t systick_ctrl = getreg32(NVIC_SYSTICK_CTRL);
uint32_t systick_reload = getreg32(NVIC_SYSTICK_RELOAD);
uint32_t systick_current = getreg32(NVIC_SYSTICK_CURRENT);
uint32_t intctrl = getreg32(NVIC_INTCTRL);

log_count++;

printf("sdc/%s systick count=%lu(+%ld) ticks=%lu(+%ld) "
"ctrl=%08lx reload=%08lx current=%08lx intctrl=%08lx\n",
tag,
(unsigned long)systick_count,
(long)(systick_count - last_systick_count),
(unsigned long)sched_ticks,
(long)(sched_ticks - last_sched_ticks),
(unsigned long)systick_ctrl,
(unsigned long)systick_reload,
(unsigned long)systick_current,
(unsigned long)intctrl);

last_systick_count = systick_count;
last_sched_ticks = sched_ticks;

UNUSED(log_count);
#else
UNUSED(tag);
#endif
}

I hope this helps.

current_usec() returns a uint64_t, but it used TICK2USEC(timebase)
to convert scheduler ticks to microseconds. On 32-bit clock_t builds,
TICK2USEC() performs the multiplication in 32-bit arithmetic before the
result is widened.

With CONFIG_USEC_PER_TICK=10000, this wraps after about 71.6 minutes:

  UINT32_MAX / 1000000 ~= 4294 seconds

After the wrap, up_timer_gettick() can report time near zero again. This
can leave absolute watchdog timeouts, such as those used by usleep() /
clock_nanosleep(), waiting for a tick value that will not be reached until
the 32-bit scheduler counter wraps.

Cast timebase to uint64_t before multiplying by USEC_PER_TICK so
current_usec() remains monotonic across the 32-bit microsecond boundary.

Signed-off-by: Max Kriegleder <max.kriegleder@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: Drivers Drivers issues Size: XS The size of the change in this PR is very small

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants