Skip to content

Implement network primitives with ideal Rust layout, not C system layout#78802

Merged
bors merged 8 commits intorust-lang:masterfrom
faern:simplify-socketaddr
Jul 31, 2022
Merged

Implement network primitives with ideal Rust layout, not C system layout#78802
bors merged 8 commits intorust-lang:masterfrom
faern:simplify-socketaddr

Conversation

@faern
Copy link
Contributor

@faern faern commented Nov 6, 2020

This PR is the result of this internals forum thread: https://internals.rust-lang.org/t/why-are-socketaddrv4-socketaddrv6-based-on-low-level-sockaddr-in-6/13321.

Instead of basing std:::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6} on system (C) structs, they are encoded in a more optimal and idiomatic Rust way.

This changes the public API of std by introducing structural equality impls for all four types here, which means that match ipv4addr { SOME_CONSTANT => ... } will now compile, whereas previously this was an error. No other intentional changes are introduced to public API.

It's possible to observe the current layout of these types (e.g., by pointer casting); most but not all libraries which were found by Crater to do this have had updates issued and affected versions yanked. See report below.

Benefits of this change

  • It will become possible to move these fundamental network types from std into core (RFC).
  • Some methods that can't be made const fns today can be made const fns with this change.
  • SocketAddrV4 only occupies 6 bytes instead of 16 bytes.
  • These simple primitives become easier to read and uses less unsafe.
  • Makes these types support structural equality, which means you can now (for instance) match an Ipv4Addr against a constant

Remaining Previous problems

This change obviously changes the memory layout of the types. And it turns out some libraries invalidly assumes the memory layout and does very dangerous pointer casts to convert them. These libraries will have undefined behaviour and perform invalid memory access until patched.

Fixed crate versions

All crates I have found that assumed the memory layout have been fixed and published. The crates and versions that will continue working even as/if this PR is merged is (please upgrade these to help unblock this PR):

  • net2 0.2.36
  • socket2 0.3.16
  • miow 0.2.2
  • miow 0.3.6
  • mio 0.7.6
  • mio 0.6.23 - Never had the invalid assumption itself, but has now been bumped to only allow fixed dependencies (net2 + miow)
  • nb-connect 1.0.3
  • quinn 0.5.4
  • quinn 0.6.2

Release notes draft

This release changes the memory layout of Ipv4Addr, Ipv6Addr, SocketAddrV4 and SocketAddrV6. The standard library no longer implements these as the corresponding libc structs (sockaddr_in, sockaddr_in6 etc.). This internal representation was never exposed, but some crates relied on it anyway by unsafely transmuting. This change will cause those crates to make invalid memory accesses. Notably net2 <0.2.36, socket2 <0.3.16, mio <0.7.6, miow <0.3.6 and a few other crates are affected. All known affected crates have been patched and have had fixed versions published over a year ago. If any affected crate is still in your dependency tree, you need to upgrade them before using this version of Rust.

@rust-highfive
Copy link
Contributor

r? @sfackler

(rust_highfive has picked a reviewer for you, use r? to override)

@rust-highfive rust-highfive added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Nov 6, 2020
@Thomasdezeeuw
Copy link
Contributor

Is the reduced size the only argument for this change? I'm opposed to it per say, but it will cost some compute to go from Rust's address to C's address format. I imagine the most common use case for most address types is use in system calls, so are there any benchmarks for this?

Also I maintain both Mio and socket2 so changes those shouldn't be problem, thanks for the heads-up.

@faern
Copy link
Contributor Author

faern commented Nov 6, 2020

Is the reduced size the only argument for this change?

No. There is a list of benefits in the PR description. I think the most asked for is the move to core. Personally I really like the constification.

I'm working on benchmarks. See the internals forums threads linked in the PR description. But my initial results show that any conversion is dirt cheap anyway so it does not really matter.

Copy link
Member

Choose a reason for hiding this comment

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

Why not use a u128 here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Any benefit of doing that? It looks like the value is accessed as an array more often, so this felt like a more natural representation. Also it lowers the memory alignment from 4 to 1.

