mpd/src/event/SocketEvent.cxx

107 lines
2.4 KiB
C++
Raw Normal View History

2023-03-12 19:57:20 +01:00
// SPDX-License-Identifier: BSD-2-Clause
// author: Max Kellermann <max.kellermann@gmail.com>
#include "SocketEvent.hxx"
#include "Loop.hxx"
#include "event/Features.h"
#include <cassert>
2019-07-05 09:59:00 +02:00
#include <utility>
#ifdef USE_EPOLL
#include <cerrno>
#endif
void
SocketEvent::Open(SocketDescriptor _fd) noexcept
{
assert(_fd.IsDefined());
assert(!fd.IsDefined());
assert(GetScheduledFlags() == 0);
fd = _fd;
}
void
SocketEvent::Close() noexcept
{
if (!fd.IsDefined())
return;
2020-12-04 09:55:01 +01:00
/* closing the socket automatically unregisters it from epoll,
so we can omit the epoll_ctl(EPOLL_CTL_DEL) call and save
one system call */
event/SocketEvent: remove FD before closing socket SocketEvent knows the FD is still open and is about to close it, so it's unnecessary to rely on the kernel (via AbandonFD) to clean up the epoll_wait list. ### Why this is relevant - `AbandonFD` assumes that upon closing the socket, the FD will be automatically removed from the epoll list. That fd is associated with a reference to the `SocketEvent`, so this is an important and dangerous assumption to get wrong. In the case that the FD isn't immediately removed from the list by the kernel, the event loop can crash due to the `SocketEvent` being destroyed and it being a use-after-free bug at that point. - If a socket FD happens to be duplicated, then closing the SocketEvent FD will not automatically remove it from epoll, and will trigger said bug/crash. It is only automatically removed when all FD references to the underlying socket/resource are closed? - A `fork()` is one example where a socket FD can be duplicated and result in this situation. - `CLOEXEC` might be considered mitigation for this but also introduces a race condition where the crash can occur between a `fork()` and `exec()` without additional synchronization to freeze the event loop. One could argue the mpd event loop isn't fork-safe, and thus should be allowed to use `AbandonFD` however it likes. A decision on whether this is intended should probably be declared; but either way this fix seems appropriate in cases where `Abandon` isn't actually necessary. It also might be possible to fix `AbandonFD` to mark the `SocketEvent` as removed without using `EPOLL_CTL_DEL`? [edit: made this dependent on HAVE_THREADED_EVENT_LOOP which is always true for MPD, but not for ncmpc, for example - mk]
2020-11-09 19:50:54 +01:00
if (std::exchange(scheduled_flags, 0) != 0) {
#ifdef HAVE_THREADED_EVENT_LOOP
/* can't use this optimization in multi-threaded
programs, because all file descriptors get
duplicated in forked processes, leaving them
registered in epoll, which could cause the parent
to crash */
loop.RemoveFD(fd.Get(), *this);
#else
loop.AbandonFD(*this);
event/SocketEvent: remove FD before closing socket SocketEvent knows the FD is still open and is about to close it, so it's unnecessary to rely on the kernel (via AbandonFD) to clean up the epoll_wait list. ### Why this is relevant - `AbandonFD` assumes that upon closing the socket, the FD will be automatically removed from the epoll list. That fd is associated with a reference to the `SocketEvent`, so this is an important and dangerous assumption to get wrong. In the case that the FD isn't immediately removed from the list by the kernel, the event loop can crash due to the `SocketEvent` being destroyed and it being a use-after-free bug at that point. - If a socket FD happens to be duplicated, then closing the SocketEvent FD will not automatically remove it from epoll, and will trigger said bug/crash. It is only automatically removed when all FD references to the underlying socket/resource are closed? - A `fork()` is one example where a socket FD can be duplicated and result in this situation. - `CLOEXEC` might be considered mitigation for this but also introduces a race condition where the crash can occur between a `fork()` and `exec()` without additional synchronization to freeze the event loop. One could argue the mpd event loop isn't fork-safe, and thus should be allowed to use `AbandonFD` however it likes. A decision on whether this is intended should probably be declared; but either way this fix seems appropriate in cases where `Abandon` isn't actually necessary. It also might be possible to fix `AbandonFD` to mark the `SocketEvent` as removed without using `EPOLL_CTL_DEL`? [edit: made this dependent on HAVE_THREADED_EVENT_LOOP which is always true for MPD, but not for ncmpc, for example - mk]
2020-11-09 19:50:54 +01:00
#endif
}
fd.Close();
}
2020-10-15 16:59:27 +02:00
void
SocketEvent::Abandon() noexcept
{
if (std::exchange(scheduled_flags, 0) != 0)
loop.AbandonFD(*this);
2020-10-15 16:59:27 +02:00
fd = SocketDescriptor::Undefined();
}
bool
SocketEvent::Schedule(unsigned flags) noexcept
{
if (flags != 0)
flags |= IMPLICIT_FLAGS;
if (flags == GetScheduledFlags())
return true;
assert(IsDefined());
bool success;
if (scheduled_flags == 0)
success = loop.AddFD(fd.Get(), flags, *this);
else if (flags == 0)
success = loop.RemoveFD(fd.Get(), *this);
else
success = loop.ModifyFD(fd.Get(), flags, *this);
if (success)
scheduled_flags = flags;
#ifdef USE_EPOLL
else if (errno == EBADF || errno == ENOENT)
/* the socket was probably closed by somebody else
(EBADF) or a new file descriptor with the same
number was created but not registered already
(ENOENT) - we can assume that there are no
scheduled events */
/* note that when this happens, we're actually lucky
that it has failed - imagine another thread may
meanwhile have created something on the same file
descriptor number, and has registered it; the
epoll_ctl() call above would then have succeeded,
but broke the other thread's epoll registration */
scheduled_flags = 0;
#endif
return success;
}
void
SocketEvent::Dispatch() noexcept
{
const unsigned flags = std::exchange(ready_flags, 0) &
GetScheduledFlags();
if (flags != 0)
callback(flags);
}