Skip to content

Fix 32-bit overflow in startBrake() ACCELERATING branch#144

Open
94xhn wants to merge 1 commit into
laurb9:masterfrom
94xhn:fix/startbrake-accelerating-overflow
Open

Fix 32-bit overflow in startBrake() ACCELERATING branch#144
94xhn wants to merge 1 commit into
laurb9:masterfrom
94xhn:fix/startbrake-accelerating-overflow

Conversation

@94xhn

@94xhn 94xhn commented Jul 13, 2026

Copy link
Copy Markdown

Problem

BasicStepperDriver::startBrake(), ACCELERATING case, computes:

steps_remaining = step_count * profile.accel / profile.decel;

step_count is long, profile.accel/profile.decel are short, so the
multiplication happens in 32-bit signed arithmetic before the division ever
runs. With legal, unclamped short values this overflows LONG_MAX.

Example: accel = 32767, decel = 32000, and startBrake() called after
step_count = 70000 steps into an accelerating LINEAR_SPEED move:

  • Correct value (64-bit): 70000 * 32767 / 32000 = 71677
  • Actual value from the current code: steps_remaining = -62539

A negative steps_remaining is treated as STOPPED by getCurrentState(),
so nextAction()'s else branch zeroes next_action_interval and stops
issuing step pulses immediately, instead of ramping down through
DECELERATING. That defeats the purpose of startBrake() (a controlled
early stop) and replaces it with an abrupt halt — the exact failure mode
startBrake() exists to avoid, risking missed/lost steps from motor inertia.

This is the same overflow class already fixed in this file for
steps_to_cruise/steps_to_brake in startMove() (see the comment at
line 164-166 and the float-cast idiom at line 187) — this call site in
startBrake() was missed.

Fix

Cast to float before the multiply, mirroring the exact idiom already used
elsewhere in this same file:

case ACCELERATING:
    // compare in float to avoid 32-bit overflow of step_count * profile.accel
    // with high microstep/rpm/accel combinations (same pattern as startMove())
    steps_remaining = (float)step_count * profile.accel / profile.decel;
    break;

Testing

Compiled the affected source (src/BasicStepperDriver.cpp) standalone
against a minimal Arduino.h host mock (pinMode/digitalWrite/micros/
yield no-ops), confirmed sizeof(long)==4 on the test toolchain (matches
the AVR/Cortex target width), and drove the object purely through its
public API (setRPM, setSpeedProfile, startMove, nextAction polling
loop, startBrake) to reproduce the overflow with the parameters above.

  • Before the fix: steps_remaining = -62539 (wrong, negative).
  • After the fix: steps_remaining = 71677 (exact match to the 64-bit
    ground truth).
  • Regression check: ran original vs. patched code across 6 additional,
    non-overflowing parameter sets (rpm 60-2000, microsteps 1-64, accel/decel
    100-32767, motor_steps 200/400, both inside and just-exiting
    ACCELERATING) — byte-identical output in every case, confirming no
    behavior change in the normal operating range.
  • Compiled with -Wall, zero warnings, before and after.

Note: if the true mathematically-correct answer itself exceeds LONG_MAX
(an extreme decel value far below accel), the final floatlong cast
can still saturate — that's an inherent capacity limit of using long for
step counts, unrelated to this arithmetic-ordering bug, and already the
accepted tradeoff elsewhere in this codebase (steps_remaining,
steps_to_cruise, etc. are all long). Out of scope for this minimal fix.

Agent: claude-sonnet-5 (Claude Code)

steps_remaining = step_count * profile.accel / profile.decel computes
the multiplication in 32-bit signed long before dividing, so it can
overflow with legal short accel/decel values (e.g. accel=32767,
decel=32000 at step_count=70000 yields steps_remaining=-62539 instead
of the correct 71677). A negative steps_remaining is treated as
STOPPED by getCurrentState(), so nextAction() stops issuing step
pulses immediately instead of ramping down, defeating startBrake()'s
purpose of a controlled deceleration.

Cast to float before the multiply, matching the same overflow-avoidance
idiom already used elsewhere in this file (line 187, and the sibling
steps_to_cruise/steps_to_brake computation in startMove()).

Verified by compiling the affected source against a minimal Arduino.h
host mock and driving the public API (setRPM/setSpeedProfile/startMove/
nextAction/startBrake) to reproduce the overflow with real, legal
parameters, then confirming the patched result exactly matches the
64-bit ground truth. Also ran 6 non-overflowing regression scenarios
(rpm 60-2000, microsteps 1-64, accel/decel 100-32767) with byte-identical
output before/after the change.

Agent: claude-sonnet-5 (Claude Code)
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