I am currently trying to write a small client to wayland to get a feel for ipc in rust and window management on linux however I'm getting stuck on step one which is to get an open unix socket to the wayland process. The book I'm reading (https://wayland-book.com) indicates that the process is essentially a couple heuristic, best-effort, attempts:
If WAYLAND_SOCKET is set, interpret it as a file descriptor number on which the connection is already established, assuming that the parent process configured the connection for us.
If WAYLAND_DISPLAY is set, concat with XDG_RUNTIME_DIR to form the path to the Unix socket.
Assume the socket name is wayland-0 and concat with XDG_RUNTIME_DIR to form the path to the Unix socket.
Give up.
2 and 3 are straightforward enough, read env strings, do some concatenations and open the socket etc. etc. but 1 is where I'm having issues. As I read it, a process is to assume that its parent has (if WAYLAND_SOCKET is set) opened a socket that it has then inherited through fork then ???. Operate on it as if it owns it? Get the unix socket path and create a new connection?
The book seems a little hazy on this but, in the interest of creating safe programs, using a file descriptor that is also owned by another process that was potentially written by another party seems... unwise. In that scenario I would go the second route of creating my own connection with the path but that too is causing me issues.
I frankly am not understanding how to safely go from a file descriptor number passed as a string to a path to a unix socket. Technically I could attempt a readlink on /proc/self/fd/$WAYLAND_SOCKET but this seems more to me like sidestepping my poor understanding of rust's fd facilities and to add on to that I would be leaving a dangling file descriptor inherited from a parent process which, tbh, I'm not even sure it's my responsibility or in my best interest to close anyway as I may want to have a single process open multiple connections to the wayland process and that initial inherited fd would still need to be valid for that.
I'm completely lost on this one so any info, even if its tangential to the topic, would be appreciated.
I don't see the issue here, I suspect you may have an incorrect mental model of file descriptors. They are duplicated when forking, so you get a copy referring to the same underlying kernel object (inode on file system, pipe, socket, etc) and that kernel object is reference counted. As such, if you close your copy you won't affect the parent's copy. (Obviously, some operations will affect all copies, such as writing, truncating a file, etc.)
Again, you have a copy (and can do what you want with that file descriptors, including copying it with the dup2 system call). Any open file descriptors at the point you exit your process will be cleaned up by the kernel.
Obviously, some operations will affect all copies, such as writing, truncating a file, etc.)
Thats exactly the issue i'm worried about. Concurrent writes from the parent process could affect invariants in a child process; I'd like to avoid that if possible. I get my process has a unique descriptor but the description is the thing im trying to get a unique instance of, ideally without unsafe blocks.
I'd like to avoid unsafe blocks if possible. Also AFAIK the resulting type from from_raw_fd will close the descriptor when its dropped which I also prefer to avoid because it would complicate multiple connection::build calls because I would have to
have a static global guard to prevent the connection initialization code from creating a second droppable object referencing the same descriptor or
wipe the WAYLAND_SOCKET environment variable (which I dont think I can actually properly do in rust)
Either way this plays far to much with memory safety or global state for my liking and makes the connection initialization code harder to test as its not pure/idempotent.
The socket you are given is one socket, one connection. I’m not familiar with the Wayland protocol, but it is almost certainly the case that you cannot meaningfully make use of the socket independently because that would corrupt the communication. You need to to up your client data structure just once, and have at least a static flag preventing it from being done twice. This is not poor engineering; it’s an inherent property of the FD-passing scheme you’ve been given to implement.
Either way this plays far to much with memory safety or global state for my liking and makes the connection initialization code harder to test as its not pure/idempotent.
Testing of your connection code other than the WAYLAND_SOCKET part should be done using sockets that aren’t passed this way.
Testing of the WAYLAND_SOCKET part of your code should be done via an integration test that starts a child process and confirms that the child process responds to the protocol.
I’m not familiar with the Wayland protocol, but it is almost certainly the case that you cannot meaningfully make use of the socket independently because that would corrupt the communication.
As far as I understand of the wayland protocol there is nothing a parent process can do for a child process (other than identifying the correct unix socket path and initializing a socket from that) which is in any way useful to the child process but it can cause issues by simultaneously sending on that connection and causing state changes on the connection with the wayland server without the child process knowing; hence my hesitance to just use the inherited socket as if my process owns it.
You need to to up your client data structure just once, and have at least a static flag preventing it from being done twice.
The way the protocol works is over a plain old unix socket though, realistically a process should be able to create several connections to a wayland server up to the max process wide socket limit and run those sessions concurrently. Implementing a process singleton like this forgoes that ability from the start.
I am aware of at least getpeername.2 for retrieving the path of named unix sockets which you could then create a new socket instance off of to achieve the functionality I'm thinking of but im unsure of how to use that function safely. In the case of inheriting an unnamed unix socket this would obviously fail but that still seems better than making initialization of this connection a singleton.
Testing of the WAYLAND_SOCKET part of your code should be done via an integration test that starts a child process and confirms that the child process responds to the protocol.
If you have any crate suggestions on this it would be appreciated as it seems like fork isn't std supported yet and iirc sockets aren't inherited through exec calls
That doesn't make sense. There is no shared state when a file descriptor is duplicated. Yes, the underlying kernel object can have state, which is most noticeable for files. But for a unix domain socket there is nothing of interest. You will share just as much state if you open the socket from the file system yourself.
Duplicating a file descriptor is no different than opening the same underlying object twice. This is true for files, sockets, pipes, etc.
That depends on what flags you set on the file descriptor. (This is how you can get a socket from your parent in the first place.) That can either be set on FD creation ( open has O_CLOEXEC for example) or later via fcntl (FD_CLOEXEC), the latter is racy though if you have several threads (if the fork happens on another thread in between open and fcntl).
As far as I understand of the wayland protocol there is nothing a parent process can do for a child process (other than identifying the correct unix socket path and initializing a socket from that) which is in any way useful to the child process but it can cause issues by simultaneously sending on that connection and causing state changes on the connection with the wayland server without the child process knowing; hence my hesitance to just use the inherited socket as if my process owns it.
I think this may be a bit too extreme of a response. In Unix land the parent is responsible for providing a lot of resources to the child and can easily set up to interfere with the child: It could always give you a misdirected socket name and man-in-the-middle it to inject its own data. You have to trust that the parent is not going to mess with the resources it gives you.
This kind of sounds like a specific case of the FFI problem in general: if read() is documented to write to the given buffer up to the returned length, is it ok for my wrapping code to assume that for the safety requirement of returning a [u8]? Is it ok because it's the operating system? What about if it's a graphics API implemented by a third-party kernel driver?
As far as I know, the assumption has to be made that external APIs basically just have to be trusted as far as they are documented to be trusted (eg you can trust a network socket read returns bytes, but not what those bytes are). Unsafe is a safety mechanism, not actually a security mechanism, after all.
Perhaps there are more security (capability?) oriented platforms that make this assumption less tenable, but while I don't know what they look like, I'm pretty sure they at least wouldn't let an untrustworthy process have this sort of ability to mess with another.
The communication protocol between a Wayland client and server is a stateful connection with state tied to a specific connection though. Which means that if the child and parent process were to allocate separate objects with the same id over that shared connection those two objects would collide causing UB and corrupted state for both processes.
Fair point, this is a bit in the weeds on specifics at this point, however I don't have to rely on the parent processes' handed down connection; I know I can readlink the relevant /proc/self/fd file and open a new connection. I was kind of hoping there would be a more rusty way of doing that though which wouldn't revolve around platform specific file reads and libc reliance.
True, half of the issue is an FFI problem, namely the reliance on an arbitrary file descriptor being fully correct and valid; If I have to accept that as a fact of life so be it. But there's also the other half of that problem which std::io specifically calls out as not only unsafe but unsound:
AFAIK rust does not provide facilities to turn an inherited file descriptor into a std::os::fd object, owned or otherwise, without propagating unsafe blocks every time I want to resize a window, or draw a frame.
While I can have a 'static flag that prevents double acquisition of that file descriptor other libraries or even user code won't respect it. And lets say for arguments sake that I create that descriptor exactly as outlined above, dup it, then std::mem::forget it as a sort of "safe" way of doing this, technically speaking another thread could swoop in, do the same thing faster (without the dup and forget), and close the sole descriptor my process owned before (and without interrupting) my thread unsafely creating its descriptor. My thread would then be holding a completely invalid descriptor object pointing to nothing assuming all was well.
Now obviously this is a contrived example with no chance of happening unless it was intentionally done but still, I would have thought that that something like fd inheritance would have a better API for something done so commonly on *nix systems.
That doesn’t actually help you very much, though. It protects you against one specific kind of misbehavior of the parent process, but you do not gain any benefit from singling out that particular misbehavior over any other variety of "the parent process gave you a FD that is not a well-behaved connection to a well-behaved Wayland server". And it’s a complication, and arguably a deviation from the Wayland protocol, and it adds more failure points (what if the socket doesn't have a path on the filesystem that you have permission to read).
In the end, your program has to trust that its parent process set it up for success and not failure.
The general model there is that the unsafe from_fd method is unsafe partially because you're promising the fd won't get used in that way, and you can promise that by the assumption provided by the documented API that a correctly implemented parent won't do that (at least, presumably)
When this is from the OS or a packaged C library, this is a pretty safe assumption to make, but random user space libraries do make it feel a lot worse, I agree.
You certainly shouldn’t do it that way. The whole point of unsafely converting the FD into an owning wrapper type is that after that conversion, you can interact with the FD using safe code. The unsafe premise that the inherited FD is used only that way (and is the right kind of FD) is localized to the point where the conversion is done.
lol yeah, I imagine it would be like developing in typescript and only using Any. I guess a ///Safety comment will have to do
That would be nice. Maybe you could write such a library!
One already exists! It has user code run a setup function at the start of the program though, so not completely infallible and very much not portable or standardized because it relies specifically on /proc/<pid>/fd/*