Skip to content

Commit aa61ace

Browse files
Ink Open Sourcecopybara-github
authored andcommitted
Add fuzz test for BrushTipModeler
According to `brush_tip_modeler_helpers.h`, the brush behavior value stack is only ever supposed to hold finite values or null values (represented by NaN in the current implementation). Some parts of the code were enforcing this already, but the new fuzz test found lots of places that weren't, so those have been fixed. PiperOrigin-RevId: 830559434
1 parent 601d99b commit aa61ace

File tree

3 files changed

+91
-16
lines changed

3 files changed

+91
-16
lines changed

ink/strokes/internal/BUILD.bazel

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,18 +226,25 @@ cc_library(
226226
cc_test(
227227
name = "brush_tip_modeler_test",
228228
srcs = ["brush_tip_modeler_test.cc"],
229+
args = ["--fuzztest_time_limit_per_input=1s"],
229230
deps = [
230231
":brush_tip_modeler",
231232
":brush_tip_state",
232233
":modeled_stroke_input",
234+
":stroke_input_modeler",
233235
"//ink/brush:brush_behavior",
236+
"//ink/brush:brush_family",
234237
"//ink/brush:brush_tip",
235238
"//ink/brush:easing_function",
239+
"//ink/brush:fuzz_domains",
236240
"//ink/geometry:angle",
237241
"//ink/geometry:point",
238242
"//ink/geometry:type_matchers",
243+
"//ink/strokes/input:fuzz_domains",
239244
"//ink/strokes/input:stroke_input",
245+
"//ink/strokes/input:stroke_input_batch",
240246
"//ink/types:duration",
247+
"@com_google_fuzztest//fuzztest",
241248
"@com_google_googletest//:gtest_main",
242249
],
243250
)

ink/strokes/internal/brush_tip_modeler_helpers.cc

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ Duration32 GetTimeSinceInput(const InputModelerState& input_modeler_state,
9696
return input_modeler_state.complete_elapsed_time - input.elapsed_time;
9797
}
9898

99+
// Returns the value of the given `Source` at the given modeled input, or
100+
// `std::nullopt` if the source value is indeterminate at that input.
99101
std::optional<float> GetSourceValue(
100102
const ModeledStrokeInput& input, std::optional<Angle> travel_direction,
101103
float brush_size, const InputModelerState& input_modeler_state,
@@ -313,14 +315,19 @@ void ProcessBehaviorNodeImpl(const BrushBehavior::SourceNode& node,
313315
return;
314316
}
315317

316-
context.stack.push_back(ApplyOutOfRangeBehavior(
318+
float value = ApplyOutOfRangeBehavior(
317319
node.source_out_of_range_behavior,
318320
InverseLerp(node.source_value_range[0], node.source_value_range[1],
319-
*source_value)));
321+
*source_value));
322+
if (!std::isfinite(value)) {
323+
value = kNullBehaviorNodeValue;
324+
}
325+
context.stack.push_back(value);
320326
}
321327

322328
void ProcessBehaviorNodeImpl(const BrushBehavior::ConstantNode& node,
323329
const BehaviorNodeContext& context) {
330+
ABSL_DCHECK(std::isfinite(node.value));
324331
context.stack.push_back(node.value);
325332
}
326333

@@ -364,7 +371,12 @@ void ProcessBehaviorNodeImpl(const NoiseNodeImplementation& node,
364371
} break;
365372
}
366373
NoiseGenerator& generator = context.noise_generators[node.generator_index];
367-
generator.AdvanceInputBy(advance_by);
374+
// If the above calculation produces an undefined `advance_by` value (e.g. due
375+
// to extreme input values overflowing and producing ill-defined computed
376+
// values), just don't advance the noise generator.
377+
if (!std::isnan(advance_by)) {
378+
generator.AdvanceInputBy(advance_by);
379+
}
368380
context.stack.push_back(generator.CurrentOutputValue());
369381
}
370382

