From 90aba4df0971cda92bdcdf7a45a9fc13f7f9b7c4 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Fri, 18 Nov 2022 02:55:28 +0100 Subject: [PATCH] Timeline: demonstrate safeguards against reversed and toxic input --- src/lib/time/timevalue.hpp | 4 + src/stage/model/zoom-window.hpp | 56 ++++++-- tests/15library.tests | 5 + tests/51-gui-model.tests | 5 + tests/stage/model/zoom-window-test.cpp | 129 +++++++++++------ wiki/thinkPad.ichthyo.mm | 190 +++++++++++++++++++++++-- 6 files changed, 328 insertions(+), 61 deletions(-) diff --git a/src/lib/time/timevalue.hpp b/src/lib/time/timevalue.hpp index 6a460dad0..f56983f2e 100644 --- a/src/lib/time/timevalue.hpp +++ b/src/lib/time/timevalue.hpp @@ -475,6 +475,10 @@ namespace time { Duration (TimeSpan const& interval); Duration (FrameCnt count, FrameRate const& fps); + Duration (Duration const& o) + : Duration{Offset(o)} + { } + static const Duration NIL; void accept (Mutation const&); diff --git a/src/stage/model/zoom-window.hpp b/src/stage/model/zoom-window.hpp index f2c68ec0b..c35ea7c0a 100644 --- a/src/stage/model/zoom-window.hpp +++ b/src/stage/model/zoom-window.hpp @@ -168,6 +168,22 @@ namespace model { const uint MAX_PX_WIDTH{1000000}; const FSecs MAX_TIMESPAN{_FSecs(Time::MAX-Time::MIN)}; const FSecs MICRO_TICK{1_r/Time::SCALE}; + + /** Maximum quantiser to be handled in fractional arithmetics without hazard. + * @remark due to the common divisor normalisation, and the typical time computations, + * DENOMINATOR * Time::Scale has to stay below INT_MAX, with some safety margin + */ + const int64_t LIM_HAZARD{int64_t{1} << 40 }; + + inline int + toxicDegree (Rat poison) + { + const int64_t HAZARD_DEGREE{util::ilog2(LIM_HAZARD)}; + int64_t magNum = util::ilog2(abs(poison.numerator())); + int64_t magDen = util::ilog2(abs(poison.denominator())); + int64_t degree = max (magNum, magDen); + return max (0, degree - HAZARD_DEGREE); + } } @@ -461,14 +477,34 @@ namespace model { */ static TimeValue - ensureNonEmpty (TimeVar& startRef, TimeValue endPoint) + ensureNonEmpty (Time start, TimeValue endPoint) { - if (startRef < endPoint) - return endPoint; - if (startRef <= Time::MAX - Time{DEFAULT_CANVAS}) - return startRef + Time{DEFAULT_CANVAS}; - startRef = Time::MAX - Time{DEFAULT_CANVAS}; - return Time::MAX; + return (start < endPoint)? endPoint + : start + Time{DEFAULT_CANVAS}; + } + + /** + * Check and possibly sanitise a rational number to avoid internal numeric overflow. + * Fractional arithmetics can be insidious, due to the frequent re-normalisation; + * seemingly "harmless" numbers with a large denominator can cause numeric wrap-around. + * As safeguard, by introducing a tiny error, problematic numbers can be re-quantised + * to smaller denominators; moreover, large numbers must be limit checked. + * @remark Both the denominator and the numerator must be kept below a toxic limit, + * which is defined by the ability to multiply with Time::Scale without wrap-around. + * This heuristic is based on the actual computations done with the zoom factor and + * is thus specific to the ZoomWindow implementation. To sanitise, the denominator + * is reduced logarithmically (bit-shift) sufficiently and then used as new quantiser, + * thus ensuring that both denominator (=quantiser) and numerator are below limit. + * @note the check is based on the 2-logarithm of numerator and denominator, which is + * pretty much the fastest possibility (even a simple comparison would have + * to do the same). Values below threshold are simply passed-through. + */ + static Rat + detox (Rat poison) + { + int toxicity = toxicDegree (poison); + return toxicity ? util::reQuant(poison, poison.denominator() >> toxicity) + : poison; } static Rat @@ -493,7 +529,7 @@ namespace model { FSecs dur{afterWin_-startWin_}; Rat adjMetric = Rat(pxWidth) / dur; ENSURE (pxWidth == rational_cast (adjMetric*dur)); - return adjMetric; + return detox (adjMetric); } void @@ -503,7 +539,7 @@ namespace model { REQUIRE (afterWin_> startWin_); FSecs dur{afterWin_-startWin_}; uint pxWidth = rational_cast (px_per_sec_*dur); - dur = Rat(pxWidth) / changedMetric; + dur = Rat(pxWidth) / detox (changedMetric); dur = min (dur, MAX_TIMESPAN); dur = max (dur, MICRO_TICK); // prevent window going void TimeVar timeDur{dur}; @@ -782,7 +818,7 @@ namespace model { posFactor = posFactor*posFactor*posFactor; // -1 ... +1 but accelerating towards boundaries posFactor = (posFactor + 1) / 2; // 0 ... 1 posFactor = util::limited (0, posFactor, 1); - return posFactor; + return detox (posFactor); } }; diff --git a/tests/15library.tests b/tests/15library.tests index e847ae880..317745078 100644 --- a/tests/15library.tests +++ b/tests/15library.tests @@ -508,6 +508,11 @@ return: 0 END +TEST "fractional arithmetics" Rational_test < (negaTime); // attempt fabricate a subverted TimeSpan + CHECK (evilDuration < Time::ZERO); // ...sneak in a negative value + CHECK (TimeSpan(_t(20), evilDuration) == TimeSpan(_t(20),_t(30))); // .....yet won't make it get past Duration copy ctor! + } + + + /** @test demonstrate sanitising of "poisonous" fractional zoom factor + * - construct an example factor of roughly 2/3, but using extremely large + * numerator and denominator close to total time axis dimensions. + * - even simple calculations with this poison value will fail + * - construct a new quantiser, based on the number to be sanitised + * - re-quantise the toxic number into this new quantiser + * - the sanitised number is almost identical to the toxic original + * - yet all the simple calculations can be carried out flawlessly + * - both toxic and sanitised number lead to the same zoom timespan + */ + void + safeguard_toxic_zoomFactor() + { + Rat poison{_raw(Time::MAX)-101010101010101010, _raw(Time::MAX)+23}; + CHECK (poison == 206435633551724850_r/307445734561825883); + CHECK (poison + Time::SCALE < 0); // simple calculations fail due to numeric overflow + CHECK (Time(FSecs(poison)) < Time::ZERO); // conversion to µ-ticks also leads to overflow + CHECK (-6 == _raw(Time(FSecs(poison)))); + + using util::ilog2; + CHECK (40 == ilog2 (LIM_HAZARD)); // LIM_HAZARD is based on MAX_INT / Time::Scale + CHECK (57 == ilog2 (poison.numerator())); // use the leading bit position as size indicator + CHECK (58 == ilog2 (poison.denominator())); // use the maximum of numerator or denominator bit position + CHECK (58-40 == 18); // everything beyond LIM_HAZARD counts as "toxic" + + int toxicity = toxicDegree (poison); + CHECK (toxicity == 18); + int64_t quant = poison.denominator() >> toxicity; // shift away the excess toxic LSB + CHECK (quant == 1172812402961); + CHECK (ilog2 (quant) == ilog2 (LIM_HAZARD)); + Rat detoxed = util::reQuant(poison, quant); // and use this "shortened" denominator for re-quantisation + CHECK (detoxed == 787489446837_r/1172812402961); // the resulting fraction uses way smaller numbers + CHECK (0.671453834f == rational_cast (poison)); // but yields approximately the same effective value + CHECK (0.671453834f == rational_cast (detoxed)); + + CHECK (detoxed+Time::SCALE == 1172813190450446837_r/1172812402961); // result: we can calculate without failure + CHECK (Time(FSecs(detoxed)) > Time::ZERO); // can convert re-quantised number to µ-ticks + CHECK (671453 == _raw(Time(FSecs(detoxed)))); + // and resulting µ-ticks will be effectively the same + CHECK (1906 == _raw(TimeValue(1280 / rational_cast(poison)))); + CHECK (1906 == _raw(TimeValue(1280 / rational_cast(detoxed)))); + ZoomWindow win{}; // SHOW_EXPR(win.overallSpan()); // SHOW_EXPR(_raw(win.visible().duration())); @@ -535,7 +581,6 @@ namespace test { // CHECK (win.visible() == TimeSpan(_t(0), _t(23))); // CHECK (win.px_per_sec() == 25); // CHECK (win.pxWidth() == 575); - } } }; diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index a70724fed..8c4e2ee44 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -39555,6 +39555,9 @@ + + + @@ -39615,11 +39618,53 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -39690,6 +39735,7 @@ + @@ -39707,13 +39753,35 @@ - + + + + + + + + + +

+ ⟹ Fazit: +

+
    +
  • + erhält Invariante +
  • +
  • + Ergebnis kann aber inhaltlich falsch sein +
  • +
+ +
+
@@ -39723,19 +39791,41 @@ - + - + + + + + + + +

+ Vorsicht: Ergebnis-Faktor kann trotzdem giftig sein +

+ +
+ +
+ +
+ + + + + + + @@ -39748,11 +39838,11 @@ - + - + @@ -39808,8 +39898,8 @@ - - + + @@ -40067,7 +40157,89 @@ - + + + + + + + + + + + + + + + + + +

+ ...und zwar in dem Fall, in dem der Nenner nur knapp über der Schwelle LIM_HAZARD liegt; dann findet nämlich fast keine Reduktion der Größenordnung statt +

+ +
+
+
+ + + + + + + + +
    +
  • + der Nenner wird garantiert gleich dem vorgegebenen Quantisierer sein +
  • +
  • + aber der Zähler ist ggfs um das Verhältnis Zähler / Nenner größer als der Quantisierer +
  • +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +