File Descriptor Transfer over Unix Domain Sockets
copyconstruct.medium.com
copyconstruct.medium.com
Al Viro's description sums up one of the recent problems (which was fixed):
Among the features provided by io_uring is the ability to "register" one or more files with an open ring; that speeds I/O operations by eliminating the need to acquire and release references to the registered files every time. When a file is registered with an io_uring, the kernel will create and hold a reference for the duration of that registration. This is a useful feature but it contained a problem that, seemingly, only somebody with a Viro-level understanding of the VFS could spot, describe, and fix; it is a new variant on the cycle problem described above. In short: a process could create a Unix-domain socket and register both ends with an io_uring. If it were then to pass the file descriptor corresponding to the io_uring itself over that socket, then close all of the file descriptors, a cycle would be created. The io_uring code was unprepared for that eventuality.
Many flavours of UNIX have bugs in the area of file descriptor passing. The most recent one I spotted was that FreeBSD would leak file descriptors on the receiving side if the cmsg space was too small to hold all incoming file descriptors numbers.
Another interesting one is that garbage collection of in-flight file descriptors is sometimes implemented by effectively calling close() recursively. This means that you can cause kernel panics due to kernel stack overflows by closing a socket pair that contains in-flight file descriptors of socket pairs that contain in-flight file descriptors of socket pairs that contain in-flight file descriptors of [...]
- not aligning the cmsg buffer
- leaking fds if more fds are received than expected
- blindly dereferencing CMSG_DATA without checking the header
- using CMSG_SPACE(fd_count) instead of CMSG_SPACE(fd_count * sizeof(int))
- using CMSG_SPACE instead of CMSG_LEN for .cmsg_len
- using CMSG_LEN instead of CMSG_SPACE for .msg_controllen
- using a length specified in number of fds instead of bytes
It's possible that Android is uniquely bad at this, but I'm skeptical.However, because in Windows TCP sockets are implemented in the user space, you can't quite use this routine to pass sockets: the part of its state in the user-space won't get duplicated (well, you can't send FILE* into another process in neither Linux nor Windows, right), so there is WSADuplicateSocket function which doesn't actually duplicates the socket, it gives you a struct full of its internal state which you have to send to another process somehow, and then the other process has to re-create the socket manually, by passing this struct into WSASocket call... and I've no idea how the inter-process sharing is then set up, but apparently it is set up, so those two sockets would actually have synchronized state from then on.
Windows TCP sockets are definitely their own thing, which is pretty unfortunate, but Windows exposes a number of other kernel abstractions as handles - processes, mutexes, shared memory sections, events, timers, &c; “Wait for either this particular process to exit or this timer to expire” is trivial. At the time I worked on it, it was one of the features I really preferred about Windows over Unix.
And Win8 added nested jobs, so now you can basically have inescapable process groups with "if the leader process dies, terminate all its children and grandchildren" semantics. On Linux, I had to just give up and use Docker containers for this: some software simply insists on daemonizing itself too much, to the point that on receiving SIGTERM, it re-sends SIGTERM to everyone in its process group (including you, the unsuspecting parent. That was very "funny" to debug).
But of course, the downside of Windows is that the amount of legacy crap and weird quirks and incompatibilities is insane, even more than in x86. "The Windows low-level APIs wouldn't recognize "general" if it showed up in a uniform with stars on the shoulder." (c) njsmith
That is not what's happening. TCP is entirely in the kernel. You are confusing Winsock with TCP.
Even then, Winsock has it's own driver (AFD.sys, which definitely does not stand for Another Fucking Driver) as well.
Most of the user-mode Winsock (ws2_32.dll, mswsock.dll, ws2tcpip.dll) implement the Winsock 2.0 spec-related bookkeeping, but Winsock calls essentially boil down to Ioctl()s to AFD.sys in the common case.
The thing to realize is that Winsock 2.0 was spec-ed at a time when Windows NT (and 95) allowed vendors to install things called "layered service providers", which were providers that hooked into Winsock and allowed you to intercept/redirect/filter various Winsock calls. Lots of internet filters worked in this way in the early days.
Now, given that these 3rd party providers were free to implement their protocol in user space completely, the socket handle you got was not guaranteed to be a kernel handle (Called an IFS handle all over Winsock documentation, for Installable FileSystem handle. )
Thus, WSADuplicateSocket() was a way to allow these providers to re-create a socket based on whatever provider-specific information was passed in through IPC.
Going back to your original claim, you can actually take a native AFD socket handle to a TCP connection and duplicate it (with DuplicateHandle) into another processes. It will also probably work just fine, but no one at Microsoft is going to officially support this usage, for the above-mentioned compatibility reasons, even though Winsock layered providers are officially deprecated now.
Edit: clarified what I meant by duplicate in last paragraph.
And I guess one could still implement an LSP in the user mode while supporting DuplicateHandle directly, without all the WSADuplicateSocket stuff, by having a service with all the custom logic, and returning pipes into this service as socket handles. That's basically what a microkernel would do as well, right?
Haven't thought about microkernels in a couple of decades :), but yes, I can see how what you are saying could work since the handle will technically be a duplicatable kernel handle. It might be better for the service (or subsystem in NT microkernel terminology) to have a driver so that the client can reference it with a proper, dedicated handle, but that's a bit beyond my wheelhouse as to which approach is better.
Edit: Completely forgot about ws2ifsl.sys which is a driver that exists just to give Non-IFS providers a kernel handle with the WPUCreateSocketHandle() call !
Here's the OpenBSD version, which is quite typical - https://man.openbsd.org/CMSG_DATA.3.
It's literally a no-brainer to code it all correctly on the first try. So the scary stuff on your list is exactly of the same nature as using printf() without reading the spec and then calling it "incredibly sharp".
Combined with OS security features like pledge(2) on OpenBSD, which has separate sendfd/recvfd promises, and unveil(2), an unprivileged process can have its access to the filesytem and other system attack surfaces (system calls, ioctls) removed completely or restricted and only be able to act on file descriptors passed by a privileged parent.
https://man.openbsd.org/pledge.2
https://man.openbsd.org/unveil.2
A skeleton example of a common style for OpenBSD privsep daemons, which uses 3 processes
https://github.com/krwesterback/newdctl
https://github.com/krwesterback/newd
This uses OpenBSD's imsg(3) API, an abstraction around the underlying Unix sendmsg/SCM_RIGHTS functionality, along with other IPC abstractions.
https://man.openbsd.org/imsg_init.3
In this case, the process will talk to another process that does have required access, the latter would open the file of interest and pass the handle back.
This is needed very rarely, but in cases where it's a good fit, it provides a very elegant and simple solution for an otherwise hairy problem.
One such case is when the program uses an Engine + UI model, whereby the engine runs under a system account and the UI is under an interactive user. As the engine runs, it writes logs and the UI needs to display them. So one solution is to tweak permissions on the log files to make them universally readable. It's not hard to do, but it makes the whole thing more fragile - these permissions may get inadvertently stripped off, the UI process may be sandboxed by an antivirus, etc. That is, the program may end up in a state when the UI cannot access the logs, but the engine can.
The alternative here is for the UI process to ask the engine to open the logs and pass the handles back. Very simple to do and resistant to accidental breakage.
Another case was when we had to ship a pre-built Linux binary (a VPN client) that needed to open a TAP device. The latter normally requires a root access, but the client was closed-source, so it had to be able to run under restricted user accounts, because asking people to run it under the root was not an option. The solution was to make a small open-source daemon that listened on a domain socket for requests to open /dev/tapx, did that and passed tap FDs back to the requesting process.
I’m transferring the stdin, stdout and stderr file descriptors instead of starting new processes :)
Remember, just handing over your sockets means you get connections in all kinds of different phases of your protocol’s state machine. So now you need to bolt on some more context transfer mechanisms as well...
I have done this for an RTMP server. It worked out pretty well.
The world of QUIC means the kernel is a little too out of the loop for this to easily work.
Assuming it's not Windows, where it is possible to explicitly clone a handle for a specific process.
Great idea!
Now, that being said, this idea, or rather, this specific solution to this specific problem -- is actually a subset of a much broader problem in Computer Science, and that is:
How to move any OS component (up to and including running programs that may have many files, locks, sockets and other shared OS objects open) to another OS on another machine,
without causing any problems!
That is, how to move such entities robustly.
There are various ideas in this field (of which the above paper/article is one) -- but due to the complexities involved, there are no easy answers (at least, not as far as moving whole running programs with many shared OS objects go).
At least one, and probably several experimental OS's have been created in the past which attempt to do this -- but they aren't mainstream, and without doing more research, I'm not sure how robust (which is always a subjective term!) they were...
But, it's a fascinating area of Computer Science, to be sure.
Anyway, great idea and great article in this area!
Here's how we've used this file descriptor transfer feature:
We made a transport which can accept local connections on a Unix Domain Socket and then "upgrades" that connection to two pipes (read and write). Those pipes are passed over the UDS and the client/server communicate over them.
We use a kernel-bypass library (OpenOnload) that implement pipes as shared memory in user-space. Very low latency and high throughput.
We made a `boost::asio` implementation of this available on GitHub. It is old and I'm not sure if it works with latest Boost, but it is quite readable for people to play with. We once bolted it onto Redis' Unix Socket transport for fun, but abandoned it as it was a hassle to maintain.
You can further restrict it using the fact that you can easily verify the PID/UID/GID on the other end of the UNIX socket. (https://man7.org/linux/man-pages/man7/unix.7.html see SO_PEERCRED) You can also manually send your PID/UID/GID (which allows you to specify any of your real-, effective-, or saved set- *ID; or if you're root anything).
The only problem with this is cancellation- you’ll need your client to propagate signals to to forked runner process, as well.
Is there an advantage to passing a file descriptor? What's the workflow in this case, the daemon runs with privileges and passes the fd to a process that's entirely separate and was never started as root? What would passing fds provide that forking + dropping privileges doesn't?
You got the gist of it: you can have a privileged process that can access resources and pass them to an unprivileged process upon request. The unprivileged process runs as nobody and is chroot into an empty directory, so the file descriptors are capabilities. It doesn't really matter if the unprivileged process is a child of the root one, so long as it discards all inherited resources/privileges other than its end of the socket before real work begins.
The advantage over merely inheriting resources is that you can grant new resources later on and decide whether to grant them in the stateful manner.
cmsg.cm.cmsg_len = CMSG_LEN(sizeof(int));
cmsg.cm.cmsg_level = SOL_SOCKET;
cmsg.cm.cmsg_type = SCM_RIGHTS;
[...]
ngx_memcpy(CMSG_DATA(&cmsg.cm), &ch->fd, sizeof(int));
[...]
n = sendmsg(s, &msg, 0);
edit: This article[2] also explains the way the master process sends commands to its workers.[1] https://github.com/nginx/nginx/blob/2a81e0556611188a1b9b3e12...
[2] https://titanwolf.org/Network/Articles/Article?AID=6fb184f9-...