Fix false positive GSO detection - #2248
Conversation
thomaseizinger
left a comment
There was a problem hiding this comment.
Left a few comments with food for thought!
| 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); |
There was a problem hiding this comment.
Perhaps a OnceLock is more convenient here?
There was a problem hiding this comment.
We can then get rid of the KernelVersion struct and have a stateless function that checks if the version is < 4.18
There was a problem hiding this comment.
I don't think we can use OnceLock for MSRV reasons, but we should be able to use once_cell?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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::unameand 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)?
|
(Please rebase to get rid of the merge commit.) |
The issue on Android with Linux kernel 4.14 is more directly reproducible by the `gso` test.
|
One outstanding question to resolve is whether to enable GSO if Fallback is to enable:
Fallback is to disable
|
thomaseizinger
left a comment
There was a problem hiding this comment.
Nice work, looks great! I left a few comments, nothing blocking :)
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 My guess would be that |
| pub(crate) const BATCH_SIZE: usize = 1; | ||
|
|
||
| #[cfg(any(target_os = "linux", target_os = "android"))] | ||
| mod linux { |
There was a problem hiding this comment.
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.
|
|
||
| let version = split | ||
| .next() | ||
| .and_then(|s| s.parse().ok()) |
There was a problem hiding this comment.
Nit: please use u8::from_str() instead of .parse(), and avoid capitalizing error messages.
There was a problem hiding this comment.
use u8::from_str() instead of .parse()
This will require use std::str::FromStr;, are you ok with it?
There was a problem hiding this comment.
Yes; that's the same stuff that parse() uses.
There was a problem hiding this comment.
avoid capitalizing error messages
Also for "GSO" in "GSO disabled:..."?
There was a problem hiding this comment.
Sorry, no -- just the first letter.
|
Thanks for working on this! |
Thanks for all the feedback :) |
djc
left a comment
There was a problem hiding this comment.
A bunch of style feedback, I think we're close!
| const fn new(version: u8, major_revision: u8) -> Self { | ||
| Self { | ||
| version, | ||
| major_revision, | ||
| } | ||
| } |
There was a problem hiding this comment.
Nit: let's drop this trivial method.
| } | ||
|
|
||
| fn from_str(release: &str) -> Result<Self, String> { | ||
| use std::str::FromStr; |
There was a problem hiding this comment.
Nit: import this at the top of the module.
| } | ||
| } | ||
|
|
||
| fn from_str(release: &str) -> Result<Self, String> { |
There was a problem hiding this comment.
Nit: suggest returning Option<Self> from this.
There was a problem hiding this comment.
That is what we originally had and I suggested returning an error to consolidate the places where we log what went wrong :)
There was a problem hiding this comment.
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.)
| let mut split = release | ||
| .split_once('-') | ||
| .map(|pair| pair.0) | ||
| .unwrap_or(release) | ||
| .split('.'); |
There was a problem hiding this comment.
... then this can be release.split_once('-')?.0.split('.').
There was a problem hiding this comment.
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.
| .unwrap_or(release) | ||
| .split('.'); | ||
|
|
||
| let version = split |
There was a problem hiding this comment.
And this can be u8::from_str(split.next()?).ok()?.
| 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:?}"))?; |
| return Err(r); | ||
| } | ||
| Ok(unsafe { | ||
| std::ffi::CStr::from_ptr(n.release[..].as_ptr()) |
There was a problem hiding this comment.
Nit: import CStr at the top of the module, and mem too.
| fn supported_by_current_kernel() -> bool { | ||
| let kernel_version_string = match kernel_version_string() { | ||
| Ok(kernel_version_string) => kernel_version_string, | ||
| Err(_errno) => { |
There was a problem hiding this comment.
When the log macros are configured out (no features active), they'd otherwise show up as unused variables.
| // 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(); |
There was a problem hiding this comment.
Nit: move SUPPORTED_BY_CURRENT_KERNEL below set_segment_size() (it should be below max_gso_segments() and above supported_by_current_kernel()).
Co-authored-by: Thomas Eizinger <thomas@eizinger.io>
mxinden
left a comment
There was a problem hiding this comment.
One comment, otherwise looks good to me.
Thank you for the work!
| let r = unsafe { libc::uname(&mut n) }; | ||
| if r != 0 { | ||
| return Err(r); | ||
| } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
djc
left a comment
There was a problem hiding this comment.
Okay, going to merge this and do some follow up. Thanks for all the iterations!
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-udpon kernels where it hasn't yet been implemented (4.18 torvalds/linux@cb586c6).