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

Safer poll timeout #1876

Merged
merged 1 commit into from
Nov 23, 2023
Merged

Conversation

JonathanWoollett-Light
Copy link
Contributor

@JonathanWoollett-Light JonathanWoollett-Light commented Nov 21, 2022

Implements a wrapper around i32 for the timeout argument in poll::poll.

This prevents the error case:

EINVAL (ppoll()) The timeout value expressed in *ip is invalid
(negative).

Most of the code additions are simple conversions to/from integer primitives.

An alternative approach to implement the same safety with less code would require something like https://internals.rust-lang.org/t/non-negative-integer-types/17796 and would offer less usability like converting to/from std::time::Durations.

@JonathanWoollett-Light JonathanWoollett-Light force-pushed the poll-timeout branch 8 times, most recently from 74f8396 to ed82f41 Compare November 23, 2022 20:31
@asomers
Copy link
Member

asomers commented Nov 23, 2022

CI is failing because you're trying to use rust 1.58.0, but the 1.58 container installs 1.58.1. Change the VERSION to 1.58.1

@JonathanWoollett-Light JonathanWoollett-Light force-pushed the poll-timeout branch 4 times, most recently from ed39c68 to 297c74c Compare November 23, 2022 20:43
@JonathanWoollett-Light
Copy link
Contributor Author

CI is failing because you're trying to use rust 1.58.0, but the 1.58 container installs 1.58.1. Change the VERSION to 1.58.1

Thank you for letting me know.

Why is this the case?

@asomers
Copy link
Member

asomers commented Nov 23, 2022

CI is failing because you're trying to use rust 1.58.0, but the 1.58 container installs 1.58.1. Change the VERSION to 1.58.1

Thank you for letting me know.

Why is this the case?

That's just the convention for the rust-lang containers. They always include the newest patch release.

@asomers asomers added this to the 0.27.0 milestone Nov 29, 2022
src/poll.rs Outdated
// specialization.
impl PollTimeout {
/// Blocks indefinitely.
pub const NONE: Self = Self(1 << 31);
Copy link
Member

Choose a reason for hiding this comment

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

Is this supposed to be C's INFTIM, and if so shouldn't it be -1?

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 I was originally using a unsigned integer type here for some reason e.g. 1 << 31 == -1. But it makes more sense to use -1 now, will change this.

src/poll.rs Outdated
@@ -132,12 +384,12 @@ libc_bitflags! {
/// in timeout means an infinite timeout. Specifying a timeout of zero
Copy link
Member

Choose a reason for hiding this comment

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

You should update the docs for the timeout variable.

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 docs

CHANGELOG.md Outdated
- The MSRV is now 1.56.1
([#1792](https://github.com/nix-rust/nix/pull/1792))
- The MSRV is now 1.58.1
([#1876](https://github.com/nix-rust/nix/pull/1876))
Copy link
Member

Choose a reason for hiding this comment

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

You can delete this part, and all of the other MSRV stuff, since we'll merge #1862 first.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Dropped this commit

@JonathanWoollett-Light JonathanWoollett-Light force-pushed the poll-timeout branch 4 times, most recently from a921a5d to 749d693 Compare December 10, 2022 13:01
src/poll.rs Outdated

/// Error type for [`PollTimeout::try_from::<i68>()`].
#[derive(Debug, Clone, Copy)]
pub enum TryFromI64Error {
Copy link
Member

Choose a reason for hiding this comment

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

I think TryFromI128Error and TryFromI64Error are exactly the same. Could you use a single error type?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Made some changes to the conversions to remove this error type.

src/poll.rs Outdated
type Error = ();
fn try_from(x: PollTimeout) -> std::result::Result<Self, ()> {
match x.timeout() {
// SAFETY: `x.0` is always positive.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// SAFETY: `x.0` is always positive.
// SAFETY: `x.0` is always nonnegative.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

src/poll.rs Outdated
pub const MAX: Self = Self(i32::MAX);
/// Returns if `self` equals [`PollTimeout::NONE`].
pub fn is_none(&self) -> bool {
*self == Self::NONE
Copy link
Member

Choose a reason for hiding this comment

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

Just to program defensively, I recommend:

Suggested change
*self == Self::NONE
*self <= Self::NONE

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You've right, fixed it.

src/poll.rs Outdated
/// in timeout means an infinite timeout. Specifying a timeout of
/// [`PollTimeout::ZERO`] causes `poll()` to return immediately, even if no file
/// descriptors are ready.
pub fn poll(fds: &mut [PollFd], timeout: PollTimeout) -> Result<libc::c_int> {
Copy link
Member

Choose a reason for hiding this comment

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

I think it would be more usable, and more backwards compatible, to write the signature like this:

Suggested change
pub fn poll(fds: &mut [PollFd], timeout: PollTimeout) -> Result<libc::c_int> {
pub fn poll<T: Into<PollTimeout>>(fds: &mut [PollFd], timeout: T) -> Result<libc::c_int> {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense, made the change.

@JonathanWoollett-Light JonathanWoollett-Light force-pushed the poll-timeout branch 2 times, most recently from 7c63be2 to f11b599 Compare December 11, 2022 08:22
@JonathanWoollett-Light JonathanWoollett-Light force-pushed the poll-timeout branch 2 times, most recently from 3786bff to 81794fe Compare December 18, 2022 13:17
@JonathanWoollett-Light JonathanWoollett-Light force-pushed the poll-timeout branch 2 times, most recently from 297be36 to 0d3316d Compare December 31, 2022 22:34
@JonathanWoollett-Light JonathanWoollett-Light force-pushed the poll-timeout branch 2 times, most recently from 899f471 to 8852303 Compare January 13, 2023 21:54
@JonathanWoollett-Light
Copy link
Contributor Author

BTW, CI is failing due the an unused import:

// test_poll.rs

error: unused import: `close`
 --> test/test_poll.rs:4:14
  |
4 |     unistd::{close, pipe, write},
  |              ^^^^^
  |
  = note: `-D unused-imports` implied by `-D warnings`

error: could not compile `nix` due to previous error

Fixed

src/poll.rs Outdated
!self.is_none()
}
/// Returns the timeout in milliseconds if there is some, otherwise returns `None`.
pub fn timeout(&self) -> Option<i32> {
Copy link
Member

Choose a reason for hiding this comment

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

I think naming this method as_millis would be more idiomatic. For example, it would mirror Duration::as_millis. And since it will never return a negative value, should it return u32 instead of i32?

Copy link
Contributor Author

@JonathanWoollett-Light JonathanWoollett-Light Nov 7, 2023

Choose a reason for hiding this comment

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

I've split this into 2 functions:

    /// Returns the timeout in milliseconds if there is some, otherwise returns `None`.
    pub fn as_millis(&self) -> Option<i32> {
        self.is_some().then_some(self.0)
    }
    /// Returns the timeout as a `Duration` if there is some, otherwise returns `None`.
    pub fn timeout(&self) -> Option<Duration> {
        self.as_millis().map(|x|Duration::from_millis(u64::try_from(x).unwrap()))
    }

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 using i32 or u32 each have their own drawbacks here, ideally we could use u31 with something like https://crates.io/crates/ux but this crate is not actively maintained. I tried my hand at making a better version as an experiment with https://crates.io/crates/ux2 but I am busy so am not able to maintain this alone.

Copy link
Member

Choose a reason for hiding this comment

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

I agree that u31 suits best here, but before switching to the ux or ux2 crate, u32 is the better choice compared to i32

Copy link
Member

Choose a reason for hiding this comment

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

What about this one?

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 am okay to use u32.

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 to u32.

src/poll.rs Outdated
Comment on lines 252 to 336
impl TryFrom<i128> for PollTimeout {
type Error = <i32 as TryFrom<i128>>::Error;
fn try_from(x: i128) -> std::result::Result<Self, Self::Error> {
match x {
// > Specifying a negative value in timeout means an infinite timeout.
i128::MIN..=-1 => Ok(Self::NONE),
millis @ 0.. => Ok(Self(i32::try_from(millis)?)),
}
}
}
impl TryFrom<i64> for PollTimeout {
type Error = <i32 as TryFrom<i64>>::Error;
fn try_from(x: i64) -> std::result::Result<Self, Self::Error> {
match x {
i64::MIN..=-1 => Ok(Self::NONE),
millis @ 0.. => Ok(Self(i32::try_from(millis)?)),
}
}
}
impl From<i32> for PollTimeout {
fn from(x: i32) -> Self {
Self(x)
}
}
impl From<i16> for PollTimeout {
fn from(x: i16) -> Self {
Self(i32::from(x))
}
}
impl From<i8> for PollTimeout {
fn from(x: i8) -> Self {
Self(i32::from(x))
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Actually, accepting negative values for timeout seems to be a Linuxism. POSIX only requires that -1 be interpreted as "forever", and FreeBSD will return EINVAL for other negative values. Please make it impossible to contruct a PollTimeout with a negative value other than -1.

@SteveLauC
Copy link
Member

BTW, we have changed our changelog mode, please see CONTRIBUTING.md on how to add changelog entries

@JonathanWoollett-Light JonathanWoollett-Light force-pushed the poll-timeout branch 7 times, most recently from 058899a to 776aaf1 Compare November 7, 2023 07:58
@JonathanWoollett-Light JonathanWoollett-Light force-pushed the poll-timeout branch 2 times, most recently from 422f03c to a8bcce3 Compare November 16, 2023 18:08
src/poll.rs Outdated Show resolved Hide resolved
src/poll.rs Show resolved Hide resolved
@SteveLauC
Copy link
Member

Left some comments, I think this PR is ready for merge after addressing these comments

@JonathanWoollett-Light JonathanWoollett-Light force-pushed the poll-timeout branch 2 times, most recently from 42bdd0e to 167d4be Compare November 21, 2023 20:37
@SteveLauC
Copy link
Member

The CI failure is not related to this PR

@asomers
Copy link
Member

asomers commented Nov 23, 2023

I can't reproduce the failure. Could github somehow be using a cached Cargo.lock file?

@SteveLauC
Copy link
Member

I can't reproduce the failure. Could github somehow be using a cached Cargo.lock file?

Try this? I can reproduce it even with an empty hello-world crate

@asomers
Copy link
Member

asomers commented Nov 23, 2023

Ahh, so it's a toolchain bug. I thought it was a libc bug. I'll open a bug upstream. In the meantime, we'll either have to disable this target, or pin our rustc version.

@SteveLauC
Copy link
Member

Ahh, so it's a toolchain bug. I thought it was a libc bug. I'll open a bug upstream. In the meantime, we'll either have to disable this target, or pin our rustc version.

I will pin the nightly toolchain to the version just before that

@JonathanWoollett-Light JonathanWoollett-Light mentioned this pull request Nov 23, 2023
3 tasks
@SteveLauC SteveLauC added this pull request to the merge queue Nov 23, 2023
Merged via the queue into nix-rust:master with commit 7e16e32 Nov 23, 2023
34 of 35 checks passed
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.

3 participants