@@ -389,17 +401,19 @@ void ProcessBehaviorNodeImpl(const BrushBehavior::ToolTypeFilterNode& node,
389401
void ProcessBehaviorNodeImpl(const DampingNodeImplementation& node,
390402
const BehaviorNodeContext& context) {
391403
ABSL_DCHECK(!context.stack.empty());
392-
float& damped_value = context.damped_values[node.damping_index];
404+
float old_damped_value = context.damped_values[node.damping_index];
405+
float new_damped_value = kNullBehaviorNodeValue;
393406
float input = context.stack.back();
394407
if (IsNullBehaviorNodeValue(input)) {
395408
// Input is null, so use previous damped value unchanged.
396-
} else if (IsNullBehaviorNodeValue(damped_value) ||
409+
new_damped_value = old_damped_value;
410+
} else if (IsNullBehaviorNodeValue(old_damped_value) ||
397411
node.damping_gap == 0.0f) {
398412
// Input is non-null. If previous damped value is null, then this is the
399413
// first non-null input, so snap the damped value to the input. Or, if the
400414
// damping_gap is zero, then there's no damping to be done, so also snap the
401415
// damped value to the input.
402-
damped_value = input;
416+
new_damped_value = input;
403417
} else {
404418
// Input and previous damped value are both non-null, so move the damped
405419
// value towards the input according to the damping settings. Note that a
@@ -411,7 +425,7 @@ void ProcessBehaviorNodeImpl(const DampingNodeImplementation& node,
411425
// If no mapping from stroke units to physical units is available, then
412426
// don't perform any damping (i.e. snap damped value to input).
413427
if (!context.input_modeler_state.stroke_unit_length.has_value()) {
414-
damped_value = input;
428+
new_damped_value = input;
415429
break;
416430
}
417431
PhysicalDistance damping_distance =
@@ -420,34 +434,49 @@ void ProcessBehaviorNodeImpl(const DampingNodeImplementation& node,
420434
*context.input_modeler_state.stroke_unit_length *
421435
(context.current_input.traveled_distance -
422436
context.previous_input_metrics->traveled_distance);
423-
damped_value = DampOffsetTransition(
424-
input, damped_value, traveled_distance_delta, damping_distance);
437+
new_damped_value = DampOffsetTransition(
438+
input, old_damped_value, traveled_distance_delta, damping_distance);
425439
} break;
426440
case BrushBehavior::DampingSource::kDistanceInMultiplesOfBrushSize: {
427441
float damping_distance = context.brush_size * node.damping_gap;
428442
float traveled_distance_delta =
429443
context.current_input.traveled_distance -
430444
context.previous_input_metrics->traveled_distance;
431-
damped_value = DampOffsetTransition(
432-
input, damped_value, traveled_distance_delta, damping_distance);
445+
new_damped_value = DampOffsetTransition(
446+
input, old_damped_value, traveled_distance_delta, damping_distance);
433447
} break;
434448
case BrushBehavior::DampingSource::kTimeInSeconds: {
435449
Duration32 damping_time = Duration32::Seconds(node.damping_gap);
436450
Duration32 elapsed_time_delta =
437451
context.current_input.elapsed_time -
438452
context.previous_input_metrics->elapsed_time;
439-
damped_value = DampOffsetTransition(input, damped_value,
440-
elapsed_time_delta, damping_time);
453+
new_damped_value = DampOffsetTransition(
454+
input, old_damped_value, elapsed_time_delta, damping_time);
441455
} break;
442456
}
443457
}
444-
context.stack.back() = damped_value;
458+
// If the calculation above produced a null or non-finite damped value
459+
// (e.g. due to float overflow or a null input), then leave the old damped
460+
// value unchanged.
461+
if (!std::isfinite(new_damped_value)) {
462+
new_damped_value = old_damped_value;
463+
}
464+
context.damped_values[node.damping_index] = new_damped_value;
465+
context.stack.back() = new_damped_value;
445466
}
446467

447468
void ProcessBehaviorNodeImpl(const EasingImplementation& node,
448469
const BehaviorNodeContext& context) {
449470
ABSL_DCHECK(!context.stack.empty());
450-
context.stack.back() = node.GetY(context.stack.back());
471+
float input = context.stack.back();
472+
float* result = &context.stack.back();
473+
if (IsNullBehaviorNodeValue(input)) return;
474+
*result = node.GetY(input);
475+
// If the easing function resulted in a non-finite value (e.g. due to overflow
476+
// to infinity), treat the result as null.
477+
if (!std::isfinite(*result)) {
478+
*result = kNullBehaviorNodeValue;
479+
}
451480
}
452481

453482
void ProcessBehaviorNodeImpl(const BrushBehavior::BinaryOpNode& node,
@@ -515,8 +544,14 @@ void ProcessBehaviorNodeImpl(const TargetNodeImplementation& node,
515544
float input = context.stack.back();
516545
context.stack.pop_back();
517546
if (IsNullBehaviorNodeValue(input)) return;
518-
context.target_modifiers[node.target_index] =
547+
548+
float modifier =
519549
Lerp(node.target_modifier_range[0], node.target_modifier_range[1], input);
550+
// If the new modifier is non-finite (e.g. due to float overflow), then leave
551+
// the previous modifier unchanged.
552+
if (!std::isfinite(modifier)) return;
553+
554+
context.target_modifiers[node.target_index] = modifier;
520555
}
521556

522557
void ProcessBehaviorNodeImpl(const PolarTargetNodeImplementation& node,
@@ -530,10 +565,15 @@ void ProcessBehaviorNodeImpl(const PolarTargetNodeImplementation& node,
530565
IsNullBehaviorNodeValue(magnitude_input)) {
531566
return;
532567
}
568+
533569
Vec modifier = Vec::FromDirectionAndMagnitude(
534570
Angle::Radians(
535571
Lerp(node.angle_range[0], node.angle_range[1], angle_input)),
536572
Lerp(node.magnitude_range[0], node.magnitude_range[1], magnitude_input));
573+
// If the new modifier vector is non-finite (e.g. due to float overflow), then
574+
// leave the previous modifier vector unchanged.
575+
if (!std::isfinite(modifier.x) || !std::isfinite(modifier.y)) return;
576+
537577
context.target_modifiers[node.target_x_index] = modifier.x;
538578
context.target_modifiers[node.target_y_index] = modifier.y;
539579
}

ink/strokes/internal/brush_tip_modeler_test.cc

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,21 @@
1919

2020
#include "gmock/gmock.h"
2121
#include "gtest/gtest.h"
22+
#include "fuzztest/fuzztest.h"
2223
#include "ink/brush/brush_behavior.h"
24+
#include "ink/brush/brush_family.h"
2325
#include "ink/brush/brush_tip.h"
2426
#include "ink/brush/easing_function.h"
27+
#include "ink/brush/fuzz_domains.h"
2528
#include "ink/geometry/angle.h"
2629
#include "ink/geometry/point.h"
2730
#include "ink/geometry/type_matchers.h"
31+
#include "ink/strokes/input/fuzz_domains.h"
2832
#include "ink/strokes/input/stroke_input.h"
33+
#include "ink/strokes/input/stroke_input_batch.h"
2934
#include "ink/strokes/internal/brush_tip_state.h"
3035
#include "ink/strokes/internal/modeled_stroke_input.h"
36+
#include "ink/strokes/internal/stroke_input_modeler.h"
3137
#include "ink/types/duration.h"
3238

3339
namespace ink::strokes_internal {
@@ -1288,5 +1294,27 @@ TEST(BrushTipModelerDeathTest, NanBrushSize) {
12881294
modeler.StartStroke(&tip, std::numeric_limits<float>::quiet_NaN()), "");
12891295
}
12901296

1297+
void CanModelAnyValidBrushTipAndInputs(const BrushTip& brush_tip,
1298+
const StrokeInputBatch& input_batch) {
1299+
float brush_size = 1;
1300+
float brush_epsilon = 0.01;
1301+
// Run an arbitrary `StrokeInputBatch` through the naive input modeler as a
1302+
// way of getting a mostly-arbitrary (but valid) input modeler state and
1303+
// sequence of modeled inputs.
1304+
StrokeInputModeler input_modeler;
1305+
input_modeler.StartStroke(BrushFamily::ExperimentalNaiveModel{},
1306+
brush_epsilon);
1307+
input_modeler.ExtendStroke(input_batch, StrokeInputBatch(),
1308+
Duration32::Zero());
1309+
// We should be able to apply the `BrushTipModeler` to any valid brush tip and
1310+
// input sequence, and not crash.
1311+
BrushTipModeler tip_modeler;
1312+
tip_modeler.StartStroke(&brush_tip, brush_size);
1313+
tip_modeler.UpdateStroke(input_modeler.GetState(),
1314+
input_modeler.GetModeledInputs());
1315+
}
1316+
FUZZ_TEST(BrushTipModelerFuzzTest, CanModelAnyValidBrushTipAndInputs)
1317+
.WithDomains(ValidBrushTip(), ArbitraryStrokeInputBatch());
1318+
12911319
} // namespace
12921320
} // namespace ink::strokes_internal

0 commit comments

Comments
 (0)