Skip to content

Change Sockaddr to a union #1544

Closed
@asomers

Description

@asomers

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 the as_ffi_pair method. The trait will be sealed, because it requires the sa_family_t and sa_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 the sa_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 any SockaddrLike 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> {...}

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions