Skip to content

Fix false positive GSO detection - #2248

Merged
djc merged 20 commits into
quinn-rs:mainfrom
inetic:samsung-issue
May 23, 2025
Merged

Fix false positive GSO detection#2248
djc merged 20 commits into
quinn-rs:mainfrom
inetic:samsung-issue

Conversation

@inetic

@inetic inetic commented May 17, 2025

Copy link
Copy Markdown
Contributor

On some devices with kernel <4.18 the existing checks for GSO not being enabled on the system don't trigger. Quinn then continues to use GSO which results in eventual disconnection as discussed in issue #2246.

This PR adds a high level test to showcase the disconnection and a runtime check based on parsing Linux kernel version string to disable GSO in quinn-udp on kernels where it hasn't yet been implemented (4.18 torvalds/linux@cb586c6).

@thomaseizinger thomaseizinger left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Left a few comments with food for thought!

Comment thread quinn/tests/send_receive.rs Outdated
Comment thread quinn-udp/src/unix.rs Outdated
const UDP_SEGMENT: libc::c_int = 103;

// Global mutex to avoid calling `libc::uname` for each socket.
static SUPPORTED_BY_OS: Mutex<Option<bool>> = Mutex::new(None);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Perhaps a OnceLock is more convenient here?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We can then get rid of the KernelVersion struct and have a stateless function that checks if the version is < 4.18

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think we can use OnceLock for MSRV reasons, but we should be able to use once_cell?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We can then get rid of the KernelVersion struct and have a stateless function that checks if the version is < 4.18

I had the opposite plan to expand the KernelVersion struct by further spliting the parsing code into KernelVersion::from_str which would be called from KernelVersion::from_uname and write small tests with sting samples. The test could be a way of tracking if some kernels have inconsistent version strings as you noted here #2246 (comment).

This wouldn't be possible with a single function returning a boolean.

Let me know which way you prefer.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This wouldn't be possible with a single function returning a boolean.

You can split it into multiple functions and test the pure ones separately:

  • 1 function that calls libc::uname and returns a string
  • 1 function that checks if a given string is a kernel version > 4.18
  • 1 function that combines the above too or perhaps just do that inline in the closure passed to once_cell

The 2nd one can then be easily unit-tested with multiple inputs.

I don't have a strong preference but if we can avoid intermediary structs, I think it is easier to understand.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't think we can use OnceLock for MSRV reasons,

Running cargo msrv find I get the result 1.74.1 (log: msrv-find.log). OnceLock seems to be supported since 1.70.0. So unless there is a plan to decrease the current MSRV I think OnceLock should be OK to use.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah, okay, good.

@inetic inetic May 19, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't have a strong preference but if we can avoid intermediary structs, I think it is easier to understand.

I did modify the code a bit, though I left the KernelVersion struct there. The reason was that I felt that the tests would be more to the point if they tested the parsing alone instead of parsing and checking >=4.18. It might be a matter of taste/style, if so, then perhaps it'd be better to leave that part to you (the code owners)?

Comment thread quinn-udp/src/unix.rs Outdated
Comment thread quinn-udp/src/unix.rs Outdated
Comment thread quinn-udp/src/unix.rs Outdated
@djc

djc commented May 19, 2025

Copy link
Copy Markdown
Member

(Please rebase to get rid of the merge commit.)

@inetic

inetic commented May 19, 2025

Copy link
Copy Markdown
Contributor Author

One outstanding question to resolve is whether to enable GSO if uname or the parsing fails, as mentioned here #2246 (comment). There are pros and cons:

Fallback is to enable:

  • pros: fast bandwidth and low cycles on modern kernels
  • cons: broken connections on old kernels

Fallback is to disable

  • pros: connections work on old kernels
  • cons: slow bandwidth and high cycles on modern kernels

@thomaseizinger thomaseizinger left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nice work, looks great! I left a few comments, nothing blocking :)

Comment thread quinn-udp/src/unix.rs Outdated
Comment thread quinn-udp/src/unix.rs Outdated
Comment thread quinn-udp/src/unix.rs Outdated
Comment thread quinn-udp/src/unix.rs Outdated
Comment thread quinn-udp/src/unix.rs Outdated
Comment thread quinn-udp/src/unix.rs Outdated
@Ralith

Ralith commented May 19, 2025

Copy link
Copy Markdown
Collaborator

whether to enable GSO if uname or the parsing fails

Are we aware of any cases where these could fail with a modern kernel? Especially uname failing suggests to me that we'd be in a very exotic environment where it might be better to be conservative, although maybe syscall filtering could cause that?

@thomaseizinger

thomaseizinger commented May 19, 2025

Copy link
Copy Markdown
Collaborator

whether to enable GSO if uname or the parsing fails

Are we aware of any cases where these could fail with a modern kernel? Especially uname failing suggests to me that we'd be in a very exotic environment where it might be better to be conservative, although maybe syscall filtering could cause that?

I am not aware of any cases. The idea of falling back to GSO enabled is that in the rare(?) case of an error, this doesn't cause a performance regression by disabling GSO.

Conservative in my eyes means just that: Preserving the current behaviour of quinn-udp.

My guess would be that quinn-udp is predominantely run on newer Linux kernels so defaulting to true seems like the better option. If it does indeed fail on an older kernel, the user will see it in the logs and can report a bug.

