tcpdump
tcpdump copied to clipboard
do less work in signal handlers
Signal handlers should at most do atomic sets of flags, and our various calls to info() are much larger than is typically considered safe. This issue is to keep track of the need to not call info() from signal handlers, but instead set flags.
So, at least in theory, the way to do this is to have the signal handlers 1) set flags and 2) call pcap_breakloop().
pcap_breakloop() dates back to libpcap 0.8, so if we're willing to kick systems running libpcap 0.7 and earlier to the curb, we can get rid of the #ifdefs checking for its availability, have the configure script and CMakeLists.txt fail if it's not found, and just assume it's present in the tcpdump code.
The problem is that pcap_breakloop() may delay the actual "breaking out of the loop".
The first problem is that it might not wake up pcap_loop() or pcap_dispatch() if it's blocked in a system call waiting for packets to arrive. If we call it in a signal handler, as is the case in tcpdump on UN*Xes, the signal should itself interrupt the system call, and the loop in libpcap should check the flag set by pcap_breakloop() and break out of the loop. So, while the general problem with pcap_breakloop() still exists on some platforms, even with the changes for Linux and Windows in libpcap 1.10, that's an issue for pcap_breakloop() being called from a separate thread rather than from an interrupt handler.
It is called from a separate thread on Windows (that's how console ^C handlers work on Windows), but 1) the timeout in WinPcap and Npcap happens to occur even if no packets have arrived, and that timeout is at most 1 second in tcpdump, and 2) in libpcap 1.10, pcap_breakloop() explicitly posts the event that's being waited for, so you don't have to rely on the timeout.
So that problem isn't an issue for tcpdump, which is the program being discussed here.
The other problem is that if pcap_breakloop() is called, in a signal handler or other thread, before libpcap calls the blocking system call but after it's checked the internal "break out of the loop" flag, libpcap will go ahead and make the call.
If it's called in a signal handler, the signal has already been delivered, and won't interrupt the system call. At best, that can delay the response to the signal by 1 second. At worst, it can delay it indefinitely, if the timeout doesn't fire unless a packet is available, as is the case on Linux.
With libpcap 1.10, as noted, on Linux and Windows, an event will be posted by pcap_breakloop(); posting the event will not only cause a system call blocked waiting for packets to wake up, it'll also cause a future system call not to block waiting for packets. So, at least with libpcap 1.10, that won't be an issue on Linux or Windows.
On *BSD and macOS, however, it is an issue. A workaround would be to have pcap_breakloop() temporarily set the timeout on the BPF device to 1 microsecond; that means that the read() will block, but only for one microsecond at most. Few users will notice that delay. :-)
One problem with setting the timeout in pcap_breakloop() is that, while researching the BPF driver code path on those OSes, I found that, apparently, if one thread is doing a select()/poll()/kqueue call waiting on a BPF device, and another thread makes a BPF ioctl, the timer that newer versions of all those OSes start when a select()/poll()/kqueue call is made on a BPF device, so that the packet buffer timeout fires so that the call eventually says the BPF device is readable even if its buffer isn't full, gets canceled, so that the call can block indefinitely.
I'm in the process of testing for that bug on the latest versions of various *BSDs (I've already reproduced it on macOS and NetBSD 9.0), and will file bugs.
Until those bugs are fixed, we probably shouldn't have pcap_breakloop() temporarily set the timeout; however, as, in tcpdump, we know that it's not being called from another thread, and we don't do select()/poll()/kqueue calls, we could do the ioctl in the signal handlers.
@guyharris This has been solved with ppoll/epoll_pwait and friends. The solution is to block all signals except while waiting for data. Without ppoll you can use the self-pipe trick, but I don't think libpcap supports such old systems without ppoll.
First setup masks:
sigset_t mask, oldmask;
struct sigaction act = {};
act.sa_handler = signal_handler;
act.sa_flags = SA_RESTART;
int signals[] = {SIGTERM, SIGINT, SIGHUP, SIGUSR1, SIGUSR2};
for (size_t i = 0; i < sizeof(signals) / sizeof(signals[0]); ++i) {
if (sigaction(signals[i], &act, nullptr) == -1) {
err(1, "sigaction");
}
if (sigaddset(&mask, signals[i]) == -1) {
err(1, "sigaddset");
}
}
if (sigprocmask(SIG_BLOCK, &mask, &oldmask) == -1) {
err(1, "sigprocmask");
}
When waiting:
if (ppoll(&pfd, 1, nullptr, &oldmask) == -1) {
if (errno != EINTR) {
err(1, "ppoll");
}
if (!active) {
break;
}
if (rotate_now) {
puts("rotating capture file at user's request");
if (pcap_dump_closex(dumper) == -1) {
err(1, "pcap_dump_close");
}
dumper = nullptr;
rotate_now = false;
}
}
This has been solved with
ppoll/epoll_pwaitand friends.
$ man ppoll
No manual entry for ppoll
$ man epoll
No manual entry for epoll
Well on macOS and BSD you would use kqueue with EVFILT_SIGNAL https://man.openbsd.org/kqueue.2#EVFILT_SIGNAL. I'm sure windows has it's way with completion ports.
On Thu, Aug 26, 2021 at 11:59 PM Guy Harris @.***> wrote:
This has been solved with ppoll/epoll_pwait and friends.
$ man ppoll No manual entry for ppoll $ man epoll No manual entry for epoll
— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/the-tcpdump-group/tcpdump/issues/840#issuecomment-906771329, or unsubscribe https://github.com/notifications/unsubscribe-auth/AABLO236IT5UVCWJHW4BJALT622NVANCNFSM4K7WQZBA .