fabric::Ramp::Set overflows on a balance moved hard left to hard right: a step, not a ramp #172

Open
opened 2026-10-08 00:53:21 +00:00 by bcox · 0 comments
Owner

fabric::Ramp::Set in fabric/inserts.h computes its step as

step_ = (target.raw() - current_.raw()) / kGainRampSamples;

raw() is the value's 32-bit raw for dsp::Sample and fabric::Gain, so the subtraction is done in int before it is widened into step_.

What goes wrong

  • A balance is a Ramp<dsp::Sample> (StripCore::balance_), Q1.30, so hard left is -2^30 and hard right is +2^30.
  • A balance moved from hard left to hard right in one event is a difference of 2^31, which does not fit int: signed overflow, undefined behavior.
  • As compiled, the step comes out as -2^25. The placement stays clamped hard left for 32 ticks, wraps on the 33rd to hard right, and snaps to the target on the 64th. That is a step where a 64-sample ramp is meant.
  • Right to left is -2^31, which fits, and ramps correctly.
  • Ramp<Gain> stays in range for non-negative gains.

Reachability

project/model.h admits a balance in [-1, 1], and the fabric sets it with SetBalance(Sample::FromDouble(event.value)) with no clamp short of the ends. An automation lane, a MIDI pan sweep or a control dragged end to end in one event produces it.

Evidence

Found on 2026-10-07 while cosimulating rtl/fabric/strip_skeleton against fabric::ChannelStrip (branch de25-va-build-budget). The task's reviewer reproduced it with a twelve-line program under clang++ -fsanitize=undefined:

signed integer overflow: 1073741824 - -1073741824 cannot be represented in type 'Raw' (aka 'int')

at Ramp::Set. The repo's --config=ubsan lane has not reported it, so no test moves a balance end to end in one event.

Fix shape

Widen to std::int64_t before subtracting. step_ is already 64 bits, and Tick adds it to current_.raw() and lands on the target exactly on the last tick, so nothing else changes.

A test: set a balance hard left, snap, set hard right, and hold each of the 64 ticks to the line between them; inject the 32-bit subtraction and watch it fail.

Where else it is recorded

  • docs/superpowers/ledgers/2026-10-03-fabric-prices.md, "Minors left unfixed".
  • rtl/fabric/strip_skeleton/sim/tb_strip_skeleton.cc keeps every ramp target inside 2^31 of where its ramp stands, so the cosim does not hold the header past that. A mixer's RTL takes the corrected arithmetic.
`fabric::Ramp::Set` in `fabric/inserts.h` computes its step as ```cpp step_ = (target.raw() - current_.raw()) / kGainRampSamples; ``` `raw()` is the value's 32-bit raw for `dsp::Sample` and `fabric::Gain`, so the subtraction is done in `int` before it is widened into `step_`. ## What goes wrong - A balance is a `Ramp<dsp::Sample>` (`StripCore::balance_`), Q1.30, so hard left is -2^30 and hard right is +2^30. - A balance moved from hard left to hard right in one event is a difference of 2^31, which does not fit `int`: signed overflow, undefined behavior. - As compiled, the step comes out as -2^25. The placement stays clamped hard left for 32 ticks, wraps on the 33rd to hard right, and snaps to the target on the 64th. That is a step where a 64-sample ramp is meant. - Right to left is -2^31, which fits, and ramps correctly. - `Ramp<Gain>` stays in range for non-negative gains. ## Reachability `project/model.h` admits a balance in [-1, 1], and the fabric sets it with `SetBalance(Sample::FromDouble(event.value))` with no clamp short of the ends. An automation lane, a MIDI pan sweep or a control dragged end to end in one event produces it. ## Evidence Found on 2026-10-07 while cosimulating `rtl/fabric/strip_skeleton` against `fabric::ChannelStrip` (branch `de25-va-build-budget`). The task's reviewer reproduced it with a twelve-line program under `clang++ -fsanitize=undefined`: ``` signed integer overflow: 1073741824 - -1073741824 cannot be represented in type 'Raw' (aka 'int') ``` at `Ramp::Set`. The repo's `--config=ubsan` lane has not reported it, so no test moves a balance end to end in one event. ## Fix shape Widen to `std::int64_t` before subtracting. `step_` is already 64 bits, and `Tick` adds it to `current_.raw()` and lands on the target exactly on the last tick, so nothing else changes. A test: set a balance hard left, snap, set hard right, and hold each of the 64 ticks to the line between them; inject the 32-bit subtraction and watch it fail. ## Where else it is recorded - `docs/superpowers/ledgers/2026-10-03-fabric-prices.md`, "Minors left unfixed". - `rtl/fabric/strip_skeleton/sim/tb_strip_skeleton.cc` keeps every ramp target inside 2^31 of where its ramp stands, so the cosim does not hold the header past that. A mixer's RTL takes the corrected arithmetic.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
bcox/tymbal#172
No description provided.