Comment thread quinn-udp/src/unix.rs Outdated
pub(crate) const BATCH_SIZE: usize = 1;

#[cfg(any(target_os = "linux", target_os = "android"))]
mod linux {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: there's no point in making a separate module for this, just move the contents of this into the existing Linux gso module. Try to keep the additions in top-down order, so callers come before callees.

Comment thread quinn-udp/src/unix.rs Outdated

let version = split
.next()
.and_then(|s| s.parse().ok())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: please use u8::from_str() instead of .parse(), and avoid capitalizing error messages.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

use u8::from_str() instead of .parse()

This will require use std::str::FromStr;, are you ok with it?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yes; that's the same stuff that parse() uses.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

avoid capitalizing error messages

Also for "GSO" in "GSO disabled:..."?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sorry, no -- just the first letter.

@djc

djc commented May 20, 2025

Copy link
Copy Markdown
Member

Thanks for working on this!

@inetic

inetic commented May 21, 2025

Copy link
Copy Markdown
Contributor Author

Thanks for working on this!

Thanks for all the feedback :)

@djc djc left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A bunch of style feedback, I think we're close!

Comment thread quinn-udp/src/unix.rs Outdated
Comment on lines +894 to +899
const fn new(version: u8, major_revision: u8) -> Self {
Self {
version,
major_revision,
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: let's drop this trivial method.

Comment thread quinn-udp/src/unix.rs Outdated
}

fn from_str(release: &str) -> Result<Self, String> {
use std::str::FromStr;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: import this at the top of the module.

Comment thread quinn-udp/src/unix.rs Outdated
}
}

fn from_str(release: &str) -> Result<Self, String> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: suggest returning Option<Self> from this.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

That is what we originally had and I suggested returning an error to consolidate the places where we log what went wrong :)

@thomaseizinger thomaseizinger May 21, 2025

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I guess as long as we print the original string returned from uname as part of the WARN log, we can always reproduce it if this gets hit in the wild.

(Our app is instrumented with crash reporting so we get sent WARNs and ERRORs automatically.)

Comment thread quinn-udp/src/unix.rs
Comment on lines +904 to +908
let mut split = release
.split_once('-')
.map(|pair| pair.0)
.unwrap_or(release)
.split('.');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

... then this can be release.split_once('-')?.0.split('.').

@inetic inetic May 22, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The .unwrap_or(release) part is there to avoid failing on strings which don't have "-", like "4.18" for example. I don't know whether such release strings exist in real life, but might be good to keep it just in case?

On the other hand if we go with a "let's not worry about what we don't know" approach, then we could get rid of the .split_once('-') part all together because the formats that I've seen so far were always "X.Y.Z-...", for which let mut split = release.split('.'); would suffice.

Comment thread quinn-udp/src/unix.rs Outdated
.unwrap_or(release)
.split('.');

let version = split

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

And this can be u8::from_str(split.next()?).ok()?.

Comment thread quinn-udp/src/unix.rs Outdated
Comment on lines +915 to +918
let major_revision = split
.next()
.and_then(|s| u8::from_str(s).ok())
.ok_or_else(|| format!("failed to parse kernel major revision from {release:?}"))?;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

And similar here.

Comment thread quinn-udp/src/unix.rs Outdated
return Err(r);
}
Ok(unsafe {
std::ffi::CStr::from_ptr(n.release[..].as_ptr())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: import CStr at the top of the module, and mem too.

Comment thread quinn-udp/src/unix.rs Outdated
fn supported_by_current_kernel() -> bool {
let kernel_version_string = match kernel_version_string() {
Ok(kernel_version_string) => kernel_version_string,
Err(_errno) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why do these need _ prefixes?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

When the log macros are configured out (no features active), they'd otherwise show up as unused variables.

Comment thread quinn-udp/src/unix.rs Outdated
// https://github.com/torvalds/linux/commit/cb586c63e3fc5b227c51fd8c4cb40b34d3750645
const SUPPORTED_SINCE: KernelVersion = KernelVersion::new(4, 18);
// Avoid calling `supported_by_current_kernel` for each socket by using `OnceLock`.
static SUPPORTED_BY_CURRENT_KERNEL: OnceLock<bool> = OnceLock::new();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: move SUPPORTED_BY_CURRENT_KERNEL below set_segment_size() (it should be below max_gso_segments() and above supported_by_current_kernel()).

Comment thread quinn-udp/src/unix.rs Outdated
Co-authored-by: Thomas Eizinger <thomas@eizinger.io>

@mxinden mxinden left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One comment, otherwise looks good to me.

Thank you for the work!

Comment thread quinn-udp/src/unix.rs
Comment on lines +878 to +881
let r = unsafe { libc::uname(&mut n) };
if r != 0 {
return Err(r);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If I understand libc's uname correctly, the return value is either 0 or -1. On -1 the actual error code can be retrieved via errno:

On success, zero is returned. On error, -1 is returned, and errno
is set to indicate the error.

https://man7.org/linux/man-pages/man2/uname.2.html

Would it make sense to retrieve the actual error code here?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@djc djc left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Okay, going to merge this and do some follow up. Thanks for all the iterations!

@djc
djc merged commit 81f9cd9 into quinn-rs:main May 23, 2025
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.

5 participants