Comment by antonvs

Comment by antonvs a day ago

12 replies

It may be memory safe but it's not using the type system to represent the domain very well.

One could imagine a more type-friendly design in which we could write that first line as follows:

    let socket: Socket<IPv4, Datagram, IcmpV4> = Socket::new()?;
Now, the specifics of socket types will be statically checked.

Edit: I realized that the issue here is actually the conversion, and that UdpSocket on its own is actually a type-safe representation of a UDP socket, not a general datagram socket. But the fact that this dubiously-safe conversion is possible and even useful suggests that an improved design is possible. For example, a method like UdpSocket's `set_broadcast` can't work with a socket like the above, and from a type safety perspective, it shouldn't be possible to call it on such a socket.

scottlamb a day ago

One could, but one probably doesn't want to have separate types for TCP-over-IPv4 vs TCP-over-IPv6 for example, even if they accept/produce different forms of addresses. That'd force a lot of code bloat with monomorphization.

So now one is making one's own enumeration which is different than the OS one and mapping between them, which can get into a mess of all the various protocols Linux and other OSs support, and I'm not sure it's solving a major problem. Opinions vary, but I prefer to use complex types sparingly.

I think there are likely a bunch of other cases where it's useful to choose these values more dynamically too. Networking gets weird!

  • antonvs a day ago

    It's precisely because networking gets weird that a good representation at the type level could be useful. But I agree that it'd need to be done carefully to avoid creating usability issues.

codys 12 hours ago

Rust has an RFC about some of the conventions for the Fd conversions. https://rust-lang.github.io/rfcs/3128-io-safety.html

It's unfortunate they did not extend marking the OwnedFd conversions as unsafe due to the focus in the RFC on a single class of unsafety in Fds instead of having a recognition that there are other issues with arbitrary Fd conversions.

knorker a day ago

> dubiously-safe

No, it's perfectly safe. Except if you expand the scope of "safe" by a lot.

OP turned the socket into an (almost) raw file descriptor, and created an UDP socket from it. Weird, yes, but since it's perfectly memory safe and invalid operations would correctly error, it's not "dubiously-safe". It's safe.

I mean, either your language has the ability to do raw (technically Owned in this case) file descriptors, or it doesn't.

Maybe you'd prefer Rust had a third mode? Safe, `unsafe {}`, and `are_you_sure_you_understand_this {}`, the last one also being 'safe', but just… odd.

  • antonvs 21 hours ago

    It's dubiously safe because it allows invalid combinations, i.e. calling UDP-related methods on non-UDP sockets. I'm using "safe" in the general English sense here, "protected from or not exposed to danger or risk."

    > invalid operations would correctly error

    At runtime, yes. I'm pointing out that Rust makes it possible to do better, and catch such issues at compile time.

    • knorker 21 hours ago

      Of course it allows invalid combinations.

      This also compiles:

          let f = std::fs::File::open("/dev/null").unwrap();
          let f: std::os::fd::OwnedFd = f.into();
          let socket: std::net::UdpSocket = f.into();
      
      If you convert a high level object into a low level one, and then back up as another type, then what exactly do you expect the language to do about that?

      > "protected from or not exposed to danger or risk."

      A computer will do what you tell it to do, not what you intend it to do. Opening a file is way more dangerous than risking errors because "this syscall doesn't work on that fd".

      There's also always risk that a syscall will fail at runtime, whether the type of fd is correct or not.

      It sounds like you would prefer if UdpSocket From<OwnedFD> should run getsockname() or something to confirm it's of the expected type, but I would prefer not. Indeed, in the general case some perfectly coded `unsafe` code could `dup2()` over the fd, so any checking at UdpSocket creation time is moot; you still don't get the safety you are asking for.

      • scottlamb 21 hours ago

        I agree with everything you wrote except for this:

        > Indeed, in the general case some perfectly coded `unsafe` code could `dup2()` over the fd, so any checking at UdpSocket creation time is moot; you still don't get the safety you are asking for.

        If `unsafe` code breaks safe code's soundness guarantees (let's assume for a second an alternate world in which "fd is of the correct type" is a soundness guarantee Rust makes), the bug is in the `unsafe` code.

        • knorker 18 hours ago

          Sure, I would strongly recommend against doing something like that. But I would expect it to work in the obvious way, and not be undefined behavior.

          E.g. if UdpSocket were to dup() internally its fd A into a fd B, and as_fd() returned B, but all actual recv/send is on fd A, then that would cause worse problems than this.

          But say an OS has a sockopt that turns an IPv4 UDP socket into a IPv6 UDP socket. Would it be OK for me to call that on UdpSocket's underlying fd? I'd say yes.

          Now if I closed the fd for a UdpSocket from underneath it, I would expect that to be basically UB, if not by spec, then in practice impossible to reason about.

          As for unsound, sure. I could be convinced of calling the dup2 thing unsound. Not sure unsound is well enough defined, but basically: don't do it.

          Is it unsound to create a UdpSocket from a non-UDP file descriptor? Not in a way that can trigger unsafe, no.

      • antonvs 21 hours ago

        There are two issues here, and you're talking about a different one from the one I'm interested in. Your main issue seems to be this:

        > If you convert a high level object into a low level one, and then back up as another type, then what exactly do you expect the language to do about that?

        One answer to this would be "prevent it entirely". That's probably not practical for a language like Rust today, though, and I don't really care about that.

        What I care about is that it's necessary to do this in the first place. The fact that doing this can be useful and necessary in a case like this suggests that it would be possible to design the types involved so that you don't need these low-level and runtime-unsafe conversions to get the job done.

        > It sounds like you would prefer if UdpSocket From<OwnedFD> should run getsockname() or something to confirm it's of the expected type

        No, I'm saying the types could be designed to prevent the need for doing this in the first place.