Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Protect against passing i128::MIN to abs() which causes overflow #2241

Merged
merged 8 commits into from
Sep 25, 2024
21 changes: 21 additions & 0 deletions crates/fuel-gas-price-algorithm/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,24 @@ pub(crate) fn cumulative_percentage_change(
// `f64` over `u64::MAX` are cast to `u64::MAX`
approx.ceil() as u64
}

pub(crate) fn safe_signed_abs(n: i128) -> i128 {
let n = if n == i128::MIN {
n.saturating_add(1)
} else {
n
};
debug_assert!(n != i128::MIN);
n.abs()
}

#[cfg(test)]
mod tests {
use crate::utils::safe_signed_abs;

#[test]
fn safe_signed_abs_does_not_overflow_on_min_value() {
let abs = safe_signed_abs(i128::MIN);
assert_eq!(abs, i128::MAX);
}
}
7 changes: 5 additions & 2 deletions crates/fuel-gas-price-algorithm/src/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ use std::{
ops::Div,
};

use crate::utils::cumulative_percentage_change;
use crate::utils::{
cumulative_percentage_change,
safe_signed_abs,
};

#[cfg(test)]
mod tests;
Expand Down Expand Up @@ -331,7 +334,7 @@ impl AlgorithmUpdaterV1 {
.saturating_mul(upcast_percent)
.saturating_div(100)
.into();
let clamped_change = pd_change.abs().min(max_change);
let clamped_change = safe_signed_abs(pd_change).min(max_change);
pd_change.signum().saturating_mul(clamped_change)
}

Expand Down
31 changes: 31 additions & 0 deletions crates/fuel-gas-price-algorithm/src/v1/tests/algorithm_v1_tests.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use std::num::NonZeroU64;

use crate::v1::{
tests::UpdaterBuilder,
AlgorithmUpdaterV1,
AlgorithmV1,
ClampedPercentage,
};
use proptest::prelude::*;

Expand Down Expand Up @@ -143,3 +147,30 @@ fn worst_case__same_block_gives_the_same_value_as_calculate() {
let expected = algorithm.calculate();
assert_eq!(expected, actual);
}

#[test]
fn da_change_should_not_panic() {
let updater = AlgorithmUpdaterV1 {
new_scaled_exec_price: 0,
min_exec_gas_price: 0,
exec_gas_price_change_percent: 0,
l2_block_height: 0,
l2_block_fullness_threshold_percent: ClampedPercentage::new(0),
new_scaled_da_gas_price: 0,
gas_price_factor: NonZeroU64::new(1).unwrap(),
min_da_gas_price: 0,
max_da_gas_price_change_percent: 0,
total_da_rewards_excess: 0,
da_recorded_block_height: 0,
latest_known_total_da_cost_excess: 0,
projected_total_da_cost: 0,
da_p_component: 0,
da_d_component: 0,
last_profit: 0,
second_to_last_profit: 0,
latest_da_cost_per_byte: 0,
unrecorded_blocks: vec![],
};

updater.da_change(i128::MIN / 2, i128::MIN / 2);
Copy link
Member

@MitchTurner MitchTurner Sep 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not test private functions. We should be able to test this with update_l2_block_data. And we can add a SUT and given/when/then.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 3047c7e, however, I'm not fully convinced this fits better here.

Rationale:
we need to carefully select the values in order to trigger the "what used to be" an overflow inside da_change(). Now, if we, for example, change how the total_da_rewards_excess is updated here or how we calculate P and D here we may cause the da_change() to be invoked with values that do not cause overflow and the test will pass with both abs() and saturating_abs().

I'm open to suggestions on how we can do this better.

Copy link
Member

@MitchTurner MitchTurner Sep 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be okay with removing the test since there are many places in the code that "could" panic if programmed poorly.

It might be worth adding some more aggressive prop testing for those cases, but that could be a separate issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this boils down to the fact that people (me, at least) do not associate an "innocent" abs() call with a possible panic. Think unwraps or expects - these are usually caught during the review process and removed from the production code + after removing them, we do not add a test.

So yes, I agree that this test is kinda redundant - I removed it in 7359bd7

What we could do is:

  1. Spawn a tech-debt issue to review the codebase against unwraps, expects, abs and similar and update relevant places
  2. Rely on a clippy lint to detect those, but as far as I know there is no such lint. Maybe we can contribute to clippy and add "potential_panic_in_std"? :)

Wdyt?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently we have

#![deny(clippy::arithmetic_side_effects)]
#![deny(clippy::cast_possible_truncation)]
#![deny(warnings)]

But I'd be open to adding more for sure.

}
Loading