Copy link
Member

Choose a reason for hiding this comment

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

It is not entirely clear to me that a lowered alignment is actually beneficial. At least given the fact there will now be a fair number of conversions between this type and the libc one when interacting with the system APIs, the alignment of 1 effectively forces (especially on architectures with strict alignment requirements) a memcpy for each such conversion.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you are talking about performance I would argue it's not a valid argument unless proven. As everything regarding optimization. Can you find any usage of this type where it's converted to/from the system representation where said conversion takes up more than a tiny fraction of the time the entire syscall takes?

I'm not saying my benchmarks are perfect. But I have at least tried to check this, and I have not found anything indicating this PR makes anything slower: https://internals.rust-lang.org/t/why-are-socketaddrv4-socketaddrv6-based-on-low-level-sockaddr-in-6/13321/13

I'm not saying alignment 1 is strictly better than 4. Just that I can't find any downsides. And it would be artificial to try to keep it at 4 unless needed, since the most natural representation does not have alignment 4 automatically.

Copy link
Contributor

Choose a reason for hiding this comment

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

If the representation was a u128 then checking if an address is in a prefix like ::ffff/96 (IPv4-mapped addresses) could be efficiently computed with:

let mask: u128 = u128::MAX << (128 - 96);
self.inner & mask == Ipv6Addr::from("::ffff").inner

Still possible if the representation is [u8; 16] using u128::from_be_bytes, but because of the alignment that might require a copy.

Copy link
Contributor

Choose a reason for hiding this comment

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

The soon to be released Go 1.18 includes a whole new IP package called net/netip: https://github.com/golang/go/blob/master/src/net/netip/netip.go

A blogpost on its design can be found here (the package was originally called inet.af/netaddr here, because it was not in the stdlib): https://tailscale.com/blog/netaddr-new-ip-type-for-go/

Importantly for this discussion is that using a pair of uint64 (Go does not have native uint128 support) was quite a bit faster than using an array of bytes. See commit message here for details: inetaf/netaddr@4eb479d

I think it's definitely worth running similar benchmarks here.

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 agree that we should run benchmarks and try to find the best internal representation. But I don't think it should block this PR. Unless we think the PR as is will mean performance regressions I think it brings so many other nice things to the table that it can be ignored for now and iterated upon later. The memory layout of these types are not part of the public API and can change any number of times after this PR is merged.

Copy link
Contributor

Choose a reason for hiding this comment

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

Regarding perf regressions, this is most likely used around syscalls and thus irrelevant. Various Rust APIs like File already heap allocate on syscalls based on same arguments and heap allocations are significantly slower than a few movs here.

@sfackler
Copy link
Member

sfackler commented Nov 6, 2020

This is not going to be able to land for quite a while, FYI.

@jyn514 jyn514 added T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. S-blocked Status: Blocked on something else such as an RFC or other implementation work. labels Nov 6, 2020
@djc
Copy link
Contributor

djc commented Nov 7, 2020

This is not going to be able to land for quite a while, FYI.

Why is that?

@sfackler
Copy link
Member

sfackler commented Nov 7, 2020

Because it will break large swaths of the ecosystem.

@Thomasdezeeuw
Copy link
Contributor

Because it will break large swaths of the ecosystem.

