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

Critical Generic comparison bug found #123

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

korken89
Copy link
Collaborator

@korken89 korken89 commented Oct 8, 2021

I found a Generic comparison bug that is quite critical.

@korken89
Copy link
Collaborator Author

korken89 commented Oct 8, 2021

The old checking code truncated the checked values causing many comparisons to become 0.cmp(&0), this is now fixed.

@andresv
Copy link

andresv commented Oct 8, 2021

I can confirm that this fixes panic in such rtic idle loop:

let mut t = monotonics::now();
loop {
    let now = monotonics::now();
    if now - t >= Milliseconds(500).into() {
        t = monotonics::now();
        let _ = ctx.local.led.toggle();
    }
}

Without this it panics in now - t:

7764 ERROR panicked at 'Sub failed', /Users/andres/.cargo/registry/src/github.com-1ecc6299db9ec823/embedded-time-0.12.1/./src/instant.rs:454:13

It is critical to get it merged.

@korken89
Copy link
Collaborator Author

korken89 commented Oct 9, 2021

Hmm, while this works the 3 multiplications can easily overflow.
One should probably run some form of GCD between terms before performing multiplications, or some other smart way of performing the check.

@PTaylor-us
Copy link
Member

Wow, yeah. This was poorly implemented on my part.

Unless I'm mistaken, you're simply finding and comparing the numerators that would result from a cross-multiplication. I agree with you about the easy overflow on the multiplication. I am going to add the GCD reductions using the existing Stein's Algorithm GCD implementation in the num-integer crate.

@PTaylor-us PTaylor-us self-assigned this Oct 10, 2021
@PTaylor-us
Copy link
Member

I'm still poking around a bit. I think an initial implementation should follow the patterns present in the named Duration/Rate implementations. They convert the value with the larger scaling factor into one with the smaller scaling factory (e.g. Seconds -> Milliseconds). If the conversion fails, than that shows us it's greater. The step would be implementing a Generic<T>::try_into_scaling_factor() that will attempt to make the conversion in the same way as the TryInto<Milliseconds<T>> for Seconds<T> (implemented as TryFroms). There are certainly more efficient ways of doing it, but it can be optimized as needed after we have the desired functionality.

@PTaylor-us
Copy link
Member

@korken89 Please take a look at my changes. It follows virtual the same pattern as the named duration PartialOrd. In addition to the normal tests, I tested it with a stress test, looping with random numbers.

Comment on lines +70 to +75
assert!(
Generic::new(200u32, Fraction::new(1, 1000)) >= Generic::new(10u32, Fraction::new(1, 100))
);
assert!(
Generic::new(200u32, Fraction::new(1, 1000)) >= Generic::new(10u32, Fraction::new(1, 100))
);
Copy link

Choose a reason for hiding this comment

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

These two test cases are identical. These should either be de-duplicated, one of these changed to cover a different part of the comparison space, or a comment added as to why the duplicate is present.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I have stopped contributing here and created fugit instead.

Comment on lines +35 to +42

/// Moves an integer into a comparable base for checking
fn checked_same_base(&self, fraction: &Fraction, rhs_fraction: &Fraction) -> Option<Self> {
let a_n = *fraction.numerator();
let b_d = *rhs_fraction.denominator();

self.checked_mul(&(b_d.into()))?.checked_mul(&(a_n.into()))
}
Copy link

Choose a reason for hiding this comment

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

This is now unused in the PR after the refactor:

Suggested change
/// Moves an integer into a comparable base for checking
fn checked_same_base(&self, fraction: &Fraction, rhs_fraction: &Fraction) -> Option<Self> {
let a_n = *fraction.numerator();
let b_d = *rhs_fraction.denominator();
self.checked_mul(&(b_d.into()))?.checked_mul(&(a_n.into()))
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I have stopped contributing here and created fugit instead.

Comment on lines +87 to +105
/* Generic comparison stress test:
loop {
let mut rands: [u32; 6] = [0; 6];
for rand in rands.iter_mut() {
unsafe {
loop {
core::arch::x86_64::_rdrand32_step(rand);
if *rand != 0 {
break;
}
}
}
}
println!("rands: {:#?}", rands);
assert!(
Generic::new(rands[0], Fraction::new(rands[1], rands[2]))
!= Generic::new(rands[3], Fraction::new(rands[4], rands[5]))
);
} */
Copy link

Choose a reason for hiding this comment

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

It might be useful to rewrite this using the proptest crate, so you retain the "random checks" testing, while also having bounded runtimes for tests, and reductions of failures to simplified cases.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I have stopped contributing here and created fugit instead.

@@ -443,14 +443,42 @@ pub struct Generic<T> {
scaling_factor: Fraction,
}

impl<T: TimeInt> Generic<T> {
/// Try to create a new, equivalent `Generic` with the given _scaling factor_
Copy link

Choose a reason for hiding this comment

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

Just to help make what is going on here a little clearer:

Suggested change
/// Try to create a new, equivalent `Generic` with the given _scaling factor_
/// Try to create a new, equivalent `Generic` with the given _scaling factor_.
///
/// To minimise errors due to truncation rounding, the new integer component is
/// calculated as `self.integer * (self.scaling_factor / scaling_factor)`.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I have stopped contributing here and created fugit instead.

Some(self.integer.cmp(&rhs.integer))
} else if self.scaling_factor < rhs.scaling_factor {
// convert to the smaller scaling factor (rhs -> self)
// if conversion fails, we know self is less than rhs
Copy link

Choose a reason for hiding this comment

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

It feels like this will be true most of the time, but I'm not entirely convinced that there aren't edge cases where rhs.scaling_factor / self.scaling_factor < self.integer / rhs.integer (so self > rhs), and rhs is close enough to the maximum bound on T that it will overflow. In particular, TimeInt::checked_mul_fraction is being called with rhs.integer and a fraction that is greater than 1, and is implemented by first multiplying by the numerator (which could overflow) and then dividing by the denominator (which if not for the overflow could bring the result back in range and below self.integer). It would be great to have tests that exercise these near-bounds edge cases.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I have stopped contributing here and created fugit instead.

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.

4 participants