Description
PR #1504 addresses a few safety and soundness issues with our Sockaddr
type, which all boil down to "We should never create a &T
reference if the entire T isn't initialized". But in reviewing it, I realized another problem: many Nix functions force the user to allocate a struct large enough for any type of sockaddr, even if they know exactly which type they need. For example, nix::sys::socket::bind
takes an enum Sockaddr
as its argument, which can be very large, even though all it does is immediately transform that argument into a pointer. Many Nix users undoubtedly know which type of Sockaddr they need before they call bind, but we force them to allocate a whole enum Sockaddr
. And that begs the question: why use an enum at all? These types are basically already an enum in C. They can all be cast to and from sockaddr and sockaddr_storage, using the s*_family field as a discriminator. So here is my proposal for how to fix the problems raised by #1504 , stop requiring such large allocations, and make the sockaddr types more ergonomic too:
- Create a
SockaddrLike
trait, mostly because for theas_ffi_pair
method. The trait will be sealed, because it requires thesa_family_t
andsa_len
fields to be located at fixed offsets. - Create newtypes for every specific sockaddr type, all with
#[repr(transparent)]
. - Create a Rust union for
SockaddrStorage
. Thanks to thesa_family_t
tag, this union will be safely, falliably convertible to the other sockaddr types. - Change all functions like
bind
to accept a generic parameter of anySockaddrLike
type. - Deprecate
enum Sockaddr
and related functions.
@coolreader18 what do you think of this idea? Here's an outline of the code:
/// Anything that, in C, can be cast back and forth to `sockaddr`.
pub trait SockaddrLike: private::Sealed {
const FAMILY: AddressFamily;
// Unsafe constructor from a variable length source
// Some C APIs from provide `len`, and others do not. If it's provided it
// will be validated. If not, it will be guessed based on the family.
unsafe from_raw(addr: *const libc::sockaddr, len: Option<libc::socklen_t>)
-> Result<Self> {...}
// The next three methods have the same implementation for all concrete
// types, since the trait is sealed! This works because all implementors
// are #[repr(transparent)] and the socklen and sa_family fields are located
// at the same offset for all structs
fn family(&self) -> AddressFamily {...}
fn socklen(&self) -> socklen_t {...}
// Used for many syscalls
fn as_ffi_pair(&self) -> (*const libc::sockaddr, libc::socklen_t) {...}
}
mod private {
pub trait Sealed {}
}
#[repr(transparent)]
pub struct Sockaddr(libc::sockaddr);
impl SockaddrLike for Sockaddr {...}
#[repr(transparent)]
pub struct SockaddrIn(libc::sockaddr_in);
impl SockaddrLike for SockaddrIn {...}
#[repr(C)]
pub union SockaddrStorage {
sa: Sockaddr,
sin: SockaddrIn,
storage: libc::sockaddr_storage,
// And several other types, too
}
impl SockaddrLike for SockaddrStorage {...}
impl SockaddrStorage {
// Safe because it validates all fields
pub fn as_sockaddr_in(&self) -> Result<&SockaddrIn> {...}
}
// Conversions by value from specific to generic are always safe, and will
// usually increase the size of the structure.
impl From<SockaddrIn> for SockaddrStorage {...}
// Conversions from generic to specific are falliable
impl TryFrom<SockaddrStorage> for SockaddrIn {...}
// These functions accept any SockaddrLike input, and do not necessarily require
// allocating any large Enum types.
pub fn bind<T: SockaddrLike>(fd: RawFd, addr: &T) -> Result<()> {...}
pub fn getpeername<T: SockaddrLike>(fd: RawFd) -> Result<T> {...}