@faern already fixed Mio (tokio-rs/mio#1388) and socket2 (rust-lang/socket2#120), I'll release updates for both next week. Could we run crater after that to test what crates also break?

@faern
Copy link
Contributor Author

faern commented Nov 7, 2020

Also waiting for this to be fixed in net2 (deprecrated/net2-rs#106). But after all of those crates have been fixed and released I'm aiming to make this PR pass the CI.

@faern faern force-pushed the simplify-socketaddr branch from ef7343b to a3c4764 Compare November 11, 2020 14:08
@faern
Copy link
Contributor Author

faern commented Nov 11, 2020

socket2 0.3.16 has been published, with this issue fixed. The first one out in the ecosystem, and also the crate that created CI failures for this PR. This PR now bumps socket2 in the main Cargo.lock (just to see if it helps the CI pass). I'll probably split all such dependency upgrades up to a separate PR later. Because the upgrades should be a no-brainer, but this PR will take a while before it lands. And lockfiles are notoriously conflicty. But I'll wait for net2 and mio to land first.

This repository is a giant workspace with a Cargo.lock at the top. And src/tools/rls is part of this workspace. So it should use the same single lockfile. But rls is also a submodule and its own repository. So there is also a src/tools/rls/Cargo.lock. A bit messy. So I guess each tool in a submodule must have version bumping done to them separately as well.

@Xanewok
Copy link
Contributor

Xanewok commented Nov 11, 2020

@faern only top-level lockfile will be respected when building a workspace member package (which RLS is), and so its lockfile will be effectively ignored when building RLS in the context of this workspace.

I can update RLS' lockfile separately in https://github.com/rust-lang/rls but this should not block anything here.

@faern
Copy link
Contributor Author

faern commented Nov 16, 2020

There is good progress. Out of all the offending crates we have already had this fixed and published in socket2 0.3.16, miow 0.3.6 and mio 0.7.6. By extension this means tokio 0.3 is now on the safe side of things. 🎉

The problem still exists in the tokio 0.2/mio 0.6 ecosystems though. And the vast majority of projects is still using that version and probably will for a long time. To unblock this, we need to get rid of this invalid casting in net2. A deprecated crate, that can hopefully receive some love for this type of issue. According to crates.io, that crate is owned by the rust-lang library team. Would it be possible to have someone from that team bless deprecrated/net2-rs#106? @sfackler @m-ou-se

@CDirkx
Copy link
Contributor

CDirkx commented Nov 17, 2020

Note that the change to #[derive(PartialEq, Eq)] makes the primitives #[structural_match], allowing new code such as:

let ip : Ipv4Addr = ...;

match ip {
  Ipv4Addr::LOCALHOST => { ... },
  _ => { ... }
}

This becomes part of the contract of these primitives, falling under the stability guarantees of the standard library, which is why I propose adding a regression test similar those added when the representation of RangeInclusive was changed and the type became #[structural_match]:

#[test]
fn range_structural_match() {
// test that all range types can be structurally matched upon
const RANGE: Range<usize> = 0..1000;
match RANGE {
RANGE => {}
_ => unreachable!(),
}
const RANGE_FROM: RangeFrom<usize> = 0..;
match RANGE_FROM {
RANGE_FROM => {}
_ => unreachable!(),
}
const RANGE_FULL: RangeFull = ..;
match RANGE_FULL {
RANGE_FULL => {}
}
const RANGE_INCLUSIVE: RangeInclusive<usize> = 0..=999;
match RANGE_INCLUSIVE {
RANGE_INCLUSIVE => {}
_ => unreachable!(),
}
const RANGE_TO: RangeTo<usize> = ..1000;
match RANGE_TO {
RANGE_TO => {}
_ => unreachable!(),
}
const RANGE_TO_INCLUSIVE: RangeToInclusive<usize> = ..=999;
match RANGE_TO_INCLUSIVE {
RANGE_TO_INCLUSIVE => {}
_ => unreachable!(),
}
}

faern added a commit to mullvad/mullvadvpn-app that referenced this pull request Nov 17, 2020
ghedo added a commit to cloudflare/quiche that referenced this pull request Aug 1, 2022
Due to rust-lang/rust#78802 we can't simply copy
the binary representation of `SocketAddr` into `struct sockaddr*` and
vice versa, so implement proper conversions.
@faern
Copy link
Contributor Author

faern commented Nov 10, 2022

This has now been in stable for a full release cycle. Seems to have worked out well. Maybe it's time to move on with the improvements this allows for? For example stabilizing the constructors as const and move the types into core.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. finished-final-comment-period The final comment period is finished for this PR / Issue. merged-by-bors This PR was explicitly merged by bors. relnotes Marks issues that should be documented in the release notes of the next release. S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-infra Relevant to the infrastructure team, which will review and decide on the PR/issue. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.